diff --git a/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift b/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift index 57861bf..002f4fa 100644 --- a/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift +++ b/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift @@ -63,12 +63,19 @@ struct IncompatibleMutantExecutor: Sendable { let duration = launched.duration await pool.release(slot) - let outcome = try await ResultParser(launcher: launcher).parse( - exitCode: launched.exitCode, - output: launched.output, - xcresultPath: launched.xcresultPath, - timeout: configuration.build.timeout - ) + let outcome: TestRunOutcome + switch configuration.build.projectType { + case .xcode: + outcome = try await ResultParser(launcher: launcher).parse( + exitCode: launched.exitCode, + output: launched.output, + xcresultPath: launched.xcresultPath, + timeout: configuration.build.timeout + ) + + case .spm: + outcome = spmOutcome(exitCode: launched.exitCode, output: launched.output) + } try? sandbox.cleanup() @@ -85,17 +92,25 @@ struct IncompatibleMutantExecutor: Sendable { sandbox: Sandbox, configuration: RunnerConfiguration ) async throws -> IncompatibleTestLaunchResult { - let derivedDataPath = sandbox.rootURL - .appendingPathComponent(".derived-data").path + switch configuration.build.projectType { + case .xcode(let scheme, _): + return try await launchXcode( + scheme: scheme, slot: slot, sandbox: sandbox, configuration: configuration) + case .spm: + return try await launchSPM(sandbox: sandbox, configuration: configuration) + } + } + + private func launchXcode( + scheme: String, + slot: SimulatorSlot, + sandbox: Sandbox, + configuration: RunnerConfiguration + ) async throws -> IncompatibleTestLaunchResult { + let derivedDataPath = sandbox.rootURL.appendingPathComponent(".derived-data").path let xcresultPath = sandbox.rootURL .appendingPathComponent("\(UUID().uuidString).xcresult").path - guard case .xcode(let scheme, _) = configuration.build.projectType else { - return IncompatibleTestLaunchResult( - exitCode: 0, output: "", xcresultPath: xcresultPath, duration: 0 - ) - } - var arguments = [ "test", "-scheme", scheme, @@ -126,6 +141,44 @@ struct IncompatibleMutantExecutor: Sendable { ) } + private func launchSPM( + sandbox: Sandbox, + configuration: RunnerConfiguration + ) async throws -> IncompatibleTestLaunchResult { + var arguments = ["test"] + + if let testTarget = configuration.build.testTarget { + arguments += ["--filter", testTarget] + } + + let start = Date() + let captured = try await launcher.launchCapturing( + executableURL: URL(fileURLWithPath: "/usr/bin/swift"), + arguments: arguments, + environment: nil, + workingDirectoryURL: sandbox.rootURL, + timeout: configuration.build.timeout + ) + + return IncompatibleTestLaunchResult( + exitCode: captured.exitCode, + output: captured.output, + xcresultPath: "", + duration: Date().timeIntervalSince(start) + ) + } + + private func spmOutcome(exitCode: Int32, output: String) -> TestRunOutcome { + if exitCode == -1 { return .timedOut } + if exitCode == 0 { return .testsSucceeded } + + switch TestOutputParser().parse(output) { + case .killed(let name): return .testsFailed(failingTest: name) + case .crashed: return .crashed + case .unviable: return .unviable + } + } + private func storeAndReport( mutant: MutantDescriptor, key: MutantCacheKey, diff --git a/Tests/SwiftMutationTestingTests/Unit/Execution/IncompatibleMutantExecutorTests.swift b/Tests/SwiftMutationTestingTests/Unit/Execution/IncompatibleMutantExecutorTests.swift index d3929b3..7775902 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Execution/IncompatibleMutantExecutorTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Execution/IncompatibleMutantExecutorTests.swift @@ -218,6 +218,68 @@ struct IncompatibleMutantExecutorTests { } } + @Test("Given SPM project type and exit code 0, when execute called, then mutant survived") + func spmExitCodeZeroProducesSurvivedStatus() async throws { + let dir = try FileHelpers.makeTemporaryDirectory() + defer { FileHelpers.cleanup(dir) } + + let executor = makeExecutorSPM(in: dir, exitCode: 0) + let pool = makePool() + try await pool.setUp() + + let results = try await executor.execute( + [makeMutant(id: "m0", content: "let x = 1")], + configuration: makeConfigurationSPM(projectPath: dir.path), + pool: pool, + testFilesHash: "hash" + ) + + #expect(results.first?.status == .survived) + } + + @Test("Given SPM project type and exit code 1 with failure output, when execute called, then mutant is killed") + func spmExitCodeOneWithFailureOutputProducesKilledStatus() async throws { + let dir = try FileHelpers.makeTemporaryDirectory() + defer { FileHelpers.cleanup(dir) } + + let output = #"Test "myTest" failed after 0.001 seconds."# + let executor = makeExecutorSPM(in: dir, exitCode: 1, output: output) + let pool = makePool() + try await pool.setUp() + + let results = try await executor.execute( + [makeMutant(id: "m0", content: "let x = 1")], + configuration: makeConfigurationSPM(projectPath: dir.path), + pool: pool, + testFilesHash: "hash" + ) + + #expect(results.first?.status == .killed(by: "myTest")) + } + + private func makeExecutorSPM( + in dir: URL, + exitCode: Int32, + output: String = "" + ) -> IncompatibleMutantExecutor { + IncompatibleMutantExecutor( + launcher: MockProcessLauncher(exitCode: exitCode, output: output), + sandboxFactory: SandboxFactory(), + cacheStore: CacheStore(storePath: dir.appendingPathComponent("cache.json").path), + reporter: MockProgressReporter(), + counter: MutationCounter(total: 1) + ) + } + + private func makeConfigurationSPM(projectPath: String) -> RunnerConfiguration { + RunnerConfiguration( + projectPath: projectPath, + build: .init(projectType: .spm, timeout: 60, concurrency: 1, noCache: false), + reporting: .init(quiet: true), + filter: .init(excludePatterns: [], operators: []) + ) + } + private func makeExecutor(in dir: URL, exitCode: Int32) -> IncompatibleMutantExecutor { IncompatibleMutantExecutor( launcher: MockProcessLauncher(exitCode: exitCode),