Skip to content
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
62 changes: 62 additions & 0 deletions Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,65 @@ extension ABI {

extension ABI.EncodedEvent: Codable {}
extension ABI.EncodedEvent.Kind: Codable {}

// MARK: - Conversion to/from library types

@_spi(ForToolsIntegrationOnly)
extension Event {
/// Initialize an instance of this type from the given value.
///
/// - Parameters:
/// - event: The encoded event to initialize this instance from.
///
/// ``testID`` and ``testCaseID`` are always `nil` because we need information
/// from the associated `ABI.EncodedTest` to properly decode those values.
Comment on lines +193 to +194
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it'll just be follow-on work to fill in these missing property values, I take it?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test.Case.ID isn't stable yet, so we can't really decode it right now. Test.ID may need to be modified to allow it to take a flat string instead of a SourceLocation, but I think it's feasible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's ok to have as nil for the time being because interop does not look at this field.

public init?<V>(decoding event: ABI.EncodedEvent<V>) {
// SkipInfo will only be decoded for skip/cancel event kinds
lazy var skipInfo = SkipInfo(decoding: event)

let kind: Kind
switch event.kind {
case .runStarted:
kind = .runStarted
case .testStarted:
kind = .testStarted
case .testCaseStarted:
kind = .testCaseStarted
case .issueRecorded:
guard let issue = Issue(decoding: event) else {
return nil
}
kind = .issueRecorded(issue)
case .valueAttached:
guard let attachment = Attachment<AnyAttachable>(decoding: event) else {
return nil
}
kind = .valueAttached(attachment)
case .testCaseEnded:
kind = .testCaseEnded
case .testCaseCancelled:
guard let skipInfo else {
return nil
}
kind = .testCaseCancelled(skipInfo)
case .testEnded:
kind = .testEnded
case .testSkipped:
guard let skipInfo else {
return nil
}
kind = .testSkipped(skipInfo)
case .testCancelled:
guard let skipInfo else {
return nil
}
kind = .testCancelled(skipInfo)
case .runEnded:
kind = .runEnded
}

guard let instant = Test.Clock.Instant(decoding: event.instant) else { return nil }

self.init(kind, testID: nil, testCaseID: nil, instant: instant)
}
}
30 changes: 16 additions & 14 deletions Sources/Testing/ExitTests/ExitTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1053,30 +1053,32 @@ extension ExitTest {
/// - Throws: Any error encountered attempting to decode or process the JSON.
private static func _processRecord(_ recordJSON: UnsafeRawBufferPointer, fromBackChannel backChannel: borrowing FileHandle) throws {
let record = try JSON.decode(ABI.Record<ABI.BackChannelVersion>.self, from: recordJSON)
guard case let .event(event) = record.kind else {
guard case let .event(encodedEvent) = record.kind,
let event = Event(decoding: encodedEvent)
else {
return
}

// Translate the event back into a "real" event (such as "issue recorded")
// and post it in the parent process. This translation is, of course, lossy
// due to the process boundary, but we make a best effort.
if var issue = Issue(decoding: event) {
// A backtrace from the child process will have the wrong address space,
// so remove the backtrace if present before recording it.
//
// Events containing a backtrace from the child process will have the wrong
// address space, so remove the backtrace if present before recording it.

switch event.kind {
case .issueRecorded(var issue):
issue.sourceContext.backtrace = nil
issue.record()
} else if let attachment = Attachment(decoding: event) {
case .valueAttached(let attachment):
Attachment.record(attachment, sourceLocation: attachment.sourceLocation)
} else if case .testCancelled = event.kind {
let comment = event._comments?.lazy
.map(Comment.init(rawValue:))
.first
let sourceContext = SourceContext(
backtrace: nil, // A backtrace from the child process will have the wrong address space.
sourceLocation: event._sourceLocation.flatMap(SourceLocation.init)
)
let skipInfo = SkipInfo(comment: comment, sourceContext: sourceContext)
case .testCancelled(var skipInfo), .testCaseCancelled(var skipInfo), .testSkipped(var skipInfo):
// In practice, an exit test won't receive .testSkipped events because
// they would happen before the exit test body starts running.
skipInfo.sourceContext.backtrace = nil
_ = try? Test.cancel(with: skipInfo)
default:
break
}
}

Expand Down
122 changes: 122 additions & 0 deletions Tests/TestingTests/ABI.EncodedEventTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//
// 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(Experimental) @_spi(ForToolsIntegrationOnly) import Testing

@Suite struct `ABI.EncodedEventTests` {
#if canImport(Foundation)
/// Creates an EncodedEvent from a JSON string.
///
/// - Throws: If the JSON doesn't represent a valid EncodedEvent.
private func encodedEvent(
_ json: String,
) throws -> ABI.EncodedEvent<ABI.CurrentVersion> {
var json = json
return try json.withUTF8 { json in
try JSON.decode(ABI.EncodedEvent<ABI.CurrentVersion>.self, from: UnsafeRawBufferPointer(json))
}
}

@Test func `Decoded event always has nil testID and testCaseID`() throws {
let event = try encodedEvent(
"""
{
"kind": "testStarted",
"instant": {"absolute": 123, "since1970": 456},
"messages": [],
"testID": "SomeValidTestID/testFunc()"
}
""")
let decoded = try #require(Event(decoding: event))

#expect(decoded.testID == nil)
#expect(decoded.testCaseID == nil)
}

@Test(arguments: [
"runStarted",
"runEnded",
"testStarted",
"testEnded",
"testCaseStarted",
"testCaseEnded",
// Following `kind`s need SkipInfo which nominally requires _sourceLocation.
// However, an empty placeholder SkipInfo can be provided when decoding.
// vvv
"testCaseCancelled",
"testSkipped",
"testCancelled",
]) func `Successfully decode events which don't require associated info`(kind: String) throws {
let event = try encodedEvent(
"""
{
"kind": "\(kind)",
"instant": {"absolute": 123, "since1970": 456},
"messages": [],
}
""")

#expect(Event(decoding: event) != nil)
}

@Test(arguments: [
"issueRecorded", // Needs issue details
"valueAttached", // Needs attachment details
]) func `Events without required associated info fail to decode`(kind: String) throws {
let event = try encodedEvent(
"""
{
"kind": "\(kind)",
"instant": {"absolute": 123, "since1970": 456},
"messages": [],
}
""")

#expect(Event(decoding: event) == nil)
}

@Test func `Decode issueRecorded`() throws {
let event = try encodedEvent(
"""
{
"kind": "issueRecorded",
"instant": {"absolute": 0, "since1970": 0},
"messages": [],
"issue": {"isKnown": true}
}
""")
let decoded = try #require(Event(decoding: event))

guard case .issueRecorded(let issue) = decoded.kind else {
Comment thread
jerryjrchen marked this conversation as resolved.
Issue.record("Expected issueRecorded but got wrong kind \(decoded.kind)")
return
}
#expect(issue.isKnown)
}

@Test func `Decode valueAttached`() throws {
let event = try encodedEvent(
"""
{
"kind": "valueAttached",
"instant": {"absolute": 0, "since1970": 0},
"messages": [],
"attachment": {"path": "/tmp/important-cheese.txt"}
}
""")
let decoded = try #require(Event(decoding: event))

guard case .valueAttached = decoded.kind else {
Issue.record("Expected valueAttached but got wrong kind \(decoded.kind)")
return
}
}
#endif
}
3 changes: 0 additions & 3 deletions Tests/TestingTests/SkipInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@
//

@testable @_spi(ForToolsIntegrationOnly) import Testing
#if canImport(Foundation)
Comment thread
jerryjrchen marked this conversation as resolved.
import Foundation
#endif

@Suite("SkipInfo Tests")
struct SkipInfoTests {
Expand Down
Loading