Files
cobol-java-v3/test-data/test_gap_coverage.py
T
hangshuo652 63b5284715 fix: _parse_llm_response now handles empty/invalid JSON gracefully
test: add gap coverage tests (hina_agent/JCL/quality gate edge cases)
2026-06-18 17:31:16 +08:00

185 lines
8.4 KiB
Python

"""
テストギャップ穴埋め — 未検証モジュールの機能テスト
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
対象: hina.hina_agent, jcl.executor, jcl.parser
"""
import sys, json
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent.parent))
PASS=0;FAIL=0;LOG=[]
def do(cat,name,fn):
global PASS,FAIL
try: fn(); PASS+=1; LOG.append(f' [{cat}] {name} -> PASS')
except Exception as e: FAIL+=1; LOG.append(f' [{cat}] {name} -> FAIL: {str(e)[:100]}')
# ── hina.hina_agent: LLM応答パース ──
from hina.hina_agent import _parse_llm_response, _validate_result, _fallback_classification, CONFUSION_PROMPT
do('HAG','_parse_llm_response: 生JSON', lambda: (
r:=_parse_llm_response('{"category":"condition_heavy","confidence":0.85}'),
r['category']=='condition_heavy' and r['confidence']==0.85))
do('HAG','_parse_llm_response: ```json ブロック', lambda: (
r:=_parse_llm_response('```json\n{"category":"data_file_centric","confidence":0.9}\n```'),
r['category']=='data_file_centric' and r['confidence']==0.9))
do('HAG','_parse_llm_response: ``` ブロック(無json)', lambda: (
r:=_parse_llm_response('```\n{"category":"simple_sequential","confidence":0.7}\n```'),
r['category']=='simple_sequential'))
do('HAG','_parse_llm_response: 空文字', lambda: (
r:=_parse_llm_response(''),
r['category']=='unknown'))
do('HAG','_parse_llm_response: 無効JSON', lambda: (
r:=_parse_llm_response('not json at all'),
r['category']=='unknown'))
do('HAG','_validate_result: 最小値', lambda: (
r:=_validate_result({}),
r['category']=='unknown' and r['confidence']==0.0 and r['required_tests']>=1))
do('HAG','_validate_result: 信頼度クランプ', lambda: (
r:=_validate_result({'confidence':5.0,'required_tests':0}),
r['confidence']<=1.0 and r['required_tests']>=1))
do('HAG','_validate_result: 信頼度下限', lambda: (
r:=_validate_result({'confidence':-1.0}),
r['confidence']>=0.0))
do('HAG','_validate_result: 不正タイプ', lambda: (
r:=_validate_result({'confidence':'abc','required_tests':'xyz'}),
r['confidence']==0.0 and r['required_tests']>=1))
do('HAG','_fallback_classification: 分岐0', lambda: (
r:=_fallback_classification({'decision_points':[],'paragraphs':[],'file_count':0}),
r['category']=='simple_sequential'))
do('HAG','_fallback_classification: SEARCH ALL', lambda: (
r:=_fallback_classification({'decision_points':[{'kind':'IF'}],'paragraphs':[],'file_count':0,'has_search_all':True,'has_call':False,'has_break':False}),
r['category']=='search_intensive'))
do('HAG','_fallback_classification: CALLベース', lambda: (
r:=_fallback_classification({'decision_points':[{'kind':'IF'}],'paragraphs':[],'file_count':0,'has_search_all':False,'has_call':True,'has_break':False}),
r['category']=='call_based'))
do('HAG','_fallback_classification: mixed_complex', lambda: (
r:=_fallback_classification({'decision_points':[{'kind':'IF'}]*5,'paragraphs':[],'file_count':2,'has_search_all':True,'has_call':True,'has_break':True}),
r['category']=='mixed_complex'))
do('HAG','CONFUSION_PROMPT 書式', lambda: (
p:=CONFUSION_PROMPT.format(paragraph_count=3,decision_count=2,if_count=1,
evaluate_count=1,file_count=1,open_directions='{}',has_search_all='false',
has_call='false',has_break='false',total_branches=2),
'paragraph_count' not in p and 'IF' in p))
# ── jcl.parser: JCL解析 ──
from jcl.parser import parse_jcl
SAMPLE_JCL = """//CREDIT25 JOB (CRD),'MONTHLY BILLING',CLASS=A,MSGCLASS=X
//STEP1 EXEC PGM=SORT
//SORTIN DD DSN=TRANSACTIONS.DATA,DISP=SHR
//SORTOUT DD DSN=SORTED.DATA,DISP=(NEW,PASS)
//SYSIN DD *
SORT FIELDS=(1,16,CH,A)
//STEP2 EXEC PGM=CRDVAL,COND=(0,NE)
//TRANSIN DD DSN=SORTED.DATA,DISP=(OLD,DELETE)
//MEMBER DD DSN=MEMBER.DATA,DISP=SHR
//VALIDOUT DD DSN=VALID.DATA,DISP=(NEW,CATLG)
//REJECT DD SYSOUT=*
//REPORTERR DD SYSOUT=*
//STEP3 EXEC PGM=CRDCALC,COND=(0,NE)
//VALIDIN DD DSN=VALID.DATA,DISP=(OLD,DELETE)
//RATE DD DSN=RATE.DATA,DISP=SHR
//CALCOUT DD DSN=CALC.DATA,DISP=(NEW,CATLG)
//STEP4 EXEC PGM=CRDRPT,COND=(0,NE)
//BILLING DD DSN=CALC.DATA,DISP=(OLD,DELETE)
//STMT DD DSN=STMT.DATA,DISP=(NEW,CATLG)
//SUMMARY DD DSN=SUMMARY.DATA,DISP=(NEW,CATLG)
// DD SYSOUT=*
"""
do('JCL','parse_jcl 4STEP解析', lambda: (
j:=parse_jcl(SAMPLE_JCL),
len(j.steps)==4))
do('JCL','JOB情報解析', lambda: (
j:=parse_jcl(SAMPLE_JCL),
j.job_name=='CREDIT25' and j.job_class=='A'))
do('JCL','STEP1:SORT PGM定義', lambda: (
j:=parse_jcl(SAMPLE_JCL),
j.steps[0].program=='SORT' and j.steps[0].step_name=='STEP1'))
do('JCL','DD定義:入力ファイル', lambda: (
j:=parse_jcl(SAMPLE_JCL),
any('TRANSACTIONS' in d.dsn for d in j.steps[0].dd_list)))
do('JCL','DD定義:出力ファイル', lambda: (
j:=parse_jcl(SAMPLE_JCL),
any('VALID.DATA' in d.dsn for d in j.steps[1].dd_list)))
do('JCL','CONDパラメータ', lambda: (
j:=parse_jcl(SAMPLE_JCL),
j.steps[1].cond is not None and '0' in str(j.steps[1].cond)))
do('JCL','SYSINインラインデータ', lambda: (
j:=parse_jcl(SAMPLE_JCL),
len(j.steps[0].sysin_lines)>0 and 'SORT' in j.steps[0].sysin_lines[0]))
do('JCL','SYSOUT出力', lambda: (
j:=parse_jcl(SAMPLE_JCL),
any('*' in d.dsn for d in j.steps[1].dd_list)))
do('JCL','空JCL', lambda: (
j:=parse_jcl(''),
len(j.steps)==0))
do('JCL','コメント行スキップ', lambda: (
j:=parse_jcl('//* THIS IS COMMENT\n//STEP1 EXEC PGM=TEST\n'),
len(j.steps)==1 and j.steps[0].program=='TEST'))
# ── jcl.executor ──
from jcl.executor import JclExecutor, CondEvaluator
do('JEX','CondEvaluator: (0,NE)', lambda: (
CondEvaluator().evaluate('(0,NE)', 0)==False))
do('JEX','CondEvaluator: (0,NE) RC=4', lambda: (
CondEvaluator().evaluate('(0,NE)', 4)==True))
do('JEX','CondEvaluator: (0,GT) RC=0', lambda: (
CondEvaluator().evaluate('(0,GT)', 0)==False))
do('JEX','CondEvaluator: (0,GT) RC=4', lambda: (
CondEvaluator().evaluate('(0,GT)', 4)==True))
do('JEX','CondEvaluator: (4,LE) RC=4', lambda: (
CondEvaluator().evaluate('(4,LE)', 4)==True))
do('JEX','CondEvaluator: (4,LE) RC=8', lambda: (
CondEvaluator().evaluate('(4,LE)', 8)==False))
do('JEX','CondEvaluator: EVEN', lambda: (
CondEvaluator().evaluate('EVEN', 0)==True))
do('JEX','CondEvaluator: ONLY', lambda: (
CondEvaluator().evaluate('ONLY', 0)==True))
do('JEX','CondEvaluator: 空文字列', lambda: (
CondEvaluator().evaluate('', 0)==None))
do('JEX','JclExecutor インスタンス', lambda: (
e:=JclExecutor(),
hasattr(e,'execute_step')))
do('JEX','DD→環境変数マッピング', lambda: (
e:=JclExecutor(),
m:=e._build_env({'TRANSIN':'/data/in.dat','VALIDOUT':'/data/out.dat'}),
'TRANSIN' in m and 'VALIDOUT' in m))
# ── quality モジュール ──
from quality.l1_offset_validate import L1OffsetValidator
from quality.l2_value_roundtrip import L2RoundtripValidator
do('QLT','L1OffsetValidator インスタンス', lambda: (
v:=L1OffsetValidator(),
hasattr(v,'validate')))
do('QLT','L2RoundtripValidator インスタンス', lambda: (
v:=L2RoundtripValidator(),
hasattr(v,'validate')))
# ── HINA gate: エッジケース ──
from hina.gate import check as gate_check, _compute_score
do('QG','スコア上限=1.0', lambda: _compute_score({'branch_rate':1.0,'paragraph_rate':1.0},{})<=1.0)
do('QG','スコア下限=0.4', lambda: _compute_score({'branch_rate':0.0,'paragraph_rate':0.0},{})>=0.4)
do('QG','境界:分岐率0.8999→不合格', lambda: (
r:=gate_check([{'x':1}],{},{'branch_rate':0.8999,'paragraph_rate':1.0,'uncovered_decision_ids':[]}),
not r['passed']))
do('QG','境界:分岐率0.9→合格', lambda: (
r:=gate_check([{'x':1}],{},{'branch_rate':0.9,'paragraph_rate':1.0,'uncovered_decision_ids':[]}),
r['passed']))
do('QG','issue:段落不足のみ', lambda: (
r:=gate_check([{'x':1}],{},{'branch_rate':1.0,'paragraph_rate':0.5,'uncovered_decision_ids':[]}),
not r['passed'] and 'paragraph_gaps' in r['issues']))
# ── 集計 ──
print(); [print(l) for l in LOG]
total=PASS+FAIL
print(f'\n{"="*67}')
print(f' Gap Coverage Test Results')
print(f' Total: {total} | PASS: {PASS} | FAIL: {FAIL} | RATE: {PASS/max(total,1)*100:.1f}%')
print(f' Untested modules covered: hina.hina_agent ✅ jcl.parser ✅ jcl.executor ✅')
print(f'{"="*67}')
sys.exit(0 if FAIL==0 else 1)