From faa935932da9722b7a29e8915f2319aae38dc688 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 08:32:32 +0000 Subject: [PATCH 1/5] PackageToJS: Bring XCTest output formatter from carton --- Examples/Testing/Package.swift | 6 +- Plugins/PackageToJS/Sources/PackageToJS.swift | 65 +++++ .../Sources/PackageToJSPlugin.swift | 4 + Plugins/PackageToJS/Sources/TestsParser.swift | 259 ++++++++++++++++++ .../PackageToJS/Tests/SnapshotTesting.swift | 4 +- .../PackageToJS/Tests/TestParserTests.swift | 137 +++++++++ .../TestParserTests/testAllPassed.txt | 9 + .../TestParserTests/testAssertFailure.txt | 14 + .../TestParserTests/testCrash.txt | 22 ++ .../TestParserTests/testSkipped.txt | 10 + .../TestParserTests/testThrowFailure.txt | 14 + 11 files changed, 541 insertions(+), 3 deletions(-) create mode 100644 Plugins/PackageToJS/Sources/TestsParser.swift create mode 100644 Plugins/PackageToJS/Tests/TestParserTests.swift create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt create mode 100644 Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt 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..4d5e44ee 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 } @@ -85,6 +87,7 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, extraArguments: extraArguments, + testParser: testOptions.verbose ? nil : FancyTestsParser(), testOptions: testOptions ) } @@ -119,6 +122,7 @@ struct PackageToJS { testRunner: URL, currentDirectoryURL: URL, extraArguments: [String], + testParser: (any TestsParser)? = nil, testOptions: TestOptions ) throws { let node = try which("node") @@ -129,11 +133,39 @@ struct PackageToJS { let task = Process() task.executableURL = node task.arguments = arguments + + var finalize: () -> Void = {} + if let testParser = testParser { + class Writer: InteractiveWriter { + func write(_ string: String) { + print(string, terminator: "") + } + } + + let writer = Writer() + let stdoutBuffer = LineBuffer { line in + testParser.onLine(line, writer) + } + 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(writer) + } + } + 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 { throw PackageToJSError("Test failed with status \(task.terminationStatus)") @@ -151,6 +183,39 @@ struct PackageToJS { 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 += 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 { 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/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift new file mode 100644 index 00000000..d222dd2e --- /dev/null +++ b/Plugins/PackageToJS/Sources/TestsParser.swift @@ -0,0 +1,259 @@ +/// The original implementation of this file is from Carton. +/// https://github.com/swiftwasm/carton/blob/1.1.3/Sources/carton-frontend-slim/TestRunners/TestsParser.swift + +import Foundation +import RegexBuilder + +protocol InteractiveWriter { + func write(_ string: String) +} + +protocol TestsParser { + /// Parse the output of a test process, format it, then output in the `InteractiveWriter`. + func onLine(_ line: String, _ terminal: InteractiveWriter) + func finalize(_ terminal: InteractiveWriter) +} + +extension String.StringInterpolation { + /// Display `value` with the specified ANSI-escaped `color` values, then apply the reset. + fileprivate mutating func appendInterpolation(_ value: T, color: String...) { + appendInterpolation("\(color.map { "\u{001B}\($0)" }.joined())\(value)\u{001B}[0m") + } +} + +class FancyTestsParser: TestsParser { + init() {} + + enum Status: Equatable { + 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) + } + } + } + + 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? + } + } + + var suites = [Suite]() + + let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# + let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# + lazy var suiteStarted = Regex { + "Test Suite '" + Capture { + OneOrMore(CharacterClass.anyOf("'").inverted) + } + "' started at " + Capture { self.timestamp } + } + lazy var suiteStatus = Regex { + "Test Suite '" + Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + } + } + " at " + Capture { self.timestamp } + } + lazy var testCaseStarted = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' started" + } + lazy var testCaseStatus = Regex { + "Test Case '" + Capture { self.swiftIdentifier } + "." + Capture { self.swiftIdentifier } + "' " + Capture { + ChoiceOf { + "failed" + "passed" + "skipped" + } + } + " (" + Capture { + OneOrMore(.digit) + "." + OneOrMore(.digit) + } + " seconds)" + } + + 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, _ terminal: InteractiveWriter) { + 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], terminal) + } + } 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 { + terminal.write(line + "\n") + } + } + } + + func finalize(_ terminal: InteractiveWriter) { + terminal.write("\n") + flushSummary(of: suites, terminal) + } + + private func flushSingleSuite(_ suite: Suite, _ terminal: InteractiveWriter) { + terminal.write(suite.statusLabel) + terminal.write(" \(suite.name)\n") + for testCase in suite.cases { + terminal.write(" \(testCase.statusMark) ") + if let duration = testCase.duration { + terminal + .write( + "\(testCase.name) \("(\(Int(Double(duration)! * 1000))ms)", color: "[90m")\n" + ) // gray + } + } + } + + private func flushSummary(of suites: [Suite], _ terminal: InteractiveWriter) { + let suitesWithCases = suites.filter { $0.cases.count > 0 } + + terminal.write("Test Suites: ") + let suitesPassed = suitesWithCases.filter { $0.status == .passed }.count + if suitesPassed > 0 { + terminal.write("\("\(suitesPassed) passed", color: "[32m"), ") + } + let suitesSkipped = suitesWithCases.filter { $0.status == .skipped }.count + if suitesSkipped > 0 { + terminal.write("\("\(suitesSkipped) skipped", color: "[97m"), ") + } + let suitesFailed = suitesWithCases.filter { $0.status == .failed }.count + if suitesFailed > 0 { + terminal.write("\("\(suitesFailed) failed", color: "[31m"), ") + } + let suitesUnknown = suitesWithCases.filter { $0.status == .unknown(nil) }.count + if suitesUnknown > 0 { + terminal.write("\("\(suitesUnknown) unknown", color: "[31m"), ") + } + terminal.write("\(suitesWithCases.count) total\n") + + terminal.write("Tests: ") + let allTests = suitesWithCases.map(\.cases).reduce([], +) + let testsPassed = allTests.filter { $0.status == .passed }.count + if testsPassed > 0 { + terminal.write("\("\(testsPassed) passed", color: "[32m"), ") + } + let testsSkipped = allTests.filter { $0.status == .skipped }.count + if testsSkipped > 0 { + terminal.write("\("\(testsSkipped) skipped", color: "[97m"), ") + } + let testsFailed = allTests.filter { $0.status == .failed }.count + if testsFailed > 0 { + terminal.write("\("\(testsFailed) failed", color: "[31m"), ") + } + let testsUnknown = allTests.filter { $0.status == .unknown(nil) }.count + if testsUnknown > 0 { + terminal.write("\("\(testsUnknown) unknown", color: "[31m"), ") + } + terminal.write("\(allTests.count) total\n") + + if suites.contains(where: { $0.name == "All tests" }) { + terminal.write("\("Ran all test suites.", color: "[90m")\n") // gray + } + + if suites.contains(where: { $0.status.isNegative }) { + print(suites.filter({ $0.status.isNegative })) + terminal.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 }) { + terminal.write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + } + } + + terminal.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__/TestParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt new file mode 100644 index 00000000..7c1d56a6 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt @@ -0,0 +1,9 @@ + PASSED  CounterTests + ✔ testIncrement (2ms) + ✔ testIncrementTwice (1ms) + 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__/TestParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt new file mode 100644 index 00000000..2adb698c --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/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 (1ms) + 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__/TestParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt new file mode 100644 index 00000000..ada55fb0 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/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__/TestParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt new file mode 100644 index 00000000..eb945cc9 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt @@ -0,0 +1,10 @@ +/tmp/Tests/CounterTests/CounterTests.swift:25: CounterTests.testIncrement : Test skipped - Skip it + PASSED  CounterTests + ➜ testIncrement (6ms) + ✔ testIncrementTwice (0ms) + 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__/TestParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt new file mode 100644 index 00000000..ec5115e4 --- /dev/null +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt @@ -0,0 +1,14 @@ +:0: error: CounterTests.testThrowFailure : threw error "TestError()" + FAILED  CounterTests + ✘ testThrowFailure (2ms) + 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. From 6bf418e82174e7e4112b778c6ac61f2c7ca855cb Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:30:47 +0000 Subject: [PATCH 2/5] Optimize compile-time --- Plugins/PackageToJS/Sources/PackageToJS.swift | 15 +- Plugins/PackageToJS/Sources/TestsParser.swift | 153 +++++++++--------- ...rserTests.swift => TestsParserTests.swift} | 19 +-- .../TestParserTests/testAllPassed.txt | 4 +- .../TestParserTests/testAssertFailure.txt | 4 +- .../TestParserTests/testCrash.txt | 4 +- .../TestParserTests/testSkipped.txt | 4 +- .../TestParserTests/testThrowFailure.txt | 4 +- 8 files changed, 94 insertions(+), 113 deletions(-) rename Plugins/PackageToJS/Tests/{TestParserTests.swift => TestsParserTests.swift} (94%) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 4d5e44ee..1f7c1e18 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -87,7 +87,7 @@ struct PackageToJS { try PackageToJS.runSingleTestingLibrary( testRunner: testRunner, currentDirectoryURL: currentDirectoryURL, extraArguments: extraArguments, - testParser: testOptions.verbose ? nil : FancyTestsParser(), + testParser: testOptions.verbose ? nil : FancyTestsParser(write: { print($0, terminator: "") }), testOptions: testOptions ) } @@ -122,7 +122,7 @@ struct PackageToJS { testRunner: URL, currentDirectoryURL: URL, extraArguments: [String], - testParser: (any TestsParser)? = nil, + testParser: FancyTestsParser? = nil, testOptions: TestOptions ) throws { let node = try which("node") @@ -136,15 +136,8 @@ struct PackageToJS { var finalize: () -> Void = {} if let testParser = testParser { - class Writer: InteractiveWriter { - func write(_ string: String) { - print(string, terminator: "") - } - } - - let writer = Writer() let stdoutBuffer = LineBuffer { line in - testParser.onLine(line, writer) + testParser.onLine(line) } let stdoutPipe = Pipe() stdoutPipe.fileHandleForReading.readabilityHandler = { handle in @@ -156,7 +149,7 @@ struct PackageToJS { stdoutBuffer.append(data) } stdoutBuffer.flush() - testParser.finalize(writer) + testParser.finalize() } } diff --git a/Plugins/PackageToJS/Sources/TestsParser.swift b/Plugins/PackageToJS/Sources/TestsParser.swift index d222dd2e..efd75712 100644 --- a/Plugins/PackageToJS/Sources/TestsParser.swift +++ b/Plugins/PackageToJS/Sources/TestsParser.swift @@ -4,16 +4,6 @@ import Foundation import RegexBuilder -protocol InteractiveWriter { - func write(_ string: String) -} - -protocol TestsParser { - /// Parse the output of a test process, format it, then output in the `InteractiveWriter`. - func onLine(_ line: String, _ terminal: InteractiveWriter) - func finalize(_ terminal: InteractiveWriter) -} - extension String.StringInterpolation { /// Display `value` with the specified ANSI-escaped `color` values, then apply the reset. fileprivate mutating func appendInterpolation(_ value: T, color: String...) { @@ -21,10 +11,14 @@ extension String.StringInterpolation { } } -class FancyTestsParser: TestsParser { - init() {} +class FancyTestsParser { + let write: (String) -> Void - enum Status: Equatable { + init(write: @escaping (String) -> Void) { + self.write = write + } + + private enum Status: Equatable { case passed, failed, skipped case unknown(String.SubSequence?) @@ -45,7 +39,7 @@ class FancyTestsParser: TestsParser { } } - struct Suite { + private struct Suite { let name: String.SubSequence var status: Status = .unknown(nil) @@ -76,11 +70,11 @@ class FancyTestsParser: TestsParser { } } - var suites = [Suite]() + private var suites = [Suite]() - let swiftIdentifier = #/[_\p{L}\p{Nl}][_\p{L}\p{Nl}\p{Mn}\p{Nd}\p{Pc}]*/# - let timestamp = #/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}/# - lazy var suiteStarted = Regex { + 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) @@ -88,7 +82,7 @@ class FancyTestsParser: TestsParser { "' started at " Capture { self.timestamp } } - lazy var suiteStatus = Regex { + private lazy var suiteStatus = Regex { "Test Suite '" Capture { OneOrMore(CharacterClass.anyOf("'").inverted) } "' " @@ -101,14 +95,14 @@ class FancyTestsParser: TestsParser { " at " Capture { self.timestamp } } - lazy var testCaseStarted = Regex { + private lazy var testCaseStarted = Regex { "Test Case '" Capture { self.swiftIdentifier } "." Capture { self.swiftIdentifier } "' started" } - lazy var testCaseStatus = Regex { + private lazy var testCaseStatus = Regex { "Test Case '" Capture { self.swiftIdentifier } "." @@ -130,10 +124,10 @@ class FancyTestsParser: TestsParser { " seconds)" } - let testSummary = + 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, _ terminal: InteractiveWriter) { + func onLine(_ line: String) { if let match = line.firstMatch( of: suiteStarted ) { @@ -145,7 +139,7 @@ class FancyTestsParser: TestsParser { let (_, suite, status, _) = match.output if let suiteIdx = suites.firstIndex(where: { $0.name == suite }) { suites[suiteIdx].status = Status(rawValue: status) - flushSingleSuite(suites[suiteIdx], terminal) + flushSingleSuite(suites[suiteIdx]) } } else if let match = line.firstMatch( of: testCaseStarted @@ -172,86 +166,87 @@ class FancyTestsParser: TestsParser { // do nothing } else { if !line.isEmpty { - terminal.write(line + "\n") + write(line + "\n") } } } - func finalize(_ terminal: InteractiveWriter) { - terminal.write("\n") - flushSummary(of: suites, terminal) - } - - private func flushSingleSuite(_ suite: Suite, _ terminal: InteractiveWriter) { - terminal.write(suite.statusLabel) - terminal.write(" \(suite.name)\n") + private func flushSingleSuite(_ suite: Suite) { + write(suite.statusLabel) + write(" \(suite.name)\n") for testCase in suite.cases { - terminal.write(" \(testCase.statusMark) ") + write(" \(testCase.statusMark) ") if let duration = testCase.duration { - terminal - .write( + write( "\(testCase.name) \("(\(Int(Double(duration)! * 1000))ms)", color: "[90m")\n" ) // gray } } } - private func flushSummary(of suites: [Suite], _ terminal: InteractiveWriter) { - let suitesWithCases = suites.filter { $0.cases.count > 0 } - - terminal.write("Test Suites: ") - let suitesPassed = suitesWithCases.filter { $0.status == .passed }.count - if suitesPassed > 0 { - terminal.write("\("\(suitesPassed) passed", color: "[32m"), ") - } - let suitesSkipped = suitesWithCases.filter { $0.status == .skipped }.count - if suitesSkipped > 0 { - terminal.write("\("\(suitesSkipped) skipped", color: "[97m"), ") - } - let suitesFailed = suitesWithCases.filter { $0.status == .failed }.count - if suitesFailed > 0 { - terminal.write("\("\(suitesFailed) failed", color: "[31m"), ") - } - let suitesUnknown = suitesWithCases.filter { $0.status == .unknown(nil) }.count - if suitesUnknown > 0 { - terminal.write("\("\(suitesUnknown) unknown", color: "[31m"), ") + 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 } - terminal.write("\(suitesWithCases.count) total\n") - terminal.write("Tests: ") - let allTests = suitesWithCases.map(\.cases).reduce([], +) - let testsPassed = allTests.filter { $0.status == .passed }.count - if testsPassed > 0 { - terminal.write("\("\(testsPassed) passed", color: "[32m"), ") - } - let testsSkipped = allTests.filter { $0.status == .skipped }.count - if testsSkipped > 0 { - terminal.write("\("\(testsSkipped) skipped", color: "[97m"), ") - } - let testsFailed = allTests.filter { $0.status == .failed }.count - if testsFailed > 0 { - terminal.write("\("\(testsFailed) failed", color: "[31m"), ") - } - let testsUnknown = allTests.filter { $0.status == .unknown(nil) }.count - if testsUnknown > 0 { - terminal.write("\("\(testsUnknown) unknown", color: "[31m"), ") + 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 } } - terminal.write("\(allTests.count) total\n") + write( + formatCategory( + label: "Tests: ", statuses: allCaseStatuses + ) + ) if suites.contains(where: { $0.name == "All tests" }) { - terminal.write("\("Ran all test suites.", color: "[90m")\n") // gray + write("\("Ran all test suites.", color: "[90m")\n") // gray } if suites.contains(where: { $0.status.isNegative }) { - print(suites.filter({ $0.status.isNegative })) - terminal.write("\n\("Failed test cases:", color: "[31m")\n") + 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 }) { - terminal.write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") + write(" \(testCase.statusMark) \(suite.name).\(testCase.name)\n") } } - terminal.write( + write( "\n\("Some tests failed. Use --verbose for raw test output.", color: "[33m")\n" ) } diff --git a/Plugins/PackageToJS/Tests/TestParserTests.swift b/Plugins/PackageToJS/Tests/TestsParserTests.swift similarity index 94% rename from Plugins/PackageToJS/Tests/TestParserTests.swift rename to Plugins/PackageToJS/Tests/TestsParserTests.swift index a42c8699..099febf1 100644 --- a/Plugins/PackageToJS/Tests/TestParserTests.swift +++ b/Plugins/PackageToJS/Tests/TestsParserTests.swift @@ -3,30 +3,23 @@ import Testing @testable import PackageToJS -@Suite struct TestParserTests { +@Suite struct TestsParserTests { func assertFancyFormatSnapshot( _ input: String, filePath: String = #filePath, function: String = #function, sourceLocation: SourceLocation = #_sourceLocation ) throws { - let parser = FancyTestsParser() + var output = "" + let parser = FancyTestsParser(write: { output += $0 }) let lines = input.split(separator: "\n", omittingEmptySubsequences: false) - class Writer: InteractiveWriter { - var output = "" - func write(_ string: String) { - output += string - } - } - - let writer = Writer() for line in lines { - parser.onLine(String(line), writer) + parser.onLine(String(line)) } - parser.finalize(writer) + parser.finalize() try assertSnapshot( filePath: filePath, function: function, sourceLocation: sourceLocation, - input: Data(writer.output.utf8), fileExtension: "txt", + input: Data(output.utf8), fileExtension: "txt", ) } diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt index 7c1d56a6..121c0519 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAllPassed.txt @@ -4,6 +4,6 @@  PASSED  /.xctest  PASSED  All tests -Test Suites: 1 passed, 1 total -Tests: 2 passed, 2 total +Test Suites: 1 passed, 1 total +Tests: 2 passed, 2 total Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt index 2adb698c..75dc7a9a 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testAssertFailure.txt @@ -4,8 +4,8 @@  FAILED  /.xctest  FAILED  All tests -Test Suites: 1 failed, 1 total -Tests: 1 failed, 1 total +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total Ran all test suites. Failed test cases: diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt index ada55fb0..02977cc1 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testCrash.txt @@ -12,8 +12,8 @@ RuntimeError: unreachable 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 +Test Suites: 1 unknown, 1 total +Tests: 1 unknown, 1 total Ran all test suites. Failed test cases: diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt index eb945cc9..7d10905f 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testSkipped.txt @@ -5,6 +5,6 @@  PASSED  /.xctest  PASSED  All tests -Test Suites: 1 passed, 1 total -Tests: 1 passed, 1 skipped, 2 total +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 skipped, 2 total Ran all test suites. diff --git a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt index ec5115e4..d33db731 100644 --- a/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt +++ b/Plugins/PackageToJS/Tests/__Snapshots__/TestParserTests/testThrowFailure.txt @@ -4,8 +4,8 @@  FAILED  /.xctest  FAILED  All tests -Test Suites: 1 failed, 1 total -Tests: 1 failed, 1 total +Test Suites: 1 failed, 1 total +Tests: 1 failed, 1 total Ran all test suites. Failed test cases: From 3002a2a52f895a3857c8830efa2f44d76288f382 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:38:41 +0000 Subject: [PATCH 3/5] test: Relax the timing constraint --- Tests/JavaScriptEventLoopTests/JavaScriptEventLoopTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 5d8f43eb685ff453e7af360272dae7921105e756 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 12:42:55 +0000 Subject: [PATCH 4/5] Fix 6.0 build of PackageToJS --- Plugins/PackageToJS/Tests/TestsParserTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Plugins/PackageToJS/Tests/TestsParserTests.swift b/Plugins/PackageToJS/Tests/TestsParserTests.swift index 099febf1..cb0f7d20 100644 --- a/Plugins/PackageToJS/Tests/TestsParserTests.swift +++ b/Plugins/PackageToJS/Tests/TestsParserTests.swift @@ -19,7 +19,7 @@ import Testing parser.finalize() try assertSnapshot( filePath: filePath, function: function, sourceLocation: sourceLocation, - input: Data(output.utf8), fileExtension: "txt", + input: Data(output.utf8), fileExtension: "txt" ) } From e5210527127a2eae4f7a213726be9ebacd18471c Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Sun, 16 Mar 2025 13:25:12 +0000 Subject: [PATCH 5/5] Optimize compile-time --- Plugins/PackageToJS/Sources/PackageToJS.swift | 63 +++++++---- Plugins/PackageToJS/Sources/ParseWasm.swift | 105 +++++++++--------- Plugins/PackageToJS/Sources/TestsParser.swift | 12 +- .../testAllPassed.txt | 4 +- .../testAssertFailure.txt | 2 +- .../testCrash.txt | 0 .../testSkipped.txt | 4 +- .../testThrowFailure.txt | 2 +- 8 files changed, 101 insertions(+), 91 deletions(-) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testAllPassed.txt (70%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testAssertFailure.txt (91%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testCrash.txt (100%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testSkipped.txt (78%) rename Plugins/PackageToJS/Tests/__Snapshots__/{TestParserTests => TestsParserTests}/testThrowFailure.txt (89%) diff --git a/Plugins/PackageToJS/Sources/PackageToJS.swift b/Plugins/PackageToJS/Sources/PackageToJS.swift index 1f7c1e18..03af2c73 100644 --- a/Plugins/PackageToJS/Sources/PackageToJS.swift +++ b/Plugins/PackageToJS/Sources/PackageToJS.swift @@ -61,28 +61,31 @@ 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, @@ -95,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, @@ -109,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 { @@ -126,7 +135,11 @@ struct PackageToJS { 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) @@ -160,16 +173,16 @@ struct PackageToJS { } 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) @@ -194,7 +207,7 @@ struct PackageToJS { func append(_ data: String) { lock.lock() defer { lock.unlock() } - buffer += data + buffer.append(data) let lines = buffer.split(separator: "\n", omittingEmptySubsequences: false) for line in lines.dropLast() { handler(String(line)) @@ -567,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(), ] @@ -587,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/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..:0: error: CounterTests.testThrowFailure : threw error "TestError()"  FAILED  CounterTests - ✘ testThrowFailure (2ms) + ✘ testThrowFailure (0.002s)  FAILED  /.xctest  FAILED  All tests