Feat/agent mode #1

Closed
tmm621 wants to merge 85 commits from feat/agent-mode into main
13 changed files with 769 additions and 188 deletions
Showing only changes of commit 54d4e81240 - Show all commits
+17 -2
View File
@@ -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
View File
@@ -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'
+59
View File
@@ -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:
+36 -1
View File
@@ -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
View File
@@ -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=FalseC01CHKRRC=0,通过)→ EMP-ID 和 APPL-DATE 都有效
# want=TrueC01CHKRRC≠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
View File
@@ -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')
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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:
+45 -24
View File
@@ -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
View File
@@ -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)
+6 -1
View File
@@ -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",
]
+67
View File
@@ -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)