feat: phase2 review fixes
- TIME injection: change from spaces to '2500' (hour>23) for NUMVAL trigger - Runner: add .resolve() to work_dir to fix chdir+relative path breakage - Coverage: per-target field overrides for DP#9-#12 (START-DATE=20240115 etc.) - .gitignore: add compilation artifacts, temp scripts, test outputs
This commit is contained in:
+164
-40
@@ -160,6 +160,10 @@ class GixsqlOrchestrator:
|
||||
copybook_dirs=[ascii_dir],
|
||||
extra_srcs=extra_srcs,
|
||||
)
|
||||
log_dir = self.runtime_dir / "logs" / "compile"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_dir.joinpath(f"{self.program_id}.log").write_text(
|
||||
result.log, encoding='utf-8')
|
||||
if result.success:
|
||||
self.exe_path = Path(result.exe_path)
|
||||
return DbPipelineResult(
|
||||
@@ -303,10 +307,70 @@ class GixsqlOrchestrator:
|
||||
if len(parts) == 2:
|
||||
rec['R01LINE'] = f"{dup_eid.ljust(8)},{parts[1]}"
|
||||
|
||||
# ── Coverage-driven data modifications (per-scenario) ──
|
||||
# Normal scenario or legacy single-run: no modifications needed.
|
||||
# Collision scenario: INSERT duplicate, OVT-MONTHLY match, COMMIT threshold.
|
||||
# Abnormal scenario: orphan cancel ABEND (last, to avoid polluting other branches).
|
||||
|
||||
apply_collision = scenario is not None and scenario.id == "collision"
|
||||
apply_abnormal = scenario is not None and scenario.id == "abnormal"
|
||||
|
||||
if apply_collision:
|
||||
# #10 T: Ensure >= 50 R01 records for COMMIT threshold (CNS-COMMIT-CNT=50)
|
||||
r01_recs = [r for r in recs if 'R01APPL-ID' in r]
|
||||
r01_count = len(r01_recs)
|
||||
if r01_count < 50:
|
||||
template = r01_recs[-1].copy() if r01_recs else {}
|
||||
needed = 50 - r01_count
|
||||
for i in range(needed):
|
||||
nr = {}
|
||||
for key, val in template.items():
|
||||
if not key.startswith('R02'):
|
||||
nr[key] = val
|
||||
nr['R01APPL-ID'] = f"X50{str(i).zfill(5)}"
|
||||
if 'R01EMP-ID' in nr:
|
||||
nr['R01EMP-ID'] = str(int(str(nr.get('R01EMP-ID', '0') or '0')) + i + 10000).zfill(8)
|
||||
recs.append(nr)
|
||||
r01_recs = [r for r in recs if 'R01APPL-ID' in r]
|
||||
logger.info(f" Coverage #10T: added {needed} R01-only records -> {len(r01_recs)} total")
|
||||
|
||||
# #8 T: Two R01 records with same APPL-ID -> 2nd INSERT collides -> UPDATE
|
||||
if len(r01_recs) >= 4:
|
||||
dup_appl_id = 'COLISN01'
|
||||
for idx in (2, 3):
|
||||
r01_recs[idx]['R01APPL-ID'] = dup_appl_id
|
||||
if 'R02APPL-ID' in r01_recs[idx]:
|
||||
r01_recs[idx]['R02APPL-ID'] = dup_appl_id
|
||||
logger.info(f" Coverage #8T: set APPL-ID={dup_appl_id} on records [2]&[3] for INSERT duplicate")
|
||||
|
||||
# #11 T: Two R01 records with same (EMP-ID, APPL-DATE, OVT-TYPE)
|
||||
if len(r01_recs) >= 2:
|
||||
match_emp = r01_recs[1].get('R01EMP-ID', '00000000').strip() or '00000000'
|
||||
match_date = r01_recs[1].get('R01APPL-DATE', '00000000').strip() or '00000000'
|
||||
match_type = r01_recs[1].get('R01OVT-TYPE', '1').strip() or '1'
|
||||
r01_recs[0]['R01EMP-ID'] = match_emp
|
||||
r01_recs[0]['R01APPL-DATE'] = match_date
|
||||
r01_recs[0]['R01OVT-TYPE'] = match_type
|
||||
r01_recs[1]['R01EMP-ID'] = match_emp
|
||||
r01_recs[1]['R01APPL-DATE'] = match_date
|
||||
r01_recs[1]['R01OVT-TYPE'] = match_type
|
||||
logger.info(
|
||||
f" Coverage #11T: unified (EMP={match_emp} DATE={match_date}"
|
||||
f" TYPE={match_type}) for R01 records [0]&[1]"
|
||||
)
|
||||
|
||||
if apply_abnormal:
|
||||
# #14 T: Last R02 record has non-existent APPL-ID -> orphan cancel ABEND
|
||||
# NOTE: ABEND prevents 3000STPSOR (#19 T/F); covered by normal scenario.
|
||||
r02_recs = [r for r in recs if 'R02APPL-ID' in r]
|
||||
if r02_recs:
|
||||
r02_recs[-1]['R02APPL-ID'] = 'ZZZZZZZZ'
|
||||
logger.info(f" Coverage #14T: set last R02 APPL-ID='ZZZZZZZZ' for orphan cancel")
|
||||
|
||||
# 出力先ディレクトリ(シナリオ毎に分離)
|
||||
run_label = f"run_{scenario.id}" if scenario else ""
|
||||
output_root = self.work_dir / run_label if scenario else self.work_dir
|
||||
input_dir = output_root / "input"
|
||||
input_dir = output_root / "main" / "input"
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# フラットファイル書き出し(全シナリオ同一)
|
||||
@@ -404,7 +468,7 @@ class GixsqlOrchestrator:
|
||||
sql_meta, declared_columns, records=recs)
|
||||
|
||||
# Write main JSON(シナリオ毎に分離)
|
||||
json_outdir = output_root / "json"
|
||||
json_outdir = output_root / "main" / "json"
|
||||
json_outdir.mkdir(parents=True, exist_ok=True)
|
||||
json_path = json_outdir / f"{self.program_id}.json"
|
||||
output_json(json_records, json_path, roles,
|
||||
@@ -443,8 +507,8 @@ class GixsqlOrchestrator:
|
||||
# シナリオ毎の出力先
|
||||
run_label = f"run_{scenario.id}" if scenario else ""
|
||||
run_dir = self.runtime_dir / run_label if scenario else self.runtime_dir
|
||||
input_dir = run_dir / "input"
|
||||
output_dir = run_dir / "output"
|
||||
input_dir = run_dir / "main" / "input"
|
||||
output_dir = run_dir / "main" / "output"
|
||||
gcov_dir = self.runtime_dir / "gcov"
|
||||
input_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -453,17 +517,17 @@ class GixsqlOrchestrator:
|
||||
# シナリオ毎の CWD = run_{id}/、単輪時は runtime_dir 直下
|
||||
cwd = run_dir
|
||||
|
||||
# 入力ファイル(work_dir/run_{id}/input/ → runtime/run_{id}/input/)
|
||||
gen_input_dir = self.work_dir / f"run_{scenario.id}" / "input" if scenario else self.work_dir / "input"
|
||||
# 入力ファイル(work_dir/run_{id}/main/input/ → runtime/run_{id}/main/input/)
|
||||
gen_input_dir = self.work_dir / f"run_{scenario.id}" / "main" / "input" if scenario else self.work_dir / "main" / "input"
|
||||
if gen_input_dir.exists():
|
||||
for f in gen_input_dir.iterdir():
|
||||
if f.is_file():
|
||||
(input_dir / f.name).write_bytes(f.read_bytes())
|
||||
|
||||
# JSON 出力(work_dir/run_{id}/json/ → runtime/run_{id}/json/)
|
||||
gen_json_dir = self.work_dir / f"run_{scenario.id}" / "json" if scenario else self.work_dir / "json"
|
||||
# JSON 出力(work_dir/run_{id}/main/json/ → runtime/run_{id}/main/json/)
|
||||
gen_json_dir = self.work_dir / f"run_{scenario.id}" / "main" / "json" if scenario else self.work_dir / "main" / "json"
|
||||
if gen_json_dir.exists():
|
||||
json_dir = run_dir / "json"
|
||||
json_dir = run_dir / "main" / "json"
|
||||
json_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in gen_json_dir.iterdir():
|
||||
if f.is_file() and f.suffix.lower() == '.json':
|
||||
@@ -474,9 +538,9 @@ class GixsqlOrchestrator:
|
||||
env_overrides = {}
|
||||
for fname, direction in assign_map.items():
|
||||
if direction == "INPUT":
|
||||
env_overrides[fname] = os.path.join("input", fname)
|
||||
env_overrides[fname] = os.path.join("main", "input", fname)
|
||||
else:
|
||||
env_overrides[fname] = os.path.join("output", fname)
|
||||
env_overrides[fname] = os.path.join("main", "output", fname)
|
||||
|
||||
# シナリオ毎の DB パス
|
||||
db_path = self._current_db_path or self.db_path
|
||||
@@ -501,6 +565,11 @@ class GixsqlOrchestrator:
|
||||
env_overrides=env_overrides,
|
||||
)
|
||||
|
||||
log_dir = self.runtime_dir / "logs"
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
log_dir.joinpath(f"{run_label or self.program_id}.log").write_text(
|
||||
result.log, encoding='utf-8')
|
||||
|
||||
# .gcda を gcov/ にコピー(シナリオ毎に gcov/run_{id}/)
|
||||
# GnuCOBOL は .gcno が生成された CWD (= compile CWD = exe_dir) に .gcda を書き出す。
|
||||
# 複数シナリオで .gcno は共有されるため COPY で行う(MOVE 不可)。
|
||||
@@ -512,13 +581,22 @@ class GixsqlOrchestrator:
|
||||
gcda_src_dirs.append(self.runtime_dir) # 従来互換
|
||||
gcda_dst_dir = gcov_dir / run_label if scenario else gcov_dir
|
||||
gcda_dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
for ext in (".gcda", ".gcno"):
|
||||
for sd in gcda_src_dirs:
|
||||
for f in sd.glob(f"*{ext}"):
|
||||
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:
|
||||
for sd in gcda_src_dirs:
|
||||
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:
|
||||
try:
|
||||
shutil.copy2(str(f), str(dst))
|
||||
except PermissionError:
|
||||
pass
|
||||
for f in sd.glob("*.gcno"):
|
||||
if f.is_file() and f.stat().st_size > 0:
|
||||
dst = gcda_dst_dir / f.name
|
||||
try:
|
||||
shutil.copy2(str(f), str(dst))
|
||||
except PermissionError:
|
||||
pass
|
||||
|
||||
return DbPipelineResult(
|
||||
self.program_id, 3, result.success,
|
||||
@@ -600,12 +678,21 @@ class GixsqlOrchestrator:
|
||||
v3_root = Path(__file__).parent
|
||||
extra_search = list(gcov_dir.glob("run_*")) + [self.work_dir / "bin"]
|
||||
for search_dir in (v3_root, self.work_dir, self.runtime_dir, gcov_dir, Path.home(), *extra_search):
|
||||
for ext in (".gcno", ".gcda"):
|
||||
for f in search_dir.glob(f"*{ext}"):
|
||||
if f.stat().st_size > 0:
|
||||
dst = gcov_dir / f.name
|
||||
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
|
||||
for f in search_dir.glob("*.gcda"):
|
||||
if f.stat().st_size > 0:
|
||||
dst = gcov_dir / f.name
|
||||
if not dst.exists() or f.stat().st_mtime > dst.stat().st_mtime:
|
||||
try:
|
||||
shutil.copy2(str(f), str(dst))
|
||||
except PermissionError:
|
||||
pass
|
||||
for f in search_dir.glob("*.gcno"):
|
||||
if f.stat().st_size > 0:
|
||||
dst = gcov_dir / f.name
|
||||
try:
|
||||
shutil.copy2(str(f), str(dst))
|
||||
except PermissionError:
|
||||
pass
|
||||
# Count what we have
|
||||
gcno_gcda_count = 0
|
||||
for ext in (".gcno", ".gcda"):
|
||||
@@ -915,26 +1002,37 @@ class GixsqlOrchestrator:
|
||||
select_to_file[sel_name] = fname
|
||||
assign_map[fname] = "UNKNOWN"
|
||||
|
||||
# Second pass: determine direction from OPEN statements
|
||||
# Handle both simple (OPEN INPUT X) and compound (OPEN INPUT X OUTPUT Y)
|
||||
# Second pass: determine direction from OPEN statements.
|
||||
# COBOL allows multi-line OPEN where files listed without a direction
|
||||
# keyword inherit the last stated direction:
|
||||
# OPEN INPUT FILEA
|
||||
# FILEB <-- inherits INPUT
|
||||
# OUTPUT FILEC
|
||||
# Strategy: extract OPEN body (up to terminating '.'), collapse
|
||||
# whitespace, then parse direction→file pairs via splitting on
|
||||
# direction keyword boundaries.
|
||||
for m in re.finditer(
|
||||
r'OPEN\s+((?:INPUT|OUTPUT|I-O|EXTEND)\s+\w+)'
|
||||
r'((?:\s+(?:INPUT|OUTPUT|I-O|EXTEND)\s+\w+)*)',
|
||||
src_text, re.IGNORECASE
|
||||
r'OPEN\s+(.+?)\.', src_text, re.IGNORECASE | re.DOTALL
|
||||
):
|
||||
# Parse the OPEN payload: "INPUT X" + " OUTPUT Y"
|
||||
payload = m.group(1) + m.group(2)
|
||||
for part in re.finditer(
|
||||
r'(INPUT|OUTPUT|I-O|EXTEND)\s+(\w+)', payload, re.IGNORECASE
|
||||
):
|
||||
direction = part.group(1).upper()
|
||||
sel_name = part.group(2)
|
||||
if sel_name in select_to_file:
|
||||
fname = select_to_file[sel_name]
|
||||
if direction in ("INPUT", "I-O"):
|
||||
assign_map[fname] = "INPUT"
|
||||
else:
|
||||
assign_map[fname] = "OUTPUT"
|
||||
full = re.sub(r'\s+', ' ', m.group(1)).strip()
|
||||
# Split on direction keyword boundaries: "INPUT X Y OUTPUT Z"
|
||||
# → ["INPUT X Y", "OUTPUT Z"]
|
||||
tokens = re.split(r'\s+(?=(?:INPUT|OUTPUT|I-O|EXTEND)\s)', full, flags=re.IGNORECASE)
|
||||
for seg in tokens:
|
||||
seg = seg.strip()
|
||||
if not seg:
|
||||
continue
|
||||
seg_m = re.match(r'(INPUT|OUTPUT|I-O|EXTEND)\s+([\w ]+)', seg, re.IGNORECASE)
|
||||
if not seg_m:
|
||||
continue
|
||||
direction = seg_m.group(1).upper()
|
||||
for fword in re.findall(r'\w+', seg_m.group(2)):
|
||||
if fword in select_to_file:
|
||||
fname = select_to_file[fword]
|
||||
if direction in ("INPUT", "I-O"):
|
||||
assign_map[fname] = "INPUT"
|
||||
else:
|
||||
assign_map[fname] = "OUTPUT"
|
||||
|
||||
return assign_map
|
||||
|
||||
@@ -1025,6 +1123,32 @@ class GixsqlOrchestrator:
|
||||
if not rows:
|
||||
logger.info(f" Table {table_name}: 0 initial rows (will be created at runtime)")
|
||||
continue
|
||||
|
||||
# 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
|
||||
try:
|
||||
pragma_cols = conn.execute(
|
||||
f"PRAGMA table_info([{table_name}])"
|
||||
).fetchall()
|
||||
valid_cols = {r[1].upper() for r in pragma_cols}
|
||||
except Exception:
|
||||
valid_cols = set()
|
||||
|
||||
remapped_rows = []
|
||||
for row in rows:
|
||||
new_row = {}
|
||||
for k, v in row.items():
|
||||
if k.upper() in valid_cols:
|
||||
new_row[k] = v
|
||||
if new_row:
|
||||
remapped_rows.append(new_row)
|
||||
rows = remapped_rows
|
||||
if not rows:
|
||||
logger.info(f" Table {table_name}: all rows filtered out, skipping")
|
||||
continue
|
||||
|
||||
col_names = list(rows[0].keys())
|
||||
placeholders = ", ".join("?" for _ in col_names)
|
||||
quoted_cols = ", ".join(f"[{c}]" for c in col_names)
|
||||
|
||||
Reference in New Issue
Block a user