vietc/scripts/gen_test_cases.py

159 lines
6.3 KiB
Python

#!/usr/bin/env python3
"""Generate 1000+ Vietnamese IME test cases and produce a Rust test file."""
import json
import subprocess
import sys
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, ".."))
EXAMPLE_PATH = os.path.join(PROJECT_DIR, "target", "release", "examples", "gen_tests")
def build_generator():
"""Build the Rust test case generator."""
subprocess.run(
["cargo", "run", "--example", "gen_tests", "--release"],
cwd=PROJECT_DIR,
capture_output=True,
check=True,
)
def run_generator():
"""Run the generator and return JSON lines."""
result = subprocess.run(
[EXAMPLE_PATH],
capture_output=True,
text=True,
timeout=30,
)
cases = []
for line in result.stdout.strip().split("\n"):
line = line.strip()
if not line or line.startswith("Generated"):
continue
try:
cases.append(json.loads(line))
except json.JSONDecodeError:
continue
return cases
def generate_rust_test(cases, output_path):
"""Generate a Rust test file with all cases."""
from datetime import datetime
lines = []
lines.append("/// Auto-generated from gen_tests example")
lines.append(f"/// Generated: {datetime.now().isoformat()}")
lines.append(f"/// Total cases: {len(cases)}")
lines.append("")
lines.append("use vietc_engine::{Engine, EngineEvent, InputMethod};")
lines.append("")
lines.append("fn get_display(events: &[EngineEvent]) -> String {")
lines.append(" let mut display = String::new();")
lines.append(" for ev in events {")
lines.append(" match ev {")
lines.append(" EngineEvent::Flush(text) => {")
lines.append(" if !display.ends_with(text) { display.push_str(text); }")
lines.append(" }")
lines.append(" EngineEvent::Insert(text) => display.push_str(text),")
lines.append(" EngineEvent::Replace { backspaces, insert } => {")
lines.append(" for _ in 0..*backspaces { display.pop(); }")
lines.append(" display.push_str(insert);")
lines.append(" }")
lines.append(" EngineEvent::AutoRestore(word) => {")
lines.append(" for _ in 0..word.len() { display.pop(); }")
lines.append(" display.push_str(word);")
lines.append(" }")
lines.append(" EngineEvent::UndoTones { backspaces, restored } => {")
lines.append(" for _ in 0..*backspaces { display.pop(); }")
lines.append(" display.push_str(restored);")
lines.append(" }")
lines.append(" }")
lines.append(" }")
lines.append(" display")
lines.append("}")
lines.append("")
lines.append("fn process_input(e: &mut Engine, input: &str) -> Vec<EngineEvent> {")
lines.append(" let mut events = Vec::new();")
lines.append(" for ch in input.chars() {")
lines.append(" if let Some(ev) = e.process_key(ch) { events.push(ev); }")
lines.append(" }")
lines.append(" events")
lines.append("}")
lines.append("")
lines.append(f"const TEST_CASES: &[(&str, &str, &str)] = &[")
for c in cases:
input_escaped = c["i"].replace("\\", "\\\\").replace("\"", "\\\"")
expected_escaped = c["e"].replace("\\", "\\\\").replace("\"", "\\\"")
mode = c["m"]
lines.append(f' ("{input_escaped}", "{expected_escaped}", "{mode}"),')
lines.append("];")
lines.append("")
lines.append("#[test]")
lines.append("fn test_generated_bulk() {")
lines.append(" let mut failures = Vec::new();")
lines.append(" for (i, &(input, expected, mode)) in TEST_CASES.iter().enumerate() {")
lines.append(" let im = match mode {")
lines.append(' "telex" => InputMethod::Telex,')
lines.append(' "vni" => InputMethod::Vni,')
lines.append(" _ => unreachable!(),")
lines.append(" };")
lines.append(" let mut e = Engine::new(im);")
lines.append(" let actual = get_display(&process_input(&mut e, input));")
lines.append(" if actual != expected {")
lines.append(" failures.push(format!(\"[{}] '{}' -> '{}' (expected '{}')\", i, input, actual, expected));")
lines.append(" }")
lines.append(" }")
lines.append(" if !failures.is_empty() {")
lines.append(" for f in &failures[..10.min(failures.len())] {")
lines.append(' eprintln!("{}", f);')
lines.append(" }")
lines.append(' panic!("{}/{} tests FAILED", failures.len(), TEST_CASES.len());')
lines.append(" }")
lines.append(' eprintln!("All {} generated tests PASSED", TEST_CASES.len());')
lines.append("}")
lines.append("")
with open(output_path, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"Generated {output_path} with {len(cases)} test cases")
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--output", default="engine/tests/generated_bulk.rs",
help="Output Rust test file path")
args = parser.parse_args()
output_path = os.path.join(PROJECT_DIR, args.output)
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Run generator & capture cases
try:
result = subprocess.run(
["cargo", "run", "--example", "gen_tests", "--release"],
cwd=PROJECT_DIR,
capture_output=True,
text=True,
timeout=120,
)
cases = []
for line in result.stdout.strip().split("\n"):
line = line.strip()
if not line or line.startswith("Generated"):
continue
try:
cases.append(json.loads(line))
except json.JSONDecodeError:
continue
except subprocess.TimeoutExpired:
print("ERROR: Generator timed out", file=sys.stderr)
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"ERROR: Generator failed: {e.stderr}", file=sys.stderr)
sys.exit(1)
print(f"Captured {len(cases)} test cases from generator")
generate_rust_test(cases, output_path)