diff --git a/Examples/Testing/Package.swift b/Examples/Testing/Package.swift index 6dd492cd..d9d1719f 100644 --- a/Examples/Testing/Package.swift +++ b/Examples/Testing/Package.swift @@ -18,7 +18,11 @@ let package = Package( ]), .testTarget( name: "CounterTests", - dependencies: ["Counter"] + dependencies: [ + "Counter", + // This is needed to run the tests in the JavaScript event loop + .product(name: "JavaScriptEventLoopTestSupport", package: "JavaScriptKit") + ] ), ] ) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 80934c24..03af2c73 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -40,6 +40,8 @@ struct PackageToJS { var inspect: Bool /// The extra arguments to pass to node var extraNodeArguments: [String] + /// Whether to print verbose output + var verbose: Bool /// The options for packaging var packageOptions: PackageOptions } @@ -59,32 +61,36 @@ struct PackageToJS { var testJsArguments: [String] = [] var testLibraryArguments: [String] = [] if testOptions.listTests { - testLibraryArguments += ["--list-tests"] + testLibraryArguments.append("--list-tests") } if let prelude = testOptions.prelude { let preludeURL = URL(fileURLWithPath: prelude, relativeTo: URL(fileURLWithPath: FileManager.default.currentDirectoryPath)) - testJsArguments += ["--prelude", preludeURL.path] + testJsArguments.append("--prelude") + testJsArguments.append(preludeURL.path) } if let environment = testOptions.environment { - testJsArguments += ["--environment", environment] + testJsArguments.append("--environment") + testJsArguments.append(environment) } if testOptions.inspect { - testJsArguments += ["--inspect"] + testJsArguments.append("--inspect") } let xctestCoverageFile = outputDir.appending(path: "XCTest.profraw") do { var extraArguments = testJsArguments if testOptions.packageOptions.enableCodeCoverage { - extraArguments += ["--coverage-file", xctestCoverageFile.path] + extraArguments.append("--coverage-file") + extraArguments.append(xctestCoverageFile.path) } - extraArguments += ["--"] - extraArguments += testLibraryArguments - extraArguments += testOptions.filter + extraArguments.append("--") + extraArguments.append(contentsOf: testLibraryArguments) + extraArguments.append(contentsOf: testOptions.filter) try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, extraArguments: extraArguments, + testParser: testOptions.verbose ? nil : FancyTestsParser(write: { print($0, terminator: "") }), testOptions: testOptions ) } @@ -92,11 +98,17 @@ struct PackageToJS { do { var extraArguments = testJsArguments if testOptions.packageOptions.enableCodeCoverage { - extraArguments += ["--coverage-file", swiftTestingCoverageFile.path] + extraArguments.append("--coverage-file") + extraArguments.append(swiftTestingCoverageFile.path) + } + extraArguments.append("--") + extraArguments.append("--testing-library") + extraArguments.append("swift-testing") + extraArguments.append(contentsOf: testLibraryArguments) + for filter in testOptions.filter { + extraArguments.append("--filter") + extraArguments.append(filter) } - extraArguments += ["--", "--testing-library", "swift-testing"] - extraArguments += testLibraryArguments - extraArguments += testOptions.filter.flatMap { ["--filter", $0] } try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, @@ -106,7 +118,7 @@ struct PackageToJS { } if testOptions.packageOptions.enableCodeCoverage { - let profrawFiles = [xctestCoverageFile, swiftTestingCoverageFile].filter { FileManager.default.fileExists(atPath: $0.path) } + let profrawFiles = [xctestCoverageFile.path, swiftTestingCoverageFile.path].filter { FileManager.default.fileExists(atPath: $0) } do { try PackageToJS.postProcessCoverageFiles(outputDir: outputDir, profrawFiles: profrawFiles) } catch { @@ -119,38 +131,97 @@ struct PackageToJS { testRunner: URL, currentDirectoryURL: URL, extraArguments: [String], + testParser: FancyTestsParser? = nil, testOptions: TestOptions ) throws { let node = try which("node") - let arguments = ["--experimental-wasi-unstable-preview1"] + testOptions.extraNodeArguments + [testRunner.path] + extraArguments + var arguments = ["--experimental-wasi-unstable-preview1"] + arguments.append(contentsOf: testOptions.extraNodeArguments) + arguments.append(testRunner.path) + arguments.append(contentsOf: extraArguments) + print("Running test...") logCommandExecution(node.path, arguments) let task = Process() task.executableURL = node task.arguments = arguments + + var finalize: () -> Void = {} + if let testParser = testParser { + let stdoutBuffer = LineBuffer { line in + testParser.onLine(line) + } + let stdoutPipe = Pipe() + stdoutPipe.fileHandleForReading.readabilityHandler = { handle in + stdoutBuffer.append(handle.availableData) + } + task.standardOutput = stdoutPipe + finalize = { + if let data = try? stdoutPipe.fileHandleForReading.readToEnd() { + stdoutBuffer.append(data) + } + stdoutBuffer.flush() + testParser.finalize() + } + } + task.currentDirectoryURL = currentDirectoryURL try task.forwardTerminationSignals { try task.run() task.waitUntilExit() } + finalize() // swift-testing returns EX_UNAVAILABLE (which is 69 in wasi-libc) for "no tests found" - guard task.terminationStatus == 0 || task.terminationStatus == 69 else { + guard [0, 69].contains(task.terminationStatus) else { throw PackageToJSError("Test failed with status \(task.terminationStatus)") } } - static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [URL]) throws { + static func postProcessCoverageFiles(outputDir: URL, profrawFiles: [String]) throws { let mergedCoverageFile = outputDir.appending(path: "default.profdata") do { // Merge the coverage files by llvm-profdata - let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles.map { $0.path } + let arguments = ["merge", "-sparse", "-output", mergedCoverageFile.path] + profrawFiles let llvmProfdata = try which("llvm-profdata") logCommandExecution(llvmProfdata.path, arguments) try runCommand(llvmProfdata, arguments) print("Saved profile data to \(mergedCoverageFile.path)") } } + + class LineBuffer: @unchecked Sendable { + let lock = NSLock() + var buffer = "" + let handler: (String) -> Void + + init(handler: @escaping (String) -> Void) { + self.handler = handler + } + + func append(_ data: Data) { + let string = String(data: data, encoding: .utf8) ?? "" + append(string) + } + + func append(_ data: String) { + lock.lock() + defer { lock.unlock() } + buffer.append(data) + let lines = buffer.split(separator: "\n", omittingEmptySubsequences: false) + for line in lines.dropLast() { + handler(String(line)) + } + buffer = String(lines.last ?? "") + } + + func flush() { + lock.lock() + defer { lock.unlock() } + handler(buffer) + buffer = "" + } + } } struct PackageToJSError: Swift.Error, CustomStringConvertible { @@ -509,12 +580,12 @@ struct PackagingPlanner { } let inputPath = selfPackageDir.appending(path: file) - let conditions = [ + let conditions: [String: Bool] = [ "USE_SHARED_MEMORY": triple == "wasm32-unknown-wasip1-threads", "IS_WASI": triple.hasPrefix("wasm32-unknown-wasi"), "USE_WASI_CDN": options.useCDN, ] - let constantSubstitutions = [ + let constantSubstitutions: [String: String] = [ "PACKAGE_TO_JS_MODULE_PATH": wasmFilename, "PACKAGE_TO_JS_PACKAGE_NAME": options.packageName ?? packageId.lowercased(), ] @@ -529,11 +600,13 @@ struct PackagingPlanner { if let wasmImportsPath = wasmImportsPath { let wasmImportsPath = $1.resolve(path: wasmImportsPath) let importEntries = try JSONDecoder().decode([ImportEntry].self, from: Data(contentsOf: wasmImportsPath)) - let memoryImport = importEntries.first { $0.module == "env" && $0.name == "memory" } + let memoryImport = importEntries.first { + $0.module == "env" && $0.name == "memory" + } if case .memory(let type) = memoryImport?.kind { - substitutions["PACKAGE_TO_JS_MEMORY_INITIAL"] = "\(type.minimum)" - substitutions["PACKAGE_TO_JS_MEMORY_MAXIMUM"] = "\(type.maximum ?? type.minimum)" - substitutions["PACKAGE_TO_JS_MEMORY_SHARED"] = "\(type.shared)" + substitutions["PACKAGE_TO_JS_MEMORY_INITIAL"] = type.minimum.description + substitutions["PACKAGE_TO_JS_MEMORY_MAXIMUM"] = (type.maximum ?? type.minimum).description + substitutions["PACKAGE_TO_JS_MEMORY_SHARED"] = type.shared.description } } diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 96102376..9013b26e 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -340,12 +340,14 @@ extension PackageToJS.TestOptions { let prelude = extractor.extractOption(named: "prelude").last let environment = extractor.extractOption(named: "environment").last let inspect = extractor.extractFlag(named: "inspect") + let verbose = extractor.extractFlag(named: "verbose") let extraNodeArguments = extractor.extractSingleDashOption(named: "Xnode") let packageOptions = PackageToJS.PackageOptions.parse(from: &extractor) var options = PackageToJS.TestOptions( buildOnly: buildOnly != 0, listTests: listTests != 0, filter: filter, prelude: prelude, environment: environment, inspect: inspect != 0, extraNodeArguments: extraNodeArguments, + verbose: verbose != 0, packageOptions: packageOptions ) @@ -369,6 +371,8 @@ extension PackageToJS.TestOptions { --inspect Whether to run tests in the browser with inspector enabled --use-cdn Whether to use CDN for dependency packages (default: false) --enable-code-coverage Whether to enable code coverage collection (default: false) + --verbose Whether to print verbose output (default: false) + -Xnode Extra arguments to pass to Node.js EXAMPLES: $ swift package --swift-sdk wasm32-unknown-wasi plugin js test diff --git a/Plugins/PackageToJS/Sources/ParseWasm.swift b/Plugins/PackageToJS/Sources/ParseWasm.swift index a35b6956..8cfb6c66 100644 --- a/Plugins/PackageToJS/Sources/ParseWasm.swift +++ b/Plugins/PackageToJS/Sources/ParseWasm.swift @@ -1,7 +1,7 @@ import struct Foundation.Data /// Represents the type of value in WebAssembly -enum ValueType: String, Codable { +enum ValueType { case i32 case i64 case f32 @@ -12,18 +12,18 @@ enum ValueType: String, Codable { } /// Represents a function type in WebAssembly -struct FunctionType: Codable { +struct FunctionType { let parameters: [ValueType] let results: [ValueType] } /// Represents a table type in WebAssembly -struct TableType: Codable { +struct TableType { let element: ElementType let minimum: UInt32 let maximum: UInt32? - - enum ElementType: String, Codable { + + enum ElementType: String { case funcref case externref } @@ -35,7 +35,7 @@ struct MemoryType: Codable { let maximum: UInt32? let shared: Bool let index: IndexType - + enum IndexType: String, Codable { case i32 case i64 @@ -43,7 +43,7 @@ struct MemoryType: Codable { } /// Represents a global type in WebAssembly -struct GlobalType: Codable { +struct GlobalType { let value: ValueType let mutable: Bool } @@ -53,12 +53,12 @@ struct ImportEntry: Codable { let module: String let name: String let kind: ImportKind - + enum ImportKind: Codable { - case function(type: FunctionType) - case table(type: TableType) + case function + case table case memory(type: MemoryType) - case global(type: GlobalType) + case global } } @@ -66,16 +66,16 @@ struct ImportEntry: Codable { private class ParseState { private let moduleBytes: Data private var offset: Int - + init(moduleBytes: Data) { self.moduleBytes = moduleBytes self.offset = 0 } - + func hasMoreBytes() -> Bool { return offset < moduleBytes.count } - + func readByte() throws -> UInt8 { guard offset < moduleBytes.count else { throw ParseError.unexpectedEndOfData @@ -84,7 +84,7 @@ private class ParseState { offset += 1 return byte } - + func skipBytes(_ count: Int) throws { guard offset + count <= moduleBytes.count else { throw ParseError.unexpectedEndOfData @@ -97,7 +97,7 @@ private class ParseState { var result: UInt32 = 0 var shift: UInt32 = 0 var byte: UInt8 - + repeat { byte = try readByte() result |= UInt32(byte & 0x7F) << shift @@ -106,39 +106,39 @@ private class ParseState { throw ParseError.integerOverflow } } while (byte & 0x80) != 0 - + return result } - + func readName() throws -> String { let nameLength = try readUnsignedLEB128() guard offset + Int(nameLength) <= moduleBytes.count else { throw ParseError.unexpectedEndOfData } - + let nameBytes = moduleBytes[offset..<(offset + Int(nameLength))] guard let name = String(bytes: nameBytes, encoding: .utf8) else { throw ParseError.invalidUTF8 } - + offset += Int(nameLength) return name } - + func assertBytes(_ expected: [UInt8]) throws { let baseOffset = offset let expectedLength = expected.count - + guard baseOffset + expectedLength <= moduleBytes.count else { throw ParseError.unexpectedEndOfData } - + for i in 0.. [ImportEntry] { let parseState = ParseState(moduleBytes: moduleBytes) try parseMagicNumber(parseState) try parseVersion(parseState) - + var types: [FunctionType] = [] var imports: [ImportEntry] = [] - + while parseState.hasMoreBytes() { let sectionId = try parseState.readByte() let sectionSize = try parseState.readUnsignedLEB128() - + switch sectionId { - case 1: // Type section + case 1: // Type section let typeCount = try parseState.readUnsignedLEB128() for _ in 0.. TableType { let elementType = try parseState.readByte() - + let element: TableType.ElementType switch elementType { case 0x70: @@ -243,7 +240,7 @@ private func parseTableType(_ parseState: ParseState) throws -> TableType { default: throw ParseError.unknownTableElementType(elementType) } - + let limits = try parseLimits(parseState) return TableType(element: element, minimum: limits.minimum, maximum: limits.maximum) } @@ -255,7 +252,7 @@ private func parseLimits(_ parseState: ParseState) throws -> MemoryType { let shared = (flags & 2) != 0 let isMemory64 = (flags & 4) != 0 let index: MemoryType.IndexType = isMemory64 ? .i64 : .i32 - + if hasMaximum { let maximum = try parseState.readUnsignedLEB128() return MemoryType(minimum: minimum, maximum: maximum, shared: shared, index: index) @@ -297,18 +294,18 @@ private func parseFunctionType(_ parseState: ParseState) throws -> FunctionType if form != 0x60 { throw ParseError.invalidFunctionTypeForm(form) } - + var parameters: [ValueType] = [] let parameterCount = try parseState.readUnsignedLEB128() for _ in 0..(_ value: T, color: String...) { + appendInterpolation("\(color.map { "\u{001B}\($0)" }.joined())\(value)\u{001B}[0m") + } +} + +class FancyTestsParser { + let write: (String) -> Void + + init(write: @escaping (String) -> Void) { + self.write = write + } + + private enum Status { + case passed, failed, skipped + case unknown(String.SubSequence?) + + var isNegative: Bool { + switch self { + case .failed, .unknown(nil): return true + default: return false + } + } + + init(rawValue: String.SubSequence) { + switch rawValue { + case "passed": self = .passed + case "failed": self = .failed + case "skipped": self = .skipped + default: self = .unknown(rawValue) + } + } + } + + private struct Suite { + let name: String.SubSequence + var status: Status = .unknown(nil) + + var statusLabel: String { + switch status { + case .passed: return "\(" PASSED ", color: "[1m", "[97m", "[42m")" + case .failed: return "\(" FAILED ", color: "[1m", "[97m", "[101m")" + case .skipped: return "\(" SKIPPED ", color: "[1m", "[97m", "[97m")" + case .unknown(let status): + return "\(" \(status ?? "UNKNOWN") ", color: "[1m", "[97m", "[101m")" + } + } + + var cases: [Case] + + struct Case { + let name: String.SubSequence + var statusMark: String { + switch status { + case .passed: return "\("\u{2714}", color: "[92m")" + case .failed: return "\("\u{2718}", color: "[91m")" + case .skipped: return "\("\u{279C}", color: "[97m")" + case .unknown: return "\("?", color: "[97m")" + } + } + var status: Status = .unknown(nil) + var duration: String.SubSequence? + } + } + + private var suites = [Suite]() + + private let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# + private let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# + private lazy var suiteStarted = Regex { + "Test Suite '" + Capture { + OneOrMore(CharacterClass.anyOf("'").inverted) + } + "' started at " + Capture { self.timestamp } + } + private lazy var suiteStatus = Regex { + "Test Suite '" + Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + } + } + " at " + Capture { self.timestamp } + } + private lazy var testCaseStarted = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' started" + } + private lazy var testCaseStatus = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + "skipped" + } + } + " (" + Capture { + OneOrMore(.digit) + "." + OneOrMore(.digit) + } + " seconds)" + } + + private let testSummary = + #/Executed \d+ (test|tests), with (?:\d+ (?:test|tests) skipped and )?\d+ (failure|failures) \((?\d+) unexpected\) in (?\d+\.\d+) \(\d+\.\d+\) seconds/# + + func onLine(_ line: String) { + if let match = line.firstMatch( + of: suiteStarted + ) { + let (_, suite, _) = match.output + suites.append(.init(name: suite, cases: [])) + } else if let match = line.firstMatch( + of: suiteStatus + ) { + let (_, suite, status, _) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + suites[suiteIdx].status = Status(rawValue: status) + flushSingleSuite(suites[suiteIdx]) + } + } else if let match = line.firstMatch( + of: testCaseStarted + ) { + let (_, suite, testCase) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + suites[suiteIdx].cases.append( + .init(name: testCase, duration: nil) + ) + } + } else if let match = line.firstMatch( + of: testCaseStatus + ) { + let (_, suite, testCase, status, duration) = match.output + if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { + if let caseIdx = suites[suiteIdx].cases.firstIndex(where: { + $0.name == testCase + }) { + suites[suiteIdx].cases[caseIdx].status = Status(rawValue: status) + suites[suiteIdx].cases[caseIdx].duration = duration + } + } + } else if line.firstMatch(of: testSummary) != nil { + // do nothing + } else { + if !line.isEmpty { + write(line + "\n") + } + } + } + + private func flushSingleSuite(_ suite: Suite) { + write(suite.statusLabel + " " + suite.name + "\n") + for testCase in suite.cases { + write(" " + testCase.statusMark + " " + testCase.name) + if let duration = testCase.duration { + write(" \("(\(duration)s)", color: "[90m")") + } + write("\n") + } + } + + func finalize() { + write("\n") + + func formatCategory( + label: String, statuses: [Status] + ) -> String { + var passed = 0 + var skipped = 0 + var failed = 0 + var unknown = 0 + for status in statuses { + switch status { + case .passed: passed += 1 + case .skipped: skipped += 1 + case .failed: failed += 1 + case .unknown: unknown += 1 + } + } + var result = "\(label) " + if passed > 0 { + result += "\u{001B}[32m\(passed) passed\u{001B}[0m, " + } + if skipped > 0 { + result += "\u{001B}[97m\(skipped) skipped\u{001B}[0m, " + } + if failed > 0 { + result += "\u{001B}[31m\(failed) failed\u{001B}[0m, " + } + if unknown > 0 { + result += "\u{001B}[31m\(unknown) unknown\u{001B}[0m, " + } + result += "\u{001B}[0m\(statuses.count) total\n" + return result + } + + let suitesWithCases = suites.filter { $0.cases.count > 0 } + write( + formatCategory( + label: "Test Suites:", statuses: suitesWithCases.map(\.status) + ) + ) + let allCaseStatuses = suitesWithCases.flatMap { + $0.cases.map { $0.status } + } + write( + formatCategory( + label: "Tests: ", statuses: allCaseStatuses + ) + ) + + if suites.contains(where: { $0.name == "All tests" }) { + write("\("Ran all test suites.", color: "[90m")\n") // gray + } + + if suites.contains(where: { $0.status.isNegative }) { + write("\n\("Failed test cases:", color: "[31m")\n") + for suite in suites.filter({ $0.status.isNegative }) { + for testCase in suite.cases.filter({ $0.status.isNegative }) { + write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + } + } + + write( + "\n\("Some tests failed. Use --verbose for raw test output.", color: "[33m")\n" + ) + } + } +} diff --git a/Plugins/PackageToJS/Tests/SnapshotTesting.swift b/Plugins/PackageToJS/Tests/SnapshotTesting.swift index 8e556357..4732cfce 100644 --- a/Plugins/PackageToJS/Tests/SnapshotTesting.swift +++ b/Plugins/PackageToJS/Tests/SnapshotTesting.swift @@ -5,7 +5,7 @@ func assertSnapshot( filePath: String = #filePath, function: String = #function, sourceLocation: SourceLocation = #_sourceLocation, variant: String? = nil, - input: Data + input: Data, fileExtension: String = "json" ) throws { let testFileName = URL(fileURLWithPath: filePath).deletingPathExtension().lastPathComponent let snapshotDir = URL(fileURLWithPath: filePath) @@ -13,7 +13,7 @@ func assertSnapshot( .appendingPathComponent("__Snapshots__") .appendingPathComponent(testFileName) try FileManager.default.createDirectory(at: snapshotDir, withIntermediateDirectories: true) - let snapshotFileName: String = "\(function[..:0: error: CounterTests.testThrowFailure : threw error "TestError()" + Test Case 'CounterTests.testThrowFailure' failed (0.002 seconds) + Test Suite 'CounterTests' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + Test Suite '/.xctest' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + Test Suite 'All tests' failed at 2025-03-16 08:40:27.290 + Executed 1 test, with 1 failure (1 unexpected) in 0.002 (0.002) seconds + """ + ) + } + + @Test func testAssertFailure() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 08:43:32.415 + Test Suite '/.xctest' started at 2025-03-16 08:43:32.465 + Test Suite 'CounterTests' started at 2025-03-16 08:43:32.465 + Test Case 'CounterTests.testAssertailure' started at 2025-03-16 08:43:32.465 + /tmp/Tests/CounterTests/CounterTests.swift:27: error: CounterTests.testAssertailure : XCTAssertEqual failed: ("1") is not equal to ("2") - + Test Case 'CounterTests.testAssertailure' failed (0.001 seconds) + Test Suite 'CounterTests' failed at 2025-03-16 08:43:32.467 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + Test Suite '/.xctest' failed at 2025-03-16 08:43:32.467 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + Test Suite 'All tests' failed at 2025-03-16 08:43:32.468 + Executed 1 test, with 1 failure (0 unexpected) in 0.001 (0.001) seconds + """ + ) + } + + @Test func testSkipped() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 09:56:50.924 + Test Suite '/.xctest' started at 2025-03-16 09:56:50.945 + Test Suite 'CounterTests' started at 2025-03-16 09:56:50.945 + Test Case 'CounterTests.testIncrement' started at 2025-03-16 09:56:50.946 + /tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + Test Case 'CounterTests.testIncrement' skipped (0.006 seconds) + Test Case 'CounterTests.testIncrementTwice' started at 2025-03-16 09:56:50.953 + Test Case 'CounterTests.testIncrementTwice' passed (0.0 seconds) + Test Suite 'CounterTests' passed at 2025-03-16 09:56:50.953 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + Test Suite '/.xctest' passed at 2025-03-16 09:56:50.954 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + Test Suite 'All tests' passed at 2025-03-16 09:56:50.954 + Executed 2 tests, with 1 test skipped and 0 failures (0 unexpected) in 0.006 (0.006) seconds + """ + ) + } + + @Test func testCrash() throws { + try assertFancyFormatSnapshot( + """ + Test Suite 'All tests' started at 2025-03-16 09:37:07.882 + Test Suite '/.xctest' started at 2025-03-16 09:37:07.903 + Test Suite 'CounterTests' started at 2025-03-16 09:37:07.903 + Test Case 'CounterTests.testIncrement' started at 2025-03-16 09:37:07.903 + CounterTests/CounterTests.swift:26: Fatal error: Crash + wasm://wasm/CounterPackageTests.xctest-0ef3150a:1 + + + RuntimeError: unreachable + at CounterPackageTests.xctest.$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[5087]:0x1475da) + at CounterPackageTests.xctest.$s12CounterTestsAAC13testIncrementyyYaKFTY1_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1448]:0x9a33b) + at CounterPackageTests.xctest.swift::runJobInEstablishedExecutorContext(swift::Job*) (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29848]:0x58cb39) + at CounterPackageTests.xctest.swift_job_run (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29863]:0x58d720) + at CounterPackageTests.xctest.$sScJ16runSynchronously2onySce_tF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1571]:0x9fe5a) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC10runAllJobsyyF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1675]:0xa32c4) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1674]:0xa30b7) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1666]:0xa2c6b) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1541]:0x9de13) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1540]:0x9dd8d) + """ + ) + } +} diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt new file mode 100644 index 00000000..16683bfb --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAllPassed.txt @@ -0,0 +1,9 @@ + PASSED  CounterTests + ✔ testIncrement (0.002s) + ✔ testIncrementTwice (0.001s) + PASSED  /.xctest + PASSED  All tests + +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total +Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt new file mode 100644 index 00000000..323cd70a --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testAssertFailure.txt @@ -0,0 +1,14 @@ +/tmp/Tests/CounterTests/CounterTests.swift:27: error: CounterTests.testAssertailure : XCTAssertEqual failed: ("1") is not equal to ("2") - + FAILED  CounterTests + ✘ testAssertailure (0.001s) + FAILED  /.xctest + FAILED  All tests + +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Ran all test suites. + +Failed test cases: + ✘ CounterTests.testAssertailure + +Some tests failed. Use --verbose for raw test output. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt new file mode 100644 index 00000000..02977cc1 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testCrash.txt @@ -0,0 +1,22 @@ +CounterTests/CounterTests.swift:26: Fatal error: Crash +wasm://wasm/CounterPackageTests.xctest-0ef3150a:1 +RuntimeError: unreachable + at CounterPackageTests.xctest.$ss17_assertionFailure__4file4line5flagss5NeverOs12StaticStringV_SSAHSus6UInt32VtF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[5087]:0x1475da) + at CounterPackageTests.xctest.$s12CounterTestsAAC13testIncrementyyYaKFTY1_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1448]:0x9a33b) + at CounterPackageTests.xctest.swift::runJobInEstablishedExecutorContext(swift::Job*) (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29848]:0x58cb39) + at CounterPackageTests.xctest.swift_job_run (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[29863]:0x58d720) + at CounterPackageTests.xctest.$sScJ16runSynchronously2onySce_tF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1571]:0x9fe5a) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC10runAllJobsyyF (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1675]:0xa32c4) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1674]:0xa30b7) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC14insertJobQueue3jobyScJ_tFyycfU0_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1666]:0xa2c6b) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_ (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1541]:0x9de13) + at CounterPackageTests.xctest.$s19JavaScriptEventLoopAAC6create33_F9DB15AFB1FFBEDBFE9D13500E01F3F2LLAByFZyyyccfU0_0aB3Kit20ConvertibleToJSValue_pAE0Q0OcfU_TA (wasm://wasm/CounterPackageTests.xctest-0ef3150a:wasm-function[1540]:0x9dd8d) + +Test Suites: 1 unknown, 1 total +Tests: 1 unknown, 1 total +Ran all test suites. + +Failed test cases: + ? CounterTests.testIncrement + +Some tests failed. Use --verbose for raw test output. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt new file mode 100644 index 00000000..9873deff --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testSkipped.txt @@ -0,0 +1,10 @@ +/tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + PASSED  CounterTests + ➜ testIncrement (0.006s) + ✔ testIncrementTwice (0.0s) + PASSED  /.xctest + PASSED  All tests + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 skipped, 2 total +Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt new file mode 100644 index 00000000..9c2e3613 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestsParserTests/testThrowFailure.txt @@ -0,0 +1,14 @@ +:0: error: CounterTests.testThrowFailure : threw error "TestError()" + FAILED  CounterTests + ✘ testThrowFailure (0.002s) + FAILED  /.xctest + FAILED  All tests + +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total +Ran all test suites. + +Failed test cases: + ✘ CounterTests.testThrowFailure + +Some tests failed. Use --verbose for raw test output. diff --git a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift index 0609232a..1cd62833 100644 --- a/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift +++ b/Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift @@ -235,7 +235,7 @@ final class JavaScriptEventLoopTests: XCTestCase { let result = try await promise!.value XCTAssertEqual(result, .number(3)) } - XCTAssertGreaterThanOrEqual(closureDiff, 200) + XCTAssertGreaterThanOrEqual(closureDiff, 150) } // MARK: - Clock Tests