"""JIN02SKL 型"明细键─主表键"后缀配对测试。 复现根因:跨文件键协调 _find_key_pairs 只做同名精确匹配 / WORK- 变体 / EMP 族回退。当明细键 base='SKILL-CODE' 与主表键 base='MST-SKILL-CODE' (多 'MST-' 限定词)时匹配不到,导致 SEARCH ALL 永未命中、 TBL-EMP-COUNT 恒 0,#7/#8(> ZERO)TRUE 分支不可达。 修复:增加"后缀匹配"层级——一方 base 是另一方真后缀且 key 类别一致 (同为 -CODE/-ID/-NO/DATE/EMP-ID)时配对。通用实现、无程序名硬编码。 """ 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, _find_key_pairs_inner, ) def _group(name): return {'name': name, 'level': 1, 'pic': None, 'pic_info': {'type': 'group', 'digits': 0, 'decimal': 0, 'length': 200, '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 _skill_fields(): # 字段顺序与源文件一致:01 记录紧跟其子字段(COPY REPLACING R01/R02) return [ _group('R01INNREC'), _field('R01SKILL-CODE', 3, 'X(004)', 'alphanumeric', 4), _field('R01EMP-ID', 3, 'X(008)', 'alphanumeric', 8), _field('R01EMP-NAME', 3, 'X(040)', 'alphanumeric', 40), _field('R01EVAL-DATE', 3, 'X(008)', 'alphanumeric', 8), _field('R01SCORE', 3, '9(003)', 'numeric', 3), _group('R02INNREC'), _field('R02MST-SKILL-CODE', 3, 'X(004)', 'alphanumeric', 4), _field('R02MST-LEVEL', 3, '9(002)', 'numeric', 2), _field('R02MST-SCORE-FROM', 3, '9(003)', 'numeric', 3), _field('R02MST-SCORE-TO', 3, '9(003)', 'numeric', 3), _field('R02MST-LEVEL-NAME', 3, 'X(020)', 'alphanumeric', 20), ] def _skill_records(n=6): recs = [] for i in range(n): recs.append({ 'R01SKILL-CODE': f'A{i + 1:03d}', 'R01EMP-ID': f'B{i + 1:07d}', 'R01EMP-NAME': 'N' * 40, 'R01EVAL-DATE': f'D{i + 1:08d}', 'R01SCORE': f'{100 + i:03d}', 'R02MST-SKILL-CODE': f'F{i + 1:03d}', 'R02MST-LEVEL': f'{20 + i:02d}', 'R02MST-SCORE-FROM': f'{300 + i:03d}', 'R02MST-SCORE-TO': f'{400 + i:03d}', 'R02MST-LEVEL-NAME': 'G' * 20, }) return recs FILE_SEC = { 'R01INNFIL': ['R01INNREC'], 'R02INNFIL': ['R02INNREC'], 'W01OUTFIL': ['W01OUTREC'], 'W02OUTFIL': ['W02OUTREC'], } OPEN_DIR = { 'R01INNFIL': 'INPUT', 'R02INNFIL': 'INPUT', 'W01OUTFIL': 'OUTPUT', 'W02OUTFIL': 'OUTPUT', } def test_find_key_pairs_suffix_skill_code(): """明细键 SKILL-CODE 应与主表键 MST-SKILL-CODE 后缀配对。""" fields = _skill_fields() pairs = _find_key_pairs_inner('R01', 'R02', fields) assert ('R01SKILL-CODE', 'R02MST-SKILL-CODE') in pairs, f'SKILL-CODE↔MST-SKILL-CODE 应配对, 实际 {pairs}' def test_find_key_pairs_suffix_no_cross_class(): """类别不一致的后缀不应误配:DATE 类键不与 CODE 类键配对。""" fields = _skill_fields() pairs = _find_key_pairs_inner('R01', 'R02', fields) date_keys = [fa for fa, _ in pairs if 'DATE' in fa.upper()] code_keys = [fb for _, fb in pairs if '-CODE' in fb.upper()] # R01EVAL-DATE 不应与 R02MST-SKILL-CODE 配对 assert not any('R01EVAL-DATE' == fa and fb == 'R02MST-SKILL-CODE' for fa, fb in pairs), f'DATE 不应与 CODE 后缀配对, 实际 {pairs}' def test_coordinate_multi_file_keys_skill_match(): """协调后应使部分记录 R01SKILL-CODE == R02MST-SKILL-CODE,运行时 SEARCH 可达。""" fields = _skill_fields() records = _skill_records(6) _coordinate_multi_file_keys( records, [], fields, {}, FILE_SEC, term_types=['normal'] * len(records), open_dir=OPEN_DIR, ) matched = [r for r in records if str(r['R01SKILL-CODE']).strip() == str(r['R02MST-SKILL-CODE']).strip()] assert matched, '至少一条记录明细键应与主表键一致' def test_coordinate_exact_match_still_pairs(): """既有同名精确匹配(EMP-ID↔EMP-ID)不受后缀层级影响。""" fields = [ _group('R01INNREC'), _field('R01EMP-ID', 3, 'X(008)', 'alphanumeric', 8), _group('R02INNREC'), _field('R02EMP-ID', 3, 'X(008)', 'alphanumeric', 8), ] pairs = _find_key_pairs_inner('R01', 'R02', fields) assert ('R01EMP-ID', 'R02EMP-ID') in pairs, f'同名 EMP-ID 应仍精确配对, 实际 {pairs}' def test_coordinate_last_record_matches_table_key(): """末条 normal 记录明细键应对齐表内键(首条 R02 键)。 社员工键中断集约(JIN09RPT 型):最终社员工组在 EOF 後の终了处理で出力される (`IF WRK-EMP-EVAL-CNT > ZERO PERFORM 3000OUTPUSOR`)。協調が先頭のみ整列だと 最終组に照合データがなく最终组输出分支不可達。末条记录も表内键に揃えることで 該分支が到達可能になる。 """ fields = _skill_fields() records = _skill_records(12) _coordinate_multi_file_keys( records, [], fields, {}, FILE_SEC, term_types=['normal'] * len(records), open_dir=OPEN_DIR, ) table_key = str(records[0]['R02MST-SKILL-CODE']).strip() last = records[-1] assert str(last['R01SKILL-CODE']).strip() == table_key, \ f'末条记录明细键应对齐表内键 {table_key}, 实际 {last["R01SKILL-CODE"]!r}'