File: MultiFileTestProject.swift

package info (click to toggle)
swiftlang 6.2.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,856,264 kB
  • sloc: cpp: 9,995,718; ansic: 2,234,019; asm: 1,092,167; python: 313,940; objc: 82,726; f90: 80,126; lisp: 38,373; pascal: 25,580; sh: 20,378; ml: 5,058; perl: 4,751; makefile: 4,725; awk: 3,535; javascript: 3,018; xml: 918; fortran: 664; cs: 573; ruby: 396
file content (252 lines) | stat: -rw-r--r-- 9,739 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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
//
//===----------------------------------------------------------------------===//

package import Foundation
package import LanguageServerProtocol
import SKLogging
package import SKOptions
package import SourceKitLSP
import SwiftExtensions
package import ToolchainRegistry

/// The location of a test file within test workspace.
package struct RelativeFileLocation: Hashable, ExpressibleByStringLiteral {
  /// The subdirectories in which the file is located.
  package let directories: [String]

  /// The file's name.
  package let fileName: String

  package init(directories: [String] = [], _ fileName: String) {
    self.directories = directories
    self.fileName = fileName
  }

  package init(stringLiteral value: String) {
    let components = value.components(separatedBy: "/")
    self.init(directories: components.dropLast(), components.last!)
  }

  package func url(relativeTo: URL) -> URL {
    var url = relativeTo
    for directory in directories {
      url = url.appendingPathComponent(directory)
    }
    url = url.appendingPathComponent(fileName)
    return url
  }
}

/// A test project that writes multiple files to disk and opens a `TestSourceKitLSPClient` client with a workspace
/// pointing to a temporary directory containing those files.
///
/// The temporary files will be deleted when the `TestSourceKitLSPClient` is destructed.
package class MultiFileTestProject {
  /// Information necessary to open a file in the LSP server by its filename.
  package struct FileData {
    /// The URI at which the file is stored on disk.
    package let uri: DocumentURI

    /// The contents of the file including location markers.
    package let markedText: String
  }

  package let testClient: TestSourceKitLSPClient

  /// Information necessary to open a file in the LSP server by its filename.
  private let fileData: [String: FileData]

  enum Error: Swift.Error {
    /// No file with the given filename is known to the `MultiFileTestProject`.
    case fileNotFound(name: String)

    /// Trying to delete a file in `changeDocument` that doesn't exist.
    case deleteNonExistentFile
  }

  /// The directory in which the temporary files are being placed.
  package let scratchDirectory: URL

  package static func writeFilesToDisk(
    files: [RelativeFileLocation: String],
    scratchDirectory: URL
  ) throws -> [String: FileData] {
    try FileManager.default.createDirectory(at: scratchDirectory, withIntermediateDirectories: true)

    var fileData: [String: FileData] = [:]
    for (fileLocation, markedText) in files {
      // Drop trailing slashes from the test dir URL, so tests can write `$TEST_DIR_URL/someFile.swift` without ending
      // up with double slashes.
      var testDirUrl = scratchDirectory.absoluteString
      while testDirUrl.hasSuffix("/") {
        testDirUrl = String(testDirUrl.dropLast())
      }
      let markedText =
        markedText
        .replacingOccurrences(of: "$TEST_DIR_URL", with: testDirUrl)
        .replacingOccurrences(
          of: "$TEST_DIR_BACKSLASH_ESCAPED",
          with: try scratchDirectory.filePath.replacing(#"\"#, with: #"\\"#)
        )
        .replacingOccurrences(of: "$TEST_DIR", with: try scratchDirectory.filePath)
      let fileURL = fileLocation.url(relativeTo: scratchDirectory)
      try FileManager.default.createDirectory(
        at: fileURL.deletingLastPathComponent(),
        withIntermediateDirectories: true
      )
      try extractMarkers(markedText).textWithoutMarkers.write(to: fileURL, atomically: false, encoding: .utf8)

      if fileData[fileLocation.fileName] != nil {
        // If we already have a file with this name, remove its data. That way we can't reference any of the two
        // conflicting documents and will throw when trying to open them, instead of non-deterministically picking one.
        fileData[fileLocation.fileName] = nil
      } else {
        fileData[fileLocation.fileName] = FileData(
          uri: DocumentURI(fileURL),
          markedText: markedText
        )
      }
    }
    return fileData
  }

  /// Writes the specified files to a temporary directory on disk and creates a `TestSourceKitLSPClient` for that
  /// temporary directory.
  ///
  /// The file contents can contain location markers, which are returned when opening a document using
  /// ``openDocument(_:)``.
  ///
  /// File contents can also contain `$TEST_DIR`, which gets replaced by the temporary directory.
  package init(
    files: [RelativeFileLocation: String],
    workspaces: (_ scratchDirectory: URL) async throws -> [WorkspaceFolder] = {
      [WorkspaceFolder(uri: DocumentURI($0))]
    },
    initializationOptions: LSPAny? = nil,
    capabilities: ClientCapabilities = ClientCapabilities(),
    options: SourceKitLSPOptions? = nil,
    toolchainRegistry: ToolchainRegistry = .forTesting,
    hooks: Hooks = Hooks(),
    enableBackgroundIndexing: Bool = false,
    usePullDiagnostics: Bool = true,
    preInitialization: ((TestSourceKitLSPClient) -> Void)? = nil,
    testScratchDir overrideTestScratchDir: URL? = nil,
    cleanUp: (@Sendable () -> Void)? = nil,
    testName: String = #function
  ) async throws {
    scratchDirectory = try overrideTestScratchDir ?? testScratchDir(testName: testName)
    self.fileData = try Self.writeFilesToDisk(files: files, scratchDirectory: scratchDirectory)

    self.testClient = try await TestSourceKitLSPClient(
      options: options,
      hooks: hooks,
      initializationOptions: initializationOptions,
      capabilities: capabilities,
      toolchainRegistry: toolchainRegistry,
      usePullDiagnostics: usePullDiagnostics,
      enableBackgroundIndexing: enableBackgroundIndexing,
      workspaceFolders: workspaces(scratchDirectory),
      preInitialization: preInitialization,
      cleanUp: { [scratchDirectory] in
        if cleanScratchDirectories {
          try? FileManager.default.removeItem(at: scratchDirectory)
        }
        cleanUp?()
      }
    )
  }

  /// Opens the document with the given file name in the SourceKit-LSP server.
  ///
  /// - Returns: The URI for the opened document and the positions of the location markers.
  package func openDocument(
    _ fileName: String,
    language: Language? = nil
  ) throws -> (uri: DocumentURI, positions: DocumentPositions) {
    guard let fileData = self.fileData[fileName] else {
      throw Error.fileNotFound(name: fileName)
    }
    let positions = testClient.openDocument(fileData.markedText, uri: fileData.uri, language: language)
    return (fileData.uri, positions)
  }

  /// Returns the URI of the file with the given name.
  package func uri(for fileName: String) throws -> DocumentURI {
    guard let fileData = self.fileData[fileName] else {
      throw Error.fileNotFound(name: fileName)
    }
    return fileData.uri
  }

  /// Returns the position of the given marker in the given file.
  package func position(of marker: String, in fileName: String) throws -> Position {
    guard let fileData = self.fileData[fileName] else {
      throw Error.fileNotFound(name: fileName)
    }
    return DocumentPositions(markedText: fileData.markedText)[marker]
  }

  package func range(from fromMarker: String, to toMarker: String, in fileName: String) throws -> Range<Position> {
    return try position(of: fromMarker, in: fileName)..<position(of: toMarker, in: fileName)
  }

  package func location(from fromMarker: String, to toMarker: String, in fileName: String) throws -> Location {
    let range = try self.range(from: fromMarker, to: toMarker, in: fileName)
    return Location(uri: try self.uri(for: fileName), range: range)
  }

  /// Modify the given file on disk, send a watched files did change notification to SourceKit-LSP and wait for that
  /// notification to get handled.
  ///
  /// When `newMarkedContents` is `nil`, the file is deleted.
  ///
  /// Returns the URI and the document positions of the new document.
  @discardableResult
  package func changeFileOnDisk(
    _ fileName: String,
    newMarkedContents: String?
  ) async throws -> (uri: DocumentURI, positions: DocumentPositions) {
    let uri = try self.uri(for: fileName)
    guard let url = uri.fileURL else {
      throw Error.fileNotFound(name: fileName)
    }
    let positions: DocumentPositions
    let newContents: String?
    if let newMarkedContents {
      (positions, newContents) = DocumentPositions.extract(from: newMarkedContents)
    } else {
      positions = DocumentPositions(markedText: "")
      newContents = nil
    }

    let fileExists = FileManager.default.fileExists(at: url)
    let changeType: FileChangeType
    switch (fileExists, newContents) {
    case (false, let newContents?):
      try await newContents.writeWithRetry(to: url)
      changeType = .created
    case (false, nil):
      throw Error.deleteNonExistentFile
    case (true, let newContents?):
      try await newContents.writeWithRetry(to: url)
      changeType = .changed
    case (true, nil):
      try FileManager.default.removeItem(at: url)
      changeType = .deleted
    }
    testClient.send(DidChangeWatchedFilesNotification(changes: [FileEvent(uri: uri, type: changeType)]))
    // Ensure that we handle the `DidChangeWatchedFilesNotification`.
    try await testClient.send(SynchronizeRequest())

    return (uri, positions)
  }
}