Skip to content

Commit 7477138

Browse files
committed
WIP: Don’t stop on error
1 parent 8b855c1 commit 7477138

File tree

8 files changed

+208
-54
lines changed

8 files changed

+208
-54
lines changed

SourceKitStressTester/Sources/StressTester/StressTester.swift

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class StressTester {
4141
}
4242
}
4343

44-
public func run(swiftc: String, compilerArgs: CompilerArgs) throws {
44+
public func run(swiftc: String, compilerArgs: CompilerArgs) -> [Error] {
4545
var document = SourceKitDocument(swiftc: swiftc,
4646
args: compilerArgs,
4747
tempDir: options.tempDir,
@@ -59,20 +59,36 @@ public class StressTester {
5959
}
6060

6161
// compute the actions for the entire tree
62-
let (tree, _) = try document.open(rewriteMode: options.rewriteMode)
62+
let tree: SourceFileSyntax
63+
do {
64+
tree = try document.open(rewriteMode: options.rewriteMode).0
65+
} catch {
66+
return [error]
67+
}
6368
let (actions, priorActions) = computeActions(from: tree)
6469

70+
var errors: [Error] = []
71+
6572
if let dryRunAction = options.dryRun {
66-
try dryRunAction(actions)
67-
return
73+
do {
74+
try dryRunAction(actions)
75+
} catch {
76+
errors.append(error)
77+
}
78+
return errors
6879
}
6980

7081
if !priorActions.isEmpty {
71-
// Update initial state
72-
_ = try document.update() { sourceState in
73-
for case .replaceText(let offset, let length, let text) in priorActions {
74-
sourceState.replace(offset: offset, length: length, with: text)
82+
do {
83+
// Update initial state
84+
_ = try document.update() { sourceState in
85+
for case .replaceText(let offset, let length, let text) in priorActions {
86+
sourceState.replace(offset: offset, length: length, with: text)
87+
}
7588
}
89+
} catch {
90+
errors.append(error)
91+
return errors
7692
}
7793
}
7894

@@ -84,29 +100,38 @@ public class StressTester {
84100
if options.printActions {
85101
print(action)
86102
}
87-
switch action {
88-
case .cursorInfo(let offset):
89-
try report(document.cursorInfo(offset: offset))
90-
case .codeComplete(let offset, let expectedResult):
91-
try report(document.codeComplete(offset: offset, expectedResult: expectedResult))
92-
case .rangeInfo(let offset, let length):
93-
try report(document.rangeInfo(offset: offset, length: length))
94-
case .replaceText(let offset, let length, let text):
95-
_ = try document.replaceText(offset: offset, length: length, text: text)
96-
case .format(let offset):
97-
try report(document.format(offset: offset))
98-
case .typeContextInfo(let offset):
99-
try report(document.typeContextInfo(offset: offset))
100-
case .conformingMethodList(let offset):
101-
try report(document.conformingMethodList(offset: offset, typeList: options.conformingMethodsTypeList))
102-
case .collectExpressionType:
103-
try report(document.collectExpressionType())
104-
case .testModule:
105-
try report(document.moduleInterfaceGen())
103+
do {
104+
switch action {
105+
case .cursorInfo(let offset):
106+
try report(document.cursorInfo(offset: offset))
107+
case .codeComplete(let offset, let expectedResult):
108+
try report(document.codeComplete(offset: offset, expectedResult: expectedResult))
109+
case .rangeInfo(let offset, let length):
110+
try report(document.rangeInfo(offset: offset, length: length))
111+
case .replaceText(let offset, let length, let text):
112+
_ = try document.replaceText(offset: offset, length: length, text: text)
113+
case .format(let offset):
114+
try report(document.format(offset: offset))
115+
case .typeContextInfo(let offset):
116+
try report(document.typeContextInfo(offset: offset))
117+
case .conformingMethodList(let offset):
118+
try report(document.conformingMethodList(offset: offset, typeList: options.conformingMethodsTypeList))
119+
case .collectExpressionType:
120+
try report(document.collectExpressionType())
121+
case .testModule:
122+
try report(document.moduleInterfaceGen())
123+
}
124+
} catch {
125+
errors.append(error)
106126
}
107127
}
108128

109-
try document.close()
129+
do {
130+
try document.close()
131+
} catch {
132+
errors.append(error)
133+
}
134+
return errors
110135
}
111136

112137
private func computeActions(from tree: SourceFileSyntax) -> (page: [Action], priorActions: [Action]) {

SourceKitStressTester/Sources/StressTester/StressTesterTool.swift

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ArgumentParser
1414
import Foundation
1515
import Common
1616
import SwiftSyntax
17+
import Dispatch
1718

1819
public struct StressTesterTool: ParsableCommand {
1920
public static var configuration = CommandConfiguration(
@@ -159,20 +160,34 @@ public struct StressTesterTool: ParsableCommand {
159160
}
160161
)
161162

162-
do {
163-
let processedArgs = CompilerArgs(for: file,
164-
args: compilerArgs,
165-
tempDir: tempDir!)
166-
let tester = StressTester(options: options)
167-
try tester.run(swiftc: swiftc!, compilerArgs: processedArgs)
168-
} catch let error as SourceKitError {
169-
let message = StressTesterMessage.detected(error)
170-
try StressTesterTool.report(message, as: format)
171-
throw ExitCode.failure
172-
}
163+
let processedArgs = CompilerArgs(for: file,
164+
args: compilerArgs,
165+
tempDir: tempDir!)
166+
let tester = StressTester(options: options)
173167

174-
// Leave for debugging purposes if there was an error
175-
try FileManager.default.removeItem(at: tempDir!)
168+
DispatchQueue.global(qos: .userInitiated).async { [self] in
169+
do {
170+
let errors = tester.run(swiftc: swiftc!, compilerArgs: processedArgs)
171+
172+
if !errors.isEmpty {
173+
for error in errors {
174+
if let error = error as? SourceKitError {
175+
let message = StressTesterMessage.detected(error)
176+
try StressTesterTool.report(message, as: format)
177+
} else {
178+
throw error
179+
}
180+
}
181+
throw ExitCode.failure
182+
}
183+
184+
// Leave for debugging purposes if there was an error
185+
try FileManager.default.removeItem(at: tempDir!)
186+
} catch {
187+
StressTesterTool.exit(withError: error)
188+
}
189+
StressTesterTool.exit(withError: nil)
190+
}
176191
}
177192

178193
private static func report<T>(_ message: T, as format: OutputFormat) throws

SourceKitStressTester/Sources/SwiftCWrapper/FailFastOperationQueue.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import Foundation
1414

15+
/// An operation queue that stops scheduling new operations as soon as the the
16+
/// completion handler returns `false`.
1517
public final class FailFastOperationQueue<Item: Operation> {
1618
private let serialQueue = DispatchQueue(label: "\(FailFastOperationQueue.self)")
1719
private let queue = OperationQueue()

SourceKitStressTester/Sources/SwiftCWrapper/SwiftCWrapper.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,11 @@ public struct SwiftCWrapper {
134134
progress?.update(step: completed, total: total, text: message)
135135
orderingHandler?.complete(operation.responses, at: index, setLast: !operation.status.isPassed)
136136
operation.responses.removeAll()
137-
return operation.status.isPassed
137+
// We can control whether to stop scheduling new operations here. As long
138+
// as we return `true`, the stress tester continues to schedule new
139+
// test operations. To stop at the first failure, return
140+
// `operation.status.isPassed`.
141+
return true
138142
}
139143
queue.waitUntilFinished()
140144

@@ -151,29 +155,31 @@ public struct SwiftCWrapper {
151155

152156
// Determine the set of processed files and the first failure (if any)
153157
var processedFiles = Set<String>()
154-
var detectedIssue: StressTesterIssue? = nil
158+
var detectedIssues: [StressTesterIssue] = []
155159
for operation in operations where detectedIssue == nil {
156160
switch operation.status {
157161
case .cancelled:
158162
fatalError("cancelled operation before failed operation")
159163
case .unexecuted:
160164
fatalError("unterminated operation")
161165
case .errored(let status):
162-
detectedIssue = .errored(status: status, file: operation.file,
163-
arguments: escapeArgs(operation.args))
166+
detectedIssues.append(.errored(status: status, file: operation.file,
167+
arguments: escapeArgs(operation.args)))
164168
case .failed(let sourceKitError):
165-
detectedIssue = .failed(sourceKitError: sourceKitError,
166-
arguments: escapeArgs(operation.args))
169+
detectedIssues.append(.failed(sourceKitError: sourceKitError,
170+
arguments: escapeArgs(operation.args)))
167171
fallthrough
168172
case .passed:
169173
processedFiles.insert(operation.file)
170174
}
171175
}
172176

173177
let matchingSpec = try issueManager?.update(for: processedFiles, issue: detectedIssue)
174-
try report(detectedIssue, matching: matchingSpec)
178+
for detectedIssue in detectedIssues {
179+
try report(detectedIssue, matching: matchingSpec)
180+
}
175181

176-
if detectedIssue == nil || matchingSpec != nil {
182+
if !detectedIssue.isEmpty || matchingSpec != nil {
177183
return EXIT_SUCCESS
178184
}
179185
return ignoreIssues ? swiftcResult.status : EXIT_FAILURE

SourceKitStressTester/Sources/SwiftSourceKit/SourcekitdClient.swift

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,31 +13,111 @@
1313
//===----------------------------------------------------------------------===//
1414

1515
import sourcekitd
16+
import Dispatch
17+
18+
fileprivate class Object {}
1619

1720
public class SourceKitdService {
1821

22+
enum State {
23+
case running
24+
case interrupted
25+
case semaDisabled
26+
}
27+
28+
/// The queue that makes sure only one request is executed at a time
29+
private let requestQueue = DispatchQueue(label: "SourceKitdService.requestQueue", qos: .userInitiated)
30+
/// The queue that guards access to the `state` and `stateChangeHandlers` variables
31+
private let stateQueue = DispatchQueue(label: "SourceKitdService.stateQueue", qos: .userInitiated)
32+
private var state: State = .running {
33+
didSet {
34+
if #available(macOS 10.12, *) {
35+
dispatchPrecondition(condition: .onQueue(stateQueue))
36+
}
37+
for handler in stateChangeHandlers.values {
38+
handler()
39+
}
40+
}
41+
}
42+
private var stateChangeHandlers: [ObjectIdentifier: () -> Void] = [:]
43+
1944
public init() {
45+
sourcekitd_set_notification_handler { [self] resp in
46+
stateQueue.async {
47+
let response = SourceKitdResponse(resp: resp!)
48+
49+
if self.state == .interrupted {
50+
self.state = .semaDisabled
51+
52+
// sourcekitd came back online. Poke it to restore semantic functionality
53+
let request = SourceKitdRequest(uid: .request_CursorInfo)
54+
request.addParameter(.key_SourceText, value: "")
55+
_ = sourcekitd_send_request_sync(request.rawRequest)
56+
}
57+
58+
if response.isConnectionInterruptionError {
59+
self.state = .interrupted
60+
} else if response.isSemaDisabledNotification {
61+
self.state = .semaDisabled
62+
} else if response.isSemaEnabledNotification {
63+
self.state = .running
64+
}
65+
}
66+
}
2067
sourcekitd_initialize()
2168
}
2269
deinit {
2370
sourcekitd_shutdown()
2471
}
2572

73+
/// Execute `callback` one this service is in `desiredState`
74+
private func waitForState(_ desiredState: State, callback: @escaping () -> Void) {
75+
stateQueue.async { [self] in
76+
if state == desiredState {
77+
callback()
78+
} else {
79+
let identifier = ObjectIdentifier(Object())
80+
stateChangeHandlers[identifier] = { [self] in
81+
if #available(macOS 10.12, *) {
82+
dispatchPrecondition(condition: .onQueue(stateQueue))
83+
}
84+
if state == desiredState {
85+
callback()
86+
stateChangeHandlers[identifier] = nil
87+
}
88+
}
89+
}
90+
}
91+
}
92+
93+
/// Block the current thread until this service is in `desiredState`.
94+
private func blockUntilState(_ desiredState: State) {
95+
let semaphore = DispatchSemaphore(value: 0)
96+
waitForState(desiredState) {
97+
semaphore.signal()
98+
}
99+
semaphore.wait()
100+
}
101+
26102
/// Send a request synchronously with a handler for its response.
27103
/// - Parameter request: The request to send.
28104
/// - Returns: The response from the sourcekitd service.
29105
public func sendSyn(request: SourceKitdRequest) -> SourceKitdResponse {
30-
return SourceKitdResponse(resp: sourcekitd_send_request_sync(request.rawRequest))
106+
return requestQueue.sync {
107+
blockUntilState(.running)
108+
return SourceKitdResponse(resp: sourcekitd_send_request_sync(request.rawRequest))
109+
}
31110
}
32111

33112
/// Send a request asynchronously with a handler for its response.
34113
/// - Parameter request: The request to send.
35114
/// - Parameter handler: The handler for the response in the future.
36115
public func send(request: SourceKitdRequest,
37116
handler: @escaping (SourceKitdResponse) -> ()) {
38-
sourcekitd_send_request(request.rawRequest, nil) { response in
39-
guard let response = response else { return }
40-
handler(SourceKitdResponse(resp: response))
117+
requestQueue.async { [self] in
118+
blockUntilState(.running)
119+
let response = SourceKitdResponse(resp: sourcekitd_send_request_sync(request.rawRequest))
120+
handler(response)
41121
}
42122
}
43123
}

SourceKitStressTester/Sources/SwiftSourceKit/SourcekitdResponse.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ public class SourceKitdResponse: CustomStringConvertible {
213213

214214
private let resp: sourcekitd_response_t
215215

216+
public var isNull: Bool {
217+
return sourcekitd_variant_get_type(sourcekitd_response_get_value(resp)).rawValue == SOURCEKITD_VARIANT_TYPE_NULL.rawValue
218+
}
219+
216220
public var value: Dictionary {
217221
return Dictionary(dict: sourcekitd_response_get_value(resp), context: self)
218222
}
@@ -234,6 +238,20 @@ public class SourceKitdResponse: CustomStringConvertible {
234238
return value.getOptional(.key_Notification) != nil
235239
}
236240

241+
public var isSemaDisabledNotification: Bool {
242+
if isNull { return false }
243+
guard let notification = value.getOptional(.key_Notification)?.getUID()
244+
else { return false }
245+
return notification == .semaDisabledNotification
246+
}
247+
248+
public var isSemaEnabledNotification: Bool {
249+
if isNull { return false }
250+
guard let notification = value.getOptional(.key_Notification)?.getUID()
251+
else { return false }
252+
return notification == .semaEnabledNotification
253+
}
254+
237255
/// Whether or not this response represents a connection interruption error.
238256
public var isConnectionInterruptionError: Bool {
239257
return sourcekitd_response_is_error(resp) &&

SourceKitStressTester/Sources/SwiftSourceKit/UIDs.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,5 +376,9 @@ extension SourceKitdUID {
376376
public static let kind_SyntaxTreeSerializationByteTree = SourceKitdUID(string: "source.syntaxtree.serialization.format.bytetree")
377377

378378
public static let compilerCrashedNotification = SourceKitdUID(string: "notification.toolchain-compiler-crashed")
379+
public static let semaDisabledNotification = SourceKitdUID(string:
380+
"source.notification.sema_disabled")
381+
public static let semaEnabledNotification = SourceKitdUID(string:
382+
"source.notification.sema_enabled")
379383
public static let source_notification_editor_documentupdate = SourceKitdUID(string: "source.notification.editor.documentupdate")
380384
}

SourceKitStressTester/Sources/sk-stress-test/main.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,9 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import StressTester
14+
import Dispatch
1415

15-
StressTesterTool.main()
16+
DispatchQueue.main.async {
17+
StressTesterTool.main()
18+
}
19+
dispatchMain()

0 commit comments

Comments
 (0)