add cobol_testgen module

This commit is contained in:
hangshuo652
2026-06-08 21:07:16 +08:00
parent 7fcdb41a85
commit 0730045e27
13 changed files with 5586 additions and 0 deletions
+775
View File
@@ -0,0 +1,775 @@
"""设计层:路径枚举 + 值生成 + 约束应用"""
import os
import re
import logging
from . import agents, CONFIG
from .models import BrSeq, BrIf, BrEval, BrPerform, Assign, CallNode, CondNot, CondLeaf, ExitNode, GoTo
from .cond import parse_single_condition, parse_compound_condition, is_field, collect_leaves, mcdc_sets, satisfying_value
from .core import trace_to_root, invert_through_chain, propagate_assignments, _basename
logger = logging.getLogger(__name__)
_STOP = ('__STOP__', '', None, True)
_MAX_PATHS = 5000
_FALLBACK_MAX_PATHS = 100
_ACTIVE_MAX_PATHS = _MAX_PATHS
_LLM_FAILED = False
def _filter_stop(cons):
return [c for c in cons if c is not _STOP]
def _cap_paths(paths):
if len(paths) > _ACTIVE_MAX_PATHS:
return paths[:_ACTIVE_MAX_PATHS]
return paths
# ── 路径枚举 ──
def _try_llm_enum_paths(node, fields):
global _LLM_FAILED
if _LLM_FAILED:
logger.debug("断路器已跳,跳过 LLM")
return None
if not CONFIG.get("llm_generator", True):
logger.debug("llm_generator 已关闭,降级规则引擎")
return None
if not os.environ.get(agents.DEEPSEEK_API_KEY_ENV):
logger.warning("DEEPSEEK_API_KEY 未设置,降级规则引擎")
return None
try:
result = agents.llm_generate_all_paths(node, fields)
if result is not None:
logger.info(f"LLM 路径生成成功,{len(result)}")
return result
logger.warning("LLM 返回空,降级规则引擎")
except Exception as e:
logger.error(f"LLM API 调用异常: {e}")
_LLM_FAILED = True
return None
def enum_paths(node, fields):
global _ACTIVE_MAX_PATHS
# === LLM 优先(整体替换整个树的路径生成) ===
llm_result = _try_llm_enum_paths(node, fields)
if llm_result is not None:
_ACTIVE_MAX_PATHS = _MAX_PATHS
return llm_result
if _ACTIVE_MAX_PATHS == _MAX_PATHS:
logger.warning("降级到规则引擎(路径上限 5000 → 100)")
_ACTIVE_MAX_PATHS = _FALLBACK_MAX_PATHS
"""枚举路径,每条路径返回 (constraints, assignments).
返回 list[tuple[list[tuple], dict]].
"""
if isinstance(node, Assign):
return [([], {node.target: [node.source_info]})]
if isinstance(node, BrSeq):
if not node.children:
return [([], {})]
paths = [([], {})]
for child in node.children:
child_paths = _cap_paths(enum_paths(child, fields))
new_active = []
for p_cons, p_assign in paths:
if any(c is _STOP for c in p_cons):
new_active.append((p_cons, p_assign))
continue
for cp_cons, cp_assign in child_paths:
merged = {}
for d in (p_assign, cp_assign):
for k, v in d.items():
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
merged_cons = p_cons + list(cp_cons)
new_active.append((merged_cons, merged))
paths = _cap_paths(new_active)
return paths
elif isinstance(node, BrIf):
parsed = parse_single_condition(node.condition, fields)
if parsed and is_field(parsed[0], fields):
field, op, val = parsed
paths = []
true_sub = _cap_paths(enum_paths(node.true_seq, fields))
for sp_cons, sp_assign in (true_sub or [([], {})]):
paths.append(([(field, op, val, True)] + sp_cons, sp_assign))
false_sub = _cap_paths(enum_paths(node.false_seq, fields))
for fp_cons, fp_assign in (false_sub or [([], {})]):
paths.append(([(field, op, val, False)] + fp_cons, fp_assign))
return paths
# CondNot wrapping a single leaf (e.g., IF NOT WS-AMOUNT > 1000)
if node.cond_tree and isinstance(node.cond_tree, CondNot):
child = node.cond_tree.child
if isinstance(child, CondLeaf) and is_field(child.field, fields):
paths = []
true_sub = _cap_paths(enum_paths(node.true_seq, fields))
for sp_cons, sp_assign in (true_sub or [([], {})]):
paths.append(([(child.field, child.op, child.value, False)] + sp_cons, sp_assign))
false_sub = _cap_paths(enum_paths(node.false_seq, fields))
for fp_cons, fp_assign in (false_sub or [([], {})]):
paths.append(([(child.field, child.op, child.value, True)] + fp_cons, fp_assign))
return paths
if node.cond_tree:
leaves = collect_leaves(node.cond_tree)
if leaves and all(is_field(l.field, fields) for l in leaves):
sets = mcdc_sets(node.cond_tree, fields)
if sets:
paths = []
for constraints, decision in sets:
body = _cap_paths(enum_paths(
node.true_seq if decision else node.false_seq, fields
))
for sp_cons, sp_assign in (body or [([], {})]):
paths.append((constraints + sp_cons, sp_assign))
return paths
# CondLeaf fallback: 单 leaf(含 88-level 解析后的条件树)MC/DC 不适用
if len(leaves) == 1:
leaf = leaves[0]
paths = []
true_sub = _cap_paths(enum_paths(node.true_seq, fields))
for sp_cons, sp_assign in (true_sub or [([], {})]):
paths.append(([(leaf.field, leaf.op, leaf.value, True)] + sp_cons, sp_assign))
false_sub = _cap_paths(enum_paths(node.false_seq, fields))
for fp_cons, fp_assign in (false_sub or [([], {})]):
paths.append(([(leaf.field, leaf.op, leaf.value, False)] + fp_cons, fp_assign))
return paths
# Fallback: parsed condition but non-field (e.g. arithmetic expr)
if parsed:
field, op, val = parsed
paths = []
true_sub = enum_paths(node.true_seq, fields)
for sp_cons, sp_assign in (true_sub or [([], {})]):
paths.append(([(field, op, val, True)] + sp_cons, sp_assign))
false_sub = enum_paths(node.false_seq, fields)
for fp_cons, fp_assign in (false_sub or [([], {})]):
paths.append(([(field, op, val, False)] + fp_cons, fp_assign))
return paths
return [([], {})]
elif isinstance(node, BrEval):
if node.subjects:
paths = []
prior_false_cons = []
for values, seq in node.when_list:
sub = _cap_paths(enum_paths(seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
when_cons = [(node.subjects[i], '=', values[i], True)
for i in range(len(node.subjects))]
constraints = list(prior_false_cons) + when_cons + sp_cons
paths.append((constraints, sp_assign))
for i in range(len(node.subjects)):
prior_false_cons.append((node.subjects[i], '=', values[i], False))
if node.has_other:
sub = _cap_paths(enum_paths(node.other_seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append((list(prior_false_cons) + sp_cons, sp_assign))
return paths
if node.subject == 'TRUE':
paths = []
prior_false = []
for value, seq in node.when_list:
cond = parse_compound_condition(value, fields)
if cond and isinstance(cond, CondLeaf) and is_field(cond.field, fields):
# Simple condition
sub = _cap_paths(enum_paths(seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
constraints = list(prior_false)
constraints.append((cond.field, cond.op, cond.value, True))
paths.append((constraints + sp_cons, sp_assign))
prior_false.append((cond.field, cond.op, cond.value, False))
elif cond:
# Compound condition — use MC/DC for path generation
leaves = collect_leaves(cond)
if leaves and all(is_field(l.field, fields) for l in leaves):
sets = mcdc_sets(cond, fields)
if sets:
sub = _cap_paths(enum_paths(seq, fields))
false_set = None
for cs, decision in sets:
if decision:
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append((list(prior_false) + list(cs) + sp_cons, sp_assign))
elif false_set is None:
false_set = cs
if false_set is not None:
prior_false.extend(false_set)
else:
prior_false = []
break
else:
prior_false = []
break
else:
prior_false = []
break
else:
prior_false = []
break
if node.has_other:
sub = _cap_paths(enum_paths(node.other_seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append((list(prior_false) + sp_cons, sp_assign))
return paths
if not is_field(node.subject, fields):
return [([], {})]
paths = []
for value, seq in node.when_list:
sub = _cap_paths(enum_paths(seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, '=', value, True)] + sp_cons, sp_assign))
if node.has_other:
case_vals = [v for v, _ in node.when_list]
sub = _cap_paths(enum_paths(node.other_seq, fields))
for sp_cons, sp_assign in (sub or [([], {})]):
paths.append(([(node.subject, 'not_in', case_vals, True)] + sp_cons, sp_assign))
return paths
elif isinstance(node, BrPerform):
if node.perf_type in ('para', 'thru'):
if node.body_seq:
return enum_paths(node.body_seq, fields)
return [([], {})]
elif node.perf_type in ('until', 'para_until', 'varying', 'para_varying'):
# 尝试单条件(现有逻辑)
parsed = parse_single_condition(node.condition, fields)
if parsed and is_field(parsed[0], fields):
field, op, val = parsed
paths = []
false_sub = _cap_paths(enum_paths(node.body_seq, fields))
for sp_cons, sp_assign in (false_sub or [([], {})]):
# PERFORM VARYING: 将 FROM 值作为 MOVE 赋值加入 Enter 路径
if node.varying_from and node.varying_var:
is_fld = any(f['name'] == node.varying_from for f in fields) if fields else False
from_asgn = {'type': 'move', 'source_vars': [node.varying_from]} if is_fld else {'type': 'move_literal', 'literal': node.varying_from}
from_assign = {node.varying_var: [from_asgn]}
merged = {}
for d in (from_assign, sp_assign):
for k, v in d.items():
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
sp_assign = merged
paths.append(([(field, op, val, False)] + sp_cons, sp_assign))
paths.append(([(field, op, val, True)], {}))
return paths
# 尝试复合条件(AND/OR
cond_tree = parse_compound_condition(node.condition, fields)
if cond_tree:
leaves = collect_leaves(cond_tree)
if leaves and all(is_field(l.field, fields) for l in leaves):
sets = mcdc_sets(cond_tree, fields)
if sets:
paths = []
false_sub = _cap_paths(enum_paths(node.body_seq, fields))
for sp_cons, sp_assign in (false_sub or [([], {})]):
# PERFORM VARYING: 将 FROM 值作为 MOVE 赋值加入 Enter 路径
if node.varying_from and node.varying_var:
is_fld = any(f['name'] == node.varying_from for f in fields) if fields else False
from_asgn = {'type': 'move', 'source_vars': [node.varying_from]} if is_fld else {'type': 'move_literal', 'literal': node.varying_from}
from_assign = {node.varying_var: [from_asgn]}
merged = {}
for d in (from_assign, sp_assign):
for k, v in d.items():
merged.setdefault(k, []).extend(v if isinstance(v, list) else [v])
sp_assign = merged
for constraints, decision in sets:
if not decision:
paths.append((list(constraints) + sp_cons, sp_assign))
for constraints, decision in sets:
if decision:
paths.append((list(constraints), {}))
if paths:
return paths
return [([], {})]
elif isinstance(node, CallNode):
return [([], {})]
elif isinstance(node, ExitNode):
return [([_STOP], {})]
elif isinstance(node, GoTo):
paths = enum_paths(node.body_seq, fields)
return [([_STOP] + c, a) for c, a in paths]
return [([], {})]
# ── 值生成 ──
def seq_numeric(seq_num: int, total_digits: int) -> str:
val = seq_num % (10 ** total_digits)
if val == 0:
val = 10 ** total_digits - 1
return str(val).zfill(total_digits)
def seq_alpha(seq_num: int, length: int) -> str:
letter = chr(65 + (seq_num - 1) % 26)
return letter * length
def seq_date(seq_num: int) -> str:
from datetime import datetime, timedelta
base = datetime(2000, 1, 1)
d = base + timedelta(days=seq_num - 1)
return d.strftime('%Y%m%d')
def _is_date_field(name: str) -> bool:
patterns = [r'DATE', r'YYMMDD', r'YYYYMM', r'YEAR', r'MONTH', r'DAY']
for p in patterns:
if re.search(p, name.upper()):
return True
return False
_SPECIAL_VALUES = {
'ZERO': '0', 'ZEROS': '0', 'ZEROES': '0',
'SPACE': ' ', 'SPACES': ' ',
'HIGH-VALUE': '\xff', 'HIGH-VALUES': '\xff',
'LOW-VALUE': '\x00', 'LOW-VALUES': '\x00',
'QUOTE': "'", 'QUOTES': "'",
'ALL': '',
}
def _apply_value(field: dict, rec: dict) -> bool:
"""尝试应用 VALUE 子句的初始值。返回 True 表示已处理。"""
raw = field.get('value')
if raw is None:
return False
val = str(raw).strip("'\"").strip()
name = field['name']
pi = field.get('pic_info', {})
# 处理 COBOL 特殊值
if val.upper() in _SPECIAL_VALUES:
val = _SPECIAL_VALUES[val.upper()]
ftype = pi.get('type', 'unknown')
if ftype == 'numeric':
digits = pi.get('digits', 0) + pi.get('decimal', 0)
if digits:
rec[name] = val.zfill(digits)
else:
rec[name] = val
else:
length = pi.get('length', 0) or 1
rec[name] = val.ljust(length)[:length]
return True
def _children_of(group_name: str, fields: list) -> list:
"""返回组项目 group_name 在 fields 中的直属子字段列表(按声明顺序)。
终止条件:遇到同/更高级别(sibling/组边界)或 77 级(独立字段)。
"""
result = []
group_level = None
found = False
for f in fields:
if not found and f['name'] == group_name:
group_level = f['level']
found = True
continue
if found:
if f['level'] <= group_level or f['level'] == 77:
break
# 88-level 是条件名,不计为子字段
if f.get('is_88'):
continue
result.append(f)
return result
def _make_numeric_value(idx: int, record_num: int, total_digits: int) -> str:
for step in (100, 10, 1):
val = idx * step + record_num
if val < 10 ** total_digits:
return str(val).zfill(total_digits)
return str(record_num).zfill(total_digits)
def _make_alpha_value(idx: int, record_num: int, length: int) -> str:
if length == 1:
ch = chr(65 + (idx + record_num - 2) % 26)
return ch
letter = chr(65 + (idx - 1) % 26)
return letter + str(record_num).zfill(length - 1)
def make_base_record(seq_num: int, fields: list) -> dict:
rec = {}
redefines_map = {} # 标量 REDEFINES: parent_name → [child_names]
group_redefines = [] # 组 REDEFINES: [(redef_name, target_name)]
filler_key_counter = 0
numeric_idx = 0
alpha_idx = 0
record_num = seq_num
for f in fields:
name = f['name']
if f.get('is_88'):
continue
if f.get('redefines'):
parent = f['redefines']
if f.get('pic'):
# 标量 REDEFINES(有 PIC,如 WS-AMOUNT-DISP REDEFINES WS-AMOUNT PIC X(9)
redefines_map.setdefault(parent, []).append(name)
continue
else:
# 组 REDEFINES(无 PIC,如 CUST-ADDR2 REDEFINES CUST-ADDR
group_redefines.append((name, parent))
# 不 continue — 组本身无 PIC 会在下方"组项目跳过"处理
# 其子字段作为独立字段正常走循环
if f.get('is_filler'):
if name in rec:
filler_key_counter += 1
name = f'FILLER_{filler_key_counter + 1}'
rec[name] = 'x' * (f.get('pic_info', {}).get('length', 0) or 1)
continue
# Pass 0: VALUE 子句初始值优先
if _apply_value(f, rec):
continue
# 组项目(无 PIC)跳过
if not f.get('pic'):
continue
pi = f.get('pic_info', {})
ftype = pi.get('type', 'unknown')
digits = pi.get('digits', 0)
decimal = pi.get('decimal', 0)
length = pi.get('length', 0)
if ftype == 'numeric':
if _is_date_field(name):
rec[name] = seq_date(record_num)
else:
numeric_idx += 1
rec[name] = _make_numeric_value(numeric_idx, record_num, digits + decimal)
elif ftype in ('alphanumeric', 'alphabetic'):
alpha_idx += 1
rec[name] = _make_alpha_value(alpha_idx, record_num, length or 1)
elif ftype == 'numeric-edited':
numeric_idx += 1
raw = _make_numeric_value(numeric_idx, record_num, digits + decimal)
rec[name] = raw.rjust(length)
else:
alpha_idx += 1
rec[name] = _make_alpha_value(alpha_idx, record_num, 8)
# Pass 2a: 标量 REDEFINES 复制
for parent_name, child_names in redefines_map.items():
if parent_name in rec:
for child_name in child_names:
rec[child_name] = rec[parent_name]
# Pass 2b: 组 REDEFINES 按位置递归复制子字段
for redef_name, target_name in group_redefines:
redef_kids = _children_of(redef_name, fields)
tgt_kids = _children_of(target_name, fields)
tgt_idx = 0
for i, rk in enumerate(redef_kids):
if tgt_idx >= len(tgt_kids):
break
if i == len(redef_kids) - 1 and len(redef_kids) < len(tgt_kids):
# 最后一个 REDEFINES 子字段,且目标更多 → 拼接剩余所有目标值
parts = [rec.get(tk['name'], '') for tk in tgt_kids[tgt_idx:]]
rec[rk['name']] = ''.join(parts)
elif i == len(redef_kids) - 1 and len(redef_kids) > len(tgt_kids):
# REDEFINES 子字段更多 → 最后一个 REDEFINES 子字段取最后目标值
rec[rk['name']] = rec.get(tgt_kids[-1]['name'], '')
else:
rec[rk['name']] = rec.get(tgt_kids[tgt_idx]['name'], '')
tgt_idx += 1
return rec
# ── 约束应用 ──
def _check_constraint_satisfied(rec, field_name, operator, value, want_true, fields):
"""检查 field_name 当前值是否满足该约束。满足返回 True。"""
for f in fields:
if f['name'] == field_name:
pi = f.get('pic_info', {})
ftype = pi.get('type', 'unknown')
val = rec.get(field_name)
if val is None:
return False
if operator == 'not_in':
cases = value if isinstance(value, list) else []
return str(val) not in cases
if ftype == 'numeric':
try:
num_val = int(float(str(val)))
num_target = int(float(str(value)))
except (ValueError, TypeError):
return False
if operator in ('>=', '>', '<', '<=', '=', '<>'):
if operator == '>=': ok = num_val >= num_target
elif operator == '>': ok = num_val > num_target
elif operator == '<': ok = num_val < num_target
elif operator == '<=': ok = num_val <= num_target
elif operator == '=': ok = num_val == num_target
elif operator == '<>': ok = num_val != num_target
return ok == want_true
return True
else:
s_val = str(val).strip().upper()
s_target = str(value).strip().upper()
eq = s_val == s_target
if operator == '=':
return eq == want_true
elif operator == '<>':
return (not eq) == want_true
return True
return False
_ARITH_BOUNDS = {
'left_big_ops': {'>', '>=', '<>'},
'left_small_ops': {'<', '<='},
}
def _arith_pic_info(field_name, fields):
for f in fields:
if f['name'] == field_name.upper():
return f.get('pic_info', {})
return {}
def _arith_numeric_pick(field_name, want_big, fields):
"""为字段选一个大值或小值,返回字符串。"""
pi = _arith_pic_info(field_name, fields)
if pi.get('type') != 'numeric':
return None
digits = pi.get('digits', 0)
decimal = pi.get('decimal', 0)
total = digits + decimal
max_val = 10 ** total - 1
if want_big:
pick = int(max_val * 0.7)
else:
pick = 1
int_part = str(pick // (10 ** decimal)).zfill(digits)
dec_part = str(pick % (10 ** decimal)).zfill(decimal)
if decimal == 0:
return int_part
return int_part + dec_part
def _apply_arith_constraint(rec, field_name, operator, value, want_true, fields):
"""对算术表达式条件进行字段值 steering。
例如 A + B > C (want_true=True):
- 左值字段(A, B)设大 → 右值字段(C)设小
例如 A + B <= C (want_true=True):
- 左值字段设小 → 右值字段设大
这是启发式 steering,不是精确求解。
主要目标是保证分支可达,不保证边界值精确。
"""
# 1. 提取左值表达式中的所有字段名(大写)
tokens = re.findall(r'\b[A-Z][A-Z0-9-]*(?:\([^)]*\))?\b', field_name.upper())
left_fields = [t for t in tokens if any(f['name'] == t for f in fields)]
# 2. 右值是否也为字段
right_field = value if any(f['name'] == value for f in fields) else None
if not left_fields:
logger.debug(f"算术表达式无法提取字段: {field_name}")
return
# 3. 确定方向:want_true 时左值应大还是小
if operator in _ARITH_BOUNDS['left_big_ops']:
left_big = want_true
elif operator in _ARITH_BOUNDS['left_small_ops']:
left_big = not want_true
else:
left_big = want_true
# 4. 设置左值字段
for lf in left_fields:
pick = _arith_numeric_pick(lf, left_big, fields)
if pick is not None:
rec[lf] = pick
# 5. 设置右值字段(如果有)
if right_field:
pick = _arith_numeric_pick(right_field, not left_big, fields)
if pick is not None:
rec[right_field] = pick
def apply_constraint(rec, field_name, operator, value, want_true, fields, assignments=None, path_assign=None):
# 标准化字段名:去除括号内空格(WS-CELL ( 1, 1 ) → WS-CELL(1,1)
field_name = re.sub(r'\s*([(),])\s*', r'\1', field_name)
# 变量下标解析:WS-FIXED-VALUE(WS-IDX) → WS-FIXED-VALUE(1)
vm = re.match(r'^(\w[\w-]*)\((\w[\w-]*)\)$', field_name)
if vm:
base_var, subscript_var = vm.groups()
if subscript_var in rec:
try:
resolved_name = f'{base_var}({int(rec[subscript_var])})'
if any(f['name'] == resolved_name for f in fields):
apply_constraint(rec, resolved_name, operator, value, want_true, fields, assignments, path_assign)
return
except (ValueError, TypeError):
pass
# 下标传播:无下标约束 → 应用到所有下标变体
base = _basename(field_name)
subscripted = [f for f in fields if f['name'] != base and _basename(f['name']) == base]
if subscripted and field_name == base:
for sf in subscripted:
apply_constraint(rec, sf['name'], operator, value, want_true, fields, assignments, path_assign)
return
# REDEFINES 字段的约束重定向到父字段(共享存储)
for f in fields:
if f['name'] == field_name:
if f.get('is_filler'):
return
if f.get('redefines'):
parent_name = f['redefines']
logger.debug(f"REDEFINES 约束重定向: {field_name}{parent_name}")
apply_constraint(rec, parent_name, operator, value, want_true, fields, assignments, path_assign)
return
break
if assignments:
root_var, chain = trace_to_root(field_name, assignments, fields, path_assign)
if root_var != field_name:
new_field_name, new_op, new_val = invert_through_chain(root_var, chain, operator, value)
if any(f['name'] == new_field_name for f in fields):
field_name, operator, value = new_field_name, new_op, new_val
# 如果当前值已满足该约束,跳过覆盖(保持先前约束的一致性)
if _check_constraint_satisfied(rec, field_name, operator, value, want_true, fields):
return
if operator == 'not_in':
for f in fields:
if f['name'] == field_name:
pi = f.get('pic_info', {})
cases = value if isinstance(value, list) else []
ftype = pi.get('type', 'unknown')
if ftype in ('alphanumeric', 'alphabetic'):
for c in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ':
if c not in cases:
rec[field_name] = c.ljust(pi.get('length', 1), c)
return
else:
for n in range(1, 100):
if str(n) not in cases:
rec[field_name] = str(n).zfill(pi.get('digits', 0) + pi.get('decimal', 0))
return
return
# 字段间比较(值侧也是字段名)
if any(f['name'] == value for f in fields):
if re.search(r'[+\-*/]', field_name):
_apply_arith_constraint(rec, field_name, operator, value, want_true, fields)
else:
logger.debug(f"字段间比较约束跳过:{field_name} {operator} {value}")
return
for f in fields:
if f['name'] == field_name:
pi = f.get('pic_info', {})
val = satisfying_value(pi, operator, value, want_true)
rec[field_name] = val
return
# ── 记录生成入口 ──
def sync_redefined_fields(rec, fields):
"""赋值/约束后同步 REDEFINES 字段:父字段的值拷贝到所有 REDEFINES 子字段。"""
redefines_map = {}
group_redefines = []
for f in fields:
if f.get('is_88') or f.get('is_filler'):
continue
if f.get('redefines') and f.get('pic'):
redefines_map.setdefault(f['redefines'], []).append(f['name'])
elif f.get('redefines') and not f.get('pic'):
group_redefines.append((f['name'], f['redefines']))
for parent_name, child_names in redefines_map.items():
if parent_name in rec:
for child_name in child_names:
rec[child_name] = rec[parent_name]
for redef_name, target_name in group_redefines:
redef_kids = _children_of(redef_name, fields)
tgt_kids = _children_of(target_name, fields)
tgt_idx = 0
for i, rk in enumerate(redef_kids):
if tgt_idx >= len(tgt_kids):
break
if i == len(redef_kids) - 1 and len(redef_kids) < len(tgt_kids):
parts = [rec.get(tk['name'], '') for tk in tgt_kids[tgt_idx:]]
rec[rk['name']] = ''.join(parts)
elif i == len(redef_kids) - 1 and len(redef_kids) > len(tgt_kids):
rec[rk['name']] = rec.get(tgt_kids[-1]['name'], '')
else:
rec[rk['name']] = rec.get(tgt_kids[tgt_idx]['name'], '')
tgt_idx += 1
def apply_occurs_depending(rec, fields):
"""根据 OCCURS DEPENDING ON 变量的当前值,清零超范围的下标字段。"""
for f in fields:
dep_var = f.get('occurs_depending')
if not dep_var:
continue
name = f['name']
m = re.search(r'\((\d+)\)$', name)
if not m:
continue
sub = int(m.group(1))
max_val = int(rec.get(dep_var, 0))
if sub <= max_val:
continue
pi = f.get('pic_info', {})
ftype = pi.get('type', 'unknown')
length = pi.get('length', 0) or 1
if ftype == 'numeric':
rec[name] = '0' * (pi.get('digits', 0) + pi.get('decimal', 0))
elif ftype in ('alphanumeric', 'alphabetic'):
rec[name] = ' ' * length
else:
rec[name] = '0' * length
def generate_records(branch_paths_with_assigns, data_fields, base_assignments=None, file_sec=None):
"""生成测试数据记录。
branch_paths_with_assigns: list of (constraints, path_assignments).
base_assignments: 全局 assignments dict (用于 trace_to_root).
"""
records = []
if branch_paths_with_assigns:
for seq, (path_cons, path_assign) in enumerate(branch_paths_with_assigns, start=1):
path_cons = _filter_stop(path_cons)
rec = make_base_record(seq, data_fields)
# Pass A: 先传播赋值(MOVE/COMPUTE/READ INTO 等),模拟到决策点前的程序状态
if isinstance(path_assign, dict):
propagate_assignments(rec, path_assign, data_fields, file_sec=file_sec)
# Pass B: 约束覆盖(确保决策条件满足,覆盖 MOVE 带来的值)
for c in path_cons:
if len(c) == 4:
field, op, val, want = c
apply_constraint(rec, field, op, val, want, data_fields, base_assignments, path_assign)
# Pass C: 同步 REDEFINES(确保共享存储一致)
sync_redefined_fields(rec, data_fields)
# Pass D: OCCURS DEPENDING ON — 清零超范围的下标字段
apply_occurs_depending(rec, data_fields)
records.append(rec)
if not records:
rec = make_base_record(1, data_fields)
if base_assignments:
propagate_assignments(rec, base_assignments, data_fields, file_sec=file_sec)
records.append(rec)
return records