#!/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// subdirectory generated_file = self.work_dir / test_filename / f"{test_filename}{suffix}.json" # Reference file should be in testdata/.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())