81 lines
3.4 KiB
Python
81 lines
3.4 KiB
Python
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()
|