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:
+17
-2
@@ -47,5 +47,20 @@ _test_flatfiles/
|
||||
gixsql/
|
||||
cobol-tna-system/
|
||||
|
||||
# 詳細設計書(設計原稿、公開禁止)
|
||||
詳細設計書/
|
||||
# Compilation artifacts
|
||||
SUB*.c
|
||||
SUB*.c.h
|
||||
SUB*.c.l.h
|
||||
SUB*.i
|
||||
|
||||
# Temp scripts
|
||||
diagnose_*.py
|
||||
|
||||
# Generated reports/documents
|
||||
SUMMARY.md
|
||||
docs/prompt-template.md
|
||||
docs/plans/
|
||||
|
||||
# Test output dirs
|
||||
output/
|
||||
test_output/
|
||||
|
||||
+116
-38
@@ -291,6 +291,78 @@ def _inject_empty_emp_rec(records, fields):
|
||||
logger.info(f" injected empty-EMP-ID record at position 0")
|
||||
|
||||
|
||||
def _inject_c01_coverage_records(records, fields, base_assignments):
|
||||
"""Inject records for uncovered C01 decision points (DP#9-#12 T).
|
||||
|
||||
Takes a base record with valid EMP-ID and all dates valid, then
|
||||
modifies ONE CSV field per copy to trigger a specific SUB04CHK failure.
|
||||
"""
|
||||
if not records or not base_assignments:
|
||||
return
|
||||
|
||||
r01_len = 80
|
||||
for f in fields:
|
||||
if isinstance(f, dict) and f.get('name') == 'R01LINE' and f.get('pic_info'):
|
||||
r01_len = f['pic_info'].get('length', 80)
|
||||
break
|
||||
|
||||
unstring_items = []
|
||||
for tgt, alist in base_assignments.items():
|
||||
for a in alist:
|
||||
if a.get('type') == 'unstring_split' and a.get('source_vars'):
|
||||
unstring_items.append((a.get('index', 0), tgt))
|
||||
if not unstring_items:
|
||||
return
|
||||
unstring_items.sort(key=lambda x: x[0])
|
||||
unstring_fields = [tgt for _, tgt in unstring_items]
|
||||
|
||||
base = None
|
||||
for rec in records:
|
||||
emp = str(rec.get('WRK-CSV-EMP-ID', '')).strip()
|
||||
if emp and emp != '0':
|
||||
base = rec
|
||||
lt = str(rec.get('WRK-CSV-LEAVE-TYPE', '')).strip()
|
||||
if lt and lt != '':
|
||||
break
|
||||
if base is None:
|
||||
return
|
||||
|
||||
# (dp_num, csv_field, invalid_value, extra_valid_fixes)
|
||||
# For DATE fields: SUB04CHK checks month 01-12, day 01-31 → spaces make month=0 < 1 → error
|
||||
# For TIME fields: SUB04CHK checks hour 00-23, minute 00-59 → spaces make NUMVAL=0 → valid!
|
||||
# Need explicit invalid value like '2500' (hour=25 > 23) or '0060' (minute=60 > 59)
|
||||
# For EMP-ID fields: SUB04CHK checks spaces + alpha/digit/special → '00000000' is valid
|
||||
field_invalid = {
|
||||
'WRK-CSV-START-DATE': ' ',
|
||||
'WRK-CSV-START-TIME': '2500',
|
||||
'WRK-CSV-END-DATE': ' ',
|
||||
'WRK-CSV-END-TIME': '2500',
|
||||
}
|
||||
field_sizes = {
|
||||
'WRK-CSV-START-DATE': 8,
|
||||
'WRK-CSV-START-TIME': 4,
|
||||
'WRK-CSV-END-DATE': 8,
|
||||
'WRK-CSV-END-TIME': 4,
|
||||
}
|
||||
targets = [
|
||||
(9, 'WRK-CSV-START-DATE', {'WRK-CSV-START-DATE': ' '}),
|
||||
(10, 'WRK-CSV-START-TIME', {'WRK-CSV-START-TIME': '2500', 'WRK-CSV-START-DATE': '20240115', 'WRK-CSV-END-DATE': '20240115'}),
|
||||
(11, 'WRK-CSV-END-DATE', {'WRK-CSV-END-DATE': ' '}),
|
||||
(12, 'WRK-CSV-END-TIME', {'WRK-CSV-END-TIME': '2500', 'WRK-CSV-START-DATE': '20240115', 'WRK-CSV-END-DATE': '20240115'}),
|
||||
]
|
||||
for dp_num, csv_field, overrides in targets:
|
||||
if csv_field not in base:
|
||||
continue
|
||||
new_rec = dict(base)
|
||||
for k, v in overrides.items():
|
||||
new_rec[k] = v
|
||||
csv_parts = [str(new_rec.get(fname, '')) for fname in unstring_fields]
|
||||
csv_value = ','.join(csv_parts).ljust(r01_len)[:r01_len]
|
||||
new_rec['R01LINE'] = csv_value
|
||||
records.append(new_rec)
|
||||
logger.info(f" injected DP#{dp_num} T coverage record ({csv_field}={repr(overrides.get(csv_field,''))})")
|
||||
|
||||
|
||||
# ── 入口 ──
|
||||
|
||||
def main():
|
||||
@@ -302,7 +374,6 @@ def main():
|
||||
|
||||
do_run = False
|
||||
gcov_mode = False
|
||||
gixsql_mode = False
|
||||
temp_dir = None
|
||||
if '--run' in args:
|
||||
do_run = True
|
||||
@@ -313,9 +384,6 @@ def main():
|
||||
if not _HAVE_RUNNER:
|
||||
logger.warning("--gcov: runner.py not found. Compile/run will be skipped. "
|
||||
"Use --gcov without runner only generates test data + static coverage.")
|
||||
if '--gixsql' in args:
|
||||
gixsql_mode = True
|
||||
args.remove('--gixsql')
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == '--temp-dir':
|
||||
@@ -372,8 +440,16 @@ def main():
|
||||
|
||||
programs = []
|
||||
|
||||
if gixsql_mode:
|
||||
# DB pipeline: GixsqlOrchestrator
|
||||
# ── Auto-route: split DB / non-DB per file ──
|
||||
db_files = []
|
||||
non_db_files = []
|
||||
for f in cobol_files:
|
||||
if 'EXEC SQL' in f.read_text(encoding='utf-8-sig').upper():
|
||||
db_files.append(f)
|
||||
else:
|
||||
non_db_files.append(f)
|
||||
|
||||
if db_files:
|
||||
import sys as _sys
|
||||
_v3_root = str(Path(__file__).parent.parent)
|
||||
if _v3_root not in _sys.path:
|
||||
@@ -381,25 +457,22 @@ def main():
|
||||
from orchestrator_db import GixsqlOrchestrator
|
||||
from config import Config
|
||||
|
||||
config = Config()
|
||||
src_dir = cobol_files[0].parent if cobol_files else Path.cwd()
|
||||
cpy_dirs = [src_dir / '..' / 'cpy']
|
||||
_db_config = Config()
|
||||
_src_dir = cobol_files[0].parent if cobol_files else Path.cwd()
|
||||
_cpy_dirs = [str(_src_dir / '..' / 'cpy')]
|
||||
|
||||
for filepath in cobol_files:
|
||||
for filepath in db_files:
|
||||
pid = filepath.stem
|
||||
prog_outdir = outdir / pid
|
||||
prog_outdir.mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'logs').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'input').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'output').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'json').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'coverage').mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"\n========== DB: {pid} ==========")
|
||||
orch = GixsqlOrchestrator(
|
||||
config=config, program_id=pid,
|
||||
cobol_src_dir=str(src_dir),
|
||||
copybook_dirs=[str(d) for d in cpy_dirs],
|
||||
config=_db_config, program_id=pid,
|
||||
cobol_src_dir=str(_src_dir),
|
||||
copybook_dirs=[str(d) for d in _cpy_dirs],
|
||||
skip_jvm=True,
|
||||
)
|
||||
vr = orch.run_all(generate_coverage=False)
|
||||
@@ -407,38 +480,38 @@ def main():
|
||||
# Copy output files to outdir
|
||||
if orch.runtime_dir.exists():
|
||||
for item in orch.runtime_dir.iterdir():
|
||||
if item.is_file():
|
||||
shutil.copy2(str(item), str(prog_outdir / item.name))
|
||||
if item.name == "gixsql.log":
|
||||
continue
|
||||
dst = prog_outdir / item.name
|
||||
if item.is_dir():
|
||||
shutil.copytree(str(item), str(dst), dirs_exist_ok=True)
|
||||
else:
|
||||
try:
|
||||
shutil.copy2(str(item), str(dst))
|
||||
except PermissionError:
|
||||
logger.warning(f" Skipping locked file: {item.name}")
|
||||
|
||||
logger.info(f" {pid}: rc={vr.exit_code} status={vr.status}")
|
||||
|
||||
# Coverage report (only once, with correct output_dir)
|
||||
if '--coverage' in getattr(config, 'gixsql_compile_flags', ''):
|
||||
# Coverage report
|
||||
if gcov_mode and '--coverage' in getattr(_db_config, 'gixsql_compile_flags', ''):
|
||||
cov_result = orch.generate_coverage_report(output_dir=str(prog_outdir / 'coverage'))
|
||||
if cov_result.success:
|
||||
cv = cov_result.data.get("coverage", "unknown")
|
||||
logger.info(f" Coverage: {cv}")
|
||||
cov_dict = cov_result.data.get("_cov_dict")
|
||||
if cov_dict:
|
||||
# Fix detail_relpath relative to top-level index
|
||||
rel = Path(prog_outdir / 'coverage' / f"{pid}_coverage.html")
|
||||
cov_dict['detail_relpath'] = str(rel.relative_to(outdir).as_posix())
|
||||
programs.append(cov_dict)
|
||||
else:
|
||||
logger.warning(" --coverage not in gixsql_compile_flags; skipping coverage")
|
||||
|
||||
if programs:
|
||||
from cobol_testgen.coverage import generate_coverage_index as _gen_idx
|
||||
_gen_idx(programs, outdir / 'coverage')
|
||||
logger.info(f"\n覆盖率总览:{outdir / 'coverage' / 'index.html'}")
|
||||
return
|
||||
|
||||
for filepath in cobol_files:
|
||||
for filepath in non_db_files:
|
||||
if not filepath.exists():
|
||||
logger.error(f"错误:文件不存在 {filepath}")
|
||||
continue
|
||||
|
||||
source = filepath.read_text(encoding='utf-8')
|
||||
orig_source = source # 用于行号定位(与 gcov 对齐)
|
||||
source = resolve_copybooks(
|
||||
source,
|
||||
str(filepath.parent),
|
||||
@@ -527,9 +600,6 @@ def main():
|
||||
prog_outdir = outdir / filepath.stem
|
||||
prog_outdir.mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'logs').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'input').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'output').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'json').mkdir(parents=True, exist_ok=True)
|
||||
(prog_outdir / 'coverage').mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logger.info(f"\n========== {filepath.name} ==========")
|
||||
@@ -581,6 +651,12 @@ def main():
|
||||
|
||||
skip_path_infos = [p for p in path_infos if _is_skip(p[0])]
|
||||
main_path_infos = [p for p in path_infos if not _is_skip(p[0])]
|
||||
_c01_types = {}
|
||||
for pi in path_infos:
|
||||
wants = tuple(c[3] for c in pi[0] if len(c) == 4 and c[0] == 'C01CHKRRC' and c[1] == '<>' and c[2] == 'ZERO')
|
||||
if wants:
|
||||
_c01_types[wants] = _c01_types.get(wants, 0) + 1
|
||||
logger.info(f" C01 path types: {dict(sorted(_c01_types.items()))}")
|
||||
path_infos = main_path_infos
|
||||
if skip_path_infos:
|
||||
logger.info(f" Skip 路径: {len(skip_path_infos)} 条(将单独生成数据集)")
|
||||
@@ -633,6 +709,8 @@ def main():
|
||||
|
||||
# P4: inject empty EMP-ID record to trigger R01EMP-ID = SPACE path
|
||||
_inject_empty_emp_rec(records, fields_dict)
|
||||
# P5: inject records for uncovered C01 decision points (DP#9-#12 T)
|
||||
_inject_c01_coverage_records(records, fields_dict, assignments)
|
||||
|
||||
if _HAVE_TOSQL:
|
||||
sql_meta = collect_sql_meta(assignments, declared_columns)
|
||||
@@ -643,7 +721,7 @@ def main():
|
||||
else:
|
||||
db_input = None
|
||||
|
||||
outpath = prog_outdir / 'json' / (filepath.stem + '.json')
|
||||
outpath = prog_outdir / 'main' / 'json' / (filepath.stem + '.json')
|
||||
output_json(records, outpath, roles,
|
||||
fd_fields=fd_fields, field_to_fd=field_to_fd,
|
||||
open_dir=open_dir,
|
||||
@@ -653,7 +731,7 @@ def main():
|
||||
|
||||
select_info = parse_file_control(preprocessed)
|
||||
|
||||
output_input_files(records, prog_outdir / 'input', filepath.stem, roles,
|
||||
output_input_files(records, prog_outdir / 'main' / 'input', filepath.stem, roles,
|
||||
fd_fields, field_to_fd, open_dir,
|
||||
term_types=term_types,
|
||||
data_fields=fields_dict, select_info=select_info)
|
||||
@@ -675,13 +753,13 @@ def main():
|
||||
if eof_fd_dir in ('INPUT', 'I-O') and r in ('input', 'inout'):
|
||||
del rec[fname]
|
||||
# 写 Skip JSON
|
||||
skip_outpath = prog_outdir / 'json' / (filepath.stem + '_skip.json')
|
||||
skip_outpath = prog_outdir / 'skip' / 'json' / (filepath.stem + '.json')
|
||||
output_json(skip_records, skip_outpath, roles,
|
||||
fd_fields=fd_fields, field_to_fd=field_to_fd,
|
||||
open_dir=open_dir, term_types=skip_term_types,
|
||||
data_fields=fields_dict)
|
||||
# 写 Skip 输入文件(主 FD 因字段已剥离而不输出)
|
||||
skip_input_dir = prog_outdir / 'input_skip'
|
||||
skip_input_dir = prog_outdir / 'skip' / 'input'
|
||||
output_input_files(skip_records, skip_input_dir,
|
||||
filepath.stem + '_skip', roles,
|
||||
fd_fields, field_to_fd, open_dir,
|
||||
@@ -791,7 +869,7 @@ def main():
|
||||
if dp3_sample:
|
||||
logger.info(f"DEBUG DP#3 other constraints: {sorted(dp3_sample)[:5]}")
|
||||
cov_result = run_coverage(branch_tree, branch_paths_with_assigns, fields_dict,
|
||||
source, cov_prefix, index_relpath='index.html',
|
||||
orig_source, cov_prefix, index_relpath='index.html',
|
||||
gcov_data=gcov_data)
|
||||
programs.append(cov_result)
|
||||
programs[-1]['detail_relpath'] = f'{filepath.stem}/coverage/{filepath.stem}_coverage.html'
|
||||
|
||||
@@ -154,6 +154,7 @@ class _BrParser:
|
||||
self.assignments = assignments if assignments is not None else {}
|
||||
self.fields = fields
|
||||
self._goto_depth = goto_depth
|
||||
self._cursors = {}
|
||||
|
||||
def peek(self):
|
||||
if self.pos < len(self.lines):
|
||||
@@ -1311,6 +1312,64 @@ class _BrParser:
|
||||
self.assignments.setdefault(synthetic, []).append(info)
|
||||
return Assign(synthetic, info)
|
||||
|
||||
# 4a) DECLARE CURSOR ... FOR SELECT ... FROM ...
|
||||
m = re.search(
|
||||
r'DECLARE\s+(\w[\w-]*)\s+CURSOR\s+FOR\s+SELECT\s+(.*?)\s+FROM\s+(\w[\w-]*)\s*(.*)',
|
||||
sql_text, re.IGNORECASE
|
||||
)
|
||||
if m:
|
||||
cursor_name = m.group(1).upper()
|
||||
select_list = m.group(2).strip()
|
||||
from_table = m.group(3).strip().upper()
|
||||
remaining = m.group(4).strip()
|
||||
|
||||
where_clause = ''
|
||||
wm = self._RE_WHERE.search(remaining)
|
||||
if wm:
|
||||
where_clause = wm.group(1).strip()
|
||||
|
||||
info = {
|
||||
'type': 'exec_sql_select',
|
||||
'cursor_name': cursor_name,
|
||||
'table': from_table,
|
||||
'select_list': select_list,
|
||||
'into_vars': [],
|
||||
'where': where_clause,
|
||||
'sql_text': sql_text,
|
||||
}
|
||||
synthetic = f'__SQL_CURSOR_{from_table}'
|
||||
self.assignments.setdefault(synthetic, []).append(info)
|
||||
self._cursors[cursor_name] = synthetic
|
||||
return Assign(synthetic, info)
|
||||
|
||||
# 4b) FETCH ... INTO ...
|
||||
m = re.search(r'FETCH\s+(\w[\w-]*)\s+INTO\s+(.+)', sql_text, re.IGNORECASE)
|
||||
if m:
|
||||
cursor_name = m.group(1).upper()
|
||||
into_raw = m.group(2).strip()
|
||||
into_vars = []
|
||||
for v in re.split(r'\s*,\s*', into_raw):
|
||||
v = v.strip().lstrip(':')
|
||||
parts = v.split(':')
|
||||
into_vars.append(parts[0].upper())
|
||||
|
||||
# Merge INTO vars into the corresponding DECLARE CURSOR entry
|
||||
syn_key = self._cursors.get(cursor_name)
|
||||
if syn_key:
|
||||
for existing in self.assignments.get(syn_key, []):
|
||||
if existing.get('cursor_name') == cursor_name:
|
||||
existing['into_vars'] = into_vars
|
||||
|
||||
info = {
|
||||
'type': 'exec_sql_fetch',
|
||||
'cursor_name': cursor_name,
|
||||
'into_vars': into_vars,
|
||||
'sql_text': sql_text,
|
||||
}
|
||||
for var in into_vars:
|
||||
self.assignments.setdefault(var, []).append(info)
|
||||
return Assign(into_vars[0], info)
|
||||
|
||||
# 4) UPDATE table SET ... WHERE ...
|
||||
m = self._RE_SQL_UPDATE.search(sql_text)
|
||||
if m:
|
||||
|
||||
@@ -373,6 +373,11 @@ def _mark_search(dp, cons, fields=None):
|
||||
if base_c == base_cond:
|
||||
branch_masks[i] = True
|
||||
break
|
||||
# Also check reversed constraints (subject field references table element)
|
||||
base_c2 = re.sub(r'\s*\(.*?\)\s*$', '', str(c[2]))
|
||||
if base_c2 == base_cond:
|
||||
branch_masks[i] = True
|
||||
break
|
||||
else:
|
||||
leaves = list(collect_leaves(cond_tree))
|
||||
assignment = {}
|
||||
@@ -458,15 +463,26 @@ def locate_decision_lines(decision_points, raw_source):
|
||||
for dp in decision_points:
|
||||
patterns = _build_search_patterns(dp)
|
||||
start = used_indices.get(dp.label, -1) + 1
|
||||
found = False
|
||||
for i in range(start, len(lines)):
|
||||
line = lines[i]
|
||||
for pat in patterns:
|
||||
if re.search(pat, line):
|
||||
dp.source_line = i + 1
|
||||
used_indices[dp.label] = i
|
||||
found = True
|
||||
break
|
||||
if dp.source_line:
|
||||
if found:
|
||||
break
|
||||
# Multi-line fallback: compound conditions (AND/OR) may span >1 line
|
||||
if not found and dp.kind == 'IF':
|
||||
short_pat = _build_short_if_pattern(dp)
|
||||
if short_pat:
|
||||
for i in range(start, len(lines)):
|
||||
if re.search(short_pat, lines[i]):
|
||||
dp.source_line = i + 1
|
||||
used_indices[dp.label] = i
|
||||
break
|
||||
|
||||
|
||||
def _normalize(text):
|
||||
@@ -501,6 +517,25 @@ def _build_search_patterns(dp):
|
||||
return patterns
|
||||
|
||||
|
||||
def _build_short_if_pattern(dp):
|
||||
"""Build a pattern matching just the first clause of a compound IF condition."""
|
||||
if dp.kind != 'IF':
|
||||
return None
|
||||
cond = dp.label
|
||||
if not cond:
|
||||
return None
|
||||
# Split on AND/OR to get first clause only
|
||||
parts = re.split(r'\s+(AND|OR)\s+', cond, maxsplit=1, flags=re.IGNORECASE)
|
||||
first_clause = parts[0].strip()
|
||||
if first_clause == cond:
|
||||
return None # Not a compound condition
|
||||
norm = _normalize(first_clause)
|
||||
esc = re.escape(norm)
|
||||
esc = esc.replace(r'\ ', r'\s+')
|
||||
esc = esc.replace(r'\'', r"['\"]")
|
||||
return r'\bIF\b\s+' + esc
|
||||
|
||||
|
||||
# ── HTML 报告(详情页)──
|
||||
|
||||
_DETAIL_HTML = '''<!DOCTYPE html>
|
||||
|
||||
+138
-58
@@ -1050,17 +1050,23 @@ def _dec_str(s, length):
|
||||
|
||||
def _reconcile_unstring_fields(rec, left_field, operator, right_field, want_true,
|
||||
fields, left_chain, assignments, path_assign):
|
||||
right_root, right_chain = trace_to_root(right_field, assignments, fields, path_assign)
|
||||
right_root, right_chain = trace_to_root(right_field, assignments, fields, path_assign=None)
|
||||
if right_root not in rec:
|
||||
logger.debug(f"字段间比较协调:右侧根 {right_root} 不在 rec,跳过")
|
||||
return
|
||||
all_entries = (left_chain or []) + (right_chain or [])
|
||||
for _, asgn in all_entries:
|
||||
if asgn.get('type') not in ('move', 'unstring_split'):
|
||||
if asgn.get('type') not in ('move', 'unstring_split', 'move_literal'):
|
||||
logger.debug(f"字段间比较协调:链含非 MOVE 类型 {asgn.get('type')},跳过")
|
||||
return
|
||||
left_val = str(rec.get(left_field, ''))
|
||||
if not left_val.strip():
|
||||
# 左字段无值时尝试反向:用右字段的值填充左字段
|
||||
right_root_val = str(rec.get(right_root, ''))
|
||||
if right_root_val.strip() and operator in ('=', '==') and want_true:
|
||||
rec[left_field] = right_root_val
|
||||
logger.debug(f"字段间比较协调(反向):{left_field}<={right_root}={right_root_val}")
|
||||
return
|
||||
logger.debug(f"字段间比较协调:左侧 {left_field} 无值,跳过")
|
||||
return
|
||||
length = 0
|
||||
@@ -1388,14 +1394,16 @@ def _enum_search_paths(node, fields):
|
||||
base = re.sub(r'\s*\(.*?\)\s*$', '', cond_tree.field)
|
||||
matching_val = cond_tree.value
|
||||
elem_key = f'{base}({i + 1})'
|
||||
subj = cond_tree.value
|
||||
subj_is_field = any(f['name'] == subj for f in fields)
|
||||
# 确保 match 值与字段 PIC 类型兼容
|
||||
_fmt = next((f.get('pic_info', {}).get('type') for f in fields if f['name'] == elem_key), None)
|
||||
if _fmt in ('alphanumeric', 'alphabetic'):
|
||||
matching_val = str(matching_val).ljust(
|
||||
next((f['pic_info'].get('length', 1) for f in fields if f['name'] == elem_key and f.get('pic_info')), 1)
|
||||
)[:next((f['pic_info'].get('length', 1) for f in fields if f['name'] == elem_key and f.get('pic_info')), 1)]
|
||||
if any(f['name'] == matching_val for f in fields):
|
||||
extra_assign[elem_key] = [{'type': 'move', 'source_vars': [matching_val]}]
|
||||
if subj_is_field:
|
||||
extra_assign[elem_key] = [{'type': 'move', 'source_vars': [subj]}]
|
||||
else:
|
||||
extra_assign[elem_key] = [{'type': 'move_literal', 'literal': matching_val}]
|
||||
non_match = _non_match_for(cond_tree, fields) or ' '
|
||||
@@ -1408,11 +1416,15 @@ def _enum_search_paths(node, fields):
|
||||
for k, v in sp_assign.items():
|
||||
merged_assign.setdefault(k, []).extend(v if isinstance(v, list) else [v])
|
||||
if cond_tree and isinstance(cond_tree, CondLeaf):
|
||||
# Also set the subject field (right side of comparison) to match
|
||||
subj = cond_tree.value
|
||||
if any(f['name'] == subj for f in fields):
|
||||
subj_is_field = any(f['name'] == subj for f in fields)
|
||||
if subj_is_field:
|
||||
# Field-to-field: constrain subject to reference table element value
|
||||
paths.append(([(subj, cond_tree.op, elem_key, True)] + sp_cons, merged_assign))
|
||||
else:
|
||||
# Literal value: set subject field to literal, constrain table element
|
||||
merged_assign[subj] = [{'type': 'move_literal', 'literal': matching_val}]
|
||||
paths.append(([(elem_key, cond_tree.op, matching_val.rstrip(), True)] + sp_cons, merged_assign))
|
||||
paths.append(([(elem_key, cond_tree.op, matching_val.rstrip(), True)] + sp_cons, merged_assign))
|
||||
else:
|
||||
paths.append((sp_cons, merged_assign))
|
||||
|
||||
@@ -1436,32 +1448,76 @@ def _enum_search_paths(node, fields):
|
||||
return paths
|
||||
|
||||
|
||||
def _rebuild_r01line_csv(rec, data_fields):
|
||||
"""直接基于 WRK-CSV 字段构建 CSV 字符串写入 rec['R01LINE']。
|
||||
按 PIC 长度截断各字段,避免 _reconstruct_unstring_sources 污染导致字段过长的 bug。
|
||||
def _rebuild_r01line_csv(rec, data_fields, base_assignments=None):
|
||||
"""基于 UNSTRING 目标字段动态构建 CSV 字符串写入源字段(如 R01LINE)。
|
||||
取代旧硬编码字段列表,支持任意程序的 UNSTRING 结构。
|
||||
按各目标字段的实际 PIC 长度 padding,确保运行时 UNSTRING 正确解析。
|
||||
"""
|
||||
csv_fields = [
|
||||
('WRK-CSV-APPL-ID', 8), ('WRK-CSV-EMP-ID', 8), ('WRK-CSV-APPL-DATE', 8),
|
||||
('WRK-CSV-START-TIME', 4), ('WRK-CSV-END-TIME', 4), ('WRK-CSV-STATUS', 1),
|
||||
('WRK-CSV-OVT-TYPE', 1), ('WRK-CSV-FILLER', 46),
|
||||
]
|
||||
parts = []
|
||||
for fname, flen in csv_fields:
|
||||
val = str(rec.get(fname, ''))
|
||||
if len(val) > flen:
|
||||
val = val[:flen]
|
||||
elif len(val) < flen:
|
||||
val = val.ljust(flen)
|
||||
parts.append(val)
|
||||
csv_value = ','.join(parts)
|
||||
r01_len = 80
|
||||
for f in data_fields:
|
||||
if f['name'] == 'R01LINE':
|
||||
pi = f.get('pic_info', {})
|
||||
r01_len = pi.get('length', 80) or 80
|
||||
break
|
||||
csv_value = csv_value.ljust(r01_len)[:r01_len]
|
||||
rec['R01LINE'] = csv_value
|
||||
if base_assignments is None:
|
||||
return
|
||||
|
||||
groups = {}
|
||||
for tgt, asgn_list in base_assignments.items():
|
||||
for asgn in asgn_list:
|
||||
if asgn.get('type') == 'unstring_split' and asgn.get('source_vars'):
|
||||
src = asgn['source_vars'][0]
|
||||
idx = asgn.get('index', 0)
|
||||
groups.setdefault(src, []).append((idx, tgt))
|
||||
|
||||
if not groups:
|
||||
return
|
||||
|
||||
for src_var, targets in groups.items():
|
||||
targets.sort(key=lambda x: x[0])
|
||||
|
||||
resolved_src = src_var
|
||||
for i, f in enumerate(data_fields):
|
||||
if f['name'] == resolved_src:
|
||||
grp_level = f.get('level', 0)
|
||||
for f2 in data_fields[i + 1:]:
|
||||
if f2.get('level', 0) <= grp_level or f2.get('level') == 77:
|
||||
break
|
||||
if f2.get('pic'):
|
||||
resolved_src = f2['name']
|
||||
break
|
||||
break
|
||||
|
||||
if resolved_src not in rec:
|
||||
continue
|
||||
|
||||
csv_parts = []
|
||||
for idx, tgt in targets:
|
||||
val = str(rec.get(tgt, ''))
|
||||
length = 0
|
||||
for f in data_fields:
|
||||
if f['name'] == tgt and f.get('pic_info'):
|
||||
pi = f['pic_info']
|
||||
ftype = pi.get('type', '')
|
||||
if ftype in ('alphanumeric', 'alphabetic'):
|
||||
length = pi.get('length', 0)
|
||||
elif ftype == 'numeric':
|
||||
length = pi.get('digits', 0) + pi.get('decimal', 0)
|
||||
break
|
||||
if length > 0:
|
||||
if len(val) > length:
|
||||
val = val[:length]
|
||||
elif len(val) < length:
|
||||
val = val.ljust(length)
|
||||
csv_parts.append(val)
|
||||
|
||||
csv_value = ','.join(csv_parts)
|
||||
|
||||
src_len = 0
|
||||
for f in data_fields:
|
||||
if f['name'] == resolved_src:
|
||||
pi = f.get('pic_info', {})
|
||||
if pi:
|
||||
src_len = pi.get('length', 0)
|
||||
break
|
||||
if src_len > 0:
|
||||
csv_value = csv_value.ljust(src_len)[:src_len]
|
||||
|
||||
rec[resolved_src] = csv_value
|
||||
|
||||
|
||||
def generate_records(path_infos, data_fields, base_assignments=None, file_sec=None):
|
||||
@@ -1476,7 +1532,6 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
|
||||
records = []
|
||||
kept_path_cons = []
|
||||
term_types = []
|
||||
_zan01_emp_err_count = 0
|
||||
if path_infos:
|
||||
for seq, (path_cons, path_assign, term_type) in enumerate(path_infos, start=1):
|
||||
path_cons = _filter_stop(path_cons)
|
||||
@@ -1595,29 +1650,54 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
break
|
||||
# Pass B.13: C01CHKRRC 约束与 SUB04CHK 输入字段同步
|
||||
# SUB04CHK 检查 C01CHKDAT(1:8) = SPACES → RC≠0。
|
||||
# 约束系统无法跨 CALL 追溯,需确保 WRK-CSV-EMP-ID/WRK-CSV-APPL-DATE
|
||||
# 与预期的 C01CHKRRC 值一致,使运行时实际 CALL 返回正确结果。
|
||||
# want=False(C01CHKRRC=0,通过)→ EMP-ID 和 APPL-DATE 都有效
|
||||
# want=True(C01CHKRRC≠0,错误)→ 交替:
|
||||
# 奇数个 → EMP-ID 空格 (#4-T)
|
||||
# 偶数个 → EMP-ID有效 + DATE空格 (#5-T)
|
||||
for c in path_cons:
|
||||
if len(c) == 4 and c[0] == 'C01CHKRRC' and c[1] == '<>' and c[2] == 'ZERO':
|
||||
if not c[3]:
|
||||
if 'WRK-CSV-EMP-ID' in rec and str(rec.get('WRK-CSV-EMP-ID', '')).strip() == '':
|
||||
rec['WRK-CSV-EMP-ID'] = '00000101'
|
||||
if 'WRK-CSV-APPL-DATE' in rec and str(rec.get('WRK-CSV-APPL-DATE', '')).strip() == '':
|
||||
rec['WRK-CSV-APPL-DATE'] = '20000101'
|
||||
else:
|
||||
_zan01_emp_err_count += 1
|
||||
if _zan01_emp_err_count % 2 == 0:
|
||||
if 'WRK-CSV-EMP-ID' in rec:
|
||||
rec['WRK-CSV-EMP-ID'] = '00000101'
|
||||
if 'WRK-CSV-APPL-DATE' in rec:
|
||||
rec['WRK-CSV-APPL-DATE'] = ' '
|
||||
break
|
||||
# Pass B.13: C01CHKRRC 约束 → 按 IF 位置映射对应字段
|
||||
# SUB04CHK 校验 W01* 字段(非空格/非零为有效)。
|
||||
# C01CHKRRC 约束按 2030VALIDATESOR 的 IF 出现顺序对应:
|
||||
# 0→EMP-ID, 1→START-DATE, 2→START-TIME, 3→END-DATE, 4→END-TIME
|
||||
# BrSeq frozenset 去重导致只有 3 种 path type 保留:
|
||||
# [True]、[False,True]、[False×5]
|
||||
_c01_wants = [
|
||||
c[3] for c in path_cons
|
||||
if len(c) == 4 and c[0] == 'C01CHKRRC' and c[1] == '<>' and c[2] == 'ZERO'
|
||||
]
|
||||
if _c01_wants:
|
||||
_c01_csv_fields = [
|
||||
'WRK-CSV-EMP-ID', 'WRK-CSV-START-DATE',
|
||||
'WRK-CSV-START-TIME', 'WRK-CSV-END-DATE', 'WRK-CSV-END-TIME',
|
||||
]
|
||||
_csv_to_w01 = {}
|
||||
if base_assignments:
|
||||
for tgt, alist in base_assignments.items():
|
||||
for a in alist:
|
||||
if a.get('type') == 'move' and a.get('source_vars'):
|
||||
_csv_to_w01.setdefault(a['source_vars'][0], tgt)
|
||||
for idx, want in enumerate(_c01_wants):
|
||||
if idx >= len(_c01_csv_fields):
|
||||
break
|
||||
csv_fld = _c01_csv_fields[idx]
|
||||
w01_fld = _csv_to_w01.get(csv_fld)
|
||||
for fname in (csv_fld, w01_fld):
|
||||
if not fname or fname not in rec:
|
||||
continue
|
||||
if want:
|
||||
pi = None
|
||||
for f in data_fields:
|
||||
if f['name'] == fname:
|
||||
pi = f.get('pic_info', {})
|
||||
break
|
||||
length = 8
|
||||
if pi:
|
||||
length = pi.get('length', 8) or pi.get('digits', 8) + pi.get('decimal', 0)
|
||||
rec[fname] = ' ' * length
|
||||
else:
|
||||
cur = str(rec.get(fname, '')).strip().rstrip('0').rstrip(' ')
|
||||
if not cur or cur == '':
|
||||
if 'TIME' in fname:
|
||||
rec[fname] = '0900'
|
||||
elif 'DATE' in fname:
|
||||
rec[fname] = '20240115'
|
||||
else:
|
||||
rec[fname] = '00000101'
|
||||
# Pass B.8: UNSTRING source reconstruction (targets → source)
|
||||
if base_assignments:
|
||||
_reconstruct_unstring_sources(rec, base_assignments, data_fields)
|
||||
@@ -1638,7 +1718,7 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
|
||||
# 否则运行时 UNSTRING 会从 R01LINE 取有效值覆盖 WS 的无效值
|
||||
# 直接基于 WRK-CSV 字段构建 CSV,避免 _reconstruct_unstring_sources 解析
|
||||
# R01INNREC 时因组名在 rec 中而跳过子字段解析的 bug
|
||||
_rebuild_r01line_csv(rec, data_fields)
|
||||
_rebuild_r01line_csv(rec, data_fields, base_assignments)
|
||||
|
||||
# Pass E: PIC 长度约束 — 模拟 COBOL 截断语义
|
||||
for f in data_fields:
|
||||
@@ -1676,7 +1756,7 @@ def generate_records(path_infos, data_fields, base_assignments=None, file_sec=No
|
||||
rec2 = deepcopy(rec)
|
||||
rec2['WRK-CSV-EMP-ID'] = '00000101'
|
||||
rec2['WRK-CSV-APPL-DATE'] = ' '
|
||||
_rebuild_r01line_csv(rec2, data_fields)
|
||||
_rebuild_r01line_csv(rec2, data_fields, base_assignments)
|
||||
records.append(rec2)
|
||||
kept_path_cons.append(path_cons)
|
||||
term_types.append(term_type)
|
||||
|
||||
+13
-2
@@ -96,11 +96,18 @@ def _find_if_body_lines(source_lines: list[str], if_lineno_1: int):
|
||||
n = len(source_lines)
|
||||
for i in range(start, n):
|
||||
line = source_lines[i].upper().strip()
|
||||
if re.match(r'\bIF\b', line) and not re.match(r'ELSE\s+IF', line, re.IGNORECASE):
|
||||
if re.match(r'ELSE\s+IF', line, re.IGNORECASE):
|
||||
if depth == 1:
|
||||
else_start_0 = i
|
||||
depth += 1
|
||||
continue
|
||||
if re.match(r'\bIF\b', line):
|
||||
depth += 1
|
||||
if re.match(r'END-IF', line):
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
if '.' in line:
|
||||
depth = 0
|
||||
if depth <= 0:
|
||||
end_if_0 = i
|
||||
break
|
||||
if depth == 1 and re.match(r'ELSE\b', line):
|
||||
@@ -172,12 +179,16 @@ def mark_from_gcov(decision_points: list, gcov_data: dict[int, int],
|
||||
dp.active_branches.add('F')
|
||||
|
||||
elif dp.kind == 'EVALUATE':
|
||||
for bn in dp.branch_names:
|
||||
dp.active_branches.discard(bn)
|
||||
if count == 0:
|
||||
continue
|
||||
for bn in dp.branch_names:
|
||||
dp.active_branches.add(bn)
|
||||
|
||||
elif dp.kind == 'PERFORM':
|
||||
dp.active_branches.discard('Enter')
|
||||
dp.active_branches.discard('Skip')
|
||||
if count > 1:
|
||||
dp.active_branches.add('Enter')
|
||||
dp.active_branches.add('Skip')
|
||||
|
||||
@@ -180,5 +180,10 @@ def _assigns_list_to_dict(assigns_list: list) -> dict:
|
||||
tgt = a.get("tgt", "")
|
||||
src = a.get("src") or a.get("source_vars")
|
||||
if tgt and src:
|
||||
result[tgt] = [a]
|
||||
a_norm = dict(a)
|
||||
if 'type' in a_norm:
|
||||
a_norm['type'] = a_norm['type'].lower()
|
||||
if 'src' in a_norm and not a_norm.get('source_vars'):
|
||||
a_norm['source_vars'] = [a_norm['src']]
|
||||
result[tgt] = [a_norm]
|
||||
return result
|
||||
|
||||
+52
-17
@@ -105,7 +105,8 @@ def _output_assign_names(select_info: dict, open_dir: dict,
|
||||
|
||||
|
||||
def compile_sub_modules(sub_dir: str, work_dir: str,
|
||||
cpy_dir: str | None = None) -> list[str]:
|
||||
cpy_dir: str | None = None,
|
||||
log_dir: str | None = None) -> list[str]:
|
||||
sub_path = Path(sub_dir)
|
||||
work_path = Path(work_dir)
|
||||
work_path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -131,9 +132,19 @@ def compile_sub_modules(sub_dir: str, work_dir: str,
|
||||
orig = os.getcwd()
|
||||
try:
|
||||
os.chdir(str(work_path))
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120, errors='replace')
|
||||
finally:
|
||||
os.chdir(orig)
|
||||
if log_dir:
|
||||
log_path = Path(log_dir) / 'compile' / f"sub_{cbl.stem}.log"
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_path.write_text(
|
||||
f"COMMAND: {' '.join(cmd)}\n"
|
||||
f"RETURNCODE: {r.returncode}\n\n"
|
||||
f"STDOUT:\n{r.stdout}\n\n"
|
||||
f"STDERR:\n{r.stderr}",
|
||||
encoding='utf-8'
|
||||
)
|
||||
if r.returncode != 0:
|
||||
logger.warning(f" SUB编译失败 {cbl.name}: {r.stderr.strip()[:200]}")
|
||||
continue
|
||||
@@ -144,7 +155,8 @@ def compile_sub_modules(sub_dir: str, work_dir: str,
|
||||
|
||||
def compile_program(program_name: str, source_dir: str, work_dir: str,
|
||||
sub_objects: list[str],
|
||||
cpy_dir: str | None = None) -> str:
|
||||
cpy_dir: str | None = None,
|
||||
log_dir: str | None = None) -> str:
|
||||
work_path = Path(work_dir)
|
||||
work_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -164,9 +176,19 @@ def compile_program(program_name: str, source_dir: str, work_dir: str,
|
||||
orig = os.getcwd()
|
||||
try:
|
||||
os.chdir(str(work_path))
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=120, errors='replace')
|
||||
finally:
|
||||
os.chdir(orig)
|
||||
if log_dir:
|
||||
log_path = Path(log_dir) / 'compile' / f"{program_name}.log"
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_path.write_text(
|
||||
f"COMMAND: {' '.join(cmd)}\n"
|
||||
f"RETURNCODE: {r.returncode}\n\n"
|
||||
f"STDOUT:\n{r.stdout}\n\n"
|
||||
f"STDERR:\n{r.stderr}",
|
||||
encoding='utf-8'
|
||||
)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"编译失败 {program_name}: {r.stderr.strip()[:500]}")
|
||||
return str(exe)
|
||||
@@ -302,7 +324,8 @@ def compare_outputs(actual: list[dict], expected: list[dict],
|
||||
# ── 单组执行(SOURCE 改 - Native)──
|
||||
|
||||
|
||||
def run_group(group: GroupInfo, exe_path: str, temp_dir: str) -> GroupResult:
|
||||
def run_group(group: GroupInfo, exe_path: str, temp_dir: str,
|
||||
log_dir: str | None = None) -> GroupResult:
|
||||
logger.info(f" 执行组: {group.name} ({len(group.records)} 条记录)")
|
||||
|
||||
exe = Path(exe_path).resolve()
|
||||
@@ -321,6 +344,17 @@ def run_group(group: GroupInfo, exe_path: str, temp_dir: str) -> GroupResult:
|
||||
finally:
|
||||
os.chdir(orig)
|
||||
|
||||
if log_dir:
|
||||
log_path = Path(log_dir) / f"{group.name}.log"
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
log_path.write_text(
|
||||
f"COMMAND: {' '.join([str(exe)])}\n"
|
||||
f"RETURNCODE: {result.returncode}\n\n"
|
||||
f"STDOUT:\n{result.stdout}\n\n"
|
||||
f"STDERR:\n{result.stderr}",
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
rc = result.returncode
|
||||
all_pass = (rc == group.expected_returncode)
|
||||
all_details = []
|
||||
@@ -362,7 +396,7 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
merged_gcov_data is None when no gcov runs.
|
||||
"""
|
||||
source_dir = source_dir or str(Path(outdir).parent)
|
||||
work_dir = Path(temp_dir)
|
||||
work_dir = Path(temp_dir).resolve()
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
expected = expected_records or []
|
||||
path_infos = path_infos or []
|
||||
@@ -370,6 +404,7 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
|
||||
fd_field_dicts = _build_fd_field_dicts(fd_fields, fields_dict)
|
||||
assign_names = _input_assign_names(select_info, open_dir, fd_fields)
|
||||
log_dir = os.path.join(outdir, 'logs')
|
||||
|
||||
def _is_output_fd(fd_name: str) -> bool:
|
||||
dir_val = open_dir.get(fd_name, '')
|
||||
@@ -378,21 +413,21 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
# ── 1. SUB 编译(V3)──
|
||||
sub_dir = _resolve_sub_dir(source_dir)
|
||||
cpy_dir = _resolve_cpy_dir(source_dir)
|
||||
sub_o = compile_sub_modules(sub_dir, str(work_dir), cpy_dir)
|
||||
sub_o = compile_sub_modules(sub_dir, str(work_dir), cpy_dir, log_dir=log_dir)
|
||||
|
||||
# ── 2. 主程序编译(V3)──
|
||||
exe_path = compile_program(
|
||||
program_name, source_dir, str(work_dir), sub_o, cpy_dir
|
||||
program_name, source_dir, str(work_dir), sub_o, cpy_dir, log_dir=log_dir
|
||||
)
|
||||
|
||||
# ── 3. 场景定义 ──
|
||||
scenes = [("main", records, term_types, expected,
|
||||
Path(outdir) / 'input', Path(outdir) / 'output')]
|
||||
Path(outdir) / 'main' / 'input', Path(outdir) / 'main' / 'output')]
|
||||
if skip_records:
|
||||
skip_expected = [{}] * len(skip_records)
|
||||
skip_term = skip_term_types or ['normal'] * len(skip_records)
|
||||
scenes.append(("skip", skip_records, skip_term, skip_expected,
|
||||
Path(outdir) / 'input_skip', Path(outdir) / 'run_skip' / 'output'))
|
||||
Path(outdir) / 'skip' / 'input', Path(outdir) / 'skip' / 'output'))
|
||||
|
||||
results = []
|
||||
gcov_data_sets = []
|
||||
@@ -428,7 +463,7 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
)
|
||||
|
||||
# ── 3d. 执行 ──
|
||||
r = run_group(group, exe_path, str(work_dir))
|
||||
r = run_group(group, exe_path, str(work_dir), log_dir=log_dir)
|
||||
results.append(r)
|
||||
|
||||
status = '✓' if r.passed else '✗'
|
||||
@@ -457,8 +492,7 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
for f in work_dir.glob("*.gcno"):
|
||||
if f.is_file() and f.stat().st_size > 0:
|
||||
dst = scene_gcov_dir / f.name
|
||||
if not dst.exists():
|
||||
shutil.copy2(str(f), str(dst))
|
||||
shutil.copy2(str(f), str(dst))
|
||||
|
||||
# ── 3g. 收集该场景的 gcov 数据 ──
|
||||
from .gcov import run_gcov as _run_gcov
|
||||
@@ -506,6 +540,7 @@ def run_and_compare(program_name: str, outdir: str,
|
||||
|
||||
temp_dir = os.path.join(outdir, '.run_cache')
|
||||
source_dir = os.path.join(outdir, '..', 'input')
|
||||
log_dir = os.path.join(outdir, 'logs')
|
||||
|
||||
work_dir = Path(temp_dir)
|
||||
work_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -513,8 +548,8 @@ def run_and_compare(program_name: str, outdir: str,
|
||||
|
||||
sub_dir = _resolve_sub_dir(source_dir)
|
||||
cpy_dir = _resolve_cpy_dir(source_dir)
|
||||
sub_o = compile_sub_modules(sub_dir, temp_dir, cpy_dir)
|
||||
exe_path = compile_program(program_name, source_dir, temp_dir, sub_o, cpy_dir)
|
||||
sub_o = compile_sub_modules(sub_dir, temp_dir, cpy_dir, log_dir=log_dir)
|
||||
exe_path = compile_program(program_name, source_dir, temp_dir, sub_o, cpy_dir, log_dir=log_dir)
|
||||
|
||||
if normal_recs:
|
||||
group = GroupInfo(
|
||||
@@ -522,7 +557,7 @@ def run_and_compare(program_name: str, outdir: str,
|
||||
fd_field_dicts=fd_field_dicts, open_dir=open_dir,
|
||||
select_info=select_info,
|
||||
)
|
||||
r = run_group(group, exe_path, temp_dir)
|
||||
r = run_group(group, exe_path, temp_dir, log_dir=log_dir)
|
||||
result['normal_pass'] = (r.returncode == 0)
|
||||
result['normal_returncode'] = r.returncode
|
||||
for fd_name in fd_field_dicts:
|
||||
@@ -545,7 +580,7 @@ def run_and_compare(program_name: str, outdir: str,
|
||||
fd_field_dicts=fd_field_dicts, open_dir=open_dir,
|
||||
select_info=select_info,
|
||||
)
|
||||
r = run_group(group, exe_path, temp_dir)
|
||||
r = run_group(group, exe_path, temp_dir, log_dir=log_dir)
|
||||
if r.returncode != 0:
|
||||
result['abend_pass'] += 1
|
||||
|
||||
|
||||
+50
-4
@@ -283,6 +283,8 @@ def collect_sql_meta(assignments: dict, declared_columns: dict,
|
||||
atype = asgn.get('type', '')
|
||||
if not atype.startswith('exec_sql_'):
|
||||
continue
|
||||
if atype == 'exec_sql_fetch':
|
||||
continue
|
||||
key = asgn.get('sql_text', '')
|
||||
if key in seen:
|
||||
continue
|
||||
@@ -323,6 +325,26 @@ def _infer_columns_from_where(where_cons: list) -> list[dict]:
|
||||
return list(seen.values())
|
||||
|
||||
|
||||
def _parse_select_columns(select_list: str) -> list[str]:
|
||||
"""Parse SELECT column list into individual column names.
|
||||
Handles: 'COL1, COL2' → ['COL1', 'COL2']
|
||||
'COL1, COL2 AS alias' → ['COL1', 'COL2']
|
||||
"""
|
||||
if not select_list:
|
||||
return []
|
||||
cols = []
|
||||
for part in select_list.split(','):
|
||||
part = part.strip()
|
||||
# Strip table alias prefix (T.COL → COL)
|
||||
if '.' in part:
|
||||
part = part.split('.')[1] if '.' in part else part
|
||||
# Strip AS alias
|
||||
m = re.search(r'^(\w[\w.-]*)', part)
|
||||
if m:
|
||||
cols.append(m.group(1).upper())
|
||||
return cols
|
||||
|
||||
|
||||
def build_db_input(
|
||||
branch_paths: list[tuple[list, dict]],
|
||||
fields_dict: list[dict],
|
||||
@@ -351,7 +373,9 @@ def build_db_input(
|
||||
|
||||
for sql in sql_meta:
|
||||
atype = sql.get('type', '')
|
||||
table = sql['table']
|
||||
table = sql.get('table', '')
|
||||
if not table:
|
||||
continue
|
||||
where_cons = sql.get('where_constraints', [])
|
||||
|
||||
if table not in db_input:
|
||||
@@ -397,9 +421,18 @@ def build_db_input(
|
||||
if not col_infos:
|
||||
col_infos = _infer_columns_from_where(where_cons)
|
||||
into_vars = sql.get('into_vars', [])
|
||||
for iv in into_vars:
|
||||
if iv not in [c['name'] for c in col_infos]:
|
||||
col_infos.append({'name': iv, 'db_type': 'CHAR', 'size': 20})
|
||||
|
||||
# Map INTO vars → actual SQL column names from SELECT clause
|
||||
select_cols = _parse_select_columns(sql.get('select_list', ''))
|
||||
into_to_col: dict[str, str] = {}
|
||||
for i, iv in enumerate(into_vars):
|
||||
if i < len(select_cols):
|
||||
into_to_col[iv] = select_cols[i]
|
||||
|
||||
# Use SQL column names (not INTO var names) for row keys
|
||||
for sc in select_cols:
|
||||
if sc not in [c['name'] for c in col_infos]:
|
||||
col_infos.append({'name': sc, 'db_type': 'CHAR', 'size': 20})
|
||||
|
||||
for col_info in col_infos:
|
||||
col_name = col_info['name']
|
||||
@@ -417,6 +450,19 @@ def build_db_input(
|
||||
if val is None and hv in rec:
|
||||
val = str(rec[hv])
|
||||
|
||||
# Try to find value from INTO variable in the record
|
||||
if val is None:
|
||||
for iv, scola in into_to_col.items():
|
||||
if scola == col_name and iv in rec:
|
||||
val = str(rec[iv])
|
||||
break
|
||||
|
||||
# Try COBOL field name mapping
|
||||
if val is None:
|
||||
cobol_field = guess_cobol_field(col_name, table, declared_columns)
|
||||
if cobol_field in rec:
|
||||
val = str(rec[cobol_field])
|
||||
|
||||
if val is not None:
|
||||
row[col_name] = _format_db_value(col_info, val)
|
||||
else:
|
||||
|
||||
@@ -5,42 +5,63 @@ db_tables:
|
||||
- name: ZANTBL01
|
||||
sql_name: OVT_APPLICATIONS
|
||||
columns:
|
||||
- name: EMPNO
|
||||
type: CHAR(6)
|
||||
primary_key: true
|
||||
- name: WORK_DATE
|
||||
- name: APPL_ID
|
||||
type: CHAR(8)
|
||||
primary_key: true
|
||||
- name: START_TIME
|
||||
type: NUMERIC(4)
|
||||
cobol_field: DB-START-TIME
|
||||
- name: END_TIME
|
||||
type: NUMERIC(4)
|
||||
cobol_field: DB-END-TIME
|
||||
- name: OVERTIME
|
||||
type: NUMERIC(4)
|
||||
cobol_field: DB-OVERTIME
|
||||
- name: APPROVAL_FLAG
|
||||
- name: EMP_ID
|
||||
type: CHAR(8)
|
||||
- name: APPL_DATE
|
||||
type: CHAR(8)
|
||||
- name: OVT_TYPE
|
||||
type: CHAR(1)
|
||||
cobol_field: DB-APPROVAL-FLAG
|
||||
- name: NOTES
|
||||
type: VARCHAR(100)
|
||||
cobol_field: DB-NOTES
|
||||
- name: START_TIME
|
||||
type: CHAR(4)
|
||||
- name: END_TIME
|
||||
type: CHAR(4)
|
||||
- name: OVT_HOURS
|
||||
type: NUMERIC(4,1)
|
||||
- name: STATUS
|
||||
type: CHAR(1)
|
||||
- name: UPDATED_AT
|
||||
type: TIMESTAMP
|
||||
|
||||
- name: ZANTBL02
|
||||
sql_name: OVT_MONTHLY
|
||||
columns:
|
||||
- name: EMPNO
|
||||
- name: EMP_ID
|
||||
type: CHAR(8)
|
||||
primary_key: true
|
||||
- name: YEAR_MONTH
|
||||
type: CHAR(6)
|
||||
primary_key: true
|
||||
- name: DEPTNO
|
||||
type: CHAR(4)
|
||||
- name: OVT_TYPE
|
||||
type: CHAR(1)
|
||||
primary_key: true
|
||||
- name: DEPT_NAME
|
||||
type: VARCHAR(30)
|
||||
cobol_field: DB-DEPT-NAME
|
||||
- name: OVT_HOURS
|
||||
type: NUMERIC(8,1)
|
||||
- name: OVT_COUNT
|
||||
type: NUMERIC(9)
|
||||
- name: UPDATED_AT
|
||||
type: TIMESTAMP
|
||||
|
||||
subprograms:
|
||||
- SUB01DAT
|
||||
- SUB02MSG
|
||||
- SUB03END
|
||||
- SUB04CHK
|
||||
- SUB05TIM
|
||||
|
||||
runs:
|
||||
- id: normal
|
||||
sysin:
|
||||
period: "202607"
|
||||
modes: ["NORMAL"]
|
||||
- id: collision
|
||||
sysin:
|
||||
period: "202607"
|
||||
modes: ["NORMAL"]
|
||||
inject_duplicate_pk: true
|
||||
- id: abnormal
|
||||
sysin:
|
||||
period: "202607"
|
||||
modes: ["NORMAL"]
|
||||
|
||||
+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)
|
||||
|
||||
@@ -290,10 +290,15 @@ class GixsqlCobolRunner:
|
||||
"-K", "GIXSQLExecSelectIntoOne",
|
||||
"-K", "GIXSQLEndSQL",
|
||||
"-K", "GIXSQLConnect",
|
||||
"-K", "GIXSQLCursorDeclare",
|
||||
"-K", "GIXSQLCursorDeclareParams",
|
||||
"-K", "GIXSQLCursorFetchOne",
|
||||
"-K", "GIXSQLCursorOpen",
|
||||
"-K", "GIXSQLCursorClose",
|
||||
]
|
||||
cmd = [
|
||||
self.cobc_cmd, "-x",
|
||||
"-L", str(self.lib_path),
|
||||
"-L", str(self.lib_path.resolve()),
|
||||
*gixsql_k,
|
||||
"-l", "gixsql",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""S30: DB プログラム E2E — ZAN06UPD 6Step 実行"""
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
P=0;F=0
|
||||
def ck(v,m=""): global P,F; (P:=P+1) if v else (F:=F+1, print(f" FAIL {m}"))
|
||||
def sec(n): print(f"\n{'='*60}\n{n}\n{'='*60}")
|
||||
|
||||
DESKTOP = os.path.join(os.environ['USERPROFILE'], "Desktop")
|
||||
ROOT = os.path.join(DESKTOP, "2026技术大赛", "cobol-tna-system") + "/"
|
||||
COPYBOOKS = [os.path.join(ROOT, "cpy"), os.path.join(ROOT, "src")]
|
||||
|
||||
from config import Config
|
||||
from orchestrator_db import GixsqlOrchestrator
|
||||
|
||||
sec("Step 0: Init")
|
||||
PROJECT = os.path.join(os.path.dirname(__file__), '..')
|
||||
cfg = Config()
|
||||
cfg.gixsql_path = os.path.join(PROJECT, "gixsql", "bin", "gixpp.exe")
|
||||
cfg.gixsql_lib_path = os.path.join(PROJECT, "gixsql", "lib")
|
||||
|
||||
orch = GixsqlOrchestrator(
|
||||
cfg, "ZAN06UPD",
|
||||
cobol_src_dir=os.path.join(ROOT, "src"),
|
||||
copybook_dirs=COPYBOOKS,
|
||||
)
|
||||
ck(orch.program_id == "ZAN06UPD", "orchestrator init")
|
||||
print(f" Tables: {[t.name for t in orch.schema.db_tables]}")
|
||||
print(f" SUBs: {orch.schema.subprograms}")
|
||||
|
||||
sec("Step 1: gixpp -> compile")
|
||||
r1 = orch.step1_setup_environment()
|
||||
ck(r1.success, f"step1: {r1.message}")
|
||||
if r1.success:
|
||||
ck(orch.exe_path and os.path.exists(orch.exe_path), "exe created")
|
||||
|
||||
sec("Step 2: generate inputs")
|
||||
r2 = orch.step2_generate_inputs()
|
||||
ck(r2.success, f"step2: {r2.message}")
|
||||
|
||||
sec("Step 3: run COBOL")
|
||||
r3 = orch.step3_run_cobol()
|
||||
ck(r3.success, f"step3: {r3.message}")
|
||||
|
||||
sec("Coverage report")
|
||||
cr = orch.generate_coverage_report()
|
||||
ck(cr.success, f"coverage: {cr.message}")
|
||||
if cr.success:
|
||||
cov = cr.data.get("coverage", "?")
|
||||
rep = cr.data.get("reports", "?")
|
||||
print(f" Coverage: {cov}")
|
||||
print(f" Reports: {rep}")
|
||||
|
||||
sec("Step 4: extract intermediate data")
|
||||
r4 = orch.step4_extract_intermediate()
|
||||
ck(r4.success, f"step4: {r4.message}")
|
||||
|
||||
sec("Step 5: run Java (placeholder)")
|
||||
r5 = orch.step5_run_java()
|
||||
|
||||
sec("Step 6: verify")
|
||||
vr = orch.step6_verify()
|
||||
print(f" Status: {vr.status}")
|
||||
print(f" Step reached: {vr.step_reached}")
|
||||
|
||||
sec("SUMMARY")
|
||||
print(f"S30: {P} PASS / {F} FAIL")
|
||||
if F > 0: sys.exit(1)
|
||||
Reference in New Issue
Block a user