runner.swift

16.1 KB · raw

1#if canImport(Testing)2import Testing3#endif45#if false6import Foundation7import XCTest89public final class SwiftPMXCTestObserver: NSObject {10    public override init() {11        super.init()12        XCTestObservationCenter.shared.addTestObserver(self)13    }14}1516extension SwiftPMXCTestObserver: XCTestObservation {17    var testOutputPath: String {18        return "/Users/june/projects/Haven/.build/arm64-apple-macosx/debug/testOutput.txt"19    }2021    private func write(record: any Encodable) {22        let lock = FileLock(at: URL(fileURLWithPath: self.testOutputPath + ".lock"))23        _ = try? lock.withLock {24            self._write(record: record)25        }26    }2728    private func _write(record: any Encodable) {29        if let data = try? JSONEncoder().encode(record) {30            if let fileHandle = FileHandle(forWritingAtPath: self.testOutputPath) {31                defer { fileHandle.closeFile() }32                fileHandle.seekToEndOfFile()33                fileHandle.write("\n".data(using: .utf8)!)34                fileHandle.write(data)35            } else {36                _ = try? data.write(to: URL(fileURLWithPath: self.testOutputPath))37            }38        }39    }4041    public func testBundleWillStart(_ testBundle: Bundle) {42        let record = TestBundleEventRecord(bundle: .init(testBundle), event: .start)43        write(record: TestEventRecord(bundleEvent: record))44    }4546    public func testSuiteWillStart(_ testSuite: XCTestSuite) {47        let record = TestSuiteEventRecord(suite: .init(testSuite), event: .start)48        write(record: TestEventRecord(suiteEvent: record))49    }5051    public func testCaseWillStart(_ testCase: XCTestCase) {52        let record = TestCaseEventRecord(testCase: .init(testCase), event: .start)53        write(record: TestEventRecord(caseEvent: record))54    }5556    #if canImport(Darwin)57    public func testCase(_ testCase: XCTestCase, didRecord issue: XCTIssue) {58        let record = TestCaseFailureRecord(testCase: .init(testCase), issue: .init(issue), failureKind: .unexpected)59        write(record: TestEventRecord(caseFailure: record))60    }6162    public func testCase(_ testCase: XCTestCase, didRecord expectedFailure: XCTExpectedFailure) {63        let record = TestCaseFailureRecord(testCase: .init(testCase), issue: .init(expectedFailure.issue), failureKind: .expected(failureReason: expectedFailure.failureReason))64        write(record: TestEventRecord(caseFailure: record))65    }66    #else67    public func testCase(_ testCase: XCTestCase, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {68        let issue = TestIssue(description: description, inFile: filePath, atLine: lineNumber)69        let record = TestCaseFailureRecord(testCase: .init(testCase), issue: issue, failureKind: .unexpected)70        write(record: TestEventRecord(caseFailure: record))71    }72    #endif7374    public func testCaseDidFinish(_ testCase: XCTestCase) {75        let record = TestCaseEventRecord(testCase: .init(testCase), event: .finish)76        write(record: TestEventRecord(caseEvent: record))77    }7879    #if canImport(Darwin)80    public func testSuite(_ testSuite: XCTestSuite, didRecord issue: XCTIssue) {81        let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: .init(issue), failureKind: .unexpected)82        write(record: TestEventRecord(suiteFailure: record))83    }8485    public func testSuite(_ testSuite: XCTestSuite, didRecord expectedFailure: XCTExpectedFailure) {86        let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: .init(expectedFailure.issue), failureKind: .expected(failureReason: expectedFailure.failureReason))87        write(record: TestEventRecord(suiteFailure: record))88    }89    #else90    public func testSuite(_ testSuite: XCTestSuite, didFailWithDescription description: String, inFile filePath: String?, atLine lineNumber: Int) {91        let issue = TestIssue(description: description, inFile: filePath, atLine: lineNumber)92        let record = TestSuiteFailureRecord(suite: .init(testSuite), issue: issue, failureKind: .unexpected)93        write(record: TestEventRecord(suiteFailure: record))94    }95    #endif9697    public func testSuiteDidFinish(_ testSuite: XCTestSuite) {98        let record = TestSuiteEventRecord(suite: .init(testSuite), event: .finish)99        write(record: TestEventRecord(suiteEvent: record))100    }101102    public func testBundleDidFinish(_ testBundle: Bundle) {103        let record = TestBundleEventRecord(bundle: .init(testBundle), event: .finish)104        write(record: TestEventRecord(bundleEvent: record))105    }106}107108// FIXME: Copied from `Lock.swift` in TSCBasic, would be nice if we had a better way109110#if canImport(Glibc)111@_exported import Glibc112#elseif canImport(Musl)113@_exported import Musl114#elseif os(Windows)115@_exported import CRT116@_exported import WinSDK117#elseif os(WASI)118@_exported import WASILibc119#elseif canImport(Android)120@_exported import Android121#else122@_exported import Darwin.C123#endif124125import Foundation126127public final class FileLock {128  #if os(Windows)129    private var handle: HANDLE?130  #else131    private var fileDescriptor: CInt?132  #endif133134    private let lockFile: URL135136    public init(at lockFile: URL) {137        self.lockFile = lockFile138    }139140    public func lock() throws {141      #if os(Windows)142        if handle == nil {143            let h: HANDLE = lockFile.path.withCString(encodedAs: UTF16.self, {144                CreateFileW(145                    $0,146                    UInt32(GENERIC_READ) | UInt32(GENERIC_WRITE),147                    UInt32(FILE_SHARE_READ) | UInt32(FILE_SHARE_WRITE),148                    nil,149                    DWORD(OPEN_ALWAYS),150                    DWORD(FILE_ATTRIBUTE_NORMAL),151                    nil152                )153            })154            if h == INVALID_HANDLE_VALUE {155                throw FileSystemError(errno: Int32(GetLastError()), lockFile)156            }157            self.handle = h158        }159        var overlapped = OVERLAPPED()160        overlapped.Offset = 0161        overlapped.OffsetHigh = 0162        overlapped.hEvent = nil163        if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0,164                           UInt32.max, UInt32.max, &overlapped) {165                throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))166            }167      #elseif os(WASI)168        // WASI doesn't support flock169      #else170        if fileDescriptor == nil {171            let fd = open(lockFile.path, O_WRONLY | O_CREAT | O_CLOEXEC, 0o666)172            if fd == -1 {173                fatalError("errno: \(errno), lockFile: \(lockFile)")174            }175            self.fileDescriptor = fd176        }177        while true {178            if flock(fileDescriptor!, LOCK_EX) == 0 {179                break180            }181            if errno == EINTR { continue }182            fatalError("unable to acquire lock, errno: \(errno)")183        }184      #endif185    }186187    public func unlock() {188      #if os(Windows)189        var overlapped = OVERLAPPED()190        overlapped.Offset = 0191        overlapped.OffsetHigh = 0192        overlapped.hEvent = nil193        UnlockFileEx(handle, 0, UInt32.max, UInt32.max, &overlapped)194      #elseif os(WASI)195        // WASI doesn't support flock196      #else197        guard let fd = fileDescriptor else { return }198        flock(fd, LOCK_UN)199      #endif200    }201202    deinit {203      #if os(Windows)204        guard let handle = handle else { return }205        CloseHandle(handle)206      #elseif os(WASI)207        // WASI doesn't support flock208      #else209        guard let fd = fileDescriptor else { return }210        close(fd)211      #endif212    }213214    public func withLock<T>(_ body: () throws -> T) throws -> T {215        try lock()216        defer { unlock() }217        return try body()218    }219220    public func withLock<T>(_ body: () async throws -> T) async throws -> T {221        try lock()222        defer { unlock() }223        return try await body()224    }225}226227// FIXME: Copied from `XCTEvents.swift`, would be nice if we had a better way228229struct TestEventRecord: Codable {230    let caseFailure: TestCaseFailureRecord?231    let suiteFailure: TestSuiteFailureRecord?232233    let bundleEvent: TestBundleEventRecord?234    let suiteEvent: TestSuiteEventRecord?235    let caseEvent: TestCaseEventRecord?236237    init(238        caseFailure: TestCaseFailureRecord? = nil,239        suiteFailure: TestSuiteFailureRecord? = nil,240        bundleEvent: TestBundleEventRecord? = nil,241        suiteEvent: TestSuiteEventRecord? = nil,242        caseEvent: TestCaseEventRecord? = nil243    ) {244        self.caseFailure = caseFailure245        self.suiteFailure = suiteFailure246        self.bundleEvent = bundleEvent247        self.suiteEvent = suiteEvent248        self.caseEvent = caseEvent249    }250}251252// MARK: - Records253254struct TestAttachment: Codable {255    let name: String?256    // TODO: Handle `userInfo: [AnyHashable : Any]?`257    let uniformTypeIdentifier: String258    let payload: Data?259}260261struct TestBundleEventRecord: Codable {262    let bundle: TestBundle263    let event: TestEvent264}265266struct TestCaseEventRecord: Codable {267    let testCase: TestCase268    let event: TestEvent269}270271struct TestCaseFailureRecord: Codable, CustomStringConvertible {272    let testCase: TestCase273    let issue: TestIssue274    let failureKind: TestFailureKind275276    var description: String {277        return "\(issue.sourceCodeContext.description)\(testCase) \(issue.compactDescription)"278    }279}280281struct TestSuiteEventRecord: Codable {282    let suite: TestSuiteRecord283    let event: TestEvent284}285286struct TestSuiteFailureRecord: Codable {287    let suite: TestSuiteRecord288    let issue: TestIssue289    let failureKind: TestFailureKind290}291292// MARK: Primitives293294struct TestBundle: Codable {295    let bundleIdentifier: String?296    let bundlePath: String297}298299struct TestCase: Codable {300    let name: String301}302303struct TestErrorInfo: Codable {304    let description: String305    let type: String306}307308enum TestEvent: Codable {309    case start310    case finish311}312313enum TestFailureKind: Codable, Equatable {314    case unexpected315    case expected(failureReason: String?)316317    var isExpected: Bool {318        switch self {319        case .expected: return true320        case .unexpected: return false321        }322    }323}324325struct TestIssue: Codable {326    let type: TestIssueType327    let compactDescription: String328    let detailedDescription: String?329    let associatedError: TestErrorInfo?330    let sourceCodeContext: TestSourceCodeContext331    let attachments: [TestAttachment]332}333334enum TestIssueType: Codable {335    case assertionFailure336    case performanceRegression337    case system338    case thrownError339    case uncaughtException340    case unmatchedExpectedFailure341    case unknown342}343344struct TestLocation: Codable, CustomStringConvertible {345    let file: String346    let line: Int347348    var description: String {349        return "\(file):\(line) "350    }351}352353struct TestSourceCodeContext: Codable, CustomStringConvertible {354    let callStack: [TestSourceCodeFrame]355    let location: TestLocation?356357    var description: String {358        return location?.description ?? ""359    }360}361362struct TestSourceCodeFrame: Codable {363    let address: UInt64364    let symbolInfo: TestSourceCodeSymbolInfo?365    let symbolicationError: TestErrorInfo?366}367368struct TestSourceCodeSymbolInfo: Codable {369    let imageName: String370    let symbolName: String371    let location: TestLocation?372}373374struct TestSuiteRecord: Codable {375    let name: String376}377378// MARK: XCTest compatibility379380extension TestIssue {381    init(description: String, inFile filePath: String?, atLine lineNumber: Int) {382        let location: TestLocation?383        if let filePath = filePath {384            location = .init(file: filePath, line: lineNumber)385        } else {386            location = nil387        }388        self.init(type: .assertionFailure, compactDescription: description, detailedDescription: description, associatedError: nil, sourceCodeContext: .init(callStack: [], location: location), attachments: [])389    }390}391392import XCTest393394#if canImport(Darwin) // XCTAttachment is unavailable in swift-corelibs-xctest.395extension TestAttachment {396    init(_ attachment: XCTAttachment) {397        self.init(398            name: attachment.name,399            uniformTypeIdentifier: attachment.uniformTypeIdentifier,400            payload: attachment.value(forKey: "payload") as? Data401        )402    }403}404#endif405406extension TestBundle {407    init(_ testBundle: Bundle) {408        self.init(409            bundleIdentifier: testBundle.bundleIdentifier,410            bundlePath: testBundle.bundlePath411        )412    }413}414415extension TestCase {416    init(_ testCase: XCTestCase) {417        self.init(name: testCase.name)418    }419}420421extension TestErrorInfo {422    init(_ error: any Swift.Error) {423        self.init(description: "\(error)", type: "\(Swift.type(of: error))")424    }425}426427#if canImport(Darwin) // XCTIssue is unavailable in swift-corelibs-xctest.428extension TestIssue {429    init(_ issue: XCTIssue) {430        self.init(431            type: .init(issue.type),432            compactDescription: issue.compactDescription,433            detailedDescription: issue.detailedDescription,434            associatedError: issue.associatedError.map { .init($0) },435            sourceCodeContext: .init(issue.sourceCodeContext),436            attachments: issue.attachments.map { .init($0) }437        )438    }439}440441extension TestIssueType {442    init(_ type: XCTIssue.IssueType) {443        switch type {444        case .assertionFailure: self = .assertionFailure445        case .thrownError: self = .thrownError446        case .uncaughtException: self = .uncaughtException447        case .performanceRegression: self = .performanceRegression448        case .system: self = .system449        case .unmatchedExpectedFailure: self = .unmatchedExpectedFailure450        @unknown default: self = .unknown451        }452    }453}454#endif455456#if canImport(Darwin) // XCTSourceCodeLocation/XCTSourceCodeContext/XCTSourceCodeFrame/XCTSourceCodeSymbolInfo is unavailable in swift-corelibs-xctest.457extension TestLocation {458    init(_ location: XCTSourceCodeLocation) {459        self.init(460            file: location.fileURL.absoluteString,461            line: location.lineNumber462        )463    }464}465466extension TestSourceCodeContext {467    init(_ context: XCTSourceCodeContext) {468        self.init(469            callStack: context.callStack.map { .init($0) },470            location: context.location.map { .init($0) }471        )472    }473}474475extension TestSourceCodeFrame {476    init(_ frame: XCTSourceCodeFrame) {477        self.init(478            address: frame.address,479            symbolInfo: (try? frame.symbolInfo()).map { .init($0) },480            symbolicationError: frame.symbolicationError.map { .init($0) }481        )482    }483}484485extension TestSourceCodeSymbolInfo {486    init(_ symbolInfo: XCTSourceCodeSymbolInfo) {487        self.init(488            imageName: symbolInfo.imageName,489            symbolName: symbolInfo.symbolName,490            location: symbolInfo.location.map { .init($0) }491        )492    }493}494#endif495496extension TestSuiteRecord {497    init(_ testSuite: XCTestSuite) {498        self.init(name: testSuite.name)499    }500}501502import XCTest503504#endif505506@main507@available(macOS 10.15, iOS 11, watchOS 4, tvOS 11, *)508@available(*, deprecated, message: "Not actually deprecated. Marked as deprecated to allow inclusion of deprecated tests (which test deprecated functionality) without warnings")509struct Runner {510    private static func testingLibrary() -> String {511        var iterator = CommandLine.arguments.makeIterator()512        while let argument = iterator.next() {513            if argument == "--testing-library", let libraryName = iterator.next() {514                return libraryName.lowercased()515            }516        }517518        // Fallback if not specified: run XCTest (legacy behavior)519        return "xctest"520    }521522    #if false523    @_silgen_name("$ss13_runAsyncMainyyyyYaKcF")524    private static func _runAsyncMain(_ asyncFun: @Sendable @escaping () async throws -> ())525    #endif526527    static func main() async {528        let testingLibrary = Self.testingLibrary()529        #if canImport(Testing)530        if testingLibrary == "swift-testing" {531            #if false532            _runAsyncMain {533                await Testing.__swiftPMEntryPoint() as Never534            }535            #else536            await Testing.__swiftPMEntryPoint() as Never537            #endif538        }539        #endif540        #if false541        if testingLibrary == "xctest" {542            543             XCTMain(__allDiscoveredTests()) as Never544        }545        #endif546    }547}