Skip to content

Commit b87420a

Browse files
karthiknadigCopilot
andcommitted
test: prove subprocess and production coverage (Fixes #534)
Verify exact idle/info child profiles and real handler/transport/writer counter increases. Add conservative production/changed-line diagnostics without altering existing raw coverage gates, and measure native macOS against the exact base on one runner. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 597b599 commit b87420a

8 files changed

Lines changed: 863 additions & 1 deletion

File tree

‎.github/workflows/coverage-baseline.yml‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ jobs:
2727
steps:
2828
- name: Checkout
2929
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
30+
with:
31+
fetch-depth: 0
32+
persist-credentials: false
3033

3134
- name: Set Python to PATH
3235
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -181,6 +184,21 @@ jobs:
181184
env:
182185
RUST_BACKTRACE: 1
183186
RUST_LOG: trace
187+
PET_SUBPROCESS_COVERAGE_PROOF: ${{ github.workspace }}/subprocess-coverage.json
188+
shell: bash
189+
190+
- name: Verify Isolated Server Coverage
191+
if: always()
192+
run: python scripts/coverage_detail.py proof --manifest subprocess-coverage.json --output subprocess-coverage
193+
shell: bash
194+
195+
- name: Report Production and Changed Coverage
196+
if: always()
197+
run: >-
198+
python scripts/coverage_detail.py report
199+
--lcov lcov.info
200+
--base "HEAD"
201+
--output production-coverage
184202
shell: bash
185203

186204
- name: Validate Coverage Baseline
@@ -194,8 +212,14 @@ jobs:
194212
shell: bash
195213

196214
- name: Upload Coverage Artifact
215+
if: always()
197216
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
198217
with:
199218
name: coverage-baseline-${{ matrix.os }}
200-
path: lcov.info
219+
path: |
220+
lcov.info
221+
coverage-baseline-report.md
222+
production-coverage/
223+
subprocess-coverage/
224+
subprocess-coverage.json
201225
retention-days: 90
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
name: Native macOS Coverage
2+
3+
on:
4+
pull_request:
5+
branches: [main, 'release*', 'release/*', 'release-*']
6+
push:
7+
branches: [main, 'release*', 'release/*', 'release-*']
8+
workflow_dispatch:
9+
10+
permissions:
11+
contents: read
12+
13+
jobs:
14+
coverage:
15+
runs-on: macos-14
16+
timeout-minutes: 35
17+
steps:
18+
- name: Checkout
19+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
20+
with:
21+
fetch-depth: 0
22+
persist-credentials: false
23+
24+
- name: Set Python to PATH
25+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
26+
with:
27+
python-version: '3.12'
28+
29+
- name: Rust Tool Chain setup
30+
uses: dtolnay/rust-toolchain@stable
31+
with:
32+
toolchain: stable
33+
components: llvm-tools-preview
34+
35+
- name: Install cargo-llvm-cov
36+
uses: taiki-e/install-action@cargo-llvm-cov
37+
38+
- name: Validate Coverage Reporting
39+
run: python -m unittest discover -s scripts/tests -p 'test_*.py' -v
40+
41+
- name: Collect Native macOS Coverage
42+
run: cargo llvm-cov --workspace --lcov --output-path lcov.info -- --test-threads=1
43+
env:
44+
PET_SUBPROCESS_COVERAGE_PROOF: ${{ github.workspace }}/subprocess-coverage.json
45+
46+
- name: Verify Isolated Server Coverage
47+
if: always()
48+
run: python scripts/coverage_detail.py proof --manifest subprocess-coverage.json --output subprocess-coverage
49+
50+
- name: Report Production and Changed Coverage
51+
if: always()
52+
env:
53+
BASE: ${{ github.event.pull_request.base.sha || github.sha }}
54+
run: python scripts/coverage_detail.py report --lcov lcov.info --base "$BASE" --output production-coverage
55+
56+
- name: Measure Exact PR Base on the Same Runner
57+
if: github.event_name == 'pull_request'
58+
env:
59+
BASE: ${{ github.event.pull_request.base.sha }}
60+
CARGO_TARGET_DIR: ${{ runner.temp }}/coverage-base-target
61+
run: |
62+
git worktree add --detach "$RUNNER_TEMP/coverage-base" "$BASE"
63+
cd "$RUNNER_TEMP/coverage-base"
64+
cargo llvm-cov --workspace --lcov --output-path "$GITHUB_WORKSPACE/baseline-lcov.info" -- --test-threads=1
65+
66+
- name: Compare Exact Base Coverage
67+
if: always() && github.event_name == 'pull_request'
68+
run: >-
69+
python scripts/quality_snapshot.py coverage
70+
--current lcov.info --baseline baseline-lcov.info --platform macOS
71+
--report coverage-report.md --summary "$GITHUB_STEP_SUMMARY"
72+
73+
- name: Validate Main Coverage
74+
if: github.event_name != 'pull_request'
75+
run: >-
76+
python scripts/quality_snapshot.py coverage
77+
--current lcov.info --baseline lcov.info --platform macOS
78+
--report coverage-report.md --summary "$GITHUB_STEP_SUMMARY"
79+
80+
- name: Upload Native macOS Coverage
81+
if: always()
82+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
83+
with:
84+
name: coverage-macos-native
85+
path: |
86+
lcov.info
87+
baseline-lcov.info
88+
coverage-report.md
89+
production-coverage/
90+
subprocess-coverage/
91+
subprocess-coverage.json
92+
retention-days: 30

‎.github/workflows/coverage.yml‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ jobs:
3232
steps:
3333
- name: Checkout
3434
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
35+
with:
36+
fetch-depth: 0
37+
persist-credentials: false
3538

3639
- name: Post Coverage Started Comment
3740
if: github.event.pull_request.head.repo.full_name == github.repository
@@ -200,6 +203,21 @@ jobs:
200203
env:
201204
RUST_BACKTRACE: 1
202205
RUST_LOG: trace
206+
PET_SUBPROCESS_COVERAGE_PROOF: ${{ github.workspace }}/subprocess-coverage.json
207+
shell: bash
208+
209+
- name: Verify Isolated Server Coverage
210+
if: always()
211+
run: python scripts/coverage_detail.py proof --manifest subprocess-coverage.json --output subprocess-coverage
212+
shell: bash
213+
214+
- name: Report Production and Changed Coverage
215+
if: always()
216+
run: >-
217+
python scripts/coverage_detail.py report
218+
--lcov lcov.info
219+
--base "${{ github.event.pull_request.base.sha }}"
220+
--output production-coverage
203221
shell: bash
204222

205223
- name: Wait for Exact PR Base Coverage
@@ -248,6 +266,9 @@ jobs:
248266
path: |
249267
lcov.info
250268
coverage-report.md
269+
production-coverage/
270+
subprocess-coverage/
271+
subprocess-coverage.json
251272
if-no-files-found: ignore
252273

253274
- name: Post Coverage Comment

‎crates/pet/tests/jsonrpc_server_test.rs‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,20 @@ struct RawRpcClient {
3838

3939
impl RawRpcClient {
4040
fn spawn() -> Self {
41+
Self::spawn_with_profile(None)
42+
}
43+
44+
fn spawn_with_profile(profile: Option<&Path>) -> Self {
4145
let mut command = Command::new(env!("CARGO_BIN_EXE_pet"));
4246
command
4347
.arg("server")
4448
.stdin(Stdio::piped())
4549
.stdout(Stdio::piped())
4650
.stderr(Stdio::inherit());
4751
jsonrpc_client::configure_isolated_pet_environment(&mut command);
52+
if let Some(profile) = profile {
53+
command.env("LLVM_PROFILE_FILE", profile);
54+
}
4855
let mut child = command.spawn().expect("raw fixture must spawn PET");
4956
let stdout = child.stdout.take().expect("PET stdout must be piped");
5057
let (sender, responses) = mpsc::channel();
@@ -819,6 +826,89 @@ fn invalid_and_oversize_framing_terminates_with_bounded_diagnostics() {
819826
}
820827
}
821828

829+
#[test]
830+
fn normal_shutdown_records_pid_unique_server_profiles() {
831+
let Some(proof_path) = std::env::var_os("PET_SUBPROCESS_COVERAGE_PROOF") else {
832+
return;
833+
};
834+
let inherited = PathBuf::from(
835+
std::env::var_os("LLVM_PROFILE_FILE")
836+
.expect("coverage proof requires cargo-llvm-cov instrumentation"),
837+
);
838+
let directory = inherited.parent().expect("profile must have a directory");
839+
assert!(
840+
directory.is_absolute(),
841+
"profile directory must be absolute"
842+
);
843+
assert!(
844+
inherited
845+
.file_name()
846+
.unwrap()
847+
.to_string_lossy()
848+
.contains("%p"),
849+
"cargo-llvm-cov must use PID-unique profiles"
850+
);
851+
let mut profiles = serde_json::Map::new();
852+
for (name, request) in [("idle", false), ("info", true)] {
853+
let prefix = format!("pet-proof-{}-{name}-", std::process::id());
854+
let pattern = directory.join(format!("{prefix}%p-%m.profraw"));
855+
let mut client = RawRpcClient::spawn_with_profile(Some(&pattern));
856+
let prefix = format!("{prefix}{}-", client.child.id());
857+
assert!(
858+
fs::read_dir(directory).unwrap().all(|entry| !entry
859+
.unwrap()
860+
.file_name()
861+
.to_string_lossy()
862+
.starts_with(&prefix)),
863+
"child profile must not predate this process exit"
864+
);
865+
if request {
866+
client.send(json!({"jsonrpc": "2.0", "id": "profile-proof", "method": "info"}));
867+
let reply = client.receive();
868+
assert_eq!(reply["id"], "profile-proof");
869+
assert_eq!(reply["result"]["petVersion"], env!("CARGO_PKG_VERSION"));
870+
}
871+
client.child.stdin.take();
872+
let status = jsonrpc_client::wait_for_exit(&mut client.child, Duration::from_secs(4))
873+
.expect("profile proof requires graceful exit, not kill fallback");
874+
assert!(
875+
status.success(),
876+
"profile probe exited unsuccessfully: {status}"
877+
);
878+
jsonrpc_client::join_reader(client.reader.take().unwrap(), Duration::from_secs(4)).unwrap();
879+
let raw: Vec<PathBuf> = fs::read_dir(directory)
880+
.unwrap()
881+
.map(|entry| entry.unwrap().path())
882+
.filter(|path| {
883+
path.file_name()
884+
.unwrap()
885+
.to_string_lossy()
886+
.starts_with(&prefix)
887+
})
888+
.collect();
889+
assert!(
890+
!raw.is_empty(),
891+
"normal server exit did not flush its own profile"
892+
);
893+
for path in &raw {
894+
assert!(
895+
fs::metadata(path).unwrap().len() > 0,
896+
"empty server profile"
897+
);
898+
}
899+
profiles.insert(name.into(), json!(raw));
900+
}
901+
fs::write(
902+
proof_path,
903+
serde_json::to_vec_pretty(&json!({
904+
"binary": env!("CARGO_BIN_EXE_pet"),
905+
"profiles": profiles,
906+
}))
907+
.unwrap(),
908+
)
909+
.expect("write subprocess coverage evidence");
910+
}
911+
822912
#[test]
823913
fn stdin_eof_after_exchange_exits_cleanly_within_one_second() {
824914
let client = PetJsonRpcClient::spawn().unwrap();

‎docs/QUALITY_SNAPSHOTS.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,53 @@ for the new client clocks.
113113

114114
Linux and Windows line and function coverage are compared with the exact base commit. A decrease greater than 0.01 percentage points blocks the pull request. Coverage artifacts and comments remain available for inspection even when the comparison fails.
115115

116+
### Production-focused and subprocess evidence
117+
118+
The raw workspace percentages still include inline tests and retain the same exact-base
119+
0.01 percentage-point line/function gate. Supplemental `production-coverage/report.md` and
120+
schema-1 `details.json` separate executable production/test lines, list uncovered production
121+
lines, and intersect added/modified Rust lines with executable production lines. Changed files
122+
without instrumentation are listed explicitly, never assumed covered. These diagnostics do not
123+
introduce a fabricated baseline, change the raw denominator, or replace regression protection.
124+
125+
Classification excludes integration-test/benchmark directories and Rust items explicitly marked
126+
`#[cfg(test)]` or `#[test]`, including inline modules and test-only helper functions. It masks
127+
strings, raw/byte strings, characters, and nested comments before matching item boundaries.
128+
Helpers outside those boundaries and complex conditional attributes remain conservatively in the
129+
production category; this is a source-focused diagnostic, not full Rust conditional-compilation
130+
analysis. Invalid/missing LCOV, missing source, inconsistent hit summaries, and source-line
131+
mismatches fail the reporting step. LLVM summaries can include more entries in `LF`/`LH`
132+
than the unique `DA` source lines (observed in real Windows exports). That deficit is reported
133+
per file (including unmatched summary hits) and conservatively retained as uncovered production,
134+
never dropped from the denominator or silently assigned coverage.
135+
Changed lines without `DA` records are listed separately in JSON, including non-executable syntax;
136+
they are not silently considered covered.
137+
138+
Every coverage job opts into `normal_shutdown_records_pid_unique_server_profiles` through
139+
`PET_SUBPROCESS_COVERAGE_PROOF`. The test requires cargo-llvm-cov's absolute, PID-unique output
140+
pattern, launches idle and known-`info` PET subprocesses, closes stdin, and requires successful
141+
bounded exit and nonempty profiles for those exact child PIDs. The raw profiles stay in the normal
142+
cargo-llvm-cov collection directory and are included in the workspace report. The verifier also
143+
merges each child's profiles separately with the matching Rust LLVM tools and proves zero idle
144+
versus positive `info` execution at the real handler, transport dispatch, and response writer.
145+
The uploaded `subprocess-coverage/proof.json` and isolated LCOV exports retain that evidence;
146+
a killed child, missing profile, or absent execution witness fails rather than appearing covered.
147+
148+
Native ARM64 macOS coverage runs workspace default-feature and native process/transport tests,
149+
including Darwin-specific process ownership paths. It intentionally does not compare this workload
150+
with Linux/Windows's installed-manager `ci` workload. For each macOS PR, the exact base revision is
151+
built and measured separately on the same runner with the same compiler and feature selection;
152+
the unchanged line/function comparator gates those comparable artifacts. This works on the first
153+
PR without silently accepting an absent macOS baseline. Main/manual runs publish the native
154+
measurement and proof for inspection. Existing functional macOS installed-manager jobs remain.
155+
156+
Stable Rust line instrumentation does not provide condition/branch outcomes. Reports show LCOV
157+
`BRDA` totals when supplied, otherwise explicitly report branch data as unavailable (not 100%).
158+
Native malformed-frame/envelope, EOF, broken-output, saturation, and descendant tests provide
159+
behavioral failure-path evidence, but line coverage cannot prove both sides of every condition.
160+
Nightly `cargo llvm-cov --branch` can be used as a separate experiment; its unstable toolchain
161+
and differing denominator are not substituted into the stable cross-platform gate.
162+
116163
## Running locally
117164

118165
The comparator requires Python 3.10 or newer.

0 commit comments

Comments
 (0)