Files
kabellaengen/lib/run_tests.py
T

483 lines
19 KiB
Python

#!/usr/bin/env python3
"""
Test runner for kabellaengen project
Executes getexdraw.bat for all DXF files in testdata directory and compares outputs
"""
import os
import sys
import json
import subprocess
import glob
from pathlib import Path
import difflib
import argparse
class TestResult:
def __init__(self, test_name):
self.test_name = test_name
self.passed = False
self.error_msg = ""
self.differences = []
class TestRunner:
def __init__(self, create_references=False, mode='getexdraw', clean=False, target_file=None):
# Use environment variables set by setenv.bat
self.project_root = Path(os.environ['PROJECT'])
self.testdata_dir = Path(os.environ.get('PROJECT_TEST', self.project_root / "testdata"))
self.work_dir = Path(os.environ.get('PROJECT_WORK', self.project_root / "work"))
self.bin_dir = Path(os.environ.get('PROJECT_BIN', self.project_root / "bin"))
self.lib_dir = Path(os.environ.get('PROJECT_LIB', self.project_root / "lib"))
self.cfg_dir = Path(os.environ.get('PROJECT_CFG', self.project_root / "cfg"))
self.getexdraw_bat = self.bin_dir / "getexdraw.bat"
self.ioconverter_bat = self.bin_dir / "ioconverter.bat"
self.translate_py = self.lib_dir / "translate.py"
self.test_results = []
self.failed_tests = []
self.create_references = create_references
self.mode = mode
self.clean = clean
self.target_file = target_file
def find_test_files(self):
"""Find DXF files in testdata directory - all files or specific file"""
if self.target_file:
# Remove .dxf suffix if present to avoid double extension
clean_target = self.target_file
if clean_target.endswith('.dxf'):
clean_target = clean_target[:-4]
# Check if specific file exists
target_dxf = self.testdata_dir / f"{clean_target}.dxf"
if target_dxf.exists():
return [clean_target]
else:
print(f"Error: Test file {target_dxf} not found")
return []
else:
# Return all DXF files
dxf_files = list(self.testdata_dir.glob("*.dxf"))
return [f.stem for f in dxf_files] # Return filenames without extension
def run_getexdraw(self, test_filename):
"""Execute getexdraw.bat for given test file"""
dxf_file = self.testdata_dir / f"{test_filename}.dxf"
if not dxf_file.exists():
return False, f"Test file {dxf_file} not found"
try:
# Change to bin directory for execution (where batch scripts expect to run from)
os.chdir(self.bin_dir)
# Execute getexdraw.bat with automated input to handle pause
cmd = [str(self.getexdraw_bat), str(dxf_file)]
# Send newline to handle the pause command
result = subprocess.run(cmd, capture_output=True, text=True, shell=True, input="\n")
# Check if the expected output files were created instead of relying on return code
# The batch script always exits with code 1 due to interactive design, but files are created
expected_positions_file = self.work_dir / test_filename / f"{test_filename}_positionsdraw.json"
expected_todraw_file = self.work_dir / test_filename / f"{test_filename}_todraw.json"
expected_errors_file = self.work_dir / test_filename / f"{test_filename}_errors.json"
# Success case: both normal files exist
if expected_positions_file.exists() and expected_todraw_file.exists():
return True, "Success"
# Error case: positions file missing but errors file exists
elif not expected_positions_file.exists() and expected_errors_file.exists():
return True, "Success (with errors)"
else:
missing_files = []
if not expected_positions_file.exists() and not expected_errors_file.exists():
missing_files.append(f"{test_filename}_positionsdraw.json or {test_filename}_errors.json")
if not expected_todraw_file.exists():
missing_files.append(str(expected_todraw_file))
return False, f"getexdraw.bat failed - expected output files not found: {', '.join(missing_files)}\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
except Exception as e:
return False, f"Exception during execution: {str(e)}"
def run_ioconverter(self, test_filename):
"""Execute ioconverter.bat for given test file"""
dxf_file = self.testdata_dir / f"{test_filename}.dxf"
if not dxf_file.exists():
return False, f"Test file {dxf_file} not found"
try:
# Change to bin directory for execution (where batch scripts expect to run from)
os.chdir(self.bin_dir)
# Execute ioconverter.bat with automated input to handle pause
cmd = [str(self.ioconverter_bat), str(dxf_file)]
# Send newline to handle the pause command
result = subprocess.run(cmd, capture_output=True, text=True, shell=True, input="\n")
# Check if the expected output files were created
expected_positions_file = self.work_dir / test_filename / f"{test_filename}_positionsconv.json"
expected_errors_file = self.work_dir / test_filename / f"{test_filename}_errors.json"
# Success case: positions file exists
if expected_positions_file.exists():
return True, "Success"
# Error case: positions file missing but errors file exists
elif expected_errors_file.exists():
return True, "Success (with errors)"
else:
return False, f"ioconverter.bat failed - expected output file not found: {expected_positions_file} or {expected_errors_file}\nSTDOUT: {result.stdout}\nSTDERR: {result.stderr}"
except Exception as e:
return False, f"Exception during execution: {str(e)}"
def run_translate(self, test_filename):
"""Execute translate.py for given test file with three variants: raw, cs, multilang"""
dxf_file = self.testdata_dir / f"{test_filename}.dxf"
if not dxf_file.exists():
return False, f"Test file {dxf_file} not found"
try:
# Set up environment for translate.py
env = os.environ.copy()
env['PROJECT_TRANSLATION'] = str(self.cfg_dir / "translation")
# Variant 1: Raw extraction (no translation)
cmd_raw = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_raw'
]
result_raw = subprocess.run(cmd_raw, capture_output=True, text=True, env=env)
# Variant 2: CS translation (single language)
cmd_cs = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_cs',
'--translate', 'CS'
]
result_cs = subprocess.run(cmd_cs, capture_output=True, text=True, env=env)
# Variant 3: Multilang translation (CS,EN)
cmd_multilang = [
sys.executable,
str(self.translate_py),
'--extract',
'--filename', str(dxf_file),
'--export-type', 'json',
'--outname', f'{test_filename}_multilang',
'--translate', 'CS,EN'
]
result_multilang = subprocess.run(cmd_multilang, capture_output=True, text=True, env=env)
# Check if all expected output files were created
expected_raw = self.work_dir / f"{test_filename}_raw.json"
expected_cs = self.work_dir / f"{test_filename}_cs.json"
expected_multilang = self.work_dir / f"{test_filename}_multilang.json"
missing_files = []
if not expected_raw.exists():
missing_files.append(f"{test_filename}_raw.json")
if not expected_cs.exists():
missing_files.append(f"{test_filename}_cs.json")
if not expected_multilang.exists():
missing_files.append(f"{test_filename}_multilang.json")
if missing_files:
error_msg = f"translate.py failed - expected output files not found: {', '.join(missing_files)}"
if result_raw.returncode != 0:
error_msg += f"\n\nRaw variant STDOUT:\n{result_raw.stdout}\nSTDERR:\n{result_raw.stderr}"
if result_cs.returncode != 0:
error_msg += f"\n\nCS variant STDOUT:\n{result_cs.stdout}\nSTDERR:\n{result_cs.stderr}"
if result_multilang.returncode != 0:
error_msg += f"\n\nMultilang variant STDOUT:\n{result_multilang.stdout}\nSTDERR:\n{result_multilang.stderr}"
return False, error_msg
else:
return True, "Success"
except Exception as e:
return False, f"Exception during execution: {str(e)}"
def cleanup_test_files(self, test_filename):
"""Remove the work folder for the given test"""
if not self.clean:
return
test_work_dir = self.work_dir / test_filename
if test_work_dir.exists():
try:
import shutil
shutil.rmtree(test_work_dir)
print(f" [cleaned up {test_work_dir}]")
except Exception as e:
print(f" [cleanup failed: {e}]")
def compare_json_files(self, test_filename):
"""Compare generated JSON files with reference JSON files"""
all_diffs = []
# Check what files actually exist and determine comparison strategy based on mode
errors_file = self.work_dir / test_filename / f"{test_filename}_errors.json"
if self.mode == 'translation':
# For translation mode: compare _raw, _cs, and _multilang variants
file_suffixes = ['_raw', '_cs', '_multilang']
# Translation files are directly in work_dir, not in subdirectory
elif self.mode == 'ioconverter':
# For ioconverter mode: check for _positionsconv.json
positions_file = self.work_dir / test_filename / f"{test_filename}_positionsconv.json"
# Determine which files to compare based on what exists
if positions_file.exists():
# Normal case: compare _positionsconv only
file_suffixes = ['_positionsconv']
elif errors_file.exists():
# Error case: compare _errors only
file_suffixes = ['_errors']
else:
return False, f"No generated files found for {test_filename}", []
else:
# For getexdraw mode: check for _positionsdraw.json and _todraw.json
positions_file = self.work_dir / test_filename / f"{test_filename}_positionsdraw.json"
todraw_file = self.work_dir / test_filename / f"{test_filename}_todraw.json"
# Determine which files to compare based on what exists
if positions_file.exists():
# Normal case: compare _positionsdraw and _todraw
file_suffixes = ['_positionsdraw', '_todraw']
elif errors_file.exists():
# Error case: compare _errors only (no _todraw expected when errors occur)
file_suffixes = ['_errors']
else:
return False, f"No generated files found for {test_filename}", []
for suffix in file_suffixes:
# For translation mode, files are directly in work_dir
if self.mode == 'translation':
generated_file = self.work_dir / f"{test_filename}{suffix}.json"
else:
# For other modes, files are in work/<testfilename>/ subdirectory
generated_file = self.work_dir / test_filename / f"{test_filename}{suffix}.json"
# Reference file should be in testdata/<testfilename><suffix>.json
reference_file = self.testdata_dir / f"{test_filename}{suffix}.json"
if not generated_file.exists():
return False, f"Generated file {generated_file} not found", []
if not reference_file.exists():
if self.create_references:
# Copy generated file as reference
import shutil
shutil.copy2(generated_file, reference_file)
print(f"\nCreated reference file: {reference_file}")
continue
else:
return False, f"Reference file {reference_file} not found", []
try:
# Load and compare JSON files
with open(generated_file, 'r', encoding='utf-8') as f:
generated_data = json.load(f)
with open(reference_file, 'r', encoding='utf-8') as f:
reference_data = json.load(f)
if generated_data != reference_data:
# Generate diff for debugging
generated_str = json.dumps(generated_data, indent=2, sort_keys=True)
reference_str = json.dumps(reference_data, indent=2, sort_keys=True)
diff = list(difflib.unified_diff(
reference_str.splitlines(keepends=True),
generated_str.splitlines(keepends=True),
fromfile=str(reference_file),
tofile=str(generated_file),
n=3
))
all_diffs.extend([f"\n=== Differences in {suffix}.json ==="])
all_diffs.extend(diff)
except json.JSONDecodeError as e:
return False, f"JSON decode error in {suffix}.json: {str(e)}", []
except Exception as e:
return False, f"Exception during comparison of {suffix}.json: {str(e)}", []
if all_diffs:
return False, "Files differ", all_diffs
else:
return True, "All files match", []
def run_single_test(self, test_filename):
"""Run a single test case"""
print(f"Running test: {test_filename} ... ", end="")
result = TestResult(test_filename)
# Step 1: Run the appropriate script based on mode
if self.mode == 'translation':
success, msg = self.run_translate(test_filename)
elif self.mode == 'ioconverter':
success, msg = self.run_ioconverter(test_filename)
else:
success, msg = self.run_getexdraw(test_filename)
if not success:
result.error_msg = f"Execution failed: {msg}"
print("FAIL", end="")
self.cleanup_test_files(test_filename)
if not self.clean:
print()
return result
# Step 2: Compare JSON files
success, msg, diff = self.compare_json_files(test_filename)
if not success:
result.error_msg = f"Comparison failed: {msg}"
result.differences = diff
print("FAIL", end="")
self.cleanup_test_files(test_filename)
if not self.clean:
print()
return result
result.passed = True
print("OK", end="")
# Step 3: Cleanup if requested
self.cleanup_test_files(test_filename)
if not self.clean:
print() # Add newline if no cleanup message was printed
return result
def run_all_tests(self):
"""Run all tests and collect results"""
test_files = self.find_test_files()
if not test_files:
if self.target_file:
print(f"Test file {self.target_file}.dxf not found in testdata directory")
else:
print("No test files found in testdata directory")
return False
if self.target_file:
print(f"Running test for file: {self.target_file}")
else:
print(f"Found {len(test_files)} test files")
print("=" * 70)
for test_file in test_files:
result = self.run_single_test(test_file)
self.test_results.append(result)
if not result.passed:
self.failed_tests.append(test_file)
print("=" * 70)
return self.print_summary()
def print_summary(self):
"""Print test summary similar to unittest"""
total_tests = len(self.test_results)
passed_tests = sum(1 for r in self.test_results if r.passed)
failed_tests = total_tests - passed_tests
print(f"\nRan {total_tests} tests in {self.testdata_dir}")
print()
if failed_tests > 0:
print("FAILURES:")
print("=" * 70)
for result in self.test_results:
if not result.passed:
print(f"FAIL: {result.test_name}")
print("-" * 70)
print(result.error_msg)
if result.differences:
print("\nDifferences:")
for line in result.differences[:50]: # Limit diff output
print(line.rstrip())
if len(result.differences) > 50:
print(f"... ({len(result.differences) - 50} more lines)")
print()
print("=" * 70)
print(f"FAILED (failures={failed_tests})")
print(f"\nFailed test files: {', '.join(self.failed_tests)}")
else:
print("OK")
return failed_tests == 0
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(
description="Runs batch scripts for all DXF files in testdata and compares results"
)
parser.add_argument(
'--create-references',
action='store_true',
help='Create reference JSON files from current outputs'
)
parser.add_argument(
'--clean',
action='store_true',
help='Remove work/filename folders after each test run'
)
parser.add_argument(
'--file',
type=str,
help='Run test for specific filename (without .dxf extension)'
)
# Mode selection (mutually exclusive)
mode_group = parser.add_mutually_exclusive_group()
mode_group.add_argument(
'--getexdraw',
action='store_true',
help='Run getexdraw.bat tests (default)'
)
mode_group.add_argument(
'--ioconverter',
action='store_true',
help='Run ioconverter.bat tests'
)
mode_group.add_argument(
'--translation',
action='store_true',
help='Run translate.py tests (extract text with raw, CS, and multilang variants)'
)
args = parser.parse_args()
# Determine mode - default to getexdraw if none is specified
if args.translation:
mode = 'translation'
elif args.ioconverter:
mode = 'ioconverter'
else:
mode = 'getexdraw'
runner = TestRunner(create_references=args.create_references, mode=mode, clean=args.clean, target_file=args.file)
success = runner.run_all_tests()
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())