diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 46084a1e0..8e0416ba0 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -47,12 +47,12 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha } oldEventHandler(event, context) } + configuration.verbosity = args.verbosity #if !SWT_NO_FILE_IO // Configure the event recorder to write events to stderr. var options = Event.ConsoleOutputRecorder.Options() options = .for(.stderr) - options.verbosity = args.verbosity let eventRecorder = Event.ConsoleOutputRecorder(options: options) { string in try? FileHandle.stderr.write(string) } @@ -91,7 +91,7 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha // Post an event for every discovered test. These events are turned into // JSON objects if JSON output is enabled. for test in tests { - Event.post(.testDiscovered, for: test, testCase: nil, configuration: configuration) + Event.post(.testDiscovered, for: (test, nil), configuration: configuration) } } else { // Run the tests. diff --git a/Sources/Testing/Events/Event.swift b/Sources/Testing/Events/Event.swift index e58083bcd..ffd55b5dd 100644 --- a/Sources/Testing/Events/Event.swift +++ b/Sources/Testing/Events/Event.swift @@ -186,8 +186,9 @@ public struct Event: Sendable { /// /// - Parameters: /// - kind: The kind of event that occurred. - /// - test: The test for which the event occurred, if any. - /// - testCase: The test case for which the event occurred, if any. + /// - testAndTestCase: The test and test case for which the event occurred, + /// if any. The default value of this argument is ``Test/current`` and + /// ``Test/Case/current``. /// - instant: The instant at which the event occurred. The default value /// of this argument is `.now`. /// - configuration: The configuration whose event handler should handle @@ -195,15 +196,18 @@ public struct Event: Sendable { /// used, if known. static func post( _ kind: Kind, - for test: Test? = .current, - testCase: Test.Case? = .current, + for testAndTestCase: (Test?, Test.Case?) = currentTestAndTestCase(), instant: Test.Clock.Instant = .now, configuration: Configuration? = nil ) { // Create both the event and its associated context here at same point, to - // ensure their task local-derived values are the same. + // ensure their task local-derived values are the same. Note we set the + // configuration property of Event.Context to nil initially because we'll + // reset it to the actual configuration that handles the event when we call + // handleEvent() later, so there's no need to make a copy of it yet. + let (test, testCase) = testAndTestCase let event = Event(kind, testID: test?.id, testCaseID: testCase?.id, instant: instant) - let context = Event.Context(test: test, testCase: testCase) + let context = Event.Context(test: test, testCase: testCase, configuration: nil) event._post(in: context, configuration: configuration) } } @@ -239,6 +243,13 @@ extension Event { /// functions), the value of this property is `nil`. public var testCase: Test.Case? + /// The configuration handling the corresponding event, if any. + /// + /// The value of this property is a copy of the configuration that owns the + /// currently-running event handler; to avoid reference cycles, the + /// ``Configuration/eventHandler`` property of this instance is cleared. + public var configuration: Configuration? + /// Initialize a new instance of this type. /// /// - Parameters: @@ -246,9 +257,10 @@ extension Event { /// if any. /// - testCase: The test case for which this instance's associated event /// occurred, if any. - init(test: Test? = .current, testCase: Test.Case? = .current) { + init(test: Test?, testCase: Test.Case?, configuration: Configuration?) { self.test = test self.testCase = testCase + self.configuration = configuration } } diff --git a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift index 53eb27d47..1f44de23a 100644 --- a/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.ConsoleOutputRecorder.swift @@ -61,14 +61,6 @@ extension Event { public var useSFSymbols = false #endif - /// The level of verbosity of the output. - /// - /// When the value of this property is greater than `0`, additional output - /// is provided. When the value of this property is less than `0`, some - /// output is suppressed. The exact effects of this property are - /// implementation-defined and subject to change. - public var verbosity = 0 - /// Storage for ``tagColors``. private var _tagColors = Tag.Color.predefined @@ -309,7 +301,7 @@ extension Event.ConsoleOutputRecorder { /// - Returns: Whether any output was produced and written to this instance's /// destination. @discardableResult public func record(_ event: borrowing Event, in context: borrowing Event.Context) -> Bool { - let messages = _humanReadableOutputRecorder.record(event, in: context, verbosity: options.verbosity) + let messages = _humanReadableOutputRecorder.record(event, in: context) for message in messages { let symbol = message.symbol?.stringValue(options: options) ?? " " @@ -342,3 +334,13 @@ extension Event.ConsoleOutputRecorder { return "\(symbol) \(message)\n" } } + +// MARK: - Deprecated + +extension Event.ConsoleOutputRecorder.Options { + @available(*, deprecated, message: "Set Configuration.verbosity instead.") + public var verbosity: Int { + get { 0 } + set {} + } +} diff --git a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift index cfc2c396e..833913cde 100644 --- a/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift +++ b/Sources/Testing/Events/Recorder/Event.HumanReadableOutputRecorder.swift @@ -202,19 +202,11 @@ extension Event.HumanReadableOutputRecorder { /// - Parameters: /// - event: The event to record. /// - eventContext: The context associated with the event. - /// - verbosity: How verbose output should be. When the value of this - /// argument is greater than `0`, additional output is provided. When the - /// value of this argument is less than `0`, some output is suppressed. - /// The exact effects of this argument are implementation-defined and - /// subject to change. /// /// - Returns: An array of zero or more messages that can be displayed to the /// user. - @discardableResult public func record( - _ event: borrowing Event, - in eventContext: borrowing Event.Context, - verbosity: Int = 0 - ) -> [Message] { + @discardableResult public func record(_ event: borrowing Event, in eventContext: borrowing Event.Context) -> [Message] { + let verbosity = eventContext.configuration?.verbosity ?? 0 let test = eventContext.test let testName = if let test { if let displayName = test.displayName { @@ -230,7 +222,7 @@ extension Event.HumanReadableOutputRecorder { "«unknown»" } let instant = event.instant - let iterationCount = Configuration.current?.repetitionPolicy.maximumIterationCount + let iterationCount = eventContext.configuration?.repetitionPolicy.maximumIterationCount // First, make any updates to the context/state associated with this // recorder. @@ -509,3 +501,12 @@ extension Event.HumanReadableOutputRecorder { // MARK: - Codable extension Event.HumanReadableOutputRecorder.Message: Codable {} + +// MARK: - Deprecated + +extension Event.HumanReadableOutputRecorder { + @available(*, deprecated, message: "Use record(_:in:) instead. Verbosity is now controlled by eventContext.configuration.verbosity.") + @discardableResult public func record(_ event: borrowing Event, in eventContext: borrowing Event.Context, verbosity: Int) -> [Message] { + record(event, in: eventContext) + } +} diff --git a/Sources/Testing/Running/Configuration+EventHandling.swift b/Sources/Testing/Running/Configuration+EventHandling.swift index 643676df0..95febe085 100644 --- a/Sources/Testing/Running/Configuration+EventHandling.swift +++ b/Sources/Testing/Running/Configuration+EventHandling.swift @@ -20,6 +20,9 @@ extension Configuration { /// `eventHandler` but this method may also be used as a customization point /// to change how the event is passed to the event handler. func handleEvent(_ event: borrowing Event, in context: borrowing Event.Context) { - eventHandler(event, context) + var contextCopy = copy context + contextCopy.configuration = self + contextCopy.configuration?.eventHandler = { _, _ in } + eventHandler(event, contextCopy) } } diff --git a/Sources/Testing/Running/Configuration.swift b/Sources/Testing/Running/Configuration.swift index 2c12fed3b..80cd65a49 100644 --- a/Sources/Testing/Running/Configuration.swift +++ b/Sources/Testing/Running/Configuration.swift @@ -195,6 +195,14 @@ public struct Configuration: Sendable { } #endif + /// How verbose human-readable output should be. + /// + /// When the value of this property is greater than `0`, additional output + /// is provided. When the value of this property is less than `0`, some + /// output is suppressed. The exact effects of this property are determined by + /// the instance's event handler. + public var verbosity = 0 + // MARK: - Test selection /// The test filter to which tests should be filtered when run. diff --git a/Sources/Testing/Running/Runner.RuntimeState.swift b/Sources/Testing/Running/Runner.RuntimeState.swift index 919b2b6d2..a128d9ce9 100644 --- a/Sources/Testing/Running/Runner.RuntimeState.swift +++ b/Sources/Testing/Running/Runner.RuntimeState.swift @@ -222,3 +222,16 @@ extension Test.Case { return try await Runner.RuntimeState.$current.withValue(runtimeState, operation: body) } } + +/// Get the current test and test case in a single operation. +/// +/// - Returns: The current test and test case. +/// +/// This function is more efficient than calling both ``Test/current`` and +/// ``Test/Case/current``. +func currentTestAndTestCase() -> (Test?, Test.Case?) { + guard let state = Runner.RuntimeState.current else { + return (nil, nil) + } + return (state.test, state.testCase) +} diff --git a/Sources/Testing/Running/Runner.swift b/Sources/Testing/Running/Runner.swift index 554f9dac3..7642ad373 100644 --- a/Sources/Testing/Running/Runner.swift +++ b/Sources/Testing/Running/Runner.swift @@ -171,18 +171,18 @@ extension Runner { // Determine what action to take for this step. if let step = stepGraph.value { - Event.post(.planStepStarted(step), for: step.test, configuration: configuration) + Event.post(.planStepStarted(step), for: (step.test, nil), configuration: configuration) // Determine what kind of event to send for this step based on its action. switch step.action { case .run: - Event.post(.testStarted, for: step.test, configuration: configuration) + Event.post(.testStarted, for: (step.test, nil), configuration: configuration) shouldSendTestEnded = true case let .skip(skipInfo): - Event.post(.testSkipped(skipInfo), for: step.test, configuration: configuration) + Event.post(.testSkipped(skipInfo), for: (step.test, nil), configuration: configuration) shouldSendTestEnded = false case let .recordIssue(issue): - Event.post(.issueRecorded(issue), for: step.test, configuration: configuration) + Event.post(.issueRecorded(issue), for: (step.test, nil), configuration: configuration) shouldSendTestEnded = false } } else { @@ -191,9 +191,9 @@ extension Runner { defer { if let step = stepGraph.value { if shouldSendTestEnded { - Event.post(.testEnded, for: step.test, configuration: configuration) + Event.post(.testEnded, for: (step.test, nil), configuration: configuration) } - Event.post(.planStepEnded(step), for: step.test, configuration: configuration) + Event.post(.planStepEnded(step), for: (step.test, nil), configuration: configuration) } } @@ -327,9 +327,9 @@ extension Runner { // Exit early if the task has already been cancelled. try Task.checkCancellation() - Event.post(.testCaseStarted, for: step.test, testCase: testCase, configuration: configuration) + Event.post(.testCaseStarted, for: (step.test, testCase), configuration: configuration) defer { - Event.post(.testCaseEnded, for: step.test, testCase: testCase, configuration: configuration) + Event.post(.testCaseEnded, for: (step.test, testCase), configuration: configuration) } await Test.Case.withCurrent(testCase) { @@ -386,19 +386,19 @@ extension Runner { // Post an event for every test in the test plan being run. These events // are turned into JSON objects if JSON output is enabled. for test in runner.plan.steps.lazy.map(\.test) { - Event.post(.testDiscovered, for: test, testCase: nil, configuration: runner.configuration) + Event.post(.testDiscovered, for: (test, nil), configuration: runner.configuration) } - Event.post(.runStarted, for: nil, testCase: nil, configuration: runner.configuration) + Event.post(.runStarted, for: (nil, nil), configuration: runner.configuration) defer { - Event.post(.runEnded, for: nil, testCase: nil, configuration: runner.configuration) + Event.post(.runEnded, for: (nil, nil), configuration: runner.configuration) } let repetitionPolicy = runner.configuration.repetitionPolicy for iterationIndex in 0 ..< repetitionPolicy.maximumIterationCount { - Event.post(.iterationStarted(iterationIndex), for: nil, testCase: nil, configuration: runner.configuration) + Event.post(.iterationStarted(iterationIndex), for: (nil, nil), configuration: runner.configuration) defer { - Event.post(.iterationEnded(iterationIndex), for: nil, testCase: nil, configuration: runner.configuration) + Event.post(.iterationEnded(iterationIndex), for: (nil, nil), configuration: runner.configuration) } await withTaskGroup(of: Void.self) { [runner] taskGroup in diff --git a/Tests/TestingTests/EventRecorderTests.swift b/Tests/TestingTests/EventRecorderTests.swift index 7a7b7569e..a6e284a84 100644 --- a/Tests/TestingTests/EventRecorderTests.swift +++ b/Tests/TestingTests/EventRecorderTests.swift @@ -97,15 +97,13 @@ struct EventRecorderTests { func verboseOutput() async throws { let stream = Stream() - var options = Event.ConsoleOutputRecorder.Options() - options.verbosity = 1 - var configuration = Configuration() configuration.deliverExpectationCheckedEvents = true - let eventRecorder = Event.ConsoleOutputRecorder(options: options, writingUsing: stream.write) + let eventRecorder = Event.ConsoleOutputRecorder(writingUsing: stream.write) configuration.eventHandler = { event, context in eventRecorder.record(event, in: context) } + configuration.verbosity = 1 await runTest(for: WrittenTests.self, configuration: configuration) @@ -124,15 +122,13 @@ struct EventRecorderTests { func quietOutput() async throws { let stream = Stream() - var options = Event.ConsoleOutputRecorder.Options() - options.verbosity = -1 - var configuration = Configuration() configuration.deliverExpectationCheckedEvents = true - let eventRecorder = Event.ConsoleOutputRecorder(options: options, writingUsing: stream.write) + let eventRecorder = Event.ConsoleOutputRecorder(writingUsing: stream.write) configuration.eventHandler = { event, context in eventRecorder.record(event, in: context) } + configuration.verbosity = -1 await runTest(for: WrittenTests.self, configuration: configuration) @@ -364,7 +360,7 @@ struct EventRecorderTests { func humanReadableRecorderCountsIssuesWithoutTests() { let issue = Issue(kind: .unconditional, comments: [], sourceContext: .init()) let event = Event(.issueRecorded(issue), testID: nil, testCaseID: nil) - let context = Event.Context(test: nil, testCase: nil) + let context = Event.Context(test: nil, testCase: nil, configuration: nil) let recorder = Event.HumanReadableOutputRecorder() let messages = recorder.record(event, in: context) @@ -379,7 +375,7 @@ struct EventRecorderTests { func junitRecorderCountsIssuesWithoutTests() async throws { let issue = Issue(kind: .unconditional, comments: [], sourceContext: .init()) let event = Event(.issueRecorded(issue), testID: nil, testCaseID: nil) - let context = Event.Context(test: nil, testCase: nil) + let context = Event.Context(test: nil, testCase: nil, configuration: nil) let recorder = Event.JUnitXMLRecorder { string in if string.contains("