blob: 30926aed59dabaa4ff321acbc2a87155e1d0827d [file] [edit]
// Copyright (C) 2026 Apple Inc. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
// BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
// THE POSSIBILITY OF SUCH DAMAGE.
import Foundation
private import Synchronization
actor SwiftTestsController: TestRunner {
nonisolated struct Storage: ~Copyable {
var tests: [SwiftTestingABI.EncodedTest.ID: SwiftTestingABI.EncodedTest] = [:]
var events: [SwiftTestingABI.EncodedTest.ID: [SwiftTestingABI.EncodedEvent]] = [:]
}
static let shared = SwiftTestsController()
private static let entryPoint = unsafe unsafeBitCast(swt_abiv0_getEntryPoint(), to: SwiftTestingABI.EntryPoint.self)
private let decoder = JSONDecoder()
private let encoder = JSONEncoder()
private let storage = Mutex<Storage>(.init())
nonisolated private func saveRecord(_ record: SwiftTestingABI.Record) {
switch record.kind {
case .test(let encodedTest):
storage.withLock { $0.tests[encodedTest.id] = encodedTest }
case .event(let encodedEvent):
guard let testID = encodedEvent.testID else {
return
}
storage.withLock { $0.events[testID, default: []].append(encodedEvent) }
}
}
nonisolated private func writeOutputIfNeeded(_ record: SwiftTestingABI.Record) {
guard case .event(let eventRecord) = record.kind else {
return
}
guard let testID = eventRecord.testID else {
return
}
let (test, events) = storage.withLock {
($0.tests[testID], $0.events[testID])
}
guard let test, let events, test.kind == .function else {
return
}
switch eventRecord.kind {
case .testEnded where events.contains { $0.kind == .issueRecorded }:
let failureDescriptions = events
.lazy
.filter { $0.kind == .issueRecorded }
.flatMap(\.messages)
.map { message in
"\(message.symbol.canonicalizedRepresentation) \(message.text)"
}
.joined(separator: "\n")
print("**FAIL** \(testID.canonicalizedRepresentation)\n\(failureDescriptions)")
case .testEnded:
print("**PASS** \(testID.canonicalizedRepresentation)")
case .testSkipped:
print("**DISABLED** \(testID.canonicalizedRepresentation)")
default:
break
}
}
nonisolated private func handleRecord(_ record: SwiftTestingABI.Record) {
saveRecord(record)
writeOutputIfNeeded(record)
}
func run(with configuration: Configuration) async throws -> Bool {
let abiConfiguration = SwiftTestingABI.__CommandLineArguments_v0(configuration)
let encodedConfiguration = try encoder.encode(abiConfiguration)
// Copies the data of `encodedConfiguration` into a new buffer that is guaranteed to be valid for the scope of this function.
let mutableBuffer = UnsafeMutableRawBufferPointer.allocate(byteCount: encodedConfiguration.count, alignment: 1)
defer {
unsafe mutableBuffer.deallocate()
}
unsafe encodedConfiguration.copyBytes(to: mutableBuffer)
let buffer = UnsafeRawBufferPointer(mutableBuffer)
return unsafe try await Self.entryPoint(buffer) { [self, abiConfiguration] recordJSON in
// Either use custom logging, or Swift Testing logging, not both
guard abiConfiguration.verbosity == .min else {
return
}
let data = unsafe Data(recordJSON)
// The program should terminate if this `try` fails, any other behavior would lead to unexpected results.
// swift-format-ignore: NeverUseForceTry
let decoded = try! decoder.decode(SwiftTestingABI.Record.self, from: data)
handleRecord(decoded)
}
}
}
extension SwiftTestingABI.EncodedTest.ID {
/// The stable representation of a test's identifier, not subject to changes based on source location.
///
/// For example, this converts
///
/// ```
/// TestWebKitAPI.WKWebViewSwiftOverlayTests/evaluateJavaScriptWithNilResponse()/WKWebViewSwiftOverlayTests.swift:40:6
/// ```
///
/// to
///
/// ```
/// TestWebKitAPI.WKWebViewSwiftOverlayTests/evaluateJavaScriptWithNilResponse()
/// ```
fileprivate var canonicalizedRepresentation: some StringProtocol {
let rawValue = stringValue
let indexOfLastDelimiter = rawValue.lastIndex(of: "/") ?? rawValue.endIndex
return rawValue[..<indexOfLastDelimiter]
}
}
extension SwiftTestingABI.EncodedMessage.Symbol {
fileprivate var canonicalizedRepresentation: some StringProtocol {
switch self {
case .fail: "✘"
case .details: "↪︎"
default: ""
}
}
}
extension SwiftTestingABI.__CommandLineArguments_v0 {
fileprivate init(_ runnerConfiguration: TestRunner.Configuration) {
self.verbosity =
if runnerConfiguration.listTests {
0 // Default Swift Testing logging level
} else if runnerConfiguration.pretty {
0 // Verbose Swift Testing logging level
} else {
Int.min // None Swift Testing logging level
}
self.filter = runnerConfiguration.filter.map(NSRegularExpression.escapedPattern(for:))
self.repetitions = runnerConfiguration.repetitions
self.skip = runnerConfiguration.skip
self.listTests = runnerConfiguration.listTests
self.parallel = runnerConfiguration.parallel
}
}