129 lines
6.9 KiB
Python
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()
|