From 8b49d46dd2438601c788ef8d9b8bdc69557eb49f Mon Sep 17 00:00:00 2001
From: kiyotis
Date: Fri, 15 May 2026 15:32:28 +0900
Subject: [PATCH 001/343] =?UTF-8?q?Excel=E3=83=86=E3=82=B9=E3=83=88?=
=?UTF-8?q?=E3=83=87=E3=83=BC=E3=82=BF=E3=81=AEYAML=E4=BB=A3=E6=9B=BF?=
=?UTF-8?q?=E3=82=B9=E3=82=AD=E3=83=BC=E3=83=9E=E8=A8=AD=E8=A8=88=E3=83=89?=
=?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=82=92=E8=BF=BD?=
=?UTF-8?q?=E5=8A=A0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 4.6
---
docs/ntf-testdata-structure.md | 188 +++++++++++++++++++
docs/ntf-testdata-yaml-design.md | 89 +++++++++
docs/ntf-testdata-yaml-examples.yaml | 232 +++++++++++++++++++++++
docs/ntf-testdata-yaml-schema.json | 268 +++++++++++++++++++++++++++
4 files changed, 777 insertions(+)
create mode 100644 docs/ntf-testdata-structure.md
create mode 100644 docs/ntf-testdata-yaml-design.md
create mode 100644 docs/ntf-testdata-yaml-examples.yaml
create mode 100644 docs/ntf-testdata-yaml-schema.json
diff --git a/docs/ntf-testdata-structure.md b/docs/ntf-testdata-structure.md
new file mode 100644
index 00000000..b6caf4fe
--- /dev/null
+++ b/docs/ntf-testdata-structure.md
@@ -0,0 +1,188 @@
+# NTFテストデータ構造 調査報告
+
+根拠: コードおよびJavadocのみ。推測なし。
+
+---
+
+## 1. テストデータの論理単位
+
+| レベル | Excel上の単位 | 備考 | 根拠クラス |
+|---|---|---|---|
+| ファイル | `.xls` / `.xlsx` ファイル | `.xls`が存在しない場合に`.xlsx`を試みる | `PoiXlsReader` |
+| シート | 1シート = 1テストデータリソース | `dataName` = `"ファイル名/シート名"`(`/`区切り) | `PoiXlsReader` |
+| データブロック | シート内の連続した行群 | データタイプ行(例: `SETUP_TABLE=...`)が起点 | `TestDataParsingTemplate` |
+| 行 | 空行・コメント行はスキップ | 先頭セルが`//`始まりはコメント行 | `TestDataParsingTemplate` |
+| セル | 全て文字列化 | `cell == null` → `""` | `PoiXlsReader` |
+
+---
+
+## 2. データ種別の完全な列挙
+
+根拠: `nablarch.test.core.reader.DataType`(enum)
+
+| enum定数 | Excel識別文字列 | 用途 | テンプレート種別 |
+|---|---|---|---|
+| `SETUP_TABLE_DATA` | `SETUP_TABLE` | DB事前準備用テーブルデータ | GroupData |
+| `EXPECTED_TABLE_DATA` | `EXPECTED_TABLE` | 期待値テーブルデータ | GroupData |
+| `EXPECTED_COMPLETED` | `EXPECTED_COMPLETE_TABLE` | 期待値テーブル(省略カラムにデフォルト値補完) | GroupData |
+| `LIST_MAP` | `LIST_MAP` | `List
+ */
+public class YamlTestDataReader implements TestDataReader {
+
+ /** DataType 名と YAML トップレベルキーのマッピング */
+ private static final List SECTION_TYPES = buildSectionTypes();
+
+ /** 読み込んだ行シーケンス */
+ private List> rows;
+
+ /** 現在の読み込み位置 */
+ private int index;
+
+ @Override
+ public void open(String path, String dataName) {
+ if (StringUtil.isNullOrEmpty(dataName)) {
+ throw new IllegalArgumentException("dataName must not be null or empty.");
+ }
+
+ File file = new File(path, dataName + ".yaml");
+ if (!file.exists()) {
+ throw new RuntimeException("YAML test data file not found: " + file.getAbsolutePath());
+ }
+
+ Map yaml = loadYaml(file);
+ rows = buildRows(yaml);
+ index = 0;
+ }
+
+ @Override
+ public void close() {
+ rows = null;
+ index = 0;
+ }
+
+ @Override
+ public List readLine() {
+ if (rows == null || index >= rows.size()) {
+ return null;
+ }
+ return rows.get(index++);
+ }
+
+ @Override
+ public boolean isResourceExisting(String basePath, String resourceName) {
+ return new File(basePath, resourceName + ".yaml").exists();
+ }
+
+ @Override
+ public boolean isDataExisting(String basePath, String resourceName) {
+ return new File(basePath, resourceName + ".yaml").exists();
+ }
+
+ // -----------------------------------------------------------------------
+ // YAML ロード
+ // -----------------------------------------------------------------------
+
+ @SuppressWarnings("unchecked")
+ private Map loadYaml(File file) {
+ LoaderOptions options = new LoaderOptions();
+ Yaml yaml = new Yaml(new SafeConstructor(options));
+ try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
+ Object result = yaml.load(reader);
+ if (result instanceof Map) {
+ return (Map) result;
+ }
+ return Collections.emptyMap();
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to load YAML file: " + file.getAbsolutePath(), e);
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // YAML → 行シーケンス変換
+ // -----------------------------------------------------------------------
+
+ private List> buildRows(Map yaml) {
+ List> result = new ArrayList>();
+ for (SectionType st : SECTION_TYPES) {
+ Object entries = yaml.get(st.yamlKey);
+ if (entries == null) {
+ continue;
+ }
+ for (Object entry : asList(entries)) {
+ Map entryMap = asMap(entry);
+ st.converter.convert(entryMap, result);
+ }
+ }
+ return result;
+ }
+
+ // -----------------------------------------------------------------------
+ // セクション種別定義
+ // -----------------------------------------------------------------------
+
+ private interface RowConverter {
+ void convert(Map entry, List> out);
+ }
+
+ private static class SectionType {
+ final String yamlKey;
+ final RowConverter converter;
+
+ SectionType(String yamlKey, RowConverter converter) {
+ this.yamlKey = yamlKey;
+ this.converter = converter;
+ }
+ }
+
+ private static List buildSectionTypes() {
+ List list = new ArrayList();
+
+ // テーブル系(GroupData)
+ list.add(new SectionType("setup_tables",
+ new TableRowConverter("SETUP_TABLE")));
+ list.add(new SectionType("expected_tables",
+ new TableRowConverter("EXPECTED_TABLE")));
+ list.add(new SectionType("expected_complete_tables",
+ new TableRowConverter("EXPECTED_COMPLETE_TABLE")));
+
+ // LIST_MAP(SingleData)
+ list.add(new SectionType("list_maps",
+ new ListMapRowConverter()));
+
+ // ファイル系(GroupData)
+ list.add(new SectionType("setup_files",
+ new FileRowConverter("setup_files")));
+ list.add(new SectionType("expected_files",
+ new FileRowConverter("expected_files")));
+
+ // メッセージ系(SingleData)
+ list.add(new SectionType("messages",
+ new MessageRowConverter("MESSAGE")));
+ list.add(new SectionType("expected_request_header_messages",
+ new MessageRowConverter("EXPECTED_REQUEST_HEADER_MESSAGES")));
+ list.add(new SectionType("expected_request_body_messages",
+ new MessageRowConverter("EXPECTED_REQUEST_BODY_MESSAGES")));
+
+ // GroupMessage 系(GroupData)
+ list.add(new SectionType("response_header_messages",
+ new GroupMessageRowConverter("RESPONSE_HEADER_MESSAGES")));
+ list.add(new SectionType("response_body_messages",
+ new GroupMessageRowConverter("RESPONSE_BODY_MESSAGES")));
+
+ return Collections.unmodifiableList(list);
+ }
+
+ // -----------------------------------------------------------------------
+ // テーブル系コンバータ
+ // -----------------------------------------------------------------------
+
+ private static class TableRowConverter implements RowConverter {
+ private final String dataTypeName;
+
+ TableRowConverter(String dataTypeName) {
+ this.dataTypeName = dataTypeName;
+ }
+
+ @Override
+ public void convert(Map entry, List> out) {
+ String groupId = asString(entry.get("group_id"));
+ String tableName = asString(entry.get("table"));
+
+ // セクションヘッダ行: ["SETUP_TABLE[groupId]=TABLE_NAME"] or ["SETUP_TABLE=TABLE_NAME"]
+ String header = groupId == null
+ ? dataTypeName + "=" + tableName
+ : dataTypeName + "[" + groupId + "]=" + tableName;
+ out.add(singletonRow(header));
+
+ List> rows = asMapList(entry.get("rows"));
+ if (rows.isEmpty()) {
+ return;
+ }
+
+ // 全行の全キーを union して列順を決定(挿入順を保持)
+ Set allKeys = collectAllKeys(rows);
+
+ // カラムヘッダ行: ["", col1, col2, ...]
+ List colHeader = new ArrayList();
+ colHeader.add("");
+ colHeader.addAll(allKeys);
+ out.add(colHeader);
+
+ // データ行: ["", val1, val2, ...]
+ for (Map row : rows) {
+ List dataRow = new ArrayList();
+ dataRow.add("");
+ for (String key : allKeys) {
+ Object val = row.get(key);
+ // キーが存在しない(省略)場合は null 扱い → "" 補完(RS-06)
+ dataRow.add(toCell(val, !row.containsKey(key)));
+ }
+ out.add(dataRow);
+ }
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // LIST_MAP コンバータ
+ // -----------------------------------------------------------------------
+
+ private static class ListMapRowConverter implements RowConverter {
+ @Override
+ public void convert(Map entry, List> out) {
+ String id = asString(entry.get("id"));
+
+ // セクションヘッダ行
+ out.add(singletonRow("LIST_MAP=" + id));
+
+ List> rows = asMapList(entry.get("rows"));
+ if (rows.isEmpty()) {
+ return;
+ }
+
+ Set allKeys = collectAllKeys(rows);
+
+ // カラムヘッダ行
+ List colHeader = new ArrayList();
+ colHeader.add("");
+ colHeader.addAll(allKeys);
+ out.add(colHeader);
+
+ // データ行
+ for (Map row : rows) {
+ List dataRow = new ArrayList();
+ dataRow.add("");
+ for (String key : allKeys) {
+ dataRow.add(toCell(row.get(key), !row.containsKey(key)));
+ }
+ out.add(dataRow);
+ }
+ }
+ }
+
+ // -----------------------------------------------------------------------
+ // ファイル系コンバータ(固定長・可変長)
+ // -----------------------------------------------------------------------
+
+ private static class FileRowConverter implements RowConverter {
+ private final String yamlKey;
+
+ FileRowConverter(String yamlKey) {
+ this.yamlKey = yamlKey;
+ }
+
+ @Override
+ public void convert(Map entry, List> out) {
+ String groupId = asString(entry.get("group_id"));
+ String path = asString(entry.get("path"));
+ String type = asString(entry.get("type")); // "fixed" or "variable"
+
+ String dataTypeName = resolveFileDataType(yamlKey, type);
+
+ // セクションヘッダ行
+ String header = groupId == null
+ ? dataTypeName + "=" + path
+ : dataTypeName + "[" + groupId + "]=" + path;
+ out.add(singletonRow(header));
+
+ // ディレクティブ行
+ Map directives = asMap(entry.get("directives"));
+ for (Map.Entry d : directives.entrySet()) {
+ out.add(Arrays.asList(d.getKey(), toCell(d.getValue(), false)));
+ }
+
+ // records
+ List