Skip to content

Commit c277875

Browse files
committed
Introduce a MultiFileTestWorkspace that can store multiple files of arbitrary languages
This allows us to remove the `BasicCXX` test directory.
1 parent dc15097 commit c277875

File tree

9 files changed

+244
-158
lines changed

9 files changed

+244
-158
lines changed

Sources/SKTestSupport/INPUTS/BasicCXX/Object.h

Lines changed: 0 additions & 5 deletions
This file was deleted.

Sources/SKTestSupport/INPUTS/BasicCXX/main.c

Lines changed: 0 additions & 6 deletions
This file was deleted.

Sources/SKTestSupport/INPUTS/BasicCXX/project.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

Sources/SKTestSupport/IndexedSingleSwiftFileWorkspace.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public struct IndexedSingleSwiftFileWorkspace {
3030
_ markedText: String,
3131
testName: String = #function
3232
) async throws {
33-
let testWorkspaceDirectory = try testScratchDirName(testName)
33+
let testWorkspaceDirectory = try testScratchDir(testName: testName)
3434

3535
let testFileURL = testWorkspaceDirectory.appendingPathComponent("test.swift")
3636
let indexURL = testWorkspaceDirectory.appendingPathComponent("index")
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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 Foundation
14+
import LanguageServerProtocol
15+
import SKCore
16+
17+
/// The location of a test file within test workspace.
18+
public struct RelativeFileLocation: Hashable, ExpressibleByStringLiteral {
19+
/// The subdirectories in which the file is located.
20+
fileprivate let directories: [String]
21+
22+
/// The file's name.
23+
fileprivate let fileName: String
24+
25+
public init(directories: [String] = [], _ fileName: String) {
26+
self.directories = directories
27+
self.fileName = fileName
28+
}
29+
30+
public init(stringLiteral value: String) {
31+
self.init(value)
32+
}
33+
}
34+
35+
/// A workspace that writes multiple files to disk and opens a `TestSourceKitLSPClient` client with a workspace pointing
36+
/// to a temporary directory containing those files.
37+
///
38+
/// The temporary files will be deleted when the `TestSourceKitLSPClient` is destructed.
39+
public class MultiFileTestWorkspace {
40+
/// Information necessary to open a file in the LSP server by its filename.
41+
private struct FileData {
42+
/// The URI at which the file is stored on disk.
43+
let uri: DocumentURI
44+
45+
/// The contents of the file including location markers.
46+
let markedText: String
47+
}
48+
49+
public let testClient: TestSourceKitLSPClient
50+
51+
/// Information necessary to open a file in the LSP server by its filename.
52+
private let fileData: [String: FileData]
53+
54+
enum Error: Swift.Error {
55+
/// No file with the given filename is known to the `SwiftPMTestWorkspace`.
56+
case fileNotFound
57+
}
58+
59+
/// The directory in which the temporary files are being placed.
60+
let scratchDirectory: URL
61+
62+
public init(
63+
files: [RelativeFileLocation: String],
64+
testName: String = #function
65+
) async throws {
66+
scratchDirectory = try testScratchDir(testName: testName)
67+
try FileManager.default.createDirectory(at: scratchDirectory, withIntermediateDirectories: true)
68+
69+
var fileData: [String: FileData] = [:]
70+
for (fileLocation, markedText) in files {
71+
var fileURL = scratchDirectory
72+
for directory in fileLocation.directories {
73+
fileURL = fileURL.appendingPathComponent(directory)
74+
}
75+
fileURL = fileURL.appendingPathComponent(fileLocation.fileName)
76+
try FileManager.default.createDirectory(
77+
at: fileURL.deletingLastPathComponent(),
78+
withIntermediateDirectories: true
79+
)
80+
try extractMarkers(markedText).textWithoutMarkers.write(to: fileURL, atomically: false, encoding: .utf8)
81+
82+
precondition(
83+
fileData[fileLocation.fileName] == nil,
84+
"Files within a `MultiFileTestWorkspace` must have unique names"
85+
)
86+
fileData[fileLocation.fileName] = FileData(
87+
uri: DocumentURI(fileURL),
88+
markedText: markedText
89+
)
90+
}
91+
self.fileData = fileData
92+
93+
self.testClient = try await TestSourceKitLSPClient(
94+
workspaceFolders: [
95+
WorkspaceFolder(uri: DocumentURI(scratchDirectory))
96+
],
97+
cleanUp: { [scratchDirectory] in
98+
try? FileManager.default.removeItem(at: scratchDirectory)
99+
}
100+
)
101+
}
102+
103+
/// Opens the document with the given file name in the SourceKit-LSP server.
104+
///
105+
/// - Returns: The URI for the opened document and the positions of the location markers.
106+
public func openDocument(_ fileName: String) throws -> (uri: DocumentURI, positions: DocumentPositions) {
107+
guard let fileData = self.fileData[fileName] else {
108+
throw Error.fileNotFound
109+
}
110+
let positions = testClient.openDocument(fileData.markedText, uri: fileData.uri)
111+
return (fileData.uri, positions)
112+
}
113+
114+
/// Returns the URI of the file with the given name.
115+
public func uri(for fileName: String) throws -> DocumentURI {
116+
guard let fileData = self.fileData[fileName] else {
117+
throw Error.fileNotFound
118+
}
119+
return fileData.uri
120+
}
121+
}

Sources/SKTestSupport/SwiftPMTestWorkspace.swift

Lines changed: 13 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -15,44 +15,12 @@ import LanguageServerProtocol
1515
import SKCore
1616
import TSCBasic
1717

18-
public struct SwiftPMTestWorkspace {
19-
/// The location of a file within a package, ie. its module and its filename.
20-
public struct FileSpec: Hashable, ExpressibleByStringLiteral {
21-
fileprivate let moduleName: String
22-
fileprivate let fileName: String
23-
24-
public init(module: String = "MyLibrary", _ fileName: String) {
25-
self.moduleName = module
26-
self.fileName = fileName
27-
}
28-
29-
public init(stringLiteral value: String) {
30-
self.init(value)
31-
}
32-
}
33-
34-
/// Information necessary to open a file in the LSP server by its filename.
35-
private struct FileData {
36-
/// The URI at which the file is stored on disk.
37-
let uri: DocumentURI
38-
39-
/// The contents of the file including location markers.
40-
let markedText: String
41-
}
42-
18+
public class SwiftPMTestWorkspace: MultiFileTestWorkspace {
4319
enum Error: Swift.Error {
4420
/// The `swift` executable could not be found.
4521
case swiftNotFound
46-
47-
/// No file with the given filename is known to the `SwiftPMTestWorkspace`.
48-
case fileNotFound
4922
}
5023

51-
public let testClient: TestSourceKitLSPClient
52-
53-
/// Information necessary to open a file in the LSP server by its filename.
54-
private let fileData: [String: FileData]
55-
5624
public static let defaultPackageManifest: String = """
5725
// swift-tools-version: 5.7
5826
@@ -68,70 +36,29 @@ public struct SwiftPMTestWorkspace {
6836
///
6937
/// If `index` is `true`, then the package will be built, indexing all modules within the package.
7038
public init(
71-
files: [FileSpec: String],
72-
manifest: String = Self.defaultPackageManifest,
39+
files: [String: String],
40+
manifest: String = SwiftPMTestWorkspace.defaultPackageManifest,
7341
index: Bool = false,
7442
testName: String = #function
7543
) async throws {
76-
let packageDirectory = try testScratchDirName(testName)
44+
var filesByPath: [RelativeFileLocation: String] = [:]
45+
for (fileName, contents) in files {
46+
filesByPath[RelativeFileLocation(directories: ["Sources", "MyLibrary"], fileName)] = contents
47+
}
48+
filesByPath["Package.swift"] = manifest
49+
try await super.init(
50+
files: filesByPath,
51+
testName: testName
52+
)
7753

7854
guard let swift = ToolchainRegistry.shared.default?.swift?.asURL else {
7955
throw Error.swiftNotFound
8056
}
8157

82-
var fileData: [String: FileData] = [:]
83-
for (fileSpec, markedText) in files {
84-
let fileURL =
85-
packageDirectory
86-
.appendingPathComponent("Sources")
87-
.appendingPathComponent(fileSpec.moduleName)
88-
.appendingPathComponent(fileSpec.fileName)
89-
try FileManager.default.createDirectory(
90-
at: fileURL.deletingLastPathComponent(),
91-
withIntermediateDirectories: true
92-
)
93-
try extractMarkers(markedText).textWithoutMarkers.write(
94-
to: fileURL,
95-
atomically: false,
96-
encoding: .utf8
97-
)
98-
99-
fileData[fileSpec.fileName] = FileData(
100-
uri: DocumentURI(fileURL),
101-
markedText: markedText
102-
)
103-
}
104-
self.fileData = fileData
105-
106-
try manifest.write(
107-
to: packageDirectory.appendingPathComponent("Package.swift"),
108-
atomically: false,
109-
encoding: .utf8
110-
)
111-
11258
if index {
113-
/// Running `swift build` might fail if the package contains syntax errors. That's intentional
114-
try await Process.checkNonZeroExit(arguments: [swift.path, "build", "--package-path", packageDirectory.path])
59+
try await Process.checkNonZeroExit(arguments: [swift.path, "build", "--package-path", scratchDirectory.path])
11560
}
116-
117-
self.testClient = try await TestSourceKitLSPClient(
118-
workspaceFolders: [
119-
WorkspaceFolder(uri: DocumentURI(packageDirectory))
120-
],
121-
cleanUp: {
122-
try? FileManager.default.removeItem(at: packageDirectory)
123-
}
124-
)
125-
12661
// Wait for the indexstore-db to finish indexing
12762
_ = try await testClient.send(PollIndexRequest())
12863
}
129-
130-
public func openDocument(_ fileName: String) throws -> (uri: DocumentURI, positions: DocumentPositions) {
131-
guard let fileData = self.fileData[fileName] else {
132-
throw Error.fileNotFound
133-
}
134-
let positions = testClient.openDocument(fileData.markedText, uri: fileData.uri)
135-
return (fileData.uri, positions)
136-
}
13764
}

Sources/SKTestSupport/Utils.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import Foundation
1414
import LanguageServerProtocol
15+
import SKCore
1516

1617
extension Language {
1718
var fileExtension: String {
@@ -44,7 +45,7 @@ extension DocumentURI {
4445
}
4546

4647
/// An empty directory in which a test with `#function` name `testName` can store temporary data.
47-
func testScratchDirName(_ testName: String) throws -> URL {
48+
public func testScratchDir(testName: String = #function) throws -> URL {
4849
let testBaseName = testName.prefix(while: \.isLetter)
4950

5051
let url = FileManager.default.temporaryDirectory
@@ -80,3 +81,5 @@ fileprivate extension URL {
8081
#endif
8182
}
8283
}
84+
85+
public let hasClangd = ToolchainRegistry.shared.default?.clangd != nil

Tests/SourceKitLSPTests/LocalClangTests.swift

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -269,21 +269,27 @@ final class LocalClangTests: XCTestCase {
269269
}
270270

271271
func testSemanticHighlighting() async throws {
272-
guard haveClangd else { return }
273-
guard let ws = try await staticSourceKitTibsWorkspace(name: "BasicCXX") else {
274-
return
275-
}
276-
let mainLoc = ws.testLoc("Object:include:main")
272+
try XCTSkipIf(!hasClangd)
277273

278-
try ws.openDocument(mainLoc.url, language: .c)
274+
let testClient = try await TestSourceKitLSPClient()
275+
let uri = DocumentURI.for(.c)
279276

280-
let diags = try await ws.testClient.nextDiagnosticsNotification()
277+
testClient.openDocument(
278+
"""
279+
int main(int argc, const char *argv[]) {
280+
}
281+
""",
282+
uri: uri
283+
)
284+
285+
let diags = try await testClient.nextDiagnosticsNotification()
281286
XCTAssertEqual(diags.diagnostics.count, 0)
282287

283-
let request = DocumentSemanticTokensRequest(textDocument: mainLoc.docIdentifier)
288+
let request = DocumentSemanticTokensRequest(textDocument: TextDocumentIdentifier(uri))
284289
do {
285-
let reply = try await ws.testClient.send(request)
286-
XCTAssertNotNil(reply)
290+
let reply = try await testClient.send(request)
291+
let data = try XCTUnwrap(reply?.data)
292+
XCTAssertGreaterThanOrEqual(data.count, 0)
287293
} catch let e {
288294
if let error = e as? ResponseError {
289295
try XCTSkipIf(
@@ -296,32 +302,49 @@ final class LocalClangTests: XCTestCase {
296302
}
297303

298304
func testDocumentDependenciesUpdated() async throws {
299-
let ws = try await mutableSourceKitTibsTestWorkspace(name: "BasicCXX")!
305+
try XCTSkipIf(!hasClangd)
306+
307+
let ws = try await MultiFileTestWorkspace(files: [
308+
"Object.h": """
309+
struct Object {
310+
int field;
311+
};
312+
313+
struct Object * newObject();
314+
""",
315+
"main.c": """
316+
#include "Object.h"
300317
301-
let cFileLoc = ws.testLoc("Object:ref:main")
318+
int main(int argc, const char *argv[]) {
319+
struct Object *obj = 1️⃣newObject();
320+
}
321+
""",
322+
"compile_flags.txt": "",
323+
])
302324

303-
try ws.openDocument(cFileLoc.url, language: .cpp)
325+
let (mainUri, _) = try ws.openDocument("main.c")
326+
let headerUri = try ws.uri(for: "Object.h")
304327

305328
// Initially the workspace should build fine.
306329
let initialDiags = try await ws.testClient.nextDiagnosticsNotification()
307330
XCTAssert(initialDiags.diagnostics.isEmpty)
308331

309332
// We rename Object to MyObject in the header.
310-
_ = try ws.sources.edit { builder in
311-
let headerFilePath = ws.sources.rootDirectory.appendingPathComponent("Object.h")
312-
var headerFile = try String(contentsOf: headerFilePath, encoding: .utf8)
313-
let targetMarkerRange = headerFile.range(of: "/*Object*/")!
314-
headerFile.replaceSubrange(targetMarkerRange, with: "My")
315-
builder.write(headerFile, to: headerFilePath)
316-
}
333+
try """
334+
struct MyObject {
335+
int field;
336+
};
337+
338+
struct MyObject * newObject();
339+
""".write(to: headerUri.fileURL!, atomically: false, encoding: .utf8)
317340

318341
let clangdServer = await ws.testClient.server._languageService(
319-
for: cFileLoc.docUri,
320-
.cpp,
321-
in: ws.testClient.server.workspaceForDocument(uri: cFileLoc.docUri)!
342+
for: mainUri,
343+
.c,
344+
in: ws.testClient.server.workspaceForDocument(uri: mainUri)!
322345
)!
323346

324-
await clangdServer.documentDependenciesUpdated(cFileLoc.docUri)
347+
await clangdServer.documentDependenciesUpdated(mainUri)
325348

326349
// Now we should get a diagnostic in main.c file because `Object` is no longer defined.
327350
let editedDiags = try await ws.testClient.nextDiagnosticsNotification()

0 commit comments

Comments
 (0)