Feat/agent mode #1

Closed
tmm621 wants to merge 85 commits from feat/agent-mode into main
40 changed files with 4397 additions and 198 deletions
Showing only changes of commit f3be17e5eb - Show all commits
+3
View File
@@ -46,3 +46,6 @@ _test_flatfiles/
# External dependencies (not committed)
gixsql/
cobol-tna-system/
# 詳細設計書(設計原稿、公開禁止)
詳細設計書/
+11 -8
View File
@@ -1,22 +1,25 @@
"""LLM 智能体包
公开 API:
LLMClient — LLM API 客户端(含缓存 + 重试)
Agent1Parser — COPYBOOK → FieldTree
Agent2Data — FieldTree → TestSuite测试数据设计)
Agent3Diagnostic — FieldResult → 诊断建议文本
LLMClient — LLM API 客户端(含缓存 + 重试)
Agent1Parser — COPYBOOK → FieldTree
DesignDataGenerator — 式样书 → 机能测试数据
Agent2Data — FieldTree → TestSuite(测试数据设计)
Agent3Diagnostic — FieldResult → 诊断建议文本
"""
from __future__ import annotations
from .llm import LLMClient
from .agent1_parser import Agent1Parser
from .design_data import DesignDataGenerator
from .agent2_data import Agent2Data
from .agent3_diagnostic import Agent3Diagnostic
__all__ = [
"LLMClient", # class
"Agent1Parser", # class
"Agent2Data", # class
"Agent3Diagnostic", # class
"LLMClient", # class
"Agent1Parser", # class
"DesignDataGenerator", # class
"Agent2Data", # class
"Agent3Diagnostic", # class
]
+291
View File
@@ -0,0 +1,291 @@
"""DesignDataGenerator — 式样书驱动测试数据生成器。
从详细设计书 .md + COBOL 源码 + COPYBOOK 中提取业务信息,
通过 LLM 生成有业务意义的機能テストデータ。
"""
import json
import logging
import os
import re
from pathlib import Path
from typing import Optional
from agents.llm import LLMClient
from agents.design_data_input_parser import (
DesignDataInputParser,
ProgramMeta,
)
logger = logging.getLogger(__name__)
# LLM 提示词
_SYSTEM_PROMPT = """你是 COBOL 测试数据生成专家。根据详细设计书、COPYBOOK 结构和 DB 定义,
生成测试数据。数据必须覆盖正常路径和边界条件。
输出格式: {"records": [{"field_name": "value", ...}]} JSON only。"""
def _resolve_field_names(
records: list[dict],
replacing_rules: dict[str, str] | None,
v3_field_names: set[str] | None,
) -> list[dict]:
"""将外部 Agent 输出的字段名映射为 V3 兼容名称。
处理顺序:
1. REPLACING 展开((A) → R01
2. 去掉前缀和字段名之间的多余连字符(R01-EMP-ID → R01EMP-ID
3. 尝试直接匹配 V3 字段名
4. 尝试以 V3 字段名 prefix 截断匹配
5. 无法映射的字段丢弃
"""
if not replacing_rules and not v3_field_names:
return records
# Build prefix map: from REPLACING rules, e.g. (A) → R01, then R01 is the prefix
prefixes = set()
prefix_from_replacing = {}
if replacing_rules:
for old, new in replacing_rules.items():
if new.strip():
prefixes.add(new)
prefix_from_replacing[old] = new
if not v3_field_names:
# Just apply replacing, no V3 validation
result = []
for rec in records:
mapped = {}
for key, val in rec.items():
new_key = key
for old, new in prefix_from_replacing.items():
new_key = new_key.replace(old, new)
mapped[new_key] = val
result.append(mapped)
return result
result = []
for rec in records:
mapped = {}
for key, val in rec.items():
new_key = key
# Step 1: REPLACING 展开
for old, new in prefix_from_replacing.items():
new_key = new_key.replace(old, new)
# Step 2: 去掉前缀和字段名间的连字符
# Agent 输出: R01-EMP-ID, V3 期望: R01EMP-ID
# 去掉 {prefix}- 前缀(如果前缀是 R01,去掉 R01-)
for p in prefixes:
if new_key.startswith(p + "-"):
new_key = p + new_key[len(p) + 1 :]
break
# Step 3: 直接匹配
if new_key in v3_field_names:
mapped[new_key] = val
continue
# Step 4: 去掉所有连字符尝试匹配
no_hyphen = new_key.replace("-", "")
if no_hyphen in v3_field_names:
mapped[no_hyphen] = val
continue
# Step 5: 去掉下划线尝试匹配
no_underscore = no_hyphen.replace("_", "")
if no_underscore in v3_field_names:
mapped[no_underscore] = val
continue
logger.debug(f" field '{key}' -> '{new_key}' not in V3 fields, dropped")
result.append(mapped)
return result
def _extract_replacing_rules(source_text: str) -> dict[str, str]:
"""从 COBOL 源码的 COPY ... REPLACING 提取替换规则。"""
rules = {}
for m in re.finditer(
r"COPY\s+(\w+)\s+REPLACING\s+==\(A\)==\s+BY\s+==(\w+)==",
source_text,
re.IGNORECASE,
):
rules["(A)"] = m.group(2)
return rules
def _dedup(
main_records: list[dict],
additional_records: list[dict],
key_fields: list[str] | None = None,
) -> list[dict]:
"""合并+去重,additional 优先保留。"""
seen = set()
result = []
for rec in additional_records:
h = _record_hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
for rec in main_records:
h = _record_hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
return result
def _record_hash(rec: dict, key_fields: list[str] | None) -> tuple:
if key_fields:
return tuple(rec.get(k, "") for k in key_fields)
return tuple(sorted(rec.items()))
def _load_rules(rules_dir: str) -> str:
"""Load all rules from pgm_pattern/ and special_feature/ directories."""
texts = []
base = Path(rules_dir)
for subdir in ["pgm_pattern", "special_feature"]:
d = base / subdir
if d.exists():
for f in sorted(d.glob("*.md")):
texts.append(f"=== {subdir}/{f.name} ===\n{f.read_text(encoding='utf-8')}")
return "\n\n".join(texts)
class DesignDataGenerator:
"""式样书驱动测试数据生成器。
使用例:
llm = LLMClient(model="deepseek-v4-flash")
gen = DesignDataGenerator(llm, cpy_dirs=["cpy"])
records = gen.generate(
design_md_text=design_text,
source_text=source_text,
)
"""
def __init__(
self,
llm_client: LLMClient,
cpy_dirs: list[str | Path],
rules_dir: str | Path = "rules",
):
self.llm = llm_client
self.cpy_dirs = cpy_dirs
self.rules_dir = Path(rules_dir)
def generate(
self,
design_md_text: str,
source_text: str,
file_db_md_text: str | None = None,
db_md_text: str | None = None,
replacing_rules: dict[str, str] | None = None,
v3_field_names: list[str] | None = None,
) -> list[dict]:
"""生成机能测试数据。
Args:
design_md_text: 式样书 .md 全文
source_text: COBOL 源码全文
file_db_md_text: COPY句定义书 .md(可选)
db_md_text: DB 定义书 .md(可选)
replacing_rules: REPLACING 展开规则
v3_field_names: V3 字段名参考列表(用于映射验证)
Returns:
list[dict]: 每条记录为 {field_name: value} 格式
"""
logger.info(" DesignDataGenerator: parsing design document...")
try:
parser = DesignDataInputParser()
meta = parser.parse(design_md_text, source_text)
except Exception as e:
logger.warning(f" Design doc parsing failed: {e}")
return []
if not meta.pgm_pattern:
logger.info(" No PGM pattern found in design doc, skipping")
return []
logger.info(
f" Program: {meta.program_id}, pattern: {meta.pgm_pattern}, "
f"type: {meta.input_type}"
)
# 加载规则
rules_text = _load_rules(str(self.rules_dir))
# 构建描述信息
files_desc = "\n".join(
f" {f.identifier}: {f.file_db_name} (I/O={f.io}, 媒体={f.medium})"
for f in meta.files
)
keys_desc = "\n".join(
f" {k.file_name}: sort={k.sort_condition}, key={k.key_condition}"
for k in meta.keys
)
user_prompt = f"""## プログラム情報
- プログラムID: {meta.program_id}
- PGMパターン: {meta.pgm_pattern}
- 入力タイプ: {meta.input_type}
## 使用ファイル一覧
{files_desc or '(なし)'}
## キー項目一覧
{keys_desc or '(なし)'}
## 処理詳細
{meta.process_detail[:2000] if meta.process_detail else '(なし)'}
## 出力レコード定義
{meta.output_records[:1000] if meta.output_records else '(なし)'}
## データ生成ルール
{rules_text[:2000] if rules_text else '(なし)'}
以下の JSON 形式でテストデータを生成してください:
{{"records": [{{"field1": "value1", "field2": "value2", ...}}]}}"""
try:
response = self.llm.call(
[
{"role": "system", "content": _SYSTEM_PROMPT},
{"role": "user", "content": user_prompt},
]
)
logger.info(" LLM response received")
except Exception as e:
logger.warning(f" LLM call failed: {e}")
return []
try:
parsed = json.loads(response)
raw_records = parsed.get("records", [])
except (json.JSONDecodeError, KeyError) as e:
logger.warning(f" LLM response parse failed: {e}")
return []
if not raw_records:
logger.info(" No records generated")
return []
# 字段名映射
v3_names_set = set(v3_field_names) if v3_field_names else None
mapped = _resolve_field_names(raw_records, replacing_rules, v3_names_set)
logger.info(f" Generated {len(mapped)} records")
return mapped
+270
View File
@@ -0,0 +1,270 @@
"""式样书解析器 — 读取详细设计书 .md,提取 ProgramMeta 信息。
从外部 agentjcl-cobol-data-create)移植,适配 V3 接口。
"""
import re
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional
@dataclass
class FileInfo:
no: int = 0
file_db_name: str = ""
identifier: str = ""
dd_name: str = ""
io: str = ""
copy_group: str = ""
record_format: str = ""
record_len: int = 0
medium: str = ""
remarks: str = ""
@dataclass
class KeyInfo:
no: int = 0
file_name: str = ""
sort_condition: str = ""
key_condition: str = ""
@dataclass
class ModuleInfo:
no: int = 0
function: str = ""
program_id: str = ""
copy_name: str = ""
@dataclass
class CopyField:
level: int = 0
name: str = ""
raw_name: str = ""
pic_type: str = ""
pic_bytes: int = 0
@dataclass
class TableColumn:
no: int = 0
name_jp: str = ""
name_en: str = ""
type: str = ""
max_len: str = ""
decimal_digits: str = ""
byte_count: str = ""
nullable: bool = True
is_pk: bool = False
@dataclass
class TableInfo:
table_name: str = ""
db_id: str = ""
copy_id: str = ""
columns: list = field(default_factory=list)
pk_columns: list = field(default_factory=list)
@dataclass
class ProgramMeta:
program_id: str = ""
program_name: str = ""
system_name: str = ""
pgm_type: str = ""
pgm_pattern: str = ""
summary_lines: list = field(default_factory=list)
prerequisites: list = field(default_factory=list)
files: list = field(default_factory=list)
keys: list = field(default_factory=list)
modules: list = field(default_factory=list)
process_detail: str = ""
output_records: str = ""
input_type: str = "file"
copy_fields: dict = field(default_factory=dict)
db_tables: dict = field(default_factory=dict)
def _extract_section(text: str, section_name: str) -> str:
"""Extract a section by name from markdown text (case-insensitive)."""
lines = text.split("\n")
result = []
in_section = False
section_pattern = re.compile(
rf"^#+\s*{re.escape(section_name)}\s*$", re.IGNORECASE
)
next_section = re.compile(r"^#+\s", re.IGNORECASE)
for line in lines:
if section_pattern.match(line):
in_section = True
continue
if in_section and next_section.match(line):
break
if in_section:
result.append(line)
return "\n".join(result).strip()
def _parse_table_rows(text: str) -> list[dict]:
"""Parse a markdown table into list of dicts."""
lines = [l.strip() for l in text.split("\n") if l.strip()]
if not lines:
return []
header_line = None
sep_line = None
data_start = 0
for i, line in enumerate(lines):
if line.startswith("|") and not header_line:
header_line = line
elif line.startswith("|") and header_line and not sep_line:
if set(line.strip("|").replace("-", "").replace(" ", "").replace("|", "")) == set():
sep_line = line
data_start = i + 1
break
if not header_line:
return []
headers = [h.strip() for h in header_line.strip("|").split("|")]
rows = []
for line in lines[data_start:]:
if not line.startswith("|"):
continue
cells = [c.strip() for c in line.strip("|").split("|")]
row = {}
for i, h in enumerate(headers):
if i < len(cells):
row[h] = cells[i]
else:
row[h] = ""
rows.append(row)
return rows
class DesignDataInputParser:
"""解析式样书 .md,返回 ProgramMeta。"""
def __init__(self):
pass
def parse(self, design_md_text: str, source_text: str) -> ProgramMeta:
"""解析式样书文本和源码文本,返回 ProgramMeta。"""
meta = ProgramMeta()
self._design_text = design_md_text
self._source_text = source_text
self._parse_basic_info(meta)
self._parse_use_files(meta)
self._parse_keys(meta)
self._parse_modules(meta)
self._parse_process_detail(meta)
self._parse_output_records(meta)
self._determine_input_type(meta)
return meta
def _parse_basic_info(self, meta: ProgramMeta):
rows = _parse_table_rows(_extract_section(self._design_text, "基本情報"))
for row in rows:
item = row.get("項目", "")
value = row.get("内容", "")
if item == "システム名":
meta.system_name = value
elif item == "プログラムID":
meta.program_id = value
elif item == "プログラム名":
meta.program_name = value
elif item == "PGMタイプ":
meta.pgm_type = value
elif item == "PGMパターン":
meta.pgm_pattern = value
elif item == "機能概要":
meta.summary_lines.append(value)
def _parse_use_files(self, meta: ProgramMeta):
rows = _parse_table_rows(_extract_section(self._design_text, "使用ファイル一覧"))
for row in rows:
try:
no_str = row.get("NO", "0")
no = int(no_str) if no_str and no_str.strip() not in ("", "") else 0
rec_len_str = row.get("レコード長", "0")
rec_len = (
int(rec_len_str)
if rec_len_str and rec_len_str.strip() not in ("", "")
else 0
)
f = FileInfo(
no=no,
file_db_name=row.get("使用ファイル/DB名", ""),
identifier=row.get("識別子", ""),
dd_name=row.get("DD名", ""),
io=row.get("I/O", ""),
copy_group=row.get("COPY群", ""),
record_format=row.get("形式", ""),
record_len=rec_len,
medium=row.get("媒体", ""),
remarks=row.get("備考", ""),
)
meta.files.append(f)
except (ValueError, KeyError):
continue
def _parse_keys(self, meta: ProgramMeta):
rows = _parse_table_rows(_extract_section(self._design_text, "キー項目一覧"))
for row in rows:
try:
k = KeyInfo(
no=int(row.get("NO", "0")),
file_name=row.get("ファイル名", ""),
sort_condition=row.get("ソート条件(キー項目)", ""),
key_condition=row.get("キー条件(マッチング/キーブレイク)", ""),
)
meta.keys.append(k)
except (ValueError, KeyError):
continue
def _parse_modules(self, meta: ProgramMeta):
rows = _parse_table_rows(
_extract_section(self._design_text, "使用モジュール一覧")
)
for row in rows:
try:
m = ModuleInfo(
no=int(row.get("NO", "0")),
function=row.get("機能", ""),
program_id=row.get("プログラムID", ""),
copy_name=row.get("使用COPY名", ""),
)
meta.modules.append(m)
except (ValueError, KeyError):
continue
def _parse_process_detail(self, meta: ProgramMeta):
meta.process_detail = _extract_section(self._design_text, "処理詳細")
def _parse_output_records(self, meta: ProgramMeta):
meta.output_records = _extract_section(self._design_text, "出力レコード定義")
def _determine_input_type(self, meta: ProgramMeta):
input_mediums = set()
for f in meta.files:
if "I" in f.io:
input_mediums.add(f.medium)
if not input_mediums:
meta.input_type = "file"
elif input_mediums == {"PS"}:
meta.input_type = "file"
elif input_mediums == {"DB"}:
meta.input_type = "db"
else:
meta.input_type = "mixed"
+34 -3
View File
@@ -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)
+130
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+3
View File
@@ -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:
+4 -4
View File
@@ -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
View File
@@ -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 兼容)──
+2 -2
View File
@@ -14,8 +14,8 @@ class Config:
project_name: str = ""
copybook_paths: list = field(default_factory=lambda: ["./copybooks"])
dialect: str = "ibm"
llm_model: str = "gpt-4o-mini"
llm_timeout: int = 15
llm_model: str = "deepseek-v4-flash"
llm_timeout: int = 120
llm_cache_dir: str = ".cache/llm"
coverage_default: str = "boundary"
rounding_mode: str = "TRUNCATE"
+29
View File
@@ -24,6 +24,24 @@ class TableDef:
sql_name: Optional[str] = None # COBOL SQL table name if different from YAML name
@dataclass
class SysinDef:
"""SYSIN card configuration for a single run scenario.
Each program defines its own sysin fields; period/modes are KIN08DBU specific.
"""
period: str | None = "202607"
include_invalid_period: bool = False
modes: list[str] = field(default_factory=lambda: ["NORMAL"])
@dataclass
class ScenarioDef:
"""A single run scenario: one set of inputs for one COBOL execution."""
id: str
sysin: SysinDef = field(default_factory=SysinDef)
inject_duplicate_pk: bool = False
@dataclass
class ProgramSchema:
program_id: str
@@ -31,6 +49,7 @@ class ProgramSchema:
subprograms: list[str] = field(default_factory=list)
db_type: str = "SQLite"
db_name: str = "OVERTIME.DB"
runs: list[ScenarioDef] = field(default_factory=list)
@classmethod
def from_yaml(cls, path: str | Path) -> ProgramSchema:
@@ -44,12 +63,22 @@ class ProgramSchema:
name=t["name"], columns=cols,
sql_name=t.get("sql_name"),
))
runs_raw = raw.get("runs", [])
runs = []
for r in runs_raw:
sysin_raw = r.get("sysin", {})
runs.append(ScenarioDef(
id=r["id"],
sysin=SysinDef(**sysin_raw),
inject_duplicate_pk=r.get("inject_duplicate_pk", False),
))
return cls(
program_id=raw["program_id"],
db_tables=tables,
subprograms=raw.get("subprograms", []),
db_type=raw.get("db_type", "SQLite"),
db_name=raw.get("db_name", "OVERTIME.DB"),
runs=runs,
)
+25
View File
@@ -52,3 +52,28 @@ db_tables:
subprograms:
- SUB02MSG
- SUB03END
runs:
- id: normal
# 標準シナリオ:P 卡有効 + RESET → 大部分の分岐をカバー
sysin:
period: "202607"
include_invalid_period: true
modes: ["NORMAL", "RESET"]
inject_duplicate_pk: false
- id: no_period
# #1 TNOT WRK-PERIOD-OK)をカバー:P 卡なし
sysin:
period: null
include_invalid_period: false
modes: ["NORMAL"]
inject_duplicate_pk: false
- id: insert_error
# #13 T / #28 TINSERT SQL エラー)をカバー:PK 競合行注入
sysin:
period: "202607"
include_invalid_period: true
modes: ["NORMAL", "RESET"]
inject_duplicate_pk: true
+53
View File
@@ -0,0 +1,53 @@
# 出力目录规则
## 单轮(runs: [] 或 YAML 不存在)
```text
runtime/{program_id}/
input/KIN08R01, KIN08S01 ← COBOL 入力平面文件
json/{program_id}.json ← JSONinput/working_storage/termination
output/KIN08W01 ← COBOL 出力平面文件
gcov/ ← .gcda/.gcno(直下)
```
## 多轮(runs: 定义在 config/programs/{program_id}.yaml 中)
```text
runtime/{program_id}/
run_{scenario_id}/
input/KIN08R01, KIN08S01 ← 該当场景の入力
json/{program_id}.json ← 該当场景の JSON
output/KIN08W01 ← 該当场景の COBOL 出力
gcov/
run_{scenario_id}/ ← 該当场景の .gcda/.gcno
```
## 单轮/多轮共有
- 编译: `work_dir/bin/{program_id}.exe`(全场景共享)
- .gcno: `work_dir/bin/{program_id}_pp.gcno`(编译时生成,COPY 到各场景)
- DB: `C:/Temp/gix/{program_id}_{scenario_id}.db`(多轮场景)或 `C:/Temp/gix/{program_id}.db`(单轮)
- 运行前 COPY 到 `C:/Temp/gix/{program_id}.db`COBOL 的 gixsql 硬编码连接此路径)
- SYSIN 文件: 由 `write_sysin_file(run_cfg=scenario.sysin)` 按场景生成
## 覆盖率合并(多轮)
- 各场景独立跑 gcov → 解析 `.cbl.gcov``{line: count}` 字典
- Python 级合并: `merged[line] = max(各场景的 count)`
- 合并后数据传给 `run_coverage()`
## YAML 场景定义格式
`config/programs/{program_id}.yaml` 追加 `runs:` 字段:
```yaml
runs:
- id: normal
sysin:
period: "202607" # P 卡値(null=无 P 卡)
include_invalid_period: true # 是否追加无效 P 卡
modes: ["NORMAL", "RESET"] # M 卡列表
inject_duplicate_pk: false
```
每个场景的 `sysin` 字段由该程序的 `write_sysin_file()` 解析,字段含义不跨程序通用。
@@ -0,0 +1,332 @@
# DesignDataGenerator — 式样书驱动测试数据生成
## 1. 概要
### 目的
V3 现有两条数据生成路径:
- **白盒数据**`cobol_testgen.generate_data()`):基于 MC/DC 路径枚举,覆盖决策点
- **LLM 测试数据**`Agent2Data.design()`):基于 FieldTree + DeepSeek API
缺少一条能读取日文详细设计书(`.md`)并生成机能测试数据的路径。
`DesignDataGenerator` 填补这个空缺。它解析式样书中的业务规则、输入输出定义、DB 结构,通过 LLM 生成有业务意义的测试数据。与白盒数据合并后,覆盖可以同时命中决策点分支和业务场景路径。
### 与其他 Agent 的关系
| Agent | 输入 | 输出 | 位置 |
|-------|------|------|------|
| `Agent1Parser` | COPYBOOK | FieldTree | `agents/agent1_parser.py` |
| **`DesignDataGenerator`** | **式样书 .md + 源码 + COPYBOOK** | **`list[dict]`(机能数据)** | **`agents/design_data.py`** ← 新增 |
| `Agent2Data` | FieldTree | TestSuiteLLM 数据) | `agents/agent2_data.py` |
| `Agent3Diagnostic` | FieldResult | 诊断文本 | `agents/agent3_diagnostic.py` |
## 2. 接口定义
### `DesignDataGenerator`
```python
class DesignDataGenerator:
def __init__(
self,
llm_client: LLMClient,
cpy_dirs: list[str | Path],
rules_dir: str | Path = "rules",
):
...
def generate(
self,
design_md_text: str, # 式样书 .md 全文
source_text: str, # COBOL 源码全文
file_db_md_text: str | None = None, # COPY句定义书 .md(可选)
db_md_text: str | None = None, # DB 定义书 .md(可选)
replacing_rules: dict[str, str] | None = None, # REPLACING 展开规则
v3_field_names: list[str] | None = None, # V3 字段名参考列表
) -> list[dict]:
"""生成机能测试数据。
Returns:
list[dict]: 每条记录为 {field_name: value} 格式。
字段名已被 REPLACING 展开,与 generate_data() 兼容。
"""
...
```
### 调用侧入口
```python
def generate_all_data(
program_id: str,
src_text: str,
copybook_dirs: list[str | Path],
st: dict | None = None, # extract_structure 结果(传入可跳过重复解析)
config: Config | None = None, # 用于读取 LLM / 设计书路径配置
design_doc_dir: str | Path | None = None, # 式样书目录
file_db_md_path: str | Path | None = None, # COPY句定义书路径
db_md_path: str | Path | None = None, # DB定义书路径
merge_strategy: str = "merge_to_normal", # merge_to_normal / as_separate_scenes / auto
) -> list[dict]:
"""白盒数据 + 机能数据 + 策略补充 全量生成与合并。
合并策略:
- merge_to_normal: 全部合到一条记录集中
- as_separate_scenes: 返回 (main_records, func_records),由调用方决定如何分场景
- auto: DEPRECATED
"""
...
```
## 3. 架构与数据流
```text
[式样书 .md] ──┐
[源码 .cbl] ───┤
[COPYBOOK] ────┤
[DB定义书 .md] ┘
DesignDataGenerator.generate()
├── InputParser 解析式样书 → ProgramMeta
├── RuleLoader 模式匹配 → 规则文本
├── PromptBuilder LLM 提示词
├── LLMClient DeepSeek API
├── 字段名映射 REPLACING 展开 + V3 字段名参考
└── 返回 list[dict]
generate_all_data()
├── ① generate_data() → 白盒数据
├── ② DesignDataGenerator → 机能数据(式样书存在时)
├── ③ strategy_supplement() → 策略补充
├── ④ 去重(基于 hash)
└── ⑤ 返回 merged_records
```
## 4. 字段名兼容性处理
### 问题
外部 Agent 从 COPYBOOK 解析字段名,含有未展开的 `(A)` 占位符:
```
(A)EMP-ID → V3 是 R01EMP-IDREPLACING ==(A)== BY ==R01== 后)
(A)DATE → V3 是 R01DATE
```
### 映射算法
```python
def _resolve_field_names(
records: list[dict],
replacing_rules: dict[str, str] | None,
v3_field_names: set[str] | None,
) -> list[dict]:
"""将外部 Agent 输出的字段名映射为 V3 兼容名称。"""
if not (replacing_rules or v3_field_names):
return records # 无需映射
result = []
for rec in records:
mapped = {}
for key, val in rec.items():
new_key = key
# 第一步:REPLACING 展开
if replacing_rules:
for old, new in replacing_rules.items():
if old in new_key:
new_key = new_key.replace(old, new)
# 第二步:如果 V3 字段名参考列表存在,精确匹配
if v3_field_names and new_key not in v3_field_names:
# 尝试去掉连字符
no_hyphen = new_key.replace("-", "")
if no_hyphen in v3_field_names:
new_key = no_hyphen
# 尝试下划线转空
no_underscore = new_key.replace("_", "")
if no_underscore in v3_field_names:
new_key = no_underscore
else:
continue # 无法映射 → 丢弃
mapped[new_key] = val
result.append(mapped)
return result
```
### REPLACING 规则提取
从 COBOL 源码中提取 REPLACING 规则:
```python
def _extract_replacing_rules(source_text: str) -> dict[str, str]:
"""从 COPY ... REPLACING ... 语句中提取替换规则。"""
rules = {}
for m in re.finditer(
r'COPY\s+(\w+)\s+REPLACING\s+(?:==(\w+)==\s+BY\s+==(\w+)==\s*)*\.',
source_text, re.IGNORECASE
):
for i in range(0, len(m.groups()) - 1, 2):
old = m.group(i + 2)
new = m.group(i + 3)
if old and new:
rules[old] = new
return rules
```
## 5. 去重策略
### 问题
白盒数据和机能数据可能包含重复记录(完全相同或高度相似)。重复会导致:
- COBOL 程序处理重复
- INSERT PK 冲突
- gcov 行计数失真
### 解决方案
```python
def _dedup(
main_records: list[dict],
additional_records: list[dict],
key_fields: list[str] | None = None,
) -> list[dict]:
"""合并+去重,additional 优先保留。"""
seen = set()
result = []
# 先处理 additional(机能数据优先)
for rec in additional_records:
h = _hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
# 后处理 main
for rec in main_records:
h = _hash(rec, key_fields)
if h not in seen:
seen.add(h)
result.append(rec)
return result
def _hash(rec: dict, key_fields: list[str] | None) -> tuple:
"""生成记录的特征哈希。"""
if key_fields:
return tuple(rec.get(k, '') for k in key_fields)
return tuple(sorted(rec.items()))
```
## 6. 多轮融合策略
### 策略选项
```yaml
# 在 config/programs/{pid}.yaml 或 CLI 参数中控制
merge_strategy: merge_to_normal # 默认值
```
| 策略 | 行为 | 适用场景 |
|------|------|---------|
| `merge_to_normal` | 机能数据合并到 normal 场景,与白盒数据一起执行 | 简单程序,一轮覆盖所有 |
| `as_separate_scenes` | 机能数据作为独立场景 `run_func/` 执行 | 复杂程序,业务场景与白盒路径不兼容 |
| `auto` | 有设计书时用 `as_separate_scenes`,否则 `merge_to_normal` | 通用场景 |
### `as_separate_scenes` 时的目录
```
runtime/{pid}/
run_normal/ ← 白盒数据(MC/DC 路径)
input/ json/ output/
run_func/ ← 机能数据(式样书驱动)
input/ json/ output/
gcov/run_normal/ ← 各场景独立 .gcda
gcov/run_func/
```
## 7. 调用链路
### DB 管道(`orchestrator_db.py:step2_generate_inputs()`
```python
# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)
# 改为
recs = generate_all_data(
program_id=self.program_id,
src_text=src_text,
copybook_dirs=cbd,
st=st,
config=self.config,
design_doc_dir=str(v3_root / "详细设计书"),
file_db_md_path=str(v3_root / "详细设计书" / "COPY句定义书.md"),
)
```
### 原生管道(`cobol_testgen/__init__.py:main()`
```python
# 原本
recs = generate_data(src_text, st, copybook_dirs=cbd)
# 改为
recs = generate_all_data(
program_id=filepath.stem,
src_text=source,
copybook_dirs=[str(filepath.parent / '..' / 'cpy')],
st=st,
design_doc_dir=str(design_doc_base),
)
```
## 8. 外部 Agent 代码迁移清单
### 从 `jcl-cobol-data-create/` 迁移
| 源文件 | 目标 | 变更 |
|--------|------|------|
| `input_parser.py` | `agents/design_data_input_parser.py` | 接收文本而非路径 |
| `rule_loader.py` | 内置到 `design_data.py` 方法 | 简化接口 |
| `prompt_builder.py` | 内置到 `design_data.py` | 使用 `LLMClient` |
| `api_client.py` | **不需要** | V3 已有 `agents/llm.py` |
| `output_writer.py` | **不需要** | 不写文件 |
| `models.py` | 部分并入 `design_data.py` | 按需移植数据类 |
| `rules/pgm_pattern/*.md` | `rules/pgm_pattern/` | 直接复制 |
| `rules/special_feature/*.md` | `rules/special_feature/` | 直接复制 |
| `layout/*.md` | `layout/` | 参考用 |
### 新建文件清单
| 文件 | 说明 |
|------|------|
| `agents/design_data.py` | DesignDataGenerator 主类 |
| `agents/design_data_input_parser.py` | 式样书解析器(从外部 Agent 移植) |
| `cobol_testgen/data_merger.py` | `generate_all_data()` + 去重 + 字段名映射 |
## 9. 错误处理
### 场景
| 场景 | 行为 |
|------|------|
| 式样书 .md 不存在 | 跳过机能数据,仅返回白盒数据 |
| LLM 调用超时 | 捕获异常,跳过机能数据,日志告警 |
| LLM 返回非法 JSON | `try/except`,返回空列表 |
| 字段名映射失败(无法匹配 V3 字段名) | 丢弃该字段,日志记录 |
| 式样书存在但无法解析(格式不标准) | `try/except`,跳过机能数据 |
## 10. 测试计划
| 测试 | 方法 |
|------|------|
| 式样书不存在时回退到仅白盒 | mock `os.path.exists` → False |
| LLM 超时/出错时回退 | mock `LLMClient.call` → raise TimeoutError |
| 字段名 REPLACING 映射 | Fixture 含 `(A)EMP-ID` → 期待 `R01EMP-ID` |
| 去重逻辑 | 白盒 10 条 + 机能 5 条(含 2 条重复)→ 期待 13 条 |
| 多轮融合策略 `as_separate_scenes` | 验证 `run_func/` 目录被创建 |
| 实际 COBOL 执行 + gcov | KIN08DBU 白盒+机能合并后覆盖率是否 >= 原 52/60 |
+347
View File
@@ -0,0 +1,347 @@
# DB定義書
## 変更履歴
| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 |
|----|---------|--------|--------|--------|------|
| 1 | 新規作成(全8テーブル) | AI | 2026/06/23 | | サブシステムA(6) + B(2) |
| 2 | SALARYDB 4テーブル追加 | AI | 2026/07/08 | | サブシステムC(給与計算) |
---
# EMP_MASTER — 社員マスタ
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | EMP_MASTER | — | PK: EMP_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 部署ID | DEPT_ID | CHAR | 4 | | 4 | NOT NULL | | | | | | |
| 3 | 氏名 | EMP_NAME | VARCHAR | 50 | | 50 | NOT NULL | | | | | | |
| 4 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=在籍, '9'=退職 |
### 備考
- サブシステムA: 参照のみ(SELECT)
- サブシステムB: 使用しない
---
# LEAVE_RECORDS — 休暇申請記録
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | LEAVE_RECORDS | — | PK: APPLICATION_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 申請番号 | APPLICATION_ID | INTEGER | 10 | | 4 | NOT NULL | | ✓ | | | | 自動採番 |
| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | |
| 3 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | | | | | 01/02/03/04 |
| 4 | 開始日 | START_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 6 | 終了日 | END_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 7 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=有効, '9'=取消 |
### 備考
- KIN02UPDがINSERT/DELETEを実行
- KIN03EXPがSELECT(日付展開用)
- 取消はDELETE FROM で物理削除
---
# HOLIDAY_CALENDAR — 休日カレンダー
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | HOLIDAY_CALENDAR | — | PK: HOLIDAY_DATE |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 休日日付 | HOLIDAY_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD |
| 2 | 説明 | DESCRIPTION | VARCHAR | 50 | | 50 | NULL許可 | NULL | | | | | 例:「建国記念の日」 |
### 備考
- 土日は曜日判定で処理するため、このテーブルには祝日のみ格納
- KIN03EXP, KIN06CLDがSELECTで参照
---
# SICK_LEAVE_RATE — 病欠控除率
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | SICK_LEAVE_RATE | — | PK: LEAVE_TYPE |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | ✓ | | | | '04'固定 |
| 2 | 控除率 | DEDUCTION_RATE | DECIMAL | 3 | 2 | 3 | NOT NULL | | | | | | 例:0.5050%控除) |
### 備考
- 全社員共通の設定値
- 現時点では参照プログラム未実装(設計上定義)
---
# DAILY_RECORDS — 日別勤怠記録
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | DAILY_RECORDS | — | PK: (EMP_ID, TARGET_DATE) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象日 | TARGET_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD |
| 3 | 出勤時刻 | TIME_IN | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし |
| 4 | 退勤時刻 | TIME_OUT | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし |
| 5 | 年休時間 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 6 | 事假時間 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 7 | 因公特批假時間 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 8 | 病欠時間 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 9 | 未申請欠勤時間 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 10 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP |
### 備考
- KIN08DBUがKIN07DAIの出力ファイルからINSERT
- 1社員1日=1レコード。出勤日のみ存在。
---
# MONTHLY_ABSENCE — 月次統計
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | MONTHLY_ABSENCE | — | PK: (EMP_ID, YEAR_MONTH) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 年休合計 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 4 | 事假合計 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 5 | 因公特批假合計 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 6 | 病欠合計 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 7 | 未申請欠勤合計 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 8 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP |
### 備考
- KIN08DBUが月次集計後にUPSERTMERGE
- サブシステムC(給与計算)への連携元
---
# OVT_APPLICATIONS — 個別加班申請テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 残業統計管理DB | OVT_APPLICATIONS | — | PK: APPL_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 申請番号 | APPL_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD+SEQ |
| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | |
| 3 | 申請日 | APPL_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 4 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | | | | | W=平日, H=休日 |
| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 6 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 7 | 加班時間 | OVT_HOURS | DECIMAL | 4 | 1 | 4 | NOT NULL | | | | | | 0.1h単位 |
| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | 0=有効, 9=取消 |
| 9 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | |
### 備考
- ZAN06UPDがINSERT/UPDATEを実行
- STATUS=9で取消(物理削除はしない)
---
# OVT_MONTHLY — 月次集計テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 残業統計管理DB | OVT_MONTHLY | — | PK: (EMP_ID, YEAR_MONTH, OVT_TYPE) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | ✓ | | | | W=平日, H=休日 |
| 4 | 加班時間合計 | OVT_HOURS | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 5 | 加班回数 | OVT_COUNT | INTEGER | 10 | | 4 | NOT NULL | | | | | | |
| 6 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | |
### 備考
- ZAN06UPDが月次集計結果をUPSERT
---
# EMP-MASTERSALARYDB — 社員マスタ(給与計算用)
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | EMP-MASTER | KYU01REC | PK: EMP-ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 氏名 | EMP-NAME | VARCHAR | 40 | | 40 | NOT NULL | | | | | | |
| 3 | 部署コード | DEPT-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | |
| 4 | 地域コード | REGION-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | |
| 5 | 職種コード | CATEGORY-CODE | CHAR | 3 | | 3 | NOT NULL | | | | | | |
| 6 | 基本給 | BASE-SALARY | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 7 | 時給 | HOURLY-RATE | DECIMAL | 7 | 0 | 4 | NOT NULL | | | | | | |
| 8 | 扶養人数 | DEPENDENT-COUNT | SMALLINT | — | | 2 | NOT NULL | | | | | | |
| 9 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '0'=在籍, '9'=退職 |
| 10 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU02REGがINSERT/UPSERTを実行
- KYU04CALがSELECTで参照
---
# TAX-TABLESALARYDB — 所得税率テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | TAX-TABLE | — | PK: (TAX-FROM, TAX-TO) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 課税下限 | TAX-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 2 | 課税上限 | TAX-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 3 | 税率 | TAX-RATE | DECIMAL | 5 | 4 | 3 | NOT NULL | | | | | | |
| 4 | 控除額 | DEDUCTION | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
### 備考
- KYU05DEDがSELECT ... BETWEENで参照
- 課税所得に対する超過累進税率
---
# INSURANCE-TABLESALARYDB — 社会保険料率テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | INSURANCE-TABLE | — | PK: (INS-INCOME-FROM, INS-INCOME-TO) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 所得下限 | INS-INCOME-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 2 | 所得上限 | INS-INCOME-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 3 | 健康保険料率 | INS-HEALTH-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | |
| 4 | 厚生年金保険料率 | INS-PENSION-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | |
| 5 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU05DEDが内部テーブル(SEARCH ALL)に取込んで参照
- 被保険者負担分のみ
---
# SALARY-RESULTSSALARYDB — 給与計算結果テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | SALARY-RESULTS | — | PK: (EMP-ID, YEAR-MONTH) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR-MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 総支給額 | GROSS-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 4 | 源泉所得税 | INCOME-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 5 | 社会保険料 | INSURANCE | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 6 | 住民税 | RESIDENT-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 7 | 差引支給額 | NET-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 8 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU06UPDがINSERT/UPSERTを実行
+435
View File
@@ -0,0 +1,435 @@
# cobol_testgen v4 生成 JSON — 格式说明 v2.0
## 修订履历
| 版本 | 日期 | 变更内容 |
|------|------|---------|
| v2.0 | | 明确1个json文件=1组数据(group)的命名规则,格式本身与v1相同 |
## 说明
v2.0 **格式本身与v1完全一致**,仅补充:
1. 一组数据单独放一个json文件
2. 一个文件内的`records`数组包含该组的全部处理单元
3. 命名规则追加 `_g{groupId}` 标识组号
---
## 完整规格 JSON`output/json/{プログラム名}_g{groupId}.json`
### 最外层结构
```json
{
"program": "SAN01MAT",
"records": [
{
"input": { ... }
}
]
}
```
**1条record = 1次处理单元**。例如匹配(1:1)时:
- 匹配成功:record内同时包含两个输入FD
- R01-onlyrecord内只含R01的FD
- R02-onlyrecord内只含R02的FD
多个record按处理顺序排列,组成一组完整的测试数据。
---
## 例1:单文件CSV输入(1组1条记录)
文件 `ZAN01CHK_g1.json`
```json
{
"program": "ZAN01CHK",
"records": [
{
"input": {
"R01INNFIL": {
"R01LINE": "A0000000, , , , ,0, , "
}
}
}
]
}
```
## 例2:单文件二进制输入(1组多条记录)
文件 `ZAN02CHK_g1.json`
```json
{
"program": "ZAN02CHK",
"records": [
{
"input": {
"R01INNFIL": {
"R01APPL-ID": "A0000001",
"R01EMP-ID": "00000101",
"R01APPL-DATE": "20000101",
"R01START-TIME":"0201",
"R01END-TIME": "0301",
"R01STATUS": "B",
"R01OVT-TYPE": "C",
"R01FILLER": "D000000000000000000000000000000000000000000001"
}
}
},
{
"input": {
"R01INNFIL": {
"R01APPL-ID": "A0000002",
"R01EMP-ID": "00000102",
"R01APPL-DATE": "20000102",
"R01START-TIME":"0202",
"R01END-TIME": "0302",
"R01STATUS": "C",
"R01OVT-TYPE": "D",
"R01FILLER": "D000000000000000000000000000000000000000000002"
}
}
}
]
}
```
## 例3:多文件输入(与v1相同)
文件 `ZAN03CHK_g1.json`
```json
{
"program": "ZAN03CHK",
"records": [
{
"input": {
"R01INNFIL": {
"R01APPL-ID": "A0000001",
"R01EMP-ID": "00000101",
"R01APPL-DATE": "20000101",
"R01START-TIME":"0201",
"R01END-TIME": "0301",
"R01STATUS": "B",
"R01OVT-TYPE": "C",
"R01FILLER": "D000000000000000000000000000000000000000000001"
},
"R02INNFIL": {
"R02EMP-ID": "00000401",
"R02DATE": "20000101",
"R02TIME-IN": "0501",
"R02TIME-OUT": "0601",
"R02FILLER": "E0000000000000000000000000000000000000000000000000000001"
},
"R03INNFIL": {
"R03HOLIDAY-DATE": "20000101",
"R03HOLIDAY-FLG": "F",
"R03FILLER": "G0000000000000000000000000000000000000000000000000000000000000000000001"
}
}
}
]
}
```
## 例4:二文件匹配(マッチング1:1)
v1中ZAN04MAT示例是1条record = 1次匹配处理。
对于匹配(1:1)测试,**1组数据 = 1个json文件**。1组内包含多条recordR01-only、R02-only、匹配),按处理顺序排列。
### 文件1`SAN01MAT_g1.json`
两端不匹配:先处理R01-only(a001) → 3次匹配(a002/b002, a003/b003, a004/b004) → 最后R02-only(a005)。
```json
{
"program": "SAN01MAT",
"records": [
{
"input": {
"R01INNFIL": {
"R01A001": "a001",
"R01A002": "b001",
"R01A003": "c001",
"R01DATA": "c00000000011"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a002",
"R01A002": "b002",
"R01A003": "c002",
"R01DATA": "c00000000012"
},
"R02INNFIL": {
"R02A001": "a002",
"R02A002": "b002",
"R02A003": "c002",
"R02DATA": "c00000000012"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a003",
"R01A002": "b003",
"R01A003": "c003",
"R01DATA": "c00000000013"
},
"R02INNFIL": {
"R02A001": "a003",
"R02A002": "b003",
"R02A003": "c003",
"R02DATA": "c00000000013"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a004",
"R01A002": "b004",
"R01A003": "c004",
"R01DATA": "c00000000014"
},
"R02INNFIL": {
"R02A001": "a004",
"R02A002": "b004",
"R02A003": "c004",
"R02DATA": "c00000000014"
}
}
},
{
"input": {
"R02INNFIL": {
"R02A001": "a005",
"R02A002": "b005",
"R02A003": "c005",
"R02DATA": "c00000000015"
}
}
}
]
}
```
### 文件2`SAN01MAT_g2.json`
反向两端不匹配:先处理R02-only(a001) → 3次匹配 → 最后R01-only(a005)。
```json
{
"program": "SAN01MAT",
"records": [
{
"input": {
"R02INNFIL": {
"R02A001": "a001",
"R02A002": "b001",
"R02A003": "c001",
"R02DATA": "c00000000011"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a002",
"R01A002": "b002",
"R01A003": "c002",
"R01DATA": "c00000000012"
},
"R02INNFIL": {
"R02A001": "a002",
"R02A002": "b002",
"R02A003": "c002",
"R02DATA": "c00000000012"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a003",
"R01A002": "b003",
"R01A003": "c003",
"R01DATA": "c00000000013"
},
"R02INNFIL": {
"R02A001": "a003",
"R02A002": "b003",
"R02A003": "c003",
"R02DATA": "c00000000013"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a004",
"R01A002": "b004",
"R01A003": "c004",
"R01DATA": "c00000000014"
},
"R02INNFIL": {
"R02A001": "a004",
"R02A002": "b004",
"R02A003": "c004",
"R02DATA": "c00000000014"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a005",
"R01A002": "b005",
"R01A003": "c005",
"R01DATA": "c00000000015"
}
}
}
]
}
```
### 文件3`SAN01MAT_g3.json`
中间不匹配:先匹配(a001) → R01-only(a003) → 匹配(a004) → R02-only(a002) → 匹配(a005)。
```json
{
"program": "SAN01MAT",
"records": [
{
"input": {
"R01INNFIL": {
"R01A001": "a001",
"R01A002": "b001",
"R01A003": "c001",
"R01DATA": "c00000000011"
},
"R02INNFIL": {
"R02A001": "a001",
"R02A002": "b001",
"R02A003": "c001",
"R02DATA": "c00000000011"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a003",
"R01A002": "b003",
"R01A003": "c003",
"R01DATA": "c00000000013"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a004",
"R01A002": "b004",
"R01A003": "c004",
"R01DATA": "c00000000014"
},
"R02INNFIL": {
"R02A001": "a004",
"R02A002": "b004",
"R02A003": "c004",
"R02DATA": "c00000000014"
}
}
},
{
"input": {
"R02INNFIL": {
"R02A001": "a002",
"R02A002": "b002",
"R02A003": "c002",
"R02DATA": "c00000000012"
}
}
},
{
"input": {
"R01INNFIL": {
"R01A001": "a005",
"R01A002": "b005",
"R01A003": "c005",
"R01DATA": "c00000000015"
},
"R02INNFIL": {
"R02A001": "a005",
"R02A002": "b005",
"R02A003": "c005",
"R02DATA": "c00000000015"
}
}
}
]
}
```
---
## DB类型时的输出
输入类型为DB时,每组生成一个独立的SQL insert文件。
| 文件种类 | 命名规则 | 例 |
|---------|--------|-----|
| Insert SQL(一组) | `{プログラムID}_g{groupId}.sql` | `SAN01MAT_g1.sql` |
例(组1):
```sql
-- Group: 1
-- 用途: 员工主表基础数据
INSERT INTO EMPLOYEE (EMP_ID, EMP_NAME, BIRTH_DATE, SALARY) VALUES
('0000000001', '测试太郎', '1990-04-01', 350000.00),
('0000000002', '测试花子', '1995-07-15', 420000.00);
```
---
## 字段值规则
| PIC | JSON内表示 | 例 |
|-----|-----------|-----|
| `PIC X(n)` | 左对齐 + 空格填充 | `"A0000001"` (8桁) |
| `PIC 9(n)` | 右对齐 + 前补零 | `"00000101"` (8桁) |
| `PIC S9(n)` | 符号 + 右对齐 + 前补零 | `"+0000101"` |
| `PIC S9(n)V9(m)` | 符号 + 右对齐 + 含小数点 | `"+001234567"` (V9(2)时实际为 +0012345.67) |
| `PIC S9(n) COMP` | 十进制数字字符串(含符号) | `"300"``"-100000"` |
| `PIC S9(n) COMP-3` | 十进制数字字符串(含符号) | `"1234"``"-5678"` |
| `PIC 9(n) COMP-3` | 十进制数字字符串(无符号) | `"1234"` |
| FILLER(纯保留) | 字段名为`FILLER`的未使用区域 | 含有组ID和记录编号的可辨识模式 `"D000000...001"` |
| FILLER(业务保留) | 字段名有具体名称但实际未使用的保留区 | PIC X(n)填全空格、PIC 9(n)填全零 |
---
## 输出目录结构
```
output/
├── json/ # 完整JSON(每组一个文件,输入类型=文件时输出)
│ ├── SAN01MAT_g1.json
│ ├── SAN01MAT_g2.json
│ └── SAN01MAT_g3.json
└── db/ # DB insert SQL(每组一个文件,输入类型=DB时输出)
├── SAN01MAT_g1.sql
├── SAN01MAT_g2.sql
└── SAN01MAT_g3.sql
```
+432
View File
@@ -0,0 +1,432 @@
IDENTIFICATION DIVISION.
PROGRAM-ID. SAN01MAT.
*****************************************************************
* システム名 : サンプルシステム *
* プログラムID : SAN01MAT *
* プログラム名 : XXXXXX処理 *
* 作成日 : YYYY-MM-DD *
* 処理概要 : マッチング(1:1)を行う。 *
* マッチの場合、ファイルW01を出力する。 *
* ファイルR01のみの場合ファイルW02を出力する。*
* ファイルR02のみの場合ファイルW03を出力する。*
* *
*****************************************************************
* 更新履歴 *
*---------------------------------------------------------------*
* 更新日付 担当者 更新内容 *
*---------------------------------------------------------------*
* YY-MM-DD @@@ 新規作成 *
* *
*****************************************************************
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
SOURCE-COMPUTER. IBM-ZSERIES.
OBJECT-COMPUTER. IBM-ZSERIES.
*
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT R01INNFIL ASSIGN TO SAN01R01.
SELECT R02INNFIL ASSIGN TO SAN01R02.
SELECT W010UTFIL ASSIGN TO SAN01W01.
SELECT W020UTFIL ASSIGN TO SAN01W02.
SELECT W030UTFIL ASSIGN TO SAN01W03.
*
DATA DIVISION.
FILE SECTION.
*
*****************************************************************
* ##RO1## *
*****************************************************************
FD R01INNFIL
LABEL RECORD IS STANDARD
BLOCK CONTAINS 0
RECORDING MODE IS F.
01 RO1INNREC.
COPY BBBBBFC REPLACING ==(A)== BY ==R01==.
*
*****************************************************************
* ##RO2## *
*****************************************************************
FD R02INNFIL
LABEL RECORD IS STANDARD
BLOCK CONTAINS 0
RECORDING MODE IS F.
01 RO2INNREC.
COPY BBBBBFC REPLACING ==(A)== BY ==R02==.
*
*****************************************************************
* ##WO1## *
*****************************************************************
FD W010UTFIL
LABEL RECORD IS STANDARD
BLOCK CONTAINS 0
RECORDING MODE IS F.
01 WO10UTREC.
COPY BBBBBFC REPLACING ==(A)== BY ==WO1==.
*
*****************************************************************
* ##WO2## *
*****************************************************************
FD W020UTFIL
LABEL RECORD IS STANDARD
BLOCK CONTAINS 0
RECORDING MODE IS F.
01 WO20UTREC.
COPY BBBBBFC REPLACING ==(A)== BY ==WO2==.
*
*****************************************************************
* ##W03## *
*****************************************************************
FD W030UTFIL
LABEL RECORD IS STANDARD
BLOCK CONTAINS 0
RECORDING MODE IS F.
01 WO30UTREC.
COPY BBBBBFC REPLACING ==(A)== BY ==WO3==.
*
WORKING-STORAGE SECTION.
*
*****************************************************************
* SYSIN領域 *
*****************************************************************
*01 SYSARA.
*
*****************************************************************
* コンスタント領域 *
*****************************************************************
01 CNSARA.
03 CNS-PRGIDX PIC X(008) VALUE 'SAN01MAT'.
03 CNS-UNYSUTKES-1 PIC X(001) VALUE '1'.
03 CNS-CFKFCA PIC X(001) VALUE 'N'.
03 CNS-MSGSTR PIC 9(003) VALUE 001.
03 CNS-MSGFIN PIC 9(003) VALUE 002.
03 CNS-MSGSUBEEK PIC 9(003) VALUE 005.
03 CNS-MSGIINKES PIC 9(003) VALUE 006.
03 CNS-MSGOUTKES PIC 9(003) VALUE 007.
03 CNS-MSGKEYINF PIC 9(003) VALUE 033.
03 CNS-KN0002 PIC 9(001) VALUE 2.
03 CNS-ABD999 PIC 9(003) VALUE 999.
*
*****************************************************************
* フラグ領域 *
*****************************************************************
*01 FLGARA.
*
*****************************************************************
* カウンタ領域 *
*****************************************************************
01 CUNARA.
03 CUN-RO1INN PIC S9(009) COMP-3 VALUE ZERO.
03 CUN-R02INN PIC S9(009) COMP-3 VALUE ZERO.
03 CUN-WO1OUT PIC S9(009) COMP-3 VALUE ZERO.
03 CUN-WO2OUT PIC S9(009) COMP-3 VALUE ZERO.
03 CUN-WO3OUT PIC S9(009) COMP-3 VALUE ZERO.
*
*****************************************************************
* 作業領域 *
*****************************************************************
01 WRKARA.
*** 運用日付
03 WRK-U06 PIC 9(008).
03 WRK-U06ARA REDEFINES WRK-U06.
05 WRK-UYOYMX PIC 9(006).
05 WRK-UYOHII PIC 9(002).
*** マッチングキー
03 WRK-R01KEY.
05 WRK-R01KEY001 PIC X(010).
05 WRK-R01KEY002 PIC X(010).
05 WRK-R01KEY003 PIC X(010).
03 WRK-R02KEY.
05 WRK-R02KEY001 PIC X(010).
05 WRK-R02KEY002 PIC X(010).
05 WRK-R02KEY003 PIC X(010).
*
*****************************************************************
* サブプログラム連絡領域 *
*****************************************************************
*** 運用日付取得
COPY D01010AC.
*** 入力件数表示/入力レコードシーケンスチェックSR用
COPY S01MSGAC.
*** メッセージ編集出力SR用
COPY MSG000AC.
*
PROCEDURE DIVISION.
*****************************************************************
* サブモジュールNO: (0.0) *
* サブモジュール名: 制御処理 *
* 処理概要 : メインコントロール処理 *
*****************************************************************
0000MAJCOLSOR SECTION.
*
*** 初期処理
PERFORM 1000ITTSOR.
*
*** メイン処理
PERFORM 2000MAJSOR
UNTIL WRK-R01KEY = HIGH-VALUE
AND WRK-R02KEY = HIGH-VALUE.
*
*** 終了処理
PERFORM 3000STPSOR.
*
0000MAJCOLSOREXT.
GOBACK.
*****************************************************************
* サブモジュールNO: (1.0) *
* サブモジュール名: 初期処理 *
* 処理概要 : 開始メッセージ出力・各種初期化処理 *
*****************************************************************
1000ITTSOR SECTION.
*
*** 開始メッセージ出力
INITIALIZE M00MHOPAR.
MOVE CNS-MSGSTR TO M00MSGCOD.
PERFORM 4000MSGOUTSOR.
*
*** コンバイル日時出力
INITIALIZE M00MHOPAR.
MOVE CNS-MSGKEYINF TO M00MSGCOD.
MOVE FUNCTION WHEN-COMPILED TO M00UMKDATS22(1).
MOVE 'COMPILED' TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
*** ワークエリア初期化
INITIALIZE WRKARA.
*
*** 運用日付取得
INITIALIZE D01UBSPAR.
CALL 'DAT01S0' USING D01UBSPAR.
IF D01FKICOD = ZERO
MOVE D01U06 TO WRK-U06
ELSE
INITIALIZE M00MHOPAR
MOVE CNS-MSGSUBEEK TO M00MSGCOD
MOVE 'DAT01SO' TO M00UMKDATS22(1)
MOVE D01FKICOD TO M00UMKDATS22(2)
PERFORM 4000MSGOUTSOR
PERFORM 9999ABDSOR
END-IF.
*
*** 入出力ファイルOPEN
OPEN INPUT R01INNFIL
R02INNFIL
OUTPUT W010UTFIL
W020UTFIL
W030UTFIL.
*
*** #R01#を読み込み
PERFORM 1100R01INNSOR.
*** #R02#を読み込み
PERFORM 1200R02INNSOR.
*
1000ITTSOREXT.
EXIT.
*****************************************************************
* サブモジュールNO:(1.1) *
* サブモジュール名:##RO1##読込処理 *
* 処理概要 :レコード読込・キー設定・SEQチェック処理 *
*****************************************************************
1100R01INNSOR SECTION.
*
READ R01INNFIL
AT END
MOVE HIGH-VALUE TO WRK-R01KEY
NOT AT END
ADD 1 TO CUN-RO1INN
MOVE R01A001 TO WRK-R01KEY001
MOVE R01A002 TO WRK-R01KEY002
MOVE R01A003 TO WRK-R01KEY003
*** 入力件数表示/シーケンスチェック処理
MOVE 'SAN01R01' TO S01DDM.
MOVE CNS-UNYSUTKES-1 TO SO1HYOKESKJCKUB
MOVE CNS-CFKFCA TO S01JKDKIYFLG
MOVE WRK-R01KEY TO SO1SEQCECKOM
COPY CHK01SRP REPLACING ==(A)==BY ==S01==.
END-READ.
*
1100R01INNSOREXT.
EXIT.
*****************************************************************
* サブモジュールNO:(1.2) *
* サブモジュール名:##RO2##読込処理 *
* 処理概要 :レコード読込・キー設定・SEQチェック処理 *
*****************************************************************
1200R02INNSOR SECTION.
*
READ R02INNFIL
AT END
MOVE HIGH-VALUE TO WRK-R02KEY
NOT AT END
ADD 1 TO CUN-R02INN
MOVE R02A001 TO WRK-R02KEY001
MOVE R02A002 TO WRK-R02KEY002
MOVE R02A003 TO WRK-R02KEY003
*** 入力件数表示/シーケンスチェック処理
MOVE 'SAN01R02' TO S01DDM.
MOVE CNS-UNYSUTKES-1 TO SO1HYOKESKJCKUB
MOVE CNS-CFKFCA TO S01JKDKIYFLG
MOVE WRK-R02KEY TO SO1SEQCECKOM
COPY CHK01SRP REPLACING ==(A)== BY ==S01==.
END-READ.
*
1200R02INNSOREXT.
EXIT.
*****************************************************************
* サブプログラムNO:(2.0) *
* サブプログラム名:主処理 *
* 処理概要 :マッチング(1:1)を行う *
*****************************************************************
2000MAJSOR SECTION.
*
EVALUATE TRUE
*** マッチ
WHEN WRK-R01KEY = WRK-R02KEY
*** #WO1#を編集出力処理
PERFORM 2100W010UTSOR
*
*** #RO1#を読み込み
PERFORM 1100R01INNSOR
*** #RO2#を読み込み
PERFORM 1200R02INNSOR
*
*** アンマッチ(##R01##のみ)
WHEN WRK-R01KEY < WRK-R02KEY
*** #WO2#を編集出力処理
PERFORM 2200W020UTSOR
*
*** #RO1#を読み込み
PERFORM 1100R01INNSOR
*
*** アンマッチ(##R02##のみ)
WHEN WRK-R01KEY > WRK-R02KEY
*** #WO3#を編集出力処理
PERFORM 2300W030UTSOR
*
*** #R02#を読み込み
PERFORM 1200R02INNSOR
END-EVALUATE.
*
2000MAJSOREXT.
EXIT.
*****************************************************************
* サブモジュールNO:(2.1) *
* サブモジュール名:##WO1##編集出力処理 *
* 処理概要 :レコード編集・出力処理 *
*****************************************************************
2100W010UTSOR SECTION.
*
MOVE RO1INNREC TO W010UTREC.
MOVE R02INNREC TO W010UTREC.
*
WRITE WO1OUTREC.
ADD 1 TO CUN-WO1OUT.
2100W010UTSOREXT.
EXIT.
*****************************************************************
* サブプログラムNO:(2.2) *
* サブプログラム名:##WO2#を編集出力処理 *
* 処理概要 :レコード編集・出力処理 *
*****************************************************************
2200W020UTSOR SECTION.
*
MOVE RO1INNREC TO W020UTREC.
*
WRITE WO2OUTREC.
ADD 1 TO CUN-WO2OUT.
*
2200W020UTSOREXT.
EXIT.
*****************************************************************
* サブプログラムNO:(2.3) *
* サブプログラム名:##WO3#を編集出力処理 *
* 処理概要 :レコード編集・出力処理 *
*****************************************************************
2300W030UTSOR SECTION.
*
MOVE R02INNREC TO W03OUTREC.
*
WRITE WO3OUTREC.
ADD 1 TO CUN-WO3OUT.
*
2300W030UTSOREXT.
EXIT.
*****************************************************************
* サブモジュールNO:(3.0) *
* サブモジュール名:終了処理 *
* 処理概要 :ファイルクローズ・件数と終了メッセージ出力 *
*****************************************************************
3000STPSOR SECTION.
*
*** 入出力ファイルCLOSE
CLOSE R01INNFIL
R02INNFIL
W010UTFIL
W020UTFIL
W030UTFIL.
*
*** 入出力ファイル件数出力
INITIALIZE M00MHOPAR.
MOVE CNS-MSGIINKES TO M00MSGCOD.
MOVE 'SAN01R01' TO M00UMKDATS22(1).
MOVE CUN-RO1INN TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
INITIALIZE M00MHOPAR.
MOVE CNS-MSGIINKES TO M00MSGCOD.
MOVE 'SAN01R02' TO M00UMKDATS22(1).
MOVE CUN-R02INN TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
INITIALIZE M00MHOPAR.
MOVE CNS-MSGIINKES TO M00MSGCOD.
MOVE 'SAN01W01' TO M00UMKDATS22(1).
MOVE CUN-WO1OUT TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
INITIALIZE M00MHOPAR.
MOVE CNS-MSGIINKES TO M00MSGCOD.
MOVE 'SAN01W02' TO M00UMKDATS22(1).
MOVE CUN-WO2OUT TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
INITIALIZE M00MHOPAR.
MOVE CNS-MSGIINKES TO M00MSGCOD.
MOVE 'SAN01W03' TO M00UMKDATS22(1).
MOVE CUN-WO3OUT TO M00UMKDATS22(2).
PERFORM 4000MSGOUTSOR.
*
*** 終了メッセージ出力
INITIALIZE M00MHOPAR.
MOVE CNS-MSGFIN TO M00MSGCOD.
PERFORM 4000MSGOUTSOR.
*
3000STPSOREXT.
EXIT.
*****************************************************************
* サブプログラムNO:(4.0) *
* サブプログラム名:メッセージ編集出力処理 *
* 処理概要 :メッセージ編集出力サブPGM呼出 *
*****************************************************************
4000MSGOUTSOR SECTION.
*
MOVE CNS-KN0002 TO M00KNOCOD
MOVE CNS-KN0002 TO M00SYUCOD.
MOVE CNS-PRGIDX TO M00PRGIDX.
CALL 'MSG0SRP' USING M00MSGPAR.
*
4000MSGOUTSOREXT.
EXIT.
*****************************************************************
* サブプログラムNO:(9.9) *
* サブプログラム名:ABEND処理 *
* 処理概要 :ABENDサブPGM呼出 *
*****************************************************************
9999ABDSOR SECTION.
*
MOVE CNS-ABD999 TO M00ABDCOD.
CALL 'END01TO' USING M00MSGPAR.
*
9999ABDSOREXT.
EXIT.
+141
View File
@@ -0,0 +1,141 @@
# 詳細設計書
## 基本情報
| # | 項目 | 内容 |
|---|------|------|
| 1 | システム名 | サンプルシステム |
| 2 | プログラムID | SAN01MAT |
| 3 | プログラム名 | XXXXXX処理 |
| 4 | PGMパターン | マッチング(1:1) |
| 5 | 機能概要 | マッチング(1:1)を行う。 |
| 6 | | マッチの場合、ファイルW01を出力する。 |
| 7 | | ファイルR01のみの場合ファイルW02を出力する。 |
| 8 | | ファイルR02のみの場合ファイルW03を出力する。 |
※PGMパターン:マッチング(1:1、1:N、M:N)、レイアウト編集のみ(GETPUT)、項目チェック、振り分け、キーブレイク、キーブレイク(集計、集約)、DB更新
### 前提条件
| NO | 対象ファイル | 条件 |
|----|-------------|------|
| 1 | ファイルR01 | キー項目A001>A002>A003で昇順でソート |
| 2 | ファイルR02 | キー項目A001>A002>A003で昇順でソート |
### 使用ファイル一覧
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 |
|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------|
| 1 | ファイルR01 | R01 | SAN01R01 | I | BBBBBFC | FB | | 120 | PS | |
| 2 | ファイルR02 | R02 | SAN01R02 | I | BBBBBFC | FB | | 120 | PS | |
| 3 | ファイルW01 | W01 | SAN01W01 | O | BBBBBFC | FB | | 120 | PS | |
| 4 | ファイルW02 | W02 | SAN01W02 | O | BBBBBFC | FB | | 120 | PS | |
| 5 | ファイルW03 | W03 | SAN01W03 | O | BBBBBFC | FB | | 120 | PS | |
### キー項目一覧
| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) |
|----|-----------|---------------------|-------------------------------------------|
| 1 | ファイルR01 | A001>A002>A003(重複NG | A001>A002>A003 |
| 2 | ファイルR02 | A001>A002>A003(重複NG | A001>A002>A003 |
### 使用モジュール一覧
| NO | 機能 | プログラムID | 使用COPY名 |
|----|------|-------------|-----------|
| 1 | 入力件数表示/シーケンスチェック処理SUB | CHK01SRP | S01MSGAC |
| 2 | 運用日取得SUB | DAT01S0 | D01010AC |
| 3 | メッセージ編集出力処理SUB | MSG0SRP | MSG000AC |
| 4 | ABEND処理SUB | END01TO | MSG000AC |
---
## 処理詳細
```
1.初期処理(1000ITTSOR
1-1.開始メッセージ出力
【メッセージ編集】
メッセージ番号:1(開始メッセージ)
1-2.コンパイル日時出力
【メッセージ編集】
メッセージ番号:33(コンパイル日時)
PARM1:コンパイル日時
PARM2'COMPILED'
1-3.ワークエリアの初期化
1-4.運用日取得SUB(DAT01S0)により運用日を取得する。
復帰コード≠ZEROの場合、メッセージを出力し、ABEND処理SUBを呼び出し異常終了する。
【メッセージ編集】
メッセージ番号:5(サブエラー)
PARM1'DAT01SO'
PARM2:復帰コード
【ABEND処理SUB】
ABENDコード:999
1-5.使用ファイルのオーブン
1-6.R01を読み込む。(1100R01INNSOR)(1回目)
1-7.R02を読み込む。(1200R02INNSOR)(1回目)
2.主処理(2000MAJSOR)(R01、R02を全て読み終えるまで下記を繰り返す)
2-1.マッチの場合
2-1-1.R01をW01にそのまま出力する。(2100W010UTSOR)
*出力編集(W01)を参照
2-1-2.R01を読み込む。(1100R01INNSOR)(2件目以降)
2-1-3.R02を読み込む。(1200R02INNSOR)(2件目以降)
2-2.R01のみの場合
2-2-1.R01をW02にそのまま出力する。(2200W020UTSOR)
*出力編集(W02)を参照
2-2-2.R01を読み込む。(1100R01INNSOR)(2件目以降)
2-3.R02のみの場合
2-3-1.R02をW03にそのまま出力する。(2300W030UTSOR)
*出力編集(W03)を参照
2-3-2.R02を読み込む。(1200R02INNSOR)(2件目以降)
3.終了処理(3000STPSOR
3-1.入出力ファイルのクローズ
3-2.入出力件数出力メッセージ出力
【入力メッセージ編集】
メッセージ番号:6(入力件数メッセージ)
PARM1:当該入力ファイルのDD名
PARM2:当該入力ファイルの件数
【出力メッセージ編集】
メッセージ番号:7(出力件数メッセージ)
PARM1:当該出力ファイルのDD名
PARM2:当該出力ファイルの件数
3-3.終了メッセージ出力
【メッセージ編集】
メッセージ番号:2(終了メッセージ)
```
---
## 出力レコード定義
### 出力ファイル1W01
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | A001 | R01.同項目を設定 | |
| 2 | A002 | R01.同項目を設定 | |
| 3 | A003 | R01.同項目を設定 | |
| 4 | … | | |
| 5 | A020 | R01.同項目を設定 | |
### 出力ファイル2W02
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | A001 | R01.同項目を設定 | |
| 2 | A002 | R01.同項目を設定 | |
| 3 | A003 | R01.同項目を設定 | |
| 4 | … | | |
| 5 | A020 | R01.同項目を設定 | |
### 出力ファイル3W03
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | A001 | R02.同項目を設定 | |
| 2 | A002 | R02.同項目を設定 | |
| 3 | A003 | R02.同項目を設定 | |
| 4 | … | | |
| 5 | A020 | R02.同項目を設定 | |
+142
View File
@@ -0,0 +1,142 @@
# ファイル定義書
## 変更履歴
| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 |
|----|---------|--------|--------|--------|------|
| 1 | 新規作成 | AI | YYYY/MM/DD | | |
## Layout(レコードレイアウト)
### ファイル基本情報
| ファイル名 | COPY ID | 媒体 | レコード形式 | レコード長 |
|-----------|---------|------|-------------|-----------|
| | | PS | FB | |
### レコード定義
| No | レベル | 項目名 | 項目名(英字名) | 属性 | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 |
|----|--------|--------|---------------|------|---------|--------|---------|-----------|-------------|------|
| 1 | | | | | | | | | | |
| 2 | | | | | | | | | | |
| 3 | | | | | | | | | | |
| 4 | | | | | | | | | | |
---
## 記載説明
| 項目 | 説明 |
|------|------|
| 媒体 | ファイル(PS)の場合は"PS"と記録 |
| レコード形式 | 固定長(FB)または可変長(VB)と記録 |
| レコード長 | レコード長は全項目のバイト数を合計して記録。FDのRECORD SIZEと一致させること。REDEFINES項目は合計に含まない。 |
| レベル | レベル番号の定義:01, 03, 05, 07, 09... と飛び飛びのレベル番号とする。同じレベル番号は兄弟関係となる。 |
| 属性(PIC TYPE) | 属性(TYPE)とバイト数を記入するものとする。原則PIC記法で記録。COBOL記述に沿ったシートの記載内容に準拠。 |
## COBOLデータ型一覧
| No | COBOL記述 | 略記法 | バイト数 | 説明 | 日本語説明 | 例 | バイト数(例) |
|----|-----------|--------|---------|------|-----------|-----|-------------|
| 1 | PIC X(n) | X(n) | n | 英数字 | 固定長英数字 | X(10) | 10 |
| 2 | PIC G(n) | G(n) | n×2 | DBCS文字 | ダブルバイト文字(日本語) | G(20) | 40 |
| 3 | PIC 9(n) | 9(n) | n | 数字・10進(符号無) | 符号無し整数 | 9(7) | 7 |
| 4 | PIC S9(n) | S9(n) | n | 数字・10進(符号有) | 符号付き整数 | S9(7) | 7 |
| 5 | PIC S9(n)V9(m) | S9(n)V9(m) | n+m | 数字・10進(符号有・小数) | 暗黙小数点付き(DISPLAY) | S9(7)V9(2) | 9 |
| 6 | PIC S9(n) COMP-3 | S9(n) COMP-3 | INT((n+2)/2) | パック10進(符号有) | 内部10進数 | S9(7) COMP-3 | 4 |
| 7 | PIC 9(n) COMP-3 | 9(n) COMP-3 | INT((n+1)/2) | パック10進(符号無) | 内部10進数(符号無) | 9(7) COMP-3 | 4 |
| 8 | PIC S9(n)V9(m) COMP-3 | S9(n)V9(m) COMP-3 | INT((n+m+2)/2) | パック10進(符号有・小数) | 暗黙小数点付きCOMP-3 | S9(7)V9(2) COMP-3 | 5 |
| 9 | PIC S9(4) COMP | S9(4) COMP | 2 | 2進(2バイト) | 半語長バイナリ | S9(4) COMP | 2 |
| 10 | PIC S9(9) COMP | S9(9) COMP | 4 | 2進(4バイト) | 全語長バイナリ | S9(9) COMP | 4 |
| 11 | PIC S9(18) COMP | S9(18) COMP | 8 | 2進(8バイト) | 2語長バイナリ | S9(18) COMP | 8 |
| 12 | PIC S9(n)V9(m) COMP | S9(n)V9(m) COMP | n+m≦4→2, ≦9→4, ≦18→8 | 2進(符号有・小数) | 暗黙小数点付きCOMP | S9(9)V9(2) COMP | 8 |
| 13 | PIC S9(n) BINARY | S9(n) BINARY | n≦4→2, n≦9→4, n≦18→8 | 2進(バイナリ) | COMPと同一 | S9(15) BINARY | 8 |
| 14 | ZZZ9 | ZZZ9 | Zの数+9の数 | 数字編集パターン | ゼロ抑制編集 | ZZZZ9 | 5 |
## COBOL記述例
### 例1: 基本レイアウト
COBOL定義:
```
01 TEST-RECORD.
03 FIELD-1 PIC X(10).
03 FIELD-2 PIC S9(7) COMP-3.
03 FIELD-3 PIC 9(4).
03 FIELD-4 PIC S9(9) COMP.
03 FILLER PIC X(5).
```
FD記述:
| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 |
|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------|
| 1 | 01 | テストレコード | TEST-RECORD | | | | 1 | | | |
| 2 | 03 | フィールド1 | FIELD-1 | X | 10 | | 1 | | TEST-RECORD | |
| 3 | 03 | フィールド2 | FIELD-2 | S9(7) COMP-3 | 5 | | 11 | | TEST-RECORD | |
| 4 | 03 | フィールド3 | FIELD-3 | 9(4) | 4 | | 16 | | TEST-RECORD | |
| 5 | 03 | 予約 | FILLER | X(5) | 5 | | 20 | | TEST-RECORD | |
### 例2: REDEFINESを使用する場合
COBOL定義:
```
01 REDEF-REC.
03 BASE-FIELD PIC X(10).
03 REDEF-FIELD REDEFINES BASE-FIELD.
05 INFO-1 PIC X(5).
05 INFO-2 PIC X(5).
```
FD記述:
| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 |
|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------|
| 1 | 01 | REDEFINESレコード | REDEF-REC | | | | 1 | | | |
| 2 | 03 | ベースフィールド | BASE-FIELD | X(10) | 10 | | 1 | | REDEF-REC | |
| 3 | 03 | 再定義フィールド | REDEF-FIELD | | | | 11 | BASE-FIELD | REDEF-REC | |
| 4 | 05 | 情報1 | INFO-1 | 9(5) | 5 | | 11 | | REDEF-REC | |
| 5 | 05 | 情報2 | INFO-2 | 9(5) | 5 | | 16 | | REDEF-REC | |
### 例3: OCCURSを使用する場合
COBOL定義:
```
01 OCCURS-REC.
03 OCCURS-ITEM OCCURS 3.
05 SUB-1 PIC X(5).
03 AFTER-ITEM PIC 9(3).
```
FD記述:
| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 |
|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------|
| 1 | 01 | OCCURSレコード | OCCURS-REC | | | | 1 | | | |
| 2 | 03 | OCCURS項目 | OCCURS-ITEM | | | 3 | 1 | | OCCURS-REC | |
| 3 | 05 | サブ項目1 | SUB-1 | X(5) | 5 | | 1 | | OCCURS-REC | |
| 4 | 03 | OCCURS後 | AFTER-ITEM | 9(3) | 3 | | 16 | | OCCURS-REC | |
### 例4: OCCURSの多重入れ子
COBOL定義:
```
01 OCCURS-MULTI-REC.
03 GRP-ITEM OCCURS 3.
05 SUB1 PIC X(5).
05 SUB2 OCCURS 4.
07 SUB2-DATA PIC 9(3).
03 AFTER-ITEM PIC 9(3).
```
FD記述:
| No | レベル | 項目名 | 項目名(英字名) | 属性(PIC TYPE) | バイト数 | OCCURS | 開始位置 | REDEFINES | 所属グループ | 備考 |
|----|--------|--------|---------------|----------------|---------|--------|---------|-----------|-------------|------|
| 1 | 01 | OCCURS多重入れ子レコード | OCCURS-MULTI-REC | | | | 1 | | | |
| 2 | 03 | グループ項目 | GRP-ITEM | | | 3 | 1 | | OCCURS-MULTI-REC | |
| 3 | 05 | サブ項目1 | SUB-1 | X(5) | 5 | | 1 | | OCCURS-MULTI-REC | |
| 4 | 05 | サブ項目2 | SUB-2 | | | 4 | 6 | | OCCURS-MULTI-REC | |
| 5 | 07 | サブ項目2データ | SUB2-DATA | 9(3) | 3 | | 6 | | OCCURS-MULTI-REC | |
| 6 | 03 | OCCURS後 | AFTER-ITEM | 9(3) | 3 | | 52 | | OCCURS-MULTI-REC | |
+145
View File
@@ -0,0 +1,145 @@
# 詳細設計書
## 基本情報
| # | 項目 | 内容 |
|---|------|------|
| 1 | システム名 | SSSSシステム |
| 2 | プログラムID | SSSnnFFF |
| 3 | プログラム名 | XXXXXX処理 |
| 4 | PGMパターン | |
| 5 | 機能概要 | |
| 6 | | |
| 7 | | |
| 8 | | |
※PGMパターン:マッチング(1:1、1:N、M:N)、レイアウト編集のみ(GETPUT)、項目チェック、振り分け、キーブレイク、キーブレイク(集計、集約)、DB更新
### 前提条件
| NO | 対象ファイル | 条件 |
|----|-------------|------|
| 1 | | |
| 2 | | |
### 使用ファイル一覧
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 |
|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------|
| 1 | | | | | | | | | | |
| 2 | | | | | | | | | | |
| 3 | | | | | | | | | | |
| 4 | | | | | | | | | | |
| 5 | | | | | | | | | | |
### キー項目一覧
| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) |
|----|-----------|---------------------|-------------------------------------------|
| 1 | | | |
| 2 | | | |
### 使用モジュール一覧
| NO | 機能 | プログラムID | 使用COPY名 |
|----|------|-------------|-----------|
| 1 | | | |
| 2 | | | |
| 3 | | | |
| 4 | | | |
---
## 処理詳細
```
大項目(1桁):「1.」「2.」「3.」のいずれか
中項目(2桁):対応するパラグラフ名を括弧書きで併記
小項目(3桁):個別処理の説明
詳細:メッセージ番号、PARM、条件分岐、ABENDコード等
```
### 処理詳細フォーマット(記入例)
```
1.初期処理 (1000ITTSOR)
1-1.開始メッセージ出力
メッセージ番号: 1(開始メッセージ)
1-2.コンパイル日時出力
メッセージ番号: 33(コンパイル日時)
PARM1: コンパイル日時
PARM2: 'COMPILED'
1-3.ワークエリア初期化
1-4.サブプログラム呼出(各種初期設定)
戻りコードがZEROの場合、正常継続。
それ以外の場合、メッセージ出力後、ABEND処理へ遷移。
メッセージ番号: 5(サブエラー)
PARM1: 呼出プログラム名
PARM2: 戻りコード
ABENDコード: 999
1-5.入出力ファイルOPEN
1-6.入力ファイル1を読み込み。
1-7.入力ファイル2を読み込み。
2.主処理 (2000MAJSOR)(全入力ファイルを読み切り終了するまで繰り返し)
2-1.条件Aの場合
2-1-1.出力ファイル1に出力する。
出力後、出力ファイル1のカウンタを加算
2-1-2.入力ファイル1を読み込み。
2-2.条件Bの場合
2-2-1.出力ファイル2に出力する。
出力後、出力ファイル2のカウンタを加算
2-2-2.入力ファイル2を読み込み。
2-3.条件Cの場合
2-3-1.出力ファイル3に出力する。
出力後、出力ファイル3のカウンタを加算
2-3-2.入力ファイル2を読み込み。
3.終了処理 (3000STPSOR)
3-1.入出力ファイルCLOSE
3-2.入出力ファイル件数出力
メッセージ番号: 6(入力件数)
PARM1: 該当ファイルDD名
PARM2: 該当ファイル件数
メッセージ番号: 7(出力件数)
PARM1: 該当ファイルDD名
PARM2: 該当ファイル件数
3-3.終了メッセージ出力
メッセージ番号: 2(終了メッセージ)
```
---
## 出力レコード定義
出力ファイルごとに以下の形式で記述する。
### 出力ファイル1(例: W01)
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | | | |
| 2 | | | |
| 3 | | | |
| 4 | | | |
| 5 | | | |
### 出力ファイル2(例: W02)
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | | | |
| 2 | | | |
| 3 | | | |
| 4 | | | |
| 5 | | | |
### 出力ファイル3(例: W03)
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | | | |
| 2 | | | |
| 3 | | | |
| 4 | | | |
| 5 | | | |
+687 -96
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import logging
import os
import re
import subprocess
import sqlite3
from dataclasses import dataclass, field
@@ -11,18 +12,21 @@ from pathlib import Path
from typing import Optional
from config import Config
from config.program_schema import ProgramSchema, load_schema
from config.program_schema import ProgramSchema, load_schema, ScenarioDef
from cobol_testgen import extract_structure, generate_data
from cobol_testgen.flatfile import write_all_files, write_sysin_file
from cobol_testgen.file_io import read_output_file
from cobol_testgen.read import preprocess, resolve_copybooks, resolve_sql_includes, parse_file_control, parse_file_section, parse_data_division, extract_data_division, scan_open_statements
from cobol_testgen.read import preprocess, resolve_copybooks, resolve_sql_includes, parse_file_control, parse_file_section, parse_data_division, extract_data_division, extract_procedure_division, scan_open_statements
from cobol_testgen.read import strip_exec_sql_from_data_div
from cobol_testgen.gcov import run_gcov
from cobol_testgen.coverage import run_coverage, generate_coverage_index
from cobol_testgen.design_mcdc import enum_paths as mcdc_enum_paths
from cobol_testgen.to_sql import collect_sql_meta, build_db_input
from cobol_testgen.core import extract_sql_assignments
from cobol_testgen.core import extract_sql_assignments, classify_field_roles, _init_child_names
from cobol_testgen import expand_occurs
from cobol_testgen.design import get_term_type, generate_records
from cobol_testgen.output import output_json
from cobol_testgen.pipeline_bridge import build_branch_tree_fallback
import shutil
from data.diff_result import VerificationRun, FieldResult
from runners.gixsql_runner import GixsqlCobolRunner, GixsqlTableData
@@ -80,6 +84,8 @@ class GixsqlOrchestrator:
self.pp_path: Optional[Path] = None
self.exe_path: Optional[Path] = None
self.java_input_path: Optional[Path] = None
self._current_db_path: Optional[Path] = None # scenario-specific DB path
self._multi_run_gcov_data: dict[int, int] | None = None # merged multi-run gcov data
self.java_output_path: Optional[Path] = None
self.generated_records: list[dict] = []
self.generated_structure: dict | None = None
@@ -110,7 +116,13 @@ class GixsqlOrchestrator:
flat_cpy.append(str(dst))
# Copy SUB programs
sub_dirs = [self.cobol_src_dir, self.cobol_src_dir.parent / "sub"]
v3_root = Path(__file__).parent
sub_dirs = [
self.cobol_src_dir,
self.cobol_src_dir.parent / "sub",
v3_root.parent / "cobol-tna-system" / "sub",
v3_root.parent / "production" / "sub",
]
for sub in self.schema.subprograms:
found = False
for sd in sub_dirs:
@@ -160,75 +172,393 @@ class GixsqlOrchestrator:
# ── Step 2: 入力データ生成 ──
def step2_generate_inputs(self) -> DbPipelineResult:
"""テストデータ生成 + フラットファイル出力 + DB初期化"""
def step2_generate_inputs(self, scenario: ScenarioDef | None = None) -> DbPipelineResult:
"""テストデータ生成 + フラットファイル出力 + DB初期化
Args:
scenario: 多輪実行時のシナリオ定義。None=単輪(従来動作)。
"""
try:
src_text = self.src_path.read_text(encoding="utf-8-sig")
# Use the pre-gixpp source for Lark parsing (gixpp output contains SQLCA etc.)
parse_text = self.pp_path.read_text(encoding="utf-8") if self.pp_path and self.pp_path.exists() else src_text
# COBOL 解析 + テストデータ生成
# COBOL 解析 + テストデータ生成(白盒 + 機能 + 策略 統合)
cbd = [str(d) for d in self.copybook_dirs]
st = extract_structure(src_text, copybook_dirs=cbd)
self.generated_structure = st
recs = generate_data(src_text, st, copybook_dirs=cbd)
from cobol_testgen.data_merger import generate_all_data
v3_root = Path(__file__).parent
design_doc_dir = v3_root / "詳細設計書"
if not design_doc_dir.exists():
design_doc_dir = None
# LLMClient は API key が必要な場合のみ初期化(未設定時は None → スキップ)
llm = None
if hasattr(self.config, 'llm_model') and self.config.llm_model:
from agents.llm import LLMClient
try:
llm = LLMClient(model=self.config.llm_model, timeout=self.config.llm_timeout)
except Exception:
pass
recs = generate_all_data(
program_id=self.program_id,
src_text=src_text,
st=st,
copybook_dirs=cbd,
design_doc_dir=str(design_doc_dir) if design_doc_dir else None,
llm_client=llm,
config=self.config,
)
# Post-process: link R02 cancel APPL-IDs to matching R01 insert APPL-IDs
for rec in recs:
if 'R02APPL-ID' in rec and 'R01APPL-ID' in rec:
rec['R02APPL-ID'] = rec['R01APPL-ID']
# DB 初期データ構築: single DB under V3 runtime/ dir
self.db_path.parent.mkdir(parents=True, exist_ok=True)
self._init_database(self.db_path)
# シナリオに応じた DB パス
if scenario:
db_path = Path("C:/Temp/gix") / f"{self.program_id}_{scenario.id}.db"
self._current_db_path = db_path
else:
db_path = self.db_path
self._current_db_path = None
# DB 初期データ構築: clean stale DB first
if db_path.exists():
db_path.unlink()
db_path.parent.mkdir(parents=True, exist_ok=True)
self._init_database(db_path)
# DB 初期行投入(DELETE/UPDATE が作用する行、SELECT が返す行)
self._populate_database(self.db_path, src_text, recs)
self._populate_database(db_path, src_text, recs)
# P5: inject duplicate-PK rows (scenario で制御)
if scenario is None or scenario.inject_duplicate_pk:
self._inject_sql_error_rows(db_path, recs)
# フラットファイル書き出し
input_dir = self.work_dir / "input"
# Patch R01LINE records with EMP-IDs matching the record's own EMP-ID
for i, rec in enumerate(recs):
line = rec.get('R01LINE', '')
if not line:
continue
parts = line.split(',', 1)
if len(parts) != 2:
continue
# Get EMP-ID from the record's own field (e.g., R01EMP-ID)
emp_id = rec.get('R01EMP-ID', '')
if not emp_id or emp_id == '00000000':
emp_id = rec.get('HV-EMP-ID', '')
if not emp_id or emp_id == '00000000':
emp_id = f"EMP{str(i).zfill(5)}"
# First record: empty EMP-ID to trigger R01EMP-ID = SPACE path (DP#12)
if i == 0:
rec['R01LINE'] = f"{' '*8},{parts[1]}"
rec['R01EMP-ID'] = ' ' * len(emp_id)
else:
rec['R01LINE'] = f"{emp_id.ljust(8)},{parts[1]}"
rec['R01EMP-ID'] = emp_id
# Inject duplicate EMP-IDs for last 3 records to trigger AGG UPDATE
# path (DP#19-#20). Use the LARGEST EMP-ID from the last 8 of the
# sorted unique list, so the dup ID falls in the last TARGET card
# batch (which sets TARGET-COUNT). Set R01DATE to keep YEAR_MONTH
# the same but different day avoids PK conflict in DAILY_RECORDS.
# Works for both FIXED (KIN07REC) and LINE SEQUENTIAL formats.
if len(recs) > 3:
# Collect unique EMP-IDs (non-blank, non-zero)
all_ids = set()
for r in recs:
eid = r.get('R01EMP-ID', '')
if eid and eid.strip() and eid != '00000000':
all_ids.add(eid)
sorted_ids = sorted(all_ids)
# Pick from the last chunk (matches last TARGET card batch)
# T chunk = 8 per card, so last chunk index = len % 8 or 8
n = len(sorted_ids)
last_chunk_start = n - (n % 8 or 8)
dup_eid = sorted_ids[last_chunk_start] if sorted_ids else ''
# Find a record with this EMP-ID for its DATE
src_date = ''
for r in recs:
if r.get('R01EMP-ID', '') == dup_eid:
src_date = r.get('R01DATE', '')
break
dup_date = src_date
for j in range(max(1, len(recs)-3), len(recs)):
rec = recs[j]
if not dup_eid:
continue
rec['R01EMP-ID'] = dup_eid
# FIXED format: keep same YEAR_MONTH but different day
# to avoid PK conflict in DAILY_RECORDS INSERT.
if dup_date and len(dup_date) >= 6:
dup_ym = dup_date[:6]
orig_date = rec.get('R01DATE', '')
if orig_date and len(orig_date) >= 8:
rec['R01DATE'] = dup_ym + orig_date[6:8]
else:
rec['R01DATE'] = dup_ym + '01'
# LINE SEQUENTIAL format: patch R01LINE
line = rec.get('R01LINE', '')
if line:
parts = line.split(',', 1)
if len(parts) == 2:
rec['R01LINE'] = f"{dup_eid.ljust(8)},{parts[1]}"
# 出力先ディレクトリ(シナリオ毎に分離)
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.mkdir(parents=True, exist_ok=True)
# フラットファイル書き出し(全シナリオ同一)
flats = write_all_files(recs, src_text, input_dir,
copybook_dirs=[str(d) for d in self.copybook_dirs])
# SYSIN 設定ファイル生成(プログラム固有のカード形式
# SYSIN 設定ファイル生成(シナリオ毎に run_cfg を渡す
run_cfg = None
if scenario:
run_cfg = {
"period": scenario.sysin.period,
"include_invalid_period": scenario.sysin.include_invalid_period,
"modes": scenario.sysin.modes,
}
sysin_path = write_sysin_file(recs, src_text, input_dir,
copybook_dirs=[str(d) for d in self.copybook_dirs])
copybook_dirs=[str(d) for d in self.copybook_dirs],
run_cfg=run_cfg)
if sysin_path:
logger.info(f" SYSIN file written: {sysin_path}")
flats.append(("SYSIN", sysin_path, 0))
# Pre-populate MONTHLY_ABSENCE with matching EMP-ID/YEAR-MONTH for
# SELECT COUNT(*) → HV-CNT > 0 → UPDATE path (DP#27). Must run AFTER
# write_all_files (so the R01 flat file exists).
db_for_seed = self._current_db_path or self.db_path
self._seed_matching_monthly_rows(db_for_seed, recs, max_seed=20,
r01_dir=input_dir)
# ── JSON 出力(Java 検証用) ──
try:
pp = preprocess(src_text, extra_search_paths=cbd)
data_div = extract_data_division(pp)
data_fields = parse_data_division(data_div) if data_div else []
fdict = []
for f in data_fields:
fdict.append({
'name': f.name, 'level': f.level, 'pic': f.pic,
'pic_info': {
'type': f.pic_info.type if f.pic_info else 'unknown',
'digits': f.pic_info.digits if f.pic_info else 0,
'decimal': f.pic_info.decimal if f.pic_info else 0,
'length': f.pic_info.length if f.pic_info else 0,
'signed': f.pic_info.signed if f.pic_info else False,
},
'section': f.section, 'occurs': f.occurs_count,
'occurs_depending': f.occurs_depending,
'value': f.value, 'values': f.values,
'redefines': f.redefines, 'usage': f.usage,
})
fdict = expand_occurs(fdict)
proc_div = extract_procedure_division(pp)
branch_tree, assignments = build_branch_tree_fallback(proc_div, fdict)
sql_assigns = extract_sql_assignments(src_text)
for tgt, asgn_list in sql_assigns.items():
for asgn in asgn_list:
assignments.setdefault(tgt, []).append(asgn)
# fd_fields / field_to_fd
file_sec = parse_file_section(pp) or {}
fd_fields = {}
field_to_fd = {}
for fd_name, rec_names in file_sec.items():
fds = []
seen = set()
for rec in rec_names:
if rec not in seen:
fds.append(rec)
seen.add(rec)
for child in _init_child_names(rec, fdict):
if child not in seen:
fds.append(child)
seen.add(child)
fd_fields[fd_name] = fds
for child in fds:
field_to_fd[child] = fd_name
open_dir = scan_open_statements(proc_div) if proc_div else {}
# Roles + path info + termination types
roles = classify_field_roles(branch_tree, assignments, fdict,
source=src_text,
proc_text=proc_div)
branch_paths = mcdc_enum_paths(branch_tree, fdict)
path_infos = [(c, a, get_term_type(c)[1]) for c, a in branch_paths]
json_records, _, term_types = generate_records(
path_infos, fdict, assignments, file_sec=file_sec)
# DB input for JSON
data_div2, declared_columns = strip_exec_sql_from_data_div(data_div)
sql_meta = collect_sql_meta(assignments, declared_columns)
db_input = None
if sql_meta:
db_input = build_db_input(
branch_paths, fdict, assignments,
sql_meta, declared_columns, records=recs)
# Write main JSON(シナリオ毎に分離)
json_outdir = output_root / "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,
fd_fields=fd_fields, field_to_fd=field_to_fd,
open_dir=open_dir, term_types=term_types,
db_input=db_input, data_fields=fdict)
logger.info(f" JSON output: {json_path}")
flats.append(("JSON", json_path, 0))
except Exception as ej:
logger.warning(f" JSON output skipped: {ej}")
self.generated_records = recs
db_for_result = str(self._current_db_path or self.db_path)
return DbPipelineResult(
self.program_id, 2, True,
data={"records": len(recs), "flat_files": len(flats),
"db_path": str(self.db_path)},
"db_path": db_for_result},
)
except Exception as e:
return DbPipelineResult(self.program_id, 2, False, str(e))
# ── Step 3: COBOL 実行 ──
def step3_run_cobol(self) -> DbPipelineResult:
"""COBOL DB プログラム実行"""
def step3_run_cobol(self, scenario: ScenarioDef | None = None) -> DbPipelineResult:
"""COBOL DB プログラム実行(環境変数で入出力先を振り分け)
Args:
scenario: 多輪実行時のシナリオ。None=単輪。
"""
if not self.exe_path or not self.exe_path.exists():
return DbPipelineResult(self.program_id, 3, False,
"exe not found (run step1 first)")
# Subprogram DLLs are in cobol-tna-system/bin/
# シナリオ毎の出力先
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"
gcov_dir = self.runtime_dir / "gcov"
input_dir.mkdir(parents=True, exist_ok=True)
output_dir.mkdir(parents=True, exist_ok=True)
gcov_dir.mkdir(parents=True, exist_ok=True)
# シナリオ毎の 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"
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"
if gen_json_dir.exists():
json_dir = run_dir / "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':
(json_dir / f.name).write_bytes(f.read_bytes())
# Scan ASSIGN TO + OPEN direction → build env overrides
assign_map = self._scan_assign_to()
env_overrides = {}
for fname, direction in assign_map.items():
if direction == "INPUT":
env_overrides[fname] = os.path.join("input", fname)
else:
env_overrides[fname] = os.path.join("output", fname)
# シナリオ毎の DB パス
db_path = self._current_db_path or self.db_path
# GIXSQL_DB_PATH が効かないため、デフォルト DB にシナリオ DB をコピーする
if scenario is not None and db_path != self.db_path:
if db_path.exists():
if self.db_path.exists():
self.db_path.unlink()
shutil.copy2(str(db_path), str(self.db_path))
db_path = self.db_path
# .gcda は CWD= run_dir)に書き出されるので、実行後に gcov/run_{id}/ に移動する
# Subprogram DLLs
cobol_bin = Path(self.cobol_src_dir).parent / "bin"
self.runtime_dir.mkdir(parents=True, exist_ok=True)
result = self.runner.run(
self.exe_path, self.runtime_dir,
self.db_path,
input_dir=self.work_dir / "input",
self.exe_path, cwd,
db_path,
input_dir=None,
cobol_lib_path=str(cobol_bin) if cobol_bin.exists() else None,
env_overrides=env_overrides,
)
# .gcda を gcov/ にコピー(シナリオ毎に gcov/run_{id}/
# GnuCOBOL は .gcno が生成された CWD (= compile CWD = exe_dir) に .gcda を書き出す。
# 複数シナリオで .gcno は共有されるため COPY で行う(MOVE 不可)。
gcda_src_dirs = [cwd] # ランタイム CWD
exe_dir_for_gcda = self.work_dir / "bin"
if exe_dir_for_gcda.exists() and exe_dir_for_gcda not in gcda_src_dirs:
gcda_src_dirs.append(exe_dir_for_gcda)
if scenario is None:
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:
shutil.copy2(str(f), str(dst))
return DbPipelineResult(
self.program_id, 3, result.success,
data={"returncode": result.returncode, "log": result.log[:500]},
data={"returncode": result.returncode, "log": result.log[:500],
"input_dir": str(input_dir), "output_dir": str(output_dir),
"gcov_dir": str(gcov_dir)},
)
# ── マルチラン gcov マージ ──
def _merge_multi_run_gcov(self) -> dict[int, int] | None:
"""Run gcov per scenario, parse results, merge {line: count} dicts.
Returns merged gcov_data or None if no multi-run data available.
"""
from cobol_testgen.gcov import run_gcov, parse_cbl_gcov
gcov_dir = self.runtime_dir / "gcov"
run_dirs = sorted(gcov_dir.glob("run_*"))
if len(run_dirs) <= 1:
return None
# Ensure .gcno is in each run dir (copy from compile CWD if needed)
bin_gcno = self.work_dir / "bin"
if bin_gcno.exists():
for sd in run_dirs:
for f in bin_gcno.glob("*.gcno"):
dst = sd / f.name
if not dst.exists():
shutil.copy2(str(f), str(dst))
merged_data: dict[int, int] = {}
for sd in run_dirs:
data = run_gcov(f"{self.program_id}_pp", str(sd))
if data:
for line, count in data.items():
merged_data[line] = max(merged_data.get(line, 0), count)
logger.info(f" Merged gcov from {len(run_dirs)} runs ({len(merged_data)} lines)")
return merged_data
# ── カバレッジレポート(パイプライン外、オプション) ──
def generate_coverage_report(self,
@@ -236,33 +566,68 @@ class GixsqlOrchestrator:
"""COBOL 実行後:gcov データ収集 + 静的パスとマージし HTML レポート"""
try:
if not self.exe_path or not self.exe_path.exists():
return DbPipelineResult(self.program_id, 0, False,
"exe not found (run step3 first)")
# Fallback: look for exe in standard build location
fallback = self.work_dir / "bin" / f"{self.program_id}.exe"
if fallback.exists():
self.exe_path = fallback
else:
return DbPipelineResult(self.program_id, 0, False,
f"exe not found at {self.exe_path} or {fallback}")
if output_dir is None:
v3_root = Path(__file__).parent
output_dir = v3_root / "reports" / self.program_id / "coverage"
output_dir = Path(output_dir)
# 1. Copy .gcno + .gcda from CWD (compile-time cwd) to runtime_dir
# cobc generates .gcno in CWD; at runtime, program writes .gcda to same CWD
gcno_gcda_count = 0
for ext in (".gcno", ".gcda"):
for f in Path.cwd().glob(f"*{ext}"):
if f.stat().st_size > 0:
shutil.copy2(str(f), str(self.runtime_dir / f.name))
gcno_gcda_count += 1
if gcno_gcda_count == 0:
return DbPipelineResult(self.program_id, 0, False,
f"no .gcno/.gcda found in CWD (--coverage missing?)")
# 1. Use pre-merged multi-run gcov data if available (skip gcov re-run)
if self._multi_run_gcov_data is not None:
gcov_data = self._multi_run_gcov_data
# Also merge subprogram gcov data from each scenario
from cobol_testgen.gcov import run_gcov as _run_gcov
gcov_dir = self.runtime_dir / "gcov"
for sub in self.schema.subprograms:
sub_merged: dict[int, int] = {}
for sd in sorted(gcov_dir.glob("run_*")):
sub_data = _run_gcov(sub, str(sd))
if sub_data:
for line, cnt in sub_data.items():
sub_merged[line] = max(sub_merged.get(line, 0), cnt)
if sub_merged:
gcov_data.update(sub_merged)
else:
# Single-run: collect .gcno/.gcda and run gcov
gcov_dir = self.runtime_dir / "gcov"
gcov_dir.mkdir(parents=True, exist_ok=True)
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:
shutil.copy2(str(f), str(dst))
# Count what we have
gcno_gcda_count = 0
for ext in (".gcno", ".gcda"):
for f in gcov_dir.glob(f"*{ext}"):
if f.stat().st_size > 0 and (f.name.startswith(self.program_id) or f.name.startswith("SUB")):
gcno_gcda_count += 1
if gcno_gcda_count == 0:
for sd in (v3_root, self.work_dir, self.runtime_dir, gcov_dir):
for ext2 in (".gcno", ".gcda"):
files = list(sd.glob(f"*{ext2}"))
logger.error(f"gcov-check: {sd}\\*{ext2} -> {len(files)} files: {[f.name for f in files[:5]]}")
return DbPipelineResult(self.program_id, 0, False,
"no .gcno/.gcda found (--coverage missing?)")
# 3. Parse gcov data
gcov_data = run_gcov(f"{self.program_id}_pp", str(self.runtime_dir))
if not gcov_data:
gcov_data = run_gcov(self.program_id, str(self.runtime_dir))
for sub in self.schema.subprograms:
sd = run_gcov(sub, str(self.runtime_dir))
if sd:
gcov_data.update(sd)
# 3. Parse gcov data
gcov_data = run_gcov(f"{self.program_id}_pp", str(gcov_dir))
if not gcov_data:
gcov_data = run_gcov(self.program_id, str(gcov_dir))
for sub in self.schema.subprograms:
sd = run_gcov(sub, str(gcov_dir))
if sd:
gcov_data.update(sd)
# 4. Static branch tree from step2
st = self.generated_structure
@@ -319,13 +684,16 @@ class GixsqlOrchestrator:
)
generate_coverage_index([cov_result], str(output_dir.parent))
# Clean up .gcno/.gcda from CWD (avoid accumulation)
for ext in (".gcno", ".gcda"):
for f in Path.cwd().glob(f"*{ext}"):
try:
f.unlink()
except PermissionError:
pass
# Clean up .gcno/.gcda from v3_root + CWD (avoid accumulation)
for clean_dir in (v3_root, Path.cwd()):
if clean_dir == gcov_dir:
continue
for ext in (".gcno", ".gcda"):
for f in clean_dir.glob(f"*{ext}"):
try:
f.unlink()
except PermissionError:
pass
total = cov_result.get("total_branches", 0)
covered = cov_result.get("covered_branches", 0)
@@ -348,12 +716,13 @@ class GixsqlOrchestrator:
def step4_extract_intermediate(self) -> DbPipelineResult:
"""SQLite → JSON 中介データ抽出(Step 4: DB→Java中介データ)"""
if not self.db_path or not self.db_path.exists():
db_path = self._current_db_path or self.db_path
if not db_path or not db_path.exists():
return DbPipelineResult(self.program_id, 4, False,
"db not found (run step3 first)")
try:
conn = sqlite3.connect(str(self.db_path))
conn = sqlite3.connect(str(db_path))
conn.row_factory = sqlite3.Row
# Read from actual COBOL SQL tables (using sql_name or name)
@@ -421,17 +790,18 @@ class GixsqlOrchestrator:
def step6_verify(self) -> VerificationRun:
"""Java 出力と COBOL 期待値を比較"""
db_path = self._current_db_path or self.db_path
vr = VerificationRun(
program=self.program_id,
runner="gixsql",
gixsql_version="0.9.1",
sqlite_path=str(self.db_path) if self.db_path else "",
sqlite_path=str(db_path) if db_path else "",
step_reached=6,
)
if self.db_path and self.db_path.exists():
if db_path and db_path.exists():
after_tables = self.runner.read_db_tables(
self.db_path,
db_path,
[t.name for t in self.schema.db_tables],
)
for table_data in after_tables:
@@ -451,58 +821,123 @@ class GixsqlOrchestrator:
def run_all(self, skip_steps: set[int] | None = None,
generate_coverage: bool = True) -> VerificationRun:
"""Step 1 → 6 を順次実行(skip_jvm=True で Step 5/6 をスキップ)"""
"""Step 1 → 6 を順次実行(skip_jvm=True で Step 5/6 をスキップ)
多輪実行:schema.runs が定義されていれば各シナリオを順次実行し、最後に gcov をマージ。
schema.runs が空の場合は単輪(従来動作)。
"""
skip = set(skip_steps or [])
if self.skip_jvm:
skip.update({5, 6})
steps = [
(1, self.step1_setup_environment),
(2, self.step2_generate_inputs),
(3, self.step3_run_cobol),
(4, self.step4_extract_intermediate),
]
if not self.skip_jvm:
steps.extend([
(5, self.step5_run_java),
(6, self.step6_verify),
])
results = []
last_step = max(s for s, _ in steps)
for step_num, step_fn in steps:
if step_num in skip:
continue
logger.info(f" Step {step_num}...")
result = step_fn()
results.append(result)
if not result.success and step_num < last_step:
vr = VerificationRun(
scenarios = self.schema.runs or [ScenarioDef(id="default")]
is_multi = len(scenarios) > 1 or (len(scenarios) == 1 and scenarios[0].id != "default")
# Step 1: compile once
if 1 not in skip:
logger.info(" Step 1 (compile)...")
r1 = self.step1_setup_environment()
if not r1.success:
return VerificationRun(
program=self.program_id, runner="gixsql",
status="BLOCKED", exit_code=2,
step_reached=step_num,
debug={"step_results": [r.__dict__ for r in results]},
step_reached=1,
)
return vr
# Optional coverage report (non-blocking, not part of numbered pipeline)
cv_flags = getattr(self.config, 'gixsql_compile_flags', '')
if '--coverage' in cv_flags and generate_coverage:
self.generate_coverage_report()
# Each scenario: generate inputs + run COBOL
for scenario in scenarios:
label = f" [{scenario.id}]" if is_multi else ""
logger.info(f" Step 2 (generate inputs){label}...")
r2 = self.step2_generate_inputs(scenario if is_multi else None)
if not r2.success:
return VerificationRun(
program=self.program_id, runner="gixsql",
status="BLOCKED", exit_code=2,
step_reached=2,
)
logger.info(f" Step 3 (run COBOL){label}...")
r3 = self.step3_run_cobol(scenario if is_multi else None)
if not r3.success:
return VerificationRun(
program=self.program_id, runner="gixsql",
status="BLOCKED", exit_code=2,
step_reached=3,
)
# Step 4: extract intermediate (last scenario wins for DB path)
if 4 not in skip:
logger.info(" Step 4 (extract)...")
self.step4_extract_intermediate()
if not self.skip_jvm:
vr = results[-1] # step6_verify returned VerificationRun
vr.debug["step_results"] = [r.__dict__ for r in results[:-1] if r]
else:
vr = VerificationRun(
program=self.program_id, runner="gixsql",
status="PASS", exit_code=0,
step_reached=last_step,
debug={"step_results": [r.__dict__ for r in results]},
)
steps_remaining = [5, 6]
for step_num in steps_remaining:
if step_num in skip:
continue
logger.info(f" Step {step_num}...")
if step_num == 5:
self.step5_run_java()
elif step_num == 6:
vr = self.step6_verify()
# Optional coverage report (non-blocking)
cv_flags = getattr(self.config, 'gixsql_compile_flags', '')
if '--coverage' in cv_flags and generate_coverage:
if is_multi:
merged = self._merge_multi_run_gcov()
self._multi_run_gcov_data = merged
self.generate_coverage_report()
vr = VerificationRun(
program=self.program_id, runner="gixsql",
status="PASS", exit_code=0,
step_reached=6 if not self.skip_jvm else 4,
)
return vr
# ── Internal helpers ──
def _scan_assign_to(self) -> dict[str, str]:
"""Scan COBOL source for SELECT/ASSIGN-TO + OPEN direction.
Returns {filename: direction} where direction is 'INPUT' or 'OUTPUT'.
Works for both quoted (\"KIN08R01\") and bare (KIN01R01) ASSIGN.
"""
src_text = self.src_path.read_text(encoding="utf-8-sig")
assign_map: dict[str, str] = {}
# First pass: collect all SELECT/ASSIGN-TO mappings
select_to_file: dict[str, str] = {}
for m in re.finditer(
r'SELECT\s+(\w+)\s+ASSIGN\s+TO\s+"?([^"\s.]+)',
src_text, re.IGNORECASE
):
sel_name = m.group(1)
fname = m.group(2).strip().rstrip('"')
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)
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
):
# 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"
return assign_map
def _init_database(self, db_path: Path):
"""Create tables from schema + COBOL EXEC SQL table definitions."""
self._create_tables(db_path)
@@ -599,3 +1034,159 @@ class GixsqlOrchestrator:
conn.commit()
conn.close()
logger.info(f" DB populated: {db_path}")
def _inject_sql_error_rows(self, db_path: Path, records: list[dict] | None = None):
"""Insert duplicate-PK rows to trigger SQL error handling paths in COBOL."""
conn = sqlite3.connect(str(db_path))
for table in self.schema.db_tables:
pk_cols = [c.name for c in table.columns if c.primary_key]
if not pk_cols:
continue
col_names = [c.name for c in table.columns]
try:
rows = conn.execute(f"SELECT * FROM [{table.name}] LIMIT 2").fetchall()
if len(rows) < 1:
# For empty tables, generate synthetic error rows from test record data
synthetic = self._make_synthetic_error_rows(table, records)
if synthetic:
rows = synthetic
else:
continue
quoted = ", ".join(f"[{c}]" for c in col_names)
ph = ", ".join("?" for _ in col_names)
for row in rows:
vals = tuple(str(rows[0][i]) if c in pk_cols else "X" for i, c in enumerate(col_names))
conn.execute(f"INSERT OR IGNORE INTO [{table.name}] ({quoted}) VALUES ({ph})", vals)
logger.info(f" SQL error test row injected into {table.name}")
except Exception as e:
logger.debug(f" SQL error row injection skipped: {e}")
conn.commit()
conn.close()
def _seed_matching_monthly_rows(self, db_path: Path, records: list[dict] | None,
max_seed: int = 1,
r01_dir: Path | None = None):
"""Pre-populate MONTHLY_ABSENCE with rows matching actual R01 record data.
Reads the generated R01 flat file (200-byte fixed records, KIN07REC layout),
extracts unique (EMP_ID, YEAR_MONTH) pairs, and inserts a SUBSET of them.
This ensures some AGG entries find HV-CNT > 0 (UPDATE, DP#27) and
others find HV-CNT = 0 (INSERT, DP#28)."""
r01_path = (r01_dir or self.work_dir / "input") / "KIN08R01"
if not r01_path.exists():
logger.info(" R01 file not found, skipping MONTHLY_ABSENCE seed")
return
conn = sqlite3.connect(str(db_path))
monthly_table = None
for t in self.schema.db_tables:
if t.name == "MONTHLY_ABSENCE":
monthly_table = t
break
if not monthly_table:
conn.close()
return
col_names = [c.name for c in monthly_table.columns]
quoted = ", ".join(f"[{c}]" for c in col_names)
ph = ", ".join("?" for _ in col_names)
seen = set()
pairs = []
rows_inserted = 0
# KIN07REC layout (each record is 200 bytes):
# EMP-ID PIC 9(008) offset 0, 8 bytes
# DATE PIC 9(008) offset 8, 8 bytes
# ... remaining fields (not needed)
rec_size = 200
with open(str(r01_path), 'rb') as f:
data = f.read()
num_recs = len(data) // rec_size
for i in range(num_recs):
off = i * rec_size
emp_id = data[off:off+8].decode('ascii', errors='replace').strip()
date = data[off+8:off+16].decode('ascii', errors='replace').strip()
year_month = date[:6] if len(date) >= 6 else date
if not emp_id or not year_month or emp_id == '00000000':
continue
key = (emp_id, year_month)
if key in seen:
continue
seen.add(key)
pairs.append((emp_id, year_month))
# Sort by EMP_ID for deterministic behavior, then seed only `max_seed` pairs
pairs.sort(key=lambda x: x[0])
conn = sqlite3.connect(str(db_path))
monthly_table = None
for t in self.schema.db_tables:
if t.name == "MONTHLY_ABSENCE":
monthly_table = t
break
if not monthly_table:
conn.close()
return
col_names = [c.name for c in monthly_table.columns]
quoted = ", ".join(f"[{c}]" for c in col_names)
ph = ", ".join("?" for _ in col_names)
for seed_idx, (emp_id, year_month) in enumerate(pairs):
if seed_idx >= max_seed:
break
vals = {
"EMP_ID": emp_id,
"YEAR_MONTH": year_month,
"ANNUAL_LEAVE_H": "0",
"PERSONAL_LEAVE_H": "0",
"OFFICIAL_LEAVE_H": "0",
"SICK_LEAVE_H": "0",
"UNAPPROVED_ABSENT_H": "0",
"UPDATED_AT": "2026-01-01 00:00:00",
}
row = tuple(vals.get(c, "") for c in col_names)
try:
conn.execute(f"INSERT OR IGNORE INTO [MONTHLY_ABSENCE] ({quoted}) VALUES ({ph})", row)
rows_inserted += 1
except Exception:
pass
conn.commit()
conn.close()
if rows_inserted:
logger.info(f" MONTHLY_ABSENCE: {rows_inserted}/{len(pairs)} matching rows seeded (DP#27 F + DP#28 F)")
elif pairs:
logger.info(f" MONTHLY_ABSENCE: 0 seeded — all AGG entries will INSERT (DP#28 F)")
def _make_synthetic_error_rows(self, table, records: list[dict] | None) -> list[tuple] | None:
"""Build synthetic error rows for an empty table from test record data."""
if not records or len(records) < 2:
return None
pk_cols = [c.name for c in table.columns if c.primary_key]
if not pk_cols:
return None
# Map COBOL host-variable names to table column names
# KIN08DBU DAILY_RECORDS: EMP_ID=HV-EMP-ID, TARGET_DATE=HV-TARGET-DATE
# KIN08DBU MONTHLY_ABSENCE: EMP_ID=HV-EMP-ID, YEAR_MONTH=HV-YEAR-MONTH
hv_map = {
'EMP_ID': ('HV-EMP-ID', 'R01EMP-ID', ''),
'TARGET_DATE': ('HV-TARGET-DATE', ''),
'YEAR_MONTH': ('HV-YEAR-MONTH', ''),
'TIME_IN': ('HV-TIME-IN', ''),
'TIME_OUT': ('HV-TIME-OUT', ''),
'ANNUAL_LEAVE_H': ('HV-ANNUAL-H', ''),
'PERSONAL_LEAVE_H': ('HV-PERSONAL-H', ''),
'OFFICIAL_LEAVE_H': ('HV-OFFICIAL-H', ''),
'SICK_LEAVE_H': ('HV-SICK-H', ''),
'UNAPPROVED_ABSENT_H': ('HV-ABSENT-H', ''),
}
result = []
for idx in range(min(2, len(records))):
rec = records[idx]
vals = []
for col in table.columns:
val = None
if col.name in hv_map:
for key in hv_map[col.name]:
if key and key in rec:
val = rec[key]
break
if val is None:
val = ' ' if col.name in pk_cols else ''
vals.append(str(val) if val is not None else '')
result.append(tuple(vals))
return result if result else None
+25
View File
@@ -0,0 +1,25 @@
# 50分割 データ生成規則
## 概要
入力ファイルのレコードを50件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。
## 入力
- 入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 通常分割 | 51件以上のデータ(50件の境界を超えるケース)+ 50件未満のデータ |
## データ生成手順
1. 入力ファイルのフィールド定義を取得
2. 組1に51件以上のレコードを生成(分割境界テスト用)
3. 各フィールドはPIC定義に従い実在しそうな値を生成
## 出力
各組1 JSONファイル。
+28
View File
@@ -0,0 +1,28 @@
# CSV→FB変換 データ生成規則
## 概要
CSV形式の入力ファイルを読み取り、固定長(FB)レコードに変換して出力するプログラムのテストデータ。
CSVのカンマ区切り分解、引用符内改行処理、項目チェックを含む。
## 入力
- 入力CSVの項目定義(処理詳細から特定)
- 出力COPYBOOKのフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 正常データ | 全項目が正しいCSVレコード 3〜5件。異なる値のバリエーションを確保 |
| 2 | 異常データ | 項目数不足、項目数過剰、不正な値のレコードを含むデータ |
## データ生成手順
1. CSVの項目数と項目名を処理詳細から特定
2. 正常データ: カンマ区切りCSV行文字列を生成。値はPIC定義に従う
3. 異常データ: 項目数不一致、空値、不正フォーマットを含む
## 出力
入力はCSV文字列(カンマ区切り)として1フィールドに格納。各組1 JSONファイル。
+79
View File
@@ -0,0 +1,79 @@
# DB更新 データ生成規則
## 概要
入力ファイルのレコードをDBテーブルにINSERT/UPDATE/DELETEするプログラムのテストデータを生成する。
出力はSQL INSERTファイル。
## 入力
- 詳細設計書の処理詳細(SQL操作と分岐条件)
- 入力ファイルのCOPYBOOKフィールド定義
- DBテーブル定義と主キー
- キー項目一覧
## 生成データ組
| 组 | 用途 | 内容 | 条件 |
|----|------|------|------|
| 1 | 正常INSERT | 5件以上の新規INSERT + 末尾に多keyテスト | 必須 |
| 2 | INSERT+UPDATE | 3件以上の重複主キーレコード(UPDATEトリガー用)+ 末尾に多keyテスト | 必須 |
| 3 | DELETE対象 | 2件以上のDELETE対象レコード + 末尾に多keyテスト | 処理詳細にDELETE操作がある場合のみ |
## 多keyテスト(各組の末尾に追加)
キーが複数項目(例: A001, A002, A003)で構成される場合、以下のテストデータを追加する。
**手順:**
1. 該当グループ内から1件のレコードを選び、基準データとする
2. キー項目がN個ある場合、N件の追加レコードを生成する
3. 各追加レコードは「1つのキー項目のみ基準データと異なり、残りのキー項目は基準データと完全一致」とする
4. 非キー項目(DATA部分)は基準データと同じ値でよい
**例: キーが (A001, A002, A003) の場合、基準データ (a001, b001, c001) に対し:**
- 追加1: (a002, b001, c001) → A001のみ異なる → INSERTされるべき(別レコード)
- 追加2: (a001, b002, c001) → A002のみ異なる → INSERTされるべき(別レコード)
- 追加3: (a001, b001, c002) → A003のみ異なる → INSERTされるべき(別レコード)
これにより、プログラムが正しいキー項目で比較していることを検証する。
## データ生成手順
1. **SQL操作の特定**: 処理詳細からINSERT/UPDATE/DELETE/UPSERTの有無を確認する
2. **キー項目の特定**: キー項目一覧からDBテーブルの主キーを特定する
3. **フィールド値生成**: 入力COPYBOOKの各フィールドに対しPIC定義に従い値を生成:
- PIC X(n): 左詰め・スペース埋め
- PIC 9(n): 右詰め・先行ゼロ
- PIC S9(n) COMP / COMP-3: 通常の10進数文字列(符号付き・なし)
- DATE / 日付系フィールド: 実在しそうな日付(YYYYMMDD形式)
- TIME / 時刻系フィールド: 実在しそうな時刻(HHMM形式)
- ID / 番号系フィールド: 連番
- NAME系フィールド: 実在しそうな氏名
- 金額 / SALARY系: 現実的な金額
4. **組1 (正常INSERT)**:
- 全レコードの主キーを重複なしで生成
- 各レコードの非キーフィールドは異なる値を設定
- 末尾に多keyテストデータを追加
5. **組2 (INSERT+UPDATE)**:
- 一部のレコードの主キーを組1と重複させる
- 重複レコードの非キーフィールド値は組1と異なる値にする
- 末尾に多keyテストデータを追加
6. **組3 (DELETE対象)**: 処理詳細にDELETE操作がある場合のみ生成
- STATUS='9'などの削除フラグ付きデータ
- 末尾に多keyテストデータを追加
## 出力形式
各組1つのSQLファイル: `{プログラムID}_g{groupId}.sql`
```sql
-- Group: 1
-- 用途: 正常INSERT + 多keyテスト
INSERT INTO TABLE_NAME (COL1, COL2, ...) VALUES
('val1', 'val2', ...),
('val3', 'val4', ...);
```
**注意:**
- 出力は純粋なSQL INSERT文のみ(コードブロックで囲まない)
- 複数行を1つのINSERT文にまとめる(VALUESの後にカンマ区切りで複数行)
@@ -0,0 +1,28 @@
# GETPUT(編集出力)データ生成規則
## 概要
DBまたはファイルからデータを読み取り、編集・整形して出力するプログラムのテストデータ。
レイアウト編集のみ(GETPUT)と同様のシンプルなREAD→編集→WRITEパターン。
## 入力
- 入力ソースのフィールド定義(COPYBOOKまたはDBテーブル定義)
- 出力レコード定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 基本データ | 通常値2〜5件。各フィールドに有効な値を設定。異なる値のバリエーションを確保 |
## データ生成手順
1. 入力フィールドを特定
2. 各フィールドのPIC定義に従い値を生成
3. 実在しそうな値(日付・時刻・金額・氏名等)を生成
4. レコード間で異なる値を設定
## 出力
各組1 JSONファイル。DBからの入力の場合はSQLファイルも出力。
+25
View File
@@ -0,0 +1,25 @@
# MERGE(複数ファイル結合) データ生成規則
## 概要
複数の入力ファイルをキーでソート・マージし、統合された1つの出力ファイルを生成するプログラムのテストデータ。
## 入力
- 詳細設計書のキー項目一覧(マージキー)
- 各入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 全ファイルカバー | 全入力ファイルのデータが出力に反映されるデータ + 末尾多keyテスト |
| 2 | キー境界 | 同一キー値が複数ファイルに分散しているケース + 末尾多keyテスト |
## 多keyテスト
キーが複数項目の場合、各組末尾に追加。各キー項目のみ異なるN件を生成し、正しいキーでマージされることを検証。
## 出力
各組1 JSONファイル。
+31
View File
@@ -0,0 +1,31 @@
# SELECT処理 データ生成規則
## 概要
DBテーブルから条件付きSELECTでデータを取得し、取得結果に基づいて処理を行うプログラムのテストデータ。入力はDB(SELECT結果)であり、出力はSQL INSERTファイル。
## 入力
- 詳細設計書の処理詳細(SELECT文のWHERE条件)
- DBテーブル定義(テーブル名、カラム、主キー)
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 基本データ | SELECT条件にマッチするデータ + マッチしないデータ + 末尾多keyテスト |
## 多keyテスト
WHERE条件に複数キーが関わる場合、基準データに対し各キー項目のみ異なるN件を生成。マッチするパターンとマッチしないパターンの両方をカバー。
## データ生成手順
1. SELECT文のWHERE条件を抽出
2. 条件にマッチするレコードを生成
3. 条件にマッチしないレコード(境界値)を生成
4. 多keyテストデータを追加
## 出力
各組1 SQL INSERTファイル。
@@ -0,0 +1,32 @@
# キーブレイク(集約) データ生成規則
## 概要
キー項目でソート済みの入力ファイルを読み取り、キーブレイク時に集約レコード(カウント・合計など)を出力するプログラムのテストデータを生成する。
## 入力
- 詳細設計書のキー項目一覧
- 入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 基本集約 | 複数グループの集約データ + 末尾に多keyテスト |
| 2 | 境界ケース | キー境界での正しい集約切替を確認するデータ + 末尾に多keyテスト |
## 多keyテスト
キーが複数項目の場合、各組末尾に追加。1レコードを基準に、各キー項目のみ異なるN件を生成。
## データ生成手順
1. キー項目の特定
2. 组1: 3グループ以上、各グループ内に複数レコード(集約対象)
3. 组2: キー値が1件ずつ異なる境界データ
4. 各組末尾に多keyテスト
## 出力
各組1 JSONファイル。
@@ -0,0 +1,37 @@
# キーブレイク(集計) データ生成規則
## 概要
キー項目でソート済みの入力ファイルを読み取り、キーが変わるタイミング(キーブレイク)で集計値を計算し、1グループにつき1件の集計レコードを出力するプログラムのテストデータを生成する。
## 入力
- 詳細設計書のキー項目一覧(ソートキー)
- 入力ファイルのCOPYBOOKフィールド定義
- 集計対象フィールド(処理詳細から特定)
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 基本集計 | 複数グループ・各グループ内複数レコードのデータ + 末尾に多keyテスト |
| 2 | 単一レコードグループ | 1レコードのみのグループを含むデータ + 末尾に多keyテスト |
## 多keyテスト(各組の末尾に追加)
キーが複数項目で構成される場合:
1. 該当グループ内から1件のレコードを選び基準データとする
2. N個のキー項目に対し、それぞれ1項目のみ異なるN件の追加レコードを生成
3. これらのレコードは別グループとして扱われるべき(集計値が別になる)
## データ生成手順
1. キー項目を特定(キー項目一覧のソート条件から)
2. 集計フィールドを特定(処理詳細からCOMPUTE/ADD対象を抽出)
3. 组1: 3グループ以上、各グループ2〜5レコード、キー昇順
4. 组2: 単一レコードグループと複数レコードグループの混在
5. 各組末尾に多keyテストデータを追加
## 出力
各組1 JSONファイル。
@@ -0,0 +1,25 @@
# キーブレイク(集計、集約) データ生成規則
## 概要
ソート済み入力ファイルを読み取り、キーブレイクで集計・集約を行い結果を出力するプログラムのテストデータ。複数ファイル入力の場合はマッチングと集計を組み合わせる。
## 入力
- 詳細設計書のキー項目一覧、使用ファイル一覧
- 各入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 通常集計 | 複数グループ・各グループ複数レコード + 末尾多keyテスト |
| 2 | 同キー内集約 | 同一キー内でN件集約されるデータ + 末尾多keyテスト |
## 多keyテスト
キーが複数項目の場合、各組末尾に追加。1レコードを基準に各キー項目のみ異なるN件を生成。
## 出力
各組1 JSONファイル。
+78
View File
@@ -0,0 +1,78 @@
# マッチング(1:1) 数据生成规则
## 概要
2个输入文件按指定键进行1:1匹配,输出匹配成功/仅R01/仅R02三种结果。
## 输入
- 前提条件:从详细设计书解析匹配键(A001>A002>A003...
- 输入文件:2个(R01、R02
- 键条件:键值唯一(重複NG),升序排序
## 生成的数据组
| 组 | 用途 | R01记录数 | R02记录数 | 说明 |
|----|------|-----------|-----------|------|
| 1 | 两端不匹配 | 4 | 4 | 第1条R01-only、最后1条R02-only、中间3条以上匹配 |
| 2 | 反向两端不匹配 | 4 | 4 | 第1条R02-only、最后1条R01-only、中间3条以上匹配 |
| 3 | 中间不匹配 | 5 | 5 | 首尾匹配、中间有R01-only和R02-only各1条 |
### 组1:两端不匹配
R01键:a001(R01-only) → a002 → a003 → a004
R02键:a002 → a003 → a004 → a005(R02-only)
records数组(按处理顺序):
| record序号 | 内容 | 说明 |
|-----------|------|------|
| 1 | R01(a001) | R01-only |
| 2 | R01(a002) + R02(a002) | 匹配 |
| 3 | R01(a003) + R02(a003) | 匹配 |
| 4 | R01(a004) + R02(a004) | 匹配 |
| 5 | R02(a005) | R02-only |
组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。
和R02(a005)的所有key相比,只有当前第n个key与R02(a005)的key不一致。保证key的比较没有问题。
### 组2:反向两端不匹配
R01键:a002 → a003 → a004 → a005(R01-only)
R02键:a001(R02-only) → a002 → a003 → a004
records数组:
| record序号 | 内容 | 说明 |
|-----------|------|------|
| 1 | R02(a001) | R02-only |
| 2 | R01(a002) + R02(a002) | 匹配 |
| 3 | R01(a003) + R02(a003) | 匹配 |
| 4 | R01(a004) + R02(a004) | 匹配 |
| 5 | R01(a005) | R01-only |
### 组3:中间不匹配
R01键:a001 → a003(R01-only) → a004 → a005
R02键:a001 → a002(R02-only) → a003 → a005
records数组:
| record序号 | 内容 | 说明 |
|-----------|------|------|
| 1 | R01(a001) + R02(a001) | 匹配 |
| 2 | R01(a003) | R01-only |
| 3 | R01(a004) + R02(a003) | 匹配 |
| 4 | R02(a002) | R02-only |
| 5 | R01(a005) + R02(a005) | 匹配 |
## 数据生成步骤
1. 从前提条件解析匹配键字段(如 A001>A002>A003
2. 从COPYBOOK获取各字段的PIC定义
3. 生成键值:键值以 a001/b001/c001 格式递增,保证升序排列
4. 非键字段的数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。
5. 按各组规则组装records数组
6. 组号从1开始连续编号
## 输出
每个组输出一个json文件:`{プログラムID}_g{groupId}.json`
+69
View File
@@ -0,0 +1,69 @@
# マッチング(1:N) 数据生成规则
## 概要
2个输入文件,R01为主驱动,R02为从侧。1件R01可对应多件R02(1:N)。
## 输入
- 输入文件:2个(R01主驱动、R02从侧)
- 前提条件:双方按key升序排序
- 核心逻辑:EVALUATE TRUE 3分岐(R01key<R02key / = / >
## 生成的数据组
| 组 | 用途 | R01记录数 | R02记录数 | 说明 |
|----|------|-----------|-----------|------|
| 1 | 1:1匹配 | 3 | 3 | 全部1:1匹配,中间R01-only和R02-only |
| 2 | 1:N匹配 | 3 | 5 | R01有1:N匹配(同key多条R02) |
| 3 | R02-先结束 | 3 | 2 | R01先结束,第一条数据的R01key<第一条的r02数据 |
| 4 | R02-only | 2 | 4 | R02先结束,第一条数据的R01key>第一条的r02数据 |
### 组11:1匹配
R01: a001→a002→a003
R02: a001→a002→a003
records按处理顺序:
| record序号 | 内容 | 说明 |
|-----------|------|------|
| 1 | R01(a001)+R02(a001) | 匹配 |
| 2 | R01(a002)+R02(a002) | 匹配 |
| 3 | R01(a003)+R02(a003) | 匹配 |
组1:需要注意当key为n个项目组合而成时,在R01的最后需要加上n条数据。
和R02(a003)的所有key相比,只有当前第n个key与R02(a003)的key不一致。保证key的比较没有问题。
### 组21:N匹配
R01: a001→a002→a003
R02: a001→a002→a002→a002→a003
(同key有3条)
records按处理顺序:
| record序号 | 内容 | 说明 |
|-----------|------|------|
| 1 | R01(a001)+R02(a001) | 匹配(第1件) |
| 2 | R01(a002)+R02(a002) | 匹配(第1件) |
| 3 | R01(a002)+R02(a002) | 匹配(第2件) |
| 4 | R01(a002)+R02(a002) | 匹配(第3件) |
| 5 | R01(a003)+R02(a003) | 匹配 |
### 组3R02-先结束
R01: a001→a002→a003
R02: a002→a002
records:全部R01-only
### 组4R01-先结束
R01: a002→a003
R02: a001→a002→a003→a004
recordsR01(a001)→R02(b001~b004)→R01(a002)
## 输出
每组一个json文件:`{プログラムID}_g{groupId}.json`
+36
View File
@@ -0,0 +1,36 @@
# マッチング(N:1) データ生成規則
## 概要
マッチング(1:1)の逆。R01が主駆動でR02が従側。1:Nの逆構成。
## 生成データ組
| 组 | 用途 | R01 | R02 | 説明 |
|----|------|-----|-----|------|
| 1 | 両端不一致 | 4 | 4 | 1件目R02-only、最終R01-only、中間3件マッチ + 末尾多keyテスト |
| 2 | 逆方向両端不一致 | 4 | 4 | 1件目R01-only、最終R02-only、中間3件マッチ + 末尾多keyテスト |
| 3 | 中間不一致 | 5 | 5 | 先頭末尾マッチ、中間にR01-onlyとR02-only各1件 + 末尾多keyテスト |
## 组1:両端不一致
R01: a002→a003→a004→a005(R01-only)
R02: a001(R02-only)→a002→a003→a004
## 组2:逆方向
R01: a001(R01-only)→a002→a003→a004
R02: a002→a003→a004→a005(R02-only)
## 组3:中間不一致
R01: a001→a003(R01-only)→a004→a005
R02: a001→a002(R02-only)→a004→a005
## 多keyテスト
キーが複数項目の場合、组1・组2の末尾に追加。
组1: R01-onlyの最終R01データを基準に、各キー項目のみ異なるN件をR02側に追加
组2: R02-onlyの最終R02データを基準に、各キー項目のみ異なるN件をR01側に追加
これによりマッチングキーの比較が全項目正しく行われていることを検証。
## 出力
各組1 JSONファイル。
@@ -0,0 +1,32 @@
# レイアウト編集のみ(GETPUT) データ生成規則
## 概要
入力ファイルまたはDBからレコードを読み取り、項目の編集・変換を行ってそのまま出力ファイルに書き出すプログラムのテストデータを生成する。
処理パターン:READ → 編集/計算/変換 → WRITE(シンプルなパイプライン)
## 入力
- 詳細設計書の処理詳細(編集・変換内容)
- 入力ファイルのCOPYBOOKフィールド定義
- 出力レコード定義(出力項目と設定元の対応)
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 基本データ | 通常パターン2〜5件。各フィールドに有効な値を設定。 |
## データ生成手順
1. **入力フィールドの把握**: COPYBOOKから全入力フィールドを取得
2. **データ生成**:
- 通常値: 各フィールドのPIC定義に従い有効な値を生成
- 異なる値: レコード間で同じ項目に異なる値を設定しバリエーションを確保
3. **値のルール**:
- PIC X(n): 左詰め・スペース埋め、実在しそうな文字列
- PIC 9(n): 右詰め・先行ゼロ、実在しそうな数値
- DATE/日付系: 実在しそうな日付
- 金額/SALARY系: 現実的な金額
4. **出力**: 1組 = 1 JSONファイル
+54
View File
@@ -0,0 +1,54 @@
# 振り分け データ生成規則
## 概要
単一の入力ファイル(CSVまたは固定長)を読み取り、条件分岐に基づいて複数の出力ファイルに振り分けるプログラムのテストデータを生成する。
## 入力
- 詳細設計書の処理詳細(分岐条件と出力先の対応)
- 入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 全分支カバー | すべての分岐条件をカバーするデータ。各出力先に最低1件ずつ振り分けられるようにする。 |
## データ生成手順
1. **分岐条件の特定**: 処理詳細からすべての分岐条件とそれに対応する出力先を抽出する
2. **分岐パターンの列挙**: 正常系(検証通過→正常出力)と異常系(エラー→ERROR-LOG)の全パターンを列挙
3. **データ生成**:
- 各分岐パターンに最低1件のレコードを生成
- フィールド値はPIC定義に従い生成
- 正常系データ: 全チェックを通過する有効な値
- 異常系データ: 該当するチェックに引っかかる値(不正な日付、不正な時刻、範囲外の値など)
4. **値のルール**:
- PIC X(n): 左詰め・スペース埋め
- PIC 9(n): 右詰め・先行ゼロ
- DATE/日付系: 実在しそうな日付(YYYYMMDD形式)、異常系では不正な日付(99999999等)
- TIME/時刻系: 実在しそうな時刻(HHMM形式)、異常系では範囲外の時刻
- STATUS系: 0/1/9 などのコード値
- CSV項番号フィールド: 分割後の項目数
5. **出力**: 1組 = 1 JSONファイルに全レコードを格納
## 出力形式
```json
{
"program": "ZAN01CHK",
"records": [
{
"input": {
"DD名": {
"フィールド名": "値",
...
}
}
}
]
}
```
各レコードが1回の処理に対応。正常系→異常系→特殊分岐の順に並べる。
+31
View File
@@ -0,0 +1,31 @@
# 項目チェック データ生成規則
## 概要
入力ファイル(CSV)の各レコードに対し、複数の項目チェックを実施し、正常データと異常データを別々の出力ファイルに振り分けるプログラムのテストデータを生成する。
## 入力
- 詳細設計書の処理詳細(チェック項目一覧)
- 入力ファイルのCOPYBOOKフィールド定義
## 生成データ組
| 组 | 用途 | 内容 |
|----|------|------|
| 1 | 全チェックカバー | すべてのチェック項目について、合格データと不合格データの両方をカバーする。 |
## データ生成手順
1. **チェック項目の特定**: 処理詳細からすべてのチェック項目(フィールド数、値域、重複、時刻範囲、コード値など)を抽出する
2. **各チェックに対するデータ生成**:
- 合格データ: 全チェックを通過する有効な値
- 不合格データ: 各チェック項目に特化した不正な値を含むレコード
3. **値のルール**:
- フィールド数チェック: 正しい項目数/足りない/多い
- 日付チェック: 正常日付 vs 不正日付 (99999999等)
- 時刻チェック: 正常時刻 (0900-1800) vs 範囲外 (2500等)
- 重複チェック: 同一キー2件 vs 全件別キー
- コード値チェック: 有効コード vs 無効コード
- 社員番号: 8桁数字 vs 非数字/桁数不足
4. **出力**: 1組 = 1 JSONファイル
+27
View File
@@ -0,0 +1,27 @@
# 条件分支 数据生成规则
## 概要
检索详细设计中処理詳細的所有条件分支,增加覆盖所有分支的输入数据。
## 输入
- 输入文件:详细设计书的処理詳細部分
## 生成的数据组
由你决定,以尽可能少的数据组完成分支覆盖
## 数据生成步骤
1. 遍历処理詳細的所有内容
2. 记录所有条件分支
3. 找到条件判断数据与输入的关系
4. 数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。
5. 按各组规则组装records数组
6. 组号从之前的最后一组+1开始连续编号
## 输出
每个组输出一个json文件:`{プログラムID}_g{groupId}.json`
+8 -15
View File
@@ -310,22 +310,13 @@ class GixsqlCobolRunner:
logger.info(f" cobc: {' '.join(cmd)}")
try:
r = subprocess.run(cmd, capture_output=True, timeout=60, env=self._build_env())
r = subprocess.run(cmd, capture_output=True, timeout=60, env=self._build_env(),
cwd=str(exe_dir))
log = (r.stdout.decode("utf-8", "replace") + "\n" +
r.stderr.decode("utf-8", "replace"))
if r.returncode != 0:
return GixsqlBuildResult(False, log=log[:1000])
# After successful compile, copy .gcno from CWD to exe_dir
pp_stem = pp_path.stem # e.g. "KIN02UPD_pp"
exe_stem = exe_path.stem # e.g. "KIN02UPD"
copied = 0
for gcno in Path.cwd().glob("*.gcno"):
# Match .gcno files belonging to this compile (by subprogram name or pp_stem partial match)
dst = exe_dir / gcno.name
dst.write_bytes(gcno.read_bytes())
copied += 1
if copied:
logger.debug(f" gcno copied: {copied} files to {exe_dir}")
# .gcno files are generated in exe_dir (compile CWD). No need to copy.
return GixsqlBuildResult(True, exe_path=str(exe_path), log=log[:500])
except subprocess.TimeoutExpired:
return GixsqlBuildResult(False, log="Compile timeout (60s)")
@@ -334,7 +325,8 @@ class GixsqlCobolRunner:
db_path: str | Path,
input_dir: str | Path | None = None,
timeout: int = 30,
cobol_lib_path: str | Path | None = None) -> GixsqlRunResult:
cobol_lib_path: str | Path | None = None,
env_overrides: dict[str, str] | None = None) -> GixsqlRunResult:
"""Step 3: COBOL DB プログラム実行"""
exe_path = Path(exe_path)
work_dir = Path(work_dir)
@@ -379,6 +371,8 @@ class GixsqlCobolRunner:
env["GIXSQL_DB_PATH"] = str(db_path)
if cobol_lib_path:
env["COB_LIBRARY_PATH"] = str(cobol_lib_path)
if env_overrides:
env.update(env_overrides)
if input_dir:
idir = Path(input_dir)
@@ -386,8 +380,7 @@ class GixsqlCobolRunner:
for f in idir.iterdir():
if f.is_file():
dst = work_dir / f.name
if not dst.exists():
dst.write_bytes(f.read_bytes())
dst.write_bytes(f.read_bytes())
cmd = [str(exe_path)]
logger.info(f" run: {' '.join(cmd)} (cwd={work_dir}, db={db_path})")