Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
9cf531b
feat: extract ProcessRequest and ProcessRunner to reduce duplication
ericodx Apr 5, 2026
9b54a35
test: split ProcessLauncherTests into SPM and Xcode test files
ericodx Apr 5, 2026
15f6805
refactor: extract toDescriptor into IndexedMutationPoint
ericodx Apr 5, 2026
ef934d3
feat: format total duration as minutes and seconds
ericodx Apr 5, 2026
a34299d
test: TextReporter duration formatting
ericodx Apr 5, 2026
d5362cd
refactor: adopt ProcessRequest across execution layer
ericodx Apr 5, 2026
190bd91
test: update execution layer tests for ProcessRequest
ericodx Apr 5, 2026
aabe7f9
feat: ProjectDetector SPM test targets and testing framework detection
ericodx Apr 5, 2026
831a142
test: ProjectDetector and Configuration coverage
ericodx Apr 5, 2026
15c8cba
test: additional coverage for XCTestRunPlist, MutantIndexing and Test…
ericodx Apr 5, 2026
95e2745
feat: add LocalizedError conformance to BuildError
ericodx Apr 5, 2026
2db2730
test: BuildError errorDescription and equality
ericodx Apr 5, 2026
f995802
feat: add LocalizedError conformance to SimulatorError
ericodx Apr 5, 2026
90e8e1e
test: SimulatorError errorDescription
ericodx Apr 5, 2026
c1fe402
refactor: collapse catch blocks and extract defaultLauncher
ericodx Apr 5, 2026
cf1cd52
test: defaultLauncher for xcode and spm project types
ericodx Apr 5, 2026
31b041b
refactor: remove unreachable guard and catch in IncompatibleMutantExe…
ericodx Apr 5, 2026
3222042
refactor: remove unreachable guard on FileManager enumerator in Proje…
ericodx Apr 5, 2026
3cd3cdc
refactor: replace try! with guard let try? in rewriteForIncompatible
ericodx Apr 5, 2026
2b2ffd6
test: MutantExecutor catch block, errorLines fallback and removingCas…
ericodx Apr 5, 2026
78501b9
test: FallbackExecutor SPM build success path
ericodx Apr 5, 2026
b655817
refactor: force-unwrap UTF-8 conversions in MutationRewriter
ericodx Apr 5, 2026
53ef217
refactor: force-unwrap UTF-8 conversions in SchemataGenerator
ericodx Apr 5, 2026
377d4a3
refactor: force-unwrap ID parsing in DiscoveryPipeline
ericodx Apr 5, 2026
c158a30
refactor: make killEscapedChildren internal for testability
ericodx Apr 5, 2026
8a3e0f5
test: killEscapedChildren process matching and prefix guard
ericodx Apr 5, 2026
b5c9085
chore: add periphery:ignore to test-only computed properties
ericodx Apr 5, 2026
066a1fa
chore: exclude Tests from SwiftLint validation
ericodx Apr 5, 2026
b5c26f9
chore: exclude Tests from Periphery index
ericodx Apr 5, 2026
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
1 change: 1 addition & 0 deletions .periphery.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ index_exclude:
- "**/checkouts/**"
- "**/.github/**"
- "**/Docs/**"
- "**/Tests/**"

# Exclude test files from reports
report_exclude:
Expand Down
1 change: 0 additions & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Project paths
included:
- Sources
- Tests

# Analyzer exclusions
excluded:
Expand Down
16 changes: 15 additions & 1 deletion Sources/SwiftMutationTesting/Build/BuildError.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
enum BuildError: Error, Equatable {
import Foundation

enum BuildError: Error, Equatable, LocalizedError {
case compilationFailed(output: String)
case xctestrunNotFound

var errorDescription: String? {
switch self {
case .compilationFailed(let output):
var message = "Build failed. The schematized source could not be compiled."
if !output.isEmpty { message = output + "\n" + message }
return message

case .xctestrunNotFound:
return "xctestrun file not found after build."
}
}

static func == (lhs: BuildError, rhs: BuildError) -> Bool {
switch (lhs, rhs) {
case (.compilationFailed, .compilationFailed): return true
Expand Down
29 changes: 16 additions & 13 deletions Sources/SwiftMutationTesting/Build/BuildStage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,14 @@ struct BuildStage: Sendable {
}

let (exitCode, buildOutput) = try await launcher.launchCapturing(
executableURL: URL(fileURLWithPath: "/usr/bin/xcodebuild"),
arguments: arguments,
environment: nil,
additionalEnvironment: [:],
workingDirectoryURL: sandbox.rootURL,
timeout: timeout
ProcessRequest(
executableURL: URL(fileURLWithPath: "/usr/bin/xcodebuild"),
arguments: arguments,
environment: nil,
additionalEnvironment: [:],
workingDirectoryURL: sandbox.rootURL,
timeout: timeout
)
)

guard exitCode == 0 else {
Expand Down Expand Up @@ -58,18 +60,19 @@ struct BuildStage: Sendable {

func buildSPM(
sandbox: Sandbox,
testTarget: String?,
timeout: Double
) async throws -> BuildArtifact {
let arguments = ["build", "--build-tests"]

let (exitCode, buildOutput) = try await launcher.launchCapturing(
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: arguments,
environment: nil,
additionalEnvironment: [:],
workingDirectoryURL: sandbox.rootURL,
timeout: timeout
ProcessRequest(
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: arguments,
environment: nil,
additionalEnvironment: [:],
workingDirectoryURL: sandbox.rootURL,
timeout: timeout
)
)

guard exitCode == 0 else { throw BuildError.compilationFailed(output: buildOutput) }
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftMutationTesting/CLI/CommandLineParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ struct CommandLineParser: Sendable {
return values
}

// swiftlint:disable:next cyclomatic_complexity
private func applyFlag(
_ flag: String,
to values: inout FlagValues,
Expand Down
9 changes: 5 additions & 4 deletions Sources/SwiftMutationTesting/Cache/CacheStore.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import Foundation

actor CacheStore {
private struct CacheEntry: Codable {
let key: MutantCacheKey
let status: ExecutionStatus
}

init(storePath: String) {
self.storePath = storePath
Expand All @@ -16,6 +12,11 @@ actor CacheStore {
private let storePath: String
private var entries: [MutantCacheKey: ExecutionStatus]

private struct CacheEntry: Codable {
let key: MutantCacheKey
let status: ExecutionStatus
}

func result(for key: MutantCacheKey) -> ExecutionStatus? {
entries[key]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ struct ConfigurationFileParser: Sendable {
}

if !disabledMutators.isEmpty {
result["disabledMutators"] = disabledMutators.joined(separator: ",")
result["disabled-mutators"] = disabledMutators.joined(separator: ",")
}

return result
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,24 @@ struct ConfigurationFileWriter: Sendable {
lines.append("")
lines.append("# Testing framework: xctest or swift-testing (default: swift-testing)")
lines.append("# When xctest is selected, concurrency is forced to 1 for deterministic results")
lines.append("testingFramework: \(testingFramework.rawValue)")
lines.append("testing-framework: \(testingFramework.rawValue)")
lines.append("")

if let testTarget {
lines.append("# Limit test execution to a specific target (recommended when the project has UI tests)")
lines.append("testTarget: \(testTarget)")
lines.append("test-target: \(testTarget)")
} else {
lines.append("# Limit test execution to a specific target (recommended when the project has UI tests)")
lines.append("# testTarget: MyAppTests")
lines.append("# test-target: MyAppTests")
}

lines.append(contentsOf: xcodeRunSection(testingFramework: testingFramework, testTarget: testTarget))

return lines.joined(separator: "\n") + "\n"
}

private func xcodeRunSection(testingFramework: TestingFramework, testTarget: String?) -> [String] {
var lines: [String] = []
lines.append("")
lines.append("# Per-mutant test timeout in seconds (default: 120)")
lines.append("timeout: 120")
Expand All @@ -76,28 +83,9 @@ struct ConfigurationFileWriter: Sendable {
} else {
lines.append("concurrency: 4")
}
lines.append("")
lines.append("# Disable result cache (re-runs all mutants on every execution)")
lines.append("# noCache: true")
lines.append("")
lines.append("# Report output paths")
lines.append("# output: mutation-report.json")
lines.append("# htmlOutput: mutation-report.html")
lines.append("sonarOutput: sonar-mutation-report.json")
lines.append("")
lines.append("# Source file glob patterns to exclude from mutation")

if let testTarget {
lines.append("exclude:")
lines.append(" - \"/\(testTarget)/\"")
} else {
lines.append("# exclude:")
lines.append("# - \"**/Generated/**\"")
}

lines.append(contentsOf: reportSection(testTarget: testTarget, excludeExample: "**/Generated/**"))
lines.append(contentsOf: mutatorsSection())

return lines.joined(separator: "\n") + "\n"
return lines
}

private func generateSPMContent(testTargets: [String], testTarget: String?) -> String {
Expand All @@ -113,37 +101,41 @@ struct ConfigurationFileWriter: Sendable {

if let testTarget {
lines.append("# Limit test execution to a specific target")
lines.append("testTarget: \(testTarget)")
lines.append("test-target: \(testTarget)")
} else {
lines.append("# Limit test execution to a specific target")
lines.append("# testTarget: MyPackageTests")
lines.append("# test-target: MyPackageTests")
}

lines.append("")
lines.append("# Per-mutant test timeout in seconds (default: 30 for SPM)")
lines.append("timeout: 30")
lines.append(contentsOf: reportSection(testTarget: testTarget, excludeExample: "**/Tests/**"))
lines.append(contentsOf: mutatorsSection())

return lines.joined(separator: "\n") + "\n"
}

private func reportSection(testTarget: String?, excludeExample: String) -> [String] {
var lines: [String] = []
lines.append("")
lines.append("# Disable result cache (re-runs all mutants on every execution)")
lines.append("# noCache: true")
lines.append("# no-cache: true")
lines.append("")
lines.append("# Report output paths")
lines.append("# output: mutation-report.json")
lines.append("# htmlOutput: mutation-report.html")
lines.append("sonarOutput: sonar-mutation-report.json")
lines.append("output: mutation-report.json")
lines.append("# html-output: mutation-report.html")
lines.append("# sonar-output: sonar-mutation-report.json")
lines.append("")
lines.append("# Source file glob patterns to exclude from mutation")

if let testTarget {
lines.append("exclude:")
lines.append(" - \"/\(testTarget)/\"")
} else {
lines.append("# exclude:")
lines.append("# - \"**/Tests/**\"")
lines.append("# - \"\(excludeExample)\"")
}

lines.append(contentsOf: mutatorsSection())

return lines.joined(separator: "\n") + "\n"
return lines
}

private func mutatorsSection() -> [String] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ struct ConfigurationResolver: Sendable {
projectPath: projectPath,
build: .init(
projectType: projectType,
testTarget: cliArguments.build.testTarget ?? fileValues["testTarget"],
testTarget: cliArguments.build.testTarget ?? fileValues["test-target"],
timeout: timeout,
concurrency: effectiveConcurrency,
noCache: cliArguments.build.noCache || fileValues["noCache"]?.lowercased() == "true",
noCache: cliArguments.build.noCache || fileValues["no-cache"]?.lowercased() == "true",
testingFramework: testingFramework
),
reporting: .init(
output: cliArguments.reporting.output ?? fileValues["output"],
htmlOutput: cliArguments.reporting.htmlOutput ?? fileValues["htmlOutput"],
sonarOutput: cliArguments.reporting.sonarOutput ?? fileValues["sonarOutput"],
htmlOutput: cliArguments.reporting.htmlOutput ?? fileValues["html-output"],
sonarOutput: cliArguments.reporting.sonarOutput ?? fileValues["sonar-output"],
quiet: cliArguments.reporting.quiet || fileValues["quiet"]?.lowercased() == "true"
),
filter: .init(
sourcesPath: cliArguments.filter.sourcesPath ?? fileValues["sourcesPath"],
sourcesPath: cliArguments.filter.sourcesPath ?? fileValues["sources-path"],
excludePatterns: resolveList(
cli: cliArguments.filter.excludePatterns,
keys: ["exclude", "excludePatterns"],
keys: ["exclude", "exclude-patterns"],
from: fileValues
),
operators: resolveOperators(cli: cliArguments, fileValues: fileValues)
Expand Down Expand Up @@ -104,7 +104,7 @@ struct ConfigurationResolver: Sendable {

private func resolvedTestingFramework(cli: ParsedArguments, fileValues: [String: String]) throws -> TestingFramework
{
let raw = cli.build.testingFramework ?? fileValues["testingFramework"]
let raw = cli.build.testingFramework ?? fileValues["testing-framework"]

guard let raw else {
return .swiftTesting
Expand All @@ -127,7 +127,7 @@ struct ConfigurationResolver: Sendable {
return DiscoveryPipeline.allOperatorNames.filter { !disabled.contains($0) }
}

let fileDisabled = resolveList(cli: [], keys: ["disabledMutators"], from: fileValues)
let fileDisabled = resolveList(cli: [], keys: ["disabled-mutators"], from: fileValues)
if !fileDisabled.isEmpty {
let disabled = Set(fileDisabled)
return DiscoveryPipeline.allOperatorNames.filter { !disabled.contains($0) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
struct DetectedProject: Sendable {
enum Kind: Sendable {
case xcode(scheme: String?, allSchemes: [String], destination: String)
case spm(testTargets: [String])
}

static let empty = DetectedProject(
kind: .xcode(scheme: nil, allSchemes: [], destination: "platform=macOS"),
Expand All @@ -14,18 +10,26 @@ struct DetectedProject: Sendable {
let testTarget: String?
var testingFramework: TestingFramework = .swiftTesting

// periphery:ignore
var scheme: String? {
guard case .xcode(let xScheme, _, _) = kind else { return nil }
return xScheme
}

// periphery:ignore
var allSchemes: [String] {
guard case .xcode(_, let all, _) = kind else { return [] }
return all
}

// periphery:ignore
var destination: String {
guard case .xcode(_, _, let dest) = kind else { return "platform=macOS" }
return dest
}

enum Kind: Sendable {
case xcode(scheme: String?, allSchemes: [String], destination: String)
case spm(testTargets: [String])
}
}
Loading