mirror of
https://github.com/apple/sourcekit-lsp.git
synced 2026-03-02 18:23:24 +01:00
This avoids having a gotcha in the API where if a build system calls these methods while it is being called back for settings or to register for notifications it deadlocks (or crashes in libdispatch to prevent the deadlock).
470 lines
18 KiB
Swift
470 lines
18 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 2014 - 2019 Apple Inc. and the Swift project authors
|
|
// Licensed under Apache License v2.0 with Runtime Library Exception
|
|
//
|
|
// See https://swift.org/LICENSE.txt for license information
|
|
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import LanguageServerProtocol
|
|
import BuildServerProtocol
|
|
import LSPTestSupport
|
|
import SKCore
|
|
import TSCBasic
|
|
import XCTest
|
|
|
|
final class BuildSystemManagerTests: XCTestCase {
|
|
|
|
func testMainFiles() {
|
|
let a = DocumentURI(string: "bsm:a")
|
|
let b = DocumentURI(string: "bsm:b")
|
|
let c = DocumentURI(string: "bsm:c")
|
|
let d = DocumentURI(string: "bsm:d")
|
|
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [
|
|
a: Set([c]),
|
|
b: Set([c, d]),
|
|
c: Set([c]),
|
|
d: Set([d]),
|
|
]
|
|
|
|
let bsm = BuildSystemManager(
|
|
buildSystem: FallbackBuildSystem(),
|
|
mainFilesProvider: mainFiles)
|
|
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: b), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), nil)
|
|
|
|
bsm.registerForChangeNotifications(for: a, language: .c)
|
|
bsm.registerForChangeNotifications(for: b, language: .c)
|
|
bsm.registerForChangeNotifications(for: c, language: .c)
|
|
bsm.registerForChangeNotifications(for: d, language: .c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), c)
|
|
let bMain = bsm._cachedMainFile(for: b)
|
|
XCTAssert(Set([c, d]).contains(bMain))
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), d)
|
|
|
|
mainFiles.mainFiles = [
|
|
a: Set([a]),
|
|
b: Set([c, d, a]),
|
|
c: Set([c]),
|
|
d: Set([d]),
|
|
]
|
|
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: b), bMain)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), d)
|
|
|
|
bsm.mainFilesChanged()
|
|
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), a)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), d)
|
|
|
|
bsm.unregisterForChangeNotifications(for: a)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: b), bMain) // never changes to a
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), c)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), d)
|
|
|
|
bsm.unregisterForChangeNotifications(for: b)
|
|
bsm.mainFilesChanged()
|
|
bsm.unregisterForChangeNotifications(for: c)
|
|
bsm.unregisterForChangeNotifications(for: d)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: a), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: b), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: c), nil)
|
|
XCTAssertEqual(bsm._cachedMainFile(for: d), nil)
|
|
}
|
|
|
|
func testSettingsMainFile() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a])]
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["x"], language: .swift)
|
|
let initial = expectation(description: "initial settings")
|
|
del.expected = [(a, bs.map[a]!, initial, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
wait(for: [initial], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[a] = nil
|
|
let changed = expectation(description: "changed settings")
|
|
del.expected = [(a, nil, changed, #file, #line)]
|
|
bsm.fileBuildSettingsChanged(Set([a]))
|
|
wait(for: [changed], timeout: 10, enforceOrder: true)
|
|
}
|
|
|
|
func testSettingsMainFileInitialNil() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a])]
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
let initial = expectation(description: "initial settings")
|
|
del.expected = [(a, nil, initial, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
wait(for: [initial], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["x"], language: .swift)
|
|
let changed = expectation(description: "changed settings")
|
|
del.expected = [(a, bs.map[a]!, changed, #file, #line)]
|
|
bsm.fileBuildSettingsChanged(Set([a]))
|
|
wait(for: [changed], timeout: 10, enforceOrder: true)
|
|
}
|
|
|
|
func testSettingsMainFileInitialIntersect() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let b = DocumentURI(string: "bsm:b.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a]), b: Set([b])]
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["x"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["y"], language: .swift)
|
|
let initial = expectation(description: "initial settings")
|
|
del.expected = [(a, bs.map[a]!, initial, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
wait(for: [initial], timeout: 10, enforceOrder: true)
|
|
let initialB = expectation(description: "initial settings")
|
|
del.expected = [(b, bs.map[b]!, initialB, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: b, language: .swift)
|
|
wait(for: [initialB], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["xx"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["yy"], language: .swift)
|
|
let changed = expectation(description: "changed settings")
|
|
del.expected = [(a, bs.map[a]!, changed, #file, #line)]
|
|
bsm.fileBuildSettingsChanged(Set([a]))
|
|
wait(for: [changed], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["xxx"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["yyy"], language: .swift)
|
|
let changedBothA = expectation(description: "changed setting a")
|
|
let changedBothB = expectation(description: "changed setting b")
|
|
del.expected = [
|
|
(a, bs.map[a]!, changedBothA, #file, #line),
|
|
(b, bs.map[b]!, changedBothB, #file, #line),
|
|
]
|
|
bsm.fileBuildSettingsChanged(Set([])) // empty => all
|
|
wait(for: [changedBothA, changedBothB], timeout: 10, enforceOrder: false)
|
|
}
|
|
|
|
func testSettingsMainFileUnchanged() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let b = DocumentURI(string: "bsm:b.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a]), b: Set([b])]
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["a"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["b"], language: .swift)
|
|
|
|
let initialA = expectation(description: "initial settings a")
|
|
del.expected = [(a, bs.map[a]!, initialA, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
wait(for: [initialA], timeout: 10, enforceOrder: true)
|
|
|
|
let initialB = expectation(description: "initial settings b")
|
|
del.expected = [(b, bs.map[b]!, initialB, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: b, language: .swift)
|
|
wait(for: [initialB], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[a] = nil
|
|
bs.map[b] = nil
|
|
let changed = expectation(description: "changed settings")
|
|
del.expected = [(b, nil, changed, #file, #line)]
|
|
bsm.fileBuildSettingsChanged(Set([b]))
|
|
wait(for: [changed], timeout: 10, enforceOrder: true)
|
|
}
|
|
|
|
func testSettingsHeaderChangeMainFile() {
|
|
let h = DocumentURI(string: "bsm:header.h")
|
|
let cpp1 = DocumentURI(string: "bsm:main.cpp")
|
|
let cpp2 = DocumentURI(string: "bsm:other.cpp")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [
|
|
h: Set([cpp1]),
|
|
cpp1: Set([cpp1]),
|
|
cpp2: Set([cpp2]),
|
|
]
|
|
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[cpp1] = FileBuildSettings(compilerArguments: ["C++ 1"], language: .cpp)
|
|
bs.map[cpp2] = FileBuildSettings(compilerArguments: ["C++ 2"], language: .cpp)
|
|
|
|
let initial = expectation(description: "initial settings via cpp1")
|
|
del.expected = [(h, bs.map[cpp1]!, initial, #file, #line)]
|
|
bsm.registerForChangeNotifications(for: h, language: .c)
|
|
wait(for: [initial], timeout: 10, enforceOrder: true)
|
|
|
|
mainFiles.mainFiles[h] = Set([cpp2])
|
|
|
|
let changed = expectation(description: "changed settings to cpp2")
|
|
del.expected = [(h, bs.map[cpp2]!, changed, #file, #line)]
|
|
bsm.mainFilesChanged()
|
|
wait(for: [changed], timeout: 10, enforceOrder: true)
|
|
|
|
let changed2 = expectation(description: "still cpp2, no update")
|
|
changed2.isInverted = true
|
|
del.expected = [(h, nil, changed2, #file, #line)]
|
|
bsm.mainFilesChanged()
|
|
wait(for: [changed2], timeout: 1, enforceOrder: true)
|
|
|
|
mainFiles.mainFiles[h] = Set([cpp1, cpp2])
|
|
|
|
let changed3 = expectation(description: "added main file, no update")
|
|
changed3.isInverted = true
|
|
del.expected = [(h, nil, changed3, #file, #line)]
|
|
bsm.mainFilesChanged()
|
|
wait(for: [changed3], timeout: 1, enforceOrder: true)
|
|
|
|
mainFiles.mainFiles[h] = Set([])
|
|
|
|
let changed4 = expectation(description: "changed settings to []")
|
|
del.expected = [(h, nil, changed4, #file, #line)]
|
|
bsm.mainFilesChanged()
|
|
wait(for: [changed4], timeout: 10, enforceOrder: true)
|
|
}
|
|
|
|
func testSettingsOneMainTwoHeader() {
|
|
let h1 = DocumentURI(string: "bsm:header1.h")
|
|
let h2 = DocumentURI(string: "bsm:header2.h")
|
|
let cpp = DocumentURI(string: "bsm:main.cpp")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [
|
|
h1: Set([cpp]),
|
|
h2: Set([cpp]),
|
|
]
|
|
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[cpp] = FileBuildSettings(compilerArguments: ["C++ Main File"], language: .cpp)
|
|
|
|
let initial1 = expectation(description: "initial settings h1 via cpp")
|
|
let initial2 = expectation(description: "initial settings h2 via cpp")
|
|
del.expected = [
|
|
(h1, bs.map[cpp]!, initial1, #file, #line),
|
|
(h2, bs.map[cpp]!, initial2, #file, #line),
|
|
]
|
|
|
|
bsm.registerForChangeNotifications(for: h1, language: .c)
|
|
bsm.registerForChangeNotifications(for: h2, language: .c)
|
|
|
|
wait(for: [initial1, initial2], timeout: 10, enforceOrder: true)
|
|
|
|
bs.map[cpp] = FileBuildSettings(compilerArguments: ["New C++ Main File"], language: .cpp)
|
|
let changed1 = expectation(description: "initial settings h1 via cpp")
|
|
let changed2 = expectation(description: "initial settings h2 via cpp")
|
|
del.expected = [
|
|
(h1, bs.map[cpp]!, changed1, #file, #line),
|
|
(h2, bs.map[cpp]!, changed2, #file, #line),
|
|
]
|
|
bsm.fileBuildSettingsChanged(Set([cpp]))
|
|
|
|
wait(for: [changed1, changed2], timeout: 10, enforceOrder: false)
|
|
|
|
bs.map[cpp] = FileBuildSettings(compilerArguments: ["Third C++ Main File"], language: .cpp)
|
|
let changed3 = expectation(description: "third settings h1 via cpp")
|
|
let changed4 = expectation(description: "third settings h2 via cpp")
|
|
del.expected = [
|
|
(h1, bs.map[cpp]!, changed3, #file, #line),
|
|
(h2, bs.map[cpp]!, changed4, #file, #line),
|
|
]
|
|
bsm.fileBuildSettingsChanged(Set([])) // Empty => all
|
|
wait(for: [changed3, changed4], timeout: 10, enforceOrder: false)
|
|
}
|
|
|
|
func testSettingsChangedAfterUnregister() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let b = DocumentURI(string: "bsm:b.swift")
|
|
let c = DocumentURI(string: "bsm:c.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a]), b: Set([b]), c: Set([c])]
|
|
let bs = ManualBuildSystem()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["a"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["b"], language: .swift)
|
|
bs.map[c] = FileBuildSettings(compilerArguments: ["c"], language: .swift)
|
|
|
|
let initialA = expectation(description: "initial settings a")
|
|
let initialB = expectation(description: "initial settings b")
|
|
let initialC = expectation(description: "initial settings c")
|
|
del.expected = [
|
|
(a, bs.map[a]!, initialA, #file, #line),
|
|
(b, bs.map[b]!, initialB, #file, #line),
|
|
(c, bs.map[c]!, initialC, #file, #line),
|
|
]
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
bsm.registerForChangeNotifications(for: b, language: .swift)
|
|
bsm.registerForChangeNotifications(for: c, language: .swift)
|
|
wait(for: [initialA, initialB, initialC], timeout: 10, enforceOrder: false)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["new-a"], language: .swift)
|
|
bs.map[b] = FileBuildSettings(compilerArguments: ["new-b"], language: .swift)
|
|
bs.map[c] = FileBuildSettings(compilerArguments: ["new-c"], language: .swift)
|
|
|
|
let changedB = expectation(description: "changed settings b")
|
|
del.expected = [
|
|
(b, bs.map[b]!, changedB, #file, #line),
|
|
]
|
|
|
|
bsm.unregisterForChangeNotifications(for: a)
|
|
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, b, c])
|
|
|
|
wait(for: [changedB], timeout: 10, enforceOrder: false)
|
|
}
|
|
|
|
func testDependenciesUpdated() {
|
|
let a = DocumentURI(string: "bsm:a.swift")
|
|
let mainFiles = ManualMainFilesProvider()
|
|
mainFiles.mainFiles = [a: Set([a])]
|
|
|
|
class DepUpdateDuringRegistrationBS: ManualBuildSystem {
|
|
override func registerForChangeNotifications(for uri: DocumentURI, language: Language) {
|
|
delegate?.filesDependenciesUpdated([uri])
|
|
}
|
|
}
|
|
|
|
let bs = DepUpdateDuringRegistrationBS()
|
|
let bsm = BuildSystemManager(buildSystem: bs, mainFilesProvider: mainFiles)
|
|
let del = BSMDelegate(bsm)
|
|
|
|
bs.map[a] = FileBuildSettings(compilerArguments: ["x"], language: .swift)
|
|
let initial = expectation(description: "initial settings")
|
|
del.expected = [(a, bs.map[a]!, initial, #file, #line)]
|
|
|
|
let depUpdate1 = expectation(description: "dependencies update during registration")
|
|
del.expectedDependenciesUpdate = [(a, depUpdate1, #file, #line)]
|
|
|
|
bsm.registerForChangeNotifications(for: a, language: .swift)
|
|
wait(for: [initial, depUpdate1], timeout: 10, enforceOrder: false)
|
|
|
|
let depUpdate2 = expectation(description: "dependencies update 2")
|
|
del.expectedDependenciesUpdate = [(a, depUpdate2, #file, #line)]
|
|
|
|
bsm.filesDependenciesUpdated([a])
|
|
wait(for: [depUpdate2], timeout: 10, enforceOrder: false)
|
|
}
|
|
}
|
|
|
|
// MARK: Helper Classes for Testing
|
|
|
|
/// A simple `MainFilesProvider` that wraps a dictionary, for testing.
|
|
private final class ManualMainFilesProvider: MainFilesProvider {
|
|
var mainFiles: [DocumentURI: Set<DocumentURI>] = [:]
|
|
|
|
func mainFilesContainingFile(_ file: DocumentURI) -> Set<DocumentURI> {
|
|
if let result = mainFiles[file] {
|
|
return result
|
|
}
|
|
return Set()
|
|
}
|
|
}
|
|
|
|
/// A simple `BuildSystem` that wraps a dictionary, for testing.
|
|
class ManualBuildSystem: BuildSystem {
|
|
var map: [DocumentURI: FileBuildSettings] = [:]
|
|
|
|
var delegate: BuildSystemDelegate? = nil
|
|
|
|
func settings(for uri: DocumentURI, _ language: Language) -> FileBuildSettings? {
|
|
return map[uri]
|
|
}
|
|
|
|
func registerForChangeNotifications(for: DocumentURI, language: Language) {
|
|
}
|
|
|
|
func unregisterForChangeNotifications(for: DocumentURI) {
|
|
}
|
|
|
|
var indexStorePath: AbsolutePath? { nil }
|
|
var indexDatabasePath: AbsolutePath? { nil }
|
|
|
|
func buildTargets(reply: @escaping (LSPResult<[BuildTarget]>) -> Void) {
|
|
fatalError()
|
|
}
|
|
|
|
func buildTargetSources(targets: [BuildTargetIdentifier],
|
|
reply: @escaping (LSPResult<[SourcesItem]>) -> Void) {
|
|
fatalError()
|
|
}
|
|
|
|
func buildTargetOutputPaths(targets: [BuildTargetIdentifier],
|
|
reply: @escaping (LSPResult<[OutputsItem]>) -> Void) {
|
|
fatalError()
|
|
}
|
|
}
|
|
|
|
/// A `BuildSystemDelegate` setup for testing.
|
|
private final class BSMDelegate: BuildSystemDelegate {
|
|
let queue: DispatchQueue = DispatchQueue(label: "\(BSMDelegate.self)")
|
|
unowned let bsm: BuildSystemManager
|
|
var expected: [(uri: DocumentURI, settings: FileBuildSettings?, expectation: XCTestExpectation, file: StaticString, line: UInt)] = []
|
|
var expectedDependenciesUpdate: [(uri: DocumentURI, expectation: XCTestExpectation, file: StaticString, line: UInt)] = []
|
|
|
|
init(_ bsm: BuildSystemManager) {
|
|
self.bsm = bsm
|
|
bsm.delegate = self
|
|
}
|
|
|
|
func fileBuildSettingsChanged(_ changedFiles: Set<DocumentURI>) {
|
|
queue.sync {
|
|
for uri in changedFiles {
|
|
guard let expected = expected.first(where: { $0.uri == uri }) else {
|
|
XCTFail("unexpected settings change for \(uri)")
|
|
continue
|
|
}
|
|
|
|
XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line)
|
|
let settings = bsm.settings(for: uri, .swift)
|
|
XCTAssertEqual(settings, expected.settings, file: expected.file, line: expected.line)
|
|
expected.expectation.fulfill()
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildTargetsChanged(_ changes: [BuildTargetEvent]) {}
|
|
func filesDependenciesUpdated(_ changedFiles: Set<DocumentURI>) {
|
|
queue.sync {
|
|
for uri in changedFiles {
|
|
guard let expected = expectedDependenciesUpdate.first(where: { $0.uri == uri }) else {
|
|
XCTFail("unexpected filesDependenciesUpdated for \(uri)")
|
|
continue
|
|
}
|
|
|
|
XCTAssertEqual(uri, expected.uri, file: expected.file, line: expected.line)
|
|
expected.expectation.fulfill()
|
|
}
|
|
}
|
|
}
|
|
}
|