diff --git a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift index e6ecbcd..b344567 100644 --- a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift +++ b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift @@ -72,21 +72,7 @@ struct TestExecutionStage: Sendable { ) try? FileManager.default.removeItem(atPath: launched.xcresultPath) - let status = outcome.asExecutionStatus - let killerTestFile = resolveKillerTestFile(status: status) - let result = ExecutionResult( - descriptor: mutant, status: status, testDuration: launched.duration, - killerTestFile: killerTestFile - ) - await deps.cacheStore.store(status: status, for: key, killerTestFile: killerTestFile) - let index = await deps.counter.increment() - await deps.reporter.report( - .mutantFinished( - descriptor: mutant, status: status, - index: index, total: deps.counter.total - ) - ) - return result + return await recordResult(mutant: mutant, key: key, outcome: outcome, duration: launched.duration) } private func runSPM( @@ -105,10 +91,19 @@ struct TestExecutionStage: Sendable { let outcome = SPMResultParser().parse(exitCode: launched.exitCode, output: launched.output) await context.pool.release(slot) + return await recordResult(mutant: mutant, key: key, outcome: outcome, duration: launched.duration) + } + + private func recordResult( + mutant: MutantDescriptor, + key: MutantCacheKey, + outcome: TestRunOutcome, + duration: Double + ) async -> ExecutionResult { let status = outcome.asExecutionStatus let killerTestFile = resolveKillerTestFile(status: status) let result = ExecutionResult( - descriptor: mutant, status: status, testDuration: launched.duration, + descriptor: mutant, status: status, testDuration: duration, killerTestFile: killerTestFile ) await deps.cacheStore.store(status: status, for: key, killerTestFile: killerTestFile) diff --git a/Tests/SwiftMutationTestingTests/Unit/Infrastructure/TestFilesHasherTests.swift b/Tests/SwiftMutationTestingTests/Unit/Infrastructure/TestFilesHasherTests.swift index c6c4a51..71eb06b 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Infrastructure/TestFilesHasherTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Infrastructure/TestFilesHasherTests.swift @@ -62,6 +62,29 @@ struct TestFilesHasherTests { #expect(result.keys.contains("Tests/FooTests.swift")) } + @Test("Given test file symlinked outside project, when hashPerFile called, then absolute path is used as key") + func hashPerFileUsesAbsolutePathForExternalSymlink() throws { + let projectDir = try FileHelpers.makeTemporaryDirectory() + defer { FileHelpers.cleanup(projectDir) } + + let externalDir = try FileHelpers.makeTemporaryDirectory() + defer { FileHelpers.cleanup(externalDir) } + + try FileHelpers.write("let t = 1", named: "ExternalTests.swift", in: externalDir) + + let testsDir = projectDir.appendingPathComponent("Tests") + try FileManager.default.createDirectory(at: testsDir, withIntermediateDirectories: true) + let symlinkURL = testsDir.appendingPathComponent("ExternalTests.swift") + let targetURL = externalDir.appendingPathComponent("ExternalTests.swift") + try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: targetURL) + + let result = TestFilesHasher().hashPerFile(projectPath: projectDir.path) + + #expect(result.count == 1) + let key = result.keys.first! + #expect(!key.hasPrefix("Tests/")) + } + @Test("Given non-existent path, when hashPerFile called, then empty map is returned") func hashPerFileReturnsEmptyForNonExistentPath() { let result = TestFilesHasher().hashPerFile(projectPath: "/nonexistent/path/xyz")