103 lines
4.4 KiB
Python
103 lines
4.4 KiB
Python
|
|
"""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()
|