| 1 | import os |
| 2 | from pathlib import Path |
| 3 | import shutil |
| 4 | import subprocess |
| 5 | import tempfile |
| 6 | import unittest |
| 7 | from unittest.mock import patch |
| 8 | |
| 9 | import cargo_fresh |
| 10 | import ci |
| 11 | import release_web |
| 12 | |
| 13 | |
| 14 | @unittest.skipUnless(shutil.which('cargo'), 'needs Cargo') |
| 15 | class CargoFreshTest(unittest.TestCase): |
| 16 | def test_old_checkout_inputs_are_rebuilt_without_cleaning_dependencies(self): |
| 17 | with tempfile.TemporaryDirectory(prefix='snowbound-cargo-fresh-') as folder: |
| 18 | folder = Path(folder) |
| 19 | for name, value in [('a', 1), ('b', 2)]: |
| 20 | root = folder / name |
| 21 | (root / 'app/src').mkdir(parents=True) |
| 22 | (root / 'owned/src').mkdir(parents=True) |
| 23 | (root / 'vendor/src').mkdir(parents=True) |
| 24 | (root / 'vendor/Cargo.toml').write_text( |
| 25 | '[package]\nname="shared_dependency"\nversion="0.1.0"\nedition="2024"\n') |
| 26 | (root / 'vendor/src/lib.rs').write_text(f'pub fn value() -> u8 {{ {value * 10} }}\n') |
| 27 | (root / 'Cargo.toml').write_text( |
| 28 | '[workspace]\nmembers=["app", "owned"]\nexclude=["vendor"]\nresolver="2"\n') |
| 29 | (root / 'app/Cargo.toml').write_text( |
| 30 | '[package]\nname="freshness_fixture"\nversion="0.1.0"\nedition="2024"\n' |
| 31 | '[dependencies]\nowned={path="../owned"}\nshared_dependency={path="../vendor"}\nitoa="=1.0.18"\n') |
| 32 | (root / 'owned/Cargo.toml').write_text( |
| 33 | '[package]\nname="owned"\nversion="0.1.0"\nedition="2024"\n') |
| 34 | (root / 'owned/src/lib.rs').write_text('include!(concat!(env!("OUT_DIR"), "/value.rs"));\n') |
| 35 | (root / 'owned/value.txt').write_text(str(value)) |
| 36 | (root / 'owned/build.rs').write_text( |
| 37 | 'fn main() {\n' |
| 38 | ' println!("cargo::rerun-if-changed=value.txt");\n' |
| 39 | ' let value = std::fs::read_to_string("value.txt").unwrap();\n' |
| 40 | ' let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").unwrap());\n' |
| 41 | ' std::fs::write(out.join("value.rs"), format!("pub fn value() -> u8 {{ {value} }}")).unwrap();\n' |
| 42 | '}\n') |
| 43 | (root / 'app/src/lib.rs').write_text( |
| 44 | f'#[test] fn checkout_{name}() {{ ' |
| 45 | f'assert_eq!(itoa::Buffer::new().format(owned::value() + shared_dependency::value()), "{value * 11}"); }}\n') |
| 46 | for path in root.rglob('*'): |
| 47 | if path.is_file(): |
| 48 | os.utime(path, (1, 1)) |
| 49 | environment = {key: value for key, value in os.environ.items() |
| 50 | if not key.startswith(('CARGO_', 'RUST', 'SNOWBOUND_', 'ONESTORE_'))} |
| 51 | environment.update(CARGO_TARGET_DIR=str(folder / 'cache'), CARGO_BUILD_JOBS='2', CARGO_NET_OFFLINE='true') |
| 52 | |
| 53 | def cargo(root, *args): |
| 54 | return subprocess.run(['cargo', *args], cwd=root, env=environment, check=True, |
| 55 | capture_output=True, text=True).stdout |
| 56 | |
| 57 | self.assertIn('test checkout_a ... ok', cargo(folder / 'a', 'test', '--offline')) |
| 58 | artifacts = {path: (path.stat().st_mtime_ns, path.read_bytes()) |
| 59 | for path in (folder / 'cache/debug/deps').glob('*itoa*')} |
| 60 | self.assertTrue(artifacts) |
| 61 | cargo(folder / 'b', 'generate-lockfile', '--offline') |
| 62 | host = subprocess.check_output(['rustc', '-vV'], env=environment, text=True) |
| 63 | host = next(line.removeprefix('host: ') for line in host.splitlines() if line.startswith('host: ')) |
| 64 | cargo(folder / 'a', 'build', '--offline', '--release') |
| 65 | cargo(folder / 'a', 'build', '--offline', '--release', '--target', host) |
| 66 | previous = {path: (path.stat().st_mtime_ns, path.read_bytes()) |
| 67 | for path in (folder / 'cache/release/deps').glob('*.rlib')} |
| 68 | self.assertTrue(previous) |
| 69 | cargo_fresh.clean_local(folder / 'b', '--release', '--target', host, environment=environment) |
| 70 | cargo(folder / 'b', 'build', '--offline', '--release', '--target', host) |
| 71 | for path, expected in previous.items(): |
| 72 | self.assertEqual((path.stat().st_mtime_ns, path.read_bytes()), expected) |
| 73 | cargo_fresh.clean_local(folder / 'b', environment=environment) |
| 74 | self.assertIn('test checkout_b ... ok', |
| 75 | cargo(folder / 'b', 'test', '--offline', '--', '--exact', 'checkout_b')) |
| 76 | for path, expected in artifacts.items(): |
| 77 | self.assertEqual((path.stat().st_mtime_ns, path.read_bytes()), expected) |
| 78 | |
| 79 | def test_clean_failure_stops_web_build_before_packaging(self): |
| 80 | with patch.object(release_web, 'environment', return_value={}), \ |
| 81 | patch.object(release_web, 'clean_local', side_effect=subprocess.CalledProcessError(1, 'cargo')), \ |
| 82 | patch.object(release_web, 'run') as run: |
| 83 | with self.assertRaises(subprocess.CalledProcessError): |
| 84 | release_web.build(Path('unused-output')) |
| 85 | run.assert_not_called() |
| 86 | |
| 87 | def test_empty_metadata_never_cleans_the_entire_cache(self): |
| 88 | with patch.object(cargo_fresh.subprocess, 'check_output', return_value='{"packages": []}'), \ |
| 89 | patch.object(cargo_fresh.subprocess, 'run') as run: |
| 90 | with self.assertRaisesRegex(ValueError, 'no local packages'): |
| 91 | cargo_fresh.clean_local(Path('unused-checkout')) |
| 92 | run.assert_not_called() |
| 93 | |
| 94 | def test_clean_failure_stops_ci_before_concurrent_lanes(self): |
| 95 | with tempfile.TemporaryDirectory(prefix='snowbound-ci-clean-') as folder, \ |
| 96 | patch.object(ci, 'RUNS', Path(folder)), \ |
| 97 | patch.object(ci, 'CI', Path(folder) / 'checkout'), \ |
| 98 | patch.object(ci, 'lanes', return_value=[{'name': 'test'}]), \ |
| 99 | patch.object(ci, 'checkout', return_value='commit'), \ |
| 100 | patch.object(ci, 'jj', return_value='fixture'), \ |
| 101 | patch.object(ci, 'local_packages', return_value=['fixture']), \ |
| 102 | patch.object(ci.sys, 'argv', ['ci.py']), \ |
| 103 | patch.object(ci.Run, 'execute', return_value=101) as execute, \ |
| 104 | patch.object(ci, 'ThreadPoolExecutor') as pool: |
| 105 | with self.assertRaisesRegex(SystemExit, 'Workspace cleanup failed'): |
| 106 | ci.main() |
| 107 | execute.assert_called_once() |
| 108 | self.assertEqual(execute.call_args.args[0], ['cargo', 'clean', '--locked', '-p', 'fixture']) |
| 109 | pool.assert_not_called() |
| 110 | |
| 111 | |
| 112 | if __name__ == '__main__': |
| 113 | unittest.main() |