77 lines
5.3 KiB
Python
77 lines
5.3 KiB
Python
"""Resource-bounded actual document/OCR children and real local HTTP startup."""
|
|
from pathlib import Path
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
from urllib import request, error
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
from verify import verify_fixture
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
manifest = json.loads((HERE / 'ARTIFACTS.json').read_text())['safetensors']
|
|
verify_fixture('/fixture', manifest)
|
|
results = []
|
|
with tempfile.TemporaryDirectory(prefix='jt-parsers-') as temporary:
|
|
directory = Path(temporary)
|
|
image = Image.new('RGB', (1000, 180), 'white')
|
|
ImageDraw.Draw(image).text((40, 50), 'PYTHON CAREER 2026', fill='black', font=ImageFont.load_default(size=48))
|
|
image.save(directory / 'ocr.png'); image.save(directory / 'ocr.pdf', resolution=150)
|
|
for file in [*(HERE / 'data').glob('benign.*'), directory / 'ocr.png', directory / 'ocr.pdf']:
|
|
output = directory / (file.name + '.json')
|
|
args = [sys.executable, '/app/parser_child.py', '--input', str(file), '--output', str(output), '--extension', file.suffix, '--cpu-seconds', '10', '--memory-bytes', str(512 * 1024 * 1024)]
|
|
result = subprocess.run(args, capture_output=True, text=True, timeout=20)
|
|
assert result.returncode == 0, (file.name, result.stdout, result.stderr, output.read_text() if output.exists() else '')
|
|
parsed = json.loads(output.read_text())
|
|
assert parsed['ok'] and parsed['text']
|
|
if file.name.startswith('ocr'):
|
|
assert all(token in parsed['text'] for token in ['PYTHON','CAREER','2026'])
|
|
assert parsed['ocr_used'] is True
|
|
else:
|
|
assert all(token in parsed['text'] for token in ['Åse','Østby','Python','C#'])
|
|
results.append({'file':file.name,'sha256':hashlib.sha256(file.read_bytes()).hexdigest(),'text':parsed['text'],'ocrUsed':parsed['ocr_used'],'childExitCode':result.returncode})
|
|
|
|
env = os.environ.copy()
|
|
env.update(AI_SERVICE_SKIP_MODEL_LOAD='0', AI_SERVICE_EAGER_MODEL_LOAD='0', AI_SERVICE_TOKEN='p9-local-qualification-only')
|
|
code = "import sys;sys.path.insert(0,'/app');import app,uvicorn;app.MODEL_NAME='/fixture';uvicorn.run(app.app,host='127.0.0.1',port=18081,log_level='warning')"
|
|
with Path('/evidence/server.log').open('w') as log:
|
|
server = subprocess.Popen([sys.executable,'-c',code],env=env,stdout=log,stderr=subprocess.STDOUT)
|
|
try:
|
|
deadline=time.monotonic()+30
|
|
while True:
|
|
try:
|
|
with request.urlopen('http://127.0.0.1:18081/health',timeout=1) as response:health=json.load(response)
|
|
break
|
|
except (OSError,error.URLError):
|
|
assert server.poll() is None,'Server exited before readiness'
|
|
assert time.monotonic()<deadline,'Server readiness deadline exceeded'
|
|
time.sleep(.1)
|
|
assert health['model_loaded'] is False
|
|
body=json.dumps({'text':'Python engineers build reliable accessible career software.','max_length':24,'min_length':8}).encode()
|
|
try:request.urlopen(request.Request('http://127.0.0.1:18081/summarize',data=body,headers={'Content-Type':'application/json'}),timeout=10)
|
|
except error.HTTPError as failure:assert failure.code==401
|
|
else:raise AssertionError('Missing service token accepted')
|
|
headers={'Content-Type':'application/json','X-Ai-Service-Token':'p9-local-qualification-only'}
|
|
replies=[]
|
|
for _ in range(2):
|
|
with request.urlopen(request.Request('http://127.0.0.1:18081/summarize',data=body,headers=headers),timeout=20) as response:replies.append(json.load(response))
|
|
assert replies[0]['cached'] is False and replies[1]['cached'] is True
|
|
assert replies[0]['summary']==replies[1]['summary']
|
|
boundary='JtSyntheticQualification'
|
|
upload=(f'--{boundary}\r\nContent-Disposition: form-data; name="file"; filename="synthetic.txt"\r\nContent-Type: text/plain\r\n\r\n'.encode()+(HERE/'data/benign.txt').read_bytes()+f'\r\n--{boundary}--\r\n'.encode())
|
|
with request.urlopen(request.Request('http://127.0.0.1:18081/extract-text',data=upload,headers={'Content-Type':'multipart/form-data; boundary='+boundary,'X-Ai-Service-Token':'p9-local-qualification-only'}),timeout=20) as response:extracted=json.load(response)
|
|
assert 'Åse' in extracted['text'] and 'C#' in extracted['text']
|
|
results.append({'case':'real-http-startup','health200':True,'unauthenticated401':True,'realSummary200':True,'repeatCache':True,'actualUploadExtraction200':True})
|
|
finally:
|
|
server.terminate()
|
|
try:server.wait(timeout=5)
|
|
except subprocess.TimeoutExpired:server.kill();server.wait(timeout=5)
|
|
assert server.poll() is not None
|
|
|
|
data=Path('/usr/share/tesseract-ocr/5/tessdata/eng.traineddata')
|
|
record={'status':'PASS','cases':results,'tesseractVersion':subprocess.check_output(['tesseract','--version'],text=True).splitlines()[0],'englishDataSha256':hashlib.sha256(data.read_bytes()).hexdigest(),'englishDataBytes':data.stat().st_size,'offline':True,'serverStopped':True,'limits':'Synthetic TXT/DOCX/PDF/image/OCR and small BART CPU HTTP contracts; no production documents, full-size model, GPU, external provider or semantic quality.'}
|
|
print(json.dumps(record),flush=True)
|