import ast import hashlib import importlib.util import json from pathlib import Path from tempfile import TemporaryDirectory import unittest from forecast_acceptance import source_paths, verify_forecast_bundle ROOT = Path(__file__).resolve().parents[1] spec = importlib.util.spec_from_file_location('forecast_release_test', ROOT / 'release_preflight.py') preflight = importlib.util.module_from_spec(spec) spec.loader.exec_module(preflight) class ForecastAcceptanceTest(unittest.TestCase): def fixture(self, root): src, bundle = root / 'source', root / 'bundle' for path in (src, bundle): path.mkdir() for name, value in {'main.py': 'x=1\n', 'telemetry_quality.py': 'x=2\n', 'requirements.txt': 'pandas\n'}.items(): (path / name).write_text(value) manifest = {p.name: hashlib.sha256(p.read_bytes()).hexdigest() for p in source_paths(src)} (bundle / 'SOURCE_MANIFEST.json').write_text(json.dumps(manifest)) return src, bundle def test_exact_source_is_verified(self): with TemporaryDirectory() as d: src, bundle = self.fixture(Path(d)) self.assertEqual(len(verify_forecast_bundle(bundle, src)), 3) def test_modified_development_source_blocks_test(self): with TemporaryDirectory() as d: src, bundle = self.fixture(Path(d)) (src / 'main.py').write_text('x=3\n') with self.assertRaises(ValueError): verify_forecast_bundle(bundle, src) def test_extra_staged_python_file_blocks_test(self): with TemporaryDirectory() as d: src, bundle = self.fixture(Path(d)) (bundle / 'surprise.py').write_text('x=3\n') with self.assertRaises(ValueError): verify_forecast_bundle(bundle, src) def test_symlink_is_rejected(self): with TemporaryDirectory() as d: src, bundle = self.fixture(Path(d)) (bundle / 'main.py').unlink() (bundle / 'main.py').symlink_to(src / 'main.py') with self.assertRaises(ValueError): verify_forecast_bundle(bundle, src) def test_models_secrets_and_databases_not_staged(self): with TemporaryDirectory() as d: src, bundle = self.fixture(Path(d)) for name in ('.env', 'model.pkl', 'users.db'): (src / name).write_text('not a source file') self.assertEqual(len(verify_forecast_bundle(bundle, src)), 3) def test_forecast_checks_have_no_network_or_live_mounts(self): args = next(args for name,args,cwd,timeout in preflight.command_list('p') if name=='forecast_candidate_python311') self.assertEqual(args[args.index('--network')+1], 'none') self.assertEqual(args[args.index('--user')+1], '1000:1000') self.assertIn('--read-only', args) for forbidden in ('--volume', '-v', '--env-file', '--privileged', 'up', 'restart'): self.assertNotIn(forbidden, args) def test_runner_does_not_import_application_main(self): source = (ROOT / 'acceptance/check_forecast.py').read_text() tree = ast.parse(source) imports = {a.name for node in ast.walk(tree) if isinstance(node, ast.Import) for a in node.names} self.assertNotIn('main', imports) self.assertNotIn('run_forecast(', source) self.assertNotIn('get_configs(', source) if __name__ == '__main__': unittest.main()