Skip to content
10 changes: 5 additions & 5 deletions Sources/SwiftMutationTesting/Cache/CacheStore.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Foundation

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

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

init(storePath: String) {
self.storePath = storePath
self.entries = [:]
Expand Down
16 changes: 12 additions & 4 deletions Sources/SwiftMutationTesting/Discovery/DiscoveryPipeline.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,28 @@ struct DiscoveryPipeline: Sendable {
let parsedSources = await ParsingStage().run(sourceFiles: sourceFiles)
let ops = resolvedOperators(from: input.operators)
let mutationPoints = await MutantDiscoveryStage(operators: ops).run(sources: parsedSources)
let result = SchematizationStage().run(mutationPoints: mutationPoints, sources: parsedSources)
let indexed = MutantIndexingStage().run(mutationPoints: mutationPoints, sources: parsedSources)
let (schematizedFiles, schematizableDescriptors) = SchematizationStage().run(indexed: indexed, sources: parsedSources)
let incompatibleDescriptors = IncompatibleRewritingStage().run(indexed: indexed, sources: parsedSources)
let allDescriptors = (schematizableDescriptors + incompatibleDescriptors)
.sorted { indexFromID($0.id) < indexFromID($1.id) }

return RunnerInput(
projectPath: input.projectPath,
projectType: input.projectType,
timeout: input.timeout,
concurrency: input.concurrency,
noCache: input.noCache,
schematizedFiles: result.schematizedFiles,
supportFileContent: result.supportFileContent,
mutants: result.descriptors
schematizedFiles: schematizedFiles,
supportFileContent: SchematizationStage.supportFileContent,
mutants: allDescriptors
)
}

private func indexFromID(_ id: String) -> Int {
Int(id.replacingOccurrences(of: "swift-mutation-testing_", with: "")) ?? 0
}

private func resolvedOperators(from identifiers: [String]) -> [any MutationOperator] {
if identifiers.isEmpty {
return Self.registry.map(\.operator)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
struct IncompatibleRewritingStage: Sendable {
func run(indexed: [IndexedMutationPoint], sources: [ParsedSource]) -> [MutantDescriptor] {
let incompatible = indexed.filter { !$0.isSchematizable }
let sourceByPath = Dictionary(uniqueKeysWithValues: sources.map { ($0.file.path, $0) })
let rewriter = MutationRewriter()

return incompatible.compactMap { entry in
guard let source = sourceByPath[entry.mutation.filePath] else { return nil }
let mutatedContent = rewriter.rewrite(source: source.file.content, applying: entry.mutation)
return makeDescriptor(from: entry.mutation, id: mutantID(entry.index), isSchematizable: false, mutatedContent: mutatedContent)
}
}

private func mutantID(_ index: Int) -> String {
"swift-mutation-testing_\(index)"
}

private func makeDescriptor(
from mutation: MutationPoint,
id: String,
isSchematizable: Bool,
mutatedContent: String?
) -> MutantDescriptor {
MutantDescriptor(
id: id,
filePath: mutation.filePath,
line: mutation.line,
column: mutation.column,
utf8Offset: mutation.utf8Offset,
originalText: mutation.originalText,
mutatedText: mutation.mutatedText,
operatorIdentifier: mutation.operatorIdentifier,
replacementKind: mutation.replacement,
description: mutation.description,
isSchematizable: isSchematizable,
mutatedSourceContent: mutatedContent
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
struct IndexedMutationPoint: Sendable {
let index: Int
let mutation: MutationPoint
let isSchematizable: Bool
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SwiftSyntax

struct MutantIndexingStage: Sendable {
func run(mutationPoints: [MutationPoint], sources: [ParsedSource]) -> [IndexedMutationPoint] {
let sorted = mutationPoints.sorted {
if $0.filePath != $1.filePath { return $0.filePath < $1.filePath }
return $0.utf8Offset < $1.utf8Offset
}

let visitors = buildVisitors(for: sources)

return sorted.enumerated().map { index, mutation in
let schematizable = visitors[mutation.filePath]?.isSchematizable(utf8Offset: mutation.utf8Offset) ?? false
return IndexedMutationPoint(index: index, mutation: mutation, isSchematizable: schematizable)
}
}

private func buildVisitors(for sources: [ParsedSource]) -> [String: TypeScopeVisitor] {
var visitors: [String: TypeScopeVisitor] = [:]
for source in sources {
let visitor = TypeScopeVisitor()
visitor.walk(source.syntax)
visitors[source.file.path] = visitor
}
return visitors
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,97 +1,45 @@
import Foundation

struct SchematizationStage: Sendable {
private static let supportFileContent = """
static let supportFileContent = """
import Foundation

var __swiftMutationTestingID: String {
ProcessInfo.processInfo.environment["__SWIFT_MUTATION_TESTING_ACTIVE"] ?? ""
}
"""

func run(mutationPoints: [MutationPoint], sources: [ParsedSource]) -> SchematizationResult {
let sorted = mutationPoints.sorted {
if $0.filePath != $1.filePath { return $0.filePath < $1.filePath }
return $0.utf8Offset < $1.utf8Offset
}

func run(indexed: [IndexedMutationPoint], sources: [ParsedSource]) -> ([SchematizedFile], [MutantDescriptor]) {
let schematizable = indexed.filter { $0.isSchematizable }
let sourceByPath = Dictionary(uniqueKeysWithValues: sources.map { ($0.file.path, $0) })
let visitors = buildVisitors(for: sources)
let indexed = assignIndices(sorted: sorted, visitors: visitors)
let byFile = Dictionary(grouping: indexed) { $0.mutation.filePath }
let byFile = Dictionary(grouping: schematizable) { $0.mutation.filePath }
let generator = SchemataGenerator()
let rewriter = MutationRewriter()
var descriptors: [MutantDescriptor] = []
var schematizedFiles: [SchematizedFile] = []
var descriptors: [MutantDescriptor] = []

for (filePath, entries) in byFile {
guard let source = sourceByPath[filePath] else { continue }

let schematizable = entries.filter { $0.isSchematizable }
let incompatible = entries.filter { !$0.isSchematizable }

if !schematizable.isEmpty {
let mutations = schematizable.map { (index: $0.index, point: $0.mutation) }
let content = generator.generate(source: source, mutations: mutations)
schematizedFiles.append(SchematizedFile(originalPath: filePath, schematizedContent: content))
let mutations = entries.map { (index: $0.index, point: $0.mutation) }
let content = generator.generate(source: source, mutations: mutations)
schematizedFiles.append(SchematizedFile(originalPath: filePath, schematizedContent: content))

for entry in schematizable {
descriptors.append(
descriptor(
from: entry.mutation, id: mutantID(entry.index),
isSchematizable: true, mutatedContent: nil
))
}
for entry in entries {
descriptors.append(makeDescriptor(
from: entry.mutation, id: mutantID(entry.index),
isSchematizable: true, mutatedContent: nil
))
}

for entry in incompatible {
let mutatedContent = rewriter.rewrite(source: source.file.content, applying: entry.mutation)
descriptors.append(
descriptor(
from: entry.mutation, id: mutantID(entry.index),
isSchematizable: false, mutatedContent: mutatedContent
))
}
}

return SchematizationResult(
schematizedFiles: schematizedFiles,
descriptors: descriptors.sorted { indexFromID($0.id) < indexFromID($1.id) },
supportFileContent: Self.supportFileContent
)
}

private func buildVisitors(for sources: [ParsedSource]) -> [String: TypeScopeVisitor] {
var result: [String: TypeScopeVisitor] = [:]

for source in sources {
let visitor = TypeScopeVisitor()
visitor.walk(source.syntax)
result[source.file.path] = visitor
}

return result
}

private func assignIndices(
sorted: [MutationPoint],
visitors: [String: TypeScopeVisitor]
) -> [(index: Int, mutation: MutationPoint, isSchematizable: Bool)] {
sorted.enumerated().map { index, mutation in
let schematizable = visitors[mutation.filePath]?.isSchematizable(utf8Offset: mutation.utf8Offset) ?? false
return (index: index, mutation: mutation, isSchematizable: schematizable)
}
return (schematizedFiles, descriptors)
}

private func mutantID(_ index: Int) -> String {
"swift-mutation-testing_\(index)"
}

private func indexFromID(_ id: String) -> Int {
Int(id.replacingOccurrences(of: "swift-mutation-testing_", with: "")) ?? 0
}

private func descriptor(
private func makeDescriptor(
from mutation: MutationPoint,
id: String,
isSchematizable: Bool,
Expand Down
102 changes: 102 additions & 0 deletions Sources/SwiftMutationTesting/Execution/FallbackExecutor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Foundation

struct FallbackExecutor: Sendable {
let deps: ExecutionDeps
let configuration: RunnerConfiguration

func execute(input: RunnerInput, pool: SimulatorPool, testFilesHash: String) async throws -> [ExecutionResult] {
var results: [ExecutionResult] = []

for file in input.schematizedFiles {
results += try await processFile(file: file, input: input, pool: pool, testFilesHash: testFilesHash)
}

return results
}

private func processFile(
file: SchematizedFile,
input: RunnerInput,
pool: SimulatorPool,
testFilesHash: String
) async throws -> [ExecutionResult] {
let fileMutants = input.mutants.filter { $0.filePath == file.originalPath && $0.isSchematizable }

guard !fileMutants.isEmpty else { return [] }

if let cached = await cachedResults(for: fileMutants, testFilesHash: testFilesHash) {
return cached
}

let sandbox = try await SandboxFactory().create(
projectPath: input.projectPath,
schematizedFiles: [file],
supportFileContent: input.supportFileContent
)

await deps.reporter.report(.fallbackBuildStarted(filePath: file.originalPath))

guard case .xcode(let scheme, let destination) = configuration.build.projectType else {
try? sandbox.cleanup()
return await markUnviable(mutants: fileMutants, testFilesHash: testFilesHash)
}

let artifact: BuildArtifact
do {
artifact = try await BuildStage(launcher: deps.launcher).build(
sandbox: sandbox,
scheme: scheme,
destination: destination,
timeout: configuration.build.timeout
)
await deps.reporter.report(.fallbackBuildFinished(filePath: file.originalPath, success: true))
} catch {
await deps.reporter.report(.fallbackBuildFinished(filePath: file.originalPath, success: false))
try? sandbox.cleanup()
return await markUnviable(mutants: fileMutants, testFilesHash: testFilesHash)
}

let context = TestExecutionContext(
artifact: artifact, sandbox: sandbox, pool: pool,
configuration: configuration, testFilesHash: testFilesHash
)

let stageResults = try await TestExecutionStage(deps: deps).execute(mutants: fileMutants, in: context)
try? sandbox.cleanup()
return stageResults
}

private func cachedResults(for mutants: [MutantDescriptor], testFilesHash: String) async -> [ExecutionResult]? {
guard !configuration.build.noCache else { return nil }

var results: [ExecutionResult] = []
for mutant in mutants {
let key = MutantCacheKey.make(for: mutant, testFilesHash: testFilesHash)
guard let status = await deps.cacheStore.result(for: key) else { return nil }
results.append(ExecutionResult(descriptor: mutant, status: status, testDuration: 0))
}

for result in results {
let index = await deps.counter.increment()
await deps.reporter.report(
.mutantFinished(
descriptor: result.descriptor, status: result.status,
index: index, total: deps.counter.total))
}

return results
}

private func markUnviable(mutants: [MutantDescriptor], testFilesHash: String) async -> [ExecutionResult] {
var results: [ExecutionResult] = []
for mutant in mutants {
let key = MutantCacheKey.make(for: mutant, testFilesHash: testFilesHash)
await deps.cacheStore.store(status: .unviable, for: key)
let index = await deps.counter.increment()
await deps.reporter.report(
.mutantFinished(descriptor: mutant, status: .unviable, index: index, total: deps.counter.total))
results.append(ExecutionResult(descriptor: mutant, status: .unviable, testDuration: 0))
}
return results
}
}
Loading