feat: config-driven multi-scenario + gcda accumulation fix + gcov merge fix
Config-driven architecture: - coverage_dates: YAML-driven date replacement for LEAVE_RECORDS - row_overrides: per-scenario field overrides (e.g. STATUS='9') - delete_all_rows: per-scenario table emptying for empty-cursor tests gcda accumulation fix (V3 bug #7): - Delete .gcda before each scenario run to prevent GnuCOBOL accumulation - Force-copy scenario gcda (skip mtime check) - Copy scenario DB to CWD data/kin.db for CONNECT TO path resolution Multi-run gcov merge fix: - Always merge multi-run gcov data regardless of generate_coverage flag - Fix v3_root UnboundLocalError in cleanup path Other fixes: - _make_key_unique: skip WHERE-constrained columns to avoid PK conflict - incremental_supplement: support fields_dict for base record generation - check_coverage: use structure coverage data if available - orchestrator.py: filter _-prefixed fields in TestCase; merge Agent2Data cases
This commit is contained in:
+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