170 lines
7.4 KiB
Python
170 lines
7.4 KiB
Python
"""KIN08DBU DB 管道修复测试。
|
||
|
||
覆盖:
|
||
- Fix A: _scan_assign_to 引号 ASSIGN 捕获(阻断运行回归)
|
||
- Fix B: _inject_sql_error_rows 目标实际 INSERT 记录(9100DBERRSOR 可达)
|
||
- Fix D: 聚合边界数据注入(overflow / agg table full)
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import tempfile
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
|
||
import pytest
|
||
|
||
from orchestrator_db import GixsqlOrchestrator
|
||
|
||
|
||
def _make_orch(src_text: str):
|
||
orch = object.__new__(GixsqlOrchestrator)
|
||
tmp = Path(tempfile.mkdtemp())
|
||
orch.src_path = tmp / "prog.cbl"
|
||
orch.src_path.write_text(src_text, encoding="utf-8-sig")
|
||
orch.work_dir = tmp / "work"
|
||
return orch, tmp
|
||
|
||
|
||
KIN08_SRC = """ IDENTIFICATION DIVISION.
|
||
PROGRAM-ID. KIN08DBU.
|
||
ENVIRONMENT DIVISION.
|
||
INPUT-OUTPUT SECTION.
|
||
FILE-CONTROL.
|
||
SELECT R01INNFIL ASSIGN TO "KIN08R01".
|
||
SELECT SYSINFILE ASSIGN TO "KIN08S01".
|
||
SELECT W01OUTFIL ASSIGN TO "KIN08W01".
|
||
DATA DIVISION.
|
||
FILE SECTION.
|
||
FD R01INNFIL.
|
||
01 R01INNREC.
|
||
PROCEDURE DIVISION.
|
||
OPEN INPUT R01INNFIL.
|
||
OPEN INPUT SYSINFILE.
|
||
OPEN OUTPUT W01OUTFIL.
|
||
"""
|
||
|
||
|
||
def test_scan_assign_to_does_not_capture_quote():
|
||
"""带引号 ASSIGN TO 应返回不带引号的文件名(Fix A 回归)。"""
|
||
orch, _ = _make_orch(KIN08_SRC)
|
||
assign_map = orch._scan_assign_to()
|
||
assert assign_map.get('KIN08S01') == 'INPUT', f'KIN08S01 应为 INPUT, 实际: {assign_map}'
|
||
assert assign_map.get('KIN08R01') == 'INPUT'
|
||
assert assign_map.get('KIN08W01') == 'OUTPUT'
|
||
# 禁止出现带前导引号的键
|
||
assert not any(k.startswith('"') for k in assign_map), f'键不应含引号: {assign_map}'
|
||
|
||
|
||
def test_make_synthetic_error_rows_skips_blanked_first_record():
|
||
"""合成错误行应跳过 records[0](其 EMP-ID 会被清空、运行时不会 INSERT)。"""
|
||
orch, _ = _make_orch(KIN08_SRC)
|
||
table = type('T', (), {
|
||
'name': 'DAILY_RECORDS',
|
||
'columns': [
|
||
type('C', (), {'name': 'EMP_ID', 'primary_key': True}),
|
||
type('C', (), {'name': 'TARGET_DATE', 'primary_key': True}),
|
||
type('C', (), {'name': 'TIME_IN', 'primary_key': False}),
|
||
],
|
||
})()
|
||
records = [
|
||
{'R01EMP-ID': ' ' * 8, 'R01DATE': '20000101', 'R01TIME-IN': '0900', 'HV-EMP-ID': ' ' * 8, 'HV-TARGET-DATE': '20000101'},
|
||
{'R01EMP-ID': '00000102', 'R01DATE': '20000102', 'R01TIME-IN': '0900', 'HV-EMP-ID': '00000102', 'HV-TARGET-DATE': '20000102'},
|
||
{'R01EMP-ID': '00000103', 'R01DATE': '20000103', 'R01TIME-IN': '0900', 'HV-EMP-ID': '00000103', 'HV-TARGET-DATE': '20000103'},
|
||
]
|
||
rows = orch._make_synthetic_error_rows(table, records)
|
||
assert rows, '应生成合成错误行'
|
||
# 第一行不应是 records[0](空 EMP-ID 记录)
|
||
assert rows[0][0].strip() != '', f'首行 EMP_ID 不应为空白: {rows[0]}'
|
||
assert rows[0][0] == '00000102', f'首行应为 records[1] 的 EMP_ID: {rows[0]}'
|
||
|
||
|
||
def test_inject_sql_error_rows_uses_per_row_values():
|
||
"""inject 循环内应逐行使用 row 值(而非固定 rows[0])。"""
|
||
orch, tmp = _make_orch(KIN08_SRC)
|
||
table = type('T', (), {
|
||
'name': 'DAILY_RECORDS',
|
||
'columns': [
|
||
type('C', (), {'name': 'EMP_ID', 'primary_key': True}),
|
||
type('C', (), {'name': 'TARGET_DATE', 'primary_key': True}),
|
||
type('C', (), {'name': 'TIME_IN', 'primary_key': False}),
|
||
],
|
||
})()
|
||
orch.schema = type('S', (), {'db_tables': [table]})()
|
||
db = tmp / 'kin.db'
|
||
|
||
import sqlite3
|
||
conn = sqlite3.connect(str(db))
|
||
conn.execute('CREATE TABLE [DAILY_RECORDS] ([EMP_ID] CHAR(8), [TARGET_DATE] CHAR(8), [TIME_IN] CHAR(4), PRIMARY KEY ([EMP_ID], [TARGET_DATE]))')
|
||
conn.commit()
|
||
conn.close()
|
||
|
||
records = [
|
||
{'R01EMP-ID': '00000101', 'R01DATE': '20000101', 'R01TIME-IN': '0900', 'HV-EMP-ID': '00000101', 'HV-TARGET-DATE': '20000101'},
|
||
{'R01EMP-ID': '00000102', 'R01DATE': '20000102', 'R01TIME-IN': '0900', 'HV-EMP-ID': '00000102', 'HV-TARGET-DATE': '20000102'},
|
||
{'R01EMP-ID': '00000103', 'R01DATE': '20000103', 'R01TIME-IN': '0900', 'HV-EMP-ID': '00000103', 'HV-TARGET-DATE': '20000103'},
|
||
]
|
||
orch._inject_sql_error_rows(db, records)
|
||
|
||
conn = sqlite3.connect(str(db))
|
||
rows = conn.execute('SELECT EMP_ID FROM [DAILY_RECORDS]').fetchall()
|
||
conn.close()
|
||
emp_ids = {r[0] for r in rows}
|
||
# 至少注入 1 条与 records[1] 匹配的 PK(运行时将被 INSERT → 冲突)
|
||
assert '00000102' in emp_ids, f'应注入 records[1] 的 PK, 实际: {emp_ids}'
|
||
|
||
|
||
def test_format_value_numeric_field_non_numeric_writes_spaces():
|
||
"""数字字段收到非数字值不应被静默转成 0(会造成 '00000000' PK 冲突),应写 SPACE。"""
|
||
from cobol_testgen.flatfile import _format_value
|
||
field = {'name': 'R01EMP-ID', 'type': 'numeric', 'length': 8,
|
||
'pic': '9(008)', 'usage': None,
|
||
'pic_info': {'type': 'numeric', 'digits': 8, 'decimal': 0,
|
||
'length': 8, 'signed': False}}
|
||
# 非数字值 → SPACE(程序空社員チェックでスキップ)
|
||
assert _format_value('U0000031', field) == b' ' * 8, \
|
||
f'非数字 EMP 应写 SPACE, 实际 {_format_value("U0000031", field)!r}'
|
||
# 数字值 → 正常 zfill
|
||
assert _format_value('102', field) == b'00000102'
|
||
# 全ゼロ → 保持(由 sanitize 负责 blank)
|
||
assert _format_value('00000000', field) == b'00000000'
|
||
|
||
|
||
def test_inject_aggregation_boundaries():
|
||
"""聚合边界注入:overflow 同键大小时数 + agg-full 超 100 组合(数字 EMP 字段)。"""
|
||
orch, tmp = _make_orch(KIN08_SRC)
|
||
recs = []
|
||
for i in range(60):
|
||
recs.append({
|
||
'R01EMP-ID': f'00000{i + 100}', 'R01DATE': f'{20000101 + i:08d}',
|
||
'R01TIME-IN': '0900', 'R01TIME-OUT': '1800',
|
||
'R01ANNUAL-H': '00010', 'R01PERSONAL-H': '00000',
|
||
'R01OFFICIAL-H': '00000', 'R01SICK-H': '00000', 'R01ABSENT-H': '00000',
|
||
'R01FILLER': ' ' * 151,
|
||
})
|
||
# 字母型 EMP(数字字段的非法值)不应被选为 dup_eid
|
||
recs.append({'R01EMP-ID': 'U0000031', 'R01DATE': '20001231',
|
||
'R01TIME-IN': '0900', 'R01TIME-OUT': '1800',
|
||
'R01ANNUAL-H': '00010', 'R01PERSONAL-H': '00000',
|
||
'R01OFFICIAL-H': '00000', 'R01SICK-H': '00000', 'R01ABSENT-H': '00000',
|
||
'R01FILLER': ' ' * 151})
|
||
orch._inject_aggregation_boundaries(recs)
|
||
# overflow: 至少 2 条同 (EMP, 月) 且 ANNUAL-H 为最大值
|
||
pairs = {}
|
||
for r in recs:
|
||
if r.get('R01ANNUAL-H'):
|
||
ym = r['R01DATE'][:6]
|
||
pairs.setdefault((r['R01EMP-ID'], ym), []).append(r['R01ANNUAL-H'])
|
||
max_seen = max('9' * len(v[0]) for v in pairs.values() if v)
|
||
over = [(k, v) for k, v in pairs.items() if len(v) >= 2 and all(h == max_seen for h in v)]
|
||
assert over, f'应存在同键 overflow 组, pairs={pairs}'
|
||
# 追加记录的 EMP 必须为数字(否则 9(008) 字段写文件会变 SPACE)
|
||
for r in recs[60:]:
|
||
assert str(r['R01EMP-ID']).isdigit(), f'追加记录 EMP 应为数字: {r["R01EMP-ID"]!r}'
|
||
# agg-full: 不同 (EMP, 月) 组合 >= 101
|
||
distinct = set()
|
||
for r in recs:
|
||
distinct.add((r['R01EMP-ID'], r['R01DATE'][:6]))
|
||
assert len(distinct) >= 101, f'应有 >=101 个聚合键, 实际 {len(distinct)}'
|