From 54a39a80fe6bb9d71c7f4dcb58dd45fe60bd7cd6 Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Thu, 16 Apr 2026 09:34:27 -0700 Subject: [PATCH 1/2] Decode EncodedEvent -> Event ### Motivation: Enables re-use of EncodedEvent decoding logic for tools support interop. Today, corelibs XCTest manually re-defines the Resolves rdar://175287183 ### Modifications: * Implement decoding EncodedEvent -> Event * Test common decoding scenarios, included decoding issueRecorded and valueAttached event kinds --- .../ABI/Encoded/ABI.EncodedEvent.swift | 62 +++++++++ .../TestingTests/ABI.EncodedEventTests.swift | 122 ++++++++++++++++++ Tests/TestingTests/SkipInfoTests.swift | 3 - 3 files changed, 184 insertions(+), 3 deletions(-) create mode 100644 Tests/TestingTests/ABI.EncodedEventTests.swift diff --git a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift index 8c496d8e6..74932085c 100644 --- a/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift +++ b/Sources/Testing/ABI/Encoded/ABI.EncodedEvent.swift @@ -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. + public init?(decoding event: ABI.EncodedEvent) { + // 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(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) + } +} diff --git a/Tests/TestingTests/ABI.EncodedEventTests.swift b/Tests/TestingTests/ABI.EncodedEventTests.swift new file mode 100644 index 000000000..405675e4b --- /dev/null +++ b/Tests/TestingTests/ABI.EncodedEventTests.swift @@ -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 { + var json = json + return try json.withUTF8 { json in + try JSON.decode(ABI.EncodedEvent.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 { + 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 +} diff --git a/Tests/TestingTests/SkipInfoTests.swift b/Tests/TestingTests/SkipInfoTests.swift index dd313974e..9971ead02 100644 --- a/Tests/TestingTests/SkipInfoTests.swift +++ b/Tests/TestingTests/SkipInfoTests.swift @@ -9,9 +9,6 @@ // @testable @_spi(ForToolsIntegrationOnly) import Testing -#if canImport(Foundation) -import Foundation -#endif @Suite("SkipInfo Tests") struct SkipInfoTests { From 471cd48957599acda40b52ac53bb674fa7eae0bd Mon Sep 17 00:00:00 2001 From: Jerry Chen Date: Wed, 22 Apr 2026 16:35:27 -0700 Subject: [PATCH 2/2] Use EncodedEvent -> Event decoding in exit test _processRecord This makes it easier to handle new event types and simplifies (?) the logic for managing currently support event kinds. No functional change in behaviour. This technically enables exit tests to handle .testSkipped event kinds, but this cannot occur in practise. --- Sources/Testing/ExitTests/ExitTest.swift | 30 +++++++++++++----------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Sources/Testing/ExitTests/ExitTest.swift b/Sources/Testing/ExitTests/ExitTest.swift index 6a9bb6fbb..f9664c044 100644 --- a/Sources/Testing/ExitTests/ExitTest.swift +++ b/Sources/Testing/ExitTests/ExitTest.swift @@ -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.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 } }