From 085ddc6474b605855cbcf2290992b31ce0f1e8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 7 Apr 2026 22:39:48 -0300 Subject: [PATCH 1/2] refactor: extract recordResult in TestExecutionStage --- .../Execution/TestExecutionStage.swift | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) 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) From bab1fee8cc1c1b2007a28d6b7842816f06058a6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 7 Apr 2026 22:39:57 -0300 Subject: [PATCH 2/2] test: cover external symlink fallback in TestFilesHasher --- .../Infrastructure/TestFilesHasherTests.swift | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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")