Enrich cases with descriptive context and traced relationships
This commit is contained in:
1 parent
0dcd2ab6df
commit
87581e5cd2
5 files changed
+460
-18
No files matched your search
@@ -0,0 +1,135 @@
|
||||
import json
|
||||
import pytest
|
||||
|
||||
from viano_bug_analysis.tools.context_expansion import ContextExpansion
|
||||
from viano_bug_analysis.tools.project_query import ProjectResearch
|
||||
from viano_bug_analysis.tools.config import ToolError
|
||||
from viano_bug_analysis.case import CaseBuilder
|
||||
from viano_bug_analysis.package import PackageReader
|
||||
from test_case_builder import case_setup
|
||||
|
||||
|
||||
class RelatedData:
|
||||
def __init__(self):self.calls=[]
|
||||
def binding(self):return {"snapshot_id":"generic-fixture"}
|
||||
def validate(self,query):pass
|
||||
def query(self,query):
|
||||
self.calls.append(query)
|
||||
value=query['filters'][0]['value']
|
||||
rows={('tasks',10):[{'id':10,'description':'Use the supplied capture device to find the missing objects.','device':22,'required':22,'unused':0}],
|
||||
('devices',22):[{'id':22,'name':'Capture device','script':'capture_handler'}]}
|
||||
return {'items':[{'table':query['table'],'row':r} for r in rows.get((query['table'],value),[])],
|
||||
'coverage':'complete_filtered_rows','truncated':False}
|
||||
|
||||
|
||||
def initial():
|
||||
return {'kind':'database.rows','table':'tasks','columns':['id','description','device','required','unused'],
|
||||
'filters':[{'column':'id','operator':'eq','value':10}],'offset':0,'limit':1}
|
||||
|
||||
|
||||
def rule(column='device'):
|
||||
return {'from_table':'tasks','from_id_column':'id','from_column':column,
|
||||
'relation':'provided_device','entity_type':'device','table':'devices','column':'id','columns':['id','name','script']}
|
||||
|
||||
|
||||
def test_relationship_traversal_keeps_text_and_deduplicates_queries():
|
||||
db=RelatedData();service=ProjectResearch({},database=db)
|
||||
additions,facts,context,entities=ContextExpansion(service,{'relationships':[rule(),rule('required'),rule('unused')]}).collect([initial()])
|
||||
assert len(db.calls)==2 and len(additions)==2
|
||||
assert 'Use the supplied capture device' in str(facts)
|
||||
assert {e['id'] for e in entities}=={'22'}
|
||||
assert all(e['roles']==['provided_device'] for e in entities)
|
||||
edges=[f for f in facts if f['name']=='project_relationship']
|
||||
assert {e['value']['from_column'] for e in edges}=={'device','required'}
|
||||
for edge in edges:
|
||||
origin=edge['origin'];capture=json.loads(additions[origin['source_ref']])
|
||||
assert capture['result']['items'][0]['row'][edge['value']['from_column']]==22
|
||||
assert all(e['value']['meaning']=='explicit_data_reference_not_causation' for e in edges)
|
||||
|
||||
|
||||
def test_descriptive_context_is_presented_once_and_keeps_raw_proof():
|
||||
service=ProjectResearch({},database=RelatedData())
|
||||
additions,facts,_,_=ContextExpansion(service,{'texts':[{'table':'tasks','id_column':'id','context_role':'task_definition','columns':['description']}]}).collect([initial()])
|
||||
description=next(f for f in facts if f['name']=='project_description')['value']
|
||||
assert description['entity_id']==10 and description['context_role']=='task_definition'
|
||||
assert 'not_verified_runtime_behavior' in description['meaning']
|
||||
text=description['texts'][0]
|
||||
capture=json.loads(additions[text['origin']['source_ref']])
|
||||
assert text['text']==capture['result']['items'][0]['row']['description']
|
||||
row=facts[0]['value']['result']['items'][0]['row']
|
||||
assert row['description']=={'text_ref':text['origin']}
|
||||
|
||||
|
||||
def test_cycle_and_operational_limits_are_explicit():
|
||||
db=RelatedData();service=ProjectResearch({},database=db)
|
||||
cycle={'from_table':'devices','from_id_column':'id','from_column':'id','relation':'self','table':'devices','column':'id','columns':['id','name','script']}
|
||||
result=ContextExpansion(service,{'relationships':[rule(),cycle]}).collect([initial()])
|
||||
assert len(db.calls)==2
|
||||
db.calls.clear()
|
||||
service=ProjectResearch({},database=db)
|
||||
_,facts,_,_=ContextExpansion(service,{'relationships':[rule()],'max_queries':1}).collect([initial()])
|
||||
assert len(db.calls)==1 and facts[-1]['value']['omitted_queries']==1
|
||||
_,facts,_,_=ContextExpansion(service,{'relationships':[rule(),rule('required')],'max_relationships':1}).collect([initial()])
|
||||
assert facts[-1]['value']['omitted_relationships']==1
|
||||
|
||||
|
||||
def test_depth_limit_does_not_claim_complete_graph():
|
||||
service=ProjectResearch({},database=RelatedData())
|
||||
further={'from_table':'devices','from_id_column':'id','from_column':'id','relation':'device_detail','table':'other','column':'id','columns':['id']}
|
||||
_,facts,_,_=ContextExpansion(service,{'relationships':[rule(),further],'max_depth':1}).collect([initial()])
|
||||
assert facts[-1]['value']['depth_limited_relationships']==1
|
||||
assert 'not_exhaustive' in facts[-1]['value']['meaning']
|
||||
|
||||
|
||||
def test_missing_relationship_rows_remain_uninterpreted():
|
||||
service=ProjectResearch({},database=RelatedData())
|
||||
absent=rule();absent['table']='unknown_devices'
|
||||
_,facts,_,entities=ContextExpansion(service,{'relationships':[absent]}).collect([initial()])
|
||||
results=[f for f in facts if f['name']=='project_query']
|
||||
assert results[-1]['value']['result']['items']==[]
|
||||
assert entities[0]['id']=='22' # Referenced identity survives missing metadata.
|
||||
assert 'root_cause' not in str(facts)
|
||||
|
||||
|
||||
def test_script_search_uses_bound_symbol_not_numeric_grep():
|
||||
db=RelatedData()
|
||||
class Source:
|
||||
def binding(self):return {'commit':'fixture'}
|
||||
def validate(self,q):assert q['kind']=='source.symbol'
|
||||
def query(self,q):return {'items':[{'path':'capture.cpp','line':3,'text':'class capture_handler {};'}],'coverage':'complete_selected_text_search','truncated':False}
|
||||
service=ProjectResearch({},database=db,source=Source())
|
||||
script={'from_table':'devices','from_column':'script','from_id_column':'id','relation':'device_script','kind':'source.symbol','path_prefix':'src/'}
|
||||
_,facts,context,_=ContextExpansion(service,{'relationships':[rule(),script]}).collect([initial()])
|
||||
assert len(context['source'])==1
|
||||
assert facts[2]['value']['query']['text']=='capture_handler'
|
||||
|
||||
|
||||
def test_casebuilder_links_entities_and_preserves_prior_inputs(case_setup):
|
||||
store=case_setup[0]
|
||||
before=store.latest('case-A','stable-package')
|
||||
with PackageReader(before) as reader:project=reader.manifest['project']
|
||||
service=ProjectResearch(project,database=RelatedData())
|
||||
service.enrichment={'queries':[initial()],'relationships':[rule()]}
|
||||
built=CaseBuilder(store,project_research=service).build('case-A','stable-package')
|
||||
with PackageReader(before) as old,PackageReader(built.package) as reader:
|
||||
case=json.loads(reader.read_bytes(built.case_ref))
|
||||
assert any(e['type']=='device' and e['id']=='22' for e in case['entities'])
|
||||
assert 'provided_device' in str(case['runtime_facts'])
|
||||
for path in old.files:
|
||||
if path!='manifest.json':assert old.read_bytes(path)==reader.read_bytes(path)
|
||||
assert CaseBuilder(store,project_research=service).build('case-A','stable-package').reused
|
||||
from viano_bug_analysis.lifecycle import State
|
||||
for i,target in enumerate([State.INTERPRETING_CASE,State.GENERATING_HYPOTHESES,State.INVESTIGATING_HYPOTHESES,
|
||||
State.BUILDING_DIAGNOSIS,State.OUTPUT_READY,State.HANDED_OFF,State.REOPENED]):
|
||||
store.advance('case-A','stable-package',target,step_id=f'context-reopen-{i}')
|
||||
reopened=CaseBuilder(store,project_research=service).build('case-A','stable-package')
|
||||
assert reopened.analysis_revision==2
|
||||
with PackageReader(reopened.package) as reader:
|
||||
case=json.loads(reader.read_bytes(reopened.case_ref))
|
||||
paths=[r['path'] for r in case['database_context']['refs']]
|
||||
assert len(paths)==len(set(paths))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('field',['max_queries','max_depth','max_relationships'])
|
||||
def test_invalid_limits_rejected(field):
|
||||
with pytest.raises(ToolError):ContextExpansion(ProjectResearch({}),{field:True})
|
||||
Reference in new issue
Block a user