Files
VianoBugAnalysis/tests/profiles/vcore/test_capture_handoff.py
T

104 lines
4.9 KiB
Python

import importlib.util
import json
from pathlib import Path
import socket
import sys
import threading
import time
from types import SimpleNamespace
import pytest
import uvicorn
from viano_bug_analysis.api import APIConfig,Client,Project,IntakeLimits
from viano_bug_analysis.api.app import create_app
ROOT=Path(__file__).resolve().parents[3]
spec=importlib.util.spec_from_file_location('buganalysis_handoff',ROOT/'deploy/capture-worker/buganalysis_handoff.py')
bridge=importlib.util.module_from_spec(spec);spec.loader.exec_module(bridge)
sys.modules['buganalysis_handoff']=bridge
def config(tmp_path,url='http://127.0.0.1:8080'):
return bridge.settings({'enabled':True,'url':url,'project_id':'core','producer_id':'test-capture','outbox':'outbox.sqlite3'},tmp_path)
def test_settings_reject_credential_leak_paths(tmp_path):
assert bridge.settings({},tmp_path) is None
for url in ('http://external.example','https://user:password@example.org','https://example.org/path','https://example.org?secret=x'):
with pytest.raises(bridge.HandoffError):config(tmp_path,url)
def test_outbox_retries_integrity_and_parallel_lease(tmp_path,source_pair):
now=[10.0];cfg=config(tmp_path);outbox=bridge.Outbox(cfg,clock=lambda:now[0])
diag,src,*_=source_pair
outbox.stage(1,20,diag,src,'revision');outbox.stage(1,20,diag,src,'revision')
assert len(outbox.status())==1
seen=[]
def failed(cfg,payload,key):seen.append(key);raise OSError('secret must not persist')
outbox.flush(send=failed)
assert outbox.status()[0]['status']=='pending'
assert 'secret' not in json.dumps(outbox.status())
other=bridge.Outbox(cfg,clock=lambda:now[0])
def successful(cfg,payload,key):
seen.append(key)
other.flush(force=True,send=lambda *_:pytest.fail('Concurrent lease ignored'))
return 'OK',{'submission_id':'id'}
now[0]+=61;outbox.flush(send=successful)
assert len(set(seen))==1 and outbox.status()[0]['status']=='sent'
outbox.stage(2,20,diag,src,'revision')
diag.write_bytes(b'changed')
outbox.flush(send=lambda *_:pytest.fail('Changed archives uploaded'))
assert outbox.status()[1]['code']=='HANDOFF_ARCHIVE_CHANGED'
def test_real_streaming_intake_and_lost_response_replay(tmp_path,source_pair,monkeypatch):
token='test_token_'+'x'*40;monkeypatch.setenv('VBA_INTAKE_TOKEN',token)
api=APIConfig(tmp_path/'service',(Project('core','wow','vcore'),),
(Client('capture','VBA_INTAKE_TOKEN',('core',)),),limits=IntakeLimits(min_free_bytes=1))
app=create_app(api)
sock=socket.socket();sock.bind(('127.0.0.1',0));sock.listen()
port=sock.getsockname()[1]
server=uvicorn.Server(uvicorn.Config(app,log_level='critical',access_log=False))
thread=threading.Thread(target=lambda:server.run(sockets=[sock]),daemon=True);thread.start()
try:
for _ in range(100):
if server.started:break
time.sleep(.02)
assert server.started
cfg=config(tmp_path,f'http://127.0.0.1:{port}')
outbox=bridge.Outbox(cfg)
outbox.stage(1,20,source_pair[0],source_pair[1],'verified-source-revision')
def lose_response(*args):
assert bridge.upload(*args)[0]=='OK'
raise OSError('Response lost after acceptance')
outbox.flush(send=lose_response)
assert outbox.status()[0]['status']=='pending'
bridge.Outbox(cfg).flush(force=True)
row=outbox.status()[0]
assert row['status']=='sent' and json.loads(row['receipt'])['case_id']=='BUG-000020'
assert len(list((tmp_path/'service/runtime/intake/requests').rglob('*.json')))==1
assert token.encode() not in outbox.path.read_bytes()
finally:
server.should_exit=True;thread.join(10);sock.close()
assert not thread.is_alive()
def test_worker_stages_before_capture_completion_and_can_disable(tmp_path,source_pair,monkeypatch):
worker_spec=importlib.util.spec_from_file_location('capture_worker_v04',ROOT/'deploy/capture-worker/wowdiag_worker.py')
worker=importlib.util.module_from_spec(worker_spec);sys.modules[worker_spec.name]=worker;worker_spec.loader.exec_module(worker)
closed=[];completed=[]
monkeypatch.setattr(worker,'connect',lambda cfg:SimpleNamespace(close=lambda:closed.append(True)))
monkeypatch.setattr(worker,'claim_one',lambda *_:worker.Job(1,20,'claim'))
monkeypatch.setattr(worker,'execute_pipeline',lambda *_:(source_pair[0],source_pair[1],'version','revision'))
cfg=SimpleNamespace(buganalysis=config(tmp_path))
def complete(*args):
if cfg.buganalysis:assert bridge.Outbox(cfg.buganalysis).status()[0]['status']=='pending'
completed.append(True)
monkeypatch.setattr(worker,'mark_completed',complete)
monkeypatch.setattr(worker,'mark_failed',lambda *_:pytest.fail('Successful capture marked failed'))
assert worker.process_one(cfg,None)
cfg.buganalysis=None
assert worker.process_one(cfg,None)
assert len(completed)==len(closed)==2