test(harness): reorganize regression suites and preserve contract coverage
This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
import copy
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import textwrap
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from harness.tests._support.paths import ROOT
|
||||
SCRIPT = ROOT / 'harness/agents/review/skills/gitea-code-review/scripts/native_review.py'
|
||||
FIXTURES = ROOT / 'harness/tests/_support/review.py'
|
||||
|
||||
|
||||
def load(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
class PublicationJournalProcessTests(unittest.TestCase):
|
||||
def test_pending_intent_survives_process_crash_and_blocks_second_post(self):
|
||||
p, before, after, exact = self.plan_data()
|
||||
plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p, ensure_ascii=False))
|
||||
counter = Path(self.tmp.name) / 'posts'
|
||||
crash = self.child_prelude() + textwrap.dedent('''
|
||||
class CrashAPI(API):
|
||||
def create_review(self, target, body):
|
||||
Path(sys.argv[4]).write_text('create-entered')
|
||||
os._exit(17)
|
||||
m.publish(p, before, CrashAPI(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'})
|
||||
''')
|
||||
entered = Path(self.tmp.name) / 'entered'
|
||||
crashed = self.run_child(crash, self.root, plan_file, counter, entered)
|
||||
self.assertEqual(crashed.returncode, 17)
|
||||
self.assertEqual(entered.read_text(), 'create-entered')
|
||||
resume = self.child_prelude() + textwrap.dedent('''
|
||||
try:
|
||||
m.publish(p, before, API(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'})
|
||||
except Exception as exc:
|
||||
print(type(exc).__name__)
|
||||
''')
|
||||
resumed = self.run_child(resume, self.root, plan_file, counter)
|
||||
self.assertEqual(resumed.returncode, 0, resumed.stderr)
|
||||
self.assertIn('UncertainPublication', resumed.stdout)
|
||||
self.assertFalse(counter.exists(), 'resume must not issue a second POST')
|
||||
def test_process_lock_contention_fails_fast_without_post(self):
|
||||
p, before, after, exact = self.plan_data()
|
||||
plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p))
|
||||
counter = Path(self.tmp.name) / 'posts'
|
||||
ready = Path(self.tmp.name) / 'ready'
|
||||
holder = self.child_prelude() + textwrap.dedent('''
|
||||
entry = m._journal.open_binding(m.canonical_journal_path(p), p, anchor_file=str(script), origin=m.ORIGIN, validate=m.validate_plan)
|
||||
fd = m._journal.acquire_lock(entry); Path(sys.argv[4]).write_text('ready'); time.sleep(3); os.close(fd)
|
||||
''')
|
||||
proc = subprocess.Popen([sys.executable, '-c', holder, str(self.root), str(plan_file), str(counter), str(ready)],
|
||||
env={'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT)}, text=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
deadline = time.monotonic() + 3
|
||||
while not ready.exists() and time.monotonic() < deadline: time.sleep(.02)
|
||||
self.assertTrue(ready.exists())
|
||||
contender = self.child_prelude() + textwrap.dedent('''
|
||||
try:
|
||||
m.publish(p, before, API(), Lease(), m.canonical_journal_path(p), declaration={'mode': 'none'})
|
||||
except Exception as exc:
|
||||
print(type(exc).__name__)
|
||||
''')
|
||||
started = time.monotonic(); result = self.run_child(contender, self.root, plan_file, counter, timeout=2)
|
||||
self.assertLess(time.monotonic() - started, 2)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('ValidationError', result.stdout)
|
||||
self.assertFalse(counter.exists())
|
||||
finally:
|
||||
proc.kill(); proc.communicate(timeout=3)
|
||||
def test_crash_after_transport_write_reconciles_without_second_write(self):
|
||||
p, _, _, _ = self.plan_data()
|
||||
plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p))
|
||||
counter = Path(self.tmp.name) / 'posts'
|
||||
crash = self.child_prelude() + textwrap.dedent("""
|
||||
class CrashAfterWrite(API):
|
||||
def create_review(self, target, body):
|
||||
super().create_review(target, body)
|
||||
os._exit(17)
|
||||
m.publish(p, before, CrashAfterWrite(), Lease(), declaration={'mode': 'none'})
|
||||
""")
|
||||
result = self.run_child(crash, self.root, plan_file, counter)
|
||||
self.assertEqual(result.returncode, 17, result.stderr)
|
||||
self.assertEqual(counter.read_text(), '1')
|
||||
resume = self.child_prelude() + textwrap.dedent("""
|
||||
result = m.publish(p, before, API(), Lease(), declaration={'mode': 'none'})
|
||||
print(result['mode'])
|
||||
""")
|
||||
result = self.run_child(resume, self.root, plan_file, counter)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('no_op', result.stdout)
|
||||
self.assertEqual(counter.read_text(), '1')
|
||||
def test_two_publishing_processes_create_only_once(self):
|
||||
p, _, _, _ = self.plan_data()
|
||||
plan_file = Path(self.tmp.name) / 'plan.json'; plan_file.write_text(json.dumps(p))
|
||||
counter = Path(self.tmp.name) / 'posts'
|
||||
entered = Path(self.tmp.name) / 'create-entered'; release = Path(self.tmp.name) / 'release'
|
||||
first = self.child_prelude() + textwrap.dedent("""
|
||||
class GatedAPI(API):
|
||||
def create_review(self, target, body):
|
||||
result = super().create_review(target, body)
|
||||
Path(sys.argv[4]).write_text('entered')
|
||||
deadline = time.monotonic() + 8
|
||||
while not Path(sys.argv[5]).exists():
|
||||
if time.monotonic() >= deadline: raise TimeoutError('test watchdog')
|
||||
time.sleep(.02)
|
||||
return result
|
||||
result = m.publish(p, before, GatedAPI(), Lease(), declaration={'mode': 'none'})
|
||||
print(result['mode'])
|
||||
""")
|
||||
proc = subprocess.Popen([sys.executable, '-B', '-c', first, str(self.root), str(plan_file), str(counter), str(entered), str(release)],
|
||||
env={'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT)},
|
||||
text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
try:
|
||||
deadline = time.monotonic() + 4
|
||||
while not entered.exists() and time.monotonic() < deadline: time.sleep(.02)
|
||||
self.assertTrue(entered.exists(), 'first publisher must reach transport')
|
||||
self.assertEqual(counter.read_text(), '1')
|
||||
contender = self.child_prelude() + textwrap.dedent("""
|
||||
try: m.publish(p, before, API(), Lease(), declaration={'mode': 'none'})
|
||||
except m.ValidationError: print('blocked')
|
||||
""")
|
||||
result = self.run_child(contender, self.root, plan_file, counter, timeout=3)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('blocked', result.stdout)
|
||||
self.assertEqual(counter.read_text(), '1')
|
||||
release.write_text('release')
|
||||
stdout, stderr = proc.communicate(timeout=3)
|
||||
self.assertEqual(proc.returncode, 0, stderr)
|
||||
self.assertIn('created', stdout)
|
||||
self.assertEqual(counter.read_text(), '1')
|
||||
finally:
|
||||
if proc.poll() is None: proc.kill()
|
||||
proc.communicate(timeout=3)
|
||||
def setUp(self):
|
||||
self.m = load(SCRIPT, 'native_review_process_' + str(id(self)))
|
||||
self.fixtures = load(FIXTURES, 'native_review_process_fixture_' + str(id(self)))
|
||||
self.tmp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.tmp.cleanup)
|
||||
self.root = Path(self.tmp.name) / 'native-review-journals'
|
||||
self.m._journal.root_for = lambda anchor, root=self.root: root
|
||||
def plan_data(self):
|
||||
p, before, after, exact = self.fixtures.publication_fixture()
|
||||
return p, before, after, exact
|
||||
def declaration(self):
|
||||
return {'mode': 'none'}
|
||||
def run_child(self, script, *args, timeout=8):
|
||||
env = {'PATH': os.environ['PATH'], 'PYTHONPATH': str(ROOT), 'PYTHONDONTWRITEBYTECODE': '1'}
|
||||
return subprocess.run([sys.executable, '-c', script, *map(str, args)], env=env,
|
||||
capture_output=True, text=True, timeout=timeout)
|
||||
def child_prelude(self):
|
||||
return textwrap.dedent(f'''
|
||||
import copy, importlib.util, json, os, sys, time
|
||||
from pathlib import Path
|
||||
root = Path(sys.argv[1]); payload = json.loads(Path(sys.argv[2]).read_text())
|
||||
script = Path({str(SCRIPT)!r}); fixtures_path = Path({str(FIXTURES)!r})
|
||||
def load(path, name):
|
||||
spec = importlib.util.spec_from_file_location(name, path)
|
||||
m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); return m
|
||||
m = load(script, 'native_child_' + str(os.getpid()))
|
||||
f = load(fixtures_path, 'fixture_child_' + str(os.getpid()))
|
||||
m._journal.root_for = lambda anchor: root
|
||||
p, before, after, exact = f.publication_fixture()
|
||||
class Lease:
|
||||
def check(self, plan): return None
|
||||
class API:
|
||||
def __init__(self): self.created = Path(sys.argv[3]).exists()
|
||||
def identity(self): return copy.deepcopy(p['reviewer'])
|
||||
def snapshot(self, target): return copy.deepcopy(after if self.created else before)
|
||||
def read_review(self, target, rid): return copy.deepcopy(exact)
|
||||
def create_review(self, target, body):
|
||||
counter = Path(sys.argv[3]); n = int(counter.read_text() or '0') if counter.exists() else 0
|
||||
counter.write_text(str(n + 1))
|
||||
with counter.open('r+') as fd:
|
||||
fd.flush(); os.fsync(fd.fileno())
|
||||
self.created = True
|
||||
return copy.deepcopy(exact)
|
||||
api = API()
|
||||
''')
|
||||
def _legacy_files(self, p, before, *, verified=True):
|
||||
directory = Path(tempfile.mkdtemp(prefix='legacy-', dir=self.tmp.name))
|
||||
directory.chmod(0o700)
|
||||
canonical = self.m.canonical
|
||||
intent = {'schema': 'gitea-agent.native-review-intent/v1',
|
||||
'fingerprint': hashlib.sha256(self.m.canonical(p)).hexdigest(),
|
||||
'before_sha256': hashlib.sha256(self.m.canonical(before)).hexdigest(),
|
||||
'head': p['target']['head'], 'event': 'APPROVED',
|
||||
'body_sha256': hashlib.sha256(p['body'].encode()).hexdigest()}
|
||||
with self.m.Journal(directory) as journal:
|
||||
journal.write('before.json', before)
|
||||
journal.write('intent.json', intent)
|
||||
if verified:
|
||||
journal.write('verified.json', {'verified': True})
|
||||
return directory
|
||||
def _legacy_digest(self, directory):
|
||||
helper = getattr(self.m, 'legacy_journal_digest', None)
|
||||
self.assertTrue(callable(helper), 'public legacy journal digest helper is required')
|
||||
return helper(directory)
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user