Skip to content

Commit cca6de2

Browse files
authored
Add the current configuration to Event.Context. (#677)
This PR adds a `var configuration: Configuration` property to `Event.Context` and adds a `verbosity` property to `Configuration`. We solve two problems: 1. An event handler cannot see the current configuration (without querying `Configuration.current`) which limits its ability to customize its behaviour; and 2. The verbosity of human-readable output is tracked by individual event handlers when it is intended to be part of a test run's configuration. Since the configuration contains the event handler to which we pass the event context, the `eventHandler` property of the context is cleared before the event handler is called (thus breaking any reference cycles.) Callers of the internal `Event.post()` function are now required to either pass _both_ the test and test case for the event, or to pass neither (in which case we now gather both from the runtime state in a single call rather than hitting the task-local twice.) This is effected with a tuple as an argument to `Event.post()` which is admittedly a bit wonky but does force our call sites to explicitly pass values for both the test and test case, where previously we could accidentally pass a specific test and then go look up the task-local current test case (which should have been `nil` in all the cases where we were doing this anyway.) A couple of interfaces on the event recorder types have changed in this PR in source-breaking ways. The old interfaces remain present but deprecated; they will be removed later in the Swift 6.1 development cycle after tools authors have had time to migrate. ### 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 9fbfe54 commit cca6de2

11 files changed

+94
-59
lines changed

Sources/Testing/ABI/EntryPoints/EntryPoint.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,12 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
4747
}
4848
oldEventHandler(event, context)
4949
}
50+
configuration.verbosity = args.verbosity
5051

5152
#if !SWT_NO_FILE_IO
5253
// Configure the event recorder to write events to stderr.
5354
var options = Event.ConsoleOutputRecorder.Options()
5455
options = .for(.stderr)
55-
options.verbosity = args.verbosity
5656
let eventRecorder = Event.ConsoleOutputRecorder(options: options) { string in
5757
try? FileHandle.stderr.write(string)
5858
}
@@ -91,7 +91,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
9191
// Post an event for every discovered test. These events are turned into
9292
// JSON objects if JSON output is enabled.
9393
for test in tests {
94-
Event.post(.testDiscovered, for: test, testCase: nil, configuration: configuration)
94+
Event.post(.testDiscovered, for: (test, nil), configuration: configuration)
9595
}
9696
} else {
9797
// Run the tests.

Sources/Testing/Events/Event.swift

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,24 +186,28 @@ public struct Event: Sendable {
186186
///
187187
/// - Parameters:
188188
/// - kind: The kind of event that occurred.
189-
/// - test: The test for which the event occurred, if any.
190-
/// - testCase: The test case for which the event occurred, if any.
189+
/// - testAndTestCase: The test and test case for which the event occurred,
190+
/// if any. The default value of this argument is ``Test/current`` and
191+
/// ``Test/Case/current``.
191192
/// - instant: The instant at which the event occurred. The default value
192193
/// of this argument is `.now`.
193194
/// - configuration: The configuration whose event handler should handle
194195
/// this event. If `nil` is passed, the current task's configuration is
195196
/// used, if known.
196197
static func post(
197198
_ kind: Kind,
198-
for test: Test? = .current,
199-
testCase: Test.Case? = .current,
199+
for testAndTestCase: (Test?, Test.Case?) = currentTestAndTestCase(),
200200
instant: Test.Clock.Instant = .now,
201201
configuration: Configuration? = nil
202202
) {
203203
// Create both the event and its associated context here at same point, to
204-
// ensure their task local-derived values are the same.
204+
// ensure their task local-derived values are the same. Note we set the
205+
// configuration property of Event.Context to nil initially because we'll
206+
// reset it to the actual configuration that handles the event when we call
207+
// handleEvent() later, so there's no need to make a copy of it yet.
208+
let (test, testCase) = testAndTestCase
205209
let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant)
206-
let context = Event.Context(test: test, testCase: testCase)
210+
let context = Event.Context(test: test, testCase: testCase, configuration: nil)
207211
event._post(in: context, configuration: configuration)
208212
}
209213
}
@@ -239,16 +243,24 @@ extension Event {
239243
/// functions), the value of this property is `nil`.
240244
public var testCase: Test.Case?
241245

246+
/// The configuration handling the corresponding event, if any.
247+
///
248+
/// The value of this property is a copy of the configuration that owns the
249+
/// currently-running event handler; to avoid reference cycles, the
250+
/// ``Configuration/eventHandler`` property of this instance is cleared.
251+
public var configuration: Configuration?
252+
242253
/// Initialize a new instance of this type.
243254
///
244255
/// - Parameters:
245256
/// - test: The test for which this instance's associated event occurred,
246257
/// if any.
247258
/// - testCase: The test case for which this instance's associated event
248259
/// occurred, if any.
249-
init(test: Test? = .current, testCase: Test.Case? = .current) {
260+
init(test: Test?, testCase: Test.Case?, configuration: Configuration?) {
250261
self.test = test
251262
self.testCase = testCase
263+
self.configuration = configuration
252264
}
253265
}
254266

Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,6 @@ extension Event {
6161
public var useSFSymbols = false
6262
#endif
6363

64-
/// The level of verbosity of the output.
65-
///
66-
/// When the value of this property is greater than `0`, additional output
67-
/// is provided. When the value of this property is less than `0`, some
68-
/// output is suppressed. The exact effects of this property are
69-
/// implementation-defined and subject to change.
70-
public var verbosity = 0
71-
7264
/// Storage for ``tagColors``.
7365
private var _tagColors = Tag.Color.predefined
7466

@@ -309,7 +301,7 @@ extension Event.ConsoleOutputRecorder {
309301
/// - Returns: Whether any output was produced and written to this instance's
310302
/// destination.
311303
@discardableResult public func record(_ event: borrowing Event, in context: borrowing Event.Context) -> Bool {
312-
let messages = _humanReadableOutputRecorder.record(event, in: context, verbosity: options.verbosity)
304+
let messages = _humanReadableOutputRecorder.record(event, in: context)
313305
for message in messages {
314306
let symbol = message.symbol?.stringValue(options: options) ?? " "
315307

@@ -342,3 +334,13 @@ extension Event.ConsoleOutputRecorder {
342334
return "\(symbol) \(message)\n"
343335
}
344336
}
337+
338+
// MARK: - Deprecated
339+
340+
extension Event.ConsoleOutputRecorder.Options {
341+
@available(*, deprecated, message: "Set Configuration.verbosity instead.")
342+
public var verbosity: Int {
343+
get { 0 }
344+
set {}
345+
}
346+
}

Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -202,19 +202,11 @@ extension Event.HumanReadableOutputRecorder {
202202
/// - Parameters:
203203
/// - event: The event to record.
204204
/// - eventContext: The context associated with the event.
205-
/// - verbosity: How verbose output should be. When the value of this
206-
/// argument is greater than `0`, additional output is provided. When the
207-
/// value of this argument is less than `0`, some output is suppressed.
208-
/// The exact effects of this argument are implementation-defined and
209-
/// subject to change.
210205
///
211206
/// - Returns: An array of zero or more messages that can be displayed to the
212207
/// user.
213-
@discardableResult public func record(
214-
_ event: borrowing Event,
215-
in eventContext: borrowing Event.Context,
216-
verbosity: Int = 0
217-
) -> [Message] {
208+
@discardableResult public func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) -> [Message] {
209+
let verbosity = eventContext.configuration?.verbosity ?? 0
218210
let test = eventContext.test
219211
let testName = if let test {
220212
if let displayName = test.displayName {
@@ -230,7 +222,7 @@ extension Event.HumanReadableOutputRecorder {
230222
"«unknown»"
231223
}
232224
let instant = event.instant
233-
let iterationCount = Configuration.current?.repetitionPolicy.maximumIterationCount
225+
let iterationCount = eventContext.configuration?.repetitionPolicy.maximumIterationCount
234226

235227
// First, make any updates to the context/state associated with this
236228
// recorder.
@@ -509,3 +501,12 @@ extension Event.HumanReadableOutputRecorder {
509501
// MARK: - Codable
510502

511503
extension Event.HumanReadableOutputRecorder.Message: Codable {}
504+
505+
// MARK: - Deprecated
506+
507+
extension Event.HumanReadableOutputRecorder {
508+
@available(*, deprecated, message: "Use record(_:in:) instead. Verbosity is now controlled by eventContext.configuration.verbosity.")
509+
@discardableResult public func record(_ event: borrowing Event, in eventContext: borrowing Event.Context, verbosity: Int) -> [Message] {
510+
record(event, in: eventContext)
511+
}
512+
}

Sources/Testing/Running/Configuration+EventHandling.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ extension Configuration {
2020
/// `eventHandler` but this method may also be used as a customization point
2121
/// to change how the event is passed to the event handler.
2222
func handleEvent(_ event: borrowing Event, in context: borrowing Event.Context) {
23-
eventHandler(event, context)
23+
var contextCopy = copy context
24+
contextCopy.configuration = self
25+
contextCopy.configuration?.eventHandler = { _, _ in }
26+
eventHandler(event, contextCopy)
2427
}
2528
}

Sources/Testing/Running/Configuration.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,14 @@ public struct Configuration: Sendable {
195195
}
196196
#endif
197197

198+
/// How verbose human-readable output should be.
199+
///
200+
/// When the value of this property is greater than `0`, additional output
201+
/// is provided. When the value of this property is less than `0`, some
202+
/// output is suppressed. The exact effects of this property are determined by
203+
/// the instance's event handler.
204+
public var verbosity = 0
205+
198206
// MARK: - Test selection
199207

200208
/// The test filter to which tests should be filtered when run.

Sources/Testing/Running/Runner.RuntimeState.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,16 @@ extension Test.Case {
222222
return try await Runner.RuntimeState.$current.withValue(runtimeState, operation: body)
223223
}
224224
}
225+
226+
/// Get the current test and test case in a single operation.
227+
///
228+
/// - Returns: The current test and test case.
229+
///
230+
/// This function is more efficient than calling both ``Test/current`` and
231+
/// ``Test/Case/current``.
232+
func currentTestAndTestCase() -> (Test?, Test.Case?) {
233+
guard let state = Runner.RuntimeState.current else {
234+
return (nil, nil)
235+
}
236+
return (state.test, state.testCase)
237+
}

Sources/Testing/Running/Runner.swift

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -171,18 +171,18 @@ extension Runner {
171171

172172
// Determine what action to take for this step.
173173
if let step = stepGraph.value {
174-
Event.post(.planStepStarted(step), for: step.test, configuration: configuration)
174+
Event.post(.planStepStarted(step), for: (step.test, nil), configuration: configuration)
175175

176176
// Determine what kind of event to send for this step based on its action.
177177
switch step.action {
178178
case .run:
179-
Event.post(.testStarted, for: step.test, configuration: configuration)
179+
Event.post(.testStarted, for: (step.test, nil), configuration: configuration)
180180
shouldSendTestEnded = true
181181
case let .skip(skipInfo):
182-
Event.post(.testSkipped(skipInfo), for: step.test, configuration: configuration)
182+
Event.post(.testSkipped(skipInfo), for: (step.test, nil), configuration: configuration)
183183
shouldSendTestEnded = false
184184
case let .recordIssue(issue):
185-
Event.post(.issueRecorded(issue), for: step.test, configuration: configuration)
185+
Event.post(.issueRecorded(issue), for: (step.test, nil), configuration: configuration)
186186
shouldSendTestEnded = false
187187
}
188188
} else {
@@ -191,9 +191,9 @@ extension Runner {
191191
defer {
192192
if let step = stepGraph.value {
193193
if shouldSendTestEnded {
194-
Event.post(.testEnded, for: step.test, configuration: configuration)
194+
Event.post(.testEnded, for: (step.test, nil), configuration: configuration)
195195
}
196-
Event.post(.planStepEnded(step), for: step.test, configuration: configuration)
196+
Event.post(.planStepEnded(step), for: (step.test, nil), configuration: configuration)
197197
}
198198
}
199199

@@ -327,9 +327,9 @@ extension Runner {
327327
// Exit early if the task has already been cancelled.
328328
try Task.checkCancellation()
329329

330-
Event.post(.testCaseStarted, for: step.test, testCase: testCase, configuration: configuration)
330+
Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration)
331331
defer {
332-
Event.post(.testCaseEnded, for: step.test, testCase: testCase, configuration: configuration)
332+
Event.post(.testCaseEnded, for: (step.test, testCase), configuration: configuration)
333333
}
334334

335335
await Test.Case.withCurrent(testCase) {
@@ -386,19 +386,19 @@ extension Runner {
386386
// Post an event for every test in the test plan being run. These events
387387
// are turned into JSON objects if JSON output is enabled.
388388
for test in runner.plan.steps.lazy.map(\.test) {
389-
Event.post(.testDiscovered, for: test, testCase: nil, configuration: runner.configuration)
389+
Event.post(.testDiscovered, for: (test, nil), configuration: runner.configuration)
390390
}
391391

392-
Event.post(.runStarted, for: nil, testCase: nil, configuration: runner.configuration)
392+
Event.post(.runStarted, for: (nil, nil), configuration: runner.configuration)
393393
defer {
394-
Event.post(.runEnded, for: nil, testCase: nil, configuration: runner.configuration)
394+
Event.post(.runEnded, for: (nil, nil), configuration: runner.configuration)
395395
}
396396

397397
let repetitionPolicy = runner.configuration.repetitionPolicy
398398
for iterationIndex in 0 ..< repetitionPolicy.maximumIterationCount {
399-
Event.post(.iterationStarted(iterationIndex), for: nil, testCase: nil, configuration: runner.configuration)
399+
Event.post(.iterationStarted(iterationIndex), for: (nil, nil), configuration: runner.configuration)
400400
defer {
401-
Event.post(.iterationEnded(iterationIndex), for: nil, testCase: nil, configuration: runner.configuration)
401+
Event.post(.iterationEnded(iterationIndex), for: (nil, nil), configuration: runner.configuration)
402402
}
403403

404404
await withTaskGroup(of: Void.self) { [runner] taskGroup in

Tests/TestingTests/EventRecorderTests.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,13 @@ struct EventRecorderTests {
9797
func verboseOutput() async throws {
9898
let stream = Stream()
9999

100-
var options = Event.ConsoleOutputRecorder.Options()
101-
options.verbosity = 1
102-
103100
var configuration = Configuration()
104101
configuration.deliverExpectationCheckedEvents = true
105-
let eventRecorder = Event.ConsoleOutputRecorder(options: options, writingUsing: stream.write)
102+
let eventRecorder = Event.ConsoleOutputRecorder(writingUsing: stream.write)
106103
configuration.eventHandler = { event, context in
107104
eventRecorder.record(event, in: context)
108105
}
106+
configuration.verbosity = 1
109107

110108
await runTest(for: WrittenTests.self, configuration: configuration)
111109

@@ -124,15 +122,13 @@ struct EventRecorderTests {
124122
func quietOutput() async throws {
125123
let stream = Stream()
126124

127-
var options = Event.ConsoleOutputRecorder.Options()
128-
options.verbosity = -1
129-
130125
var configuration = Configuration()
131126
configuration.deliverExpectationCheckedEvents = true
132-
let eventRecorder = Event.ConsoleOutputRecorder(options: options, writingUsing: stream.write)
127+
let eventRecorder = Event.ConsoleOutputRecorder(writingUsing: stream.write)
133128
configuration.eventHandler = { event, context in
134129
eventRecorder.record(event, in: context)
135130
}
131+
configuration.verbosity = -1
136132

137133
await runTest(for: WrittenTests.self, configuration: configuration)
138134

@@ -364,7 +360,7 @@ struct EventRecorderTests {
364360
func humanReadableRecorderCountsIssuesWithoutTests() {
365361
let issue = Issue(kind: .unconditional, comments: [], sourceContext: .init())
366362
let event = Event(.issueRecorded(issue), testID: nil, testCaseID: nil)
367-
let context = Event.Context(test: nil, testCase: nil)
363+
let context = Event.Context(test: nil, testCase: nil, configuration: nil)
368364

369365
let recorder = Event.HumanReadableOutputRecorder()
370366
let messages = recorder.record(event, in: context)
@@ -379,7 +375,7 @@ struct EventRecorderTests {
379375
func junitRecorderCountsIssuesWithoutTests() async throws {
380376
let issue = Issue(kind: .unconditional, comments: [], sourceContext: .init())
381377
let event = Event(.issueRecorded(issue), testID: nil, testCaseID: nil)
382-
let context = Event.Context(test: nil, testCase: nil)
378+
let context = Event.Context(test: nil, testCase: nil, configuration: nil)
383379

384380
let recorder = Event.JUnitXMLRecorder { string in
385381
if string.contains("<testsuite") {

Tests/TestingTests/EventTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ struct EventTests {
6767

6868
@Test("Event.Contexts's Codable Conformances")
6969
func codable() async throws {
70-
let eventContext = Event.Context()
70+
let eventContext = Event.Context(test: .current, testCase: .current, configuration: .current)
7171
let snapshot = Event.Context.Snapshot(snapshotting: eventContext)
7272

7373
let decoded = try JSON.encodeAndDecode(snapshot)

Tests/TestingTests/SwiftPMTests.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ struct SwiftPMTests {
167167
}
168168
do {
169169
let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--xunit-output", temporaryFilePath])
170-
let eventContext = Event.Context()
171-
configuration.eventHandler(Event(.runStarted, testID: nil, testCaseID: nil), eventContext)
172-
configuration.eventHandler(Event(.runEnded, testID: nil, testCaseID: nil), eventContext)
170+
let eventContext = Event.Context(test: nil, testCase: nil, configuration: nil)
171+
configuration.handleEvent(Event(.runStarted, testID: nil, testCaseID: nil), in: eventContext)
172+
configuration.handleEvent(Event(.runEnded, testID: nil, testCaseID: nil), in: eventContext)
173173
}
174174

175175
let fileHandle = try FileHandle(forReadingAtPath: temporaryFilePath)
@@ -236,12 +236,12 @@ struct SwiftPMTests {
236236
do {
237237
let configuration = try configurationForEntryPoint(withArguments: ["PATH", outputArgumentName, temporaryFilePath, versionArgumentName, version])
238238
let test = Test {}
239-
let eventContext = Event.Context(test: test)
239+
let eventContext = Event.Context(test: test, testCase: nil, configuration: nil)
240240

241241
configuration.handleEvent(Event(.testDiscovered, testID: test.id, testCaseID: nil), in: eventContext)
242242
configuration.handleEvent(Event(.runStarted, testID: nil, testCaseID: nil), in: eventContext)
243243
do {
244-
let eventContext = Event.Context(test: test)
244+
let eventContext = Event.Context(test: test, testCase: nil, configuration: nil)
245245
configuration.handleEvent(Event(.testStarted, testID: test.id, testCaseID: nil), in: eventContext)
246246
configuration.handleEvent(Event(.testEnded, testID: test.id, testCaseID: nil), in: eventContext)
247247
}

0 commit comments

Comments
 (0)