Files
behavior-tree/tests/test_native_soak.py
T

129 lines
6.9 KiB
Python

"""Real short-duration process tests; these never stand in for an eight-hour run."""
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import time
import unittest
ROOT = Path(__file__).resolve().parents[1]
TOOL = ROOT / 'tools/native_soak.py'
class VerdictTests(unittest.TestCase):
def test_eight_hour_liveness_is_not_full_dr_acceptance(self):
# Classification logic only: this test supplies a number, not duration evidence.
spec = importlib.util.spec_from_file_location('native_soak', TOOL)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
report = module.verdict(28800, True)
self.assertTrue(report['duration_liveness_pass'])
self.assertFalse(report['full_dr_acceptance'])
self.assertTrue(report['resource_growth_review_required'])
self.assertFalse(report['no_deadlock_claim'])
for seconds, progress, gap in [(28799, True, False), (28800, False, False), (28800, True, True)]:
self.assertFalse(module.verdict(seconds, progress, sampling_gap=gap)['eight_hour_pass'])
@unittest.skipUnless(sys.platform.startswith('linux'), 'Linux /proc required')
class NativeSoakTests(unittest.TestCase):
def run_monitor(self, directory, *args):
output = Path(directory) / 'evidence'
process = subprocess.run([sys.executable, str(TOOL), '--output', str(output),
'--duration-seconds', '0.45', '--sample-interval-seconds', '0.05',
*args], capture_output=True, text=True, timeout=8)
self.assertTrue((output / 'summary.json').exists(), process.stderr)
return process, json.loads((output / 'summary.json').read_text()), output
def test_late_increment_cannot_erase_elapsed_progress_deadline(self):
# Fault-injection unit test only; this clock is never duration evidence.
from unittest.mock import patch
spec = importlib.util.spec_from_file_location('native_soak_deadline', TOOL)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
with tempfile.TemporaryDirectory() as directory:
progress = Path(directory) / 'counter.json'
progress.write_text('{"completed":0}')
output = Path(directory) / 'evidence'
clock = [0.0]
def advance(seconds):
clock[0] += seconds
# The new completion is only visible after the .12-second deadline.
if clock[0] >= .16:
progress.write_text('{"completed":1}')
with patch.object(module.time, 'monotonic', side_effect=lambda: clock[0]), \
patch.object(module.time, 'sleep', side_effect=advance), patch('builtins.print'):
code = module.main(['--pid', str(os.getpid()), '--output', str(output),
'--duration-seconds', '.16', '--sample-interval-seconds', '.08',
'--max-progress-gap-seconds', '.12', '--progress-file', str(progress)])
report = json.loads((output / 'summary.json').read_text())
self.assertEqual(code, 1)
self.assertEqual(report['status'], 'failed')
self.assertIn('progress deadline', report['reason'])
def test_real_short_workload_is_incomplete_and_one_process(self):
with tempfile.TemporaryDirectory() as directory:
progress = Path(directory) / 'progress.json'
script = ('import json,os,time,sys; from pathlib import Path; p=Path(sys.argv[1]); n=0\n'
'while True:\n n+=1; q=p.with_suffix(".tmp"); q.write_text(json.dumps({"completed":n})); os.replace(q,p); time.sleep(.025)\n')
process, report, output = self.run_monitor(directory, '--progress-file', str(progress),
'--max-progress-gap-seconds', '0.25', '--command', sys.executable, '-u', '-c', script, str(progress))
self.assertEqual(process.returncode, 2, process.stderr)
self.assertEqual(report['status'], 'qualification_incomplete')
self.assertFalse(report['eight_hour_pass'])
self.assertGreaterEqual(report['elapsed_seconds'], .45)
self.assertTrue(report['progress_observed'])
samples = [json.loads(line) for line in (output / 'samples.jsonl').read_text().splitlines()]
self.assertGreater(len(samples), 3)
self.assertEqual(len({(s['pid'], s['start_ticks']) for s in samples}), 1)
self.assertGreater(samples[-1]['rss_bytes'], 0)
self.assertIn('cpu_seconds', samples[-1])
self.assertIn('fd_count', samples[-1])
self.assertFalse(report['no_deadlock_claim'])
def test_attached_process_is_left_alive_and_no_metric_not_pass(self):
with tempfile.TemporaryDirectory() as directory:
child = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(10)'])
try:
process, report, _ = self.run_monitor(directory, '--pid', str(child.pid))
self.assertEqual(process.returncode, 2, process.stderr)
self.assertIsNone(child.poll())
self.assertFalse(report['progress_observed'])
self.assertFalse(report['no_deadlock_claim'])
finally:
child.terminate()
child.wait(timeout=3)
def test_early_exit_fails(self):
with tempfile.TemporaryDirectory() as directory:
process, report, _ = self.run_monitor(directory, '--command', sys.executable, '-c', 'pass')
self.assertEqual(process.returncode, 1)
self.assertEqual(report['status'], 'failed')
self.assertIn('process', report['reason'])
def test_stalled_metric_fails_even_if_process_alive(self):
with tempfile.TemporaryDirectory() as directory:
progress = Path(directory) / 'progress.json'
progress.write_text('{"completed":1}')
process, report, _ = self.run_monitor(directory, '--progress-file', str(progress),
'--max-progress-gap-seconds', '.15', '--command', sys.executable, '-c', 'import time; time.sleep(10)')
self.assertEqual(process.returncode, 1)
self.assertIn('progress', report['reason'])
def test_journal_size_is_observed(self):
with tempfile.TemporaryDirectory() as directory:
journal = Path(directory) / 'goals.log'
journal.write_bytes(b'goal\n')
process, report, output = self.run_monitor(directory, '--journal', str(journal),
'--command', sys.executable, '-c', 'import time; time.sleep(10)')
self.assertEqual(process.returncode, 2)
sample = json.loads((output / 'samples.jsonl').read_text().splitlines()[-1])
self.assertEqual(sample['journal_bytes'][str(journal.resolve())], 5)
if __name__ == '__main__':
unittest.main()