diff --git a/tests/languages.json b/tests/languages.json deleted file mode 100644 index 351d845a..00000000 --- a/tests/languages.json +++ /dev/null @@ -1,154 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft-07/schema#", - "description": "Language definitions for struct-frame code generation framework", - "version": "1.0", - - "languages": { - "c": { - "name": "C", - "enabled": true, - "code_generation": { - "flag": "--build_c", - "output_path_flag": "--c_path", - "output_dir": "tests/generated/c" - }, - "compilation": { - "enabled": true, - "compiler": "gcc", - "compiler_check": "gcc --version", - "flags": ["-I{generated_dir}", "-o", "{output}", "{source}", "-lm"], - "source_extension": ".c", - "executable_extension": ".exe" - }, - "test_dir": "tests/c", - "build_dir": "tests/c/build" - }, - - "cpp": { - "name": "C++", - "file_prefix": "cpp", - "enabled": true, - "code_generation": { - "flag": "--build_cpp", - "output_path_flag": "--cpp_path", - "output_dir": "tests/generated/cpp" - }, - "compilation": { - "enabled": true, - "compiler": "g++", - "compiler_check": "g++ --version", - "flags": ["-std=c++14", "-I{generated_dir}", "-o", "{output}", "{source}"], - "source_extension": ".cpp", - "executable_extension": ".exe" - }, - "test_dir": "tests/cpp", - "build_dir": "tests/cpp/build" - }, - - "py": { - "name": "Python", - "enabled": true, - "code_generation": { - "flag": "--build_py", - "output_path_flag": "--py_path", - "output_dir": "tests/generated/py" - }, - "compilation": { - "enabled": false - }, - "execution": { - "interpreter": "python", - "source_extension": ".py", - "env": { - "PYTHONPATH": "{generated_dir}:{generated_parent_dir}" - } - }, - "test_dir": "tests/py", - "build_dir": "tests/py/build" - }, - - "ts": { - "name": "TypeScript", - "enabled": true, - "code_generation": { - "flag": "--build_ts", - "output_path_flag": "--ts_path", - "output_dir": "tests/generated/ts" - }, - "compilation": { - "enabled": true, - "compiler": "npx tsc", - "compiler_check": "npx tsc --version", - "command": "npx tsc --project {generated_dir}/tsconfig.json", - "output_dir": "tests/generated/ts/js", - "source_extension": ".ts", - "compiled_extension": ".js", - "working_dir": "tests/ts" - }, - "execution": { - "interpreter": "node", - "script_dir": "tests/generated/ts/js" - }, - "test_dir": "tests/ts", - "build_dir": "tests/ts/build" - }, - - "js": { - "name": "JavaScript", - "enabled": true, - "code_generation": { - "flag": "--build_js", - "output_path_flag": "--js_path", - "output_dir": "tests/generated/js" - }, - "compilation": { - "enabled": false - }, - "execution": { - "interpreter": "node", - "source_extension": ".js", - "script_dir": "tests/generated/js" - }, - "test_dir": "tests/js", - "build_dir": "tests/js/build" - }, - - "gql": { - "name": "GraphQL", - "enabled": true, - "code_generation": { - "flag": "--build_gql", - "output_path_flag": "--gql_path", - "output_dir": "tests/generated/gql" - }, - "compilation": { - "enabled": false - }, - "generation_only": true - }, - - "csharp": { - "name": "C#", - "enabled": true, - "code_generation": { - "flag": "--build_csharp", - "output_path_flag": "--csharp_path", - "output_dir": "tests/generated/csharp" - }, - "compilation": { - "enabled": true, - "compiler": "dotnet", - "compiler_check": "dotnet --version", - "command": "dotnet build \"{test_dir}/StructFrameTests.csproj\" -c Release -o \"{build_dir}\" --verbosity quiet" - }, - "execution": { - "type": "dotnet", - "interpreter": "dotnet", - "source_extension": ".cs", - "run_command": "dotnet run --project \"{test_dir}/StructFrameTests.csproj\" --no-build --verbosity quiet -- {args}" - }, - "test_dir": "tests/csharp", - "build_dir": "tests/csharp/bin/Release/net10.0" - } - } -} diff --git a/tests/languages.py b/tests/languages.py new file mode 100644 index 00000000..74da8022 --- /dev/null +++ b/tests/languages.py @@ -0,0 +1,516 @@ +""" +Language definitions for struct-frame code generation framework. + +This module provides a Language base class with action methods for: +- Compiler checking and compilation +- Interpreter checking and script execution +- Test running + +Each language subclass defines its specific behavior. +""" + +import os +import subprocess +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +# Base language used for cross-platform compatibility testing +BASE_LANGUAGE = "c" + + +class Language: + """Base class for language definitions with action methods.""" + + name: str = "" + enabled: bool = True + generation_only: bool = False + file_prefix: Optional[str] = None + + # Code generation settings + gen_flag: str = "" + gen_output_path_flag: str = "" + gen_output_dir: str = "" + + # Directory settings + test_dir: str = "" + build_dir: str = "" + + # Compilation settings + compiler: Optional[str] = None + compiler_check_cmd: Optional[str] = None + source_extension: str = "" + executable_extension: str = "" + compiled_extension: str = "" + compile_command: Optional[str] = None + compile_working_dir: Optional[str] = None + compile_output_dir: Optional[str] = None + + # Execution settings + interpreter: Optional[str] = None + script_dir: Optional[str] = None + run_command: Optional[str] = None + execution_type: Optional[str] = None + + def __init__(self, project_root: Path): + self.project_root = project_root + self.verbose = False + self.verbose_failure = False + # Initialize mutable attributes per instance to avoid shared state + self.compile_flags: List[str] = self._get_compile_flags() + self.env_vars: Dict[str, str] = self._get_env_vars() + + def _get_compile_flags(self) -> List[str]: + """Override in subclass to provide compile flags.""" + return [] + + def _get_env_vars(self) -> Dict[str, str]: + """Override in subclass to provide environment variables.""" + return {} + + def _run_command(self, command: str, cwd: Optional[Path] = None, + env: Optional[Dict[str, str]] = None, timeout: int = 30) -> Tuple[bool, str, str]: + """Run a shell command and return (success, stdout, stderr).""" + cmd_env = {**os.environ, **(env or {})} + try: + result = subprocess.run( + command, shell=True, cwd=cwd or self.project_root, + capture_output=True, text=True, timeout=timeout, env=cmd_env + ) + success = result.returncode == 0 + if self.verbose: + if result.stdout: + print(f" STDOUT: {result.stdout}") + if result.stderr: + print(f" STDERR: {result.stderr}") + elif self.verbose_failure and not success: + if result.stdout: + print(f" STDOUT: {result.stdout}") + if result.stderr: + print(f" STDERR: {result.stderr}") + return success, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return False, "", "Timeout" + except Exception as e: + return False, "", str(e) + + # ------------------------------------------------------------------------- + # Tool Checking Methods + # ------------------------------------------------------------------------- + + def check_compiler(self) -> Dict[str, Any]: + """Check if the compiler is available. + + Returns dict with keys: name, available, version + """ + if not self.compiler: + return {'name': None, 'available': True, 'version': ''} + + check_cmd = self.compiler_check_cmd or f"{self.compiler} --version" + working_dir = None + if self.compile_working_dir: + working_dir = self.project_root / self.compile_working_dir + + success, stdout, stderr = self._run_command(check_cmd, cwd=working_dir, timeout=5) + + version = "" + if success: + output = stdout or stderr + version = output.strip().split('\n')[0] if output else "" + + return { + 'name': self.compiler, + 'available': success, + 'version': version + } + + def check_interpreter(self) -> Dict[str, Any]: + """Check if the interpreter is available. + + Returns dict with keys: name, available, version + """ + if not self.interpreter: + return {'name': None, 'available': True, 'version': ''} + + success, stdout, stderr = self._run_command(f"{self.interpreter} --version", timeout=5) + + version = "" + if success: + output = stdout or stderr + version = output.strip().split('\n')[0] if output else "" + + return { + 'name': self.interpreter, + 'available': success, + 'version': version + } + + def check_tools(self) -> Dict[str, Any]: + """Check all required tools for this language. + + Returns dict with keys: name, available, compiler, interpreter, reason + """ + info = { + 'name': self.name, + 'available': True, + 'compiler': None, + 'interpreter': None, + } + + if self.generation_only: + info['generation_only'] = True + return info + + # Check compiler if needed + if self.compiler: + compiler_info = self.check_compiler() + info['compiler'] = compiler_info + if not compiler_info['available']: + info['available'] = False + info['reason'] = f"Compiler '{self.compiler}' not found" + + # Check interpreter if needed + if self.interpreter: + interp_info = self.check_interpreter() + info['interpreter'] = interp_info + if not interp_info['available']: + info['available'] = False + info['reason'] = f"Interpreter '{self.interpreter}' not found" + + return info + + # ------------------------------------------------------------------------- + # Compilation Methods + # ------------------------------------------------------------------------- + + def compile(self, sources: List[Path], output: Path, gen_dir: Path) -> bool: + """Compile source files into an executable. + + Args: + sources: List of source file paths + output: Output executable path + gen_dir: Generated code directory (for includes) + + Returns: + True if compilation succeeded + """ + if not self.compiler: + return True # No compilation needed + + for source in sources: + if not source.exists(): + return False + + # Build flags, replacing placeholders + sources_str = ' '.join(f'"{s}"' for s in sources) + flags = [] + for f in self.compile_flags: + f = f.replace('{generated_dir}', str(gen_dir)) + f = f.replace('{output}', str(output)) + if '{source}' in f: + f = sources_str + flags.append(f) + + cmd = f"{self.compiler} {' '.join(flags)}" + return self._run_command(cmd)[0] + + def compile_project(self, test_dir: Path, build_dir: Path, gen_dir: Path) -> bool: + """Compile a project using a custom command (e.g., TypeScript, C#). + + Args: + test_dir: Test source directory + build_dir: Build output directory + gen_dir: Generated code directory + + Returns: + True if compilation succeeded + """ + if not self.compile_command: + return True + + # Format command with placeholders + output_dir = self.project_root / self.compile_output_dir if self.compile_output_dir else build_dir + cmd = self.compile_command.format( + test_dir=test_dir, + build_dir=build_dir, + output_dir=output_dir, + generated_dir=gen_dir + ) + + working_dir = None + if self.compile_working_dir: + working_dir = self.project_root / self.compile_working_dir + + return self._run_command(cmd, cwd=working_dir)[0] + + # ------------------------------------------------------------------------- + # Execution Methods + # ------------------------------------------------------------------------- + + def get_env(self, gen_dir: Path) -> Dict[str, str]: + """Get environment variables for running scripts.""" + if not self.env_vars: + return {} + + gen_parent_dir = gen_dir.parent + env = {} + for k, v in self.env_vars.items(): + v = v.replace('{generated_dir}', str(gen_dir)) + v = v.replace('{generated_parent_dir}', str(gen_parent_dir)) + v = v.replace(':', os.pathsep) + env[k] = v + return env + + def run(self, script_path: Path, args: str = "", cwd: Optional[Path] = None, + gen_dir: Optional[Path] = None) -> bool: + """Run a script or executable. + + Args: + script_path: Path to the script or executable + args: Command-line arguments + cwd: Working directory + gen_dir: Generated code directory (for environment vars) + + Returns: + True if execution succeeded + """ + if not script_path.exists(): + return False + + env = self.get_env(gen_dir) if gen_dir else {} + + # Compiled executable (no interpreter needed) + if self.executable_extension and script_path.suffix == self.executable_extension: + cmd = str(script_path) + if args: + cmd = f"{cmd} {args}" + return self._run_command(cmd, cwd=cwd, env=env)[0] + + # Script with interpreter + if self.interpreter: + cmd = f"{self.interpreter} {script_path}" + if args: + cmd = f"{cmd} {args}" + return self._run_command(cmd, cwd=cwd, env=env)[0] + + return False + + def run_project(self, test_dir: Path, args: str = "") -> bool: + """Run a project using a custom run command (e.g., dotnet run). + + Args: + test_dir: Project directory + args: Command-line arguments + + Returns: + True if execution succeeded + """ + if not self.run_command: + return False + + cmd = self.run_command.format(test_dir=test_dir, args=args) + return self._run_command(cmd, cwd=test_dir)[0] + + # ------------------------------------------------------------------------- + # Utility Methods + # ------------------------------------------------------------------------- + + def get_test_dir(self) -> Path: + """Get the test directory path.""" + return self.project_root / self.test_dir + + def get_build_dir(self) -> Path: + """Get the build directory path.""" + return self.project_root / (self.build_dir or self.test_dir) + + def get_gen_dir(self) -> Path: + """Get the generated code directory path.""" + return self.project_root / self.gen_output_dir + + def get_script_dir(self) -> Optional[Path]: + """Get the script execution directory path.""" + if self.script_dir: + return self.project_root / self.script_dir + return None + +# ============================================================================= +# Language Implementations +# ============================================================================= + +class CLanguage(Language): + """C language configuration and actions.""" + + name = "C" + gen_flag = "--build_c" + gen_output_path_flag = "--c_path" + gen_output_dir = "tests/generated/c" + + compiler = "gcc" + compiler_check_cmd = "gcc --version" + source_extension = ".c" + executable_extension = ".exe" + + test_dir = "tests/c" + build_dir = "tests/c/build" + + def _get_compile_flags(self) -> List[str]: + return ["-I{generated_dir}", "-o", "{output}", "{source}", "-lm"] + + +class CppLanguage(Language): + """C++ language configuration and actions.""" + + name = "C++" + file_prefix = "cpp" + gen_flag = "--build_cpp" + gen_output_path_flag = "--cpp_path" + gen_output_dir = "tests/generated/cpp" + + compiler = "g++" + compiler_check_cmd = "g++ --version" + source_extension = ".cpp" + executable_extension = ".exe" + + test_dir = "tests/cpp" + build_dir = "tests/cpp/build" + + def _get_compile_flags(self) -> List[str]: + return ["-std=c++14", "-I{generated_dir}", "-o", "{output}", "{source}"] + + +class PythonLanguage(Language): + """Python language configuration and actions.""" + + name = "Python" + gen_flag = "--build_py" + gen_output_path_flag = "--py_path" + gen_output_dir = "tests/generated/py" + + interpreter = "python" + source_extension = ".py" + + test_dir = "tests/py" + build_dir = "tests/py/build" + + def _get_env_vars(self) -> Dict[str, str]: + return {"PYTHONPATH": "{generated_dir}:{generated_parent_dir}"} + + +class TypeScriptLanguage(Language): + """TypeScript language configuration and actions.""" + + name = "TypeScript" + gen_flag = "--build_ts" + gen_output_path_flag = "--ts_path" + gen_output_dir = "tests/generated/ts" + + compiler = "npx tsc" + compiler_check_cmd = "npx tsc --version" + compile_command = "npx tsc --project {generated_dir}/tsconfig.json" + compile_output_dir = "tests/generated/ts/js" + compile_working_dir = "tests/ts" + source_extension = ".ts" + compiled_extension = ".js" + + interpreter = "node" + script_dir = "tests/generated/ts/js" + + test_dir = "tests/ts" + build_dir = "tests/ts/build" + + +class JavaScriptLanguage(Language): + """JavaScript language configuration and actions.""" + + name = "JavaScript" + gen_flag = "--build_js" + gen_output_path_flag = "--js_path" + gen_output_dir = "tests/generated/js" + + interpreter = "node" + source_extension = ".js" + script_dir = "tests/generated/js" + + test_dir = "tests/js" + build_dir = "tests/js/build" + + +class GraphQLLanguage(Language): + """GraphQL language configuration (generation only).""" + + name = "GraphQL" + generation_only = True + gen_flag = "--build_gql" + gen_output_path_flag = "--gql_path" + gen_output_dir = "tests/generated/gql" + + +class CSharpLanguage(Language): + """C# language configuration and actions.""" + + name = "C#" + gen_flag = "--build_csharp" + gen_output_path_flag = "--csharp_path" + gen_output_dir = "tests/generated/csharp" + + compiler = "dotnet" + compiler_check_cmd = "dotnet --version" + compile_command = 'dotnet build "{test_dir}/StructFrameTests.csproj" -c Release -o "{build_dir}" --verbosity quiet' + + interpreter = "dotnet" + execution_type = "dotnet" + source_extension = ".cs" + run_command = 'dotnet run --project "{test_dir}/StructFrameTests.csproj" --no-build --verbosity quiet -- {args}' + + test_dir = "tests/csharp" + build_dir = "tests/csharp/bin/Release/net10.0" + + +# ============================================================================= +# Language Registry +# ============================================================================= + +# Language class registry +LANGUAGE_CLASSES = { + "c": CLanguage, + "cpp": CppLanguage, + "py": PythonLanguage, + "ts": TypeScriptLanguage, + "js": JavaScriptLanguage, + "gql": GraphQLLanguage, + "csharp": CSharpLanguage, +} + + +def get_language(lang_id: str, project_root: Path) -> Optional[Language]: + """Get a language instance. + + Args: + lang_id: Language identifier (e.g., 'c', 'py', 'ts') + project_root: Path to the project root directory + + Returns: + Language instance or None if language not found + """ + lang_class = LANGUAGE_CLASSES.get(lang_id) + if lang_class: + return lang_class(project_root) + return None + + +def get_all_language_ids() -> List[str]: + """Get list of all language IDs.""" + return list(LANGUAGE_CLASSES.keys()) + + +def get_enabled_language_ids() -> List[str]: + """Get list of enabled language IDs.""" + dummy_root = Path(".") + return [lang_id for lang_id, lang_class in LANGUAGE_CLASSES.items() + if lang_class(dummy_root).enabled] + + +def get_testable_language_ids() -> List[str]: + """Get list of enabled languages that can run tests (excludes generation_only).""" + dummy_root = Path(".") + return [lang_id for lang_id, lang_class in LANGUAGE_CLASSES.items() + if lang_class(dummy_root).enabled and not lang_class(dummy_root).generation_only] diff --git a/tests/proto/basic_types.proto b/tests/proto/basic_types.proto deleted file mode 100644 index d8ac3485..00000000 --- a/tests/proto/basic_types.proto +++ /dev/null @@ -1,31 +0,0 @@ -package basic_types; - -// Test all basic data types -message BasicTypesMessage { - option msgid = 201; - - // Integer types - int8 small_int = 1; - int16 medium_int = 2; - int32 regular_int = 3; - int64 large_int = 4; - - // Unsigned integer types - uint8 small_uint = 5; - uint16 medium_uint = 6; - uint32 regular_uint = 7; - uint64 large_uint = 8; - - // Floating point types - float single_precision = 9; - double double_precision = 10; - - // Boolean type - bool flag = 11; - - // Fixed string - string device_id = 12 [size=32]; - - // Variable string - string description = 13 [max_size=128]; -} \ No newline at end of file diff --git a/tests/proto/comprehensive_arrays.proto b/tests/proto/comprehensive_arrays.proto deleted file mode 100644 index 2ae94965..00000000 --- a/tests/proto/comprehensive_arrays.proto +++ /dev/null @@ -1,42 +0,0 @@ -package comprehensive_arrays; - -enum Status { - INACTIVE = 0; - ACTIVE = 1; - ERROR = 2; - MAINTENANCE = 3; -} - -message Sensor { - uint8 id = 1; - float value = 2; - Status status = 3; - string name = 4 [size=16]; -} - -message ComprehensiveArrayMessage { - option msgid = 203; - - // Fixed arrays of primitives - repeated int32 fixed_ints = 1 [size=3]; - repeated float fixed_floats = 2 [size=2]; - repeated bool fixed_bools = 3 [size=4]; - - // Bounded arrays of primitives - repeated uint16 bounded_uints = 4 [max_size=3]; - repeated double bounded_doubles = 5 [max_size=2]; - - // Fixed string arrays (smaller strings and fewer of them) - repeated string fixed_strings = 6 [size=2, element_size=8]; - - // Bounded string arrays (smaller) - repeated string bounded_strings = 7 [max_size=2, element_size=12]; - - // Enum arrays - repeated Status fixed_statuses = 8 [size=2]; - repeated Status bounded_statuses = 9 [max_size=2]; - - // Nested message arrays (reduced) - repeated Sensor fixed_sensors = 10 [size=1]; - repeated Sensor bounded_sensors = 11 [max_size=1]; -} \ No newline at end of file diff --git a/tests/proto/nested_messages.proto b/tests/proto/nested_messages.proto deleted file mode 100644 index 41dea7f8..00000000 --- a/tests/proto/nested_messages.proto +++ /dev/null @@ -1,30 +0,0 @@ -package nested_messages; - -enum Priority { - LOW = 0; - MEDIUM = 1; - HIGH = 2; - CRITICAL = 3; -} - -message Header { - uint32 sequence = 1; - uint64 timestamp = 2; - Priority priority = 3; - string source = 4 [size=16]; -} - -message Payload { - string data = 1 [max_size=256]; - uint32 checksum = 2; -} - -message NestedMessage { - option msgid = 202; - - Header header = 1; - Payload payload = 2; - - // Test flatten attribute - Header flattened_header = 3 [flatten=true]; -} \ No newline at end of file diff --git a/tests/proto/serialization_test.proto b/tests/proto/serialization_test.proto deleted file mode 100644 index 694ca4ac..00000000 --- a/tests/proto/serialization_test.proto +++ /dev/null @@ -1,12 +0,0 @@ -package serialization_test; - -// A simple message for cross-language serialization testing -message SerializationTestMessage { - option msgid = 204; - - uint32 magic_number = 1; - string test_string = 2 [max_size=64]; - float test_float = 3; - bool test_bool = 4; - repeated int32 test_array = 5 [max_size=5]; -} \ No newline at end of file diff --git a/tests/proto/test_messages.proto b/tests/proto/test_messages.proto new file mode 100644 index 00000000..cc537c00 --- /dev/null +++ b/tests/proto/test_messages.proto @@ -0,0 +1,113 @@ +// Combined test messages for struct-frame code generation framework +// This file consolidates all test message definitions for testing +// +// Note: NestedMessage (from original nested_messages.proto) is intentionally +// excluded because the JavaScript code generator has a bug with nested struct +// references. The test suite only uses SerializationTestMessage for cross-language +// testing, so this exclusion doesn't affect test coverage. +package serialization_test; + +// ============================================================================ +// Enums +// ============================================================================ + +enum Priority { + LOW = 0; + MEDIUM = 1; + HIGH = 2; + CRITICAL = 3; +} + +enum Status { + INACTIVE = 0; + ACTIVE = 1; + ERROR = 2; + MAINTENANCE = 3; +} + +// ============================================================================ +// Basic Types Test Message (msgid: 201) +// ============================================================================ + +// Test all basic data types +message BasicTypesMessage { + option msgid = 201; + + // Integer types + int8 small_int = 1; + int16 medium_int = 2; + int32 regular_int = 3; + int64 large_int = 4; + + // Unsigned integer types + uint8 small_uint = 5; + uint16 medium_uint = 6; + uint32 regular_uint = 7; + uint64 large_uint = 8; + + // Floating point types + float single_precision = 9; + double double_precision = 10; + + // Boolean type + bool flag = 11; + + // Fixed string + string device_id = 12 [size=32]; + + // Variable string + string description = 13 [max_size=128]; +} + +// ============================================================================ +// Comprehensive Arrays Test (msgid: 203) +// ============================================================================ + +message Sensor { + uint8 id = 1; + float value = 2; + Status status = 3; + string name = 4 [size=16]; +} + +message ComprehensiveArrayMessage { + option msgid = 203; + + // Fixed arrays of primitives + repeated int32 fixed_ints = 1 [size=3]; + repeated float fixed_floats = 2 [size=2]; + repeated bool fixed_bools = 3 [size=4]; + + // Bounded arrays of primitives + repeated uint16 bounded_uints = 4 [max_size=3]; + repeated double bounded_doubles = 5 [max_size=2]; + + // Fixed string arrays (smaller strings and fewer of them) + repeated string fixed_strings = 6 [size=2, element_size=8]; + + // Bounded string arrays (smaller) + repeated string bounded_strings = 7 [max_size=2, element_size=12]; + + // Enum arrays + repeated Status fixed_statuses = 8 [size=2]; + repeated Status bounded_statuses = 9 [max_size=2]; + + // Nested message arrays (reduced) + repeated Sensor fixed_sensors = 10 [size=1]; + repeated Sensor bounded_sensors = 11 [max_size=1]; +} + +// ============================================================================ +// Serialization Test Message (msgid: 204) +// ============================================================================ + +// A simple message for cross-language serialization testing +message SerializationTestMessage { + option msgid = 204; + + uint32 magic_number = 1; + string test_string = 2 [max_size=64]; + float test_float = 3; + bool test_bool = 4; + repeated int32 test_array = 5 [max_size=5]; +} diff --git a/tests/run_tests.py b/tests/run_tests.py index a8f9dd63..21e70d91 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -32,6 +32,7 @@ def clean_test_files(config_path: str, verbose: bool = False) -> bool: """Clean all generated and compiled test files.""" from runner.base import TestRunnerBase + from languages import get_language project_root = Path(__file__).parent.parent config = TestRunnerBase.load_config(Path(config_path), verbose) @@ -40,22 +41,24 @@ def clean_test_files(config_path: str, verbose: bool = False) -> bool: print("Cleaning test files...") - # Clean generated code directories - for lang_id, lang_config in config['languages'].items(): - gen_dir = project_root / lang_config['code_generation']['output_dir'] + # Clean generated code directories and build directories + for lang_id in config['language_ids']: + lang = get_language(lang_id, project_root) + if not lang: + continue + + gen_dir = project_root / lang.gen_output_dir if gen_dir.exists(): if verbose: print(f" Removing generated directory: {gen_dir}") shutil.rmtree(gen_dir) cleaned_count += 1 - # Clean build directories (executables and binary outputs) - for lang_id, lang_config in config['languages'].items(): # Skip languages without build directories - if 'build_dir' not in lang_config: + if not lang.build_dir: continue - build_dir = project_root / lang_config['build_dir'] + build_dir = project_root / lang.build_dir if build_dir.exists(): if verbose: print(f" Removing build directory: {build_dir}") diff --git a/tests/runner/base.py b/tests/runner/base.py index f31f78f0..37e09c1d 100644 --- a/tests/runner/base.py +++ b/tests/runner/base.py @@ -11,7 +11,10 @@ import sys from contextlib import contextmanager from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING + +if TYPE_CHECKING: + from languages import Language class TestRunnerBase: @@ -25,16 +28,28 @@ def __init__(self, config: Dict[str, Any], project_root: Path, verbose: bool = F self.verbose = verbose self.verbose_failure = verbose_failure self.skipped_languages: List[str] = [] + # Cache language instances + self._languages: Dict[str, 'Language'] = {} + + def get_lang(self, lang_id: str) -> Optional['Language']: + """Get a Language instance for the given language ID.""" + if lang_id not in self._languages: + from languages import get_language + lang = get_language(lang_id, self.project_root) + if lang: + lang.verbose = self.verbose + lang.verbose_failure = self.verbose_failure + self._languages[lang_id] = lang + return self._languages.get(lang_id) @classmethod def load_config(cls, config_path: Path, verbose: bool = False) -> Dict[str, Any]: """Load the test configuration from JSON file(s). The main config can reference external files via: - - languages_file: Path to language definitions - test_suites_file: Path to test suite definitions - These are merged into the final config. + Languages are loaded from languages.py module. """ try: with open(config_path, 'r') as f: @@ -50,22 +65,13 @@ def load_config(cls, config_path: Path, verbose: bool = False) -> Dict[str, Any] config_dir = config_path.parent - # Load and merge languages file if specified - if 'languages_file' in config: - languages_path = config_dir / config['languages_file'] - try: - with open(languages_path, 'r') as f: - languages_config = json.load(f) - if 'languages' in languages_config: - config['languages'] = languages_config['languages'] - except FileNotFoundError: - cls._static_log( - f"Languages file not found: {languages_path}", "ERROR") - sys.exit(1) - except json.JSONDecodeError as e: - cls._static_log( - f"Invalid JSON in languages file: {e}", "ERROR") - sys.exit(1) + # Load language IDs from languages.py module + # Note: This import is done here rather than at module level because + # the languages module is in tests/ and sys.path is modified at runtime + # by test_all.py before this method is called + from languages import get_all_language_ids, BASE_LANGUAGE + config['language_ids'] = get_all_language_ids() + config['base_language'] = BASE_LANGUAGE # Load and merge test suites file if specified if 'test_suites_file' in config: @@ -130,33 +136,25 @@ def run_command(self, command: str, cwd: Optional[Path] = None, def get_lang_env(self, lang_id: str) -> Dict[str, str]: """Get environment variables for a language""" - lang_config = self.config['languages'][lang_id] - execution = lang_config.get('execution', {}) - env = {} - if 'env' in execution: - gen_dir = str(self.project_root / - lang_config['code_generation']['output_dir']) - gen_parent_dir = str(Path(gen_dir).parent) - env = {} - for k, v in execution['env'].items(): - v = v.replace('{generated_dir}', gen_dir) - v = v.replace('{generated_parent_dir}', gen_parent_dir) - # Handle cross-platform path separators (: on Linux, ; on Windows) - v = v.replace(':', os.pathsep) - env[k] = v - return env + lang = self.get_lang(lang_id) + if lang: + return lang.get_env(lang.get_gen_dir()) + return {} def get_active_languages(self) -> List[str]: """Get list of enabled languages that are not skipped""" - return [lang_id for lang_id, cfg in self.config['languages'].items() - if cfg.get('enabled', True) and lang_id not in self.skipped_languages] + return [lang_id for lang_id in self.config['language_ids'] + if self.get_lang(lang_id) and self.get_lang(lang_id).enabled + and lang_id not in self.skipped_languages] def get_testable_languages(self) -> List[str]: """Get list of enabled languages that can run tests (excludes generation_only)""" - return [lang_id for lang_id, cfg in self.config['languages'].items() - if cfg.get('enabled', True) - and lang_id not in self.skipped_languages - and not cfg.get('generation_only', False)] + result = [] + for lang_id in self.config['language_ids']: + lang = self.get_lang(lang_id) + if lang and lang.enabled and not lang.generation_only and lang_id not in self.skipped_languages: + result.append(lang_id) + return result @contextmanager def temp_copy(self, src: Path, dst: Path): @@ -180,27 +178,24 @@ def temp_copy(self, src: Path, dst: Path): def get_source_extension(self, lang_id: str) -> str: """Get source file extension for a language""" - lang_config = self.config['languages'][lang_id] - # Check compilation first, then execution - ext = lang_config.get('compilation', {}).get('source_extension') - if not ext: - ext = lang_config.get('execution', {}).get('source_extension', '') - return ext + lang = self.get_lang(lang_id) + return lang.source_extension if lang else '' def get_executable_extension(self, lang_id: str) -> str: """Get executable extension for compiled languages""" - return self.config['languages'][lang_id].get('compilation', {}).get('executable_extension', '') + lang = self.get_lang(lang_id) + return lang.executable_extension if lang else '' def get_compiled_extension(self, lang_id: str) -> str: """Get compiled output extension (e.g., .js for TypeScript)""" - return self.config['languages'][lang_id].get('compilation', {}).get('compiled_extension', '') + lang = self.get_lang(lang_id) + return lang.compiled_extension if lang else '' def get_test_files(self, lang_id: str, test_name: str) -> Dict[str, str]: """Get all file names for a test based on test_name and language extensions. Returns dict with keys: source_file, executable (if compiled), compiled_file (if transpiled) """ - lang_config = self.config['languages'][lang_id] files = {} # Source file @@ -244,11 +239,12 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], test_dir: Optional test directory override args: Optional command-line arguments to pass to the test """ - lang_config = self.config['languages'][lang_id] - test_dir = test_dir or (self.project_root / lang_config['test_dir']) - build_dir = self.project_root / \ - lang_config.get('build_dir', lang_config['test_dir']) - execution = lang_config.get('execution', {}) + lang = self.get_lang(lang_id) + if not lang: + return False + + test_dir = test_dir or lang.get_test_dir() + build_dir = lang.get_build_dir() # Compiled executable (C, C++) if 'executable' in test_config: @@ -259,19 +255,19 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], return exe_path.exists() and self.run_command(cmd, cwd=build_dir)[0] # Compiled script language (e.g., TypeScript -> JS) - if 'compiled_file' in test_config and 'script_dir' in execution: - script_dir = self.project_root / execution.get('script_dir', '') + if 'compiled_file' in test_config and lang.script_dir: + script_dir = lang.get_script_dir() script_path = script_dir / test_config['compiled_file'] if not script_path.exists(): return False - cmd = f"{execution['interpreter']} {script_path.name}" + cmd = f"{lang.interpreter} {script_path.name}" if args: cmd = f"{cmd} {args}" return self.run_command(cmd, cwd=script_dir)[0] # Script language with script_dir (runs test files from generated code directory) - if 'script_dir' in execution and 'source_file' in test_config: - script_dir = self.project_root / execution.get('script_dir', '') + if lang.script_dir and 'source_file' in test_config: + script_dir = lang.get_script_dir() source_path = test_dir / test_config['source_file'] # Copy test file to generated directory for execution target_path = script_dir / test_config['source_file'] @@ -280,7 +276,7 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], shutil.copy2(source_path, target_path) if not target_path.exists(): return False - cmd = f"{execution['interpreter']} {target_path.name}" + cmd = f"{lang.interpreter} {target_path.name}" if args: cmd = f"{cmd} {args}" return self.run_command(cmd, cwd=script_dir)[0] @@ -290,12 +286,11 @@ def run_test_script(self, lang_id: str, test_config: Dict[str, Any], source_path = test_dir / test_config['source_file'] if not source_path.exists(): return False - interpreter = execution.get('interpreter') - if not interpreter: + if not lang.interpreter: return False # Ensure build_dir exists and run from there build_dir.mkdir(parents=True, exist_ok=True) - cmd = f"{interpreter} {source_path}" + cmd = f"{lang.interpreter} {source_path}" if args: cmd = f"{cmd} {args}" return self.run_command( diff --git a/tests/runner/code_generator.py b/tests/runner/code_generator.py index dddf5a35..32da0e4a 100644 --- a/tests/runner/code_generator.py +++ b/tests/runner/code_generator.py @@ -18,7 +18,7 @@ def __init__(self, config: Dict[str, Any], project_root: Path, verbose: bool = F verbose_failure: bool = False): super().__init__(config, project_root, verbose, verbose_failure) self.results: Dict[str, bool] = {} - for lang_id in self.config['languages']: + for lang_id in self.config['language_ids']: self.results[lang_id] = False def generate_code(self) -> bool: @@ -40,9 +40,10 @@ def generate_code(self) -> bool: # Build generation command cmd_parts = [sys.executable, "-m", "struct_frame", str(proto_path)] for lang_id in active: - gen = self.config['languages'][lang_id]['code_generation'] - cmd_parts += [gen['flag'], gen['output_path_flag'], - str(self.project_root / gen['output_dir'])] + lang = self.get_lang(lang_id) + if lang: + cmd_parts += [lang.gen_flag, lang.gen_output_path_flag, + str(self.project_root / lang.gen_output_dir)] env = {"PYTHONPATH": str(self.project_root / "src")} success, _, _ = self.run_command(" ".join(cmd_parts), env=env) diff --git a/tests/runner/compiler.py b/tests/runner/compiler.py index dedee241..6777e165 100644 --- a/tests/runner/compiler.py +++ b/tests/runner/compiler.py @@ -18,7 +18,7 @@ def __init__(self, config: Dict[str, Any], project_root: Path, verbose: bool = F verbose_failure: bool = False): super().__init__(config, project_root, verbose, verbose_failure) self.results: Dict[str, bool] = {} - for lang_id in self.config['languages']: + for lang_id in self.config['language_ids']: self.results[lang_id] = False def compile_all(self) -> bool: @@ -32,7 +32,7 @@ def compile_all(self) -> bool: self._copy_js_test_files() compiled = [l for l in self.get_active_languages() - if self.config['languages'][l].get('compilation', {}).get('enabled')] + if self.get_lang(l) and self.get_lang(l).compiler] if not compiled: print(" No languages require compilation") @@ -46,19 +46,14 @@ def compile_all(self) -> bool: def _copy_js_test_files(self): """Copy JavaScript test files to the generated JS directory.""" - if 'js' not in self.config['languages']: + lang = self.get_lang('js') + if not lang or not lang.enabled: return - lang_config = self.config['languages']['js'] - if not lang_config.get('enabled', True): - return - - test_dir = self.project_root / lang_config['test_dir'] - execution = lang_config.get('execution', {}) - script_dir_path = execution.get('script_dir') + test_dir = lang.get_test_dir() + script_dir = lang.get_script_dir() - if script_dir_path: - script_dir = self.project_root / script_dir_path + if script_dir: script_dir.mkdir(parents=True, exist_ok=True) for filename in ['test_runner.js', 'test_codec.js']: source_file = test_dir / filename @@ -67,33 +62,27 @@ def _copy_js_test_files(self): def _compile_language(self, lang_id: str) -> bool: """Compile code for a specific language""" - lang_config = self.config['languages'][lang_id] - comp = lang_config.get('compilation', {}) - - # Check compiler availability - if comp.get('compiler_check'): - # Use working_dir if specified - working_dir = None - if comp.get('working_dir'): - working_dir = self.project_root / comp['working_dir'] - if not self.run_command(comp['compiler_check'], cwd=working_dir)[0]: - self.log( - f"{lang_config['name']} compiler not found - skipping", "WARNING") - return True - - test_dir = self.project_root / lang_config['test_dir'] - build_dir = self.project_root / \ - lang_config.get('build_dir', lang_config['test_dir']) - gen_dir = self.project_root / \ - lang_config['code_generation']['output_dir'] + lang = self.get_lang(lang_id) + if not lang: + return False + + # Check compiler availability using Language's check_compiler method + compiler_info = lang.check_compiler() + if not compiler_info['available']: + self.log(f"{lang.name} compiler not found - skipping", "WARNING") + return True + + test_dir = lang.get_test_dir() + build_dir = lang.get_build_dir() + gen_dir = lang.get_gen_dir() all_success = True # Ensure build directory exists build_dir.mkdir(parents=True, exist_ok=True) # Compile the unified test_runner with test_codec for C/C++ - source_ext = comp.get('source_extension', '') - exe_ext = comp.get('executable_extension', '') + source_ext = lang.source_extension + exe_ext = lang.executable_extension if exe_ext and source_ext in ['.c', '.cpp']: # For C: compile test_runner.c with test_codec.c @@ -103,21 +92,17 @@ def _compile_language(self, lang_id: str) -> bool: runner_output = build_dir / f"test_runner{exe_ext}" if runner_source.exists() and codec_source.exists(): - if not self._compile_multi_source(lang_id, [runner_source, codec_source], - runner_output, gen_dir): + if not lang.compile([runner_source, codec_source], runner_output, gen_dir): all_success = False - # Special handling for languages with 'command' (project-based compilation like TypeScript) - # These languages copy test files to generated dir and run a project-level compile command - source_ext = comp.get('source_extension', '') - if comp.get('command') and source_ext: - # Copy test files to generated directory - for source_file in test_dir.glob(f"*{source_ext}"): - shutil.copy2(source_file, gen_dir / source_file.name) - - # Create tsconfig.json in generated dir for TypeScript projects - # This is needed to resolve modules from tests dir node_modules + # Special handling for languages with compile_command (project-based compilation like TypeScript, C#) + if lang.compile_command and source_ext: + # Copy test files to generated directory (for TypeScript) if source_ext == '.ts': + for source_file in test_dir.glob(f"*{source_ext}"): + shutil.copy2(source_file, gen_dir / source_file.name) + + # Create tsconfig.json in generated dir for TypeScript projects tsconfig_path = gen_dir / 'tsconfig.json' if not tsconfig_path.exists(): import json @@ -138,48 +123,9 @@ def _compile_language(self, lang_id: str) -> bool: } tsconfig_path.write_text(json.dumps(tsconfig, indent=2)) - output_dir = self.project_root / comp['output_dir'] - cmd = comp['command'].format( - output_dir=output_dir, generated_dir=gen_dir) - - # Use working_dir if specified (for running npm/npx from tests dir) - working_dir = None - if comp.get('working_dir'): - working_dir = str(self.project_root / comp['working_dir']) - all_success = self.run_command(cmd, cwd=working_dir)[ - 0] and all_success + # Use the Language's compile_project method + if not lang.compile_project(test_dir, build_dir, gen_dir): + all_success = False self.results[lang_id] = all_success return all_success - - def _compile_file(self, lang_id: str, source: Path, output: Path, gen_dir: Path) -> bool: - """Compile a single source file""" - if not source.exists(): - return False - comp = self.config['languages'][lang_id]['compilation'] - flags = [f.replace('{generated_dir}', str(gen_dir)) - .replace('{output}', str(output)) - .replace('{source}', str(source)) for f in comp.get('flags', [])] - return self.run_command(f"{comp['compiler']} {' '.join(flags)}")[0] - - def _compile_multi_source(self, lang_id: str, sources: List[Path], output: Path, - gen_dir: Path) -> bool: - """Compile multiple source files into a single executable""" - for source in sources: - if not source.exists(): - return False - comp = self.config['languages'][lang_id]['compilation'] - - # Build flags, replacing {source} with all source files - sources_str = ' '.join(f'"{s}"' for s in sources) - flags = [] - for f in comp.get('flags', []): - f = f.replace('{generated_dir}', str(gen_dir)) - f = f.replace('{output}', str(output)) - if '{source}' in f: - # Replace {source} with all sources - f = sources_str - flags.append(f) - - cmd = f"{comp['compiler']} {' '.join(flags)}" - return self.run_command(cmd)[0] diff --git a/tests/runner/output_formatter.py b/tests/runner/output_formatter.py index 55d4ef61..cc268224 100644 --- a/tests/runner/output_formatter.py +++ b/tests/runner/output_formatter.py @@ -21,7 +21,8 @@ def print_lang_results(self, languages: List[str], results: Dict[str, bool]): """Print results for each language""" print() for lang_id in languages: - name = self.config['languages'][lang_id]['name'] + lang = self.get_lang(lang_id) + name = lang.name if lang else lang_id status = "PASS" if results.get(lang_id, False) else "FAIL" print(f" {name:>10}: {status}") @@ -81,7 +82,8 @@ def print_summary(self, generation_results: Dict[str, bool], # Compilation for lang_id in active_languages: - if self.config['languages'][lang_id].get('compilation', {}).get('enabled'): + lang = self.get_lang(lang_id) + if lang and lang.compiler: total += 1 passed += compilation_results.get(lang_id, False) diff --git a/tests/runner/plugins.py b/tests/runner/plugins.py index bc2465d6..b54d2abf 100644 --- a/tests/runner/plugins.py +++ b/tests/runner/plugins.py @@ -4,6 +4,7 @@ Plugins allow tests to define their own execution and output logic. """ +import os from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Dict, List, Optional, TYPE_CHECKING @@ -86,21 +87,26 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: def _get_output_dir(self, lang_id: str) -> Path: """Get the output directory for a language (build_dir for binaries)""" - lang_config = self.config['languages'][lang_id] + lang = self.executor.get_lang(lang_id) + if not lang: + return self.project_root + # Languages with script_dir in execution use that for output - if 'execution' in lang_config: - script_dir = lang_config['execution'].get('script_dir') - if script_dir: - return self.project_root / script_dir + script_dir = lang.get_script_dir() + if script_dir: + return script_dir + # Use build_dir for output files - return self.project_root / lang_config.get('build_dir', lang_config['test_dir']) + return lang.get_build_dir() def _get_output_file_name(self, suite: Dict[str, Any], lang_id: str) -> str: """Get the output file name for a language""" - lang_config = self.config['languages'][lang_id] + lang = self.executor.get_lang(lang_id) + if not lang: + return suite.get('output_file', 'output.bin') + # Use file_prefix if specified, otherwise lowercase display name - file_prefix = lang_config.get( - 'file_prefix', lang_config['name'].lower()) + file_prefix = lang.file_prefix or lang.name.lower() pattern = suite.get('output_file', '{lang_name}_output.bin') return pattern.replace('{lang_name}', file_prefix) @@ -149,14 +155,16 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: # Test all encoders against C decoder, and C encoder against all decoders for enc_lang in testable: - enc_name = self.config['languages'][enc_lang]['name'] + enc_lang_obj = self.executor.get_lang(enc_lang) + enc_name = enc_lang_obj.name if enc_lang_obj else enc_lang matrix[enc_name] = {} # Check if this encoder produced a file data_file = encoded_files.get(enc_lang) for dec_lang in testable: - dec_name = self.config['languages'][dec_lang]['name'] + dec_lang_obj = self.executor.get_lang(dec_lang) + dec_name = dec_lang_obj.name if dec_lang_obj else dec_lang # Only test: C encodes → all decode, OR all encode → C decodes if enc_lang != base_lang and dec_lang != base_lang: @@ -195,9 +203,11 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: def _run_decoder_with_file(self, lang_id: str, test_config: Dict[str, Any], data_file: Path) -> bool: """Run a decoder test with a specific input file""" - lang_config = self.config['languages'][lang_id] - build_dir = self.project_root / \ - lang_config.get('build_dir', lang_config['test_dir']) + lang = self.executor.get_lang(lang_id) + if not lang: + return False + + build_dir = lang.get_build_dir() target_file = build_dir / data_file.name # Ensure build directory exists @@ -206,10 +216,8 @@ def _run_decoder_with_file(self, lang_id: str, test_config: Dict[str, Any], try: with self.executor.temp_copy(data_file, target_file): # Languages with script_dir need file copied there too - execution = lang_config.get('execution', {}) - script_dir_path = execution.get('script_dir') - if script_dir_path: - script_dir = self.project_root / script_dir_path + script_dir = lang.get_script_dir() + if script_dir: script_target = script_dir / data_file.name with self.executor.temp_copy(data_file, script_target): return self.executor.run_test_script( @@ -282,7 +290,8 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: # First pass: run all encode tests and collect output files for lang_id in testable: - lang_name = self.config['languages'][lang_id]['name'] + lang = self.executor.get_lang(lang_id) + lang_name = lang.name if lang else lang_id output_file = self._get_output_file( lang_id, output_file_pattern) @@ -306,7 +315,8 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: base_data_file = encoded_files.get(base_lang) for lang_id in testable: - lang_name = self.config['languages'][lang_id]['name'] + lang = self.executor.get_lang(lang_id) + lang_name = lang.name if lang else lang_id if base_data_file is None or not base_data_file.exists(): matrix[display_name][lang_name]['decode'] = None @@ -334,31 +344,26 @@ def run(self, suite: Dict[str, Any]) -> Dict[str, Any]: def _run_test_runner(self, lang_id: str, mode: str, format_name: str, output_file: Path) -> bool: """Run the unified test_runner for a language.""" - lang_config = self.config['languages'][lang_id] - build_dir = self.project_root / \ - lang_config.get('build_dir', lang_config['test_dir']) + lang = self.executor.get_lang(lang_id) + if not lang: + return False + + build_dir = lang.get_build_dir() # Ensure build directory exists build_dir.mkdir(parents=True, exist_ok=True) - # Get test_runner executable/script path - exe_ext = lang_config.get('compilation', {}).get( - 'executable_extension', '') - source_ext = lang_config.get( - 'compilation', {}).get('source_extension', '') - execution = lang_config.get('execution', {}) - # For compiled languages (C, C++) - if exe_ext: - runner_path = build_dir / f"test_runner{exe_ext}" + if lang.executable_extension: + runner_path = build_dir / f"test_runner{lang.executable_extension}" if not runner_path.exists(): return False cmd = f'"{runner_path}" {mode} {format_name} "{output_file}"' return self.executor.run_command(cmd, cwd=build_dir)[0] # For C# (dotnet run) - if execution.get('type') == 'dotnet': - test_dir = self.project_root / lang_config['test_dir'] + if lang.execution_type == 'dotnet': + test_dir = lang.get_test_dir() csproj_path = test_dir / 'StructFrameTests.csproj' if not csproj_path.exists(): return False @@ -366,50 +371,47 @@ def _run_test_runner(self, lang_id: str, mode: str, format_name: str, return self.executor.run_command(cmd, cwd=test_dir)[0] # For TypeScript (compiles to JS) - if lang_config.get('compilation', {}).get('compiled_extension'): - script_dir = self.project_root / execution.get('script_dir', '') + if lang.compiled_extension: + script_dir = lang.get_script_dir() + if not script_dir: + return False runner_path = script_dir / 'test_runner.js' if not runner_path.exists(): return False - interpreter = execution.get('interpreter', 'node') + interpreter = lang.interpreter or 'node' cmd = f'{interpreter} "{runner_path}" {mode} {format_name} "{output_file}"' return self.executor.run_command(cmd, cwd=script_dir)[0] # For interpreted languages (Python, JavaScript) - if 'interpreter' in execution: - script_dir_path = execution.get('script_dir') - if script_dir_path: + if lang.interpreter: + script_dir = lang.get_script_dir() + if script_dir: # JS runs from generated dir - script_dir = self.project_root / script_dir_path runner_path = script_dir / 'test_runner.js' else: # Python runs from test dir - test_dir = self.project_root / lang_config['test_dir'] - source_ext = execution.get('source_extension', '.py') + test_dir = lang.get_test_dir() + source_ext = lang.source_extension or '.py' runner_path = test_dir / f'test_runner{source_ext}' if not runner_path.exists(): return False - interpreter = execution['interpreter'] cwd = runner_path.parent # Handle Python's PYTHONPATH - env_vars = execution.get('env', {}) env_prefix = '' - if env_vars: - gen_dir = self.project_root / \ - lang_config['code_generation']['output_dir'] - for key, val in env_vars.items(): + if lang.env_vars: + gen_dir = lang.get_gen_dir() + for key, val in lang.env_vars.items(): val = val.replace('{generated_dir}', str(gen_dir)) val = val.replace( '{generated_parent_dir}', str(gen_dir.parent)) # Use cross-platform path separator - import os val = val.replace(':', os.pathsep) env_prefix = f'set {key}={val} && ' if os.name == 'nt' else f'{key}={val} ' - cmd = f'{env_prefix}{interpreter} "{runner_path}" {mode} {format_name} "{output_file}"' + cmd = f'{env_prefix}{lang.interpreter} "{runner_path}" {mode} {format_name} "{output_file}"' return self.executor.run_command(cmd, cwd=cwd)[0] return False @@ -417,9 +419,11 @@ def _run_test_runner(self, lang_id: str, mode: str, format_name: str, def _run_decode_with_file(self, lang_id: str, format_name: str, data_file: Path) -> bool: """Run decoder with a specific input file.""" - lang_config = self.config['languages'][lang_id] - build_dir = self.project_root / \ - lang_config.get('build_dir', lang_config['test_dir']) + lang = self.executor.get_lang(lang_id) + if not lang: + return False + + build_dir = lang.get_build_dir() target_file = build_dir / data_file.name build_dir.mkdir(parents=True, exist_ok=True) @@ -427,10 +431,8 @@ def _run_decode_with_file(self, lang_id: str, format_name: str, try: with self.executor.temp_copy(data_file, target_file): # For script languages, also copy to script_dir - execution = lang_config.get('execution', {}) - script_dir_path = execution.get('script_dir') - if script_dir_path: - script_dir = self.project_root / script_dir_path + script_dir = lang.get_script_dir() + if script_dir: script_target = script_dir / data_file.name with self.executor.temp_copy(data_file, script_target): return self._run_test_runner(lang_id, 'decode', format_name, script_target) @@ -443,25 +445,27 @@ def _run_decode_with_file(self, lang_id: str, format_name: str, def _get_output_file(self, lang_id: str, pattern: str) -> Optional[Path]: """Get the output file path for a language.""" - lang_config = self.config['languages'][lang_id] - file_prefix = lang_config.get( - 'file_prefix', lang_config['name'].lower()) + lang = self.executor.get_lang(lang_id) + if not lang: + return None + + file_prefix = lang.file_prefix or lang.name.lower() filename = pattern.replace('{lang_name}', file_prefix) # Check script_dir first (for JS/TS) - if 'execution' in lang_config: - script_dir = lang_config['execution'].get('script_dir') - if script_dir: - return self.project_root / script_dir / filename + script_dir = lang.get_script_dir() + if script_dir: + return script_dir / filename # Use build_dir - build_dir = lang_config.get('build_dir', lang_config['test_dir']) - return self.project_root / build_dir / filename + return lang.get_build_dir() / filename def _print_matrix_header(self, testable: list): """Print the matrix header with language columns.""" - all_langs = [self.config['languages'][lang_id]['name'] - for lang_id in testable] + all_langs = [] + for lang_id in testable: + lang = self.executor.get_lang(lang_id) + all_langs.append(lang.name if lang else lang_id) self._matrix_langs = all_langs self._matrix_col_width = 12 @@ -474,13 +478,15 @@ def _print_matrix_header(self, testable: list): def _print_matrix_row(self, frame_format: str, lang_results: Dict[str, Dict[str, Optional[bool]]], testable: list): """Print a single row of the matrix.""" - all_langs = [self.config['languages'][lang_id]['name'] - for lang_id in testable] + all_langs = [] + for lang_id in testable: + lang = self.executor.get_lang(lang_id) + all_langs.append(lang.name if lang else lang_id) col_width = 12 row = frame_format.ljust(20) - for lang in all_langs: - val = lang_results.get(lang) + for lang_name in all_langs: + val = lang_results.get(lang_name) if val is None: cell = "N/A" elif isinstance(val, dict): @@ -536,67 +542,6 @@ def _print_matrix_summary(self, matrix: Dict[str, Dict[str, Dict[str, Optional[b print( f"\nSuccess rate: {success_count}/{total_count} ({100*success_count/total_count:.1f}%)\n") - def _print_frame_format_matrix(self, matrix: Dict[str, Dict[str, Dict[str, Optional[bool]]]]): - """Print the frame format compatibility matrix with detailed status.""" - if not matrix: - return - - # Get all language columns - all_langs = sorted(set().union( - *[set(d.keys()) for d in matrix.values()])) - - col_width = 12 - print("\nFrame Format Language Test Matrix:") - print("Legend: OK=pass, SER=serialization failed, DES=deserialization failed, BOTH=both failed") - header = "Frame Format".ljust( - 20) + "".join(l.center(col_width) for l in all_langs) - print(header) - print("-" * len(header)) - - success_count = 0 - total_count = 0 - - for frame_format, lang_results in matrix.items(): - row = frame_format.ljust(20) - for lang in all_langs: - val = lang_results.get(lang) - if val is None: - cell = "N/A" - elif isinstance(val, dict): - encode_ok = val.get('encode') - decode_ok = val.get('decode') - - if encode_ok is None and decode_ok is None: - cell = "N/A" - elif encode_ok and decode_ok: - cell = "OK" - success_count += 1 - total_count += 1 - elif not encode_ok and (decode_ok is False or decode_ok is None): - cell = "BOTH" - total_count += 1 - elif not encode_ok: - cell = "SER" - total_count += 1 - elif not decode_ok: - cell = "DES" - total_count += 1 - else: - cell = "N/A" - elif val: - cell = "OK" - success_count += 1 - total_count += 1 - else: - cell = "FAIL" - total_count += 1 - row += cell.center(col_width) - print(row) - - if total_count > 0: - print( - f"\nSuccess rate: {success_count}/{total_count} ({100*success_count/total_count:.1f}%)\n") - # Registry of available plugins PLUGIN_REGISTRY: Dict[str, type] = { diff --git a/tests/runner/runner.py b/tests/runner/runner.py index a83e7872..9709ed3a 100644 --- a/tests/runner/runner.py +++ b/tests/runner/runner.py @@ -62,8 +62,12 @@ def _sync_skipped_languages(self): def _get_active_languages(self) -> List[str]: """Get list of enabled languages that are not skipped""" - return [lang_id for lang_id, cfg in self.config['languages'].items() - if cfg.get('enabled', True) and lang_id not in self.skipped_languages] + result = [] + for lang_id in self.config['language_ids']: + lang = self.tool_checker.get_lang(lang_id) + if lang and lang.enabled and lang_id not in self.skipped_languages: + result.append(lang_id) + return result # ------------------------------------------------------------------------- # Delegated Methods (for backward compatibility) @@ -148,17 +152,17 @@ def run_all_tests(self, generate_only: bool = False) -> bool: # Filter to only available languages active = [l for l in self._get_active_languages() if l in available_langs] - lang_names = [self.config['languages'][l]['name'] for l in active] + lang_names = [self.tool_checker.get_lang(l).name for l in active] print(f"Testing languages: {', '.join(lang_names)}") start_time = time.time() try: # Create output directories - for lang_id, cfg in self.config['languages'].items(): - if cfg.get('enabled', True): - (self.project_root / cfg['code_generation'] - ['output_dir']).mkdir(parents=True, exist_ok=True) + for lang_id in self.config['language_ids']: + lang = self.tool_checker.get_lang(lang_id) + if lang and lang.enabled: + (self.project_root / lang.gen_output_dir).mkdir(parents=True, exist_ok=True) if not self.generate_code(): print("[ERROR] Code generation failed - aborting remaining tests") diff --git a/tests/runner/test_executor.py b/tests/runner/test_executor.py index 7a250e7b..af51ea39 100644 --- a/tests/runner/test_executor.py +++ b/tests/runner/test_executor.py @@ -17,7 +17,7 @@ def __init__(self, config: Dict[str, Any], project_root: Path, verbose: bool = F verbose_failure: bool = False): super().__init__(config, project_root, verbose, verbose_failure) self.results: Dict[str, Dict[str, bool]] = {} - for lang_id in self.config['languages']: + for lang_id in self.config['language_ids']: self.results[lang_id] = {} self.cross_platform_results: Dict[str, Dict[str, bool]] = {} # Store output files from suites for other suites to consume diff --git a/tests/runner/tool_checker.py b/tests/runner/tool_checker.py index ba95aa01..381f1706 100644 --- a/tests/runner/tool_checker.py +++ b/tests/runner/tool_checker.py @@ -29,94 +29,17 @@ def check_tool_availability(self) -> Dict[str, Dict[str, Any]]: """ results = {} - for lang_id, lang_config in self.config['languages'].items(): - if not lang_config.get('enabled', True): + for lang_id in self.config['language_ids']: + lang = self.get_lang(lang_id) + if not lang or not lang.enabled: continue - info = { - 'name': lang_config['name'], - 'available': True, - 'compiler': None, - 'interpreter': None, - } - - # Generation-only languages don't need tools checked - if lang_config.get('generation_only'): - info['generation_only'] = True - results[lang_id] = info - continue - - # Check compiler if compilation is enabled - info = self._check_compiler(lang_config, info) - - # Check interpreter if execution config exists - info = self._check_interpreter(lang_config, info) - + # Use the Language class's check_tools() method + info = lang.check_tools() results[lang_id] = info return results - def _check_compiler(self, lang_config: Dict[str, Any], info: Dict[str, Any]) -> Dict[str, Any]: - """Check compiler availability for a language""" - comp = lang_config.get('compilation', {}) - if not comp.get('enabled'): - return info - - compiler = comp.get('compiler', '') - check_cmd = comp.get('compiler_check', f"{compiler} --version") - - # Use working_dir if specified (for npm/npx commands) - working_dir = None - if comp.get('working_dir'): - working_dir = self.project_root / comp['working_dir'] - - success, stdout, stderr = self.run_command( - check_cmd, cwd=working_dir, timeout=5) - - version = "" - if success: - output = stdout or stderr - version = output.strip().split('\n')[0] if output else "" - - info['compiler'] = { - 'name': compiler, - 'available': success, - 'version': version - } - - if not success: - info['available'] = False - info['reason'] = f"Compiler '{compiler}' not found" - - return info - - def _check_interpreter(self, lang_config: Dict[str, Any], info: Dict[str, Any]) -> Dict[str, Any]: - """Check interpreter availability for a language""" - execution = lang_config.get('execution', {}) - if not execution.get('interpreter'): - return info - - interpreter = execution['interpreter'] - success, stdout, stderr = self.run_command( - f"{interpreter} --version", timeout=5) - - version = "" - if success: - output = stdout or stderr - version = output.strip().split('\n')[0] if output else "" - - info['interpreter'] = { - 'name': interpreter, - 'available': success, - 'version': version - } - - if not success: - info['available'] = False - info['reason'] = f"Interpreter '{interpreter}' not found" - - return info - def print_tool_availability(self) -> bool: """Print a summary of available tools and return True if all tools available.""" from .output_formatter import OutputFormatter diff --git a/tests/test_config.json b/tests/test_config.json index 43198a80..1b54f74c 100644 --- a/tests/test_config.json +++ b/tests/test_config.json @@ -3,15 +3,9 @@ "description": "Test configuration for struct-frame code generation framework", "version": "2.0", - "base_language": "c", - - "languages_file": "languages.json", "test_suites_file": "test_suites.json", "proto_files": [ - "basic_types.proto", - "nested_messages.proto", - "comprehensive_arrays.proto", - "serialization_test.proto" + "test_messages.proto" ] }