Skip to content

Commit 4b57491

Browse files
authored
Extract serial per-test coverage profiler substrate (#30)
1 parent 710004c commit 4b57491

5 files changed

Lines changed: 117 additions & 2 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import MutationExecution
2+
3+
/// The result of attempting an authoritative per-test coverage profile.
4+
///
5+
/// A fast profiler must either provide the complete map or decline so the
6+
/// serial reference profiler can produce it. Partial attribution is never a
7+
/// representable result.
8+
enum PerTestCoverageProfileAttempt: Sendable {
9+
case complete(PerTestCoverageMap)
10+
case unavailable(reason: String)
11+
}

Sources/AppleBuildAdapters/SwiftPackageMacOSAdapter.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,35 @@ extension SwiftPackageMacOSAdapter: TestSelecting {
552552
return selectedTests.count
553553
}
554554

555+
public func measurePerTestCoverage(
556+
artifact: BuildArtifact,
557+
in workspace: URL,
558+
timeoutSeconds: Double
559+
) async -> PerTestCoverageMap? {
560+
switch await measurePerTestCoverageFast(
561+
artifact: artifact,
562+
in: workspace,
563+
timeoutSeconds: timeoutSeconds
564+
) {
565+
case .complete(let map):
566+
return map
567+
case .unavailable:
568+
return await measurePerTestCoverageSerial(
569+
artifact: artifact,
570+
in: workspace,
571+
timeoutSeconds: timeoutSeconds
572+
)
573+
}
574+
}
575+
576+
private func measurePerTestCoverageFast(
577+
artifact: BuildArtifact,
578+
in workspace: URL,
579+
timeoutSeconds: Double
580+
) async -> PerTestCoverageProfileAttempt {
581+
.unavailable(reason: "no fast per-test coverage backend is implemented yet")
582+
}
583+
555584
/// Runs every test `swift test list` reports, one at a time, with
556585
/// coverage enabled, against the artifact already built for the
557586
/// baseline — no separate coverage build step: `swift test
@@ -592,7 +621,7 @@ extension SwiftPackageMacOSAdapter: TestSelecting {
592621
/// from `swift test list`, not from the artifact, and per-test
593622
/// coverage needs its own coverage-instrumented run regardless of what
594623
/// `artifact` was built with.
595-
public func measurePerTestCoverage(
624+
private func measurePerTestCoverageSerial(
596625
artifact: BuildArtifact,
597626
in workspace: URL,
598627
timeoutSeconds: Double

Sources/AppleBuildAdapters/XcodeBuildAdapter.swift

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1216,6 +1216,35 @@ extension XcodeBuildAdapter: TestSelecting {
12161216
)
12171217
}
12181218

1219+
public func measurePerTestCoverage(
1220+
artifact: BuildArtifact,
1221+
in workspace: URL,
1222+
timeoutSeconds: Double
1223+
) async -> PerTestCoverageMap? {
1224+
switch await measurePerTestCoverageFast(
1225+
artifact: artifact,
1226+
in: workspace,
1227+
timeoutSeconds: timeoutSeconds
1228+
) {
1229+
case .complete(let map):
1230+
return map
1231+
case .unavailable:
1232+
return await measurePerTestCoverageSerial(
1233+
artifact: artifact,
1234+
in: workspace,
1235+
timeoutSeconds: timeoutSeconds
1236+
)
1237+
}
1238+
}
1239+
1240+
private func measurePerTestCoverageFast(
1241+
artifact: BuildArtifact,
1242+
in workspace: URL,
1243+
timeoutSeconds: Double
1244+
) async -> PerTestCoverageProfileAttempt {
1245+
.unavailable(reason: "no fast per-test coverage backend is implemented yet")
1246+
}
1247+
12191248
/// Runs every test the baseline bundle reports, one at a time, against
12201249
/// the artifact already built for the baseline — no rebuild — with
12211250
/// coverage enabled and its own scratch result bundle, and merges what
@@ -1257,7 +1286,7 @@ extension XcodeBuildAdapter: TestSelecting {
12571286
/// every individual test run, exactly the same "instrumented copy is
12581287
/// never what activation evidence is measured against" split
12591288
/// `readCoverage` makes.
1260-
public func measurePerTestCoverage(
1289+
private func measurePerTestCoverageSerial(
12611290
artifact: BuildArtifact,
12621291
in workspace: URL,
12631292
timeoutSeconds: Double

Tests/MutantKitTests/Acceptance/PerTestProfilingFailClosedAcceptanceTests.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ import Testing
2121
.enabled(if: Acceptance.isEnabled)
2222
)
2323
struct PerTestProfilingFailClosedAcceptanceTests {
24+
@Test("The unavailable fast profiler falls back to the serial reference profiler")
25+
func unavailableFastProfilerFallsBackToSerialReferenceProfiler() async throws {
26+
let workspace = try Acceptance.stageFixture("SwiftPackageMacOS")
27+
defer { try? FileManager.default.removeItem(at: workspace) }
28+
29+
let adapter = SwiftPackageMacOSAdapter(configuration: Configuration())
30+
let artifact = try await adapter.buildBaseline(in: workspace)
31+
32+
let map = await adapter.measurePerTestCoverage(artifact: artifact, in: workspace, timeoutSeconds: 60)
33+
34+
let profile = try #require(map, "the serial reference profiler produced no attribution")
35+
#expect(
36+
profile.source == "swiftpm-codecov-per-test",
37+
"the public profiler did not return the serial reference profiler's result"
38+
)
39+
}
40+
2441
@Test("An unmeasurable test invalidates the whole per-test coverage map, not just its own entry")
2542
func unmeasurableTestProducesNoUsableMap() async throws {
2643
let workspace = try Acceptance.stageFixture("PerTestProfilingPartialFailure")

Tests/MutantKitTests/Acceptance/XcodePerTestProfilingFailClosedAcceptanceTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,35 @@ import Testing
2525
.enabled(if: Acceptance.simulatorEnabled)
2626
)
2727
struct XcodePerTestProfilingFailClosedAcceptanceTests {
28+
@Test("The unavailable fast profiler falls back to the serial reference profiler")
29+
func unavailableFastProfilerFallsBackToSerialReferenceProfiler() async throws {
30+
let workspace = try Acceptance.stageFixture("XcodeProject")
31+
defer { try? FileManager.default.removeItem(at: workspace) }
32+
33+
let destination = try Acceptance.iPhoneDestination()
34+
let configuration = Configuration(
35+
project: ProjectSettings(kind: .xcodeProject, scheme: "Checkout", destination: destination)
36+
)
37+
let adapter = XcodeBuildAdapter(
38+
configuration: configuration,
39+
kind: .xcodeProject,
40+
projectFile: nil,
41+
projectRoot: workspace
42+
)
43+
44+
let artifact = try await adapter.buildBaseline(in: workspace)
45+
let baseline = try await adapter.runBaseline(artifact, in: workspace, timeoutSeconds: 120)
46+
#expect(baseline.status == .passed, "the Checkout baseline must produce the serial profiler's test bundle")
47+
let profile = try #require(
48+
await adapter.measurePerTestCoverage(artifact: artifact, in: workspace, timeoutSeconds: 120),
49+
"the serial reference profiler produced no attribution"
50+
)
51+
#expect(
52+
profile.source == "xcodebuild-xccov-per-test",
53+
"the public profiler did not return the serial reference profiler's result"
54+
)
55+
}
56+
2857
@Test("An unmeasurable test invalidates the whole per-test coverage map, not just its own entry")
2958
func unmeasurableTestProducesNoUsableMap() async throws {
3059
let workspace = try Acceptance.stageFixture("XcodeProject")

0 commit comments

Comments
 (0)