diff --git a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift index 6163e7bd1..85167da1a 100644 --- a/Sources/Testing/ABI/EntryPoints/EntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/EntryPoint.swift @@ -331,6 +331,9 @@ public struct __CommandLineArguments_v0: Sendable { /// The value of the `--attachments-path` argument. public var attachmentsPath: String? + + /// The value of the `--testing-library` argument. + public var testingLibrary: String? } extension __CommandLineArguments_v0: Codable { @@ -353,6 +356,7 @@ extension __CommandLineArguments_v0: Codable { case repetitions case repeatUntil case attachmentsPath + case testingLibrary } } @@ -466,6 +470,11 @@ func parseCommandLineArguments(from args: [String]) throws -> __CommandLineArgum } #endif + // Testing library + if let testingLibrary = args.argumentValue(forLabel: "--testing-library") { + result.testingLibrary = testingLibrary + } + // XML output if let xunitOutputPath = args.argumentValue(forLabel: "--xunit-output") { result.xunitOutput = xunitOutputPath diff --git a/Sources/Testing/ABI/EntryPoints/Library.swift b/Sources/Testing/ABI/EntryPoints/Library.swift new file mode 100644 index 000000000..8b63aaa25 --- /dev/null +++ b/Sources/Testing/ABI/EntryPoints/Library.swift @@ -0,0 +1,204 @@ +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2023 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 +// + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) private import _TestDiscovery +private import _TestingInternals + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +public struct Library: Sendable { + /* @c */ fileprivate struct Record { + typealias EntryPoint = @Sendable @convention(c) ( + _ configurationJSON: UnsafeRawPointer, + _ configurationJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeRawPointer, + _ recordJSONHandler: RecordJSONHandler, + _ completionHandler: CompletionHandler + ) -> Void + + typealias RecordJSONHandler = @Sendable @convention(c) ( + _ recordJSON: UnsafeRawPointer, + _ recordJSONByteCount: Int, + _ reserved: UInt, + _ context: UnsafeRawPointer + ) -> Void + + typealias CompletionHandler = @Sendable @convention(c) ( + _ exitCode: CInt, + _ reserved: UInt, + _ context: UnsafeRawPointer + ) -> Void + + nonisolated(unsafe) var name: UnsafePointer + var entryPoint: EntryPoint + var reserved: UInt + } + + private var _record: Record + + public var name: String { + String(validatingCString: _record.name) ?? "" + } + + public func callEntryPoint( + passing args: __CommandLineArguments_v0? = nil, + recordHandler: @escaping @Sendable ( + _ recordJSON: UnsafeRawBufferPointer + ) -> Void = { _ in } + ) async -> CInt { + let configurationJSON: [UInt8] + do { + let args = try args ?? parseCommandLineArguments(from: CommandLine.arguments) + configurationJSON = try JSON.withEncoding(of: args) { configurationJSON in + configurationJSON.withMemoryRebound(to: UInt8.self) { Array($0) } + } + } catch { + // TODO: more advanced error recovery? + return EINVAL + } + + return await withCheckedContinuation { continuation in + struct Context { + var continuation: CheckedContinuation + var recordHandler: @Sendable (UnsafeRawBufferPointer) -> Void + } + let context = Unmanaged.passRetained( + Context( + continuation: continuation, + recordHandler: recordHandler + ) as AnyObject + ).toOpaque() + configurationJSON.withUnsafeBytes { configurationJSON in + _record.entryPoint( + configurationJSON.baseAddress!, + configurationJSON.count, + 0, + context, + /* recordJSONHandler: */ { recordJSON, recordJSONByteCount, _, context in + guard let context = Unmanaged.fromOpaque(context).takeUnretainedValue() as? Context else { + return + } + let recordJSON = UnsafeRawBufferPointer(start: recordJSON, count: recordJSONByteCount) + context.recordHandler(recordJSON) + }, + /* completionHandler: */ { exitCode, _, context in + guard let context = Unmanaged.fromOpaque(context).takeRetainedValue() as? Context else { + return + } + context.continuation.resume(returning: exitCode) + } + ) + } + } + } +} + +// MARK: - Discovery + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Library.Record: DiscoverableAsTestContent { + static var testContentKind: TestContentKind { + "main" + } + + typealias TestContentAccessorHint = UnsafePointer +} + +@_spi(Experimental) @_spi(ForToolsIntegrationOnly) +extension Library { + public init?(named name: String) { + let result = name.withCString { name in + Record.allTestContentRecords().lazy + .compactMap { $0.load(withHint: name) } + .map(Self.init(_record:)) + .first + } + if let result { + self = result + } else { + return nil + } + } + + public static var all: some Sequence { + Record.allTestContentRecords().lazy + .compactMap { $0.load() } + .map(Self.init(_record:)) + } +} + +// MARK: - Our very own entry point + +private let testingLibraryDiscoverableEntryPoint: Library.Record.EntryPoint = { configurationJSON, configurationJSONByteCount, _, context, recordJSONHandler, completionHandler in + do { + nonisolated(unsafe) let context = context + let configurationJSON = UnsafeRawBufferPointer(start: configurationJSON, count: configurationJSONByteCount) + let args = try JSON.decode(__CommandLineArguments_v0.self, from: configurationJSON) + let eventHandler = try eventHandlerForStreamingEvents(withVersionNumber: args.eventStreamVersionNumber, encodeAsJSONLines: false) { recordJSON in + recordJSONHandler(recordJSON.baseAddress!, recordJSON.count, 0, context) + } + + Task.detached { + let exitCode = await Testing.entryPoint(passing: args, eventHandler: eventHandler) + completionHandler(exitCode, 0, context) + } + } catch { + // TODO: more advanced error recovery? + return completionHandler(EINVAL, 0, context) + } +} + +private func testingLibraryDiscoverableAccessor(_ outValue: UnsafeMutableRawPointer, _ type: UnsafeRawPointer, _ hint: UnsafeRawPointer?, _ reserved: UInt) -> CBool { + return false +// #if !hasFeature(Embedded) +// guard type.load(as: Any.Type.self) == Library.Record.self else { +// return false +// } +// #endif +// let hint = hint.map { $0.load(as: UnsafePointer.self) } +// if let hint { +// guard let hint = String(validatingCString: hint), +// String(hint.filter(\.isLetter)).lowercased() == "swifttesting" else { +// return false +// } +// } +// let name: StaticString = "Swift Testing" +// name.utf8Start.withMemoryRebound(to: CChar.self, capacity: name.utf8CodeUnitCount + 1) { name in +// _ = outValue.initializeMemory( +// as: Library.Record.self, +// to: .init( +// name: name, +// entryPoint: testingLibraryDiscoverableEntryPoint, +// reserved: 0 +// ) +// ) +// } +// return true +} + +#if compiler(>=6.3) +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) || os(visionOS) +@section("__DATA_CONST,__swift5_tests") +#elseif os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Android) || os(WASI) +@section("swift5_tests") +#elseif os(Windows) +@section(".sw5test$B") +#else +//@__testing(warning: "Platform-specific implementation missing: test content section name unavailable") +#endif +@used +private let testingLibraryRecord: __TestContentRecord = ( + 0x6D61696E, /* 'main' */ + 0, + testingLibraryDiscoverableAccessor, + 0, + 0 +) +#endif \ No newline at end of file diff --git a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift index 3c72e9f20..42970bc4d 100644 --- a/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift +++ b/Sources/Testing/ABI/EntryPoints/SwiftPMEntryPoint.swift @@ -59,6 +59,13 @@ public func __swiftPMEntryPoint(passing args: __CommandLineArguments_v0? = nil) } #endif + // FIXME: this is probably the wrong layering for this check + if let args = try? args ?? parseCommandLineArguments(from: CommandLine.arguments), + let libraryName = args.testingLibrary, + let library = Library(named: libraryName) { + return await library.callEntryPoint(passing: args) + } + return await entryPoint(passing: args, eventHandler: nil) }