Files
gitea-agent/harness/tests/review/test_publication_journal_processes.py
T

278 lines
15 KiB
Python

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
ROOT = Path(__file__).resolve().parents[3]
SCRIPT = ROOT / 'harness/agents/review/skills/gitea-code-review/scripts/native_review.py'
FIXTURES = ROOT / 'harness/tests/review/test_native_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 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 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 _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)
def test_valid_legacy_registration_is_read_only_and_preserves_source_bytes(self):
p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before)
original = {name: (source / name).read_bytes() for name in ['before.json', 'intent.json', 'verified.json']}
modes = {name: stat.S_IMODE((source / name).stat().st_mode) for name in original}
api = self.fixtures.FakeAPI(p, before, after, exact)
result = self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source),
api=api, lease=self.fixtures.FakeLease(), declaration=self.declaration())
self.assertTrue(result['read_only']); self.assertEqual(api.posts, 0)
for name, data in original.items():
self.assertEqual((source / name).read_bytes(), data)
self.assertEqual(stat.S_IMODE((source / name).stat().st_mode), modes[name])
target = self.m.canonical_journal_path(p)
self.assertEqual((target / 'before.json').read_bytes(), original['before.json'])
self.assertFalse((target / 'import.pending.json').exists())
def test_legacy_source_mutation_digest_mismatch_and_extra_intent_fields_reject(self):
p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before)
digest = self._legacy_digest(source)
mutated = json.loads((source / 'before.json').read_text()); mutated['issue']['body'] = 'changed'
(source / 'before.json').write_text(json.dumps(mutated, separators=(',', ':')))
with self.assertRaises(self.m.ValidationError):
self.m.register_legacy_journal(p, source, expected_legacy_sha256=digest, api=self.fixtures.FakeAPI(p, before, after, exact),
lease=self.fixtures.FakeLease(), declaration=self.declaration())
source = self._legacy_files(p, before)
intent = json.loads((source / 'intent.json').read_text()); intent['unexpected'] = True
(source / 'intent.json').write_text(json.dumps(intent, separators=(',', ':')))
with self.assertRaises(self.m.ValidationError):
self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=self.fixtures.FakeAPI(p, before, after, exact),
lease=self.fixtures.FakeLease(), declaration=self.declaration())
def test_legacy_symlink_hardlink_and_duplicate_json_are_rejected(self):
p, before, after, exact = self.plan_data()
source = self._legacy_files(p, before)
(source / 'before.json').unlink(); (source / 'before.json').symlink_to(Path(self.tmp.name) / 'elsewhere')
with self.assertRaises(self.m.ValidationError):
self.m.register_legacy_journal(p, source, expected_legacy_sha256='0' * 64, api=self.fixtures.FakeAPI(p, before, after, exact),
lease=self.fixtures.FakeLease(), declaration=self.declaration())
source = self._legacy_files(p, before)
os.link(source / 'before.json', source / 'duplicate.json')
with self.assertRaises(self.m.ValidationError):
self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=self.fixtures.FakeAPI(p, before, after, exact),
lease=self.fixtures.FakeLease(), declaration=self.declaration())
def test_registration_rejects_existing_canonical_binding_without_post(self):
p, before, after, exact = self.plan_data(); source = self._legacy_files(p, before)
target = self.m.canonical_journal_path(p)
target.parent.mkdir(parents=True, exist_ok=True); target.mkdir(mode=0o700)
(target / 'intent.json').write_text('{}'); (target / 'intent.json').chmod(0o600)
api = self.fixtures.FakeAPI(p, before, after, exact)
with self.assertRaises(self.m.ValidationError):
self.m.register_legacy_journal(p, source, expected_legacy_sha256=self._legacy_digest(source), api=api,
lease=self.fixtures.FakeLease(), declaration=self.declaration())
self.assertEqual(api.posts, 0)
if __name__ == '__main__':
unittest.main()