From f00faaa9bcae60e89ee4787d5d768afb44eec4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:39:35 -0300 Subject: [PATCH 1/6] fix: merge parent environment in TestExecutionStage launchSPM --- .../SwiftMutationTesting/Execution/TestExecutionStage.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift index b3412d0..a914099 100644 --- a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift +++ b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift @@ -113,11 +113,14 @@ struct TestExecutionStage: Sendable { arguments += ["--filter", testTarget] } + var environment = ProcessInfo.processInfo.environment + environment["__SWIFT_MUTATION_TESTING_ACTIVE"] = mutant.id + let start = Date() let captured = try await launcher.launchCapturing( executableURL: URL(fileURLWithPath: "/usr/bin/swift"), arguments: arguments, - environment: ["__SWIFT_MUTATION_TESTING_ACTIVE": mutant.id], + environment: environment, workingDirectoryURL: context.sandbox.rootURL, timeout: context.configuration.build.timeout ) From 0a8742e2538e05e482310fea26b88bcbeb727d01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:41:02 -0300 Subject: [PATCH 2/6] feat: add CalcLibrary SPM fixture --- .gitignore | 1 + Fixtures/CalcLibrary/Package.swift | 11 +++++++++ .../Sources/CalcLibrary/Calculator.swift | 5 ++++ .../Sources/CalcLibrary/Logic.swift | 3 +++ .../Sources/CalcLibrary/Validator.swift | 3 +++ .../CalcLibraryTests/CalcLibraryTests.swift | 23 +++++++++++++++++++ 6 files changed, 46 insertions(+) create mode 100644 Fixtures/CalcLibrary/Package.swift create mode 100644 Fixtures/CalcLibrary/Sources/CalcLibrary/Calculator.swift create mode 100644 Fixtures/CalcLibrary/Sources/CalcLibrary/Logic.swift create mode 100644 Fixtures/CalcLibrary/Sources/CalcLibrary/Validator.swift create mode 100644 Fixtures/CalcLibrary/Tests/CalcLibraryTests/CalcLibraryTests.swift diff --git a/.gitignore b/.gitignore index ff38c9a..c96eecd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ Makefile # Generated by swift-mutation-testing .swift-mutation-testing-cache/ Fixtures/CalcApp/.xmr-cache/ +Fixtures/CalcLibrary/.build/ diff --git a/Fixtures/CalcLibrary/Package.swift b/Fixtures/CalcLibrary/Package.swift new file mode 100644 index 0000000..254ee3a --- /dev/null +++ b/Fixtures/CalcLibrary/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "CalcLibrary", + platforms: [.macOS(.v13)], + targets: [ + .target(name: "CalcLibrary"), + .testTarget(name: "CalcLibraryTests", dependencies: ["CalcLibrary"]), + ] +) diff --git a/Fixtures/CalcLibrary/Sources/CalcLibrary/Calculator.swift b/Fixtures/CalcLibrary/Sources/CalcLibrary/Calculator.swift new file mode 100644 index 0000000..79ce862 --- /dev/null +++ b/Fixtures/CalcLibrary/Sources/CalcLibrary/Calculator.swift @@ -0,0 +1,5 @@ +struct Calculator { + func add(_ a: Int, _ b: Int) -> Int { a + b } + func subtract(_ a: Int, _ b: Int) -> Int { a - b } + func isPositive(_ n: Int) -> Bool { n > 0 } +} diff --git a/Fixtures/CalcLibrary/Sources/CalcLibrary/Logic.swift b/Fixtures/CalcLibrary/Sources/CalcLibrary/Logic.swift new file mode 100644 index 0000000..3a4a807 --- /dev/null +++ b/Fixtures/CalcLibrary/Sources/CalcLibrary/Logic.swift @@ -0,0 +1,3 @@ +struct Logic { + func isNonNegative(_ n: Int) -> Bool { n >= 0 } +} diff --git a/Fixtures/CalcLibrary/Sources/CalcLibrary/Validator.swift b/Fixtures/CalcLibrary/Sources/CalcLibrary/Validator.swift new file mode 100644 index 0000000..7372080 --- /dev/null +++ b/Fixtures/CalcLibrary/Sources/CalcLibrary/Validator.swift @@ -0,0 +1,3 @@ +struct Validator { + func isInRange(_ value: Int) -> Bool { value >= 0 && value <= 100 } +} diff --git a/Fixtures/CalcLibrary/Tests/CalcLibraryTests/CalcLibraryTests.swift b/Fixtures/CalcLibrary/Tests/CalcLibraryTests/CalcLibraryTests.swift new file mode 100644 index 0000000..18941e9 --- /dev/null +++ b/Fixtures/CalcLibrary/Tests/CalcLibraryTests/CalcLibraryTests.swift @@ -0,0 +1,23 @@ +import XCTest + +@testable import CalcLibrary + +final class CalcLibraryTests: XCTestCase { + func testAdd() { + XCTAssertEqual(Calculator().add(2, 3), 5) + } + + func testSubtract() { + XCTAssertEqual(Calculator().subtract(5, 3), 2) + } + + func testIsPositive() { + XCTAssertTrue(Calculator().isPositive(1)) + } + + func testIsInRange() { + XCTAssertTrue(Validator().isInRange(50)) + XCTAssertFalse(Validator().isInRange(-1)) + XCTAssertTrue(Validator().isInRange(0)) + } +} From 6f16dd0299b993452dac9b3c5916115cd8958678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:44:09 -0300 Subject: [PATCH 3/6] test: add SPM integration tests --- .../MutantExecutorSPMIntegrationTests.swift | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 Tests/SwiftMutationTestingTests/Integration/MutantExecutorSPMIntegrationTests.swift diff --git a/Tests/SwiftMutationTestingTests/Integration/MutantExecutorSPMIntegrationTests.swift b/Tests/SwiftMutationTestingTests/Integration/MutantExecutorSPMIntegrationTests.swift new file mode 100644 index 0000000..8c12c68 --- /dev/null +++ b/Tests/SwiftMutationTestingTests/Integration/MutantExecutorSPMIntegrationTests.swift @@ -0,0 +1,188 @@ +import Foundation +import Testing + +@testable import SwiftMutationTesting + +@Suite(.tags(.integration)) +struct MutantExecutorSPMIntegrationTests { + + @Test("Given SPM fixture with partial coverage, when executed, then killed and survived mutants match expected") + func spmFixtureResultsMatchExpected() async throws { + let fixtureURL = calcLibraryURL() + let configuration = makeConfiguration(fixtureURL: fixtureURL) + let input = makeInput(fixtureURL: fixtureURL) + + let results = try await MutantExecutor(configuration: configuration).execute(input) + + let killed = results.filter { + if case .killed = $0.status { return true } + return false + } + let survived = results.filter { $0.status == .survived } + let killedIDs = Set(killed.map { $0.descriptor.id }) + + #expect(killed.count == 3) + #expect(survived.count == 3) + #expect(killedIDs == Set(["m1", "m2", "m4"])) + } + + @Test("Given SPM fixture, when executed, then original source files are not modified") + func spmFixtureSourceFilesNotModified() async throws { + let fixtureURL = calcLibraryURL() + let calculatorURL = fixtureURL.appending(path: "Sources/CalcLibrary/Calculator.swift") + + let before = try String(contentsOf: calculatorURL, encoding: .utf8) + + let configuration = makeConfiguration(fixtureURL: fixtureURL) + let input = makeInput(fixtureURL: fixtureURL) + _ = try await MutantExecutor(configuration: configuration).execute(input) + + let after = try String(contentsOf: calculatorURL, encoding: .utf8) + + #expect(before == after) + } +} + +private func calcLibraryURL() -> URL { + URL(filePath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .appending(path: "Fixtures/CalcLibrary") +} + +private func makeConfiguration(fixtureURL: URL) -> RunnerConfiguration { + RunnerConfiguration( + projectPath: fixtureURL.path, + build: .init(projectType: .spm, timeout: 120.0, concurrency: 1, noCache: true), + reporting: .init(quiet: true), + filter: .init(excludePatterns: [], operators: []) + ) +} + +private func makeInput(fixtureURL: URL) -> RunnerInput { + RunnerInput( + projectPath: fixtureURL.path, + projectType: .spm, + timeout: 120.0, + concurrency: 1, + noCache: true, + schematizedFiles: makeSchematizedFiles(fixtureURL: fixtureURL), + supportFileContent: activatingSupportFileContent, + mutants: makeMutants(fixtureURL: fixtureURL) + ) +} + +private func makeSchematizedFiles(fixtureURL: URL) -> [SchematizedFile] { + let calculatorPath = fixtureURL.appending(path: "Sources/CalcLibrary/Calculator.swift").path + let validatorPath = fixtureURL.appending(path: "Sources/CalcLibrary/Validator.swift").path + + return [ + SchematizedFile( + originalPath: calculatorPath, + schematizedContent: """ + struct Calculator { + func add(_ a: Int, _ b: Int) -> Int { + (__swiftMutationTestingID == "m1") ? a - b : a + b + } + func subtract(_ a: Int, _ b: Int) -> Int { + (__swiftMutationTestingID == "m2") ? a + b : a - b + } + func isPositive(_ n: Int) -> Bool { + (__swiftMutationTestingID == "m3") ? n >= 0 : n > 0 + } + } + """ + ), + SchematizedFile( + originalPath: validatorPath, + schematizedContent: """ + struct Validator { + func isInRange(_ value: Int) -> Bool { + ((__swiftMutationTestingID == "m4") ? value > 0 : value >= 0) + && ((__swiftMutationTestingID == "m5") ? value < 100 : value <= 100) + } + } + """ + ), + ] +} + +private func makeMutants(fixtureURL: URL) -> [MutantDescriptor] { + let calculatorPath = fixtureURL.appending(path: "Sources/CalcLibrary/Calculator.swift").path + let validatorPath = fixtureURL.appending(path: "Sources/CalcLibrary/Validator.swift").path + let logicPath = fixtureURL.appending(path: "Sources/CalcLibrary/Logic.swift").path + + return calculatorMutants(path: calculatorPath) + + validatorMutants(path: validatorPath) + + incompatibleMutants(path: logicPath) +} + +private func calculatorMutants(path: String) -> [MutantDescriptor] { + [ + MutantDescriptor( + id: "m1", filePath: path, + line: 2, column: 44, utf8Offset: 64, + originalText: "+", mutatedText: "-", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace + with -", isSchematizable: true, mutatedSourceContent: nil + ), + MutantDescriptor( + id: "m2", filePath: path, + line: 3, column: 47, utf8Offset: 119, + originalText: "-", mutatedText: "+", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace - with +", isSchematizable: true, mutatedSourceContent: nil + ), + MutantDescriptor( + id: "m3", filePath: path, + line: 4, column: 40, utf8Offset: 167, + originalText: ">", mutatedText: ">=", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace > with >=", isSchematizable: true, mutatedSourceContent: nil + ), + ] +} + +private func validatorMutants(path: String) -> [MutantDescriptor] { + [ + MutantDescriptor( + id: "m4", filePath: path, + line: 2, column: 49, utf8Offset: 68, + originalText: ">=", mutatedText: ">", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace >= with >", isSchematizable: true, mutatedSourceContent: nil + ), + MutantDescriptor( + id: "m5", filePath: path, + line: 2, column: 62, utf8Offset: 81, + originalText: "<=", mutatedText: "<", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace <= with <", isSchematizable: true, mutatedSourceContent: nil + ), + ] +} + +private func incompatibleMutants(path: String) -> [MutantDescriptor] { + [ + MutantDescriptor( + id: "mi1", filePath: path, + line: 2, column: 45, utf8Offset: 59, + originalText: ">=", mutatedText: ">", + operatorIdentifier: "binaryOperator", replacementKind: .binaryOperator, + description: "Replace >= with >", + isSchematizable: false, + mutatedSourceContent: """ + struct Logic { + func isNonNegative(_ n: Int) -> Bool { n > 0 } + } + """ + ), + ] +} + +private let activatingSupportFileContent = + "import Foundation\n" + + "var __swiftMutationTestingID: String" + + #" { ProcessInfo.processInfo.environment["__SWIFT_MUTATION_TESTING_ACTIVE"] ?? "" }"# From 5975645080c51fafe81f55887854fcf2fcb6c3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:58:27 -0300 Subject: [PATCH 4/6] feat: add additionalEnvironment to ProcessLaunching and update all call sites --- .../Configuration/ProjectDetector.swift | 3 +++ .../Execution/IncompatibleMutantExecutor.swift | 2 ++ .../Execution/Parsing/ResultParser.swift | 1 + .../Execution/TestExecutionStage.swift | 7 +++---- .../Infrastructure/ProcessLauncher.swift | 9 +++++++++ .../Infrastructure/ProcessLaunching.swift | 1 + .../Simulator/SimulatorManager.swift | 2 ++ .../SwiftMutationTesting/Simulator/SimulatorPool.swift | 1 + 8 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftMutationTesting/Configuration/ProjectDetector.swift b/Sources/SwiftMutationTesting/Configuration/ProjectDetector.swift index 918e65f..904a154 100644 --- a/Sources/SwiftMutationTesting/Configuration/ProjectDetector.swift +++ b/Sources/SwiftMutationTesting/Configuration/ProjectDetector.swift @@ -69,6 +69,7 @@ struct ProjectDetector: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcodebuild"), arguments: [container.flag, container.path, "-list", "-json"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: workingDirectory, timeout: 30 ), @@ -86,6 +87,7 @@ struct ProjectDetector: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/swift"), arguments: ["package", "dump-package"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: projectURL, timeout: 30 ), @@ -195,6 +197,7 @@ struct ProjectDetector: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["simctl", "list", "devices", "available", "--json"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "."), timeout: 10 ), diff --git a/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift b/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift index 51b0080..9b4f568 100644 --- a/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift +++ b/Sources/SwiftMutationTesting/Execution/IncompatibleMutantExecutor.swift @@ -129,6 +129,7 @@ struct IncompatibleMutantExecutor: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcodebuild"), arguments: arguments, environment: nil, + additionalEnvironment: [:], workingDirectoryURL: sandbox.rootURL, timeout: configuration.build.timeout ) @@ -156,6 +157,7 @@ struct IncompatibleMutantExecutor: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/swift"), arguments: arguments, environment: nil, + additionalEnvironment: [:], workingDirectoryURL: sandbox.rootURL, timeout: configuration.build.timeout ) diff --git a/Sources/SwiftMutationTesting/Execution/Parsing/ResultParser.swift b/Sources/SwiftMutationTesting/Execution/Parsing/ResultParser.swift index d2b14ce..7a3de1a 100644 --- a/Sources/SwiftMutationTesting/Execution/Parsing/ResultParser.swift +++ b/Sources/SwiftMutationTesting/Execution/Parsing/ResultParser.swift @@ -16,6 +16,7 @@ struct ResultParser: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["xcresulttool", "get", "test-results", "tests", "--path", xcresultPath], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: timeout ) diff --git a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift index a914099..06b190a 100644 --- a/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift +++ b/Sources/SwiftMutationTesting/Execution/TestExecutionStage.swift @@ -113,14 +113,12 @@ struct TestExecutionStage: Sendable { arguments += ["--filter", testTarget] } - var environment = ProcessInfo.processInfo.environment - environment["__SWIFT_MUTATION_TESTING_ACTIVE"] = mutant.id - let start = Date() let captured = try await launcher.launchCapturing( executableURL: URL(fileURLWithPath: "/usr/bin/swift"), arguments: arguments, - environment: environment, + environment: nil, + additionalEnvironment: ["__SWIFT_MUTATION_TESTING_ACTIVE": mutant.id], workingDirectoryURL: context.sandbox.rootURL, timeout: context.configuration.build.timeout ) @@ -166,6 +164,7 @@ struct TestExecutionStage: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcodebuild"), arguments: arguments, environment: nil, + additionalEnvironment: [:], workingDirectoryURL: context.sandbox.rootURL, timeout: context.configuration.build.timeout ) diff --git a/Sources/SwiftMutationTesting/Infrastructure/ProcessLauncher.swift b/Sources/SwiftMutationTesting/Infrastructure/ProcessLauncher.swift index b178855..432aabf 100644 --- a/Sources/SwiftMutationTesting/Infrastructure/ProcessLauncher.swift +++ b/Sources/SwiftMutationTesting/Infrastructure/ProcessLauncher.swift @@ -30,6 +30,7 @@ struct ProcessLauncher: Sendable, ProcessLaunching { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { @@ -42,6 +43,14 @@ struct ProcessLauncher: Sendable, ProcessLaunching { process.environment = environment } + if !additionalEnvironment.isEmpty { + var env = process.environment ?? ProcessInfo.processInfo.environment + for (key, value) in additionalEnvironment { + env[key] = value + } + process.environment = env + } + let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) FileManager.default.createFile(atPath: tempURL.path, contents: nil) diff --git a/Sources/SwiftMutationTesting/Infrastructure/ProcessLaunching.swift b/Sources/SwiftMutationTesting/Infrastructure/ProcessLaunching.swift index 427f486..042634c 100644 --- a/Sources/SwiftMutationTesting/Infrastructure/ProcessLaunching.swift +++ b/Sources/SwiftMutationTesting/Infrastructure/ProcessLaunching.swift @@ -12,6 +12,7 @@ protocol ProcessLaunching: Sendable { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) diff --git a/Sources/SwiftMutationTesting/Simulator/SimulatorManager.swift b/Sources/SwiftMutationTesting/Simulator/SimulatorManager.swift index ce06d67..507be59 100644 --- a/Sources/SwiftMutationTesting/Simulator/SimulatorManager.swift +++ b/Sources/SwiftMutationTesting/Simulator/SimulatorManager.swift @@ -32,6 +32,7 @@ struct SimulatorManager: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["simctl", "list", "devices", "--json"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) @@ -57,6 +58,7 @@ struct SimulatorManager: Sendable { executableURL: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["simctl", "list", "devices", "--json"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) diff --git a/Sources/SwiftMutationTesting/Simulator/SimulatorPool.swift b/Sources/SwiftMutationTesting/Simulator/SimulatorPool.swift index c25f970..4fad837 100644 --- a/Sources/SwiftMutationTesting/Simulator/SimulatorPool.swift +++ b/Sources/SwiftMutationTesting/Simulator/SimulatorPool.swift @@ -54,6 +54,7 @@ actor SimulatorPool { executableURL: URL(fileURLWithPath: "/usr/bin/xcrun"), arguments: ["simctl", "clone", base, "XMR-\(session)-\(index)"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 60 ) From c83a20740ee690b06f90676fe806aadb914093ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:58:33 -0300 Subject: [PATCH 5/6] test: update mocks and add additionalEnvironment test for ProcessLauncher --- Package.swift | 1 + .../TestSupport/MockProcessLauncher.swift | 1 + .../TestSupport/SimulatorCommandMock.swift | 1 + .../Unit/CLI/SwiftMutationTestingTests.swift | 2 +- .../Unit/Execution/MutantExecutorTests.swift | 1 + .../Infrastructure/ProcessLauncherTests.swift | 21 +++++++++++++++++++ .../Simulator/SimulatorManagerTests.swift | 1 + 7 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index b0be819..ea6b1b3 100644 --- a/Package.swift +++ b/Package.swift @@ -35,6 +35,7 @@ let package = Package( name: "SwiftMutationTestingTests", dependencies: ["SwiftMutationTesting"], path: "Tests/SwiftMutationTestingTests", + exclude: ["TestSupport/Fixtures"], swiftSettings: [ .swiftLanguageMode(.v6) ] diff --git a/Tests/SwiftMutationTestingTests/TestSupport/MockProcessLauncher.swift b/Tests/SwiftMutationTestingTests/TestSupport/MockProcessLauncher.swift index dde52c4..d9b129c 100644 --- a/Tests/SwiftMutationTestingTests/TestSupport/MockProcessLauncher.swift +++ b/Tests/SwiftMutationTestingTests/TestSupport/MockProcessLauncher.swift @@ -34,6 +34,7 @@ struct MockProcessLauncher: ProcessLaunching { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { diff --git a/Tests/SwiftMutationTestingTests/TestSupport/SimulatorCommandMock.swift b/Tests/SwiftMutationTestingTests/TestSupport/SimulatorCommandMock.swift index 471a290..491bba5 100644 --- a/Tests/SwiftMutationTestingTests/TestSupport/SimulatorCommandMock.swift +++ b/Tests/SwiftMutationTestingTests/TestSupport/SimulatorCommandMock.swift @@ -26,6 +26,7 @@ struct SimulatorCommandMock: ProcessLaunching { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { diff --git a/Tests/SwiftMutationTestingTests/Unit/CLI/SwiftMutationTestingTests.swift b/Tests/SwiftMutationTestingTests/Unit/CLI/SwiftMutationTestingTests.swift index 8e1c968..ba46293 100644 --- a/Tests/SwiftMutationTestingTests/Unit/CLI/SwiftMutationTestingTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/CLI/SwiftMutationTestingTests.swift @@ -15,7 +15,7 @@ private struct IOSSimulatorMock: ProcessLaunching { func launchCapturing( executableURL: URL, arguments: [String], environment: [String: String]?, - workingDirectoryURL: URL, timeout: Double + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { if arguments.contains("clone") { return (0, cloneUDID + "\n") } return (0, listJSON) diff --git a/Tests/SwiftMutationTestingTests/Unit/Execution/MutantExecutorTests.swift b/Tests/SwiftMutationTestingTests/Unit/Execution/MutantExecutorTests.swift index 107b31f..e0f7b70 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Execution/MutantExecutorTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Execution/MutantExecutorTests.swift @@ -29,6 +29,7 @@ private actor FallbackBuildSucceedingMock: ProcessLaunching { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { diff --git a/Tests/SwiftMutationTestingTests/Unit/Infrastructure/ProcessLauncherTests.swift b/Tests/SwiftMutationTestingTests/Unit/Infrastructure/ProcessLauncherTests.swift index 318a6b7..d9f8912 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Infrastructure/ProcessLauncherTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Infrastructure/ProcessLauncherTests.swift @@ -37,6 +37,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/bin/echo"), arguments: ["hello world"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) @@ -51,6 +52,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/bin/sh"), arguments: ["-c", "echo $TEST_VAR"], environment: ["TEST_VAR": "expected_value"], + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) @@ -65,6 +67,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/bin/sh"), arguments: ["-c", "echo error_text >&2"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) @@ -103,6 +106,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/nonexistent/binary"), arguments: [], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 10 ) @@ -115,6 +119,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/bin/sleep"), arguments: ["60"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 0.5 ) @@ -140,6 +145,21 @@ struct ProcessLauncherTests { #expect(exitCode == -1) } + @Test("Given additionalEnvironment, when launched capturing, then process receives merged variable") + func launchCapturingMergesAdditionalEnvironment() async throws { + let result = try await launcher.launchCapturing( + executableURL: URL(fileURLWithPath: "/bin/sh"), + arguments: ["-c", "echo $EXTRA_VAR"], + environment: nil, + additionalEnvironment: ["EXTRA_VAR": "merged_value"], + workingDirectoryURL: URL(fileURLWithPath: "/tmp"), + timeout: 10 + ) + + #expect(result.exitCode == 0) + #expect(result.output.contains("merged_value")) + } + @Test("Given task is cancelled while launchCapturing running, when cancelled, then process is terminated") func cancelledLaunchCapturingTerminatesProcess() async throws { let task = Task { @@ -147,6 +167,7 @@ struct ProcessLauncherTests { executableURL: URL(fileURLWithPath: "/bin/sleep"), arguments: ["60"], environment: nil, + additionalEnvironment: [:], workingDirectoryURL: URL(fileURLWithPath: "/tmp"), timeout: 60 ) diff --git a/Tests/SwiftMutationTestingTests/Unit/Simulator/SimulatorManagerTests.swift b/Tests/SwiftMutationTestingTests/Unit/Simulator/SimulatorManagerTests.swift index 997e658..58b0bc8 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Simulator/SimulatorManagerTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Simulator/SimulatorManagerTests.swift @@ -158,6 +158,7 @@ private actor SequentialOutputMock: ProcessLaunching { executableURL: URL, arguments: [String], environment: [String: String]?, + additionalEnvironment: [String: String], workingDirectoryURL: URL, timeout: Double ) async throws -> (exitCode: Int32, output: String) { From 48d6d4776e64c433a55c93dd8c2eb861d122a2d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89rico=20Perez=20Neto?= Date: Tue, 31 Mar 2026 09:58:38 -0300 Subject: [PATCH 6/6] test: add SPM crash fixture tests for TestOutputParser --- .../spm_xctest_exc_bad_instruction.txt | 5 ++++ .../Fixtures/spm_xctest_fatal_error.txt | 5 ++++ .../Parsing/TestOutputParserTests.swift | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_exc_bad_instruction.txt create mode 100644 Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_fatal_error.txt diff --git a/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_exc_bad_instruction.txt b/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_exc_bad_instruction.txt new file mode 100644 index 0000000..78708e8 --- /dev/null +++ b/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_exc_bad_instruction.txt @@ -0,0 +1,5 @@ +Test Suite 'All tests' started at 2024-01-15 10:23:45.123. +Test Suite 'CalcLibraryTests.xctest' started at 2024-01-15 10:23:45.124. +Test Suite 'CalcLibraryTests' started at 2024-01-15 10:23:45.125. +Test Case '-[CalcLibraryTests testAdd]' started. +EXC_BAD_INSTRUCTION (SIGILL) diff --git a/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_fatal_error.txt b/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_fatal_error.txt new file mode 100644 index 0000000..da7f13c --- /dev/null +++ b/Tests/SwiftMutationTestingTests/TestSupport/Fixtures/spm_xctest_fatal_error.txt @@ -0,0 +1,5 @@ +Test Suite 'All tests' started at 2024-01-15 10:23:45.123. +Test Suite 'CalcLibraryTests.xctest' started at 2024-01-15 10:23:45.124. +Test Suite 'CalcLibraryTests' started at 2024-01-15 10:23:45.125. +Test Case '-[CalcLibraryTests testAdd]' started. +Fatal error: precondition failed: expected positive result: file Sources/CalcLibrary/Calculator.swift, line 3 diff --git a/Tests/SwiftMutationTestingTests/Unit/Execution/Parsing/TestOutputParserTests.swift b/Tests/SwiftMutationTestingTests/Unit/Execution/Parsing/TestOutputParserTests.swift index 22d86a6..e0561a2 100644 --- a/Tests/SwiftMutationTestingTests/Unit/Execution/Parsing/TestOutputParserTests.swift +++ b/Tests/SwiftMutationTestingTests/Unit/Execution/Parsing/TestOutputParserTests.swift @@ -1,3 +1,4 @@ +import Foundation import Testing @testable import SwiftMutationTesting @@ -90,4 +91,30 @@ struct TestOutputParserTests { #expect(result == .unviable) } + + @Test("Given SPM swift test output with fatal error, when parsed, then returns crashed") + func parsesSPMFatalErrorCrash() throws { + let output = try loadFixture("spm_xctest_fatal_error") + let result = TestOutputParser().parse(output) + + #expect(result == .crashed) + } + + @Test("Given SPM swift test output with EXC_BAD_INSTRUCTION, when parsed, then returns crashed") + func parsesSPMEXCBadInstructionCrash() throws { + let output = try loadFixture("spm_xctest_exc_bad_instruction") + let result = TestOutputParser().parse(output) + + #expect(result == .crashed) + } +} + +private func loadFixture(_ name: String) throws -> String { + let fixturesURL = URL(filePath: #filePath) + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .deletingLastPathComponent() + .appending(path: "TestSupport/Fixtures/\(name).txt") + return try String(contentsOf: fixturesURL, encoding: .utf8) }