242 lines
15 KiB
Python
242 lines
15 KiB
Python
"""Temporary filesystem tests only; no process/brain/GPU execution."""
|
|
import copy
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import tempfile
|
|
import unittest
|
|
from unittest.mock import patch
|
|
|
|
import append_ledger as L
|
|
|
|
|
|
def put(path, value):
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_bytes(value if type(value) is bytes else L.raw_json(value))
|
|
return L.identity(path)
|
|
|
|
|
|
class LedgerTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup)
|
|
self.root=Path(self.temp.name).resolve()/'normal';self.root.mkdir()
|
|
self.reg=self.root/L.REGISTRATION; info=put(self.reg,{'only':'synthetic-registration'})
|
|
self.initial={L.REGISTRATION:info};self.sha=info['sha256']
|
|
self.ledger=L.Ledger(self.root,self.sha,self.initial,1<<20,0)
|
|
|
|
def job(self,name):
|
|
job=self.root/'jobs'/(name+'.json');put(job,{'entry_id':name})
|
|
self.ledger.begin(name,job,{'synthetic':True})
|
|
return job
|
|
|
|
def completion(self,name,job,*,payload=b'output',changes=None):
|
|
out=self.root/'runs'/name/'payload.dat';put(out,payload)
|
|
process=self.root/'receipts'/(name+'.json')
|
|
value=dict(entry_id=name,registration_sha256=self.sha,job_sha256=L.digest(job),complete=True,
|
|
exit_code=0,timed_out=False,exit_observed_by_parent=True)
|
|
value.update(changes or {});put(process,value)
|
|
inputs={str(self.reg):L.identity(self.reg),str(job):L.identity(job)}
|
|
outputs={str(p):L.identity(p) for p in (job,out,process)}
|
|
metadata=dict(job_file=str(job),process_receipt_file=str(process),scope='synthetic completed parent job only',observed_returncode=0)
|
|
return inputs,outputs,metadata
|
|
|
|
def commit(self,name):
|
|
job=self.job(name);args=self.completion(name,job)
|
|
return self.ledger.append(name,*args)
|
|
|
|
def test_two_entries_chain_full_tree_and_cumulative_bytes(self):
|
|
one=self.commit('one');two=self.commit('two')
|
|
second=L.read_json(self.root/L.CONTROL/'entries/0000000000000002.json')
|
|
self.assertEqual(second['prev_entry_sha256'],one['entry_sha256'])
|
|
self.assertEqual(two['entry_sha256'],L.digest(self.root/L.CONTROL/'entries/0000000000000002.json'))
|
|
inspected=self.ledger.inspect();self.assertEqual(inspected['entry_ids'],['one','two'])
|
|
self.assertFalse(inspected['retry_authorized']);self.assertTrue(inspected['all_tracked_history_rehashed'])
|
|
final=self.ledger.final_verify(['one','two']);self.assertTrue(final['pass_all'])
|
|
self.assertEqual(final['actual_total_bytes'],sum(p.stat().st_size for p in self.root.rglob('*') if p.is_file()))
|
|
self.assertFalse(final['scientific_gate_claimed'])
|
|
|
|
def test_reopen_independent_read_verifies_no_reexecution_or_duplicate_append(self):
|
|
self.commit('one')
|
|
reopened=L.Ledger(self.root,self.sha,self.initial,1<<20,0)
|
|
self.assertEqual(reopened.inspect()['entry_ids'],['one'])
|
|
with self.assertRaisesRegex(ValueError,'already started/committed'):
|
|
reopened.begin('one',self.root/'jobs/one.json')
|
|
with self.assertRaisesRegex(ValueError,'No unique uncommitted'):
|
|
reopened.append('one',{}, {},dict(job_file='x',process_receipt_file='y',scope='s',observed_returncode=0))
|
|
|
|
def test_pending_or_live_handle_is_unknown_and_never_retry_authority(self):
|
|
job=self.job('live');state=self.ledger.inspect()
|
|
self.assertEqual(state['uncommitted_state'],'STARTED-UNCOMMITTED-STATE-UNKNOWN')
|
|
self.assertFalse(state['retry_authorized'])
|
|
with self.assertRaisesRegex(ValueError,'uncommitted start'):self.ledger.begin('another',job)
|
|
args=list(self.completion('live',job));args[2]['observed_returncode']=None
|
|
with self.assertRaisesRegex(ValueError,'Live/failed'):self.ledger.append('live',*args)
|
|
final=self.ledger.final_verify(['live']);self.assertFalse(final['pass_all']);self.assertTrue(final['uncommitted_state_unknown'])
|
|
reopened=L.Ledger(self.root,self.sha,self.initial,1<<20,0)
|
|
self.assertEqual(reopened.inspect()['uncommitted_entry_ids'],['live'])
|
|
|
|
def test_actual_parent_receipt_required_not_worker_success_or_bool_exit(self):
|
|
job=self.job('one')
|
|
for mutation in ({'exit_code':False},{'complete':False},{'exit_observed_by_parent':False},
|
|
{'timed_out':True},{'registration_sha256':'0'*64},{'job_sha256':'0'*64},{'entry_id':'other'}):
|
|
args=self.completion('one',job,changes=mutation)
|
|
with self.subTest(mutation=mutation),self.assertRaisesRegex(ValueError,'parent process receipt'):
|
|
self.ledger.append('one',*args)
|
|
|
|
def test_same_size_same_mtime_mutation_fails_actual_SHA(self):
|
|
job=self.job('one');args=self.completion('one',job,payload=b'aaaa')
|
|
out=self.root/'runs/one/payload.dat';stat=out.stat();out.write_bytes(b'bbbb')
|
|
os.utime(out,ns=(stat.st_atime_ns,stat.st_mtime_ns))
|
|
with self.assertRaisesRegex(ValueError,'Actual file SHA/bytes'):self.ledger.append('one',*args)
|
|
|
|
def test_missing_extra_changed_files_reported_at_full_audit(self):
|
|
self.commit('one');out=self.root/'runs/one/payload.dat'
|
|
extra=self.root/'unexpected.log';put(extra,b'extra')
|
|
final=self.ledger.final_verify(['one']);self.assertFalse(final['pass_all']);self.assertIn('unexpected.log',final['extra_files'])
|
|
out.unlink();final=self.ledger.final_verify(['one'])
|
|
self.assertIn('runs/one/payload.dat',final['missing_files'])
|
|
put(out,b'different');final=self.ledger.final_verify(['one'])
|
|
self.assertIn('runs/one/payload.dat',final['changed_files'])
|
|
|
|
def test_tail_and_middle_tamper_not_trusted_by_inspect(self):
|
|
self.commit('one');self.commit('two')
|
|
first=self.root/L.CONTROL/'entries/0000000000000001.json'
|
|
value=L.read_json(first);value['metadata']['scope']='tampered'
|
|
put(first,value)
|
|
with self.assertRaisesRegex(ValueError,'chain/start identity'):self.ledger.inspect()
|
|
self.assertFalse(self.ledger.final_verify(['one','two'])['pass_all'])
|
|
|
|
def test_previously_observed_head_rewrite_rejected_even_new_valid_JSON(self):
|
|
self.commit('one');path=self.root/L.CONTROL/'entries/0000000000000001.json'
|
|
value=L.read_json(path);value['metadata']['scope']='changed';put(path,value)
|
|
with self.assertRaisesRegex(ValueError,'head changed'):self.ledger.inspect()
|
|
|
|
def test_registered_final_entry_order_and_coverage_are_exact(self):
|
|
self.commit('one');self.commit('two')
|
|
for expected in (['two','one'],['one'],['one','two','three']):
|
|
report=self.ledger.final_verify(expected)
|
|
self.assertFalse(report['pass_all']);self.assertFalse(report['expected_order_exact'])
|
|
with self.assertRaisesRegex(ValueError,'expected entry order'):self.ledger.final_verify(['one','one'])
|
|
|
|
def test_path_escape_aliases_and_overwrite_rejected(self):
|
|
self.commit('one');job=self.job('two');inputs,outputs,meta=self.completion('two',job)
|
|
outside=self.root.parent/'outside.dat';put(outside,b'external')
|
|
outputs[str(outside)]=L.identity(outside)
|
|
with self.assertRaisesRegex(ValueError,'escapes experiment'):self.ledger.append('two',inputs,outputs,meta)
|
|
del outputs[str(outside)];old=self.root/'runs/one/payload.dat';outputs[str(old)]=L.identity(old)
|
|
with self.assertRaisesRegex(ValueError,'previously tracked'):self.ledger.append('two',inputs,outputs,meta)
|
|
del outputs[str(old)];outputs['runs/two/payload.dat']=outputs[str(self.root/'runs/two/payload.dat')]
|
|
with self.assertRaisesRegex(ValueError,'Duplicate path aliases'):self.ledger.append('two',inputs,outputs,meta)
|
|
with self.assertRaisesRegex(ValueError,'Escaping/noncanonical'):L.relative_name('../escape')
|
|
|
|
def test_consumed_tracked_input_cannot_be_silently_reauthorized_with_new_SHA(self):
|
|
self.commit('one');job=self.job('two');inputs,outputs,meta=self.completion('two',job)
|
|
prior=self.root/'runs/one/payload.dat';put(prior,b'overwritten')
|
|
inputs[str(prior)]=L.identity(prior)
|
|
with self.assertRaisesRegex(ValueError,'committed identity'):self.ledger.append('two',inputs,outputs,meta)
|
|
|
|
def test_external_inputs_allowed_but_declared_bytes_reverified(self):
|
|
job=self.job('one');inputs,outputs,meta=self.completion('one',job)
|
|
external=self.root.parent/'frozen-reference.json';put(external,{'read_only':'data'})
|
|
inputs[str(external)]=L.identity(external);self.ledger.append('one',inputs,outputs,meta)
|
|
row=L.read_json(self.root/L.CONTROL/'entries/0000000000000001.json')
|
|
self.assertIsNone(next(r for r in row['consumed_inputs'] if r['path']==str(external))['relative_path'])
|
|
put(external,{'read_only':'changed'})
|
|
with self.assertRaisesRegex(ValueError,'Actual file SHA/bytes'):self.ledger.inspect()
|
|
|
|
def test_full_audit_hashes_shared_external_input_once_per_invocation(self):
|
|
external=self.root.parent/'large-shared-reference';put(external,b'fixed external bytes')
|
|
expected=L.identity(external)
|
|
for name in ('one','two','three'):
|
|
job=self.job(name);inputs,outputs,meta=self.completion(name,job)
|
|
inputs[str(external)]=expected;self.ledger.append(name,inputs,outputs,meta)
|
|
original=L.digest;counts={}
|
|
def counted(path):
|
|
key=str(Path(path));counts[key]=counts.get(key,0)+1
|
|
return original(path)
|
|
with patch.object(L,'digest',side_effect=counted):
|
|
self.assertTrue(self.ledger.final_verify(['one','two','three'])['pass_all'])
|
|
self.assertEqual(counts[str(external)],1)
|
|
counts.clear();self.ledger.inspect();self.assertEqual(counts[str(external)],1)
|
|
counts.clear();self.ledger.inspect();self.assertEqual(counts[str(external)],1)
|
|
oldstat=external.stat();external.write_bytes(b'other external bytes')
|
|
os.utime(external,ns=(oldstat.st_atime_ns,oldstat.st_mtime_ns))
|
|
with self.assertRaisesRegex(ValueError,'Actual file SHA/bytes'):self.ledger.inspect()
|
|
|
|
def test_append_does_not_iterate_or_sum_historical_maps_or_search_ordered_ids(self):
|
|
self.commit('one')
|
|
class LookupOnlyDict(dict):
|
|
def __iter__(self):raise AssertionError('Historical map iteration in fast append')
|
|
def keys(self):raise AssertionError('Historical map copy in fast append')
|
|
def values(self):raise AssertionError('Historical map sum in fast append')
|
|
def items(self):raise AssertionError('Historical map iteration in fast append')
|
|
class NoLinearMembership(list):
|
|
def __contains__(self,value):raise AssertionError('Linear completed-ID membership')
|
|
self.ledger._tracked=LookupOnlyDict(self.ledger._tracked)
|
|
self.ledger._controls=LookupOnlyDict(self.ledger._controls)
|
|
self.ledger._completed=NoLinearMembership(self.ledger._completed)
|
|
self.commit('two')
|
|
self.assertEqual(self.ledger._completed,['one','two'])
|
|
self.assertTrue(self.ledger.final_verify(['one','two'])['pass_all'])
|
|
|
|
def test_head_is_freshly_checked_after_cached_full_tree_walk(self):
|
|
self.commit('one');tail=self.root/L.CONTROL/'entries/0000000000000001.json'
|
|
original_replay=self.ledger._replay;original_identity=self.ledger._identity_once
|
|
state={'walking':False,'mutated':False}
|
|
def replay(verified):
|
|
original_replay(verified);state['walking']=True
|
|
def during_walk(path,verified):
|
|
if state['walking'] and not state['mutated']:
|
|
value=L.read_json(tail);value['metadata']['scope']='mutation during final walk'
|
|
put(tail,value);state['mutated']=True
|
|
return original_identity(path,verified)
|
|
with patch.object(self.ledger,'_replay',side_effect=replay),\
|
|
patch.object(self.ledger,'_identity_once',side_effect=during_walk):
|
|
report=self.ledger.final_verify(['one'])
|
|
self.assertTrue(state['mutated']);self.assertFalse(report['pass_all'])
|
|
self.assertTrue(any('Known ledger tail changed' in message for message in report['errors']))
|
|
|
|
def test_shared_mutable_build_separate_current_inventory_and_cap(self):
|
|
build=self.root/'extension-build/cache.bin';put(build,b'first');self.commit('one')
|
|
put(build,b'changed build is allowed');self.commit('two')
|
|
report=self.ledger.final_verify(['one','two']);self.assertTrue(report['pass_all'])
|
|
self.assertEqual(report['shared_mutable_build_bytes'],len(b'changed build is allowed'))
|
|
self.assertNotIn('extension-build/cache.bin',report['extra_files'])
|
|
put(build,b'x'*(1<<20));report=self.ledger.final_verify(['one','two'])
|
|
self.assertFalse(report['pass_all']);self.assertFalse(report['output_cap_respected'])
|
|
|
|
def test_stale_writer_lock_and_partial_publication_never_auto_cleared(self):
|
|
lock=self.root/L.CONTROL/'writer.lock';put(lock,b'unknown owner')
|
|
with self.assertRaises(FileExistsError):self.ledger.begin('one',self.reg)
|
|
with self.assertRaisesRegex(ValueError,'state unknown'):self.ledger.inspect()
|
|
self.assertEqual(lock.read_bytes(),b'unknown owner')
|
|
|
|
def test_exclusive_atomic_publication_and_preserved_failed_temporary(self):
|
|
target=self.root/'standalone.json';info=L.publish_exclusive(target,{'a':1})
|
|
self.assertEqual(info,L.identity(target))
|
|
with self.assertRaisesRegex(ValueError,'already exists'):L.publish_exclusive(target,{'a':2})
|
|
self.assertEqual(L.read_json(target),{'a':1})
|
|
target2=self.root/'failed.json'
|
|
with patch.object(L.os,'link',side_effect=OSError('synthetic unsupported filesystem')):
|
|
with self.assertRaises(OSError):L.publish_exclusive(target2,{'a':2})
|
|
self.assertFalse(target2.exists());self.assertEqual(len(list(self.root.glob('.publishing-*'))),1)
|
|
|
|
def test_linked_output_rejected_if_platform_supports_links(self):
|
|
source=self.root.parent/'other';source.mkdir();link=self.root/'linked'
|
|
try:link.symlink_to(source,target_is_directory=True)
|
|
except OSError:self.skipTest('Platform disallows synthetic symlink')
|
|
job=self.job('one');inputs,outputs,meta=self.completion('one',job)
|
|
put(source/'payload',b'x');outputs[str(link/'payload')]=L.identity(source/'payload')
|
|
with self.assertRaisesRegex(ValueError,'Linked'):self.ledger.append('one',inputs,outputs,meta)
|
|
|
|
def test_initial_and_header_authority_are_not_optional(self):
|
|
for change in ('registration','options'):
|
|
with self.subTest(change=change),self.assertRaises(ValueError):
|
|
L.Ledger(self.root,'0'*64 if change=='registration' else self.sha,self.initial,
|
|
2<<20 if change=='options' else 1<<20,0)
|
|
header=self.root/L.CONTROL/'header.json';value=L.read_json(header);value['max_output_bytes']+=1;put(header,value)
|
|
with self.assertRaisesRegex(ValueError,'header changed'):self.ledger.inspect()
|
|
|
|
|
|
if __name__=='__main__':unittest.main()
|