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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ To measure existing tests before preprocessing, run `utgen coverage -p <original

On a fresh working copy of a standalone crate, run `utgen pre-process -p <target-crate-path>` before `utgen analyze -p <target-crate-path>`. Analysis clears Cargo's check cache, runs brinfo and focxt, and verifies their outputs. It rejects existing `brinfo/` or `focxt/` directories so previous analysis results cannot be mixed into the run.

Then use `utgen gen` and `utgen fix` for generation and compilation repair. Pass `--requirement --context` to `utgen gen` to include path constraints and focal context. Preprocessing and test checks modify the target tree; use a working copy of the target project.
Then use `utgen gen` and `utgen fix` for generation and compilation repair. Pass `--requirement --context` to `utgen gen` to include path constraints and focal context. For integration tests, pass `--integration` to both `gen` and `fix`. Preprocessing and test checks modify the target tree; use a working copy of the target project.

Start with the [minimal pipeline check](docs/minimal-pipeline.md), which uses fixed local model responses and verifies analysis, generation, repair, source restoration, and coverage. The [bytes analysis check](docs/bytes-analysis.md) validates analysis on the larger bundled target without a model service. For model-based generation and repair, follow the [small model trial](examples/README.md#prepare-a-small-model-trial) using an explicit function list and separate request limits, then inspect the request reports and test statistics. See [utgen usage](utgen/README.md) for command details and supported scope.

Expand Down
3 changes: 2 additions & 1 deletion docs/minimal-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ 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. |
| 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. |

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 All @@ -63,7 +64,7 @@ Explicit `coverage(on)` is respected. Expression fragments are preserved; the ch

`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.

Additional cases cover direct and input/prefix/oracle generation, integration imports, function selection, request budgets, and worker/consumer failures. Checks require source restoration, preservation of unrelated backups, retention of repair backups on failure, and skipped statistics after a candidate-stage error.
Additional cases cover direct and input/prefix/oracle generation, integration imports and repair concurrency, function selection, request budgets, and worker/consumer failures. Checks require source restoration, preservation of unrelated backups, retention of repair backups on failure, and skipped statistics after a candidate-stage error.

## Model Request Regression

Expand Down
10 changes: 6 additions & 4 deletions docs/palm-rust-unit-test-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ Rust 的一个核心挑战是 MIR 的去糖。比如高级语法 `match color`
utgen pre-process --project-dir <PROJECT_DIR> [--work-dir <WORK_DIR>...]
utgen analyze --project-dir <STANDALONE_CRATE_DIR>
utgen gen --project-dir <PROJECT_DIR> [--work-dir <WORK_DIR>...] [--tasks <N>] [--integration] [--requirement] [--context] [--oracle]
utgen fix --project-dir <PROJECT_DIR> [--work-dir <WORK_DIR>...] [--tasks <N>]
utgen fix --project-dir <PROJECT_DIR> [--work-dir <WORK_DIR>...] [--tasks <N>] [--integration]
```

`analyze` 现在执行 `cargo clean`、`cargo brinfo` 和 `focxt`,随后核对分析索引及上下文文件。当前支持用 `-p` 指定单个独立 crate;目录中已有 `brinfo/` 或 `focxt/` 时会拒绝执行,应使用新的工作副本,避免混入旧分析结果。
Expand Down Expand Up @@ -358,7 +358,7 @@ export PALM_CONFIG="$(pwd)/utgen/res/api.json"
- `gen_full_tests` 使用 `test_prompt.json` 一次性生成测试。
- `gen_tests_cot` 使用 `input -> prefix -> oracle` 三步流程。
- `check_unit` 会临时把测试插入被测源码文件,运行 `cargo build --tests`,记录每个候选是否可编译,然后恢复文件。
- `check_integration` 会写入 `tests/test_check.rs` 做编译检查。
- `check_integration` 会写入临时 `tests/palm_candidate.rs`,只编译该集成测试目标。

- `utgen/src/gene/cot/*.rs`
- `input_infer.rs`:生成输入范围。
Expand Down Expand Up @@ -620,7 +620,9 @@ utgen fix -p <target-crate-path>

修复阶段读取 `utgen/generation/pre_fix`,针对不可编译测试调用 LLM 生成 ChangeLog 并重新验证。修复结果写入 `utgen/generation/llm_fix`,覆盖率统计写入 `utgen/fixed_result`。

`fix` 当前将测试插入源码,以单元测试方式修复和统计,没有独立的 integration 修复选项。因此 `gen --integration` 后接 `fix` 并不表示全过程保留集成测试模式。运行时断言失败也不属于当前编译修复流程的目标。
`fix` 默认将测试插入源码,以单元测试方式修复和统计。集成测试需要在生成和修复时都传入 `--integration`:修复阶段把单个候选写入临时集成测试目标,只编译该目标;可以修改导入、模块级辅助代码和测试体,并单独保存各候选的导入与辅助代码。整体执行和逐候选覆盖率统计都保持集成测试模式,不再把候选插入源码。覆盖率阶段仍会临时添加排除测试代码的属性并恢复源文件。

候选缓存记录生成模式,模式不匹配时会报错;没有模式字段的旧缓存按单元测试处理,旧集成测试缓存应在新的工作副本中重新生成。两条命令还应传入相同的函数清单。运行时断言失败不属于编译修复流程的目标。

### 10.7 示例:`examples/bytes`

Expand Down Expand Up @@ -709,7 +711,7 @@ integration 模式先按 `brinfo` 的 `visible` 标志筛选函数,并且 `utg

- `utgen pre-process`:重命名所选 crate 的 `tests`,将测试源码区间替换为空白。
- `utgen gen`:可能追加 `ntest` 依赖,写入临时测试文件或插入测试做编译检查。
- `utgen fix`:临时插入测试并恢复,结束时删除备份。
- `utgen fix`:默认临时插入单元测试并恢复,成功时删除本轮备份;`--integration` 使用临时集成测试文件,恢复或删除本轮临时输入。

建议用 git 查看差异:

Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,10 @@ Use a fresh working copy and repeat preprocessing and analysis before choosing t

```sh
utgen gen -p <target-crate-path> --integration --requirement --context
utgen fix -p <target-crate-path> --integration
```

Integration mode generates files under the target's `tests/` directory after filtering functions using the analysis visibility flag and checking compilation. `utgen fix` currently uses source-inserted unit tests for repair and post-repair statistics; it does not provide a separate integration-mode repair pipeline.
Integration mode generates files under the target's `tests/` directory after filtering functions using the analysis visibility flag and checking compilation. Pass `--integration` to `fix` as well to repair and evaluate candidates in integration-test targets. Imports and helpers stay with each candidate, and both combined and per-candidate coverage retain integration scope. Keep the same function selection for both commands; mixing cached generation modes is rejected.

### Results

Expand Down
138 changes: 132 additions & 6 deletions scripts/check_minimal.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ def run(label, command, cwd=target, expected=0, timeout=240):
finally:
lib.write_bytes(saved_lib)
assert snapshot(original) == baseline
state = {'generation': 0, 'repair': 0, 'errors': []}
state = {'generation': 0, 'repair': 0, 'errors': [], 'integration': False, 'reject_repair': False}

class Model(http.server.BaseHTTPRequestHandler):
def log_message(self, *_):
Expand All @@ -226,19 +226,60 @@ def do_POST(self):
'double': 'assert_eq!(double("bad"), 4);',
}[function]
answer = 'fn generated_helper() -> i32 { 1 }\n#[test]\nfn generated() {\n assert_eq!(generated_helper(), 1);\n ' + body + '\n}\n'
if state['integration']:
answer = {
'classify': '''use std::cmp::max as target;
use std::fmt::Write;
fn helper() -> i32 { 1 }
#[test]
fn alias_case() {
let mut buffer = Vec::new();
buffer.write_all(b"x").unwrap();
assert_eq!(target(2), helper());
}
#[test]
fn sibling_case() {
assert_eq!(target(2, 1), 2);
assert_eq!(palm_fixture::classify(-1), 0);
}
''',
'double': '''use palm_fixture::nested::double as twice;
fn helper() -> i32 { "bad-helper" }
#[test]
fn body_case() {
assert_eq!(twice("bad-body") + helper(), 5);
}
''',
}[function]
state['generation'] += 1
else:
file = re.search(r'You can only modify lines \d+ to \d+ in file (.+?)\. For your answer', user).group(1)
match = re.search(r'^\[(\d+)\](.*assert_eq!\(double\("bad"\), 4\);.*)$', user, re.M)
assert match, 'repair prompt lacks numbered failing statement'
number, old = match.groups()
new = old.replace('double("bad")', 'double(2)')
answer = f'ChangeLog:1@{file}\nFixDescription: Use an integer argument.\nOriginalCode@{number}-{number}:\n[{number}]{old}\nFixedCode@{number}-{number}:\n[{number}]{new}\n'
replacements = {
'double("bad")': 'double(2)',
'use std::cmp::max as target;': 'use palm_fixture::classify as target;',
'use std::fmt::Write;': 'use std::io::Write;',
'"bad-helper"': '1',
'"bad-body"': '2',
}
changes = {}
for number, old in re.findall(r'^\[(\d+)\](.*)$', user, re.M):
new = old
for before, after in replacements.items():
new = new.replace(before, after)
if new != old:
changes[number] = (old, new)
assert changes, 'repair prompt lacks numbered failing code'
answer = f'ChangeLog:1@{file}\nFixDescription: Fix candidate imports and arguments.\n'
for number, (old, new) in changes.items():
answer += f'OriginalCode@{number}-{number}:\n[{number}]{old}\nFixedCode@{number}-{number}:\n[{number}]{new}\n'
state['repair'] += 1
response = {'id': 'fixture', 'object': 'chat.completion', 'created': 0, 'model': 'fixture',
'choices': [{'index': 0, 'message': {'role': 'assistant', 'content': answer}, 'finish_reason': 'stop'}],
'usage': {'prompt_tokens': 1, 'completion_tokens': 1, 'total_tokens': 2}}
code = 200
if state['reject_repair']:
response = {'error': {'message': 'fixture request rejected'}}
code = 401
except Exception as error:
state['errors'].append(str(error))
response = {'error': {'message': str(error), 'type': 'fixture_error'}}
Expand Down Expand Up @@ -307,6 +348,8 @@ def do_POST(self):
timeout_prepared = snapshot(target)
requests_before = (state['generation'], state['repair'])
for integration in [False, True]:
data['integration'] = integration
candidate.write_text(json.dumps(data, indent=2) + '\n')
run(f'candidate-timeouts-{integration}',
['utgen', 'gen', '-p', str(target), '--functions-file', str(selection),
*(['--integration'] if integration else [])], timeout=90)
Expand Down Expand Up @@ -376,6 +419,89 @@ def integration_snapshot():
assert report['attempts'] == 0, report
shutil.rmtree(target / 'tests')
assert snapshot(target) == prepared

# Exercise the complete integration path with real Rust diagnostics.
# One answer has two candidates needing different meanings of `target`.
state['integration'] = True
selected_names = [selected, 'palm_fixture::nested::double']
integration_selection = work / 'integration-functions.txt'
integration_selection.write_text('\n'.join(selected_names) + '\n')
for directory in ['generation/pre_fix', 'generation/llm_fix']:
for name in selected_names:
(target / 'utgen' / directory / (names[name] + '.json')).unlink()
integration_args = ['-p', str(target), '--functions-file', str(integration_selection),
'--integration', '--tasks', '2', '--max-requests', '8']
run('integration-generate-imports', ['utgen', 'gen', *integration_args])
for name, compiled in zip(selected_names, [1, 0]):
result = json.loads((target / f'utgen/result/{names[name]}.json').read_text())
assert result['tests_compiled'] == result['tests_run'] == compiled, result
source_before = {p: content for p, content in snapshot(target).items() if not p.startswith('tests/')}
assert source_before == prepared
# Unselected targets must neither pollute diagnostics nor be executed.
unrelated = target / 'tests/unrelated.rs'
unrelated.write_text('compile_error!("unselected test must not compile");\n')
temporary_file = target / 'tests/palm_candidate.rs'
temporary_file.write_text('compile_error!("pre-existing temporary path");\n')
before_fix = snapshot(target)
requests_before = (state['generation'], state['repair'])
wrong_mode = run('integration-reject-unit-fix',
['utgen', 'fix', '-p', str(target), '--functions-file', str(integration_selection)], expected=1)
assert 'Test mode mismatch' in wrong_mode.stderr
assert snapshot(target) == before_fix
assert (state['generation'], state['repair']) == requests_before
wrong_mode = run('integration-reject-unit-gen',
['utgen', 'gen', '-p', str(target), '--functions-file', str(integration_selection)], expected=1)
assert 'Test mode mismatch' in wrong_mode.stderr
assert snapshot(target) == before_fix
assert (state['generation'], state['repair']) == requests_before
run('integration-repair-imports', ['utgen', 'fix', *integration_args])
assert state['repair'] > requests_before[1]
for name, count in zip(selected_names, [2, 1]):
result = json.loads((target / f'utgen/fixed_result/{names[name]}.json').read_text())
assert result['tests'] == result['tests_compiled'] == result['tests_run'] == result['tests_passed'] == count, result
assert len(result['codes_lines_covered']) == count, result
assert result['lines_covered'] > 0, result
fixed_path = target / f'utgen/generation/llm_fix/{names[selected]}.json'
fixed = json.loads(fixed_path.read_text())
tests = fixed['fn_tests'][0]['answers'][0]['chain_tests']
assert 'use palm_fixture::classify as target;' in tests[0]['integration_contexts'][0]['uses']
assert 'use std::io::Write;' in tests[0]['integration_contexts'][0]['uses']
assert 'use std::cmp::max as target;' in tests[1]['integration_contexts'][0]['uses']
assert {p: content for p, content in snapshot(target).items() if not p.startswith('tests/')} == prepared
assert unrelated.read_bytes() == before_fix['tests/unrelated.rs']
assert temporary_file.read_bytes() == before_fix['tests/palm_candidate.rs']
assert not list((target / 'src').rglob('*.bak'))
stable = snapshot(target)
requests_before = (state['generation'], state['repair'])
run('integration-repair-cached', ['utgen', 'fix', *integration_args])
assert snapshot(target) == stable
assert (state['generation'], state['repair']) == requests_before
report = json.loads((target / 'utgen/generation/fix-requests.json').read_text())
assert report['attempts'] == 0 and report['invocation']['integration'], report
source_file = target / 'src/lib.rs'
original_source = source_file.read_bytes()
source_file.write_bytes(original_source + b'\ncompile_error!("production compilation failure");\n')
broken = snapshot(target)
failure = run('integration-repair-build-error', ['utgen', 'fix', *integration_args], expected=1)
assert 'production compilation failure' in failure.stderr
assert snapshot(target) == broken
assert (state['generation'], state['repair']) == requests_before
assert not list((target / 'src').rglob('*.bak'))
source_file.write_bytes(original_source)
# Request failure must restore the temporary input without touching src/.
saved_fixed = {}
for name in selected_names:
path = target / f'utgen/generation/llm_fix/{names[name]}.json'
saved_fixed[path] = path.read_bytes()
path.unlink()
state['reject_repair'] = True
run('integration-repair-request-error', ['utgen', 'fix', *integration_args], expected=1)
state['reject_repair'] = False
assert snapshot(target) == stable
assert not list((target / 'src').rglob('*.bak'))
for path, content in saved_fixed.items():
path.write_bytes(content)
assert not state['errors'], state
(work / 'summary.json').write_text(json.dumps(state, indent=2) + '\n')
print('Minimal analysis/generation/repair/coverage checks passed.', flush=True)
finally:
Expand Down
Loading
Loading