Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ struct TestExecutionStage: Sendable {
}

guard let plist = context.artifact.plist else {
return ExecutionResult(descriptor: mutant, status: .unviable, testDuration: 0)
return try await runSPM(mutant: mutant, key: key, in: context)
}

let plistData = plist.activating(mutant.id)
Expand Down Expand Up @@ -79,6 +79,68 @@ struct TestExecutionStage: Sendable {
return result
}

private func runSPM(
mutant: MutantDescriptor,
key: MutantCacheKey,
in context: TestExecutionContext
) async throws -> ExecutionResult {
let slot = try await context.pool.acquire()
let launched: TestLaunchResult
do {
launched = try await launchSPM(mutant: mutant, in: context)
} catch {
await context.pool.release(slot)
throw error
}
await context.pool.release(slot)

let outcome = spmOutcome(exitCode: launched.exitCode, output: launched.output)
let status = outcome.asExecutionStatus
let result = ExecutionResult(descriptor: mutant, status: status, testDuration: launched.duration)
await cacheStore.store(status: status, for: key)
let index = await counter.increment()
await reporter.report(.mutantFinished(descriptor: mutant, status: status, index: index, total: counter.total))
return result
}

private func launchSPM(
mutant: MutantDescriptor,
in context: TestExecutionContext
) async throws -> TestLaunchResult {
var arguments = ["test", "--skip-build"]

if let testTarget = context.configuration.build.testTarget {
arguments += ["--filter", testTarget]
}

let start = Date()
let captured = try await launcher.launchCapturing(
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: arguments,
environment: ["__SWIFT_MUTATION_TESTING_ACTIVE": mutant.id],
workingDirectoryURL: context.sandbox.rootURL,
timeout: context.configuration.build.timeout
)

return TestLaunchResult(
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 launch(
plistData: Data,
slot: SimulatorSlot,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,8 @@ struct MutantExecutorTests {
#expect(incompatibleResult?.status == .unviable)
}

@Test("Given SPM project type and schematizable mutants, when execute called, then returns unviable")
func spmSchematizableMutantsAreUnviable() async throws {
@Test("Given SPM project type and schematizable mutants with exit code 0, when execute called, then survived")
func spmSchematizableMutantsSurviveOnExitCodeZero() async throws {
let dir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(dir) }

Expand All @@ -244,7 +244,7 @@ struct MutantExecutorTests {
let results = try await executor.execute(input)

#expect(results.count == 1)
#expect(results[0].status == .unviable)
#expect(results[0].status == .survived)
}

@Test("Given SPM project type and build failure, when execute called, then throws compilationFailed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,71 @@ struct TestExecutionStageTests {
}
}

@Test("Given SPM artifact and exit code 0, when execute called, then mutant survived")
func spmExitCodeZeroProducesSurvivedStatus() async throws {
let dir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(dir) }

let (stage, context) = makeSPMFixture(in: dir, exitCode: 0)
try await context.pool.setUp()

let results = try await stage.execute(mutants: [makeMutant(id: "m0")], in: context)

#expect(results.first?.status == .survived)
}

@Test("Given SPM artifact 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 (stage, context) = makeSPMFixture(in: dir, exitCode: 1, output: output)
try await context.pool.setUp()

let results = try await stage.execute(mutants: [makeMutant(id: "m0")], in: context)

#expect(results.first?.status == .killed(by: "myTest"))
}

@Test("Given SPM artifact with testTarget, when execute called, then returns result")
func spmWithTestTargetReturnsResult() async throws {
let dir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(dir) }

let pool = SimulatorPool(
baseUDID: nil, size: 1,
destination: "platform=macOS", launcher: MockProcessLauncher(exitCode: 0)
)
try await pool.setUp()
let config = RunnerConfiguration(
projectPath: "/tmp",
build: .init(
projectType: .spm, testTarget: "MyLibTests",
timeout: 60, concurrency: 1, noCache: false),
reporting: .init(quiet: true),
filter: .init(excludePatterns: [], operators: [])
)
let stage = TestExecutionStage(
launcher: MockProcessLauncher(exitCode: 0),
cacheStore: CacheStore(storePath: dir.appendingPathComponent("cache.json").path),
reporter: MockProgressReporter(),
counter: MutationCounter(total: 1)
)
let context = TestExecutionContext(
artifact: BuildArtifact(derivedDataPath: dir.path, xctestrunURL: nil, plist: nil),
sandbox: Sandbox(rootURL: dir),
pool: pool,
configuration: config,
testFilesHash: "hash"
)

let results = try await stage.execute(mutants: [makeMutant(id: "m0")], in: context)

#expect(results.count == 1)
#expect(results.first?.status == .survived)
}

private func makeFixture(
in dir: URL,
exitCode: Int32,
Expand All @@ -230,6 +295,38 @@ struct TestExecutionStageTests {
return (stage, context)
}

private func makeSPMFixture(
in dir: URL,
exitCode: Int32,
output: String = ""
) -> (TestExecutionStage, TestExecutionContext) {
let launcher = MockProcessLauncher(exitCode: exitCode, output: output)
let pool = SimulatorPool(
baseUDID: nil, size: 1,
destination: "platform=macOS", launcher: launcher
)
let stage = TestExecutionStage(
launcher: launcher,
cacheStore: CacheStore(storePath: dir.appendingPathComponent("cache.json").path),
reporter: MockProgressReporter(),
counter: MutationCounter(total: 1)
)
let config = RunnerConfiguration(
projectPath: "/tmp",
build: .init(projectType: .spm, timeout: 60, concurrency: 1, noCache: false),
reporting: .init(quiet: true),
filter: .init(excludePatterns: [], operators: [])
)
let context = TestExecutionContext(
artifact: BuildArtifact(derivedDataPath: dir.path, xctestrunURL: nil, plist: nil),
sandbox: Sandbox(rootURL: dir),
pool: pool,
configuration: config,
testFilesHash: "hash"
)
return (stage, context)
}

private func makeBuildArtifact(in dir: URL) -> BuildArtifact {
let plistDict: [String: Any] = ["MyTarget": ["EnvironmentVariables": [String: String]()]]
let data = try! PropertyListSerialization.data(
Expand Down