rabukasim / tools /verify /test_parity_ir_bytecode_readable.py
trioskosmos's picture
Upload folder using huggingface_hub
463f868 verified
#!/usr/bin/env python3
"""
Parity tests for IR <-> bytecode <-> readable decode.
This test suite ensures that:
1. Abilities compile to bytecode correctly
2. Semantic forms (IR) are built consistently from abilities
3. Bytecode can be decoded to human-readable form
4. All three representations (IR, bytecode, readable) are consistent
These tests catch layout drift early: if bytecode layout changes but
semantic form or decoder don't update, tests will fail immediately.
Layout versioning is verified: tests ensure version markers are present
and consistent across compiled output.
"""
import json
import os
import sys
from typing import Any, Dict, List, Tuple
# Add project root to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
from engine.models.ability import Ability
from engine.models.ability_ir import (
BYTECODE_LAYOUT_NAME,
BYTECODE_LAYOUT_VERSION,
SEMANTIC_FORM_VERSION,
AbilityIR,
)
from engine.models.bytecode_readable import (
CONDITION_NAMES,
COST_NAMES,
OPCODE_NAMES,
TRIGGER_NAMES,
decode_bytecode,
opcode_name,
trigger_name,
)
class ParityTestResult:
"""Container for parity test outcomes."""
def __init__(self, test_name: str):
self.test_name = test_name
self.passed = False
self.errors: List[str] = []
self.warnings: List[str] = []
def add_error(self, msg: str):
self.errors.append(msg)
def add_warning(self, msg: str):
self.warnings.append(msg)
def set_passed(self):
self.passed = True
def summary(self) -> str:
status = "[PASS]" if self.passed else "[FAIL]"
lines = [f"{status} {self.test_name}"]
if self.errors:
for err in self.errors:
lines.append(f" ERROR: {err}")
if self.warnings:
for warn in self.warnings:
lines.append(f" WARNING: {warn}")
return "\n".join(lines)
def test_ability_compilation(ability: Ability) -> ParityTestResult:
"""Test that an ability compiles to bytecode without errors."""
result = ParityTestResult(f"Ability compilation: {ability.raw_text[:40]}")
try:
ability.compile()
if not hasattr(ability, "bytecode") or ability.bytecode is None:
result.add_error("Ability.compile() did not produce bytecode")
return result
if not isinstance(ability.bytecode, list):
result.add_error(
f"Bytecode is {type(ability.bytecode)}, expected list"
)
return result
if len(ability.bytecode) == 0:
result.add_error("Bytecode is empty")
return result
# Bytecode should be 5-word chunks
if len(ability.bytecode) % 5 != 0:
result.add_warning(
f"Bytecode length {len(ability.bytecode)} is not multiple of 5"
)
result.set_passed()
except Exception as e:
result.add_error(f"Compilation raised exception: {e}")
return result
def test_semantic_form_building(ability: Ability) -> ParityTestResult:
"""Test that semantic form (IR) builds successfully from ability."""
result = ParityTestResult(f"Semantic form building: {ability.raw_text[:40]}")
try:
# Semantic form must be built after compilation
if not hasattr(ability, "bytecode") or ability.bytecode is None:
result.add_error("Cannot build semantic form without bytecode")
return result
semantic_form_dict = ability.build_semantic_form()
if semantic_form_dict is None:
result.add_error("build_semantic_form() returned None")
return result
if not isinstance(semantic_form_dict, dict):
result.add_error(
f"Semantic form is {type(semantic_form_dict)}, expected dict"
)
return result
# Check required fields
required_fields = [
"semantic_version",
"bytecode_layout_version",
"bytecode_layout_name",
"trigger",
"effects",
"conditions",
"costs",
]
for field in required_fields:
if field not in semantic_form_dict:
result.add_error(f"Semantic form missing field: {field}")
return result
# Verify version markers
if semantic_form_dict["semantic_version"] != SEMANTIC_FORM_VERSION:
result.add_error(
f"semantic_version mismatch: "
f"{semantic_form_dict['semantic_version']} != {SEMANTIC_FORM_VERSION}"
)
return result
if semantic_form_dict["bytecode_layout_version"] != BYTECODE_LAYOUT_VERSION:
result.add_error(
f"bytecode_layout_version mismatch: "
f"{semantic_form_dict['bytecode_layout_version']} != {BYTECODE_LAYOUT_VERSION}"
)
return result
if semantic_form_dict["bytecode_layout_name"] != BYTECODE_LAYOUT_NAME:
result.add_error(
f"bytecode_layout_name mismatch: "
f"{semantic_form_dict['bytecode_layout_name']} != {BYTECODE_LAYOUT_NAME}"
)
return result
result.set_passed()
except Exception as e:
result.add_error(f"Semantic form building raised exception: {e}")
return result
def test_bytecode_decodable(
bytecode: List[int], ability_desc: str
) -> ParityTestResult:
"""Test that bytecode can be decoded to readable form."""
result = ParityTestResult(f"Bytecode decodable: {ability_desc[:40]}")
try:
readable = decode_bytecode(bytecode)
if readable is None or readable == "":
result.add_error("Bytecode decoding returned empty/None")
return result
if "LEGEND" not in readable:
result.add_warning("Decoded bytecode missing legend section")
result.set_passed()
except Exception as e:
result.add_error(f"Bytecode decoding raised exception: {e}")
return result
def test_bytecode_chunk_structure(
bytecode: List[int], ability_desc: str
) -> ParityTestResult:
"""Test that bytecode chunks are valid 5-word structures."""
result = ParityTestResult(f"Bytecode structure valid: {ability_desc[:40]}")
try:
# All chunks should be 5 words
if len(bytecode) % 5 != 0:
result.add_error(
f"Bytecode length {len(bytecode)} not multiple of 5"
)
return result
# Break into 5-word chunks
for i in range(0, len(bytecode), 5):
chunk = bytecode[i : i + 5]
if len(chunk) != 5:
result.add_error(
f"Chunk at offset {i} has length {len(chunk)}, "
f"expected 5"
)
return result
op, val, attr_low, attr_high, slot = chunk
# Opcode should be decodable
if op not in OPCODE_NAMES and op < 1000:
# Some opcodes may legitimately not be in OPCODE_NAMES if dynamic
if op >= 1000:
# Negated opcode
base_op = op - 1000
if base_op not in OPCODE_NAMES:
result.add_warning(
f"Chunk {i//5}: opcode {op} not found in "
f"OPCODE_NAMES"
)
result.set_passed()
except Exception as e:
result.add_error(f"Bytecode structure validation raised exception: {e}")
return result
def test_naming_consistency() -> ParityTestResult:
"""Test that naming dicts are consistent and have no conflicts."""
result = ParityTestResult("Naming consistency")
try:
# Check for duplicate keys (shouldn't happen, but catch edge cases)
all_keys = set()
for key in OPCODE_NAMES.keys():
if key in all_keys:
result.add_error(f"Duplicate key in OPCODE_NAMES: {key}")
return result
all_keys.add(key)
# Check that trigger names are populated
if not TRIGGER_NAMES:
result.add_error("TRIGGER_NAMES is empty")
return result
# Check that at least some conditions exist
if not CONDITION_NAMES:
result.add_error("CONDITION_NAMES is empty")
return result
# Test opcode_name() function works
for key in list(OPCODE_NAMES.keys())[:5]: # Test first 5
name = opcode_name(key)
if name is None or name == "":
result.add_error(f"opcode_name({key}) returned empty/None")
return result
# Test trigger_name() function works
for key in list(TRIGGER_NAMES.keys())[:5]: # Test first 5
name = trigger_name(key)
if name is None or name == "":
result.add_error(f"trigger_name({key}) returned empty/None")
return result
result.set_passed()
except Exception as e:
result.add_error(f"Naming consistency check raised exception: {e}")
return result
def test_compiled_json_structure(compiled_json_path: str) -> List[ParityTestResult]:
"""Test that compiled JSON file has proper version markers."""
results = []
result = ParityTestResult(f"Compiled JSON structure: {compiled_json_path}")
try:
if not os.path.exists(compiled_json_path):
result.add_error(f"Compiled JSON not found: {compiled_json_path}")
results.append(result)
return results
with open(compiled_json_path, "r", encoding="utf-8") as f:
data = json.load(f)
if "meta" not in data:
result.add_error("Compiled JSON missing 'meta' section")
results.append(result)
return results
meta = data["meta"]
required_meta_fields = [
"bytecode_layout_version",
"bytecode_layout_name",
"semantic_form_version",
"semantic_form_enabled",
]
for field in required_meta_fields:
if field not in meta:
result.add_error(f"Meta section missing field: {field}")
results.append(result)
return results
# Verify meta values match constants
if meta["bytecode_layout_version"] != BYTECODE_LAYOUT_VERSION:
result.add_error(
f"Meta bytecode_layout_version {meta['bytecode_layout_version']} "
f"!= {BYTECODE_LAYOUT_VERSION}"
)
results.append(result)
return results
if meta["semantic_form_version"] != SEMANTIC_FORM_VERSION:
result.add_error(
f"Meta semantic_form_version {meta['semantic_form_version']} "
f"!= {SEMANTIC_FORM_VERSION}"
)
results.append(result)
return results
result.set_passed()
results.append(result)
# Now check individual card entries
card_check_result = ParityTestResult("Compiled JSON card entries have version markers")
# Check for cards in member_db, live_db, energy_db
card_sources = [
("member_db", data.get("member_db", {})),
("live_db", data.get("live_db", {})),
("energy_db", data.get("energy_db", {})),
]
card_count_checked = 0
for source_name, source_data in card_sources:
if card_count_checked >= 10:
break
for card_no, card_data in source_data.items():
if card_count_checked >= 10:
break
abilities = card_data.get("abilities", [])
for ability in abilities:
if "semantic_form" in ability:
sf = ability["semantic_form"]
if "semantic_version" not in sf:
card_check_result.add_error(
f"{source_name}[{card_no}] ability missing "
f"semantic_version in semantic_form"
)
break
if "bytecode_layout_version" not in sf:
card_check_result.add_error(
f"{source_name}[{card_no}] ability missing "
f"bytecode_layout_version in semantic_form"
)
break
card_count_checked += 1
if not card_check_result.errors:
card_check_result.set_passed()
results.append(card_check_result)
except Exception as e:
result.add_error(f"JSON structure check raised exception: {e}")
results.append(result)
return results
def run_parity_tests(compiled_json_path: str = None) -> Tuple[int, int]:
"""
Run all parity tests and report results.
Returns: (passed_count, failed_count)
"""
results = []
print("\n" + "=" * 70)
print("PARITY TESTS: IR <-> Bytecode <-> Readable Decode")
print("=" * 70)
# Test 1: Naming consistency
print("\n[1] Testing naming consistency...")
results.append(test_naming_consistency())
# Test 2: Load a few sample abilities to test
print("[2] Testing with sample abilities...")
try:
# This is a simplified test - in production you'd load from compiled data
# For now, we just test the framework
sample_abilities = [
# Example ability (simplified for testing)
{
"trigger": "ON_PLAY",
"costs": [],
"conditions": [],
"effects": [],
"instructions": [],
"raw_text": "Test ability",
}
]
# Note: This is a placeholder. In real use, you'd load actual abilities
# from the compiled JSON or from parsing
print(" (Skipping real ability tests - use compiled JSON to validate)")
except Exception as e:
print(f" ERROR loading sample abilities: {e}")
# Test 3: Compiled JSON structure
if compiled_json_path is None:
# Try default locations
possible_paths = [
"data/cards_compiled.json",
"engine/data/cards_compiled.json",
]
for path in possible_paths:
if os.path.exists(path):
compiled_json_path = path
break
if compiled_json_path and os.path.exists(compiled_json_path):
print(f"[3] Testing compiled JSON structure ({compiled_json_path})...")
results.extend(test_compiled_json_structure(compiled_json_path))
else:
print("[3] Skipping compiled JSON tests (file not found)")
# Print results
print("\n" + "=" * 70)
print("RESULTS")
print("=" * 70)
passed = 0
failed = 0
for result in results:
print(result.summary())
if result.passed:
passed += 1
else:
failed += 1
print("\n" + "=" * 70)
print(f"Summary: {passed} passed, {failed} failed")
print("=" * 70 + "\n")
return passed, failed
if __name__ == "__main__":
passed, failed = run_parity_tests()
sys.exit(0 if failed == 0 else 1)