feat: SQL between/hostvar-key alignment, class-condition parsing, gcov merge across scenario runs
This commit is contained in:
+96
-12
@@ -98,10 +98,15 @@ def parse_single_condition(text, fields=None):
|
||||
if ' OF ' in text.upper():
|
||||
text = text.split(' OF ')[0].strip()
|
||||
|
||||
# COBOL class condition: WS-KEY-DGT-N NUMERIC
|
||||
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE):
|
||||
m = re.match(r'^([A-Z][A-Z0-9_-]*(?:\([^)]*\))?)\s+(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|POSITIVE|NEGATIVE|ZERO)\s*$', text, re.IGNORECASE)
|
||||
return (m.group(1), '=', m.group(2).upper())
|
||||
# COBOL class condition: WS-KEY-DGT-N [IS] [NOT] NUMERIC/ALPHABETIC/...
|
||||
# Return (field, 'IS', CLASS, want) — want=True → field must satisfy the class,
|
||||
# want=False → field must NOT satisfy the class (IS NOT / NOT).
|
||||
m = re.match(
|
||||
r'^([A-Z][A-Z0-9_-]*(?:\([^)]*\))?)\s+(?:IS\s+)?'
|
||||
r'(NOT\s+)?(NUMERIC|ALPHABETIC|ALPHABETIC-UPPER|ALPHABETIC-LOWER|POSITIVE|NEGATIVE|ZERO)\s*$',
|
||||
text, re.IGNORECASE)
|
||||
if m:
|
||||
return (m.group(1), 'IS', m.group(3).upper(), not bool(m.group(2)))
|
||||
|
||||
# Bare field reference (no operator, no NOT): WS-EOF → WS-EOF = 'Y'
|
||||
if re.match(r'^[A-Z][A-Z0-9_-]*(?:\([^)]*\))?\s*$', text, re.IGNORECASE):
|
||||
@@ -150,7 +155,7 @@ def parse_single_condition(text, fields=None):
|
||||
if text.upper().startswith('FUNCTION '):
|
||||
# After not_map normalization, NOT = has been converted to <>
|
||||
func_match = re.match(
|
||||
r'^FUNCTION\s+(\w+)\(([^)]*)\)\s*(>=|<=|<>|>|<|=)\s*(.*)$',
|
||||
r'^FUNCTION\s+(\w+)\s*\(([^)]*)\)\s*(>=|<=|<>|>|<|=)\s*(.*)$',
|
||||
normalized, re.IGNORECASE
|
||||
)
|
||||
if func_match:
|
||||
@@ -244,6 +249,11 @@ def parse_compound_condition(text, fields=None):
|
||||
# Leaf condition
|
||||
parsed = parse_single_condition(text, fields)
|
||||
if parsed:
|
||||
if len(parsed) == 4:
|
||||
# class condition (field, 'IS', CLASS, want): represent negation via CondNot
|
||||
field, op, cls, want = parsed
|
||||
leaf = CondLeaf(field, op, cls)
|
||||
return leaf if want else CondNot(leaf)
|
||||
return CondLeaf(*parsed)
|
||||
return None
|
||||
|
||||
@@ -332,12 +342,69 @@ def mcdc_sets(tree, fields=None):
|
||||
|
||||
# ── 值计算 ──
|
||||
|
||||
def evaluate_class_value(value, class_name):
|
||||
"""COBOL class-condition evaluation on a raw field value.
|
||||
|
||||
Returns True if `value` belongs to `class_name`.
|
||||
class_name ∈ {NUMERIC, ALPHABETIC, ALPHABETIC-UPPER, ALPHABETIC-LOWER,
|
||||
POSITIVE, NEGATIVE, ZERO}. Empty / all-space values never
|
||||
satisfy the ALPHABETIC* / NUMERIC classes (COBOL class-condition semantics).
|
||||
"""
|
||||
s = str(value)
|
||||
cls = str(class_name).upper()
|
||||
if cls == 'ALPHABETIC-UPPER':
|
||||
return bool(s) and all('A' <= ch <= 'Z' for ch in s)
|
||||
if cls == 'ALPHABETIC-LOWER':
|
||||
return bool(s) and all('a' <= ch <= 'z' for ch in s)
|
||||
if cls == 'ALPHABETIC':
|
||||
return bool(s) and all(ch.isalpha() for ch in s)
|
||||
if cls == 'NUMERIC':
|
||||
return bool(s) and all('0' <= ch <= '9' for ch in s)
|
||||
if cls in ('POSITIVE', 'NEGATIVE', 'ZERO'):
|
||||
try:
|
||||
n = float(str(s).strip())
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
if cls == 'POSITIVE':
|
||||
return n > 0
|
||||
if cls == 'NEGATIVE':
|
||||
return n < 0
|
||||
return n == 0
|
||||
return False
|
||||
|
||||
|
||||
def satisfying_value(field_info: dict, operator: str, value, want_true: bool) -> str:
|
||||
ftype = field_info.get('type', 'unknown')
|
||||
digits = field_info.get('digits', 0)
|
||||
decimal = field_info.get('decimal', 0)
|
||||
total = digits + decimal
|
||||
|
||||
# COBOL class-condition constraint: generate a value that is / is not in the class
|
||||
if operator == 'IS':
|
||||
cls = str(value).upper()
|
||||
length = field_info.get('length', 1)
|
||||
if cls in ('ALPHABETIC', 'ALPHABETIC-UPPER'):
|
||||
if want_true:
|
||||
return 'A' * length
|
||||
return ('A' * (length - 1) + '0') if length > 1 else '0'
|
||||
if cls == 'ALPHABETIC-LOWER':
|
||||
if want_true:
|
||||
return 'a' * length
|
||||
return ('a' * (length - 1) + '0') if length > 1 else '0'
|
||||
if cls == 'NUMERIC':
|
||||
if want_true:
|
||||
return '0' * length
|
||||
return ('0' * max(length - 1, 1) + 'A') if length > 1 else 'A'
|
||||
if cls == 'ZERO':
|
||||
return '0' * max(total, 1)
|
||||
if cls == 'POSITIVE':
|
||||
return str(1).zfill(total) if total else '1'
|
||||
if cls == 'NEGATIVE':
|
||||
if field_info.get('signed'):
|
||||
return '-' + '1'.zfill(max(total - 1, 1))
|
||||
return '0' * max(total, 1)
|
||||
return '0'.zfill(max(total, 1))
|
||||
|
||||
if ftype == 'numeric':
|
||||
try:
|
||||
val_str = str(value)
|
||||
@@ -376,13 +443,31 @@ def satisfying_value(field_info: dict, operator: str, value, want_true: bool) ->
|
||||
|
||||
elif ftype in ('alphanumeric', 'alphabetic'):
|
||||
length = field_info.get('length', 1)
|
||||
base_chr = value[0].upper() if isinstance(value, str) and value else 'A'
|
||||
# 图式常量解析:SPACES/SPACE→空格、ZERO(S)/ZEROES→'0'、LOW-VALUES→\x00、
|
||||
# HIGH-VALUES→\xff、QUOTE(S)→"'"。否则按值首字符作基础字符。
|
||||
_uv = str(value).strip().upper() if isinstance(value, str) else ''
|
||||
_FIG = {
|
||||
'SPACE': ' ', 'SPACES': ' ',
|
||||
'ZERO': '0', 'ZEROS': '0', 'ZEROES': '0',
|
||||
'LOW-VALUE': '\x00', 'LOW-VALUES': '\x00',
|
||||
'HIGH-VALUE': '\xff', 'HIGH-VALUES': '\xff',
|
||||
'QUOTE': "'", 'QUOTES': "'",
|
||||
}
|
||||
base_chr = _FIG.get(_uv) or (value[0].upper() if isinstance(value, str) and value else 'A')
|
||||
# 精确值目标:字面值(或图式常量)截断/右对齐到字段长。
|
||||
# 例如 (= '0001', want=True) / (<> '0001', want=False) 应得到 '0001',
|
||||
# 而非按首字符填充的 '0000'(旧行为错误)。
|
||||
if _uv in _FIG:
|
||||
_exact = (_FIG[_uv] * length)[:length]
|
||||
else:
|
||||
_exact = str(value)[:length].ljust(length)
|
||||
_other = chr(65 + (ord(base_chr) - 64) % 26)
|
||||
_diff = _other.ljust(length, _other)
|
||||
if want_true:
|
||||
if operator in ('=', '=='):
|
||||
return base_chr.ljust(length, base_chr)
|
||||
return _exact
|
||||
elif operator in ('<>', '!='):
|
||||
other = chr(65 + (ord(base_chr) - 64) % 26)
|
||||
return other.ljust(length, other)
|
||||
return _diff
|
||||
elif operator == '>':
|
||||
sv = str(value)[:length].ljust(length)
|
||||
chars = list(sv)
|
||||
@@ -403,10 +488,9 @@ def satisfying_value(field_info: dict, operator: str, value, want_true: bool) ->
|
||||
return ''.join(chars)
|
||||
else:
|
||||
if operator in ('=', '=='):
|
||||
other = chr(65 + (ord(base_chr) - 64) % 26)
|
||||
return other.ljust(length, other)
|
||||
return _diff
|
||||
elif operator in ('<>', '!='):
|
||||
return base_chr.ljust(length, base_chr)
|
||||
return _exact
|
||||
elif operator in ('>', '<'):
|
||||
return str(value)[:length].ljust(length)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user