黑盒,白盒处理合并
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
|
||||
@pytest.fixture
|
||||
def data_dir():
|
||||
return os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
from unittest.mock import patch, MagicMock
|
||||
from agent.api_client import APIClient
|
||||
|
||||
|
||||
def test_parse_json_valid():
|
||||
text = '{"groups": {"g1": {"records": []}}}'
|
||||
result = APIClient._parse_json(text)
|
||||
assert result is not None
|
||||
assert 'groups' in result
|
||||
|
||||
|
||||
def test_parse_json_with_markdown_wrapper():
|
||||
text = '```json\n{"key": "value"}\n```'
|
||||
result = APIClient._parse_json(text)
|
||||
assert result is not None
|
||||
assert result['key'] == 'value'
|
||||
|
||||
|
||||
def test_parse_json_with_text_before():
|
||||
text = '说明\n{"key": "value"}\n以上'
|
||||
result = APIClient._parse_json(text)
|
||||
assert result is not None
|
||||
assert result['key'] == 'value'
|
||||
|
||||
|
||||
def test_parse_json_invalid():
|
||||
text = '这不是有效的JSON。'
|
||||
result = APIClient._parse_json(text)
|
||||
assert result is None
|
||||
|
||||
|
||||
@patch('agent.api_client.requests.post')
|
||||
def test_generate_success(mock_post):
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
'choices': [{'message': {'content': '{"groups": {"g1": {"type": "json"}}}'}}]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
client = APIClient(api_key='test-key')
|
||||
result = client.generate("test prompt")
|
||||
|
||||
assert result['groups']['g1']['type'] == 'json'
|
||||
|
||||
|
||||
@patch('agent.api_client.requests.post')
|
||||
def test_generate_retry_on_json_error(mock_post):
|
||||
bad_response = MagicMock()
|
||||
bad_response.json.return_value = {
|
||||
'choices': [{'message': {'content': 'invalid response'}}]
|
||||
}
|
||||
bad_response.raise_for_status = MagicMock()
|
||||
|
||||
good_response = MagicMock()
|
||||
good_response.json.return_value = {
|
||||
'choices': [{'message': {'content': '{"result": "ok"}'}}]
|
||||
}
|
||||
good_response.raise_for_status = MagicMock()
|
||||
|
||||
mock_post.side_effect = [bad_response, good_response]
|
||||
|
||||
client = APIClient(api_key='test-key', max_retries=3)
|
||||
result = client.generate("test")
|
||||
|
||||
assert result['result'] == 'ok'
|
||||
assert mock_post.call_count == 2
|
||||
@@ -0,0 +1,87 @@
|
||||
# tests/test_copy_parser.py
|
||||
import os
|
||||
import tempfile
|
||||
from agent.input_parser import InputParser
|
||||
|
||||
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
|
||||
|
||||
def test_parse_single_copybook():
|
||||
parser = InputParser(
|
||||
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
|
||||
file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR,
|
||||
db_md_path='dummy_db.md'
|
||||
)
|
||||
fields = parser._parse_single_copybook(
|
||||
os.path.join(FIXTURE_DIR, 'ZAN01REC.cpy'),
|
||||
prefix='R01'
|
||||
)
|
||||
|
||||
assert len(fields) == 8
|
||||
|
||||
f0 = fields[0]
|
||||
assert f0.name == 'R01-APPL-ID'
|
||||
assert f0.raw_name == '(A)APPL-ID'
|
||||
assert 'X' in f0.pic_type
|
||||
assert f0.pic_bytes == 8
|
||||
|
||||
f1 = fields[1]
|
||||
assert f1.name == 'R01-EMP-ID'
|
||||
assert '9' in f1.pic_type
|
||||
assert f1.pic_bytes == 8
|
||||
|
||||
f3 = fields[3]
|
||||
assert f3.name == 'R01-START-TIME'
|
||||
assert f3.pic_bytes == 4
|
||||
|
||||
f7 = fields[7]
|
||||
assert 'FILLER' in f7.name
|
||||
assert f7.pic_bytes == 46
|
||||
|
||||
|
||||
def test_extract_copy_replacing():
|
||||
source_text = """
|
||||
FD R01INNFIL.
|
||||
01 R01INNREC.
|
||||
COPY ZAN01REC REPLACING ==(A)== BY ==R01==.
|
||||
FD R02INNFIL.
|
||||
01 R02INNREC.
|
||||
COPY ZAN04REC REPLACING ==(A)== BY ==R02==.
|
||||
"""
|
||||
|
||||
parser = InputParser(
|
||||
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
|
||||
file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR,
|
||||
db_md_path='dummy_db.md'
|
||||
)
|
||||
parser._source_text = source_text
|
||||
|
||||
result = parser._extract_copy_replacing()
|
||||
assert len(result) == 2
|
||||
assert result[0] == ('ZAN01REC', 'R01')
|
||||
assert result[1] == ('ZAN04REC', 'R02')
|
||||
|
||||
|
||||
def test_calculate_pic_bytes_x():
|
||||
assert InputParser._calculate_pic_bytes('X(008)') == 8
|
||||
assert InputParser._calculate_pic_bytes('X(10)') == 10
|
||||
|
||||
|
||||
def test_calculate_pic_bytes_9():
|
||||
assert InputParser._calculate_pic_bytes('9(008)') == 8
|
||||
assert InputParser._calculate_pic_bytes('9(004)') == 4
|
||||
|
||||
|
||||
def test_calculate_pic_bytes_comp3():
|
||||
assert InputParser._calculate_pic_bytes('S9(009) COMP-3') == 5
|
||||
assert InputParser._calculate_pic_bytes('S9(7) COMP-3') == 4
|
||||
assert InputParser._calculate_pic_bytes('9(7) COMP-3') == 4
|
||||
|
||||
|
||||
def test_calculate_pic_bytes_comp():
|
||||
assert InputParser._calculate_pic_bytes('S9(4) COMP') == 2
|
||||
assert InputParser._calculate_pic_bytes('S9(9) COMP') == 4
|
||||
|
||||
|
||||
def test_calculate_pic_bytes_decimal():
|
||||
assert InputParser._calculate_pic_bytes('S9(7)V9(2)') == 9
|
||||
@@ -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を実行
|
||||
@@ -0,0 +1,11 @@
|
||||
*
|
||||
* 加班申請レコード 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)EMP-ID PIC 9(008).
|
||||
03 (A)APPL-DATE PIC 9(008).
|
||||
03 (A)START-TIME PIC 9(004).
|
||||
03 (A)END-TIME PIC 9(004).
|
||||
03 (A)STATUS PIC X(001).
|
||||
03 (A)OVT-TYPE PIC X(001).
|
||||
03 (A)FILLER PIC X(046).
|
||||
@@ -0,0 +1,12 @@
|
||||
*
|
||||
* マッチング結果レコード 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)EMP-ID PIC 9(008).
|
||||
03 (A)APPL-DATE PIC 9(008).
|
||||
03 (A)START-TIME PIC 9(004).
|
||||
03 (A)END-TIME PIC 9(004).
|
||||
03 (A)STATUS PIC X(001).
|
||||
03 (A)OVT-TYPE PIC X(001).
|
||||
03 (A)PROC-SEQ PIC 9(002).
|
||||
03 (A)FILLER PIC X(044).
|
||||
@@ -0,0 +1,5 @@
|
||||
*
|
||||
* DBCLEAN対象レコード 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)FILLER PIC X(072).
|
||||
@@ -0,0 +1,11 @@
|
||||
*
|
||||
* 加班申请记录 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)EMP-ID PIC 9(008).
|
||||
03 (A)APPL-DATE PIC 9(008).
|
||||
03 (A)START-TIME PIC 9(004).
|
||||
03 (A)END-TIME PIC 9(004).
|
||||
03 (A)STATUS PIC X(001).
|
||||
03 (A)OVT-TYPE PIC X(001).
|
||||
03 (A)FILLER PIC X(046).
|
||||
@@ -0,0 +1,11 @@
|
||||
*
|
||||
* 加班申请记录 80B
|
||||
*
|
||||
03 (A)APPL-ID PIC X(008).
|
||||
03 (A)EMP-ID PIC 9(008).
|
||||
03 (A)APPL-DATE PIC 9(008).
|
||||
03 (A)START-TIME PIC 9(004).
|
||||
03 (A)END-TIME PIC 9(004).
|
||||
03 (A)STATUS PIC X(001).
|
||||
03 (A)OVT-TYPE PIC X(001).
|
||||
03 (A)FILLER PIC X(046).
|
||||
@@ -0,0 +1,144 @@
|
||||
# 詳細設計書
|
||||
|
||||
## 基本情報
|
||||
|
||||
| # | 項目 | 内容 |
|
||||
|---|------|------|
|
||||
| 1 | システム名 | 残業統計管理システム |
|
||||
| 2 | プログラムID | ZAN04MAT |
|
||||
| 3 | プログラム名 | 取消マッチング処理 |
|
||||
| 4 | PGMタイプ | メイン |
|
||||
| 5 | PGMパターン | マッチング(1:1) |
|
||||
| 6 | 機能概要 | OVT-SORTED(有効申請)とOVT-CSORT(取消申請)を申請番号(APPL-ID)で1:1マッチングし、結果を振り分ける。 |
|
||||
| 7 | | 申請番号一致(取消済):監査証跡としてERROR-LOGに記録 |
|
||||
| 8 | | 申請のみ(取消なし):OVT-MATCHEDに出力(処理番号=1) |
|
||||
| 9 | | 取消のみ(既DB登録済):OVT-DBCLEANに出力(DB削除用) |
|
||||
|
||||
※PGMタイプ:メイン、サブ
|
||||
※PGMパターン:マッチング(1:1、1:N、N:1、M:N)、レイアウト編集のみ(GETPUT)、振り分け(IF文、EVALUATE文)、キーブレイク(集計、集約、集計・集約の以外)、DB更新
|
||||
|
||||
### 前提条件
|
||||
|
||||
| NO | 対象ファイル | 条件 |
|
||||
|----|-------------|------|
|
||||
| 1 | ファイルR01(OVT-SORTED) | 申請番号(APPL-ID)で昇順ソート済、重複なし |
|
||||
| 2 | ファイルR02(OVT-CSORT) | 申請番号(APPL-ID)で昇順ソート済、重複なし |
|
||||
|
||||
### 使用ファイル一覧
|
||||
|
||||
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 |
|
||||
|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------|
|
||||
| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | FB | | 80 | PS | 有効申請(APPL-ID昇順) |
|
||||
| 2 | OVT-CSORT | R02 | ZAN04R02 | I | ZAN01REC | FB | | 80 | PS | 取消申請(APPL-ID昇順) |
|
||||
| 3 | OVT-MATCHED | W01 | ZAN04W01 | O | ZAN02REC | FB | | 80 | PS | マッチング結果(処理番号付) |
|
||||
| 4 | OVT-DBCLEAN | W02 | ZAN04W02 | O | ZAN04REC | FB | | 80 | PS | DB削除用(APPL-IDのみ) |
|
||||
| 5 | ERROR-LOG | W03 | ZAN04W03 | O | ZAN05REC | VB | | 200 | PS | 監査証跡(取消マッチ記録) |
|
||||
|
||||
### キー項目一覧
|
||||
|
||||
| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) |
|
||||
|----|-----------|---------------------|-------------------------------------------|
|
||||
| 1 | ファイルR01 | APPL-ID(重複NG) | APPL-ID |
|
||||
| 2 | ファイルR02 | APPL-ID(重複NG) | APPL-ID |
|
||||
|
||||
### 使用モジュール一覧
|
||||
|
||||
| NO | 機能 | プログラムID | 使用COPY名 |
|
||||
|----|------|-------------|-----------|
|
||||
| 1 | 運用日付取得SUB | SUB01DAT | ZANDATAC |
|
||||
| 2 | メッセージ編集出力SUB | SUB02MSG | ZANMSGAC |
|
||||
| 3 | ABEND処理SUB | SUB03END | ZANENDAC |
|
||||
|
||||
---
|
||||
|
||||
## 処理詳細
|
||||
|
||||
```
|
||||
1.初期処理(1000ITTSOR)
|
||||
1-1.開始メッセージ出力
|
||||
【メッセージ編集】
|
||||
メッセージ番号:1(開始メッセージ)
|
||||
1-2.コンパイル日時出力
|
||||
【メッセージ編集】
|
||||
メッセージ番号:33(コンパイル日時)
|
||||
PARM1:コンパイル日時
|
||||
PARM2:'COMPILED'
|
||||
1-3.ワークエリアの初期化
|
||||
1-4.運用日付取得SUB(SUB01DAT)により運用日を取得する。
|
||||
復帰コード≠ZEROの場合、メッセージを出力し、ABEND処理SUBを呼び出し異常終了する。
|
||||
【メッセージ編集】
|
||||
メッセージ番号:5(サブエラー)
|
||||
PARM1:'SUB01DAT'
|
||||
PARM2:復帰コード
|
||||
【ABEND処理SUB】
|
||||
ABENDコード:999
|
||||
1-5.使用ファイルのオープン
|
||||
1-6.R01を読み込む。(1100R01INNSOR)(1回目)
|
||||
1-7.R02を読み込む。(1200R02INNSOR)(1回目)
|
||||
|
||||
2.主処理(2000MAJSOR)(R01、R02を全て読み終えるまで下記を繰り返す)
|
||||
2-1.マッチの場合(R01.APPL-ID = R02.APPL-ID)
|
||||
取消済みの申請のため、OVT-MATCHEDには出力しない。
|
||||
監査証跡としてERROR-LOGにマッチ情報を出力する。(2100MATCHSOR)
|
||||
【STRING編集】
|
||||
ERR-CATEGORY = 04
|
||||
ERR-DETAIL = 'CANCEL-MATCH: ' + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻
|
||||
R01を読み込む。(1100R01INNSOR)(2件目以降)
|
||||
R02を読み込む。(1200R02INNSOR)(2件目以降)
|
||||
2-2.R01のみの場合(R01.APPL-ID < R02.APPL-ID)
|
||||
R01をOVT-MATCHEDにSTRING編集して出力する。(2200R01OUTSOR)
|
||||
【STRING編集】
|
||||
W01 = 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 + ステータス + OVT-TYPE + 処理番号(01)
|
||||
R01を読み込む。(1100R01INNSOR)(2件目以降)
|
||||
2-3.R02のみの場合(R01.APPL-ID > R02.APPL-ID)
|
||||
R02をOVT-DBCLEANに出力する。(2300R02OUTSOR)
|
||||
W02.APPL-ID = R02.APPL-ID
|
||||
R02を読み込む。(1200R02INNSOR)(2件目以降)
|
||||
|
||||
3.終了処理(3000STPSOR)
|
||||
3-1.入出力ファイルのクローズ
|
||||
3-2.入出力件数出力メッセージ出力
|
||||
【入力メッセージ編集】
|
||||
メッセージ番号:6(入力件数メッセージ)
|
||||
PARM1:当該入力ファイルのDD名
|
||||
PARM2:当該入力ファイルの件数
|
||||
【出力メッセージ編集】
|
||||
メッセージ番号:7(出力件数メッセージ)
|
||||
PARM1:当該出力ファイルのDD名
|
||||
PARM2:当該出力ファイルの件数
|
||||
3-3.終了メッセージ出力
|
||||
【メッセージ編集】
|
||||
メッセージ番号:2(終了メッセージ)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 出力レコード定義
|
||||
|
||||
### 出力ファイル1(W01/OVT-MATCHED)
|
||||
|
||||
| No | 項目名 | 設定元 | 備考 |
|
||||
|----|--------|--------|------|
|
||||
| 1 | APPL-ID | R01.APPL-ID | |
|
||||
| 2 | EMP-ID | R01.EMP-ID | |
|
||||
| 3 | APPL-DATE | R01.APPL-DATE | |
|
||||
| 4 | START-TIME | R01.START-TIME | |
|
||||
| 5 | END-TIME | R01.END-TIME | |
|
||||
| 6 | STATUS | R01.STATUS | |
|
||||
| 7 | OVT-TYPE | R01.OVT-TYPE | |
|
||||
| 8 | PROC-SEQ | 01固定 | 同一申請番号グループ内連番 |
|
||||
| 9 | FILLER | 初期値 | |
|
||||
|
||||
### 出力ファイル2(W02/OVT-DBCLEAN)
|
||||
|
||||
| No | 項目名 | 設定元 | 備考 |
|
||||
|----|--------|--------|------|
|
||||
| 1 | APPL-ID | R02.APPL-ID | DB削除対象申請番号 |
|
||||
| 2 | FILLER | 初期値 | |
|
||||
|
||||
### 出力ファイル3(W03/ERROR-LOG)
|
||||
|
||||
| No | 項目名 | 設定元 | 備考 |
|
||||
|----|--------|--------|------|
|
||||
| 1 | ERR-CATEGORY | 04で固定 | 取消マッチ監査証跡 |
|
||||
| 2 | ERR-DETAIL | STRINGで編集 | CANCEL-MATCH: + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 |
|
||||
@@ -0,0 +1,53 @@
|
||||
# tests/test_db_parser.py
|
||||
import os
|
||||
from agent.input_parser import InputParser
|
||||
from agent.models import ProgramMeta
|
||||
|
||||
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
|
||||
|
||||
def test_parse_db_definition():
|
||||
parser = InputParser(
|
||||
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
|
||||
file_db_md_path='dummy.md', cpy_dir='dummy_cpy',
|
||||
db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md')
|
||||
)
|
||||
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='テスト', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='DB更新',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='mixed',
|
||||
copy_fields={}, db_tables={}
|
||||
)
|
||||
|
||||
parser._parse_db_definition(meta)
|
||||
|
||||
assert len(meta.db_tables) > 0
|
||||
assert 'EMP_MASTER' in meta.db_tables
|
||||
|
||||
emp = meta.db_tables['EMP_MASTER']
|
||||
assert emp.db_id == 'EMP_MASTER'
|
||||
assert len(emp.columns) >= 3
|
||||
assert emp.pk_columns == ['EMP_ID']
|
||||
|
||||
|
||||
def test_parse_db_skipped_for_file_type():
|
||||
parser = InputParser(
|
||||
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
|
||||
file_db_md_path='dummy.md', cpy_dir='dummy_cpy',
|
||||
db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md')
|
||||
)
|
||||
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='テスト', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file',
|
||||
copy_fields={}, db_tables={}
|
||||
)
|
||||
|
||||
parser._parse_db_definition(meta)
|
||||
assert len(meta.db_tables) == 0
|
||||
@@ -0,0 +1,149 @@
|
||||
# tests/test_input_parser.py
|
||||
import os
|
||||
from agent.input_parser import InputParser
|
||||
|
||||
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
|
||||
|
||||
def test_parse_design_basic_info():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl',
|
||||
file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy',
|
||||
db_md_path='dummy_db.md'
|
||||
)
|
||||
parser._design_text = parser._read_file(parser.design_md_path)
|
||||
|
||||
from agent.models import ProgramMeta
|
||||
meta = ProgramMeta(program_id='', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
|
||||
parser._parse_basic_info(meta)
|
||||
assert meta.program_id == 'ZAN04MAT'
|
||||
assert meta.pgm_pattern == 'マッチング(1:1)'
|
||||
assert meta.pgm_type == 'メイン'
|
||||
assert '残業統計管理システム' in meta.system_name
|
||||
|
||||
|
||||
def test_parse_use_files():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
|
||||
)
|
||||
parser._design_text = parser._read_file(parser.design_md_path)
|
||||
|
||||
from agent.models import ProgramMeta
|
||||
meta = ProgramMeta(program_id='', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
|
||||
parser._parse_use_files(meta)
|
||||
assert len(meta.files) == 5
|
||||
r01 = [f for f in meta.files if f.identifier == 'R01'][0]
|
||||
assert r01.file_db_name == 'OVT-SORTED'
|
||||
assert r01.copy_group == 'ZAN01REC'
|
||||
assert r01.medium == 'PS'
|
||||
assert r01.io == 'I'
|
||||
|
||||
|
||||
def test_determine_input_type_file():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
|
||||
)
|
||||
|
||||
from agent.models import ProgramMeta, FileInfo
|
||||
meta = ProgramMeta(program_id='ZAN04MAT', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
meta.files = [
|
||||
FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1',
|
||||
io='I', copy_group='C1', format='FB', record_len=80,
|
||||
medium='PS', remarks=''),
|
||||
FileInfo(no=2, file_db_name='F2', identifier='W01', dd_name='DD2',
|
||||
io='O', copy_group='C2', format='FB', record_len=80,
|
||||
medium='PS', remarks=''),
|
||||
]
|
||||
parser._determine_input_type(meta)
|
||||
assert meta.input_type == 'file'
|
||||
|
||||
|
||||
def test_determine_input_type_mixed():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
|
||||
)
|
||||
|
||||
from agent.models import ProgramMeta, FileInfo
|
||||
meta = ProgramMeta(program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
meta.files = [
|
||||
FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1',
|
||||
io='I', copy_group='C1', format='FB', record_len=80,
|
||||
medium='PS', remarks=''),
|
||||
FileInfo(no=2, file_db_name='DB1', identifier='DB', dd_name='',
|
||||
io='I', copy_group='', format='', record_len=0,
|
||||
medium='DB', remarks=''),
|
||||
]
|
||||
parser._determine_input_type(meta)
|
||||
assert meta.input_type == 'mixed'
|
||||
|
||||
|
||||
def test_parse_process_detail():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
|
||||
)
|
||||
parser._design_text = parser._read_file(parser.design_md_path)
|
||||
|
||||
from agent.models import ProgramMeta
|
||||
meta = ProgramMeta(program_id='', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
|
||||
parser._parse_process_detail(meta)
|
||||
assert '1000ITTSOR' in meta.process_detail
|
||||
assert 'マッチの場合' in meta.process_detail
|
||||
|
||||
|
||||
def test_parse_output_records():
|
||||
parser = InputParser(
|
||||
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
|
||||
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
|
||||
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
|
||||
)
|
||||
parser._design_text = parser._read_file(parser.design_md_path)
|
||||
|
||||
from agent.models import ProgramMeta
|
||||
meta = ProgramMeta(program_id='', program_name='', system_name='',
|
||||
pgm_type='', pgm_pattern='',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={})
|
||||
|
||||
parser._parse_output_records(meta)
|
||||
assert 'OVT-MATCHED' in meta.output_records
|
||||
assert 'OVT-DBCLEAN' in meta.output_records
|
||||
@@ -0,0 +1,77 @@
|
||||
# tests/test_integration.py
|
||||
import os
|
||||
import tempfile
|
||||
from unittest.mock import patch, MagicMock
|
||||
from agent import generate
|
||||
|
||||
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
|
||||
|
||||
|
||||
@patch('agent.api_client.requests.post')
|
||||
def test_full_pipeline_mock_api(mock_post):
|
||||
"""完整流水线测试,API 调用使用 mock。"""
|
||||
mock_response = MagicMock()
|
||||
mock_response.json.return_value = {
|
||||
'choices': [{'message': {'content': '''{
|
||||
"groups": {
|
||||
"g1": {
|
||||
"program": "ZAN04MAT",
|
||||
"records": [
|
||||
{
|
||||
"input": {
|
||||
"R01INNFIL": {
|
||||
"R01-APPL-ID": "A0000001",
|
||||
"R01-EMP-ID": "00000101",
|
||||
"R01-APPL-DATE": "20260101",
|
||||
"R01-START-TIME": "0900",
|
||||
"R01-END-TIME": "1800",
|
||||
"R01-STATUS": "0",
|
||||
"R01-OVT-TYPE": "W",
|
||||
"R01-FILLER": "D000000000000000000000000000000000000000000001"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}'''}}]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
mock_post.return_value = mock_response
|
||||
|
||||
cpy_dir = FIXTURE_DIR
|
||||
output_dir = tempfile.mkdtemp()
|
||||
rules_dir = os.path.join(os.path.dirname(__file__), '..', 'rules')
|
||||
|
||||
design_md = os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md')
|
||||
source_cbl = os.path.join(FIXTURE_DIR, '..', '..', '..',
|
||||
'cobol-tna-system', 'src', 'ZAN04MAT.cbl')
|
||||
|
||||
# Fallback if real source not found
|
||||
if not os.path.exists(source_cbl):
|
||||
source_cbl = os.path.join(output_dir, 'dummy.cbl')
|
||||
with open(source_cbl, 'w', encoding='utf-8') as f:
|
||||
f.write("COPY ZAN01REC REPLACING ==(A)== BY ==R01==.\n")
|
||||
|
||||
try:
|
||||
result = generate(
|
||||
design_md=design_md,
|
||||
source_cbl=source_cbl,
|
||||
file_db_md='dummy.md',
|
||||
cpy_dir=cpy_dir,
|
||||
db_md='dummy_db.md',
|
||||
output_dir=output_dir,
|
||||
api_key='test-key',
|
||||
rules_dir=rules_dir,
|
||||
)
|
||||
|
||||
assert result['program_id'] == 'ZAN04MAT'
|
||||
assert result['groups'] > 0
|
||||
assert len(result['output_files']) > 0
|
||||
|
||||
for path in result['output_files'].values():
|
||||
assert os.path.exists(path), f"输出文件不存在: {path}"
|
||||
|
||||
finally:
|
||||
if source_cbl.endswith('dummy.cbl') and os.path.exists(source_cbl):
|
||||
os.remove(source_cbl)
|
||||
@@ -0,0 +1,65 @@
|
||||
# tests/test_markdown_utils.py
|
||||
from agent.markdown_utils import extract_section, parse_table_rows, parse_table_from_section, find_row_by_key
|
||||
|
||||
SAMPLE_MD = """
|
||||
## 基本情報
|
||||
|
||||
| # | 項目 | 内容 |
|
||||
|---|------|------|
|
||||
| 1 | システム名 | 残業統計管理システム |
|
||||
| 4 | PGMパターン | マッチング(1:1) |
|
||||
| 5 | 機能概要 | 取消マッチング処理 |
|
||||
|
||||
## 使用ファイル一覧
|
||||
|
||||
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 媒体 | 備考 |
|
||||
|----|------------------|--------|------|-----|--------|------|------|
|
||||
| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | PS | |
|
||||
| 2 | ERROR-LOG | W01 | ZAN04W01 | O | ZAN05REC | PS | |
|
||||
|
||||
## 出力レコード定義
|
||||
|
||||
### 出力ファイル1(W01/OVT-MATCHED)
|
||||
|
||||
| No | 項目名 | 設定元 | 備考 |
|
||||
|----|--------|--------|------|
|
||||
| 1 | APPL-ID | R01.APPL-ID | |
|
||||
| 2 | EMP-ID | R01.EMP-ID | |
|
||||
"""
|
||||
|
||||
|
||||
def test_extract_section():
|
||||
result = extract_section(SAMPLE_MD, "基本情報")
|
||||
assert "残業統計管理システム" in result
|
||||
assert "使用ファイル一覧" not in result
|
||||
|
||||
|
||||
def test_extract_section_not_found():
|
||||
result = extract_section(SAMPLE_MD, "存在しないセクション")
|
||||
assert result == ''
|
||||
|
||||
|
||||
def test_parse_table_rows():
|
||||
rows = parse_table_from_section(SAMPLE_MD, "基本情報")
|
||||
assert len(rows) >= 3
|
||||
assert rows[0]['項目'] == 'システム名'
|
||||
|
||||
|
||||
def test_find_row_by_key():
|
||||
rows = parse_table_from_section(SAMPLE_MD, "基本情報")
|
||||
row = find_row_by_key(rows, '項目', 'PGMパターン')
|
||||
assert row is not None
|
||||
assert row['内容'] == 'マッチング(1:1)'
|
||||
|
||||
|
||||
def test_parse_use_file_table():
|
||||
rows = parse_table_from_section(SAMPLE_MD, "使用ファイル一覧")
|
||||
assert len(rows) == 2
|
||||
assert rows[0]['識別子'] == 'R01'
|
||||
assert rows[0]['DD名'] == 'ZAN04R01'
|
||||
|
||||
|
||||
def test_extract_output_records_section():
|
||||
result = extract_section(SAMPLE_MD, "出力レコード定義")
|
||||
assert "出力ファイル1" in result
|
||||
assert "W01/OVT-MATCHED" in result
|
||||
@@ -0,0 +1,59 @@
|
||||
from agent.models import FileInfo, CopyField, ProgramMeta, KeyInfo, ModuleInfo, TableColumn, TableInfo
|
||||
|
||||
|
||||
def test_file_info():
|
||||
f = FileInfo(
|
||||
no=1, file_db_name="OVT-SORTED", identifier="R01",
|
||||
dd_name="ZAN04R01", io="I", copy_group="ZAN01REC",
|
||||
format="FB", record_len=80, medium="PS", remarks="有效申请"
|
||||
)
|
||||
assert f.identifier == "R01"
|
||||
assert f.medium == "PS"
|
||||
|
||||
|
||||
def test_copy_field():
|
||||
cf = CopyField(level=3, name="R01-APPL-ID", raw_name="(A)APPL-ID",
|
||||
pic_type="X(008)", pic_bytes=8)
|
||||
assert cf.name == "R01-APPL-ID"
|
||||
assert cf.pic_bytes == 8
|
||||
|
||||
|
||||
def test_program_meta_defaults():
|
||||
meta = ProgramMeta(
|
||||
program_id="TEST", program_name="", system_name="",
|
||||
pgm_type="", pgm_pattern="",
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail="", output_records="",
|
||||
input_type="file",
|
||||
copy_fields={}, db_tables={}
|
||||
)
|
||||
assert meta.program_id == "TEST"
|
||||
assert meta.input_type == "file"
|
||||
|
||||
|
||||
def test_table_info():
|
||||
col = TableColumn(no=1, name_jp="社員番号", name_en="EMP_ID",
|
||||
type="CHAR", max_len="8", decimal_digits="",
|
||||
byte_count="8", nullable=False, is_pk=True)
|
||||
table = TableInfo(table_name="EMP_MASTER", db_id="EMP_MASTER",
|
||||
copy_id="", columns=[col], pk_columns=["EMP_ID"])
|
||||
assert table.pk_columns == ["EMP_ID"]
|
||||
assert len(table.columns) == 1
|
||||
|
||||
|
||||
def test_key_info():
|
||||
k = KeyInfo(no=1, file_name="ファイルR01",
|
||||
sort_condition="A001>A002>A003",
|
||||
key_condition="A001>A002>A003")
|
||||
assert k.no == 1
|
||||
assert k.file_name == "ファイルR01"
|
||||
assert "A001" in k.sort_condition
|
||||
|
||||
|
||||
def test_module_info():
|
||||
m = ModuleInfo(no=1, function="メッセージ編集出力SUB",
|
||||
program_id="SUB02MSG", copy_name="ZANMSGAC")
|
||||
assert m.function == "メッセージ編集出力SUB"
|
||||
assert m.program_id == "SUB02MSG"
|
||||
assert m.copy_name == "ZANMSGAC"
|
||||
@@ -0,0 +1,117 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from agent.output_writer import OutputWriter
|
||||
|
||||
|
||||
def test_write_json_files():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
writer = OutputWriter(tmpdir)
|
||||
|
||||
ai_result = {
|
||||
"groups": {
|
||||
"g1": {
|
||||
"program": "TEST",
|
||||
"records": [
|
||||
{"input": {"R01": {"R01-FIELD": "A001"}}}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
written = writer.write("TESTPGM", ai_result, input_type="file")
|
||||
|
||||
assert len(written) == 1
|
||||
json_path = written["g1/json"]
|
||||
assert os.path.exists(json_path)
|
||||
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
content = json.load(f)
|
||||
assert content['program'] == 'TEST'
|
||||
assert len(content['records']) == 1
|
||||
|
||||
|
||||
def test_write_mixed_type():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
writer = OutputWriter(tmpdir)
|
||||
|
||||
ai_result = {
|
||||
"groups": {
|
||||
"g1": {
|
||||
"program": "MIXED",
|
||||
"records": [{"input": {"R01": {"F1": "X"}}}],
|
||||
"sql": "INSERT INTO T VALUES ('x');"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
written = writer.write("MIXEDPGM", ai_result, input_type="mixed")
|
||||
|
||||
assert len(written) == 2
|
||||
assert os.path.exists(written["g1/json"])
|
||||
assert os.path.exists(written["g1/sql"])
|
||||
|
||||
|
||||
def test_write_multiple_groups():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
writer = OutputWriter(tmpdir)
|
||||
|
||||
ai_result = {
|
||||
"groups": {
|
||||
"g1": {"records": [{}]},
|
||||
"g2": {"records": [{}]},
|
||||
"g3": {"records": [{}]},
|
||||
}
|
||||
}
|
||||
|
||||
written = writer.write("MULTI", ai_result, input_type="file")
|
||||
|
||||
assert len(written) == 3
|
||||
for g in ['g1', 'g2', 'g3']:
|
||||
assert f'{g}/json' in written
|
||||
assert os.path.exists(written[f'{g}/json'])
|
||||
|
||||
|
||||
def test_directory_structure():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
writer = OutputWriter(tmpdir)
|
||||
ai_result = {"groups": {"g1": {"records": [{}]}}}
|
||||
writer.write("MYPROG", ai_result, input_type="file")
|
||||
|
||||
expected_dir = os.path.join(tmpdir, "MYPROG", "black_box", "g1")
|
||||
assert os.path.isdir(expected_dir)
|
||||
assert os.path.isfile(os.path.join(expected_dir, "MYPROG_g1.json"))
|
||||
|
||||
|
||||
def test_write_case_table_md():
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
writer = OutputWriter(tmpdir)
|
||||
|
||||
ai_result = {
|
||||
"groups": {
|
||||
"g1": {"program": "TEST", "records": [{}]},
|
||||
"g2": {"program": "TEST", "records": [{}]},
|
||||
},
|
||||
"summary_md": (
|
||||
"| 组 | 规则md中的定义/用途 | 本次是否生成 |\n"
|
||||
"|----|--------------------|--------------|\n"
|
||||
"| g1 | 完全匹配 | 已生成 |\n"
|
||||
"| g2 | 键值乱序 | 已生成 |\n"
|
||||
"| g3 | 主键重复 | ✗ |\n"
|
||||
),
|
||||
}
|
||||
|
||||
written = writer.write("TESTPGM", ai_result, input_type="file")
|
||||
|
||||
md_path = written["case表"]
|
||||
assert os.path.exists(md_path)
|
||||
assert os.path.basename(md_path) == 'case表.md'
|
||||
|
||||
assert os.path.dirname(md_path).endswith(
|
||||
os.path.join('TESTPGM', 'black_box')
|
||||
)
|
||||
|
||||
with open(md_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
assert 'g3' in content
|
||||
assert '✗' in content
|
||||
@@ -0,0 +1,98 @@
|
||||
from agent.prompt_builder import PromptBuilder
|
||||
from agent.models import ProgramMeta, FileInfo, CopyField
|
||||
|
||||
|
||||
def test_build_basic_prompt():
|
||||
meta = ProgramMeta(
|
||||
program_id='ZAN04MAT', program_name='取消マッチング処理',
|
||||
system_name='残業統計管理システム',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
|
||||
summary_lines=['取消申請のマッチング'], prerequisites=[],
|
||||
files=[], keys=[], modules=[],
|
||||
process_detail='1.初期処理...\n2.主処理...',
|
||||
output_records='### 出力ファイル1...',
|
||||
input_type='file', copy_fields={}, db_tables={}
|
||||
)
|
||||
|
||||
builder = PromptBuilder()
|
||||
prompt = builder.build(
|
||||
meta=meta,
|
||||
rules_text='# マッチング(1:1) データ生成規則\n...',
|
||||
group_descriptions=['两端不一致', '逆方向两端不一致', '中间不一致'],
|
||||
group_count=3
|
||||
)
|
||||
|
||||
assert 'ZAN04MAT' in prompt
|
||||
assert 'マッチング(1:1)' in prompt
|
||||
assert '1.初期処理' in prompt
|
||||
assert '生成するグループ数: 3' in prompt
|
||||
assert 'g1: 两端不一致' in prompt
|
||||
assert 'g2: 逆方向两端不一致' in prompt
|
||||
assert 'g3: 中间不一致' in prompt
|
||||
assert 'JSON形式' in prompt
|
||||
assert 'summary_md' in prompt
|
||||
assert 'case表' in prompt
|
||||
|
||||
|
||||
def test_build_with_copy_fields():
|
||||
fields = {
|
||||
'R01': [
|
||||
CopyField(level=3, name='R01-APPL-ID', raw_name='(A)APPL-ID',
|
||||
pic_type='X(008)', pic_bytes=8),
|
||||
CopyField(level=3, name='R01-EMP-ID', raw_name='(A)EMP-ID',
|
||||
pic_type='9(008)', pic_bytes=8),
|
||||
]
|
||||
}
|
||||
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[
|
||||
FileInfo(no=1, file_db_name='INPUT-FILE', identifier='R01',
|
||||
dd_name='TESTR01', io='I', copy_group='TESTREC',
|
||||
format='FB', record_len=80, medium='PS', remarks='')
|
||||
],
|
||||
keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields=fields, db_tables={}
|
||||
)
|
||||
|
||||
builder = PromptBuilder()
|
||||
prompt = builder.build(
|
||||
meta=meta,
|
||||
rules_text='# 規則',
|
||||
group_descriptions=['テスト'],
|
||||
group_count=1
|
||||
)
|
||||
|
||||
assert 'R01-APPL-ID' in prompt
|
||||
assert 'X(008)' in prompt
|
||||
assert 'R01-EMP-ID' in prompt
|
||||
|
||||
|
||||
def test_build_db_prompt():
|
||||
meta = ProgramMeta(
|
||||
program_id='TESTDB', program_name='DB更新', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='DB更新',
|
||||
summary_lines=[], prerequisites=[],
|
||||
files=[
|
||||
FileInfo(no=1, file_db_name='DB-TABLE', identifier='DB',
|
||||
dd_name='', io='I', copy_group='',
|
||||
format='', record_len=0, medium='DB', remarks='')
|
||||
],
|
||||
keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='db', copy_fields={}, db_tables={}
|
||||
)
|
||||
|
||||
builder = PromptBuilder()
|
||||
prompt = builder.build(
|
||||
meta=meta,
|
||||
rules_text='# DB更新規則',
|
||||
group_descriptions=['テスト'],
|
||||
group_count=1
|
||||
)
|
||||
|
||||
assert 'DB' in prompt or 'SQL' in prompt
|
||||
assert 'INSERT' in prompt
|
||||
@@ -0,0 +1,278 @@
|
||||
import os
|
||||
import pytest
|
||||
from agent.rule_loader import RuleLoader, PGM_PATTERN_MAP
|
||||
from agent.models import ProgramMeta
|
||||
|
||||
RULES_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules')
|
||||
|
||||
ALL_35_TYPES = [
|
||||
'マッチング(1:1)',
|
||||
'マッチング(1:N)',
|
||||
'マッチング(N:1)',
|
||||
'レイアウト編集のみ(GETPUT)',
|
||||
'振り分け(IF文)',
|
||||
'振り分け(EVALUATE文)',
|
||||
'キーブレイク(集計)',
|
||||
'キーブレイク(集約)',
|
||||
'DB更新',
|
||||
'50分割',
|
||||
'25分割',
|
||||
'100分割',
|
||||
'項目チェック(重複含まず)',
|
||||
'オンラインPGM',
|
||||
'CSV→FB変換(改行なし)',
|
||||
'2段階マッチング(1:1⇒1:1)',
|
||||
'2段階マッチング(N:1⇒N:1)',
|
||||
'マッチングM:N⇒出力M件',
|
||||
'マッチングM:N⇒出力N件',
|
||||
'マッチングM:N⇒出力M×N件',
|
||||
'CSV→FB変換(改行あり)',
|
||||
'2段階マッチング(M:N⇒M:N)',
|
||||
'SELECT条件',
|
||||
'内部テーブル検索',
|
||||
'サブプログラム使用',
|
||||
'DB検索',
|
||||
'項目チェック(半角20桁/4桁)',
|
||||
'SYSIN読込',
|
||||
'ASCII→EBCDIC変換',
|
||||
'キーブレイク(集計集約以外)',
|
||||
'項目チェック(重複含む)',
|
||||
'1:N+キーブレイク(同キー)',
|
||||
'1:N+キーブレイク(異キー)',
|
||||
'SORT(INPUT/OUTPUT PROCEDURE)',
|
||||
'MERGE(複数ファイル結合)',
|
||||
]
|
||||
|
||||
DETAILED_DESIGN_PATTERNS = [
|
||||
'29(ASCII→EBCDIC変換)',
|
||||
'18(M:N→M件マッチング)',
|
||||
'20(M:N→M×N件直積出力)',
|
||||
'マッチング(1:1→1:1 2段階)',
|
||||
'マッチング(N:1→N:1 2段階)',
|
||||
'マッチング(M:N→M:N 2段階)',
|
||||
'33(1:N+異キーキーブレイク)',
|
||||
'34(SORT INPUT/OUTPUT PROCEDURE)',
|
||||
'11(25分割)',
|
||||
'12(100分割)',
|
||||
'27(半角20桁/4桁チェック)',
|
||||
'19(M:N→N件マッチング+集計)',
|
||||
'DB更新 + SYSIN読込(P28)',
|
||||
'編集出力(ランキング生成)',
|
||||
'32(1:N+同キーキーブレイク)',
|
||||
'編集出力(印刷制御)',
|
||||
]
|
||||
|
||||
|
||||
def test_pgm_pattern_map_has_known_patterns():
|
||||
assert 'マッチング(1:1)' in PGM_PATTERN_MAP
|
||||
assert 'マッチング(1:N)' in PGM_PATTERN_MAP
|
||||
assert 'DB更新' in PGM_PATTERN_MAP
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern', ALL_35_TYPES)
|
||||
def test_all_classification_types_loadable(pattern):
|
||||
"""程序分类说明.md の35类型すべてが規則に解決できること。"""
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
assert loader._load_pgm_pattern_rule(pattern) is not None, f'未実装: {pattern}'
|
||||
|
||||
|
||||
@pytest.mark.parametrize('pattern', DETAILED_DESIGN_PATTERNS)
|
||||
def test_detailed_design_pattern_names_loadable(pattern):
|
||||
"""詳細設計書の PGMパターン/プログラムタイプ 表記が規則に解決できること。"""
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
assert loader._load_pgm_pattern_rule(pattern) is not None, f'マッピング不足: {pattern}'
|
||||
|
||||
|
||||
def test_non_aggregate_keybreak_uses_dedicated_rule():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('キーブレイク(集計集約以外)')
|
||||
assert rule is not None
|
||||
assert '非集計' in rule
|
||||
|
||||
|
||||
def test_two_stage_matching_split_by_form():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
r1 = loader._load_pgm_pattern_rule('2段階マッチング(1:1⇒1:1)')
|
||||
r2 = loader._load_pgm_pattern_rule('2段階マッチング(N:1⇒N:1)')
|
||||
r3 = loader._load_pgm_pattern_rule('マッチング(M:N→M:N 2段階)')
|
||||
assert r1 is not None and r2 is not None and r3 is not None
|
||||
assert '1:1⇒1:1' in r1
|
||||
assert 'N:1⇒N:1' in r2
|
||||
assert 'M:N⇒M:N' in r3
|
||||
assert r1 != r2 and r2 != r3 and r1 != r3
|
||||
|
||||
|
||||
def test_load_matching_1_1_rule():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={}
|
||||
)
|
||||
combined, descriptions, count = loader.load(meta)
|
||||
|
||||
assert count > 0
|
||||
assert len(combined) > 0
|
||||
assert 'マッチング(1:1)' in combined
|
||||
assert '数据生成' in combined
|
||||
|
||||
|
||||
def test_load_matching_1_n_rule():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:N)',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={}
|
||||
)
|
||||
combined, descriptions, count = loader.load(meta)
|
||||
|
||||
assert count > 0
|
||||
assert len(combined) > 0
|
||||
assert 'マッチング(1:N)' in combined
|
||||
|
||||
|
||||
def test_matching_n_1_has_only_n_1_imbalance():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('マッチング(N:1)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 9
|
||||
assert 'N:1匹配(MT-N003)' in rule or 'N:1匹配(MT-N003)' in rule
|
||||
assert '极端不平衡 N:1' in rule
|
||||
assert '极端不平衡 1:N' not in rule
|
||||
|
||||
|
||||
def test_if_branching_rule_covers_if_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('振り分け(IF文)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 5
|
||||
for marker in ('B-N001', 'B-N005', 'IF 2路分支'):
|
||||
assert marker in rule
|
||||
assert 'B-N006' not in rule
|
||||
assert 'B-N010' not in rule
|
||||
assert 'B-R001' not in rule
|
||||
|
||||
|
||||
def test_evaluate_branching_rule_covers_evaluate_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('振り分け(EVALUATE文)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 5
|
||||
for marker in ('B-N006', 'B-N009', 'B-A002', 'EVALUATE WHEN 多值'):
|
||||
assert marker in rule
|
||||
assert 'B-N001' not in rule
|
||||
assert 'B-N010' not in rule
|
||||
assert 'B-R001' not in rule
|
||||
|
||||
|
||||
def test_keybreak_summary_rule_covers_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('キーブレイク(集計)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 7
|
||||
for marker in ('KB-N001', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'):
|
||||
assert marker in rule
|
||||
assert 'KB-N002' not in rule
|
||||
|
||||
|
||||
def test_keybreak_aggregation_rule_covers_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('キーブレイク(集約)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 7
|
||||
for marker in ('KB-N002', 'KB-N004', 'KB-N005', 'KB-N006', 'KB-A001', 'KB-A002', 'KB-R001'):
|
||||
assert marker in rule
|
||||
assert 'KB-N001' not in rule
|
||||
|
||||
|
||||
def test_combined_keybreak_rule_removed_and_remapped():
|
||||
combined = os.path.join(RULES_DIR, 'pgm_pattern', 'キーブレイク(集計、集約).md')
|
||||
assert not os.path.exists(combined)
|
||||
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
summary = loader._load_pgm_pattern_rule('キーブレイク(集計)')
|
||||
for pattern in ('キーブレイク(集計、集約)', '1:N+キーブレイク(同キー)',
|
||||
'1:Nキーブレイク(同キー集約)', '32(1:N+同キーキーブレイク)'):
|
||||
rule = loader._load_pgm_pattern_rule(pattern)
|
||||
assert rule is not None
|
||||
assert rule == summary
|
||||
|
||||
|
||||
def test_item_check_no_dup_rule_covers_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('項目チェック(重複含まず)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 5
|
||||
for marker in ('VF-N001', 'VF-N002', 'VF-N003', 'VF-N004', 'VF-R001'):
|
||||
assert marker in rule
|
||||
assert 'VF-N005' not in rule
|
||||
assert not os.path.exists(os.path.join(RULES_DIR, 'pgm_pattern', '項目チェック.md'))
|
||||
generic = loader._load_pgm_pattern_rule('項目チェック')
|
||||
assert generic == rule
|
||||
|
||||
|
||||
def test_item_check_halfwidth_rule_covers_benchmark():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
rule = loader._load_pgm_pattern_rule('項目チェック(半角20桁/4桁)')
|
||||
assert rule is not None
|
||||
_, count = loader._parse_group_info(rule)
|
||||
assert count == 4
|
||||
for marker in ('VF-N005', 'VF-N006', 'VF-A001', 'VF-A002'):
|
||||
assert marker in rule
|
||||
assert 'VF-N001' not in rule
|
||||
|
||||
|
||||
def test_load_nonexistent_pattern_raises():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='存在しないパターン',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='', output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={}
|
||||
)
|
||||
try:
|
||||
loader.load(meta)
|
||||
assert False, 'Should have raised FileNotFoundError'
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def test_detect_conditional_branch():
|
||||
process = """
|
||||
2-1-2.ロジック分岐判定(EVALUATE)
|
||||
2-1-2-1.STATUS='1'の場合
|
||||
INSERT処理
|
||||
"""
|
||||
assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True
|
||||
|
||||
|
||||
def test_no_keyword_match():
|
||||
process = "1.初期処理\n2.主処理\n3.終了処理"
|
||||
assert RuleLoader._detect_feature(process, ['EVALUATE']) is False
|
||||
|
||||
|
||||
def test_rule_with_conditional_feature():
|
||||
loader = RuleLoader(RULES_DIR)
|
||||
meta = ProgramMeta(
|
||||
program_id='TEST', program_name='', system_name='',
|
||||
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
|
||||
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
|
||||
process_detail='2-1-2.EVALUATEで条件分岐する。',
|
||||
output_records='',
|
||||
input_type='file', copy_fields={}, db_tables={}
|
||||
)
|
||||
combined, descriptions, count = loader.load(meta)
|
||||
|
||||
assert count > 0
|
||||
assert '条件分支' in combined
|
||||
Reference in New Issue
Block a user