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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ jobs:
- name: Check coverage annotation compatibility
if: steps.scope.outputs.run_code == 'true'
run: python3 scripts/check_coverage.py
- name: Check coverage baselines for unit and integration builds
if: steps.scope.outputs.run_code == 'true'
run: cargo test -p utgen --locked coverage_baselines_match_test_build_modes -- --ignored
- name: Validate the minimal analysis and test pipeline
if: steps.scope.outputs.run_code == 'true'
run: python3 scripts/check_minimal.py
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion docs/minimal-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Run the related checks when changing coverage/preprocessing or task scheduling:

```sh
python3 scripts/check_coverage.py
cargo test -p utgen --locked coverage_baselines_match_test_build_modes -- --ignored
python3 scripts/check_tasks.py
```

Expand All @@ -41,7 +42,8 @@ The default binary directory is `target/debug`. The coverage tool must be availa
| Coverage | Excludes test bodies and test-only helpers, preserves production coverage, and executes tests once when exporting XML and JSON. |
| Cleanup | Restores sources after the covered success, assertion-failure, compilation-failure, and report-export-failure cases. |
| Candidate timeout | Ordinary and `should_panic` infinite loops count as failures; an expected panic and a later ordinary test pass in unit and integration statistics. |
| Statistics | Oracle groups count once across multiple candidates; pass results match complete test names; an all-compilation-failure result reports unavailable coverage without exporting an empty run. |
| Statistics | Oracle groups count once across multiple candidates; pass results match complete test names; all-compilation-failure results retain a zero-hit denominator without a synthetic candidate. A passing integration candidate that does not call the focal function has zero focal coverage. |
| Coverage baselines | No-test unit/library and binary harnesses; ordinary-library integration maps; conditional compilation, generics, no-map targets, per-target reuse, separation from real profiles, and source/helper restoration after build failure. |
| Integration repair | Repairs alias/trait imports, helpers, and test bodies in integration targets; keeps sibling candidates independent; checks cached repair, mode mismatch, unselected targets, and cleanup after request failure. |
| Integration directories | Original and generated tests coexist after normal runs and cached reruns; original-test compilation failure restores both directories; an existing staging directory is preserved and reported. |

Expand All @@ -61,6 +63,8 @@ The minimal check's temporary directory contains `logs/`, separate `original/` a

Explicit `coverage(on)` is respected. Expression fragments are preserved; the checks do not provide general macro expansion or infer that arbitrary unmarked helpers are test-only. See the [coverage reference](../utgen/README.md#coverage-of-existing-tests) for supported scope.

The baseline test is normally ignored because it needs the pinned coverage and LLVM tools. The explicit command above and Linux CI run it after installing those tools. It compares ordinary-library baseline maps with a real integration run, checks that a binary's `main` and existing test bodies never run during baseline collection, and removes its temporary fixtures afterward. Per-function `coverage_available=false` values must be excluded from coverage aggregates; they do not mean zero coverage of a known denominator.

## Task Scheduling Regression

`check_tasks.py` exercises function limits N=1, N=2, and the default N=4, with more than 2N functions in each scheduling case. It checks overlapping model requests, serial compiler activity, bounded-queue backpressure, and completion after queued work resumes. Cargo's target directory is reused between scenarios.
Expand Down
2 changes: 2 additions & 0 deletions docs/palm-rust-unit-test-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ export PALM_CONFIG="$(pwd)/utgen/res/api.json"

最终统计以 `utgen/result/` 和 `utgen/fixed_result/` 中的 JSON 为准;当前实现不生成 HTML 报告。`coverage_available=false` 表示没有取得被测函数的覆盖映射,覆盖数值仅为占位,不应参与汇总,也不能直接解释为条件编译排除了函数。旧结果缺少该字段时,其可用性未确认。`tests_*` 按候选计数,`oracles_*` 按原有 TestInfo 分组计数;一组中有候选通过,该组即计为通过。运行数保留包含 ignored 条目的既有口径。

候选覆盖率缺少被测函数映射时,统计会补采集零命中基线。单元测试使用 `--lib` 或 `--bin` 的测试编译产物,过滤全部测试,不再插入空测试;集成测试从普通库产物取得映射,因此能够保留 `#[cfg(not(test))]` 下的函数和分支。基线不增加测试或 oracle 计数,不生成候选覆盖记录,也不混入真实命中数据;同一轮统计按目标复用基线,编译或工具错误仍然报错。函数级集成测试需要库目标;同名库与二进制目标共享模块、无法唯一确定函数归属时,会在模型请求前拒绝执行,完整支持需要进一步完善分析索引中的目标身份。

### 8.7 LLM 修复流程

![编译错误修复流程](assets/palm-fix-flow.png)
Expand Down
27 changes: 23 additions & 4 deletions scripts/check_minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ def do_POST(self):
for _, branches in classify['codes_branches_covered'] for branch in branches), classify
double = json.loads((target / f"utgen/result/{names['palm_fixture::nested::double']}.json").read_text())
assert double['tests_compiled'] == 0 and double['tests'] == 1, double
assert double['coverage_available'] and double['lines'] == 3, double
assert double['lines_covered'] == double['branches_covered'] == 0, double
assert double['codes_lines_covered'] == double['codes_branches_covered'] == [], double
assert not list((target / 'src').rglob('*.bak'))

# Replay cached candidates on one function: ordinary and should_panic
Expand Down Expand Up @@ -371,7 +374,7 @@ def do_POST(self):
assert snapshot(target) == prepared

# Multiple candidates belong to one oracle; all failed compilation is
# still a valid result and must not attempt to export empty coverage.
# still a valid result with a zero-hit ordinary-library denominator.
data['integration'] = True
answer['common'] = []
answer['chain_tests'] = [dict(
Expand All @@ -396,9 +399,25 @@ def do_POST(self):
assert result['tests'] == result['oracles'] == 1, result
assert result['tests_compiled'] == result['tests_run'] == result['tests_passed'] == 0, result
assert result['oracles_compiled'] == result['oracles_run'] == result['oracles_passed'] == 0, result
assert not result['coverage_available'], result
assert result['lines'] == result['branches'] == 0, result
assert result['codes_lines_covered'] == [], result
assert result['coverage_available'], result
assert result['lines'] == 5 and result['branches'] == 2, result
assert result['lines_covered'] == result['branches_covered'] == 0, result
assert result['codes_lines_covered'] == result['codes_branches_covered'] == [], result
test['codes'] = [['{', 'assert_eq!(palm_fixture::nested::double(2), 4);', '}']]
test['can_compile'] = [{'Ok': None}]
candidate.write_text(json.dumps(data))
run('integration-no-focal-call', command)
result = json.loads((target / f'utgen/result/{names[selected]}.json').read_text())
assert result['tests_run'] == result['tests_passed'] == 1, result
assert result['coverage_available'] and result['lines'] == 5 and result['branches'] == 2, result
assert result['lines_covered'] == result['branches_covered'] == 0, result
test['codes'] = [['{', 'assert_eq!(2 + 2, 4);', '}']]
candidate.write_text(json.dumps(data))
run('integration-no-library-call', command)
result = json.loads((target / f'utgen/result/{names[selected]}.json').read_text())
assert result['tests_run'] == result['tests_passed'] == 1, result
assert result['coverage_available'] and result['lines'] == 5 and result['branches'] == 2, result
assert result['lines_covered'] == result['branches_covered'] == 0, result
shutil.rmtree(target / 'tests')
assert snapshot(target) == prepared

Expand Down
1 change: 1 addition & 0 deletions utgen/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ reqwest = { version = "0.12", default-features = false, features = ["json", "rus
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
simplelog = "0.12"
shlex = "1.3"
strfmt = "0.2"
syn = { version = "2.0", features = ["full", "visit"] }
time = "0.3"
Expand Down
4 changes: 4 additions & 0 deletions utgen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ Paths under `utgen/` below are relative to `--project-dir`:

Per-function results include `coverage_available`. When it is false, the numeric coverage fields are placeholders and must not enter coverage aggregates; report the number of unavailable functions separately. Missing mappings are not assumed to mean `cfg(test)` exclusion. Old results without this field have unconfirmed availability. Candidate/compilation statistics remain valid when no candidate compiles.

When candidate coverage has no focal-function mapping, statistics collect a zero-hit baseline for the selected Cargo target. Unit mode builds the `--lib` or `--bin` test harness with every test filtered out; it does not insert an empty test. Integration mode obtains mappings from the ordinary library artifact, using a temporary integration harness without executing tests. Thus `cfg(not(test))` code can contribute to the integration denominator while being absent in unit mode. Compilation and tool failures remain errors. Baselines add no candidate/oracle counts or `codes_*_covered` entries; their profiles stay separate from real test hits. Parsed baselines are reused only within the current statistics invocation.

Function-level integration mode requires a library target. Target selection currently uses the analyzed crate name and, when needed, the source entry file. Ambiguous shared modules in same-name library/binary targets are rejected before model requests. Full mixed-target analysis requires retaining target identity in the analysis index; use a standalone library or binary working copy for those cases.

`tests_*` count candidates; `oracles_*` count the existing `TestInfo` groups. A group counts once if any of its candidates compiles, is registered to run, or passes, respectively. Run counts retain the existing libtest convention, including ignored tests. Integration pass outcomes match complete test names.

`coverage.xml`, `coverage.json`, and `error_output.json` are intermediate files in the work directory and may be deleted after generation/repair parses them. The standalone `coverage` command retains both reports. The current implementation does not produce an HTML report.
Expand Down
1 change: 1 addition & 0 deletions utgen/src/gene/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub async fn gen_tests_project(
));
}
validate_analysis(project_dir, work_dir)?;
crate::run::validate_targets(project_dir, work_dir, functions, integration)?;
let saved_dir = project_dir.join("utgen/generation/pre_fix");
if saved_dir.is_dir() {
for entry in fs::read_dir(saved_dir)? {
Expand Down
62 changes: 39 additions & 23 deletions utgen/src/run/coverage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,27 +184,30 @@ pub fn collect_coverage(work_dir: &Path, json: bool) -> io::Result<Output> {
collect_coverage_for_tests(work_dir, json, &[])
}

pub(crate) fn collect_coverage_for_tests(
work_dir: &Path,
json: bool,
targets: &[String],
) -> io::Result<Output> {
let canonical_dir = work_dir.canonicalize()?;
let work_dir = canonical_dir.as_path();
pub(crate) fn project_metadata(work_dir: &Path) -> io::Result<cargo_metadata::Metadata> {
let metadata = MetadataCommand::new()
.manifest_path(work_dir.join("Cargo.toml"))
.current_dir(work_dir)
.no_deps()
.exec()
.map_err(io::Error::other)?;
let package = metadata.root_package().ok_or_else(|| {
metadata.root_package().ok_or_else(|| {
io::Error::other("coverage requires a standalone crate with Cargo.toml and src/")
})?;
if metadata.workspace_members.len() != 1 || !work_dir.join("src").is_dir() {
return Err(io::Error::other(
"coverage currently supports one standalone crate",
));
}
Ok(metadata)
}

pub(crate) fn with_test_exclusions<T>(
work_dir: &Path,
metadata: &cargo_metadata::Metadata,
action: impl FnOnce() -> io::Result<T>,
) -> io::Result<T> {
let package = metadata.root_package().unwrap();
let roots: BTreeSet<PathBuf> = package
.targets
.iter()
Expand Down Expand Up @@ -237,6 +240,33 @@ pub(crate) fn collect_coverage_for_tests(
for (path, _, annotated) in &changes {
fs::write(path, annotated)?;
}
action()
})();
// Restore even when compilation, test launch, or report export failed.
// Try every changed file before reporting a restoration error.
let mut restore_error = None;
for (path, original, _) in changes {
if let Err(err) = fs::write(&path, original) {
restore_error.get_or_insert_with(|| {
io::Error::other(format!("failed to restore {}: {err}", path.display()))
});
}
}
if let Some(err) = restore_error {
return Err(err);
}
result
}

pub(crate) fn collect_coverage_for_tests(
work_dir: &Path,
json: bool,
targets: &[String],
) -> io::Result<Output> {
let canonical = work_dir.canonicalize()?;
let work_dir = canonical.as_path();
let metadata = project_metadata(work_dir)?;
with_test_exclusions(work_dir, &metadata, || {
// The first command uses llvm-cov's default cleanup. --no-report would
// imply --no-clean and mix profiles from different generated candidates.
let mut args = Vec::new();
Expand All @@ -262,21 +292,7 @@ pub(crate) fn collect_coverage_for_tests(
)?;
}
Ok(output)
})();
// Restore even when compilation, test launch, or report export failed.
// Try every changed file before reporting a restoration error.
let mut restore_error = None;
for (path, original, _) in changes {
if let Err(err) = fs::write(&path, original) {
restore_error.get_or_insert_with(|| {
io::Error::other(format!("failed to restore {}: {err}", path.display()))
});
}
}
if let Some(err) = restore_error {
return Err(err);
}
result
})
}

#[cfg(test)]
Expand Down
Loading
Loading