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 @@ -80,6 +80,9 @@ jobs:
- name: Validate the minimal analysis and test pipeline
if: steps.scope.outputs.run_code == 'true'
run: python3 scripts/check_minimal.py
- name: Check mixed library and binary targets
if: steps.scope.outputs.run_code == 'true'
run: python3 scripts/check_mixed.py
- name: Check generation and repair task limits
if: steps.scope.outputs.run_code == 'true'
run: python3 scripts/check_tasks.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.

2 changes: 1 addition & 1 deletion brinfo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cargo clean
cargo brinfo
```

The wrapper analyzes the crate's `lib` and `bin` targets through `cargo check`. Clearing the build cache ensures the compiler analysis runs even if the crate was previously checked. Use a fresh working copy without previous analysis output when repeating an experiment; `cargo clean` does not remove PALM's output files.
The wrapper analyzes a single ordinary `lib` or `bin` target through `cargo check`. For packages with both or with multiple binaries, use `utgen analyze`; it runs each target separately and combines the results with library preference for shared source definitions. Clearing the build cache ensures the compiler analysis runs even if the crate was previously checked. Use a fresh working copy without previous analysis output when repeating an experiment; `cargo clean` does not remove PALM's output files.

## Output

Expand Down
34 changes: 30 additions & 4 deletions brinfo/src/bin/cargo-brinfo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,28 @@ fn in_cargo_brinfo() {
let verbose = has_arg_flag("-v");

let current_crate = current_crate();
if std::env::var_os("PALM_TARGET_NAME").is_none()
&& current_crate
.targets
.iter()
.filter(|t| t.kind.iter().any(|k| k == "lib" || k == "bin"))
.count()
> 1
{
show_error(
"Multiple Cargo targets: use utgen analyze to preserve target ownership".to_owned(),
);
}

let path = Path::new(&current_crate.manifest_path).parent().unwrap();
let path_str = path.to_str().unwrap();

// Now run the command.
for target in current_crate.targets.into_iter() {
if let Ok(selected) = std::env::var("PALM_TARGET_NAME") {
let kind = std::env::var("PALM_TARGET_KIND").unwrap();
if target.name != selected || !target.kind.contains(&kind) { continue; }
}
let mut args = std::env::args().skip(2);
let kind = target
.kind
Expand All @@ -197,7 +213,7 @@ fn in_cargo_brinfo() {
info!("Kind of target {:?} is: {}.", target.name, kind);
match kind.as_str() {
"bin" => {
cmd.arg("--bin").arg(target.name);
cmd.arg("--bin").arg(&target.name);
}
"lib" => {
cmd.arg("--lib");
Expand Down Expand Up @@ -226,7 +242,9 @@ fn in_cargo_brinfo() {
"BRINFO_ARGS",
serde_json::to_string(&args_vec).expect("failed to serialize args"),
);
cmd.env("BRINFO_TOP_CRATE_NAME", current_crate.name.clone());
cmd.env("BRINFO_TOP_CRATE_NAME", &target.name);
cmd.env("BRINFO_TOP_CRATE_KIND", kind);
cmd.env("BRINFO_TOP_CRATE_SRC", target.src_path.as_str());

// Replace the rustc executable through RUSTC_WRAPPER environment variable
let path = std::env::current_exe().expect("current executable path invalid");
Expand All @@ -237,7 +255,7 @@ fn in_cargo_brinfo() {
eprintln!("+ {:?}", cmd);
}

cmd.env("BRINFO_CRATE_DIR", path_str);
cmd.env("BRINFO_CRATE_DIR", std::env::var_os("PALM_OUTPUT_DIR").unwrap_or_else(|| path_str.into()));

// Execute cmd
let exit_status = cmd
Expand Down Expand Up @@ -269,7 +287,15 @@ fn inside_cargo_rustc() {
std::env::var("BRINFO_TOP_CRATE_NAME").expect("missing BRINFO_TOP_CRATE_NAME");
let top_crate_name = top_crate_name.replace("-", "_"); // Cargo seems to rename hyphens to underscores

if get_arg_flag_value("--crate-name").as_deref() == Some(&top_crate_name) {
let kind = std::env::var("BRINFO_TOP_CRATE_KIND").unwrap();
let entry = std::fs::canonicalize(std::env::var_os("BRINFO_TOP_CRATE_SRC").unwrap()).unwrap();
let is_entry = std::env::args()
.skip(2)
.any(|arg| std::fs::canonicalize(arg).is_ok_and(|path| path == entry));
if get_arg_flag_value("--crate-name").as_deref() == Some(&top_crate_name)
&& get_arg_flag_value("--crate-type").as_deref() == Some(&kind)
&& is_entry
{
// If we are analyzing the crate that we want to analyze, add args for `brinfo`
let magic = std::env::var("BRINFO_ARGS").expect("missing BRINFO_ARGS");
let brinfo_args: Vec<String> =
Expand Down
5 changes: 4 additions & 1 deletion docs/minimal-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ Run the related checks when changing coverage/preprocessing or task scheduling:
python3 scripts/check_coverage.py
cargo test -p utgen --locked coverage_baselines_match_test_build_modes -- --ignored
python3 scripts/check_tasks.py
python3 scripts/check_mixed.py
```

All three scripts are included in the full Linux CI suite. Their options differ:
All four scripts are included in the full Linux CI suite. Their options differ:

| Script | Options |
| --- | --- |
| `check_minimal.py` | `--bin-dir <directory>`; `--work-dir <new-directory>`; `--analysis-only` to stop after analysis checks. |
| `check_coverage.py` | `--bin-dir <directory>`. |
| `check_tasks.py` | `--bin-dir <directory>`. |
| `check_mixed.py` | `--bin-dir <directory>`. |

The default binary directory is `target/debug`. The coverage tool must be available in `PATH` at the version specified in the prerequisites. `--work-dir` must name a new directory.

Expand All @@ -45,6 +47,7 @@ The default binary directory is `target/debug`. The coverage tool must be availa
| 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. |
| Mixed targets | Shared definitions prefer lib; same-name distinct definitions and bin-only functions remain separate. Checks cover dependency context, custom target names/entries, generic/trait methods, multiple binaries, generation, repair with warnings, cached results, automatic integration selection, and legacy mixed-cache rejection. |
| 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. |

The integration-directory cases retain the original `tests.bak/` while generating `tests/`. They use one cached candidate and make no additional model requests. Separate timeout cases also check integration execution without an original-test backup.
Expand Down
4 changes: 3 additions & 1 deletion docs/palm-rust-unit-test-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,9 @@ 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 计数,不生成候选覆盖记录,也不混入真实命中数据;同一轮统计按目标复用基线,编译或工具错误仍然报错。函数级集成测试需要库目标;同名库与二进制目标共享模块、无法唯一确定函数归属时,会在模型请求前拒绝执行,完整支持需要进一步完善分析索引中的目标身份。
候选覆盖率缺少被测函数映射时,统计会补采集零命中基线。单元测试使用 `--lib` 或 `--bin` 的测试编译产物,过滤全部测试,不再插入空测试;集成测试从普通库产物取得映射,因此能够保留 `#[cfg(not(test))]` 下的函数和分支。基线不增加测试或 oracle 计数,不生成候选覆盖记录,也不混入真实命中数据;同一轮统计按目标复用基线,编译或工具错误仍然报错。分析会分别保存 lib/bin 产物,再汇总被测函数:共享源码定义优先归 lib,独有定义保留原目标,同名但不同位置的定义不会合并。候选与统计保存目标身份,单元测试编译、修复和覆盖率始终使用相应 `--lib` 或 `--bin`。bin 的库依赖上下文单独保留,选择 lib 版本不代表已验证全部 bin 版本。

混合目标的函数索引包含目标限定,如 `lib:demo::demo::shared::parse`;`--functions-file` 也接受能唯一定位的原 Rust 路径。统计中的 `function_name` 是索引标识,`rust_name` 是原 Rust 路径,`target` 记录目标。函数级集成模式自动选择库函数并报告未参与的 bin 函数;显式指定 bin 函数会在模型请求前报错。Cargo 仍可能为集成测试构建 bin,其构建错误需要正常处理。旧单目标缓存继续兼容,旧混合目标产物需要在新工作副本中重新分析和生成。

### 8.7 LLM 修复流程

Expand Down
7 changes: 7 additions & 0 deletions examples/mixed-targets/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "mixed_fixture"
version = "0.1.0"
edition = "2021"
publish = false

[workspace]
11 changes: 11 additions & 0 deletions examples/mixed-targets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Mixed-target fixture

This standalone package is used by `scripts/check_mixed.py`. The library and binary both include `shared.rs`, while each defines its own `helper`. Their different results expose incorrect target selection. The binary also calls the library and uses a library type.

From the PALM repository root, after building the tools and installing the pinned coverage tool:

```sh
python3 scripts/check_mixed.py
```

The script works in temporary copies with fixed local model responses. It checks analysis ownership, dependency context, unit and integration statistics, compilation repair, and source restoration. Additional temporary variants cover custom Cargo targets and definitions shared between binaries. The fixture's `main` panics so accidental execution is visible; the generated unit-test pipeline must use the test harness.
3 changes: 3 additions & 0 deletions examples/mixed-targets/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly-2025-03-19"
components = ["rust-src", "rustc-dev", "llvm-tools-preview", "rust-analyzer"]
13 changes: 13 additions & 0 deletions examples/mixed-targets/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
pub mod shared;

pub fn helper(value: i32) -> i32 {
value + 10
}

pub fn lib_only(value: i32) -> i32 {
shared::classify(value)
}

pub struct Subject {
pub value: i32,
}
17 changes: 17 additions & 0 deletions examples/mixed-targets/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
mod shared;

fn helper(value: i32) -> i32 {
value - 10
}

fn bin_only(value: i32) -> i32 {
shared::classify(value) + mixed_fixture::helper(value)
}

fn library_type(subject: mixed_fixture::Subject) -> i32 {
subject.value
}

fn main() {
panic!("the unit-test pipeline must not execute main");
}
7 changes: 7 additions & 0 deletions examples/mixed-targets/src/shared.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
pub fn classify(value: i32) -> i32 {
if value > 0 {
crate::helper(value)
} else {
0
}
}
1 change: 1 addition & 0 deletions focxt/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repository.workspace = true
# members = ["call_chain"]

[dependencies]
cargo_metadata = "0.18"
call_chain = { path = "call_chain" }
clap = { version = "4.5", features = ["derive"] }
prettyplease = "0.2"
Expand Down
4 changes: 3 additions & 1 deletion focxt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,16 @@ focxt invokes `cargo call-chain` at runtime, so install [call_chain](call_chain/

Use a working copy of a standalone crate with the same pinned toolchain. For the test-generation workflow, [preprocess and analyze with utgen](../utgen/README.md#analysis): it runs both brinfo and focxt and validates their outputs.

To construct context directly from prepared source:
For a single-target package, construct context directly from prepared source:

```sh
focxt --crate /absolute/path/to/target-crate
```

`-c` is the short form of `--crate`. focxt runs `cargo clean` and `cargo call-chain` inside the target before building context; running call-chain separately is unnecessary for this command. Start with a fresh working copy when repeating analysis, since build-cache cleanup does not remove previous context artifacts.

Mixed packages use `utgen analyze`, which selects each Cargo entry and preserves separate context data. Binary contexts keep local definitions separate from the package library; referenced library functions and types are included in a labeled dependency section. Library preference applies to the final focal list, without discarding binary dependency information.

## Output

Paths are relative to the target crate:
Expand Down
2 changes: 1 addition & 1 deletion focxt/call_chain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ cargo clean
cargo call-chain
```

The wrapper analyzes `lib` and `bin` targets through `cargo check`. Clearing the build cache ensures analysis executes after an earlier check; it does not remove previous `focxt/` output.
The wrapper analyzes a single ordinary `lib` or `bin` target through `cargo check`. Use `utgen analyze` for mixed packages so each target keeps separate artifacts. Calls and types from the package library retain their dependency origin when analyzing a binary. Clearing the build cache ensures analysis executes after an earlier check; it does not remove previous `focxt/` output.

## Output

Expand Down
29 changes: 27 additions & 2 deletions focxt/call_chain/src/analysis/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ impl CallChainCallbacks {
// println!("{}", mod_info.name);
impl_informations.push(impl_information.clone());
let mut calls: HashSet<String> = HashSet::new();
let mut library_calls = HashSet::new();
let mut library_types = HashSet::new();
let library = std::env::var("PALM_LIBRARY_NAME").ok();
let from_library = |id: rustc_hir::def_id::DefId| !id.is_local()
&& library.as_deref() == Some(tcx.crate_name(id.krate).as_str());
let mut tys: HashSet<Ty<'tcx>> = HashSet::new();
let mut types: HashSet<String> = HashSet::new();
for basic_block in basic_blocks.iter() {
Expand All @@ -129,7 +134,17 @@ impl CallChainCallbacks {
let kind_strings: Vec<&str> = kind_string.splitn(3, ' ').into_iter().collect();
let kind_string = kind_strings[2];
let call_string = &kind_string[..kind_string.find("(").unwrap()];
calls.insert(call_string.to_string());
let dependency = match func {
Operand::Constant(constant) => match constant.ty().kind() {
TyKind::FnDef(id, _) if from_library(*id) => {
library_calls.insert(super::hirvisitor::function_name(tcx, *id));
true
}
_ => false,
},
_ => false,
};
if !dependency { calls.insert(call_string.to_string()); }

for arg in args.iter() {
if let Operand::Constant(constant) = &arg.node {
Expand All @@ -152,6 +167,12 @@ impl CallChainCallbacks {
collect_subtypes(local_decl.ty, tcx, &mut tys);
}
for ty in tys.iter() {
if let TyKind::Adt(adt, _) = ty.kind() {
if from_library(adt.did()) {
library_types.insert(tcx.def_path_str(adt.did()));
continue;
}
}
types.insert(ty.to_string());
}
// println!("Types:");
Expand All @@ -178,7 +199,11 @@ impl CallChainCallbacks {
// for new_call in new_calls.iter() {
// calls.insert(new_call.clone());
// }
let calls_and_types = CallsAndTypes::new(&mod_info.name, &calls, &types);
let mut calls_and_types = CallsAndTypes::new(&mod_info.name, &calls, &types);
calls_and_types.library_calls = library_calls.into_iter().collect();
calls_and_types.library_types = library_types.into_iter().collect();
calls_and_types.library_calls.sort();
calls_and_types.library_types.sort();
let directory_path = self.crate_path.join("focxt/callsandtypes");
create_dir_all(&directory_path).unwrap();
let file_path = PathBuf::from(&directory_path)
Expand Down
18 changes: 18 additions & 0 deletions focxt/call_chain/src/analysis/exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,10 @@ impl Cond {

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallsAndTypes {
#[serde(default)]
pub library_calls: Vec<String>,
#[serde(default)]
pub library_types: Vec<String>,
pub mod_name: String,
pub calls: Vec<String>,
pub types: Vec<String>,
Expand All @@ -187,6 +191,8 @@ pub struct CallsAndTypes {
impl CallsAndTypes {
pub fn new_empty() -> Self {
CallsAndTypes {
library_calls: Vec::new(),
library_types: Vec::new(),
mod_name: String::new(),
calls: Vec::new(),
types: Vec::new(),
Expand All @@ -203,13 +209,25 @@ impl CallsAndTypes {
types_vec.push(a_type.clone());
}
CallsAndTypes {
library_calls: Vec::new(),
library_types: Vec::new(),
mod_name: mod_name.clone(),
calls: calls_vec,
types: types_vec,
}
}

pub fn add_data(&mut self, data: &CallsAndTypes) {
for name in &data.library_calls {
if !self.library_calls.contains(name) {
self.library_calls.push(name.clone());
}
}
for name in &data.library_types {
if !self.library_types.contains(name) {
self.library_types.push(name.clone());
}
}
for other_call in data.calls.iter() {
if !self.calls.contains(other_call) {
self.calls.push(other_call.clone());
Expand Down
Loading
Loading