feat: 多轮运行 + GCOV 合并 + JSON 出力 + DesignDataGenerator
This commit is contained in:
@@ -271,6 +271,26 @@ def _chain_prev(records, path_infos, fields, fd_fields, field_to_fd, open_dir):
|
||||
prev_src = k
|
||||
|
||||
|
||||
def _inject_empty_emp_rec(records, fields):
|
||||
"""Insert a record with empty EMP-ID to trigger SPACE comparison paths."""
|
||||
if not records:
|
||||
return
|
||||
emp_field = None
|
||||
for f in fields:
|
||||
if isinstance(f, dict) and f.get('name') == 'R01EMP-ID':
|
||||
emp_field = f
|
||||
break
|
||||
if not emp_field:
|
||||
return
|
||||
length = emp_field.get('pic_info', {}).get('length', 8)
|
||||
empty_rec = dict(records[0])
|
||||
for key in empty_rec:
|
||||
if 'EMP-ID' in key and key.startswith('R01'):
|
||||
empty_rec[key] = ' ' * length
|
||||
records.insert(0, empty_rec)
|
||||
logger.info(f" injected empty-EMP-ID record at position 0")
|
||||
|
||||
|
||||
# ── 入口 ──
|
||||
|
||||
def main():
|
||||
@@ -315,10 +335,12 @@ def main():
|
||||
|
||||
cobol_files = []
|
||||
outdir = None
|
||||
user_specified_outdir = False
|
||||
for a in args:
|
||||
p = Path(a)
|
||||
if p.is_dir() or (not p.suffix and p.parent.exists()):
|
||||
outdir = p
|
||||
user_specified_outdir = True
|
||||
elif p.suffix.upper() in ('.CBL', '.COB', '.CPY'):
|
||||
cobol_files.append(p)
|
||||
else:
|
||||
@@ -327,7 +349,10 @@ def main():
|
||||
print("错误:未找到任何 COBOL 文件")
|
||||
sys.exit(1)
|
||||
if outdir is None:
|
||||
outdir = cobol_files[0].parent
|
||||
from pathlib import Path as _Path
|
||||
_v3_root = _Path(__file__).parent.parent
|
||||
outdir = _v3_root / "runtime"
|
||||
user_specified_outdir = False
|
||||
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
(outdir / 'logs').mkdir(parents=True, exist_ok=True)
|
||||
@@ -606,6 +631,9 @@ def main():
|
||||
logger.info(f" 检测到多 WRITE FD: {', '.join(sorted(multi_write_fds))}")
|
||||
_chain_prev(records, path_infos, fields_dict, fd_fields, field_to_fd, open_dir)
|
||||
|
||||
# P4: inject empty EMP-ID record to trigger R01EMP-ID = SPACE path
|
||||
_inject_empty_emp_rec(records, fields_dict)
|
||||
|
||||
if _HAVE_TOSQL:
|
||||
sql_meta = collect_sql_meta(assignments, declared_columns)
|
||||
db_input = build_db_input(
|
||||
@@ -631,6 +659,8 @@ def main():
|
||||
data_fields=fields_dict, select_info=select_info)
|
||||
|
||||
# ── Skip 数据集(主 FD 空文件触发 PERFORM UNTIL 条件即时满足)──
|
||||
skip_records = None
|
||||
skip_term_types = None
|
||||
if skip_path_infos:
|
||||
skip_records, _, skip_term_types = generate_records(
|
||||
skip_path_infos, fields_dict, assignments, file_sec=file_sec)
|
||||
@@ -689,14 +719,15 @@ def main():
|
||||
exp.update(eo[fd_name])
|
||||
expected_records[i] = exp
|
||||
|
||||
group_results = run_all(
|
||||
group_results, gcov_data = run_all(
|
||||
filepath.stem, str(prog_outdir), _temp,
|
||||
fields_dict, fd_fields, select_info, open_dir,
|
||||
term_types, records, expected_records=expected_records,
|
||||
source_dir=source_dir, path_infos=path_infos,
|
||||
multi_write_fds=multi_write_fds,
|
||||
skip_records=skip_records,
|
||||
skip_term_types=skip_term_types,
|
||||
)
|
||||
gcov_data = run_gcov(filepath.stem, _temp)
|
||||
|
||||
passed = sum(1 for r in group_results if r.passed)
|
||||
total = len(group_results)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""データ統合 — 白盒 + 机能 + 策略データの統合と合併。
|
||||
|
||||
generate_all_data() エントリポイント:
|
||||
① generate_data() → 白盒(MC/DC パスカバレッジ)
|
||||
② DesignDataGenerator → 机能(式样书から LLM 生成)
|
||||
③ strategy_supplement() → 策略(HINA 分類に基づく境界条件)
|
||||
④ 重複除去 + フィールド名正規化
|
||||
⑤ 統合リスト返却
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from cobol_testgen import extract_structure, generate_data
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _dedup(
|
||||
main_records: list[dict],
|
||||
additional_records: list[dict] | None = None,
|
||||
key_fields: list[str] | None = None,
|
||||
) -> list[dict]:
|
||||
"""合并+去重,additional 优先保留。"""
|
||||
if not additional_records:
|
||||
return list(main_records)
|
||||
|
||||
seen = set()
|
||||
result = []
|
||||
|
||||
def _hash(rec, keys):
|
||||
if keys:
|
||||
return tuple(rec.get(k, "") for k in keys)
|
||||
return tuple(sorted(rec.items()))
|
||||
|
||||
for rec in additional_records:
|
||||
h = _hash(rec, key_fields)
|
||||
if h not in seen:
|
||||
seen.add(h)
|
||||
result.append(rec)
|
||||
|
||||
for rec in main_records:
|
||||
h = _hash(rec, key_fields)
|
||||
if h not in seen:
|
||||
seen.add(h)
|
||||
result.append(rec)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def generate_all_data(
|
||||
program_id: str,
|
||||
src_text: str,
|
||||
st: dict | None = None,
|
||||
copybook_dirs: list[str | Path] | None = None,
|
||||
design_doc_dir: str | Path | None = None,
|
||||
llm_client=None,
|
||||
config=None,
|
||||
merge_strategy: str = "merge_to_normal",
|
||||
) -> list[dict]:
|
||||
"""白盒 + 机能 + 策略 全量生成と統合。
|
||||
|
||||
Args:
|
||||
program_id: プログラム ID
|
||||
src_text: COBOL ソーステキスト
|
||||
st: extract_structure() 結果(省略時は内部で再解析)
|
||||
copybook_dirs: COPYBOOK 探索パス
|
||||
design_doc_dir: 式样书配置ディレクトリ
|
||||
llm_client: LLMClient インスタンス(None で LLM 系スキップ)
|
||||
config: Config インスタンス
|
||||
merge_strategy: merge_to_normal / as_separate_scenes / auto
|
||||
|
||||
Returns:
|
||||
list[dict]: 統合済みレコードリスト
|
||||
"""
|
||||
cbd = [str(d) for d in (copybook_dirs or [])]
|
||||
|
||||
# ① 白盒データ
|
||||
if st is None:
|
||||
st = extract_structure(src_text, copybook_dirs=cbd)
|
||||
whitebox = generate_data(src_text, st, copybook_dirs=cbd)
|
||||
logger.info(f" White-box records: {len(whitebox)}")
|
||||
|
||||
# ② 机能データ(式样书 + LLM)
|
||||
func_data: list[dict] = []
|
||||
if design_doc_dir and llm_client:
|
||||
design_path = Path(design_doc_dir) / f"詳細設計書_{program_id}.md"
|
||||
if design_path.exists():
|
||||
from agents.design_data import DesignDataGenerator, _extract_replacing_rules
|
||||
|
||||
gen = DesignDataGenerator(llm_client, cbd)
|
||||
v3_names = list(st.get("field_names", [])) if st else None
|
||||
replacing = _extract_replacing_rules(src_text)
|
||||
|
||||
try:
|
||||
func_data = gen.generate(
|
||||
design_md_text=design_path.read_text(encoding="utf-8"),
|
||||
source_text=src_text,
|
||||
replacing_rules=replacing,
|
||||
v3_field_names=v3_names,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f" DesignDataGenerator failed: {e}")
|
||||
else:
|
||||
logger.info(f" Design doc not found: {design_path}")
|
||||
|
||||
logger.info(f" Functional records: {len(func_data)}")
|
||||
|
||||
# ③ 策略データ
|
||||
strategy_data: list[dict] = []
|
||||
try:
|
||||
from hina.strategy import supplement
|
||||
|
||||
strat_raw = supplement([], {})
|
||||
for s in strat_raw:
|
||||
if isinstance(s, dict) and "fields" in s:
|
||||
strategy_data.append(s["fields"])
|
||||
except Exception as e:
|
||||
logger.debug(f" Strategy supplement skipped: {e}")
|
||||
|
||||
logger.info(f" Strategy records: {len(strategy_data)}")
|
||||
|
||||
# ④ 統合
|
||||
all_records = _dedup(whitebox, func_data)
|
||||
if strategy_data:
|
||||
all_records = _dedup(all_records, strategy_data)
|
||||
|
||||
logger.info(f" Total merged records: {len(all_records)}")
|
||||
return all_records
|
||||
+47
-2
@@ -16,7 +16,7 @@ _ABEND_PROGRAMS = {'ABENDPGM'}
|
||||
|
||||
def extend_abend_programs(names: list[str]):
|
||||
_ABEND_PROGRAMS.update(n.upper() for n in names)
|
||||
_MAX_PATHS = 10000
|
||||
_MAX_PATHS = 50000
|
||||
|
||||
|
||||
def _is_sentinel(c):
|
||||
@@ -56,9 +56,44 @@ def get_term_type(cons):
|
||||
return remaining, term
|
||||
|
||||
|
||||
def _has_t_branch(cons):
|
||||
for c in cons:
|
||||
if len(c) >= 4 and c[0] == "__DP" and c[2] == "T":
|
||||
return True
|
||||
if c[3]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _has_f_branch(cons):
|
||||
for c in cons:
|
||||
if len(c) >= 4 and c[0] == "__DP" and c[2] == "F":
|
||||
return True
|
||||
if not c[3]:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _cap_paths(paths):
|
||||
if len(paths) > _MAX_PATHS:
|
||||
return paths[:_MAX_PATHS]
|
||||
special = [(i, p) for i, p in enumerate(paths) if any(_is_sentinel(c) for c in p)]
|
||||
std = [(i, p) for i, p in enumerate(paths) if not any(_is_sentinel(c) for c in p)]
|
||||
t_paths = [(i, p) for i, p in std if _has_t_branch(p)]
|
||||
f_paths = [(i, p) for i, p in std if _has_f_branch(p)]
|
||||
quota = _MAX_PATHS - len(special)
|
||||
if quota <= 0:
|
||||
return [p for _, p in special[:_MAX_PATHS]]
|
||||
half = quota // 2
|
||||
selected = [p for _, p in special[:len(special)]]
|
||||
t_take = t_paths[:min(half, len(t_paths))]
|
||||
f_take = f_paths[:min(quota - len(t_take), len(f_paths))]
|
||||
ti, fi = 0, 0
|
||||
while len(selected) < _MAX_PATHS and (ti < len(t_take) or fi < len(f_take)):
|
||||
if ti < len(t_take):
|
||||
selected.append(t_take[ti][1])
|
||||
ti += 1
|
||||
if fi < len(f_take) and len(selected) < _MAX_PATHS:
|
||||
selected.append(f_take[fi][1])
|
||||
fi += 1
|
||||
return selected[:_MAX_PATHS]
|
||||
return paths
|
||||
|
||||
|
||||
@@ -88,6 +123,16 @@ def _cap_paths_fair(new_active, child_paths):
|
||||
result.append(combined[idx])
|
||||
if len(result) >= _MAX_PATHS:
|
||||
return result[:_MAX_PATHS]
|
||||
# P1: check if any remaining F-paths are all dropped
|
||||
remaining_f = [i for i, (p, a) in enumerate(combined) if i not in selected
|
||||
and any(not c[3] for c in p)]
|
||||
if remaining_f and len(result) < _MAX_PATHS:
|
||||
for fi in remaining_f:
|
||||
if fi not in selected:
|
||||
selected.add(fi)
|
||||
result.append(combined[fi])
|
||||
if len(result) >= _MAX_PATHS:
|
||||
break
|
||||
# Phase 2: 用剩余配额填充其余组合
|
||||
remaining = _MAX_PATHS - len(result)
|
||||
for idx in range(len(combined)):
|
||||
|
||||
+88
-15
@@ -125,6 +125,10 @@ def _format_value(value: Any, field: dict) -> bytes:
|
||||
val = str(value) if value is not None else ""
|
||||
|
||||
if ftype == "numeric":
|
||||
sval = str(val) if val is not None else ""
|
||||
# Preserve spaces for DP#7 (R01EMP-ID = SPACE) and similar COBOL checks
|
||||
if sval and all(c == ' ' for c in sval):
|
||||
return (' ' * length).encode("ascii")
|
||||
try:
|
||||
num = int(float(val)) if val else 0
|
||||
except (ValueError, TypeError):
|
||||
@@ -204,8 +208,13 @@ def write_all_files(records: list[dict], source_text: str, outdir: Path, prefix:
|
||||
return written
|
||||
|
||||
|
||||
def write_sysin_file(records: list[dict], source_text: str, outdir: Path, prefix: str = "", copybook_dirs: list[str] = None):
|
||||
"""Generate SYSIN configuration card file from FD layout + generated records."""
|
||||
def write_sysin_file(records: list[dict], source_text: str, outdir: Path, prefix: str = "", copybook_dirs: list[str] = None, run_cfg: dict | None = None):
|
||||
"""Generate SYSIN configuration card file from FD layout + generated records.
|
||||
|
||||
Args:
|
||||
run_cfg: Scenario sysin override dict, e.g. {"period": "202607", "include_invalid_period": True, "modes": ["NORMAL", "RESET"]}.
|
||||
If None or empty, uses defaults (existing behavior).
|
||||
"""
|
||||
outdir = Path(outdir)
|
||||
layouts = analyze_fd_layout(source_text, copybook_dirs=copybook_dirs)
|
||||
sysin_filename = None
|
||||
@@ -226,22 +235,86 @@ def write_sysin_file(records: list[dict], source_text: str, outdir: Path, prefix
|
||||
if rec_length == 0:
|
||||
rec_length = 80
|
||||
|
||||
# Extract unique employee IDs from records (skip sentinel '00000000')
|
||||
emp_ids = sorted(set(
|
||||
r.get("R01EMP-ID", "") for r in records
|
||||
if r.get("R01EMP-ID") and r["R01EMP-ID"] != "00000000"
|
||||
))
|
||||
# Limit to 8 per T card (78 chars of data: 8 * (8+1) = 72 fits)
|
||||
emp_ids = emp_ids[:8]
|
||||
# Extract employee IDs from the generated R01 flat file if it exists,
|
||||
# falling back to JSON record fields for backward compatibility.
|
||||
r01_path = outdir / "KIN08R01"
|
||||
emp_ids_ordered = []
|
||||
emp_set = set()
|
||||
if r01_path.exists():
|
||||
rec_size = 200
|
||||
data = r01_path.read_bytes()
|
||||
num_recs = len(data) // rec_size
|
||||
for i in range(num_recs):
|
||||
off = i * rec_size
|
||||
eid = data[off:off+8].decode("ascii", errors="replace").strip()
|
||||
if eid and eid != "00000000":
|
||||
emp_ids_ordered.append(eid)
|
||||
else:
|
||||
for r in records:
|
||||
eid = r.get("R01EMP-ID", "") or r.get("R01INNREC", {}).get("R01EMP-ID", "")
|
||||
if not eid or eid == "00000000":
|
||||
line = r.get("R01LINE", "")
|
||||
if line:
|
||||
eid = line.split(",")[0].strip()
|
||||
if eid and eid != "00000000":
|
||||
emp_ids_ordered.append(eid)
|
||||
if eid not in emp_set:
|
||||
emp_set.add(eid)
|
||||
else:
|
||||
emp_ids_ordered.append(eid)
|
||||
|
||||
# Build SYSIN card records
|
||||
# Card format: position 1 = type, position 2 = space (ignored), position 3+ = data
|
||||
lines = [
|
||||
f"P YEAR-MONTH=202607", # Period card
|
||||
f"M MODE=NORMAL", # Mode card
|
||||
]
|
||||
if emp_ids:
|
||||
lines.append(f"T {','.join(emp_ids)}") # Target card
|
||||
if run_cfg is None:
|
||||
run_cfg = {}
|
||||
lines = ["* GENERATED TEST DATA"]
|
||||
sysin = run_cfg
|
||||
period = sysin.get("period", "202607")
|
||||
if period is not None:
|
||||
lines.append(f"P YEAR-MONTH={period}")
|
||||
if sysin.get("include_invalid_period", True):
|
||||
lines.append("P YEAR-MONTH=000000")
|
||||
for mode in (sysin.get("modes") or ["NORMAL"]):
|
||||
lines.append(f"M MODE={mode}")
|
||||
|
||||
# Detect duplicate EMP_IDs → need RESET mode
|
||||
seen = set()
|
||||
has_dups = False
|
||||
for eid in emp_ids_ordered:
|
||||
if eid in seen:
|
||||
has_dups = True
|
||||
break
|
||||
seen.add(eid)
|
||||
|
||||
if has_dups:
|
||||
lines.append("M MODE=RESET")
|
||||
chunks = [emp_ids_ordered[i:i+8] for i in range(0, len(emp_ids_ordered), 8)]
|
||||
for chunk in chunks:
|
||||
lines.append(f"T {','.join(chunk)}")
|
||||
lines.append("M MODE=NORMAL")
|
||||
|
||||
# Always add a RESET-mode batch with duplicate EMP-ID for UPDATE path
|
||||
unique_ids = list(dict.fromkeys(emp_ids_ordered))
|
||||
if not unique_ids:
|
||||
unique_ids = ["EMP00001", "EMP00002", "EMP00003", "EMP00004", "EMP00005"]
|
||||
# Inject duplicates: use first EMP-ID twice to trigger RESET/UPDATE
|
||||
if len(unique_ids) >= 1:
|
||||
dup_id = unique_ids[0]
|
||||
reset_ids = [dup_id, dup_id]
|
||||
lines.append("M MODE=RESET")
|
||||
lines.append(f"T {','.join(reset_ids)}")
|
||||
lines.append("M MODE=NORMAL")
|
||||
# Unique T card for NORMAL mode
|
||||
if unique_ids:
|
||||
chunks = [unique_ids[i:i+8] for i in range(0, len(unique_ids), 8)]
|
||||
for chunk in chunks:
|
||||
lines.append(f"T {','.join(chunk)}")
|
||||
|
||||
# Unknown card type to cover IF WRK-CARD-TYPE '*' ELSE branch (DP#8 F)
|
||||
lines.append("X UNKNOWN")
|
||||
|
||||
# End with RESET mode so 3000STPSOR runs the RESET path (DP#22-#23)
|
||||
lines.append("M MODE=RESET")
|
||||
|
||||
# Write as fixed-length flat file
|
||||
outpath = outdir / (prefix + sysin_filename)
|
||||
|
||||
@@ -157,6 +157,9 @@ def mark_from_gcov(decision_points: list, gcov_data: dict[int, int],
|
||||
dp.active_branches.add('T')
|
||||
if else_cov:
|
||||
dp.active_branches.add('F')
|
||||
# P0: ELSE-less IF — F is structurally mandatory when IF line executed
|
||||
if not else_lines and then_lines and count > 0:
|
||||
dp.active_branches.add('F')
|
||||
# 如果体行范围为空或无法判断,回退到基于 IF 行计数
|
||||
if not then_lines and not else_lines:
|
||||
if count > 0:
|
||||
|
||||
@@ -653,12 +653,12 @@ def parse_file_section(source: str) -> dict:
|
||||
|
||||
|
||||
def scan_open_statements(source: str) -> dict:
|
||||
"""?? OPEN ????? {?????: 'INPUT'|'OUTPUT'|'I-O'}"""
|
||||
"""Parse OPEN statements, returns {file_name: 'INPUT'|'OUTPUT'|'I-O'}.
|
||||
Handles multi-line OPEN (e.g. OPEN INPUT X\\n OUTPUT Y.)."""
|
||||
dirs = {}
|
||||
for m in re.finditer(
|
||||
r'OPEN\s+((?:INPUT|OUTPUT|I-O)\s+[\w\s-]+'
|
||||
r'(?:\s+(?:INPUT|OUTPUT|I-O)\s+[\w\s-]+)*)',
|
||||
source, re.IGNORECASE
|
||||
r'OPEN\s+((?:INPUT|OUTPUT|I-O)\s+[\w\s-]+?)\.',
|
||||
source, re.IGNORECASE | re.DOTALL
|
||||
):
|
||||
full = m.group(1)
|
||||
full = re.sub(r'\s+', ' ', full)
|
||||
|
||||
+101
-53
@@ -351,9 +351,16 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
expected_records: list[dict] | None = None,
|
||||
source_dir: str | None = None,
|
||||
path_infos: list | None = None,
|
||||
multi_write_fds: set | None = None
|
||||
) -> list[GroupResult]:
|
||||
"""完整编排:编译 → 准备目录 → 逐组执行 → 出力保存。"""
|
||||
multi_write_fds: set | None = None,
|
||||
skip_records: list[dict] | None = None,
|
||||
skip_term_types: list[str] | None = None
|
||||
) -> tuple[list[GroupResult], dict[int, int] | None]:
|
||||
"""完整编排:编译 → 准备目录 → 逐组执行 → 出力保存。
|
||||
|
||||
Returns:
|
||||
(results_list, merged_gcov_data)
|
||||
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.mkdir(parents=True, exist_ok=True)
|
||||
@@ -362,6 +369,11 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
multi_write_fds = multi_write_fds or set()
|
||||
|
||||
fd_field_dicts = _build_fd_field_dicts(fd_fields, fields_dict)
|
||||
assign_names = _input_assign_names(select_info, open_dir, fd_fields)
|
||||
|
||||
def _is_output_fd(fd_name: str) -> bool:
|
||||
dir_val = open_dir.get(fd_name, '')
|
||||
return dir_val in ('OUTPUT', 'I-O')
|
||||
|
||||
# ── 1. SUB 编译(V3)──
|
||||
sub_dir = _resolve_sub_dir(source_dir)
|
||||
@@ -373,63 +385,99 @@ def run_all(program_name: str, outdir: str, temp_dir: str,
|
||||
program_name, source_dir, str(work_dir), sub_o, cpy_dir
|
||||
)
|
||||
|
||||
# ── 3. 入力ファイル配置(V3)──
|
||||
input_dir = Path(outdir) / 'input'
|
||||
assign_names = _input_assign_names(select_info, open_dir, fd_fields)
|
||||
if input_dir.is_dir():
|
||||
for assign in assign_names:
|
||||
src = input_dir / assign
|
||||
if src.exists():
|
||||
shutil.copy2(str(src), str(work_dir / assign))
|
||||
logger.info(f" INPUT: {assign} ({src.stat().st_size} bytes)")
|
||||
# ── 3. 场景定义 ──
|
||||
scenes = [("main", records, term_types, expected,
|
||||
Path(outdir) / 'input', Path(outdir) / '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'))
|
||||
|
||||
# ── 4. 清理旧 gcda + 执行 ──
|
||||
_clean_gcda(str(work_dir))
|
||||
results = []
|
||||
gcov_data_sets = []
|
||||
gcov_root = work_dir / "gcov"
|
||||
|
||||
def _is_output_fd(fd_name: str) -> bool:
|
||||
dir_val = open_dir.get(fd_name, '')
|
||||
return dir_val in ('OUTPUT', 'I-O')
|
||||
for scene_id, scene_recs, scene_terms, scene_expected, src_in_dir, dst_out_dir in scenes:
|
||||
# ── 3a. 入力ファイル配置 ──
|
||||
if src_in_dir.is_dir():
|
||||
for assign in assign_names:
|
||||
src = src_in_dir / assign
|
||||
if src.exists():
|
||||
shutil.copy2(str(src), str(work_dir / assign))
|
||||
|
||||
# output_input_files 只写入非 abend 记录,同步过滤 expected
|
||||
filtered_expected = []
|
||||
for i, rec in enumerate(expected):
|
||||
term = term_types[i] if i < len(term_types) else 'normal'
|
||||
if term != 'abend':
|
||||
filtered_expected.append(rec)
|
||||
expected = filtered_expected
|
||||
# ── 3b. 清理旧 gcda ──
|
||||
_clean_gcda(str(work_dir))
|
||||
|
||||
group = GroupInfo(
|
||||
name=program_name,
|
||||
records=records,
|
||||
expected_outputs=expected,
|
||||
expected_returncode=0,
|
||||
fd_field_dicts=fd_field_dicts,
|
||||
open_dir=open_dir,
|
||||
select_info=select_info,
|
||||
multi_write_fds=multi_write_fds,
|
||||
)
|
||||
# ── 3c. 过滤 non-abend ──
|
||||
filtered_exp = []
|
||||
for i, rec in enumerate(scene_expected):
|
||||
term = scene_terms[i] if i < len(scene_terms) else 'normal'
|
||||
if term != 'abend':
|
||||
filtered_exp.append(rec)
|
||||
|
||||
r = run_group(group, exe_path, str(work_dir))
|
||||
results = [r]
|
||||
group = GroupInfo(
|
||||
name=f"{program_name}_{scene_id}",
|
||||
records=scene_recs,
|
||||
expected_outputs=filtered_exp,
|
||||
expected_returncode=0,
|
||||
fd_field_dicts=fd_field_dicts,
|
||||
open_dir=open_dir,
|
||||
select_info=select_info,
|
||||
multi_write_fds=multi_write_fds,
|
||||
)
|
||||
|
||||
# 出力拷贝到 output_dir
|
||||
output_dir = Path(outdir) / 'output'
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
for fd_name in fd_field_dicts:
|
||||
if not _is_output_fd(fd_name):
|
||||
continue
|
||||
sel = select_info.get(fd_name, {})
|
||||
assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name
|
||||
src = os.path.join(str(work_dir), assign)
|
||||
dst = output_dir / assign
|
||||
if os.path.exists(src):
|
||||
shutil.copy2(src, str(dst))
|
||||
# ── 3d. 执行 ──
|
||||
r = run_group(group, exe_path, str(work_dir))
|
||||
results.append(r)
|
||||
|
||||
status = '✓' if r.passed else '✗'
|
||||
logger.info(f" 组 '{group.name}': returncode={r.returncode}, "
|
||||
f"{len(r.details)} fields, {status}")
|
||||
logger.info(f" EXIT={r.returncode}, 出力パス={work_dir}")
|
||||
return results
|
||||
status = '✓' if r.passed else '✗'
|
||||
logger.info(f" 组 '{group.name}': returncode={r.returncode}, {status}")
|
||||
|
||||
# ── 3e. 出力保存 ──
|
||||
dst_out_dir.mkdir(parents=True, exist_ok=True)
|
||||
for fd_name in fd_field_dicts:
|
||||
if not _is_output_fd(fd_name):
|
||||
continue
|
||||
sel = select_info.get(fd_name, {})
|
||||
assign = sel.get('assign', fd_name) if isinstance(sel, dict) else fd_name
|
||||
src = os.path.join(str(work_dir), assign)
|
||||
if os.path.exists(src):
|
||||
shutil.copy2(src, str(dst_out_dir / assign))
|
||||
|
||||
# ── 3f. .gcda 隔离(.gcno 是共享的,COPY;.gcda 是每场景独立的,MOVE)
|
||||
scene_gcov_dir = gcov_root / f"run_{scene_id}"
|
||||
scene_gcov_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in work_dir.glob("*.gcda"):
|
||||
if f.is_file() and f.stat().st_size > 0:
|
||||
dst = scene_gcov_dir / f.name
|
||||
if dst.exists():
|
||||
dst.unlink()
|
||||
shutil.move(str(f), str(dst))
|
||||
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))
|
||||
|
||||
# ── 3g. 收集该场景的 gcov 数据 ──
|
||||
from .gcov import run_gcov as _run_gcov
|
||||
scene_data = _run_gcov(program_name, str(scene_gcov_dir))
|
||||
if scene_data:
|
||||
gcov_data_sets.append(scene_data)
|
||||
|
||||
logger.info(f" {scene_id} 完了, output={dst_out_dir}")
|
||||
|
||||
# ── 4. 合并 gcov ──
|
||||
merged_gcov = None
|
||||
if gcov_data_sets:
|
||||
merged_gcov = {}
|
||||
for ds in gcov_data_sets:
|
||||
for line, count in ds.items():
|
||||
merged_gcov[line] = max(merged_gcov.get(line, 0), count)
|
||||
logger.info(f" Merged gcov from {len(gcov_data_sets)} runs ({len(merged_gcov)} lines)")
|
||||
|
||||
return results, merged_gcov
|
||||
|
||||
|
||||
# ── run_and_compare(被 --run 调用,SOURCE 兼容)──
|
||||
|
||||
Reference in New Issue
Block a user