Files
Claro/tools/validate_resource_guards.py
Hermes Agent ed23524817 fix: harden Claro diagnostics and runtime guards
Preserve source locations, validate malformed JSON, and add opt-in resource limits. Include executable error-guide coverage, learner pilot materials, native Windows acceptance helpers, and updated CI/documentation.
2026-10-25 02:42:53 +00:00

72 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Exercise bounded malformed JSON, opt-in resource guards and empty loops."""
from pathlib import Path
import json
import os
import shlex
import subprocess
import tempfile
ROOT = Path(__file__).resolve().parents[1]
def main():
with tempfile.TemporaryDirectory(prefix='claro-resource-guards-', dir=os.environ.get('TMPDIR')) as tmp:
work = Path(tmp)
override = os.environ.get('CLARO_EXE')
exe = Path(override).resolve() if override else work / 'claro'
if not override:
subprocess.run(shlex.split(os.environ.get('CLARO_CC', 'gcc')) + ['-std=c89', '-pedantic-errors', str(ROOT/'src/claro.c'), '-o', str(exe), '-lm'], check=True)
def run(name, text, error=None, limits=None):
(work/name).write_text(text)
env = dict(os.environ)
for key in ['CLARO_STEP_LIMIT', 'CLARO_READ_FILE_LIMIT']:
env.pop(key, None)
env.update(limits or {})
try:
r = subprocess.run([str(exe), name], cwd=work, env=env, capture_output=True, text=True, timeout=8)
except subprocess.TimeoutExpired:
raise AssertionError(f'{name}: interpreter failed to terminate') from None
output = r.stdout+r.stderr
assert not any(t in output for t in ["AddressSanitizer", "LeakSanitizer", "runtime error:"]), output
assert r.returncode == (1 if error else 0), (name, r.returncode, output)
if error:
assert error in output and ':0:' not in output, (name, output)
print('PASS:', name)
return r
(work/'invalid.json').write_text('[?]')
run('bad-json.claro', 'READ FILE "invalid.json" AS text\nPARSE JSON text AS data\n', 'JSON')
for number, text in enumerate(['[1,]', '{"x" 1}', '[1 2]', '"unterminated', '1 extra', '01', '[', '{', '{"x":}', '1e+', '"\\uD800"', '"\\uDC00"', '"\\u12zz"', '"\\u0000"', '"\\q"']):
(work/'invalid.json').write_text(text)
run(f'malformed-{number}.claro', 'READ FILE "invalid.json" AS text\nPARSE JSON text AS data\n', 'JSON')
(work/'nested.json').write_text('['*129+'0'+']'*129)
run('deep-json.claro','READ FILE "nested.json" AS text\nPARSE JSON text AS data\n','nesting')
(work/'valid.json').write_text('['*128+'0'+']'*128)
run('valid-depth.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\n')
(work/'valid.json').write_text('{"text":"\\u0041\\uD83D\\uDE00", "n": -1.25e2, "ok": true}')
r=run('valid-json.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\nGET data KEY "text" AS result\nSAY result\n')
assert r.stdout=='A😀\n',r.stdout
original = {"text": "tab\tnewline\ncontrol\x01quote\"", "values": [1, True, None, "A😀"]}
(work/'valid.json').write_text(json.dumps(original))
run('roundtrip.claro','READ FILE "valid.json" AS text\nPARSE JSON text AS data\nMAKE JSON data AS encoded\nWRITE FILE "roundtrip.json" WITH encoded\n')
assert json.loads((work/'roundtrip.json').read_text()) == original
for name, text in [('empty-repeat.claro','REPEAT\nUNTIL NO\n'),('empty-count.claro','DO 1000000000 TIMES\nEND\n'),('empty-for.claro','FOR i FROM 1 TO 1000000000\nEND\n')]:
run(name,text,'step limit',{'CLARO_STEP_LIMIT':'20'})
run('finite.claro','DO 3 TIMES\nSAY "Yes"\nEND\n',limits={'CLARO_STEP_LIMIT':'20'})
run('missing-until.claro','REPEAT\nSAY "Oops"\n','UNTIL')
(work/'large.txt').write_bytes(b'x'*1048577)
run('read-limit.claro','READ FILE "large.txt" AS data\n','file limit',{'CLARO_READ_FILE_LIMIT':'1048576'})
(work/'boundary.txt').write_bytes(b'x'*32)
r=run('read-boundary.claro','READ FILE "boundary.txt" AS data\nTEXT LENGTH data AS length\nSAY length\n',limits={'CLARO_READ_FILE_LIMIT':'32'})
assert r.stdout=='32\n',r.stdout
run('unlimited-read.claro','READ FILE "large.txt" AS data\n')
run('caught-budget.claro','TRY\nREPEAT\nUNTIL NO\nCATCH\nSAY "must not escape"\nENDTRY\n','step limit',{'CLARO_STEP_LIMIT':'20'})
r=run('long-finite.claro','SET i 0\nREPEAT\nSET i i + 1\nUNTIL i = 20000\nSAY i\n',limits={'CLARO_STEP_LIMIT':'200000'})
assert r.stdout=='20000\n',r.stdout
run('overflow-limit.claro','SAY "must not execute"\n','CLARO_STEP_LIMIT',{'CLARO_STEP_LIMIT':'999999999999999999999999999999'})
run('invalid-limit.claro','SAY "must not execute"\n','CLARO_STEP_LIMIT',{'CLARO_STEP_LIMIT':'-1'})
if __name__=='__main__':
main()