Skip to content

Commit 8a6cd8f

Browse files
authored
Configurable interop modes (#1615)
Support none/limited/complete/strict interop modes with varying degrees of noisiness when handling interop issues. ### Motivation: Allow users to configure the interop mode for their tests. ### Modifications: * Modes will only apply if `SWT_EXPERIMENTAL_INTEROP_ENABLED` is enabled. * limited: warnings only, complete: report issues as errors, strict: fatalError when handling interop issues * Default mode is "complete" if opted-in to interop. If interop becomes enabled by default in the future, this will be updated. Resolves rdar://171907588 ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 3adf6d6 commit 8a6cd8f

File tree

4 files changed

+316
-78
lines changed

4 files changed

+316
-78
lines changed

Sources/Testing/Events/Event+FallbackEventHandler.swift

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,63 @@
1010

1111
private import _TestingInternals
1212

13+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
14+
public enum Interop: Sendable {}
15+
16+
extension Interop {
17+
public enum Mode: String, Sendable, Codable, CaseIterable {
18+
/// The interop feature is not active.
19+
case none
20+
21+
/// Show runtime warnings for assertion failures caused by primitives
22+
/// from other test libraries. The overall test case success/failure is
23+
/// therefore not affected.
24+
///
25+
/// Show runtime warning issues for XCTest API usage when running a
26+
/// Swift Testing test.
27+
case limited
28+
29+
/// Show assertion failures caused by primitives from other test
30+
/// libraries.
31+
///
32+
/// Show runtime warning issues for XCTest API usage when running a
33+
/// Swift Testing test.
34+
case complete
35+
36+
/// Show assertion failures caused by primitives from other test
37+
/// libraries.
38+
///
39+
/// `fatalError` upon any XCTest assertion failures when running a
40+
/// Swift Testing test.
41+
case strict
42+
}
43+
}
44+
45+
extension Interop {
46+
/// Name of the environment variable flag to set when opting-in to the
47+
/// experimental interop feature.
48+
static let experimentalOptInKey = "SWT_EXPERIMENTAL_INTEROP_ENABLED"
49+
}
50+
51+
extension Interop.Mode {
52+
/// The name for the environment variable which if set, overrides the default
53+
/// interop mode.
54+
static let interopModeEnvKey = "SWIFT_TESTING_XCTEST_INTEROP_MODE"
55+
56+
/// Whether this interop mode causes Swift Testing to install a fallback event
57+
/// handler ahead of running tests.
58+
var requiresInstallation: Bool {
59+
Environment.flag(named: Interop.experimentalOptInKey) == true && self != .none
60+
}
61+
62+
/// Current interop mode, which should not be changed after tests start
63+
/// running.
64+
@_spi(Experimental) @_spi(ForToolsIntegrationOnly)
65+
public static let current: Self = {
66+
Environment.variable(named: interopModeEnvKey).flatMap(Interop.Mode.init) ?? .limited
67+
}()
68+
}
69+
1370
extension Event {
1471
/// Attempt to handle an event encoded as JSON as if it had been generated in
1572
/// the current testing context.
@@ -28,22 +85,39 @@ extension Event {
2885
where V: ABI.Version {
2986
let record = try JSON.decode(ABI.Record<V>.self, from: recordJSON)
3087
guard case .event(let event) = record.kind,
31-
let issue = Issue(decoding: event) else {
88+
var issue = Issue(decoding: event)
89+
else {
3290
return
3391
}
3492

93+
let xctestWarningMessage =
94+
"XCTest API was used in a Swift Testing test. Adopt Swift Testing primitives, such as #expect, instead."
95+
3596
// For the time being, assume that foreign test events originate from XCTest
36-
let warnForXCTestUsageIssue = {
37-
return Issue(
97+
lazy var warnForXCTestUsageIssue =
98+
Issue(
3899
kind: .apiMisused, severity: .warning,
39100
comments: [
40-
"XCTest API was used in a Swift Testing test. Adopt Swift Testing primitives, such as #expect, instead."
41-
], sourceContext: issue.sourceContext
42-
)
43-
}()
101+
.init(rawValue: xctestWarningMessage)
102+
], sourceContext: issue.sourceContext)
44103

45-
issue.record()
46-
warnForXCTestUsageIssue.record()
104+
switch Interop.Mode.current {
105+
case .none: return // no-op
106+
case .limited:
107+
issue.severity = .warning
108+
issue.record()
109+
warnForXCTestUsageIssue.record()
110+
case .complete:
111+
issue.severity = .error
112+
issue.record()
113+
warnForXCTestUsageIssue.record()
114+
case .strict:
115+
issue.severity = .error
116+
issue.record()
117+
fatalError(
118+
"\(xctestWarningMessage) This is a fatal error because strict interop mode is active (\(Interop.Mode.interopModeEnvKey)=strict)",
119+
)
120+
}
47121
}
48122

49123
/// Get the best available source location to use when diagnosing an issue
@@ -54,7 +128,9 @@ extension Event {
54128
///
55129
/// - Returns: A source location to use when reporting an issue about
56130
/// `recordJSON`.
57-
private static func _bestAvailableSourceLocation(forInvalidRecordJSON recordJSON: UnsafeRawBufferPointer) -> SourceLocation {
131+
private static func _bestAvailableSourceLocation(
132+
forInvalidRecordJSON recordJSON: UnsafeRawBufferPointer
133+
) -> SourceLocation {
58134
// TODO: try to actually extract a source location from arbitrary JSON?
59135

60136
// If there's a test associated with the current task, it should have a
@@ -66,12 +142,13 @@ extension Event {
66142
return .unknown
67143
}
68144

69-
#if !SWT_NO_INTEROP
145+
#if !SWT_NO_INTEROP
70146
/// The fallback event handler to install when Swift Testing is the active
71147
/// testing library.
72148
private static let _ourFallbackEventHandler: SWTFallbackEventHandler = {
73149
recordJSONSchemaVersionNumber, recordJSONBaseAddress, recordJSONByteCount, _ in
74-
let version = String(validatingCString: recordJSONSchemaVersionNumber)
150+
let version =
151+
String(validatingCString: recordJSONSchemaVersionNumber)
75152
.flatMap(VersionNumber.init)
76153
.flatMap { ABI.version(forVersionNumber: $0) } ?? ABI.v0.self
77154
let recordJSON = UnsafeRawBufferPointer(
@@ -104,15 +181,15 @@ extension Event {
104181
).record()
105182
}
106183
}
107-
#endif
184+
#endif
108185

109186
/// The implementation of ``installFallbackEventHandler()``.
110187
private static let _installFallbackEventHandler: Bool = {
111-
#if !SWT_NO_INTEROP
112-
if Environment.flag(named: "SWT_EXPERIMENTAL_INTEROP_ENABLED") == true {
188+
#if !SWT_NO_INTEROP
189+
if Interop.Mode.current.requiresInstallation {
113190
return _swift_testing_installFallbackEventHandler(Self._ourFallbackEventHandler)
114191
}
115-
#endif
192+
#endif
116193
return false
117194
}()
118195

@@ -141,14 +218,14 @@ extension Event {
141218
/// currently-installed handler belongs to the testing library, returns
142219
/// `false`.
143220
borrowing func postToFallbackEventHandler(in context: borrowing Context) -> Bool {
144-
#if !SWT_NO_INTEROP
221+
#if !SWT_NO_INTEROP
145222
return Self._postToFallbackEventHandler?(self, context) != nil
146-
#else
223+
#else
147224
return false
148-
#endif
225+
#endif
149226
}
150227

151-
#if !SWT_NO_INTEROP
228+
#if !SWT_NO_INTEROP
152229
/// The implementation of ``postToFallbackEventHandler(in:)`` that actually
153230
/// invokes the installed fallback event handler.
154231
///
@@ -161,7 +238,8 @@ extension Event {
161238
}
162239

163240
let fallbackEventHandlerAddress = castCFunction(fallbackEventHandler, to: UnsafeRawPointer.self)
164-
let ourFallbackEventHandlerAddress = castCFunction(Self._ourFallbackEventHandler, to: UnsafeRawPointer.self)
241+
let ourFallbackEventHandlerAddress = castCFunction(
242+
Self._ourFallbackEventHandler, to: UnsafeRawPointer.self)
165243
if fallbackEventHandlerAddress == ourFallbackEventHandlerAddress {
166244
// The fallback event handler belongs to Swift Testing, so we don't want
167245
// to call it on our own behalf.
@@ -178,5 +256,5 @@ extension Event {
178256
)
179257
}
180258
}()
181-
#endif
259+
#endif
182260
}

0 commit comments

Comments
 (0)