From 471bfa94e24b208cae1f13ff641c12f57fcdd8a3 Mon Sep 17 00:00:00 2001 From: cppbear Date: Fri, 9 Oct 2026 00:03:35 +0800 Subject: [PATCH] fix: restore integration test directories around original coverage --- scripts/check_minimal.py | 52 +++++++++++++++++++++++++++++++++ utgen/src/run/run.rs | 62 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 110 insertions(+), 4 deletions(-) diff --git a/scripts/check_minimal.py b/scripts/check_minimal.py index 7062f339ee..59273e5672 100644 --- a/scripts/check_minimal.py +++ b/scripts/check_minimal.py @@ -324,6 +324,58 @@ def do_POST(self): assert report['attempts'] == 0, report shutil.move(original_tests, target / 'tests.bak') assert snapshot(target) == prepared + + # Original and generated integration tests must coexist, including on + # a cached rerun and after original-test compilation fails. + answer['chain_tests'] = [dict( + attrs=[], prefix=[], oracles=[], codes=[['{', 'assert_eq!(classify(2), 1);', '}']], + can_compile=[{'Ok': None}], repaired=[False])] + candidate.write_text(json.dumps(data, indent=2) + '\n') + integration_command = ['utgen', 'gen', '-p', str(target), '--functions-file', str(selection), '--integration'] + + def integration_snapshot(): + # Cached generation rewrites imports from a HashSet in any order. + return {p: re.sub(rb'(?m)(?:^use [^\n]*\n)+', + lambda m: b''.join(sorted(m[0].splitlines(keepends=True))), content) + if p.startswith('tests/') else content + for p, content in snapshot(target).items()} + + for label in ['integration-original-tests', 'integration-original-tests-cached']: + run(label, integration_command) + result = json.loads((target / f'utgen/result/{names[selected]}.json').read_text()) + assert result['tests_run'] == result['tests_passed'] == 1, result + original_result = json.loads((target / 'utgen/original_result.json').read_text()) + assert [r['function_name'] for r in original_result] == [selected] + assert original_result[0]['lines_covered'], original_result + current = snapshot(target) + assert {p: content for p, content in current.items() if not p.startswith('tests/')} == prepared + assert any(p.startswith('tests/') for p in current) + assert not (target / 'tests.bak2').exists() + integration_prepared = integration_snapshot() + original_file = target / 'tests.bak/original.rs' + original_content = original_file.read_bytes() + original_file.write_bytes(original_content + b'\ncompile_error!("original-test failure fixture");\n') + failed_prepared = integration_snapshot() + failure = run('integration-original-tests-compile-error', integration_command, expected=1) + assert 'original-test failure fixture' in failure.stderr + assert integration_snapshot() == failed_prepared + assert not (target / 'tests.bak2').exists() + original_file.write_bytes(original_content) + assert integration_snapshot() == integration_prepared + # An existing, even empty, staging directory must not be overwritten. + staging = target / 'tests.bak2' + staging.mkdir() + failure = run('integration-existing-staging', integration_command, expected=1) + assert 'tests.bak2 already exists' in failure.stderr + assert staging.is_dir() and not list(staging.iterdir()) + assert integration_snapshot() == integration_prepared + staging.rmdir() + assert not list((target / 'src').rglob('*.bak')) + assert (state['generation'], state['repair']) == requests_before, state + report = json.loads((target / 'utgen/generation/gen-requests.json').read_text()) + assert report['attempts'] == 0, report + shutil.rmtree(target / 'tests') + assert snapshot(target) == prepared (work / 'summary.json').write_text(json.dumps(state, indent=2) + '\n') print('Minimal analysis/generation/repair/coverage checks passed.', flush=True) finally: diff --git a/utgen/src/run/run.rs b/utgen/src/run/run.rs index b942b8db70..c76eb9571b 100644 --- a/utgen/src/run/run.rs +++ b/utgen/src/run/run.rs @@ -25,6 +25,52 @@ pub enum TestType { Error, } +/// Return a temporarily moved test directory on normal completion or unwind. +struct MovedTestDirectory { + original: PathBuf, + temporary: PathBuf, + restored: bool, +} + +impl MovedTestDirectory { + fn new(original: &Path, temporary: &Path) -> std::io::Result { + if temporary.exists() { + return Err(std::io::Error::new( + std::io::ErrorKind::AlreadyExists, + format!( + "{} already exists; preserve it and use a fresh working copy", + temporary.display() + ), + )); + } + fs::rename(original, temporary)?; + Ok(Self { + original: original.to_owned(), + temporary: temporary.to_owned(), + restored: false, + }) + } + + fn finish(mut self) -> std::io::Result<()> { + fs::rename(&self.temporary, &self.original)?; + self.restored = true; + Ok(()) + } +} + +impl Drop for MovedTestDirectory { + fn drop(&mut self) { + if !self.restored { + if let Err(error) = fs::rename(&self.temporary, &self.original) { + error!( + "Cannot restore {}: {error}; directory retained at {}", + self.original.display(), self.temporary.display() + ); + } + } + } +} + pub fn run_test( project_dir: &Path, work_path: &Path, @@ -418,14 +464,14 @@ pub fn gen_test_rate( // let _ = target_clean(work_dir); let test_path = work_dir.join("tests"); let bak_test_path2 = work_dir.join("tests.bak2"); - fs::rename(&test_path, &bak_test_path2).unwrap(); + let generated_tests = MovedTestDirectory::new(&test_path, &bak_test_path2) + .expect("cannot temporarily move generated tests"); gen_codes_lines_and_branches_covered( project_dir, work_dir, &test_gen_infos, &mut test_rate_infos, ); - fs::rename(&bak_test_path2, &test_path).unwrap(); dump_result_for_test_rate_infos( &test_rate_infos, project_dir, @@ -437,6 +483,7 @@ pub fn gen_test_rate( if is_pre { gen_coverage_rate_for_original_tests(project_dir, work_dir, &test_gen_infos); } + generated_tests.finish().expect("cannot restore generated tests"); } } @@ -1410,7 +1457,13 @@ fn gen_coverage_rate_for_original_tests( let bak_test_dir = work_dir.join("tests.bak"); if exists(&bak_test_dir).unwrap() { info!("Generate coverage rate for original tests"); - fs::rename(&bak_test_dir, &test_dir).unwrap(); + // Import checks can leave an empty tests/ directory in unit mode. + if test_dir.exists() { + fs::remove_dir(&test_dir) + .expect("tests directory must be empty before restoring original tests"); + } + let original_tests = MovedTestDirectory::new(&bak_test_dir, &test_dir) + .expect("cannot restore original tests for coverage"); // let _ = target_clean(work_dir); let test_type = TestType::CoverageRate; run_test(project_dir, work_dir, test_type, true, false); @@ -1443,7 +1496,8 @@ fn gen_coverage_rate_for_original_tests( ); original_test_rate_infos.push(original_test_rate_info); } - fs::rename(&test_dir, &bak_test_dir).unwrap(); + original_tests.finish() + .expect("cannot return original tests to their backup"); dump_result_for_original_test_rate_infos(&original_test_rate_infos, project_dir, work_dir); let coverage_file_path = work_dir.join("coverage.xml"); let lcov_file_path = work_dir.join("coverage.json");