-
Notifications
You must be signed in to change notification settings - Fork 145
Expand file tree
/
Copy pathEventHandlingInteropTests.swift
More file actions
233 lines (200 loc) · 9.49 KB
/
EventHandlingInteropTests.swift
File metadata and controls
233 lines (200 loc) · 9.49 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
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2026 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
//
@testable @_spi(ForToolsIntegrationOnly) import Testing
private import _TestingInternals.InteropOnly
#if canImport(Foundation)
import Foundation
#endif
#if !SWT_TARGET_OS_APPLE && canImport(Synchronization)
import Synchronization
#endif
#if SWT_TARGET_OS_APPLE
// Xcode already installs a handler, so the preconditions for this suite may not be met
let interopHandlerMayBeInstalled = Environment.variable(named: "XCTestSessionIdentifier") != nil
#else
let interopHandlerMayBeInstalled = false
#endif
#if !SWT_NO_EXIT_TESTS && !SWT_NO_INTEROP && canImport(Foundation)
@Suite(.disabled(if: interopHandlerMayBeInstalled))
struct EventHandlingInteropTests {
static let handlerContents = Mutex<(version: String, record: String?)?>()
private static let capturingHandler: SWTFallbackEventHandler = {
schemaVersion, recordJSONBaseAddress, recordJSONByteCount, _ in
let version = String(cString: schemaVersion)
let record = String(
data: Data(bytes: recordJSONBaseAddress, count: recordJSONByteCount),
encoding: .utf8)
Self.handlerContents.withLock {
$0 = (version: version, record: record)
}
}
/// Sets the env var that enables the experimental interop feature. Must be
/// set before we call `Event.installFallbackEventHandler()` which will cache
/// the install outcome.
static func enableExperimentalInterop() {
Environment.setVariable("1", named: "SWT_EXPERIMENTAL_INTEROP_ENABLED")
}
/// This uses an exit test to run in a clean process, ensuring that the
/// installed fallback event handler does not affect other tests.
///
/// Note this test will no longer work once Swift Testing starts installing
/// its own fallback handler.
@Test func `Post event without config -> fallback handler`() async throws {
await #expect(processExitsWith: .success) {
Configuration.removeAll()
try #require(
_swift_testing_installFallbackEventHandler(Self.capturingHandler),
"Installation of fallback handler should succeed")
// The detached task forces the event to be posted when Configuration.current
// is nil and triggers the post to fallback handler path
await Task.detached {
Event.post(.issueRecorded(Issue(kind: .system)), configuration: nil)
}.value
// Assert that the expectation failure contents were sent to the fallback event handler
try Self.handlerContents.withLock {
let contents = try #require(
$0, "Fallback should have been called with non nil contents")
#expect(contents.version == "\(ABI.CurrentVersion.versionNumber)")
#expect(contents.record?.contains("A system failure occurred") ?? false)
}
}
}
@Test func `Enabling experimental interop lets you install the handler`() async {
await #expect(processExitsWith: .success) {
// Experimental interop not set
let ok = Event.installFallbackEventHandler()
#expect(!ok, "Should fail because experimental interop not enabled")
}
await #expect(processExitsWith: .success) {
Self.enableExperimentalInterop()
let ok = Event.installFallbackEventHandler()
#expect(ok, "Should succeed because experimental interop is enabled")
}
}
@Test func `Running tests installs the fallback handler`() async {
await #expect(processExitsWith: .success) {
Self.enableExperimentalInterop()
let handlerBefore = _swift_testing_getFallbackEventHandler()
await Test {}.run()
let handlerAfter = _swift_testing_getFallbackEventHandler()
#expect(handlerBefore == nil, "There should be no handler before running the test")
#expect(handlerAfter != nil, "There should be a handler after running the test")
}
}
private static let unusableHandler: SWTFallbackEventHandler = { _, _, _, _ in
fatalError("The fallback event handler should NOT have been called!")
}
/// Regression testing for a bug where we incorrectly directed issues to
/// fallback event path for issues recorded without an associated configuration
@Test(.bug("rdar://170161483"), .filterIssues { !$0.description.contains("[FILTER OUT]") })
func `Recording issue in detached task doesn't forward to fallback event handler`() async {
await #expect(processExitsWith: .success) {
// Install a handler that shouldn't ever get called.
try #require(
_swift_testing_installFallbackEventHandler(Self.unusableHandler),
"Installation of fallback handler should succeed")
// Record an issue in a detached task, which should be forwarded to Configuration.all
// and NOT the installed fallback event handler.
_ = await Task.detached {
Issue.record("[FILTER OUT] This issue was recorded in a detached task", severity: .warning)
}.value
// If this recurses infinitely, the process will likely exhaust the stack space and crash here.
}
}
@Test func `Sending fallback event to ourselves doesn't cause infinite loop`() async {
await #expect(processExitsWith: .success) {
Self.enableExperimentalInterop()
try #require(Event.installFallbackEventHandler(), "Should successfully install a handler")
// Force the event to be handled by the fallback event handler
Configuration.removeAll()
try await Task.detached {
try #require(
Configuration.current == nil,
"There should be no current config so that we trigger the fallback path")
Event.post(.issueRecorded(Issue(kind: .system)), configuration: nil)
}.value
// If this recurses infinitely, the process will likely exhaust the stack space and crash here.
}
}
@Test func `Fallback handler records an issue if invalid event provided`() async throws {
await #expect(processExitsWith: .success) {
// Install and retrieve the fallback event handler.
// Capture all observed issues when running tests.
Self.enableExperimentalInterop()
try #require(Event.installFallbackEventHandler(), "Should successfully install a new handler")
let currentHandler = try #require(
_swift_testing_getFallbackEventHandler(), "Should successfully retrieve installed handler")
let issues = Mutex<[Issue]>()
var configuration = Configuration()
configuration.eventHandler = { event, _ in
if case .issueRecorded(let issue) = event.kind {
issues.withLock { $0.append(issue) }
}
}
// Pass an invalid record JSON to the event handler
struct Empty: Encodable {}
await Test {
try JSONEncoder().encode(Empty()).withUnsafeBytes { ptr in
let vers = String(describing: ABI.CurrentVersion.versionNumber)
currentHandler(vers, ptr.baseAddress!, ptr.count, nil)
}
}.run(configuration: configuration)
// Assert that we record an issue with a helpful debug message
let expectedPrefix =
"A system failure occurred (error): 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."
let actualMessages = issues.rawValue.map { $0.description }
#expect(actualMessages.count == 1)
#expect(
actualMessages.first?.hasPrefix(expectedPrefix) == true,
"\(actualMessages) did not match the expected message")
}
}
@Test func `Handle fallback event warns issue about XCTest API usage`() async throws {
await #expect(processExitsWith: .success) {
// Install and retrieve the fallback event handler.
// Prepare a test issue to inject into that handler to simulate receiving an interop issue.
let eventJSON = try {
let issue = Issue(kind: .unconditional)
let event = Event(.issueRecorded(issue), testID: nil, testCaseID: nil, instant: .now)
let encodedEvent = ABI.Record<ABI.CurrentVersion>(
encoding: event, in: .init(test: nil, testCase: nil, iteration: nil, configuration: nil),
messages: [])
return try JSONEncoder().encode(encodedEvent)
}()
Self.enableExperimentalInterop()
try #require(Event.installFallbackEventHandler(), "Should successfully install a new handler")
let currentHandler = try #require(
_swift_testing_getFallbackEventHandler(), "Should successfully retrieved installed handler")
// Test configuration records all issues actually reported by Testing as a
// result of the interop issue.
let issues = Mutex<[Issue]>()
var configuration = Configuration()
configuration.eventHandler = { event, _ in
if case .issueRecorded(let issue) = event.kind {
issues.withLock { $0.append(issue) }
}
}
// Run the test, which should record two issues in response to the interop one
await Test {
eventJSON.withUnsafeBytes { ptr in
let vers = String(describing: ABI.CurrentVersion.versionNumber)
currentHandler(vers, ptr.baseAddress!, ptr.count, nil)
}
}.run(configuration: configuration)
#expect(
issues.rawValue.map { $0.description }.sorted() == [
"An API was misused (warning): XCTest API was used in a Swift Testing test. Adopt Swift Testing primitives, such as #expect, instead.",
"Issue recorded (error)",
]
)
}
}
}
#endif