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
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ jobs:
run: cargo metadata --manifest-path examples/bytes/Cargo.toml --no-deps --format-version 1 > /dev/null
- name: Install the validated coverage tool
run: cargo +stable install cargo-llvm-cov --version 0.6.16 --locked
- name: Check coverage annotation compatibility
run: python3 scripts/check_coverage.py
- name: Validate the minimal analysis and test pipeline
run: python3 scripts/check_minimal.py
7 changes: 7 additions & 0 deletions docs/minimal-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ From the repository root:
```sh
cargo build --workspace --locked
cargo +stable install cargo-llvm-cov --version 0.6.16 --locked
python3 scripts/check_coverage.py
python3 scripts/check_minimal.py
```

Expand Down Expand Up @@ -49,3 +50,9 @@ Recursive context lookup now resolves full function names through the compiler's
The local macOS/Apple Silicon run passed using six generation responses and one repair response. Real-model behavior, bytes-scale execution, general multi-crate support, request concurrency limits, hard process timeouts, recovery after forced termination, and explicit run/resume behavior remain later work. Git history is retained.

The coverage entry temporarily annotates sources with `#[coverage(off)]` and enables the nightly feature at crate roots without adding newlines. It preserves compiler flags, restores original bytes on returned errors, and performs no hashing or source fingerprint checks. `cargo llvm-cov report --json` reuses the first run's data; the first XML-producing command retains the tool's normal profile cleanup between candidates. This does not provide crash recovery or general macro expansion. See the [coverage command reference](../utgen/README.md#coverage-of-existing-tests) for scope.

## Coverage Compatibility Regression

`scripts/check_coverage.py` exercises 11 small standalone crates, without a model service or extra crate dependencies. It covers existing coverage attributes, `cfg_attr(test, ...)`, compound/nested conditions with Cargo features enabled and disabled, test-only methods and impls, file-level test modules, and an expression used by `include!`. Each case has an integration test so both normal-library and test-harness builds are checked. It compares execution outcomes, checks raw production-only coverage, verifies source restoration, and confirms preprocessing leaves no original tests while preserving byte offsets. A custom target entry containing an expression is rejected by both commands.

The script runs in Linux CI. Coverage and preprocessing share only source parsing and test predicates; they keep separate edit visitors. Coverage attributes and their feature gate are added under the complement of existing conditions, without a cfg evaluator or compiler-flag overrides. Explicit `coverage(on)` remains an intentional opt-in. Expression fragments are preserved, without macro expansion or automatic test exclusion inside those fragments.
6 changes: 3 additions & 3 deletions docs/palm-rust-unit-test-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,10 +393,10 @@ export PALM_CONFIG="$(pwd)/utgen/res/api.json"
- `gen_test_rate` 读取生成结果,运行覆盖率,计算每个被测函数的测试数、编译通过数、运行通过数、行覆盖和分支覆盖。

- `utgen/src/run/coverage.rs`
- 覆盖率入口复用预处理的测试识别规则,在测试模块、独立测试函数及 `cfg(test)` 辅助函数前临时添加 `#[coverage(off)]`。整个测试模块(含生成的 `llmtests`)排除后,其中的辅助代码也不计入覆盖率分子和分母;被调用的生产函数仍参与统计。
- 在 crate 根文件同一行临时开启 `coverage_attribute`,保持原行号,不覆盖编译 flags。成功或编译/导出报错返回前恢复源文件原始内容;强制终止后的恢复留待后续处理。
- 覆盖率入口复用预处理的测试识别规则,在测试模块、独立测试函数及 `cfg(test)` 辅助函数/方法前临时添加 `#[coverage(off)]`。整个测试模块(含生成的 `llmtests`)排除后,其中的辅助代码默认不计入覆盖率分子和分母;测试专用 `impl` 按方法处理,带文件级 `#![cfg(test)]` 的模块整文件排除。被调用的生产函数仍参与统计。
- 已有 coverage 属性和 feature 开关予以复用;条件属性只在原条件不成立时补充,条件由编译器判断,显式 `coverage(on)` 保留。在 crate 根文件同一行临时开启 `coverage_attribute`,保持原行号,不覆盖编译 flags。成功或编译/导出报错返回前恢复源文件原始内容;强制终止后的恢复留待后续处理。
- 测试只执行一次,先输出 XML,再由 `report --json` 导出同一份运行数据。第一条命令保留默认清理,避免候选之间累积旧覆盖率。
- `utgen coverage -p <原始 crate 工作副本>` 可在预处理前测量原有测试,无需模型配置和分析产物;结果保留在该副本的 `coverage.xml`、`coverage.json`。断言失败仍导出覆盖率,编译或报告错误则返回失败。当前支持源码位于 `src/` 的独立 crate,不展开宏,也不自动推断未标记的公共辅助函数是否仅用于测试。
- `utgen coverage -p <原始 crate 工作副本>` 可在预处理前测量原有测试,无需模型配置和分析产物;结果保留在该副本的 `coverage.xml`、`coverage.json`。断言失败仍导出覆盖率,编译或报告错误则返回失败。当前支持源码位于 `src/` 的独立 crate,非入口文件若是合法表达式片段(如 `include!` 引入的表达式),预处理与覆盖率均原样保留,不自动改写片段内部的测试代码;入口文件与其他解析错误仍严格检查。不展开宏,也不自动推断未标记的公共辅助函数是否仅用于测试。

- `utgen/src/run/coverage_json.rs`
- 解析 `coverage.json` 中的 branch 信息。
Expand Down
144 changes: 144 additions & 0 deletions scripts/check_coverage.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Compile coverage compatibility cases in temporary standalone crates."""
import argparse
import json
import os
from pathlib import Path
import re
import subprocess
import tempfile
import xml.etree.ElementTree as ET

REPO = Path(__file__).resolve().parents[1]
PRODUCTION = 'pub fn production() -> u8 { 42 }\n'
TEST = '#[test] fn checks() { assert_eq!(production(), 42); }\n'
NIGHTLY_GATE = '#![cfg_attr(coverage_nightly, feature(coverage_attribute))]\n'
CONDITIONAL = {
'src/lib.rs': (
'#![cfg_attr(all(coverage_nightly, feature="existing"), feature(coverage_attribute))]\n'
+ PRODUCTION
+ '#[cfg_attr(coverage_nightly, cfg_attr(feature="existing", coverage(off)))]\n'
+ TEST
),
}
CASES = {
'unconditional': {
'src/lib.rs': '#![feature(coverage_attribute)]\n' + PRODUCTION + '#[coverage(off)]\n' + TEST,
},
'conditional_off': {
'src/lib.rs': NIGHTLY_GATE + PRODUCTION + '#[cfg_attr(coverage_nightly, coverage(off))]\n' + TEST,
},
'test_gate': {
'src/lib.rs': '#![cfg_attr(test, feature(coverage_attribute))]\n' + PRODUCTION + TEST,
},
'all_gate': {
'src/lib.rs': '#![cfg_attr(all(coverage_nightly), feature(coverage_attribute))]\n' + PRODUCTION + TEST,
},
'feature_disabled': CONDITIONAL,
'feature_enabled': CONDITIONAL,
'method_cfg': {
'src/lib.rs': PRODUCTION + 'struct S;\nimpl S { #[cfg(test)] fn method_helper() -> u8 { production() } }\n'
'#[test] fn checks() { assert_eq!(S::method_helper(), 42); }\n',
},
'impl_cfg': {
'src/lib.rs': PRODUCTION + 'struct S;\n#[cfg(test)] impl S { fn impl_helper() -> u8 { production() } }\n'
'#[test] fn checks() { assert_eq!(S::impl_helper(), 42); }\n',
},
'inner_cfg': {
'src/lib.rs': PRODUCTION + 'mod support;\n',
'src/support.rs': '#![cfg(test)]\nfn inner_helper() -> u8 { super::production() }\n'
'#[test] fn checks() { assert_eq!(inner_helper(), 42); }\n',
},
'outer_cfg': {
'src/lib.rs': PRODUCTION + '#[cfg(test)] #[cfg_attr(coverage_nightly, coverage(off))] mod support;\n',
'src/support.rs': 'fn outer_helper() -> u8 { super::production() }\n'
'#[test] fn checks() { assert_eq!(outer_helper(), 42); }\n',
},
'expression_fragment': {
'src/lib.rs': 'pub fn production() -> u8 { include!("value.rs") }\n' + TEST,
'src/value.rs': '42\n',
},
}


def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bin-dir', type=Path, default=REPO / 'target/debug')
args = parser.parse_args()
utgen = str(args.bin_dir.resolve() / 'utgen')
work = Path(tempfile.mkdtemp(prefix='palm-coverage-check-')).resolve()
print(f'Coverage compatibility artifacts: {work}', flush=True)
env = os.environ.copy()
env.pop('CARGO_TARGET_DIR', None)
for name in ['PALM_CONFIG', 'PALM_API_BASE', 'PALM_API_KEY', 'PALM_MODEL']:
env.pop(name, None)

def run(root, label, command, success=True):
result = subprocess.run(command, cwd=root, env=env, capture_output=True, text=True, timeout=180)
log = root / (label + '.log')
log.write_text(result.stdout + result.stderr)
assert (result.returncode == 0) == success, f'{label}: see {log}\n{(result.stdout + result.stderr)[-6000:]}'
return result.stdout

for name, sources in CASES.items():
print(name, flush=True)
root = work / name
root.mkdir()
default = '["existing"]' if name == 'feature_enabled' else '[]'
(root / 'Cargo.toml').write_text(
'[package]\nname="coverage_fixture"\nversion="0.1.0"\nedition="2021"\n'
f'[features]\nexisting=[]\ndefault={default}\n'
'[lints.rust]\nunexpected_cfgs={level="deny", check-cfg=["cfg(coverage_nightly)"]}\n'
)
(root / 'rust-toolchain.toml').write_text('[toolchain]\nchannel="nightly-2025-03-19"\n')
# An integration test also compiles the library without cfg(test), so
# conditional feature gates must work in both compilation modes.
sources = sources | {'tests/integration.rs':
'#[test] fn integration() { assert_eq!(coverage_fixture::production(), 42); }\n'}
for relative, content in sources.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
baseline = {relative: (root / relative).read_bytes() for relative in sources}
original = run(root, 'ordinary-tests', ['cargo', 'test', '--offline', '--tests'])
covered = run(root, 'coverage', [utgen, 'coverage', '-p', str(root)])
outcomes = r'test result: (\w+)\. (\d+) passed; (\d+) failed; (\d+) ignored;'
assert re.findall(outcomes, original) == re.findall(outcomes, covered)
assert all((root / relative).read_bytes() == contents for relative, contents in baseline.items())
report = json.loads((root / 'coverage.json').read_text())
functions = [f for data in report['data'] for f in data['functions']]
assert functions and all('production' in f['name'] for f in functions), functions
assert all(f['count'] > 0 for f in functions), functions
xml = ET.parse(root / 'coverage.xml')
assert all('helper' not in m.get('name', '') and 'checks' not in m.get('name', '')
for m in xml.iter('method'))

run(root, 'preprocess', [utgen, 'pre-process', '-p', str(root)])
for relative, original_bytes in baseline.items():
if not relative.startswith('src/'):
continue
prepared = (root / relative).read_bytes()
assert len(prepared) == len(original_bytes)
assert [i for i, b in enumerate(prepared) if b == 10] == [i for i, b in enumerate(original_bytes) if b == 10]
if name == 'expression_fragment':
assert (root / 'src/value.rs').read_bytes() == baseline['src/value.rs']
prepared_output = run(root, 'prepared-tests', ['cargo', 'test', '--offline', '--tests'])
assert re.findall(r'running (\d+) tests?', prepared_output) == ['0'], prepared_output

# A custom Cargo entry path is still an entry, not an include! fragment.
root = work / 'invalid_entry'
(root / 'src').mkdir(parents=True)
(root / 'Cargo.toml').write_text(
'[package]\nname="invalid_entry"\nversion="0.1.0"\nedition="2021"\n'
'[lib]\npath="src/entry.rs"\n'
)
(root / 'rust-toolchain.toml').write_text('[toolchain]\nchannel="nightly-2025-03-19"\n')
(root / 'src/entry.rs').write_text('42\n')
for command in ['coverage', 'pre-process']:
run(root, command, [utgen, command, '-p', str(root)], success=False)
assert (root / 'src/entry.rs').read_text() == '42\n'
print(f'All {len(CASES)} coverage/preprocessing cases passed.', flush=True)


if __name__ == '__main__':
main()
8 changes: 4 additions & 4 deletions utgen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ For multiple crates, use explicit work-directory paths:
utgen pre-process -p <project-root> -w <crate-path-1> -w <crate-path-2>
```

The preprocessor removes modules whose `cfg` predicates are provably disabled without `test`, and functions with `test` or namespaced `test` attributes. It preserves production predicates such as `cfg(not(test))` and treats unknown feature/target predicates conservatively. Source files retain their line breaks and byte offsets. UTF-8 character columns can change inside blanked ranges; perform analysis after preprocessing. Existing `tests.bak` is never overwritten. There is no general reverse-preprocessing command.
The preprocessor removes modules whose `cfg` predicates are provably disabled without `test`, functions with `test` or namespaced `test` attributes, test-only impls/methods, and the contents of files with an inner `#![cfg(test)]` attribute. It preserves production predicates such as `cfg(not(test))` and treats unknown feature/target predicates conservatively. Source files retain their line breaks and byte offsets. UTF-8 character columns can change inside blanked ranges; perform analysis after preprocessing. Existing `tests.bak` is never overwritten. Non-entry files that parse as expressions (for example, `include!("value.rs")` fragments) are left unchanged; other parse failures remain errors. Cargo metadata identifies target entry files, which must parse as complete Rust files. There is no general reverse-preprocessing command.

### Analysis

Expand All @@ -120,11 +120,11 @@ utgen coverage -p <original-crate-copy>

This command needs neither model configuration nor analysis artifacts. It writes `coverage.xml` and `coverage.json` in the crate directory from a single test run. Assertion failures remain visible in test output and coverage is still exported; compilation or report-export failures return a nonzero exit status. A failed command does not guarantee valid output files.

The same coverage entry point is used by generation and repair. It temporarily adds `#[coverage(off)]` to test-only modules (including generated `llmtests`), standalone test functions, and `#[cfg(test)]` helper functions. Module exclusion also covers nested helpers. Production functions called by tests remain instrumented; mixed production/test files are never excluded wholesale. Integration-test files use cargo-llvm-cov's default directory exclusions. Doc tests are not run by `--tests`.
The same coverage entry point is used by generation and repair. It temporarily adds `#[coverage(off)]` to test-only modules (including generated `llmtests`), standalone test functions, and `#[cfg(test)]` helper functions/methods. Methods inside test-only impls are excluded individually; files with `#![cfg(test)]` are excluded as a whole. Module exclusion also covers nested helpers. Production functions called by tests remain instrumented; mixed production/test files are never excluded wholesale. Integration-test files use cargo-llvm-cov's default directory exclusions. Doc tests are not run by `--tests`.

The nightly feature gate is inserted temporarily on the same line at crate roots, leaving compiler flags and Cargo configuration untouched. Ordinary compilation and repair need no coverage attributes. Coverage annotations are removed by restoring the bytes saved at coverage entry, after success or a returned build/report error. Temporary insertions preserve line numbers, but columns and byte offsets can change while measuring; use working copies and do not edit them concurrently. Recovery for the broader generation/repair backup flow and forced termination is still pending.
Existing coverage attributes and feature gates are reused. For conditional attributes, PALM adds a complementary `cfg_attr` only where the existing setting is absent; rustc evaluates the conditions for each build. Explicit `coverage(on)` settings are respected. The nightly feature gate is inserted temporarily on the same line at crate roots, leaving compiler flags and Cargo configuration untouched. Ordinary compilation and repair need no coverage attributes. Coverage annotations are removed by restoring the bytes saved at coverage entry, after success or a returned build/report error. Temporary insertions preserve line numbers, but columns and byte offsets can change while measuring; use working copies and do not edit them concurrently. Recovery for the broader generation/repair backup flow and forced termination is still pending.

Scope is a standalone crate with sources under `src/` and Cargo target entry files. Test identification reuses preprocessing's conservative `cfg` rules; macros are not expanded, and unmarked helpers outside test-only modules cannot be inferred as test-only. The pinned nightly and cargo-llvm-cov version are required.
Scope is a standalone crate with sources under `src/` and Cargo target entry files. Test identification reuses preprocessing's conservative `cfg` rules. Non-entry expression fragments are kept unchanged, including any code inside them; macros are not expanded, and unmarked helpers outside test-only modules cannot be inferred as test-only. The pinned nightly and cargo-llvm-cov version are required.

### Generation

Expand Down
Loading
Loading