fix: retain planning feedback and add acceptance probes
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user