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
4 changes: 2 additions & 2 deletions Sources/SwiftMutationTesting/Build/BuildArtifact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import Foundation

struct BuildArtifact: Sendable {
let derivedDataPath: String
let xctestrunURL: URL
let plist: XCTestRunPlist
let xctestrunURL: URL?
let plist: XCTestRunPlist?
}
27 changes: 27 additions & 0 deletions Sources/SwiftMutationTesting/Build/BuildStage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,33 @@ struct BuildStage: Sendable {
)
}

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

if let testTarget {
arguments += ["--target", testTarget]
}

let exitCode = try await launcher.launch(
executableURL: URL(fileURLWithPath: "/usr/bin/swift"),
arguments: arguments,
workingDirectoryURL: sandbox.rootURL,
timeout: timeout
)

guard exitCode == 0 else { throw BuildError.compilationFailed }

return BuildArtifact(
derivedDataPath: sandbox.rootURL.appendingPathComponent(".build").path,
xctestrunURL: nil,
plist: nil
)
}

private func findXcworkspace(in directory: URL) -> URL? {
let items =
(try? FileManager.default.contentsOfDirectory(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ struct TestExecutionStage: Sendable {
return result
}

let plistData = context.artifact.plist.activating(mutant.id)
guard let plist = context.artifact.plist else {
return ExecutionResult(descriptor: mutant, status: .unviable, testDuration: 0)
}

let plistData = plist.activating(mutant.id)
let slot = try await context.pool.acquire()
let launched: TestLaunchResult
do {
Expand Down Expand Up @@ -80,8 +84,10 @@ struct TestExecutionStage: Sendable {
slot: SimulatorSlot,
in context: TestExecutionContext
) async throws -> TestLaunchResult {
let xctestrunURL = context.artifact.xctestrunURL.deletingLastPathComponent()
.appendingPathComponent("\(UUID().uuidString).xctestrun")
let baseURL =
context.artifact.xctestrunURL?.deletingLastPathComponent()
?? context.sandbox.rootURL
let xctestrunURL = baseURL.appendingPathComponent("\(UUID().uuidString).xctestrun")
let xcresultPath = context.sandbox.rootURL
.appendingPathComponent("\(UUID().uuidString).xcresult").path

Expand Down
17 changes: 16 additions & 1 deletion Sources/SwiftMutationTesting/Sandbox/SandboxFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -268,8 +268,9 @@ struct SandboxFactory: Sendable {
let sourcesURL = sandboxURL.appendingPathComponent("Sources")

if FileManager.default.fileExists(atPath: sourcesURL.path) {
let targetURL = firstSourcesTargetDirectory(in: sourcesURL) ?? sourcesURL
try content.write(
to: sourcesURL.appendingPathComponent("__SMTSupport.swift"),
to: targetURL.appendingPathComponent("__SMTSupport.swift"),
atomically: true,
encoding: .utf8
)
Expand All @@ -290,4 +291,18 @@ struct SandboxFactory: Sendable {

try (existing + "\n" + content).write(to: sandboxFileURL, atomically: true, encoding: .utf8)
}

private func firstSourcesTargetDirectory(in sourcesURL: URL) -> URL? {
let items =
(try? FileManager.default.contentsOfDirectory(
at: sourcesURL,
includingPropertiesForKeys: [.isDirectoryKey]
)) ?? []

return
items
.filter { (try? $0.resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true }
.sorted { $0.lastPathComponent < $1.lastPathComponent }
.first
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct BuildStageTests {
)

#expect(artifact.derivedDataPath == projectDir.appendingPathComponent(".xmr-derived-data").path)
#expect(artifact.xctestrunURL.lastPathComponent == "App.xctestrun")
#expect(artifact.xctestrunURL?.lastPathComponent == "App.xctestrun")
}

@Test("Given build failure, when build called, then throws compilationFailed")
Expand Down Expand Up @@ -77,7 +77,7 @@ struct BuildStageTests {
sandbox: sandbox, scheme: "App", destination: "platform=macOS", timeout: 60
)

#expect(artifact.xctestrunURL.lastPathComponent == "App.xctestrun")
#expect(artifact.xctestrunURL?.lastPathComponent == "App.xctestrun")
}

@Test("Given xcodeproj in sandbox, when build called, then project flag is passed")
Expand Down Expand Up @@ -105,7 +105,7 @@ struct BuildStageTests {
sandbox: sandbox, scheme: "App", destination: "platform=macOS", timeout: 60
)

#expect(artifact.xctestrunURL.lastPathComponent == "App.xctestrun")
#expect(artifact.xctestrunURL?.lastPathComponent == "App.xctestrun")
}

@Test("Given xctestrun file with invalid plist data, when build called, then throws xctestrunNotFound")
Expand Down Expand Up @@ -147,4 +147,47 @@ struct BuildStageTests {
)
}
}

@Test("Given successful SPM build, when buildSPM called, then returns artifact with nil xctestrunURL and plist")
func spmBuildReturnsArtifactWithNilXctestrunAndPlist() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(projectDir) }

let sandbox = Sandbox(rootURL: projectDir)
let stage = BuildStage(launcher: MockProcessLauncher(exitCode: 0))

let artifact = try await stage.buildSPM(sandbox: sandbox, testTarget: nil, timeout: 60)

#expect(artifact.derivedDataPath == projectDir.appendingPathComponent(".build").path)
#expect(artifact.xctestrunURL == nil)
#expect(artifact.plist == nil)
}

@Test("Given SPM build failure, when buildSPM called, then throws compilationFailed")
func spmBuildFailureThrowsCompilationFailed() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(projectDir) }

let sandbox = Sandbox(rootURL: projectDir)
let stage = BuildStage(launcher: MockProcessLauncher(exitCode: 1))

await #expect(throws: BuildError.compilationFailed) {
try await stage.buildSPM(sandbox: sandbox, testTarget: nil, timeout: 60)
}
}

@Test("Given SPM build with test target, when buildSPM called, then target flag is included in arguments")
func spmBuildWithTestTargetIncludesTargetFlag() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(projectDir) }

let launcher = MockProcessLauncher(exitCode: 0)
let sandbox = Sandbox(rootURL: projectDir)
let stage = BuildStage(launcher: launcher)

let artifact = try await stage.buildSPM(sandbox: sandbox, testTarget: "MyLibTests", timeout: 60)

#expect(artifact.xctestrunURL == nil)
#expect(artifact.plist == nil)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,63 @@ struct SandboxFactoryTests {
#expect(content == "let support = true")
}

@Test(
"Given SPM project with target subdir, when sandbox created, then __SMTSupport.swift is in first target dir"
)
func injectsSupportFileIntoFirstSPMTargetDirectory() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(projectDir) }

let targetDir = projectDir.appendingPathComponent("Sources/MyLib")
try FileManager.default.createDirectory(at: targetDir, withIntermediateDirectories: true)
try FileHelpers.write("original content", named: "File.swift", in: targetDir)
let filePath = targetDir.appendingPathComponent("File.swift").path

let schematized = SchematizedFile(originalPath: filePath, schematizedContent: "schematized content")
let sandbox = try await factory.create(
projectPath: projectDir.path,
schematizedFiles: [schematized],
supportFileContent: "let support = true"
)
defer { try? sandbox.cleanup() }

let supportURL = sandbox.rootURL.appendingPathComponent("Sources/MyLib/__SMTSupport.swift")
let rootLevelURL = sandbox.rootURL.appendingPathComponent("Sources/__SMTSupport.swift")
let content = try String(contentsOf: supportURL, encoding: .utf8)

#expect(content == "let support = true")
#expect(!FileManager.default.fileExists(atPath: rootLevelURL.path))
}

@Test(
"Given SPM multiple targets, when sandbox created, then __SMTSupport.swift goes into first alphabetical dir"
)
func injectsSupportFileIntoFirstAlphabeticalSPMTargetDirectory() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
defer { FileHelpers.cleanup(projectDir) }

let alphaDir = projectDir.appendingPathComponent("Sources/Alpha")
let betaDir = projectDir.appendingPathComponent("Sources/Beta")
try FileManager.default.createDirectory(at: alphaDir, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: betaDir, withIntermediateDirectories: true)
try FileHelpers.write("let a = 1", named: "Alpha.swift", in: alphaDir)
let filePath = alphaDir.appendingPathComponent("Alpha.swift").path

let schematized = SchematizedFile(originalPath: filePath, schematizedContent: "let a = 2")
let sandbox = try await factory.create(
projectPath: projectDir.path,
schematizedFiles: [schematized],
supportFileContent: "let support = true"
)
defer { try? sandbox.cleanup() }

let supportInAlpha = sandbox.rootURL.appendingPathComponent("Sources/Alpha/__SMTSupport.swift")
let supportInBeta = sandbox.rootURL.appendingPathComponent("Sources/Beta/__SMTSupport.swift")

#expect(FileManager.default.fileExists(atPath: supportInAlpha.path))
#expect(!FileManager.default.fileExists(atPath: supportInBeta.path))
}

@Test("Given Xcode project, when sandbox created, then support content is appended to first schematized file")
func injectsSupportContentIntoFirstSchematizedFileForXcodeProject() async throws {
let projectDir = try FileHelpers.makeTemporaryDirectory()
Expand Down