Skip to content

Commit ae215f9

Browse files
committed
Fix a bug that would cause a file to never be part of the syntactic index again after it got deleted once
1 parent b3c519b commit ae215f9

File tree

2 files changed

+72
-5
lines changed

2 files changed

+72
-5
lines changed

Sources/SourceKitLSP/Swift/SyntacticTestIndex.swift

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ fileprivate enum TaskMetadata: DependencyTracker, Equatable {
3939
// Adding the dependency also elevates the index task's priorities.
4040
return true
4141
case (.index(let lhsUris), .index(let rhsUris)):
42-
// Technically, we should be able to allow simultaneous indexing of the same file. When a file gets re-scheduled
43-
// for indexing, all previous index invocations should get cancelled. But conceptually the code becomes simpler
44-
// if we don't need to think racing indexing tasks for the same file and it shouldn't make a performance impact
45-
// in practice because of the cancellation described before.
42+
// Technically, we should be able to allow simultaneous indexing of the same file. But conceptually the code
43+
// becomes simpler if we don't need to think racing indexing tasks for the same file and it shouldn't make a
44+
// performance impact in practice because if a first task indexes a file, a subsequent index task for the same
45+
// file will realize that the index is already up-to-date based on the file's mtime and early exit.
4646
return !lhsUris.intersection(rhsUris).isEmpty
4747
}
4848
}
@@ -82,14 +82,18 @@ fileprivate func testItems(in url: URL) async -> [TestItem] {
8282
return await swiftTestingTests + xcTests
8383
}
8484

85+
/// An in-memory syntactic index of test items within a workspace.
86+
///
87+
/// The index does not get persisted to disk but instead gets rebuilt every time a workspace is opened (ie. usually when
88+
/// sourcekit-lsp is launched). Building it takes only a few seconds, even for large projects.
8589
actor SyntacticTestIndex {
8690
/// The tests discovered by the index.
8791
private var indexedTests: [DocumentURI: IndexedTests] = [:]
8892

8993
/// Files that have been removed using `removeFileForIndex`.
9094
///
9195
/// We need to keep track of these files because when the files get removed, there might be an in-progress indexing
92-
/// operation running for that file. We need to ensure that this indexing operation doesn't write add the removed file
96+
/// operation running for that file. We need to ensure that this indexing operation doesn't add the removed file
9397
/// back to `indexTests`.
9498
private var removedFiles: Set<DocumentURI> = []
9599

@@ -138,6 +142,10 @@ actor SyntacticTestIndex {
138142

139143
/// Called when a list of files was updated. Re-scans those files
140144
private func rescanFiles(_ uris: [DocumentURI]) {
145+
// If we scan a file again, it might have been added after being removed before. Remove it from the list of removed
146+
// files.
147+
removedFiles.subtract(uris)
148+
141149
// Divide the files into multiple batches. This is more efficient than spawning a new task for every file, mostly
142150
// because it keeps the number of pending items in `indexingQueue` low and adding a new task to `indexingQueue` is
143151
// in O(number of pending tasks), since we need to scan for dependency edges to add, which would make scanning files

Tests/SourceKitLSPTests/WorkspaceTestDiscoveryTests.swift

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,63 @@ final class WorkspaceTestDiscoveryTests: XCTestCase {
748748
]
749749
)
750750
}
751+
752+
func testDeleteFileAndAddItAgain() async throws {
753+
let markedFileContents = """
754+
import XCTest
755+
756+
1️⃣class MyTests: XCTestCase {
757+
2️⃣func testMyLibrary() {}3️⃣
758+
func unrelatedFunc() {}
759+
var testVariable: Int = 0
760+
}4️⃣
761+
"""
762+
763+
let project = try await SwiftPMTestProject(
764+
files: [
765+
"Tests/MyLibraryTests/MyTests.swift": markedFileContents
766+
],
767+
manifest: packageManifestWithTestTarget
768+
)
769+
770+
let expectedTests = [
771+
TestItem(
772+
id: "MyTests",
773+
label: "MyTests",
774+
disabled: false,
775+
style: TestStyle.xcTest,
776+
location: try project.location(from: "1️⃣", to: "4️⃣", in: "MyTests.swift"),
777+
children: [
778+
TestItem(
779+
id: "MyTests/testMyLibrary()",
780+
label: "testMyLibrary()",
781+
disabled: false,
782+
style: TestStyle.xcTest,
783+
location: try project.location(from: "2️⃣", to: "3️⃣", in: "MyTests.swift"),
784+
children: [],
785+
tags: []
786+
)
787+
],
788+
tags: []
789+
)
790+
]
791+
792+
let testsBeforeFileRemove = try await project.testClient.send(WorkspaceTestsRequest())
793+
XCTAssertEqual(testsBeforeFileRemove, expectedTests)
794+
795+
let myTestsUri = try project.uri(for: "MyTests.swift")
796+
let myTestsUrl = try XCTUnwrap(myTestsUri.fileURL)
797+
798+
try FileManager.default.removeItem(at: myTestsUrl)
799+
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: myTestsUri, type: .deleted)]))
800+
801+
let testsAfterFileRemove = try await project.testClient.send(WorkspaceTestsRequest())
802+
XCTAssertEqual(testsAfterFileRemove, [])
803+
804+
try extractMarkers(markedFileContents).textWithoutMarkers.write(to: myTestsUrl, atomically: true, encoding: .utf8)
805+
project.testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: myTestsUri, type: .created)]))
806+
807+
let testsAfterFileReAdded = try await project.testClient.send(WorkspaceTestsRequest())
808+
XCTAssertEqual(testsAfterFileReAdded, expectedTests)
809+
}
751810
}

0 commit comments

Comments
 (0)