"""Behavioral acceptance; Linux process tests also run on the target host.""" import json import os from pathlib import Path import signal import subprocess import sys import tempfile import time import unittest ROOT = Path(__file__).resolve().parents[1] RUNNER = ROOT / "scripts" / "run_smoke.py" class RunnerTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.results = Path(self.temp.name) def command(self, mode="success", run_id="test-1", timeout="3"): self.assertTrue(RUNNER.is_file(), "run_smoke.py is not implemented") return [sys.executable, "-u", str(RUNNER), "--mode", mode, "--run-id", run_id, "--timeout", timeout, "--results-root", str(self.results)] def run_task(self, **kwargs): return subprocess.run(self.command(**kwargs), capture_output=True, text=True, timeout=12) def summary(self, run_id="test-1"): return json.loads((self.results / run_id / "summary.json").read_text()) def test_success_streams_progress_and_records_synthetic_result(self): result = self.run_task() self.assertEqual(result.returncode, 0, result.stdout + result.stderr) summary = self.summary() self.assertEqual(summary["status"], "succeeded") self.assertEqual(summary["run_id"], "test-1") self.assertTrue(summary["synthetic"]) self.assertEqual(summary["result"]["checks_passed"], 3) self.assertIn("progress=1/3", result.stdout) self.assertIn("progress=3/3", result.stdout) self.assertEqual(len(summary["source_sha256"]["smoke_task.py"]), 64) def test_nonzero_exit_is_recorded_as_failure(self): result = self.run_task(mode="failure") self.assertNotEqual(result.returncode, 0) self.assertEqual(self.summary()["status"], "failed") self.assertEqual(self.summary()["exit_code"], 23) def test_zero_exit_with_invalid_report_is_failure(self): result = self.run_task(mode="invalid-report") self.assertNotEqual(result.returncode, 0) self.assertEqual(self.summary()["status"], "invalid_result") def test_reused_run_id_preserves_previous_result(self): self.assertEqual(self.run_task().returncode, 0) before = (self.results / "test-1" / "summary.json").read_bytes() self.assertNotEqual(self.run_task(mode="failure").returncode, 0) self.assertEqual((self.results / "test-1" / "summary.json").read_bytes(), before) def test_invalid_inputs_do_not_create_run_directory(self): for kwargs in ({"run_id": "../escape"}, {"mode": "arbitrary"}, {"timeout": "0"}, {"timeout": "31"}, {"timeout": "nan"}): with self.subTest(kwargs=kwargs): self.assertNotEqual(self.run_task(**kwargs).returncode, 0) self.assertEqual(list(self.results.iterdir()), []) @unittest.skipUnless(sys.platform == "linux", "requires Linux process groups") def test_timeout_terminates_child_and_records_timeout(self): result = self.run_task(mode="wait", timeout="2") self.assertEqual(result.returncode, 124, result.stdout + result.stderr) summary = self.summary() self.assertEqual(summary["status"], "timed_out") self.assertFalse(Path(f'/proc/{summary["child_pid"]}').exists()) @unittest.skipUnless(sys.platform == "linux", "requires Linux process groups") def test_cancel_terminates_parent_and_child(self): proc = subprocess.Popen(self.command(mode="wait", timeout="15"), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) try: deadline = time.monotonic() + 4 child_file = self.results / "test-1" / "child.pid" while not child_file.exists() and time.monotonic() < deadline: time.sleep(.05) self.assertTrue(child_file.exists(), "worker did not start") child_pid = int(child_file.read_text()) os.kill(proc.pid, signal.SIGTERM) out, err = proc.communicate(timeout=4) self.assertEqual(proc.returncode, 130, out + err) self.assertEqual(self.summary()["status"], "cancelled") self.assertFalse(Path(f"/proc/{child_pid}").exists()) finally: if proc.poll() is None: proc.kill() proc.communicate() if __name__ == "__main__": unittest.main()