Skip to content

Emit ABIv0 JSON objects for tests when listing them. #511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 42 additions & 34 deletions Sources/Testing/ABI/EntryPoints/EntryPoint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,58 +31,66 @@ func entryPoint(passing args: __CommandLineArguments_v0?, eventHandler: Event.Ha
let exitCode = Locked(rawValue: EXIT_SUCCESS)

do {
let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments)
if args.listTests ?? false {
for testID in await listTestsForEntryPoint(Test.all) {
#if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO
try? FileHandle.stdout.write("\(testID)\n")
#else
print(testID)
#endif
}
} else {
#if !SWT_NO_EXIT_TESTS
// If an exit test was specified, run it. `exitTest` returns `Never`.
if let exitTest = ExitTest.findInEnvironmentForEntryPoint() {
await exitTest()
}
#endif

// Configure the test runner.
var configuration = try configurationForEntryPoint(from: args)
let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments)
// Configure the test runner.
var configuration = try configurationForEntryPoint(from: args)

// Set up the event handler.
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
exitCode.withLock { exitCode in
exitCode = EXIT_FAILURE
}
// Set up the event handler.
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
if case let .issueRecorded(issue) = event.kind, !issue.isKnown {
exitCode.withLock { exitCode in
exitCode = EXIT_FAILURE
}
oldEventHandler(event, context)
}
oldEventHandler(event, context)
}

#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)
}
// 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)
}
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
eventRecorder.record(event, in: context)
oldEventHandler(event, context)
}
#endif

// If the caller specified an alternate event handler, hook it up too.
if let eventHandler {
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
eventRecorder.record(event, in: context)
eventHandler(event, context)
oldEventHandler(event, context)
}
#endif
}

// If the caller specified an alternate event handler, hook it up too.
if let eventHandler {
configuration.eventHandler = { [oldEventHandler = configuration.eventHandler] event, context in
eventHandler(event, context)
oldEventHandler(event, context)
}
if args.listTests ?? false {
let tests = await Test.all
for testID in listTestsForEntryPoint(tests) {
// Print the test ID to stdout (classical CLI behavior.)
#if SWT_TARGET_OS_APPLE && !SWT_NO_FILE_IO
try? FileHandle.stdout.write("\(testID)\n")
#else
print(testID)
#endif
}

// 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)
}
} else {
// Run the tests.
let runner = await Runner(configuration: configuration)
await runner.run()
Expand Down
25 changes: 14 additions & 11 deletions Sources/Testing/ABI/v0/ABIv0.Record+Streaming.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,15 @@ extension ABIv0.Record {
) -> Event.Handler {
let humanReadableOutputRecorder = Event.HumanReadableOutputRecorder()
return { event, context in
if case let .runStarted(plan) = event.kind {
// Gather all tests in the run and forward records for them.
for test in plan.steps.lazy.map(\.test) {
try? JSON.withEncoding(of: Self(encoding: test)) { testJSON in
eventHandler(testJSON)
}
if case .testDiscovered = event.kind, let test = context.test {
try? JSON.withEncoding(of: Self(encoding: test)) { testJSON in
eventHandler(testJSON)
}
} else {
let messages = humanReadableOutputRecorder.record(event, in: context)
if let eventRecord = Self(encoding: event, in: context, messages: messages) {
try? JSON.withEncoding(of: eventRecord, eventHandler)
}
}

let messages = humanReadableOutputRecorder.record(event, in: context)
if let eventRecord = Self(encoding: event, in: context, messages: messages) {
try? JSON.withEncoding(of: eventRecord, eventHandler)
}
}
}
Expand Down Expand Up @@ -92,6 +89,12 @@ func eventHandlerForStreamingEventSnapshots(
to eventHandler: @escaping @Sendable (_ eventAndContextJSON: UnsafeRawBufferPointer) -> Void
) -> Event.Handler {
return { event, context in
if case .testDiscovered = event.kind {
// Discard events of this kind rather than forwarding them to avoid a
// crash in Xcode 16 Beta 1 (which does not expect any events to occur
// before .runStarted.)
return
}
let snapshot = EventAndContextSnapshot(
event: Event.Snapshot(snapshotting: event),
eventContext: Event.Context.Snapshot(snapshotting: context)
Expand Down
37 changes: 31 additions & 6 deletions Sources/Testing/Events/Event.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@
public struct Event: Sendable {
/// An enumeration describing the various kinds of event that can be observed.
public enum Kind: Sendable {
/// A test run started.
/// A test was discovered during test run planning.
///
/// - Parameters:
/// - plan: The test plan of the run that started.
/// This event is recorded once per discovered test when ``Runner/run()`` is
/// called. It does not indicate whether or not a test will run or be
/// skipped—only that the test was found by the testing library and is part
/// of the runner's plan.
///
/// This event is the first event posted after ``Runner/run()`` is called.
indirect case runStarted(_ plan: Runner.Plan)
/// This event is also posted once per test when `swift test list` is
/// called. In that case, events are posted for all discovered tests
/// regardless of whether or not they would run.
case testDiscovered

/// A test run started.
///
/// This event is posted when ``Runner/run()`` is called after
/// ``testDiscovered`` has been posted for all tests in the runner's plan.
case runStarted

/// An iteration of the test run started.
///
Expand Down Expand Up @@ -318,9 +328,22 @@ extension Event {
extension Event.Kind {
/// A serializable enumeration describing the various kinds of event that can be observed.
public enum Snapshot: Sendable, Codable {
/// A test was discovered during test run planning.
///
/// This event is recorded once per discovered test when ``Runner/run()`` is
/// called. It does not indicate whether or not a test will run or be
/// skipped—only that the test was found by the testing library and is part
/// of the runner's plan.
///
/// This event is also posted once per test when `swift test list` is
/// called. In that case, events are posted for all discovered tests
/// regardless of whether or not they would run.
case testDiscovered

/// A test run started.
///
/// This is the first event posted after ``Runner/run()`` is called.
/// This event is posted when ``Runner/run()`` is called after
/// ``testDiscovered`` has been posted for all tests in the runner's plan.
case runStarted

/// An iteration of the test run started.
Expand Down Expand Up @@ -420,6 +443,8 @@ extension Event.Kind {
/// - Parameter kind: The original ``Event.Kind`` to snapshot.
public init(snapshotting kind: Event.Kind) {
switch kind {
case .testDiscovered:
self = .testDiscovered
case .runStarted:
self = .runStarted
case let .iterationStarted(index):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ extension Event.HumanReadableOutputRecorder {

// Finally, produce any messages for the event.
switch event.kind {
case .testDiscovered:
// Suppress events of this kind from output as they are not generally
// interesting in human-readable output.
break

case .runStarted:
var comments = [Comment]()
if verbosity > 0 {
Expand Down
12 changes: 9 additions & 3 deletions Sources/Testing/Running/Runner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public struct Runner: Sendable {
/// - tests: The tests to run.
/// - configuration: The configuration to use for running.
public init(testing tests: [Test], configuration: Configuration = .init()) async {
self.plan = await Plan(tests: tests, configuration: configuration)
self.configuration = configuration
let plan = await Plan(tests: tests, configuration: configuration)
self.init(plan: plan, configuration: configuration)
}

/// Initialize an instance of this type that runs the tests in the specified
Expand Down Expand Up @@ -343,7 +343,13 @@ extension Runner {
}

await Configuration.withCurrent(runner.configuration) {
Event.post(.runStarted(runner.plan), for: nil, testCase: nil, configuration: runner.configuration)
// 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(.runStarted, for: nil, testCase: nil, configuration: runner.configuration)
defer {
Event.post(.runEnded, for: nil, testCase: nil, configuration: runner.configuration)
}
Expand Down
19 changes: 19 additions & 0 deletions Tests/TestingTests/ABIEntryPointTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ struct ABIEntryPointTests {
#expect(result)
}

@Test("v0 entry point listing tests only")
func v0_listingTestsOnly() async throws {
var arguments = __CommandLineArguments_v0()
arguments.listTests = true
arguments.eventStreamVersion = 0
arguments.verbosity = .min

try await confirmation("Test matched", expectedCount: 1...) { testMatched in
_ = try await _invokeEntryPointV0(passing: arguments) { recordJSON in
let record = try! JSON.decode(ABIv0.Record.self, from: recordJSON)
if case .test = record.kind {
testMatched()
} else {
Issue.record("Unexpected record \(record)")
}
}
}
}

private func _invokeEntryPointV0(
passing arguments: __CommandLineArguments_v0,
recordHandler: @escaping @Sendable (_ recordJSON: UnsafeRawBufferPointer) -> Void = { _ in }
Expand Down
2 changes: 1 addition & 1 deletion Tests/TestingTests/EventTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ struct EventTests {
sourceLocation: nil)
)
),
Event.Kind.runStarted(Runner.Plan(steps: [])),
Event.Kind.runStarted,
Event.Kind.runEnded,
Event.Kind.testCaseStarted,
Event.Kind.testCaseEnded,
Expand Down
14 changes: 5 additions & 9 deletions Tests/TestingTests/SwiftPMTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ struct SwiftPMTests {
do {
let configuration = try configurationForEntryPoint(withArguments: ["PATH", "--xunit-output", temporaryFilePath])
let eventContext = Event.Context()
configuration.eventHandler(Event(.runStarted(Runner.Plan(steps: [])), testID: nil, testCaseID: nil), eventContext)
configuration.eventHandler(Event(.runStarted, testID: nil, testCaseID: nil), eventContext)
configuration.eventHandler(Event(.runEnded, testID: nil, testCaseID: nil), eventContext)
}

Expand Down Expand Up @@ -222,15 +222,11 @@ struct SwiftPMTests {
}
do {
let configuration = try configurationForEntryPoint(withArguments: ["PATH", outputArgumentName, temporaryFilePath, versionArgumentName, version])
let eventContext = Event.Context()

let test = Test {}
let plan = Runner.Plan(
steps: [
Runner.Plan.Step(test: test, action: .run(options: .init(isParallelizationEnabled: true)))
]
)
configuration.handleEvent(Event(.runStarted(plan), testID: nil, testCaseID: nil), in: eventContext)
let eventContext = Event.Context(test: test)

configuration.handleEvent(Event(.testDiscovered, testID: test.id, testCaseID: nil), in: eventContext)
configuration.handleEvent(Event(.runStarted, testID: nil, testCaseID: nil), in: eventContext)
do {
let eventContext = Event.Context(test: test)
configuration.handleEvent(Event(.testStarted, testID: test.id, testCaseID: nil), in: eventContext)
Expand Down