Skip to content

Commit 6aba969

Browse files
authored
Merge pull request #1501 from ahoppen/watch-package-resolved
Watch for changes to `Package.resolved`
2 parents b8d4c62 + 4d90f40 commit 6aba969

File tree

7 files changed

+179
-20
lines changed

7 files changed

+179
-20
lines changed

Sources/SKSwiftPMWorkspace/SwiftPMBuildSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ extension SwiftPMBuildSystem: SKCore.BuildSystem {
691691
packageGraph: self.modulesGraph
692692
)
693693
case .changed:
694-
return fileURL.lastPathComponent == "Package.swift"
694+
return fileURL.lastPathComponent == "Package.swift" || fileURL.lastPathComponent == "Package.resolved"
695695
default: // Unknown file change type
696696
return false
697697
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import LSPTestSupport
14+
import XCTest
15+
16+
/// Runs the body repeatedly once per second until it returns `true`, giving up after `timeout`.
17+
///
18+
/// This is useful to test some request that requires global state to be updated but will eventually converge on the
19+
/// correct result.
20+
///
21+
/// If `bodyHasOneSecondDelay` is true, it is assume that the body already has a one-second delay between iterations.
22+
public func repeatUntilExpectedResult(
23+
_ body: () async throws -> Bool,
24+
bodyHasOneSecondDelay: Bool = false,
25+
timeout: TimeInterval = defaultTimeout,
26+
file: StaticString = #filePath,
27+
line: UInt = #line
28+
) async throws {
29+
for _ in 0..<Int(timeout) {
30+
if try await body() {
31+
return
32+
}
33+
if !bodyHasOneSecondDelay {
34+
try await Task.sleep(for: .seconds(1))
35+
}
36+
}
37+
XCTFail("Failed to get expected result", file: file, line: line)
38+
}

Sources/SKTestSupport/SwiftPMDependencyProject.swift

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,15 +86,20 @@ public class SwiftPMDependencyProject {
8686
}
8787

8888
try await runGitCommand(["init"], workingDirectory: packageDirectory)
89+
try await tag(changedFiles: files.keys.map { $0.url(relativeTo: packageDirectory) }, version: "1.0.0")
90+
}
91+
92+
public func tag(changedFiles: [URL], version: String) async throws {
8993
try await runGitCommand(
90-
["add"] + files.keys.map { $0.url(relativeTo: packageDirectory).path },
94+
["add"] + changedFiles.map(\.path),
9195
workingDirectory: packageDirectory
9296
)
9397
try await runGitCommand(
94-
["-c", "user.name=Dummy", "-c", "[email protected]", "commit", "-m", "Initial commit"],
98+
["-c", "user.name=Dummy", "-c", "[email protected]", "commit", "-m", "Version \(version)"],
9599
workingDirectory: packageDirectory
96100
)
97-
try await runGitCommand(["tag", "1.0.0"], workingDirectory: packageDirectory)
101+
102+
try await runGitCommand(["tag", version], workingDirectory: self.packageDirectory)
98103
}
99104

100105
deinit {

Sources/SourceKitLSP/SourceKitLSPServer.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,7 @@ extension SourceKitLSPServer {
11951195
return FileSystemWatcher(globPattern: "**/*.\(fileExtension)", kind: [.create, .change, .delete])
11961196
}
11971197
watchers.append(FileSystemWatcher(globPattern: "**/Package.swift", kind: [.change]))
1198+
watchers.append(FileSystemWatcher(globPattern: "**/Package.resolved", kind: [.change]))
11981199
watchers.append(FileSystemWatcher(globPattern: "**/compile_commands.json", kind: [.create, .change, .delete]))
11991200
watchers.append(FileSystemWatcher(globPattern: "**/compile_flags.txt", kind: [.create, .change, .delete]))
12001201
// Watch for changes to `.swiftmodule` files to detect updated modules during a build.

Tests/SourceKitLSPTests/BackgroundIndexingTests.swift

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212

1313
import LSPTestSupport
1414
import LanguageServerProtocol
15-
import SKCore
15+
@_spi(Testing) import SKCore
1616
import SKTestSupport
1717
import SemanticIndex
1818
import SourceKitLSP
1919
import XCTest
2020

21+
import class TSCBasic.Process
22+
2123
final class BackgroundIndexingTests: XCTestCase {
2224
func testBackgroundIndexingOfSingleFile() async throws {
2325
let project = try await SwiftPMTestProject(
@@ -1107,4 +1109,126 @@ final class BackgroundIndexingTests: XCTestCase {
11071109
)
11081110
XCTAssertEqual(response, .locations([try project.location(from: "1️⃣", to: "1️⃣", in: "LibB.swift")]))
11091111
}
1112+
1113+
func testUpdatePackageDependency() async throws {
1114+
try SkipUnless.longTestsEnabled()
1115+
1116+
let dependencyProject = try await SwiftPMDependencyProject(files: [
1117+
"Sources/MyDependency/Dependency.swift": """
1118+
/// Do something v1.0.0
1119+
public func doSomething() {}
1120+
"""
1121+
])
1122+
let dependencySwiftURL = dependencyProject.packageDirectory
1123+
.appendingPathComponent("Sources")
1124+
.appendingPathComponent("MyDependency")
1125+
.appendingPathComponent("Dependency.swift")
1126+
defer { dependencyProject.keepAlive() }
1127+
1128+
let project = try await SwiftPMTestProject(
1129+
files: [
1130+
"Test.swift": """
1131+
import MyDependency
1132+
1133+
func test() {
1134+
1️⃣doSomething()
1135+
}
1136+
"""
1137+
],
1138+
manifest: """
1139+
let package = Package(
1140+
name: "MyLibrary",
1141+
dependencies: [.package(url: "\(dependencyProject.packageDirectory)", from: "1.0.0")],
1142+
targets: [
1143+
.target(
1144+
name: "MyLibrary",
1145+
dependencies: [.product(name: "MyDependency", package: "MyDependency")]
1146+
)
1147+
]
1148+
)
1149+
""",
1150+
enableBackgroundIndexing: true
1151+
)
1152+
let packageResolvedURL = project.scratchDirectory.appendingPathComponent("Package.resolved")
1153+
1154+
let originalPackageResolvedContents = try String(contentsOf: packageResolvedURL)
1155+
1156+
// First check our setup to see that we get the expected hover response before changing the dependency project.
1157+
let (uri, positions) = try project.openDocument("Test.swift")
1158+
let hoverBeforeUpdate = try await project.testClient.send(
1159+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1160+
)
1161+
XCTAssert(
1162+
hoverBeforeUpdate?.contents.markupContent?.value.contains("Do something v1.0.0") ?? false,
1163+
"Did not contain expected string: \(String(describing: hoverBeforeUpdate))"
1164+
)
1165+
1166+
// Just committing a new version of the dependency shouldn't change anything because we didn't update the package
1167+
// dependencies.
1168+
try """
1169+
/// Do something v1.1.0
1170+
public func doSomething() {}
1171+
""".write(to: dependencySwiftURL, atomically: true, encoding: .utf8)
1172+
try await dependencyProject.tag(changedFiles: [dependencySwiftURL], version: "1.1.0")
1173+
1174+
let hoverAfterNewVersionCommit = try await project.testClient.send(
1175+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1176+
)
1177+
XCTAssert(
1178+
hoverAfterNewVersionCommit?.contents.markupContent?.value.contains("Do something v1.0.0") ?? false,
1179+
"Did not contain expected string: \(String(describing: hoverBeforeUpdate))"
1180+
)
1181+
1182+
// Updating Package.swift causes a package reload but should not cause dependencies to be updated.
1183+
project.testClient.send(
1184+
DidChangeWatchedFilesNotification(changes: [
1185+
FileEvent(uri: DocumentURI(project.scratchDirectory.appendingPathComponent("Package.resolved")), type: .changed)
1186+
])
1187+
)
1188+
_ = try await project.testClient.send(PollIndexRequest())
1189+
XCTAssertEqual(try String(contentsOf: packageResolvedURL), originalPackageResolvedContents)
1190+
1191+
// Simulate a package update which goes as follows:
1192+
// - The user runs `swift package update`
1193+
// - This updates `Package.resolved`, which we watch
1194+
// - We reload the package, which updates `Dependency.swift` in `.index-build/checkouts`, which we also watch.
1195+
try await Process.run(
1196+
arguments: [
1197+
unwrap(ToolchainRegistry.forTesting.default?.swift?.pathString),
1198+
"package", "update",
1199+
"--package-path", project.scratchDirectory.path,
1200+
],
1201+
workingDirectory: nil
1202+
)
1203+
XCTAssertNotEqual(try String(contentsOf: packageResolvedURL), originalPackageResolvedContents)
1204+
project.testClient.send(
1205+
DidChangeWatchedFilesNotification(changes: [
1206+
FileEvent(uri: DocumentURI(project.scratchDirectory.appendingPathComponent("Package.resolved")), type: .changed)
1207+
])
1208+
)
1209+
_ = try await project.testClient.send(PollIndexRequest())
1210+
project.testClient.send(
1211+
DidChangeWatchedFilesNotification(
1212+
changes: FileManager.default.findFiles(named: "Dependency.swift", in: project.scratchDirectory).map {
1213+
FileEvent(uri: DocumentURI($0), type: .changed)
1214+
}
1215+
)
1216+
)
1217+
1218+
try await repeatUntilExpectedResult {
1219+
let hoverAfterPackageUpdate = try await project.testClient.send(
1220+
HoverRequest(textDocument: TextDocumentIdentifier(uri), position: positions["1️⃣"])
1221+
)
1222+
return hoverAfterPackageUpdate?.contents.markupContent?.value.contains("Do something v1.1.0") ?? false
1223+
}
1224+
}
1225+
}
1226+
1227+
extension HoverResponseContents {
1228+
var markupContent: MarkupContent? {
1229+
switch self {
1230+
case .markupContent(let markupContent): return markupContent
1231+
default: return nil
1232+
}
1233+
}
11101234
}

Tests/SourceKitLSPTests/BuildSystemTests.swift

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,15 +187,10 @@ final class BuildSystemTests: XCTestCase {
187187

188188
await buildSystem.delegate?.fileBuildSettingsChanged([doc])
189189

190-
var receivedCorrectDiagnostic = false
191-
for _ in 0..<Int(defaultTimeout) {
190+
try await repeatUntilExpectedResult {
192191
let refreshedDiags = try await testClient.nextDiagnosticsNotification(timeout: .seconds(1))
193-
if refreshedDiags.diagnostics.count == 0, try text == documentManager.latestSnapshot(doc).text {
194-
receivedCorrectDiagnostic = true
195-
break
196-
}
192+
return try text == documentManager.latestSnapshot(doc).text && refreshedDiags.diagnostics.count == 0
197193
}
198-
XCTAssert(receivedCorrectDiagnostic)
199194
}
200195

201196
func testSwiftDocumentUpdatedBuildSettings() async throws {

Tests/SourceKitLSPTests/MainFilesProviderTests.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,16 +201,12 @@ final class MainFilesProviderTests: XCTestCase {
201201
// `clangd` may return diagnostics from the old build settings sometimes (I believe when it's still building the
202202
// preamble for shared.h when the new build settings come in). Check that it eventually returns the correct
203203
// diagnostics.
204-
var receivedCorrectDiagnostic = false
205-
for _ in 0..<Int(defaultTimeout) {
204+
try await repeatUntilExpectedResult {
206205
let refreshedDiags = try await project.testClient.nextDiagnosticsNotification(timeout: .seconds(1))
207-
if let diagnostic = refreshedDiags.diagnostics.only,
208-
diagnostic.message == "Unused variable 'fromMyFancyLibrary'"
209-
{
210-
receivedCorrectDiagnostic = true
211-
break
206+
guard let diagnostic = refreshedDiags.diagnostics.only else {
207+
return false
212208
}
209+
return diagnostic.message == "Unused variable 'fromMyFancyLibrary'"
213210
}
214-
XCTAssert(receivedCorrectDiagnostic)
215211
}
216212
}

0 commit comments

Comments
 (0)