Feat/agent mode #1
@@ -28,7 +28,7 @@ from .core import build_branch_tree, classify_field_roles, _init_child_names, sq
|
||||
from .cond import parse_single_condition, is_field, collect_leaves
|
||||
from .pipeline_bridge import build_branch_tree_fallback
|
||||
from .design_mcdc import enum_paths as mcdc_enum_paths, _filter_stop
|
||||
from .design import enum_paths, generate_records, get_term_type, extend_abend_programs
|
||||
from .design import enum_paths, generate_records, get_term_type, extend_abend_programs, make_base_record
|
||||
from .output import output_json, output_input_files
|
||||
from .coverage import run_coverage, generate_coverage_index, collect_decision_points, mark_coverage
|
||||
from japanese_data import generate_fullwidth_text, generate_halfwidth_katakana, generate_wareki_date
|
||||
@@ -1195,6 +1195,7 @@ def extract_structure(cobol_source: str, copybook_dirs: list = None) -> dict:
|
||||
"if_types": if_types,
|
||||
"variable_patterns": variable_patterns,
|
||||
"open_pattern": open_pattern,
|
||||
"data_fields": fields_dict,
|
||||
}
|
||||
|
||||
|
||||
@@ -1353,12 +1354,13 @@ def generate_data(cobol_source: str, structure: dict = None,
|
||||
return records
|
||||
|
||||
|
||||
def incremental_supplement(branch_tree, decision_gaps: list[int]) -> list[dict]:
|
||||
def incremental_supplement(branch_tree, decision_gaps: list[int], fields_dict: list = None) -> list[dict]:
|
||||
"""针对未覆盖的决策点,增量生成补充测试数据。
|
||||
|
||||
Args:
|
||||
branch_tree: extract_structure() 返回的 branch_tree 字段
|
||||
decision_gaps: 未覆盖的决策点 ID 列表,如 [1, 3, 5]
|
||||
fields_dict: 字段定义列表(DATA DIVISION 展开后),提供后生成含字段值的记录
|
||||
|
||||
Returns:
|
||||
list[dict]: 增量测试数据,格式与 generate_data() 兼容
|
||||
@@ -1390,10 +1392,12 @@ def incremental_supplement(branch_tree, decision_gaps: list[int]) -> list[dict]:
|
||||
|
||||
supplements = []
|
||||
for i, (kind, label) in enumerate(found):
|
||||
supplements.append({
|
||||
"_dec_id": f"incr_{i}",
|
||||
"_kind": kind,
|
||||
"_label": str(label)[:60],
|
||||
})
|
||||
rec = {}
|
||||
if fields_dict:
|
||||
rec = make_base_record(i + 1, fields_dict)
|
||||
rec["_dec_id"] = f"incr_{i}"
|
||||
rec["_kind"] = kind
|
||||
rec["_label"] = str(label)[:60]
|
||||
supplements.append(rec)
|
||||
|
||||
return supplements
|
||||
|
||||
@@ -1358,11 +1358,33 @@ def run_coverage(branch_tree, branch_paths_with_assigns, fields,
|
||||
def check_coverage(structure: dict, test_records: list[dict]) -> dict:
|
||||
total_paragraphs = structure.get("total_paragraphs", 0)
|
||||
total_branches = structure.get("total_branches", 0)
|
||||
decision_points = structure.get("decision_points", [])
|
||||
has_data = len(test_records) > 0
|
||||
|
||||
paragraph_rate = 1.0 if (total_paragraphs > 0 and has_data) else 0.0
|
||||
|
||||
cov = structure.get("coverage", {})
|
||||
if cov and "total" in cov:
|
||||
total = cov["total"]
|
||||
covered = cov["covered"]
|
||||
branch_rate = covered / max(total, 1) if total > 0 else 0.0
|
||||
decision_rate = branch_rate
|
||||
|
||||
uncovered = []
|
||||
for dp in cov.get("decision_points", []):
|
||||
if dp["covered"] < dp["branches"]:
|
||||
uncovered.append(dp["id"])
|
||||
|
||||
return {
|
||||
"paragraph_rate": paragraph_rate,
|
||||
"branch_rate": branch_rate,
|
||||
"decision_rate": decision_rate,
|
||||
"uncovered_decision_ids": uncovered,
|
||||
"total_branches": total,
|
||||
"total_paragraphs": total_paragraphs,
|
||||
"records_count": len(test_records),
|
||||
"note": "静态分析覆盖率(来自 mark_coverage)。精确数据通过 gcov 获取。",
|
||||
}
|
||||
|
||||
return {
|
||||
"paragraph_rate": paragraph_rate,
|
||||
"branch_rate": 0.0,
|
||||
|
||||
@@ -434,12 +434,14 @@ def build_db_input(
|
||||
if sc not in [c['name'] for c in col_infos]:
|
||||
col_infos.append({'name': sc, 'db_type': 'CHAR', 'size': 20})
|
||||
|
||||
where_cols = set()
|
||||
for col_info in col_infos:
|
||||
col_name = col_info['name']
|
||||
val = None
|
||||
for wc in where_cons:
|
||||
if wc['type'] == 'literal' and wc.get('col', '').upper() == col_name:
|
||||
val = wc.get('literal', '')
|
||||
where_cols.add(col_name)
|
||||
break
|
||||
if wc['type'] == 'host_var':
|
||||
hv = wc.get('host_var', '').upper()
|
||||
@@ -473,7 +475,7 @@ def build_db_input(
|
||||
|
||||
if col_infos:
|
||||
first_col = col_infos[0]['name']
|
||||
if first_col in row:
|
||||
if first_col in row and (not where_cols or first_col not in where_cols):
|
||||
row[first_col] = _make_key_unique(row[first_col], path_idx, seen_keys[table])
|
||||
|
||||
db_input[table].append(row)
|
||||
|
||||
@@ -40,6 +40,8 @@ class ScenarioDef:
|
||||
id: str
|
||||
sysin: SysinDef = field(default_factory=SysinDef)
|
||||
inject_duplicate_pk: bool = False
|
||||
row_overrides: dict[str, dict[str, str]] = field(default_factory=dict)
|
||||
delete_all_rows: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -50,6 +52,7 @@ class ProgramSchema:
|
||||
db_type: str = "SQLite"
|
||||
db_name: str = "OVERTIME.DB"
|
||||
runs: list[ScenarioDef] = field(default_factory=list)
|
||||
coverage_dates: dict[str, list[dict[str, str]]] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_yaml(cls, path: str | Path) -> ProgramSchema:
|
||||
@@ -71,6 +74,8 @@ class ProgramSchema:
|
||||
id=r["id"],
|
||||
sysin=SysinDef(**sysin_raw),
|
||||
inject_duplicate_pk=r.get("inject_duplicate_pk", False),
|
||||
row_overrides=r.get("row_overrides", {}),
|
||||
delete_all_rows=r.get("delete_all_rows", False),
|
||||
))
|
||||
return cls(
|
||||
program_id=raw["program_id"],
|
||||
@@ -79,6 +84,7 @@ class ProgramSchema:
|
||||
db_type=raw.get("db_type", "SQLite"),
|
||||
db_name=raw.get("db_name", "OVERTIME.DB"),
|
||||
runs=runs,
|
||||
coverage_dates=raw.get("coverage_dates", {}),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -35,3 +35,47 @@ subprograms:
|
||||
- SUB01DAT
|
||||
- SUB02MSG
|
||||
- SUB03END
|
||||
|
||||
coverage_dates:
|
||||
LEAVE_RECORDS:
|
||||
- start: "20260701"
|
||||
end: "20260701"
|
||||
emp: "00000001"
|
||||
- start: "20260702"
|
||||
end: "20260702"
|
||||
emp: "00000001"
|
||||
- start: "20260704"
|
||||
end: "20260704"
|
||||
emp: "00000001"
|
||||
- start: "20260705"
|
||||
end: "20260705"
|
||||
emp: "00000001"
|
||||
- start: "20260131"
|
||||
end: "20260203"
|
||||
emp: "00000002"
|
||||
- start: "20260430"
|
||||
end: "20260502"
|
||||
emp: "00000002"
|
||||
- start: "20240228"
|
||||
end: "20240302"
|
||||
emp: "00000003"
|
||||
- start: "20230228"
|
||||
end: "20230302"
|
||||
emp: "00000003"
|
||||
- start: "20240229"
|
||||
end: "20240302"
|
||||
emp: "00000004"
|
||||
- start: "20230227"
|
||||
end: "20230302"
|
||||
emp: "00000004"
|
||||
- start: "20261230"
|
||||
end: "20270101"
|
||||
emp: "00000099"
|
||||
|
||||
runs:
|
||||
- id: normal
|
||||
- id: empty_cursor
|
||||
delete_all_rows: true
|
||||
row_overrides:
|
||||
LEAVE_RECORDS:
|
||||
STATUS: "9"
|
||||
|
||||
+13
-4
@@ -49,7 +49,8 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
||||
# 转换为 TestCase 列表(增强管线的基础数据集)
|
||||
complete_tests = []
|
||||
for i, rec in enumerate(base_records):
|
||||
complete_tests.append(TestCase(id=f"CTG-{i+1:04d}", fields=dict(rec)))
|
||||
fields = {k: v for k, v in rec.items() if not k.startswith('_')}
|
||||
complete_tests.append(TestCase(id=f"CTG-{i+1:04d}", fields=fields))
|
||||
|
||||
# HINA 完整类型判定管道(Keyword / 规则引擎 / LLM 辅助三路径)
|
||||
classification: dict = {}
|
||||
@@ -84,13 +85,15 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
||||
break
|
||||
gaps = gate_result.get("issues", {}).get("decision_gaps", [])
|
||||
if gaps and structure.get("branch_tree_obj"):
|
||||
delta = incremental_supplement(structure["branch_tree_obj"], gaps)
|
||||
data_fields = structure.get("data_fields", [])
|
||||
delta = incremental_supplement(structure["branch_tree_obj"], gaps, data_fields)
|
||||
base_records.extend(delta)
|
||||
# 同步更新 complete_tests
|
||||
for i, d in enumerate(delta):
|
||||
d_fields = {k: v for k, v in d.items() if not k.startswith('_')}
|
||||
complete_tests.append(TestCase(
|
||||
id=f"CTG-S{attempt+1}-{i+1:04d}",
|
||||
fields=dict(d),
|
||||
fields=d_fields,
|
||||
))
|
||||
cov = check_coverage(structure, base_records)
|
||||
else:
|
||||
@@ -109,7 +112,13 @@ def run_pipeline(cfg: Config, cpath: str, cbl: str, java: str, map_path: str) ->
|
||||
|
||||
suite = Agent2Data(llm).design(tree, cfg.coverage_default, cfg.runner_mode == "spark")
|
||||
vr.llm_cost += 0.002
|
||||
suite.test_cases = complete_tests # 替换为增强管线数据(P1/P2 修复)
|
||||
# 合并: cobol_testgen 基线 + 策略标记 + Agent2Data LLM 场景数据
|
||||
merged = list(complete_tests)
|
||||
existing_ids = {tc.id for tc in merged}
|
||||
for tc in suite.test_cases:
|
||||
if tc.id not in existing_ids:
|
||||
merged.append(tc)
|
||||
suite.test_cases = merged
|
||||
vr.debug["test_cases"] = [{"id":tc.id,"fields":tc.fields,"targets":tc.coverage_targets} for tc in suite.test_cases]
|
||||
vr.debug["spark_config"] = {"records":suite.spark_config.num_records} if suite.has_spark else None
|
||||
|
||||
|
||||
+86
-8
@@ -234,7 +234,7 @@ class GixsqlOrchestrator:
|
||||
self._init_database(db_path)
|
||||
|
||||
# DB 初期行投入(DELETE/UPDATE が作用する行、SELECT が返す行)
|
||||
self._populate_database(db_path, src_text, recs)
|
||||
self._populate_database(db_path, src_text, recs, scenario=scenario)
|
||||
# P5: inject duplicate-PK rows (scenario で制御)
|
||||
if scenario is None or scenario.inject_duplicate_pk:
|
||||
self._inject_sql_error_rows(db_path, recs)
|
||||
@@ -551,8 +551,23 @@ class GixsqlOrchestrator:
|
||||
self.db_path.unlink()
|
||||
shutil.copy2(str(db_path), str(self.db_path))
|
||||
db_path = self.db_path
|
||||
# CONNECT TO 'data/kin.db' のパス解釈に備え CWD にもコピー
|
||||
if scenario is not None:
|
||||
cwd_data = cwd / "data"
|
||||
cwd_data.mkdir(parents=True, exist_ok=True)
|
||||
cwd_db = cwd_data / "kin.db"
|
||||
if cwd_db.exists():
|
||||
cwd_db.unlink()
|
||||
shutil.copy2(str(db_path), str(cwd_db))
|
||||
|
||||
# .gcda は CWD(= run_dir)に書き出されるので、実行後に gcov/run_{id}/ に移動する
|
||||
# 各シナリオ実行前に前回の .gcda を削除(GnuCOBOL は累積書込みを行うため)
|
||||
exe_dir_for_gcda = self.work_dir / "bin"
|
||||
for f in exe_dir_for_gcda.glob("*.gcda"):
|
||||
try:
|
||||
f.unlink()
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
# Subprogram DLLs
|
||||
cobol_bin = Path(self.cobol_src_dir).parent / "bin"
|
||||
@@ -585,7 +600,7 @@ class GixsqlOrchestrator:
|
||||
for f in sd.glob("*.gcda"):
|
||||
if f.is_file() and f.stat().st_size > 0:
|
||||
dst = gcda_dst_dir / f.name
|
||||
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
|
||||
if scenario or not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
|
||||
try:
|
||||
shutil.copy2(str(f), str(dst))
|
||||
except PermissionError:
|
||||
@@ -772,7 +787,8 @@ class GixsqlOrchestrator:
|
||||
generate_coverage_index([cov_result], str(output_dir.parent))
|
||||
|
||||
# Clean up .gcno/.gcda from v3_root + CWD (avoid accumulation)
|
||||
for clean_dir in (v3_root, Path.cwd()):
|
||||
_v3_root = Path(__file__).parent
|
||||
for clean_dir in (_v3_root, Path.cwd()):
|
||||
if clean_dir == gcov_dir:
|
||||
continue
|
||||
for ext in (".gcno", ".gcda"):
|
||||
@@ -967,12 +983,13 @@ class GixsqlOrchestrator:
|
||||
elif step_num == 6:
|
||||
vr = self.step6_verify()
|
||||
|
||||
# Always merge multi-run gcov data (needed by external coverage report)
|
||||
if is_multi:
|
||||
merged = self._merge_multi_run_gcov()
|
||||
self._multi_run_gcov_data = merged
|
||||
# Optional coverage report (non-blocking)
|
||||
cv_flags = getattr(self.config, 'gixsql_compile_flags', '')
|
||||
if '--coverage' in cv_flags and generate_coverage:
|
||||
if is_multi:
|
||||
merged = self._merge_multi_run_gcov()
|
||||
self._multi_run_gcov_data = merged
|
||||
self.generate_coverage_report()
|
||||
|
||||
vr = VerificationRun(
|
||||
@@ -1061,7 +1078,8 @@ class GixsqlOrchestrator:
|
||||
conn.close()
|
||||
logger.info(f" DB initialized: {db_path}")
|
||||
|
||||
def _populate_database(self, db_path: Path, src_text: str, records: list[dict]):
|
||||
def _populate_database(self, db_path: Path, src_text: str, records: list[dict],
|
||||
scenario: ScenarioDef | None = None):
|
||||
"""テストデータから DB 初期行を生成し挿入する。"""
|
||||
from cobol_testgen.pipeline_bridge import build_branch_tree_fallback
|
||||
from cobol_testgen.read import extract_procedure_division
|
||||
@@ -1118,6 +1136,43 @@ class GixsqlOrchestrator:
|
||||
logger.info(" No DB input rows generated")
|
||||
return
|
||||
|
||||
# -- Coverage-driven data enrichment --
|
||||
# build_db_input generates counter-value dates; replace with valid
|
||||
# YYYYMMDD dates targeting specific uncovered decision branches.
|
||||
# Configurations are from YAML coverage_dates (program-specific).
|
||||
if 'LEAVE_RECORDS' in db_input:
|
||||
lr_rows = db_input['LEAVE_RECORDS']
|
||||
date_cfgs_raw = (self.schema.coverage_dates or {}).get('LEAVE_RECORDS', [])
|
||||
date_cfgs = [
|
||||
(d['start'], d['end'], d['emp'])
|
||||
for d in date_cfgs_raw
|
||||
]
|
||||
for i, row in enumerate(lr_rows):
|
||||
if i < len(date_cfgs):
|
||||
sd, ed, eid = date_cfgs[i]
|
||||
else:
|
||||
sd, ed, eid = ('20260701', '20260703', f'{i+10:08d}')
|
||||
row['START_DATE'] = sd
|
||||
row['END_DATE'] = ed
|
||||
row['EMP_ID'] = eid
|
||||
row['APPLICATION_ID'] = str(i + 1)
|
||||
|
||||
if 'HOLIDAY_CALENDAR' in db_input:
|
||||
hc_rows = db_input['HOLIDAY_CALENDAR']
|
||||
holiday_overrides = ['20260701', '20260715', '20260801',
|
||||
'20260101', '20260501', '20261001']
|
||||
for i, row in enumerate(hc_rows):
|
||||
if i < len(holiday_overrides):
|
||||
row['HOLIDAY_DATE'] = holiday_overrides[i]
|
||||
|
||||
# -- Per-scenario row overrides (from YAML runs[].row_overrides) --
|
||||
if scenario and scenario.row_overrides:
|
||||
for table_name, overrides in scenario.row_overrides.items():
|
||||
if table_name in db_input:
|
||||
for row in db_input[table_name]:
|
||||
for col, val in overrides.items():
|
||||
row[col.upper()] = val
|
||||
|
||||
conn = sqlite3.connect(str(db_path))
|
||||
for table_name, rows in db_input.items():
|
||||
if not rows:
|
||||
@@ -1127,12 +1182,14 @@ class GixsqlOrchestrator:
|
||||
# Debug
|
||||
logger.info(f" Table {table_name}: {len(rows)} rows, cols={list(rows[0].keys()) if rows else []}")
|
||||
|
||||
# Filter columns: only keep those that actually exist in the table
|
||||
# Query DB column types for type-aware value conversion
|
||||
col_types = {}
|
||||
try:
|
||||
pragma_cols = conn.execute(
|
||||
f"PRAGMA table_info([{table_name}])"
|
||||
).fetchall()
|
||||
valid_cols = {r[1].upper() for r in pragma_cols}
|
||||
col_types = {r[1].upper(): r[2].upper() for r in pragma_cols}
|
||||
except Exception:
|
||||
valid_cols = set()
|
||||
|
||||
@@ -1149,12 +1206,33 @@ class GixsqlOrchestrator:
|
||||
logger.info(f" Table {table_name}: all rows filtered out, skipping")
|
||||
continue
|
||||
|
||||
# Convert values to match DB column types
|
||||
for row in rows:
|
||||
for k in list(row.keys()):
|
||||
ct = col_types.get(k.upper(), '')
|
||||
v = row[k]
|
||||
if ct.startswith('INTEGER') or ct in ('INT', 'SMALLINT', 'BIGINT', 'TINYINT'):
|
||||
try:
|
||||
row[k] = str(int(v)) if v and v.strip() else '0'
|
||||
except (ValueError, TypeError):
|
||||
row[k] = '0'
|
||||
elif ct.startswith('DECIMAL') or ct.startswith('NUMERIC') or ct.startswith('FLOAT') or ct.startswith('REAL'):
|
||||
try:
|
||||
row[k] = str(float(v)) if v and v.strip() else '0'
|
||||
except (ValueError, TypeError):
|
||||
row[k] = '0'
|
||||
|
||||
col_names = list(rows[0].keys())
|
||||
placeholders = ", ".join("?" for _ in col_names)
|
||||
quoted_cols = ", ".join(f"[{c}]" for c in col_names)
|
||||
sql = f"INSERT OR IGNORE INTO [{table_name}] ({quoted_cols}) VALUES ({placeholders})"
|
||||
conn.executemany(sql, [tuple(r.get(c, "") for c in col_names) for r in rows])
|
||||
logger.info(f" Table {table_name}: {len(rows)} initial rows inserted")
|
||||
# -- Per-scenario row deletion (e.g. empty cursor scenario) --
|
||||
if scenario and scenario.delete_all_rows:
|
||||
for table_name in db_input.keys():
|
||||
conn.execute(f"DELETE FROM [{table_name}]")
|
||||
logger.info(f" Table {table_name}: all rows deleted (scenario={scenario.id})")
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f" DB populated: {db_path}")
|
||||
|
||||
Reference in New Issue
Block a user