You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
8.8 KiB
187 lines
8.8 KiB
#!/usr/bin/env python3 |
|
"""Регрессии запускателя: лимит jobs, CPU-барьеры, ошибки и освобождение дочерних процессов.""" |
|
|
|
import json |
|
import os |
|
from pathlib import Path |
|
import signal |
|
import subprocess |
|
import sys |
|
import tempfile |
|
import time |
|
import unittest |
|
|
|
from run_tests import available_cpus |
|
|
|
|
|
@unittest.skipUnless(os.name == "posix", "fixtures use POSIX executables and fork") |
|
class RunnerTests(unittest.TestCase): |
|
def setUp(self): |
|
self.directory = tempfile.TemporaryDirectory(prefix="utun_runner_") |
|
self.addCleanup(self.directory.cleanup) |
|
self.root = Path(self.directory.name) |
|
self.runner = Path(__file__).with_name("run_tests.py").resolve() |
|
|
|
def fixture(self, name, body=""): |
|
path = self.root / name |
|
path.write_text(f"#!{sys.executable}\n" + """ |
|
import json, os, signal, sys, time |
|
from pathlib import Path |
|
name = Path(__file__).name |
|
def event(state): |
|
fd = os.open('events', os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o600) |
|
os.write(fd, (json.dumps([name, state]) + '\\n').encode()); os.close(fd) |
|
event('start') |
|
""" + body + "\nevent('end')\nprint('fixture output: ' + name)\n") |
|
path.chmod(0o755) |
|
|
|
def command(self, tests, options=()): |
|
return [sys.executable, str(self.runner), "--log-dir", "logs", *options, *tests] |
|
|
|
def run_runner(self, tests, options=(), environment=None): |
|
env = dict(os.environ, TEST_JOBS="2") if environment is None else environment |
|
return subprocess.run(self.command(tests, options), cwd=self.root, env=env, |
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, timeout=15) |
|
|
|
def test_default_cpu_count_and_override(self): |
|
self.fixture("fast") |
|
env = os.environ.copy(); env.pop("TEST_JOBS", None) |
|
result = self.run_runner(["fast"], environment=env) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
self.assertIn(f"jobs={available_cpus()} available_cpus=", result.stdout) |
|
if hasattr(os, "sched_setaffinity"): |
|
affinity = {min(os.sched_getaffinity(0))} |
|
result = subprocess.run(self.command(["fast"]), cwd=self.root, env=env, text=True, |
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=10, |
|
preexec_fn=lambda: os.sched_setaffinity(0, affinity)) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
self.assertIn("jobs=1 available_cpus=1", result.stdout) |
|
result = self.run_runner(["fast"], environment=dict(env, TEST_JOBS="1")) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
self.assertIn("jobs=1 available_cpus=", result.stdout) |
|
result = self.run_runner(["fast"], environment=dict(env, TEST_JOBS="0")) |
|
self.assertEqual(result.returncode, 2, result.stdout) |
|
self.assertIn("expected a positive integer", result.stdout) |
|
|
|
def test_parallel_limit_and_measurements_after_pool(self): |
|
for name, pair in (("first_a", "first"), ("first_b", "first"), ("last_a", "last"), ("last_b", "last")): |
|
self.fixture(name, f""" |
|
Path(name + '.ready').touch() |
|
deadline = time.monotonic() + 5 |
|
while len(list(Path('.').glob('{pair}_*.ready'))) < 2: |
|
if time.monotonic() > deadline: sys.exit(9) |
|
time.sleep(0.01) |
|
time.sleep(0.05) |
|
""") |
|
self.fixture("test_media_async", "time.sleep(0.05)\n") |
|
result = self.run_runner(["first_a", "first_b", "test_media_async", "last_a", "last_b"]) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
active, maximum = set(), 0 |
|
events = [json.loads(line) for line in (self.root / "events").read_text().splitlines()] |
|
for name, state in events: |
|
if state == "start": |
|
if name == "test_media_async": self.assertFalse(active) |
|
self.assertNotIn("test_media_async", active) |
|
active.add(name); maximum = max(maximum, len(active)) |
|
else: |
|
active.remove(name) |
|
self.assertFalse(active) |
|
self.assertEqual(maximum, 2) |
|
self.assertEqual({name for name, _ in events[:2]}, {"first_a", "first_b"}) |
|
self.assertEqual(events[-2:], [["test_media_async", "start"], ["test_media_async", "end"]]) |
|
self.assertIn("test_media_async: running alone to check CPU/wall-time limits", result.stdout) |
|
|
|
def test_long_network_tests_share_pool(self): |
|
names = ["test_bbr_integration", "test_auto_socket_dynamic"] |
|
for name in names: |
|
self.fixture(name, """ |
|
Path(name + '.ready').touch() |
|
deadline = time.monotonic() + 5 |
|
while len(list(Path('.').glob('*.ready'))) < 2: |
|
if time.monotonic() > deadline: sys.exit(9) |
|
time.sleep(0.01) |
|
time.sleep(0.05) |
|
""") |
|
# У фиктивного autosocket нет требований root: не вызываем sudo в этой регрессии. |
|
result = self.run_runner(names, environment=dict(os.environ, TEST_JOBS="2", PATH=str(self.root))) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
events = [json.loads(line) for line in (self.root / "events").read_text().splitlines()] |
|
self.assertEqual({name for name, _ in events[:2]}, set(names)) |
|
self.assertEqual([state for _, state in events], ["start", "start", "end", "end"]) |
|
|
|
def test_exit_codes_and_separate_logs(self): |
|
self.fixture("ok") |
|
self.fixture("bad", "print('failure detail', flush=True)\nsys.exit(42)\n") |
|
self.fixture("skip", "sys.exit(77)\n") |
|
result = self.run_runner(["ok", "bad", "skip", "missing"]) |
|
self.assertEqual(result.returncode, 1, result.stdout) |
|
self.assertIn("1 passed, 1 failed, 2 skipped (4 total)", result.stdout) |
|
self.assertIn("Failed tests: bad", result.stdout) |
|
self.assertIn("failure detail", (self.root / "logs/bad.log").read_text()) |
|
self.assertNotIn("failure detail", (self.root / "logs/ok.log").read_text()) |
|
|
|
def test_serial_override(self): |
|
self.fixture("serial_a", "time.sleep(0.05)\n") |
|
self.fixture("serial_b", "time.sleep(0.05)\n") |
|
result = self.run_runner(["serial_a", "serial_b"], environment=dict(os.environ, TEST_JOBS="1")) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
events = [json.loads(line) for line in (self.root / "events").read_text().splitlines()] |
|
self.assertEqual(events, [["serial_a", "start"], ["serial_a", "end"], ["serial_b", "start"], ["serial_b", "end"]]) |
|
|
|
def stubborn_fixture(self): |
|
self.fixture("stubborn", """ |
|
signal.signal(signal.SIGTERM, signal.SIG_IGN) |
|
pid = os.fork() |
|
Path('child.pid' if pid == 0 else 'parent.pid').write_text(str(os.getpid())) |
|
while True: time.sleep(1) |
|
""") |
|
|
|
def assert_descendant_stopped(self): |
|
pid = int((self.root / "child.pid").read_text()) |
|
deadline = time.monotonic() + 2 |
|
while time.monotonic() < deadline: |
|
try: |
|
os.kill(pid, 0) |
|
stat = Path(f"/proc/{pid}/stat") |
|
if stat.exists() and stat.read_text().split(")", 1)[1].split()[0] == "Z": return |
|
except ProcessLookupError: |
|
return |
|
time.sleep(0.01) |
|
self.fail(f"descendant pid={pid} survived runner cleanup") |
|
|
|
def test_timeout_stops_descendant(self): |
|
self.stubborn_fixture() |
|
result = self.run_runner(["stubborn"], ["--timeout", "1"]) |
|
self.assertEqual(result.returncode, 1, result.stdout) |
|
self.assertIn("timeout=1s", result.stdout) |
|
self.assertIn("0 passed, 1 failed, 0 skipped", result.stdout) |
|
self.assert_descendant_stopped() |
|
|
|
def test_interruption_and_log_directory_lock(self): |
|
self.stubborn_fixture() |
|
process = subprocess.Popen(self.command(["stubborn"]), cwd=self.root, |
|
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) |
|
try: |
|
deadline = time.monotonic() + 5 |
|
while not (self.root / "child.pid").exists() and time.monotonic() < deadline: |
|
time.sleep(0.01) |
|
self.assertTrue((self.root / "child.pid").exists()) |
|
result = self.run_runner(["stubborn"]) |
|
self.assertEqual(result.returncode, 1, result.stdout) |
|
self.assertIn("concurrent runs", result.stdout) |
|
self.assertIsNone(process.poll()) |
|
process.send_signal(signal.SIGTERM) |
|
output, _ = process.communicate(timeout=5) |
|
self.assertEqual(process.returncode, 128 + signal.SIGTERM, output) |
|
self.assertIn("test processes stopped", output) |
|
self.assert_descendant_stopped() |
|
self.fixture("ok") |
|
result = self.run_runner(["ok"]) |
|
self.assertEqual(result.returncode, 0, result.stdout) |
|
finally: |
|
if process.poll() is None: |
|
process.send_signal(signal.SIGTERM); process.communicate(timeout=5) |
|
|
|
|
|
if __name__ == "__main__": |
|
unittest.main()
|
|
|