From dffcc939f7f0d9bde3915a5d19fa580a9670a3bd Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Fri, 29 Sep 2023 21:59:59 -0700 Subject: [PATCH] Change the build system to only notify delegate about changed files, not about new build settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This defines away an entire class of data races if delegate callbacks are delivered out-of-order. If we aren’t providing the new build settings in the delegate callback, then it doesn’t matter if two `fileBuildSettingsChanged` calls change order since they don’t carry any state. --- Sources/SKCore/BuildServerBuildSystem.swift | 6 +-- Sources/SKCore/BuildSystemDelegate.swift | 6 +-- Sources/SKCore/BuildSystemManager.swift | 46 +++++++++---------- .../CompilationDatabaseBuildSystem.swift | 11 ++--- Sources/SKCore/FallbackBuildSystem.swift | 3 +- .../SKSwiftPMWorkspace/SwiftPMWorkspace.swift | 17 +------ .../Clang/ClangLanguageServer.swift | 4 +- Sources/SourceKitLSP/SourceKitServer.swift | 12 ++--- .../Swift/SwiftLanguageServer.swift | 2 +- .../ToolchainLanguageServer.swift | 2 +- .../BuildServerBuildSystemTests.swift | 5 +- .../SKCoreTests/BuildSystemManagerTests.swift | 34 +++++--------- .../SourceKitLSPTests/BuildSystemTests.swift | 15 +++--- 13 files changed, 59 insertions(+), 104 deletions(-) diff --git a/Sources/SKCore/BuildServerBuildSystem.swift b/Sources/SKCore/BuildServerBuildSystem.swift index 4f7e3a66..68659401 100644 --- a/Sources/SKCore/BuildServerBuildSystem.swift +++ b/Sources/SKCore/BuildServerBuildSystem.swift @@ -202,11 +202,7 @@ public actor BuildServerBuildSystem: MessageHandler { /// about the changed build settings. private func buildSettingsChanged(for document: DocumentURI, settings: FileBuildSettings?) async { buildSettings[document] = settings - if let settings { - await self.delegate?.fileBuildSettingsChanged([document: .modified(settings)]) - } else { - await self.delegate?.fileBuildSettingsChanged([document: .removedOrUnavailable]) - } + await self.delegate?.fileBuildSettingsChanged([document]) } } diff --git a/Sources/SKCore/BuildSystemDelegate.swift b/Sources/SKCore/BuildSystemDelegate.swift index 948a861a..2e13029a 100644 --- a/Sources/SKCore/BuildSystemDelegate.swift +++ b/Sources/SKCore/BuildSystemDelegate.swift @@ -21,11 +21,7 @@ public protocol BuildSystemDelegate: AnyObject { func buildTargetsChanged(_ changes: [BuildTargetEvent]) async /// Notify the delegate that the given files' build settings have changed. - /// - /// The delegate should cache the new build settings for any of the given - /// files that they are interested in. - func fileBuildSettingsChanged( - _ changedFiles: [DocumentURI: FileBuildSettingsChange]) async + func fileBuildSettingsChanged(_ changedFiles: Set) async /// Notify the delegate that the dependencies of the given files have changed /// and that ASTs may need to be refreshed. If the given set is empty, assume diff --git a/Sources/SKCore/BuildSystemManager.swift b/Sources/SKCore/BuildSystemManager.swift index 596cdb4e..1bfe4cef 100644 --- a/Sources/SKCore/BuildSystemManager.swift +++ b/Sources/SKCore/BuildSystemManager.swift @@ -210,10 +210,8 @@ extension BuildSystemManager { let newStatus = await self.cachedStatusOrRegisterForSettings(for: mainFile, language: language) - if let mainChange = newStatus.buildSettingsChange, - let delegate = self._delegate { - let change = self.convert(change: mainChange, ofMainFile: mainFile, to: uri) - await delegate.fileBuildSettingsChanged([uri: change]) + if newStatus.buildSettingsChange != nil, let delegate = self._delegate { + await delegate.fileBuildSettingsChanged([uri]) } } @@ -294,7 +292,7 @@ extension BuildSystemManager { /// Update and notify our delegate for the given main file changes if they are /// convertible into `FileBuildSettingsChange`. func updateAndNotifyStatuses(changes: [DocumentURI: MainFileStatus]) async { - var changedWatchedFiles = [DocumentURI: FileBuildSettingsChange]() + var changedWatchedFiles = Set() for (mainFile, status) in changes { let watches = self.watchedFiles.filter { $1.mainFile == mainFile } guard !watches.isEmpty else { @@ -310,12 +308,11 @@ extension BuildSystemManager { guard prevStatus == .waiting || status.buildSettings != prevStatus?.buildSettings else { continue } - if let change = status.buildSettingsChange { - for watch in watches { - let newChange = - self.convert(change: change, ofMainFile: mainFile, to: watch.key) - changedWatchedFiles[watch.key] = newChange - } + guard status.buildSettingsChange != nil else { + continue + } + for watch in watches { + changedWatchedFiles.insert(watch.key) } } @@ -374,26 +371,26 @@ extension BuildSystemManager { extension BuildSystemManager: BuildSystemDelegate { // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has ben asyncified - public nonisolated func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) { + public nonisolated func fileBuildSettingsChanged(_ changes: Set) { Task { await fileBuildSettingsChangedImpl(changes) } } - public func fileBuildSettingsChangedImpl(_ changes: [DocumentURI: FileBuildSettingsChange]) async { - let statusChanges: [DocumentURI: MainFileStatus] = - changes.reduce(into: [:]) { (result, entry) in - let mainFile = entry.key - let settingsChange = entry.value + public func fileBuildSettingsChangedImpl(_ changes: Set) async { + var statusChanges: [DocumentURI: MainFileStatus] = [:] + for mainFile in changes { let watches = self.watchedFiles.filter { $1.mainFile == mainFile } guard let firstWatch = watches.first else { // Possible notification after the file was unregistered. Ignore. - return + continue } let newStatus: MainFileStatus - if let newSettings = settingsChange.newSettings { - newStatus = settingsChange.isFallback ? .fallback(newSettings) : .primary(newSettings) + let settings = await self.buildSettings(for: mainFile, language: firstWatch.value.language) + + if let settings { + newStatus = settings.isFallback ? .fallback(settings.buildSettings) : .primary(settings.buildSettings) } else if let fallback = self.fallbackBuildSystem { // FIXME: we need to stop threading the language everywhere, or we need the build system // itself to pass it in here. Or alternatively cache the fallback settings/language earlier? @@ -406,7 +403,7 @@ extension BuildSystemManager: BuildSystemDelegate { } else { newStatus = .unsupported } - result[mainFile] = newStatus + statusChanges[mainFile] = newStatus } await self.updateAndNotifyStatuses(changes: statusChanges) } @@ -474,7 +471,7 @@ extension BuildSystemManager: MainFilesDelegate { public func mainFilesChangedImpl() async { let origWatched = self.watchedFiles self.watchedFiles = [:] - var buildSettingsChanges = [DocumentURI: FileBuildSettingsChange]() + var buildSettingsChanges = Set() for (uri, state) in origWatched { let mainFiles = self._mainFilesProvider?.mainFilesContainingFile(uri) ?? [] @@ -489,9 +486,8 @@ extension BuildSystemManager: MainFilesDelegate { let newStatus = await self.cachedStatusOrRegisterForSettings( for: newMainFile, language: language) - if let change = newStatus.buildSettingsChange { - let newChange = self.convert(change: change, ofMainFile: newMainFile, to: uri) - buildSettingsChanges[uri] = newChange + if newStatus.buildSettingsChange != nil { + buildSettingsChanges.insert(uri) } } } diff --git a/Sources/SKCore/CompilationDatabaseBuildSystem.swift b/Sources/SKCore/CompilationDatabaseBuildSystem.swift index 3425d474..d64f6750 100644 --- a/Sources/SKCore/CompilationDatabaseBuildSystem.swift +++ b/Sources/SKCore/CompilationDatabaseBuildSystem.swift @@ -96,8 +96,7 @@ extension CompilationDatabaseBuildSystem: BuildSystem { guard let delegate = self.delegate else { return } - let settings = self.settings(for: uri) - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + await delegate.fileBuildSettingsChanged([uri]) } /// We don't support change watching. @@ -148,13 +147,9 @@ extension CompilationDatabaseBuildSystem: BuildSystem { self.compdb = tryLoadCompilationDatabase(directory: projectRoot, self.fileSystem) if let delegate = self.delegate { - var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:] + var changedFiles = Set() for (uri, _) in self.watchedFiles { - if let settings = self.settings(for: uri) { - changedFiles[uri] = FileBuildSettingsChange(settings) - } else { - changedFiles[uri] = .removedOrUnavailable - } + changedFiles.insert(uri) } await delegate.fileBuildSettingsChanged(changedFiles) } diff --git a/Sources/SKCore/FallbackBuildSystem.swift b/Sources/SKCore/FallbackBuildSystem.swift index c03bfb44..25b20f57 100644 --- a/Sources/SKCore/FallbackBuildSystem.swift +++ b/Sources/SKCore/FallbackBuildSystem.swift @@ -62,9 +62,8 @@ public final class FallbackBuildSystem: BuildSystem { public func registerForChangeNotifications(for uri: DocumentURI, language: Language) { guard let delegate = self.delegate else { return } - let settings = self.buildSettings(for: uri, language: language) Task { - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + await delegate.fileBuildSettingsChanged([uri]) } } diff --git a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift index df39d785..44e12940 100644 --- a/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift +++ b/Sources/SKSwiftPMWorkspace/SwiftPMWorkspace.swift @@ -242,16 +242,7 @@ extension SwiftPMWorkspace { }) guard let delegate = self.delegate else { return } - var changedFiles: [DocumentURI: FileBuildSettingsChange] = [:] - for (uri, language) in self.watchedFiles { - orLog { - if let settings = try self.buildSettings(for: uri, language: language) { - changedFiles[uri] = FileBuildSettingsChange(settings) - } else { - changedFiles[uri] = .removedOrUnavailable - } - } - } + let changedFiles = Set(self.watchedFiles.keys) await delegate.fileBuildSettingsChanged(changedFiles) await delegate.fileHandlingCapabilityChanged() } @@ -316,11 +307,7 @@ extension SwiftPMWorkspace: SKCore.BuildSystem { } catch { log("error computing settings: \(error)") } - if let settings = settings { - await delegate.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) - } else { - await delegate.fileBuildSettingsChanged([uri: .removedOrUnavailable]) - } + await delegate.fileBuildSettingsChanged([uri]) } /// Unregister the given file for build-system level change notifications, such as command diff --git a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift index e71cfa61..a4df45b1 100644 --- a/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift +++ b/Sources/SourceKitLSP/Clang/ClangLanguageServer.swift @@ -404,7 +404,7 @@ extension ClangLanguageServerShim { // Send clangd the build settings for the new file. We need to do this before // sending the open notification, so that the initial diagnostics already // have build settings. - await documentUpdatedBuildSettings(note.textDocument.uri, change: .removedOrUnavailable) + await documentUpdatedBuildSettings(note.textDocument.uri) clangd.send(note) } @@ -427,7 +427,7 @@ extension ClangLanguageServerShim { // MARK: - Build System Integration - public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async { + public func documentUpdatedBuildSettings(_ uri: DocumentURI) async { guard let url = uri.fileURL else { // FIXME: The clang workspace can probably be reworked to support non-file URIs. log("Received updated build settings for non-file URI '\(uri)'. Ignoring the update.") diff --git a/Sources/SourceKitLSP/SourceKitServer.swift b/Sources/SourceKitLSP/SourceKitServer.swift index 6f09a06d..bc620ecb 100644 --- a/Sources/SourceKitLSP/SourceKitServer.swift +++ b/Sources/SourceKitLSP/SourceKitServer.swift @@ -649,7 +649,7 @@ extension SourceKitServer: BuildSystemDelegate { // FIXME: (async) Make this method isolated once `BuildSystemDelegate` has been asyncified /// Non-async variant that executes `fileBuildSettingsChangedImpl` in a new task. - public nonisolated func fileBuildSettingsChanged(_ changedFiles: [DocumentURI: FileBuildSettingsChange]) { + public nonisolated func fileBuildSettingsChanged(_ changedFiles: Set) { Task { await self.fileBuildSettingsChangedImpl(changedFiles) } @@ -659,10 +659,8 @@ extension SourceKitServer: BuildSystemDelegate { /// This has two primary cases: /// - Initial settings reported for a given file, now we can fully open it /// - Changed settings for an already open file - public func fileBuildSettingsChangedImpl( - _ changedFiles: [DocumentURI: FileBuildSettingsChange] - ) async { - for (uri, change) in changedFiles { + public func fileBuildSettingsChangedImpl(_ changedFiles: Set) async { + for uri in changedFiles { // Non-ready documents should be considered open even though we haven't // opened it with the language service yet. guard self.documentManager.openDocuments.contains(uri) else { continue } @@ -687,7 +685,7 @@ extension SourceKitServer: BuildSystemDelegate { } // Notify the language server so it can apply the proper arguments. - await service.documentUpdatedBuildSettings(uri, change: change) + await service.documentUpdatedBuildSettings(uri) // Catch up on any queued notifications and requests. while !(documentToPendingQueue[uri]?.queue.isEmpty ?? true) { @@ -708,7 +706,7 @@ extension SourceKitServer: BuildSystemDelegate { // Case 2: changed settings for an already open file. log("Build settings changed for opened file \(uri)") if let service = workspace.documentService[uri] { - await service.documentUpdatedBuildSettings(uri, change: change) + await service.documentUpdatedBuildSettings(uri) } } } diff --git a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift index 4a0b44e6..6eaf99a9 100644 --- a/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift +++ b/Sources/SourceKitLSP/Swift/SwiftLanguageServer.swift @@ -485,7 +485,7 @@ extension SwiftLanguageServer { response: dict, for: snapshot, compileCommand: compileCmd) } - public func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async { + public func documentUpdatedBuildSettings(_ uri: DocumentURI) async { // We may not have a snapshot if this is called just before `openDocument`. guard let snapshot = self.documentManager.latestSnapshot(uri) else { return diff --git a/Sources/SourceKitLSP/ToolchainLanguageServer.swift b/Sources/SourceKitLSP/ToolchainLanguageServer.swift index 2cdce149..5964a857 100644 --- a/Sources/SourceKitLSP/ToolchainLanguageServer.swift +++ b/Sources/SourceKitLSP/ToolchainLanguageServer.swift @@ -70,7 +70,7 @@ public protocol ToolchainLanguageServer: AnyObject { /// Sent when the `BuildSystem` has resolved build settings, such as for the intial build settings /// or when the settings have changed (e.g. modified build system files). This may be sent before /// the respective `DocumentURI` has been opened. - func documentUpdatedBuildSettings(_ uri: DocumentURI, change: FileBuildSettingsChange) async + func documentUpdatedBuildSettings(_ uri: DocumentURI) async /// Sent when the `BuildSystem` has detected that dependencies of the given file have changed /// (e.g. header files, swiftmodule files, other compiler input files). diff --git a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift index 7e2bcacd..d86d381b 100644 --- a/Tests/SKCoreTests/BuildServerBuildSystemTests.swift +++ b/Tests/SKCoreTests/BuildServerBuildSystemTests.swift @@ -98,9 +98,8 @@ final class TestDelegate: BuildSystemDelegate { } } - func fileBuildSettingsChanged( - _ changedFiles: [DocumentURI: FileBuildSettingsChange]) { - for (uri, _) in changedFiles { + func fileBuildSettingsChanged(_ changedFiles: Set) { + for uri in changedFiles { settingsExpectations[uri]?.fulfill() } } diff --git a/Tests/SKCoreTests/BuildSystemManagerTests.swift b/Tests/SKCoreTests/BuildSystemManagerTests.swift index 04be5a5e..033b2632 100644 --- a/Tests/SKCoreTests/BuildSystemManagerTests.swift +++ b/Tests/SKCoreTests/BuildSystemManagerTests.swift @@ -110,7 +110,7 @@ final class BuildSystemManagerTests: XCTestCase { bs.map[a] = nil let changed = expectation(description: "changed settings") await del.setExpected([(a, .swift, nil, changed, #file, #line)]) - bsm.fileBuildSettingsChanged([a: .removedOrUnavailable]) + bsm.fileBuildSettingsChanged([a]) try await fulfillmentOfOrThrow([changed]) } @@ -133,7 +133,7 @@ final class BuildSystemManagerTests: XCTestCase { bs.map[a] = FileBuildSettings(compilerArguments: ["x"]) let changed = expectation(description: "changed settings") await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)]) - bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) + bsm.fileBuildSettingsChanged([a]) try await fulfillmentOfOrThrow([changed]) } @@ -158,13 +158,13 @@ final class BuildSystemManagerTests: XCTestCase { bs.map[a] = FileBuildSettings(compilerArguments: ["non-fallback", "args"]) let changed = expectation(description: "changed settings") await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)]) - bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) + bsm.fileBuildSettingsChanged([a]) try await fulfillmentOfOrThrow([changed]) bs.map[a] = nil let revert = expectation(description: "revert to fallback settings") await del.setExpected([(a, .swift, fallbackSettings, revert, #file, #line)]) - bsm.fileBuildSettingsChanged([a: .removedOrUnavailable]) + bsm.fileBuildSettingsChanged([a]) try await fulfillmentOfOrThrow([revert]) } @@ -196,7 +196,7 @@ final class BuildSystemManagerTests: XCTestCase { bs.map[b] = FileBuildSettings(compilerArguments: ["yy"]) let changed = expectation(description: "changed settings") await del.setExpected([(a, .swift, bs.map[a]!, changed, #file, #line)]) - bsm.fileBuildSettingsChanged([a: .modified(bs.map[a]!)]) + bsm.fileBuildSettingsChanged([a]) try await fulfillmentOfOrThrow([changed]) // Test multiple changes. @@ -208,10 +208,7 @@ final class BuildSystemManagerTests: XCTestCase { (a, .swift, bs.map[a]!, changedBothA, #file, #line), (b, .swift, bs.map[b]!, changedBothB, #file, #line), ]) - bsm.fileBuildSettingsChanged([ - a:. modified(bs.map[a]!), - b: .modified(bs.map[b]!) - ]) + bsm.fileBuildSettingsChanged([a, b]) try await fulfillmentOfOrThrow([changedBothA, changedBothB]) } @@ -245,9 +242,7 @@ final class BuildSystemManagerTests: XCTestCase { bs.map[b] = nil let changed = expectation(description: "changed settings") await del.setExpected([(b, .swift, nil, changed, #file, #line)]) - bsm.fileBuildSettingsChanged([ - b: .removedOrUnavailable - ]) + bsm.fileBuildSettingsChanged([b]) try await fulfillmentOfOrThrow([changed]) } @@ -354,7 +349,7 @@ final class BuildSystemManagerTests: XCTestCase { (h1, .c, newArgsH1, changed1, #file, #line), (h2, .c, newArgsH2, changed2, #file, #line), ]) - bsm.fileBuildSettingsChanged([cpp: .modified(bs.map[cpp]!)]) + bsm.fileBuildSettingsChanged([cpp]) try await fulfillmentOfOrThrow([changed1, changed2]) } @@ -403,11 +398,7 @@ final class BuildSystemManagerTests: XCTestCase { await bsm.unregisterForChangeNotifications(for: c) // At this point only b is registered, but that can race with notifications, // so ensure nothing bad happens and we still get the notification for b. - bsm.fileBuildSettingsChanged([ - a: .modified(bs.map[a]!), - b: .modified(bs.map[b]!), - c: .modified(bs.map[c]!) - ]) + bsm.fileBuildSettingsChanged([a, b, c]) try await fulfillmentOfOrThrow([changedB]) } @@ -484,8 +475,7 @@ class ManualBuildSystem: BuildSystem { } func registerForChangeNotifications(for uri: DocumentURI, language: Language) async { - let settings = self.buildSettings(for: uri, language: language) - await self.delegate?.fileBuildSettingsChanged([uri: FileBuildSettingsChange(settings)]) + await self.delegate?.fileBuildSettingsChanged([uri]) } func unregisterForChangeNotifications(for: DocumentURI) { @@ -536,8 +526,8 @@ private actor BSMDelegate: BuildSystemDelegate { }() } - func fileBuildSettingsChanged(_ changes: [DocumentURI: FileBuildSettingsChange]) async { - for (uri, _) in changes { + func fileBuildSettingsChanged(_ changedFiles: Set) async { + for uri in changedFiles { guard let expected = expected.first(where: { $0.uri == uri }) else { XCTFail("unexpected settings change for \(uri)") continue diff --git a/Tests/SourceKitLSPTests/BuildSystemTests.swift b/Tests/SourceKitLSPTests/BuildSystemTests.swift index 3d77efa6..8dc921cf 100644 --- a/Tests/SourceKitLSPTests/BuildSystemTests.swift +++ b/Tests/SourceKitLSPTests/BuildSystemTests.swift @@ -55,13 +55,12 @@ final class TestBuildSystem: BuildSystem { watchedFiles.insert(uri) // Inform our delegate of settings for the file if they're available. - guard let delegate = self.delegate, - let settings = self.buildSettingsByFile[uri] else { + guard let delegate = self.delegate else { return } Task { - await delegate.fileBuildSettingsChanged([uri: .modified(settings)]) + await delegate.fileBuildSettingsChanged([uri]) } } @@ -196,7 +195,7 @@ final class BuildSystemTests: XCTestCase { expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) try await fulfillmentOfOrThrow([expectation]) } @@ -251,7 +250,7 @@ final class BuildSystemTests: XCTestCase { XCTAssertEqual(note.params.diagnostics.count, 0) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) try await fulfillmentOfOrThrow([expectation]) } @@ -304,7 +303,7 @@ final class BuildSystemTests: XCTestCase { expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(newSettings)]) + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) try await fulfillmentOfOrThrow([expectation]) } @@ -358,7 +357,7 @@ final class BuildSystemTests: XCTestCase { XCTAssertEqual(note.params.diagnostics.count, 2) expectation.fulfill() } - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .modified(primarySettings)]) + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) try await fulfillmentOfOrThrow([expectation]) } @@ -388,7 +387,7 @@ final class BuildSystemTests: XCTestCase { // Modify the build settings and inform the SourceKitServer. // This shouldn't trigger new diagnostics since nothing actually changed (false alarm). - await buildSystem.delegate?.fileBuildSettingsChanged([doc: .removedOrUnavailable]) + await buildSystem.delegate?.fileBuildSettingsChanged([doc]) let expectation = XCTestExpectation(description: "refresh doesn't occur") expectation.isInverted = true