"""KIN07DAI 型多文件照合程序的前缀检测与前置序列构造测试。 复现根因:_coordinate_multi_file_keys 从 01 记录名推导前缀(R01INNREC→'R01'), 但 COPY REPLACING 把字段前缀改为 SW/SR/SL,导致键对匹配不到、跨文件键协调失效。 修复后应生成前置匹配序列,运行时 merge-join 应覆盖全匹配/单明细匹配等组合。 """ import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) import pytest from cobol_testgen.__init__ import ( _coordinate_multi_file_keys, _coordinate_tertiary_fd, _find_key_pairs_inner, ) def _group(name): return {'name': name, 'level': 1, 'pic': None, 'pic_info': {'type': 'group', 'digits': 0, 'decimal': 0, 'length': 80, 'signed': False}, 'section': 'FILE', 'is_filler': False, 'redefines': None, 'usage': 'DISPLAY', 'occurs': 0, 'occurs_depending': None, 'value': None, 'values': None} def _field(name, level, pic, ftype, length): return {'name': name, 'level': level, 'pic': pic, 'pic_info': {'type': ftype, 'digits': 0, 'decimal': 0, 'length': length, 'signed': False}, 'section': 'FILE', 'is_filler': False, 'redefines': None, 'usage': 'DISPLAY', 'occurs': 0, 'occurs_depending': None, 'value': None, 'values': None} def _make_data_fields(): # 字段顺序与源文件一致:01 记录紧跟其子字段 return [ _group('R01INNREC'), _field('SWEMP-ID', 3, 'X(008)', 'alphanumeric', 8), _field('SWDATE', 3, '9(008)', 'numeric', 8), _group('R02INNREC'), _field('SREMP-ID', 3, 'X(008)', 'alphanumeric', 8), _field('SRWORK-DATE', 3, '9(008)', 'numeric', 8), _field('SRLEAVE-TYPE', 3, 'X(002)', 'alphanumeric', 2), _group('R03INNREC'), _field('SLEMP-ID', 3, 'X(008)', 'alphanumeric', 8), _field('SLDATE', 3, '9(008)', 'numeric', 8), _field('SLLEAVE-TYPE', 3, 'X(002)', 'alphanumeric', 2), _field('SLSTART-TIME', 3, '9(004)', 'numeric', 4), _field('SLEND-TIME', 3, '9(004)', 'numeric', 4), ] def _make_records(n=12): recs = [] for i in range(n): recs.append({ 'SWEMP-ID': f'R{i:07d}', 'SWDATE': f'{20000101 + i:08d}', 'SREMP-ID': f'C{i:07d}', 'SRWORK-DATE': f'{20000101 + i:08d}', 'SRLEAVE-TYPE': '99', 'SLEMP-ID': f'I{i:07d}', 'SLDATE': f'{20000101 + i:08d}', 'SLLEAVE-TYPE': '99', 'SLSTART-TIME': '0822', 'SLEND-TIME': '0922', }) return recs FILE_SEC = { 'R01INNFIL': ['R01INNREC'], 'R02INNFIL': ['R02INNREC'], 'R03INNFIL': ['R03INNREC'], 'W01OUTFIL': ['W01OUTREC'], } OPEN_DIR = { 'R01INNFIL': 'INPUT', 'R02INNFIL': 'INPUT', 'R03INNFIL': 'INPUT', 'W01OUTFIL': 'OUTPUT', } def test_find_key_pairs_uses_field_prefix_not_record_prefix(): """字段前缀 SW/SR/SL 应能配对。""" fields = _make_data_fields() assert _find_key_pairs_inner('SW', 'SR', fields), 'SW-SR 应配对' assert _find_key_pairs_inner('SW', 'SL', fields), 'SW-SL 应配对' def test_coordinate_multi_file_keys_builds_front_sequence(): """KIN07DAI 形态(记录名 R01INNREC + 字段 SW/SR/SL)下协调应生效: 记录0 三文件键一致、R03 同键重复块 + 休暇种别注入、键单调递增。""" fields = _make_data_fields() records = _make_records(12) _coordinate_multi_file_keys( records, [], fields, {}, FILE_SEC, term_types=['normal'] * len(records), open_dir=OPEN_DIR, ) r0 = records[0] assert r0['SWEMP-ID'] == r0['SREMP-ID'] == r0['SLEMP-ID'], '记录0 三文件键应一致' assert r0['SWDATE'] == r0['SRWORK-DATE'] == r0['SLDATE'], '记录0 三文件日期应一致' # R03 同键重复块(休暇种别 01-04 + WHEN OTHER '99') r03_keys = [records[i]['SLEMP-ID'] for i in range(5)] assert all(k == r03_keys[0] for k in r03_keys), 'R03 前5条记录键应一致' lts = [records[i]['SLLEAVE-TYPE'] for i in range(5)] assert lts == ['01', '02', '03', '04', '99'], "休暇种别应注入 01-04+'99', 实际 {lts}".format(lts=lts) # 键单调递增(避免 merge-join 指针跳过) k0, k1 = records[0]['SWEMP-ID'], records[1]['SWEMP-ID'] assert k0 < k1, f'前置序列主键应递增: {k0} < {k1}' def _simulate_merge(R01, R02, R03): """模拟 KIN07DAI 的 3 文件 merge-join,返回 (pattern_counts, match_flags)。 pattern 集合: A=全匹配, B=仅R02, C=仅R03, D=无匹配 match_flags: (r02_match, r03_match, leave_calc, lt_seen) """ def key(e, d): return (str(e).strip(), str(d)) n1, n2, n3 = len(R01), len(R02), len(R03) r1 = r2 = r3 = 0 e1 = e2 = e3 = False def rdR1(): nonlocal r1, e1 if r1 < n1: r1 += 1 else: e1 = True def rdR2(): nonlocal r2, e2 if r2 < n2: r2 += 1 else: e2 = True def rdR3(): nonlocal r3, e3 if r3 < n3: r3 += 1 else: e3 = True rdR1(); rdR2(); rdR3() pat = {'A': 0, 'B': 0, 'C': 0, 'D': 0} r02m = r03m = calcs = 0 lt_seen = set() while not e1: r01k = key(R01[r1 - 1][0], R01[r1 - 1][1]) ptn = 4 while not e2: if key(R02[r2 - 1][0], R02[r2 - 1][1]) >= r01k: break rdR2() if not e2 and key(R02[r2 - 1][0], R02[r2 - 1][1]) == r01k: ptn = 2 r02m += 1 rdR2() while not e3: if key(R03[r3 - 1][0], R03[r3 - 1][1]) >= r01k: break rdR3() if not e3 and key(R03[r3 - 1][0], R03[r3 - 1][1]) == r01k: r03m += 1 while True: calcs += 1 lt_seen.add(R03[r3 - 1][3]) rdR3() if e3: break if key(R03[r3 - 1][0], R03[r3 - 1][1]) != r01k: break ptn = 1 if ptn == 2 else 3 pat[['A', 'B', 'C', 'D'][ptn - 1]] += 1 rdR1() return pat, (r02m, r03m, calcs, lt_seen) def test_coordination_reaches_all_match_combinations_at_runtime(): """协调后运行时 merge-join 应覆盖全匹配/仅R02/仅R03 三种组合。""" fields = _make_data_fields() records = _make_records(12) _coordinate_multi_file_keys( records, [], fields, {}, FILE_SEC, term_types=['normal'] * len(records), open_dir=OPEN_DIR, ) R01 = [(r['SWEMP-ID'], r['SWDATE']) for r in records] R02 = [(r['SREMP-ID'], r['SRWORK-DATE']) for r in records] R03 = [(r['SLEMP-ID'], r['SLDATE'], r['SLLEAVE-TYPE'], r['SLLEAVE-TYPE']) for r in records] pat, (r02m, r03m, calcs, lt_seen) = _simulate_merge(R01, R02, R03) assert r02m >= 1, 'R02 照合应至少发生 1 次' assert r03m >= 1, 'R03 照合应至少发生 1 次' assert calcs >= 4, '休暇計算应至少 4 次(EVALUATE 全 WHEN)' assert lt_seen >= {'01', '02', '03', '04'}, f'休暇種別应全覆盖, 实际 {lt_seen}' assert pat['A'] >= 1, f'全匹配(PATTERN-A)应覆盖, 实际 {pat}' assert pat['B'] >= 1, f'仅R02匹配(PATTERN-B)应覆盖, 实际 {pat}' assert pat['C'] >= 1, f'仅R03匹配(PATTERN-C)应覆盖, 实际 {pat}' def test_coordinate_tertiary_fd_long_leave_for_cap(): """PATTERN-C 用判别块应注入长休暇时长(覆盖 8h 上限 T 分支)。""" fields = _make_data_fields() records = _make_records(12) _coordinate_tertiary_fd(records, ['SW', 'SR', 'SL'], fields) # 块2(索引 max(3,5)=5..9):长时长 0600-2200 for i in range(5, 10): assert records[i]['SLSTART-TIME'] == '0600', f'rec{i} 长休暇开始时间' assert records[i]['SLEND-TIME'] == '2200', f'rec{i} 长休暇结束时间'