From f3be17e5eb5f6f33f0abc9ac37ca82dafc21549f Mon Sep 17 00:00:00 2001 From: hangshuo652 Date: Sun, 12 Jul 2026 21:04:58 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A4=9A=E8=BD=AE=E8=BF=90=E8=A1=8C=20?= =?UTF-8?q?+=20GCOV=20=E5=90=88=E5=B9=B6=20+=20JSON=20=E5=87=BA=E5=8A=9B?= =?UTF-8?q?=20+=20DesignDataGenerator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + agents/__init__.py | 19 +- agents/design_data.py | 291 +++++++ agents/design_data_input_parser.py | 270 ++++++ cobol_testgen/__init__.py | 37 +- cobol_testgen/data_merger.py | 130 +++ cobol_testgen/design.py | 49 +- cobol_testgen/flatfile.py | 103 ++- cobol_testgen/gcov.py | 3 + cobol_testgen/read.py | 8 +- cobol_testgen/runner.py | 154 ++-- config/__init__.py | 4 +- config/program_schema.py | 29 + config/programs/KIN08DBU.yaml | 25 + docs/output-directory-rules.md | 53 ++ .../specs/2026-07-12-design-data-generator.md | 332 ++++++++ layout/DB定義書.md | 347 ++++++++ layout/JSON格式说明v2.0.md | 435 ++++++++++ layout/sample_ソース_SAN01MAT.cbl | 432 ++++++++++ layout/sample_詳細設計書_SAN01MAT.md | 141 ++++ layout/ファイル定義書_xxxxxxx.md | 142 ++++ layout/詳細設計書_XXXXXXXX.md | 145 ++++ orchestrator_db.py | 783 +++++++++++++++--- rules/pgm_pattern/50分割.md | 25 + rules/pgm_pattern/CSV→FB変換.md | 28 + rules/pgm_pattern/DB更新.md | 79 ++ rules/pgm_pattern/GETPUT(編集出力).md | 28 + rules/pgm_pattern/MERGE.md | 25 + rules/pgm_pattern/SELECT処理.md | 31 + rules/pgm_pattern/キーブレイク(集約).md | 32 + rules/pgm_pattern/キーブレイク(集計).md | 37 + rules/pgm_pattern/キーブレイク(集計、集約).md | 25 + rules/pgm_pattern/マッチング(1-1).md | 78 ++ rules/pgm_pattern/マッチング(1-N).md | 69 ++ rules/pgm_pattern/マッチング(N-1).md | 36 + .../pgm_pattern/レイアウト編集のみ(GETPUT).md | 32 + rules/pgm_pattern/振り分け.md | 54 ++ rules/pgm_pattern/項目チェック.md | 31 + rules/special_feature/条件分支.md | 27 + runners/gixsql_runner.py | 23 +- 40 files changed, 4397 insertions(+), 198 deletions(-) create mode 100644 agents/design_data.py create mode 100644 agents/design_data_input_parser.py create mode 100644 cobol_testgen/data_merger.py create mode 100644 docs/output-directory-rules.md create mode 100644 docs/superpowers/specs/2026-07-12-design-data-generator.md create mode 100644 layout/DB定義書.md create mode 100644 layout/JSON格式说明v2.0.md create mode 100644 layout/sample_ソース_SAN01MAT.cbl create mode 100644 layout/sample_詳細設計書_SAN01MAT.md create mode 100644 layout/ファイル定義書_xxxxxxx.md create mode 100644 layout/詳細設計書_XXXXXXXX.md create mode 100644 rules/pgm_pattern/50分割.md create mode 100644 rules/pgm_pattern/CSV→FB変換.md create mode 100644 rules/pgm_pattern/DB更新.md create mode 100644 rules/pgm_pattern/GETPUT(編集出力).md create mode 100644 rules/pgm_pattern/MERGE.md create mode 100644 rules/pgm_pattern/SELECT処理.md create mode 100644 rules/pgm_pattern/キーブレイク(集約).md create mode 100644 rules/pgm_pattern/キーブレイク(集計).md create mode 100644 rules/pgm_pattern/キーブレイク(集計、集約).md create mode 100644 rules/pgm_pattern/マッチング(1-1).md create mode 100644 rules/pgm_pattern/マッチング(1-N).md create mode 100644 rules/pgm_pattern/マッチング(N-1).md create mode 100644 rules/pgm_pattern/レイアウト編集のみ(GETPUT).md create mode 100644 rules/pgm_pattern/振り分け.md create mode 100644 rules/pgm_pattern/項目チェック.md create mode 100644 rules/special_feature/条件分支.md diff --git a/.gitignore b/.gitignore index fc90079..d25ba26 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,6 @@ _test_flatfiles/ # External dependencies (not committed) gixsql/ cobol-tna-system/ + +# ԔOӋOӋԭ塢_ֹ +ԔOӋ/ diff --git a/agents/__init__.py b/agents/__init__.py index e26bc93..b42b038 100644 --- a/agents/__init__.py +++ b/agents/__init__.py @@ -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 ] diff --git a/agents/design_data.py b/agents/design_data.py new file mode 100644 index 0000000..dc1f1a0 --- /dev/null +++ b/agents/design_data.py @@ -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 diff --git a/agents/design_data_input_parser.py b/agents/design_data_input_parser.py new file mode 100644 index 0000000..8d35bcf --- /dev/null +++ b/agents/design_data_input_parser.py @@ -0,0 +1,270 @@ +"""式样书解析器 — 读取详细设计书 .md,提取 ProgramMeta 信息。 + +从外部 agent(jcl-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" diff --git a/cobol_testgen/__init__.py b/cobol_testgen/__init__.py index 792f6f3..89a6d38 100644 --- a/cobol_testgen/__init__.py +++ b/cobol_testgen/__init__.py @@ -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) diff --git a/cobol_testgen/data_merger.py b/cobol_testgen/data_merger.py new file mode 100644 index 0000000..ac2ff66 --- /dev/null +++ b/cobol_testgen/data_merger.py @@ -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 diff --git a/cobol_testgen/design.py b/cobol_testgen/design.py index 9cfe3ee..73f10f1 100644 --- a/cobol_testgen/design.py +++ b/cobol_testgen/design.py @@ -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)): diff --git a/cobol_testgen/flatfile.py b/cobol_testgen/flatfile.py index 58a3a91..dd2350c 100644 --- a/cobol_testgen/flatfile.py +++ b/cobol_testgen/flatfile.py @@ -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) diff --git a/cobol_testgen/gcov.py b/cobol_testgen/gcov.py index 01abdff..664466f 100644 --- a/cobol_testgen/gcov.py +++ b/cobol_testgen/gcov.py @@ -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: diff --git a/cobol_testgen/read.py b/cobol_testgen/read.py index 20bf67b..9ffadfe 100644 --- a/cobol_testgen/read.py +++ b/cobol_testgen/read.py @@ -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) diff --git a/cobol_testgen/runner.py b/cobol_testgen/runner.py index 39a89c4..15fadc1 100644 --- a/cobol_testgen/runner.py +++ b/cobol_testgen/runner.py @@ -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 兼容)── diff --git a/config/__init__.py b/config/__init__.py index 21fd025..1d0af63 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -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" diff --git a/config/program_schema.py b/config/program_schema.py index f1b956e..030940b 100644 --- a/config/program_schema.py +++ b/config/program_schema.py @@ -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, ) diff --git a/config/programs/KIN08DBU.yaml b/config/programs/KIN08DBU.yaml index e78b7be..9929298 100644 --- a/config/programs/KIN08DBU.yaml +++ b/config/programs/KIN08DBU.yaml @@ -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 T(NOT WRK-PERIOD-OK)をカバー:P 卡なし + sysin: + period: null + include_invalid_period: false + modes: ["NORMAL"] + inject_duplicate_pk: false + + - id: insert_error + # #13 T / #28 T(INSERT SQL エラー)をカバー:PK 競合行注入 + sysin: + period: "202607" + include_invalid_period: true + modes: ["NORMAL", "RESET"] + inject_duplicate_pk: true diff --git a/docs/output-directory-rules.md b/docs/output-directory-rules.md new file mode 100644 index 0000000..0769f82 --- /dev/null +++ b/docs/output-directory-rules.md @@ -0,0 +1,53 @@ +# 出力目录规则 + +## 单轮(runs: [] 或 YAML 不存在) + +```text +runtime/{program_id}/ + input/KIN08R01, KIN08S01 ← COBOL 入力平面文件 + json/{program_id}.json ← JSON(input/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()` 解析,字段含义不跨程序通用。 diff --git a/docs/superpowers/specs/2026-07-12-design-data-generator.md b/docs/superpowers/specs/2026-07-12-design-data-generator.md new file mode 100644 index 0000000..98d45b2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-design-data-generator.md @@ -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 | TestSuite(LLM 数据) | `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-ID(REPLACING ==(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 | diff --git a/layout/DB定義書.md b/layout/DB定義書.md new file mode 100644 index 0000000..34a4ecd --- /dev/null +++ b/layout/DB定義書.md @@ -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.50(50%控除) | + +### 備考 + +- 全社員共通の設定値 +- 現時点では参照プログラム未実装(設計上定義) + +--- + +# 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が月次集計後にUPSERT(MERGE) +- サブシステム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-MASTER(SALARYDB) — 社員マスタ(給与計算用) + +## 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-TABLE(SALARYDB) — 所得税率テーブル + +## 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-TABLE(SALARYDB) — 社会保険料率テーブル + +## 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-RESULTS(SALARYDB) — 給与計算結果テーブル + +## 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を実行 diff --git a/layout/JSON格式说明v2.0.md b/layout/JSON格式说明v2.0.md new file mode 100644 index 0000000..9063a7d --- /dev/null +++ b/layout/JSON格式说明v2.0.md @@ -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-only:record内只含R01的FD +- R02-only:record内只含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组内包含多条record(R01-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 +``` diff --git a/layout/sample_ソース_SAN01MAT.cbl b/layout/sample_ソース_SAN01MAT.cbl new file mode 100644 index 0000000..5bc28a5 --- /dev/null +++ b/layout/sample_ソース_SAN01MAT.cbl @@ -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. diff --git a/layout/sample_詳細設計書_SAN01MAT.md b/layout/sample_詳細設計書_SAN01MAT.md new file mode 100644 index 0000000..5549762 --- /dev/null +++ b/layout/sample_詳細設計書_SAN01MAT.md @@ -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(終了メッセージ) +``` + +--- + +## 出力レコード定義 + +### 出力ファイル1(W01) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R01.同項目を設定 | | +| 2 | A002 | R01.同項目を設定 | | +| 3 | A003 | R01.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R01.同項目を設定 | | + +### 出力ファイル2(W02) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R01.同項目を設定 | | +| 2 | A002 | R01.同項目を設定 | | +| 3 | A003 | R01.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R01.同項目を設定 | | + +### 出力ファイル3(W03) + +| No | 項目名 | 設定元 | 備考 | +|----|--------|--------|------| +| 1 | A001 | R02.同項目を設定 | | +| 2 | A002 | R02.同項目を設定 | | +| 3 | A003 | R02.同項目を設定 | | +| 4 | … | | | +| 5 | A020 | R02.同項目を設定 | | diff --git a/layout/ファイル定義書_xxxxxxx.md b/layout/ファイル定義書_xxxxxxx.md new file mode 100644 index 0000000..25de2e3 --- /dev/null +++ b/layout/ファイル定義書_xxxxxxx.md @@ -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 | | diff --git a/layout/詳細設計書_XXXXXXXX.md b/layout/詳細設計書_XXXXXXXX.md new file mode 100644 index 0000000..a36a817 --- /dev/null +++ b/layout/詳細設計書_XXXXXXXX.md @@ -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 | | | | diff --git a/orchestrator_db.py b/orchestrator_db.py index a4170ba..b21e942 100644 --- a/orchestrator_db.py +++ b/orchestrator_db.py @@ -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 diff --git a/rules/pgm_pattern/50分割.md b/rules/pgm_pattern/50分割.md new file mode 100644 index 0000000..a034ab2 --- /dev/null +++ b/rules/pgm_pattern/50分割.md @@ -0,0 +1,25 @@ +# 50分割 データ生成規則 + +## 概要 + +入力ファイルのレコードを50件ずつ分割し、複数の出力ファイルに振り分けるプログラムのテストデータ。 + +## 入力 + +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 通常分割 | 51件以上のデータ(50件の境界を超えるケース)+ 50件未満のデータ | + +## データ生成手順 + +1. 入力ファイルのフィールド定義を取得 +2. 組1に51件以上のレコードを生成(分割境界テスト用) +3. 各フィールドはPIC定義に従い実在しそうな値を生成 + +## 出力 + +各組1 JSONファイル。 diff --git a/rules/pgm_pattern/CSV→FB変換.md b/rules/pgm_pattern/CSV→FB変換.md new file mode 100644 index 0000000..43e0d2f --- /dev/null +++ b/rules/pgm_pattern/CSV→FB変換.md @@ -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ファイル。 diff --git a/rules/pgm_pattern/DB更新.md b/rules/pgm_pattern/DB更新.md new file mode 100644 index 0000000..73efd5a --- /dev/null +++ b/rules/pgm_pattern/DB更新.md @@ -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の後にカンマ区切りで複数行) diff --git a/rules/pgm_pattern/GETPUT(編集出力).md b/rules/pgm_pattern/GETPUT(編集出力).md new file mode 100644 index 0000000..8ab739e --- /dev/null +++ b/rules/pgm_pattern/GETPUT(編集出力).md @@ -0,0 +1,28 @@ +# GETPUT(編集出力)データ生成規則 + +## 概要 + +DBまたはファイルからデータを読み取り、編集・整形して出力するプログラムのテストデータ。 +レイアウト編集のみ(GETPUT)と同様のシンプルなREAD→編集→WRITEパターン。 + +## 入力 + +- 入力ソースのフィールド定義(COPYBOOKまたはDBテーブル定義) +- 出力レコード定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本データ | 通常値2〜5件。各フィールドに有効な値を設定。異なる値のバリエーションを確保 | + +## データ生成手順 + +1. 入力フィールドを特定 +2. 各フィールドのPIC定義に従い値を生成 +3. 実在しそうな値(日付・時刻・金額・氏名等)を生成 +4. レコード間で異なる値を設定 + +## 出力 + +各組1 JSONファイル。DBからの入力の場合はSQLファイルも出力。 diff --git a/rules/pgm_pattern/MERGE.md b/rules/pgm_pattern/MERGE.md new file mode 100644 index 0000000..f77e297 --- /dev/null +++ b/rules/pgm_pattern/MERGE.md @@ -0,0 +1,25 @@ +# MERGE(複数ファイル結合) データ生成規則 + +## 概要 + +複数の入力ファイルをキーでソート・マージし、統合された1つの出力ファイルを生成するプログラムのテストデータ。 + +## 入力 + +- 詳細設計書のキー項目一覧(マージキー) +- 各入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 全ファイルカバー | 全入力ファイルのデータが出力に反映されるデータ + 末尾多keyテスト | +| 2 | キー境界 | 同一キー値が複数ファイルに分散しているケース + 末尾多keyテスト | + +## 多keyテスト + +キーが複数項目の場合、各組末尾に追加。各キー項目のみ異なるN件を生成し、正しいキーでマージされることを検証。 + +## 出力 + +各組1 JSONファイル。 diff --git a/rules/pgm_pattern/SELECT処理.md b/rules/pgm_pattern/SELECT処理.md new file mode 100644 index 0000000..dd8b995 --- /dev/null +++ b/rules/pgm_pattern/SELECT処理.md @@ -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ファイル。 diff --git a/rules/pgm_pattern/キーブレイク(集約).md b/rules/pgm_pattern/キーブレイク(集約).md new file mode 100644 index 0000000..71706a0 --- /dev/null +++ b/rules/pgm_pattern/キーブレイク(集約).md @@ -0,0 +1,32 @@ +# キーブレイク(集約) データ生成規則 + +## 概要 + +キー項目でソート済みの入力ファイルを読み取り、キーブレイク時に集約レコード(カウント・合計など)を出力するプログラムのテストデータを生成する。 + +## 入力 + +- 詳細設計書のキー項目一覧 +- 入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 基本集約 | 複数グループの集約データ + 末尾に多keyテスト | +| 2 | 境界ケース | キー境界での正しい集約切替を確認するデータ + 末尾に多keyテスト | + +## 多keyテスト + +キーが複数項目の場合、各組末尾に追加。1レコードを基準に、各キー項目のみ異なるN件を生成。 + +## データ生成手順 + +1. キー項目の特定 +2. 组1: 3グループ以上、各グループ内に複数レコード(集約対象) +3. 组2: キー値が1件ずつ異なる境界データ +4. 各組末尾に多keyテスト + +## 出力 + +各組1 JSONファイル。 diff --git a/rules/pgm_pattern/キーブレイク(集計).md b/rules/pgm_pattern/キーブレイク(集計).md new file mode 100644 index 0000000..56bbccd --- /dev/null +++ b/rules/pgm_pattern/キーブレイク(集計).md @@ -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ファイル。 diff --git a/rules/pgm_pattern/キーブレイク(集計、集約).md b/rules/pgm_pattern/キーブレイク(集計、集約).md new file mode 100644 index 0000000..7c15963 --- /dev/null +++ b/rules/pgm_pattern/キーブレイク(集計、集約).md @@ -0,0 +1,25 @@ +# キーブレイク(集計、集約) データ生成規則 + +## 概要 + +ソート済み入力ファイルを読み取り、キーブレイクで集計・集約を行い結果を出力するプログラムのテストデータ。複数ファイル入力の場合はマッチングと集計を組み合わせる。 + +## 入力 + +- 詳細設計書のキー項目一覧、使用ファイル一覧 +- 各入力ファイルのCOPYBOOKフィールド定義 + +## 生成データ組 + +| 组 | 用途 | 内容 | +|----|------|------| +| 1 | 通常集計 | 複数グループ・各グループ複数レコード + 末尾多keyテスト | +| 2 | 同キー内集約 | 同一キー内でN件集約されるデータ + 末尾多keyテスト | + +## 多keyテスト + +キーが複数項目の場合、各組末尾に追加。1レコードを基準に各キー項目のみ異なるN件を生成。 + +## 出力 + +各組1 JSONファイル。 diff --git a/rules/pgm_pattern/マッチング(1-1).md b/rules/pgm_pattern/マッチング(1-1).md new file mode 100644 index 0000000..b45c577 --- /dev/null +++ b/rules/pgm_pattern/マッチング(1-1).md @@ -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` diff --git a/rules/pgm_pattern/マッチング(1-N).md b/rules/pgm_pattern/マッチング(1-N).md new file mode 100644 index 0000000..6ab3e51 --- /dev/null +++ b/rules/pgm_pattern/マッチング(1-N).md @@ -0,0 +1,69 @@ +# マッチング(1:N) 数据生成规则 + +## 概要 + +2个输入文件,R01为主驱动,R02为从侧。1件R01可对应多件R02(1:N)。 + +## 输入 + +- 输入文件:2个(R01主驱动、R02从侧) +- 前提条件:双方按key升序排序 +- 核心逻辑:EVALUATE TRUE 3分岐(R01key) + +## 生成的数据组 + +| 组 | 用途 | 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数据 | + +### 组1:1: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的比较没有问题。 + + +### 组2:1: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) | 匹配 | + +### 组3:R02-先结束 + +R01: a001→a002→a003 +R02: a002→a002 + +records:全部R01-only + +### 组4:R01-先结束 + +R01: a002→a003 +R02: a001→a002→a003→a004 + +records:R01(a001)→R02(b001~b004)→R01(a002) + +## 输出 + +每组一个json文件:`{プログラムID}_g{groupId}.json` diff --git a/rules/pgm_pattern/マッチング(N-1).md b/rules/pgm_pattern/マッチング(N-1).md new file mode 100644 index 0000000..7033de7 --- /dev/null +++ b/rules/pgm_pattern/マッチング(N-1).md @@ -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ファイル。 diff --git a/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md b/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md new file mode 100644 index 0000000..794c268 --- /dev/null +++ b/rules/pgm_pattern/レイアウト編集のみ(GETPUT).md @@ -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ファイル diff --git a/rules/pgm_pattern/振り分け.md b/rules/pgm_pattern/振り分け.md new file mode 100644 index 0000000..361be83 --- /dev/null +++ b/rules/pgm_pattern/振り分け.md @@ -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回の処理に対応。正常系→異常系→特殊分岐の順に並べる。 diff --git a/rules/pgm_pattern/項目チェック.md b/rules/pgm_pattern/項目チェック.md new file mode 100644 index 0000000..fdb553f --- /dev/null +++ b/rules/pgm_pattern/項目チェック.md @@ -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ファイル diff --git a/rules/special_feature/条件分支.md b/rules/special_feature/条件分支.md new file mode 100644 index 0000000..82a21b0 --- /dev/null +++ b/rules/special_feature/条件分支.md @@ -0,0 +1,27 @@ +# 条件分支 数据生成规则 + +## 概要 + +检索详细设计中処理詳細的所有条件分支,增加覆盖所有分支的输入数据。 + +## 输入 + +- 输入文件:详细设计书的処理詳細部分 + +## 生成的数据组 + +由你决定,以尽可能少的数据组完成分支覆盖 + + +## 数据生成步骤 + +1. 遍历処理詳細的所有内容 +2. 记录所有条件分支 +3. 找到条件判断数据与输入的关系 +4. 数据生成:根据字段名关键词生成符合实际的数据,同时尽量保证每条相邻的数据的相同项目不一致。 +5. 按各组规则组装records数组 +6. 组号从之前的最后一组+1开始连续编号 + +## 输出 + +每个组输出一个json文件:`{プログラムID}_g{groupId}.json` diff --git a/runners/gixsql_runner.py b/runners/gixsql_runner.py index c610b7d..5f0cb8a 100644 --- a/runners/gixsql_runner.py +++ b/runners/gixsql_runner.py @@ -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})")