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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ Makefile
# Generated by swift-mutation-testing
.swift-mutation-testing-cache/
Fixtures/CalcApp/.xmr-cache/
Fixtures/CalcLibrary/.build/
11 changes: 11 additions & 0 deletions Fixtures/CalcLibrary/Package.swift
Original file line number Diff line number Diff line change
@@ -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"]),
]
)
5 changes: 5 additions & 0 deletions Fixtures/CalcLibrary/Sources/CalcLibrary/Calculator.swift
Original file line number Diff line number Diff line change
@@ -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 }
}
3 changes: 3 additions & 0 deletions Fixtures/CalcLibrary/Sources/CalcLibrary/Logic.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
struct Logic {
func isNonNegative(_ n: Int) -> Bool { n >= 0 }
}
3 changes: 3 additions & 0 deletions Fixtures/CalcLibrary/Sources/CalcLibrary/Validator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
struct Validator {
func isInRange(_ value: Int) -> Bool { value >= 0 && value <= 100 }
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ let package = Package(
name: "SwiftMutationTestingTests",
dependencies: ["SwiftMutationTesting"],
path: "Tests/SwiftMutationTestingTests",
exclude: ["TestSupport/Fixtures"],
swiftSettings: [
.swiftLanguageMode(.v6)
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
),
Expand All @@ -86,6 +87,7 @@ struct ProjectDetector: Sendable {
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: ["package", "dump-package"],
environment: nil,
additionalEnvironment: [:],
workingDirectoryURL: projectURL,
timeout: 30
),
Expand Down Expand Up @@ -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
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ struct TestExecutionStage: Sendable {
let captured = try await launcher.launchCapturing(
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: arguments,
environment: ["__SWIFT_MUTATION_TESTING_ACTIVE": mutant.id],
environment: nil,
additionalEnvironment: ["__SWIFT_MUTATION_TESTING_ACTIVE": mutant.id],
workingDirectoryURL: context.sandbox.rootURL,
timeout: context.configuration.build.timeout
)
Expand Down Expand Up @@ -163,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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand All @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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"] ?? "" }"#
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading