-
Notifications
You must be signed in to change notification settings - Fork 145
Expand file tree
/
Copy pathEvent+FallbackEventHandler.swift
More file actions
261 lines (233 loc) · 9.18 KB
/
Event+FallbackEventHandler.swift
File metadata and controls
261 lines (233 loc) · 9.18 KB
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
253
254
255
256
257
258
259
260
261
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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 Swift project authors
//
private import _TestingInternals.InteropOnly
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
public enum Interop: Sendable {}
extension Interop {
public enum Mode: String, Sendable, Codable, CaseIterable {
/// The interop feature is not active.
case none
/// Show runtime warnings for assertion failures caused by primitives
/// from other test libraries. The overall test case success/failure is
/// therefore not affected.
///
/// Show runtime warning issues for XCTest API usage when running a
/// Swift Testing test.
case limited
/// Show assertion failures caused by primitives from other test
/// libraries.
///
/// Show runtime warning issues for XCTest API usage when running a
/// Swift Testing test.
case complete
/// Show assertion failures caused by primitives from other test
/// libraries.
///
/// `fatalError` upon any XCTest assertion failures when running a
/// Swift Testing test.
case strict
}
}
extension Interop {
/// Name of the environment variable flag to set when opting-in to the
/// experimental interop feature.
static let experimentalOptInKey = "SWT_EXPERIMENTAL_INTEROP_ENABLED"
}
extension Interop.Mode {
/// The name for the environment variable which if set, overrides the default
/// interop mode.
static let interopModeEnvKey = "SWIFT_TESTING_XCTEST_INTEROP_MODE"
/// Whether this interop mode causes Swift Testing to install a fallback event
/// handler ahead of running tests.
var requiresInstallation: Bool {
Environment.flag(named: Interop.experimentalOptInKey) == true && self != .none
}
/// Current interop mode, which should not be changed after tests start
/// running.
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
public static let current: Self = {
Environment.variable(named: interopModeEnvKey).flatMap(Interop.Mode.init) ?? .limited
}()
}
extension Event {
/// Attempt to handle an event encoded as JSON as if it had been generated in
/// the current testing context.
///
/// If the event contains an issue, handle it, but also record a warning issue
/// notifying the user that interop was performed.
///
/// - Parameters:
/// - recordJSON: The JSON encoding of an event record.
/// - version: The ABI version to use for decoding `recordJSON`.
///
/// - Throws: Any error that prevented handling the encoded record.
///
/// - Important: This function only handles a subset of event kinds.
static func handle<V>(_ recordJSON: UnsafeRawBufferPointer, encodedWith version: V.Type) throws
where V: ABI.Version {
let record = try JSON.decode(ABI.Record<V>.self, from: recordJSON)
guard case .event(let event) = record.kind,
var issue = Issue(decoding: event)
else {
return
}
let xctestWarningMessage =
"XCTest API was used in a Swift Testing test. Adopt Swift Testing primitives, such as #expect, instead."
// For the time being, assume that foreign test events originate from XCTest
lazy var warnForXCTestUsageIssue =
Issue(
kind: .apiMisused, severity: .warning,
comments: [
.init(rawValue: xctestWarningMessage)
], sourceContext: issue.sourceContext)
// Unconditionally downgrade interop issues to warning for limited interop mode.
// Otherwise, preserve the issue severity.
switch Interop.Mode.current {
case .none: return // no-op
case .limited:
issue.severity = .warning
issue.record()
warnForXCTestUsageIssue.record()
case .complete:
issue.record()
warnForXCTestUsageIssue.record()
case .strict:
issue.record()
fatalError(
"\(xctestWarningMessage) This is a fatal error because strict interop mode is active (\(Interop.Mode.interopModeEnvKey)=strict)",
)
}
}
/// Get the best available source location to use when diagnosing an issue
/// decoding a bad record JSON blob.
///
/// - Parameters:
/// - recordJSON: The undecodable JSON.
///
/// - Returns: A source location to use when reporting an issue about
/// `recordJSON`.
private static func _bestAvailableSourceLocation(
forInvalidRecordJSON recordJSON: UnsafeRawBufferPointer
) -> SourceLocation {
// TODO: try to actually extract a source location from arbitrary JSON?
// If there's a test associated with the current task, it should have a
// source location associated with it.
if let test = Test.current {
return test.sourceLocation
}
return .unknown
}
#if !SWT_NO_INTEROP
/// The fallback event handler to install when Swift Testing is the active
/// testing library.
private static let _ourFallbackEventHandler: SWTFallbackEventHandler = {
recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in
let version =
String(validatingCString: recordJSONSchemaVersionNumber)
.flatMap(VersionNumber.init)
.flatMap { ABI.version(forVersionNumber: $0) } ?? ABI.v0.self
let recordJSON = UnsafeRawBufferPointer(
start: recordJSONBaseAddress, count: recordJSONByteCount)
do {
try Self.handle(recordJSON, encodedWith: version)
} catch {
// Surface otherwise "unhandleable" records instead of dropping them silently
let errorContext: Comment = """
Another test library reported a test event that Swift Testing could not decode. Inspect the payload to determine if this was a test assertion failure.
Error:
\(error)
Raw payload:
\(recordJSON)
"""
// Try to figure out a reasonable source context for this issue.
let sourceContext = SourceContext(
backtrace: .current(),
sourceLocation: _bestAvailableSourceLocation(forInvalidRecordJSON: recordJSON)
)
// Record the issue.
Issue(
kind: .system,
comments: [errorContext],
sourceContext: sourceContext
).record()
}
}
#endif
/// The implementation of ``installFallbackEventHandler()``.
private static let _installFallbackEventHandler: Bool = {
#if !SWT_NO_INTEROP
if Interop.Mode.current.requiresInstallation {
return _swift_testing_installFallbackEventHandler(Self._ourFallbackEventHandler)
}
#endif
return false
}()
/// Installs the Swift Testing's fallback event handler, indicating that it is
/// the active testing library. You can only try installing the handler once,
/// so extra attempts will return the status from the first attempt.
///
/// The handler receives events created by other testing libraries and tries
/// to emulate behaviour in Swift Testing where possible. For example, an
/// `XCTAssert` failure reported by the XCTest API can be recorded as an
/// `Issue` in Swift Testing.
///
/// - Returns: Whether the installation succeeded. The installation typically
/// fails because the _TestingInterop library was not available at runtime or
/// another testing library has already installed a fallback event handler.
static func installFallbackEventHandler() -> Bool {
_installFallbackEventHandler
}
/// Post this event to the currently-installed fallback event handler.
///
/// - Parameters:
/// - context: The context associated with this event.
///
/// - Returns: Whether or not the fallback event handler was invoked. If the
/// currently-installed handler belongs to the testing library, returns
/// `false`.
borrowing func postToFallbackEventHandler(in context: borrowing Context) -> Bool {
#if !SWT_NO_INTEROP
return Self._postToFallbackEventHandler?(self, context) != nil
#else
return false
#endif
}
#if !SWT_NO_INTEROP
/// The implementation of ``postToFallbackEventHandler(in:)`` that actually
/// invokes the installed fallback event handler.
///
/// If there was no fallback event handler installed, or if the installed
/// handler belongs to the testing library (and so shouldn't be called by us),
/// the value of this property is `nil`.
private static let _postToFallbackEventHandler: Event.Handler? = {
guard let fallbackEventHandler = _swift_testing_getFallbackEventHandler() else {
return nil
}
let fallbackEventHandlerAddress = castCFunction(fallbackEventHandler, to: UnsafeRawPointer.self)
let ourFallbackEventHandlerAddress = castCFunction(
Self._ourFallbackEventHandler, to: UnsafeRawPointer.self)
if fallbackEventHandlerAddress == ourFallbackEventHandlerAddress {
// The fallback event handler belongs to Swift Testing, so we don't want
// to call it on our own behalf.
return nil
}
// Encode the event as JSON and pass it to the handler.
let abiVersion = ABI.v6_3.self
return abiVersion.eventHandler(encodeAsJSONLines: false) { recordJSON in
fallbackEventHandler(
String(describing: abiVersion.versionNumber),
recordJSON.baseAddress!,
recordJSON.count,
nil
)
}
}()
#endif
}