from typing import ClassVar, List, Dict, Any, Optional, Type from abc import ABC, abstractmethod from dataclasses import dataclass, asdict @dataclass class Token: type: str value: Any @dataclass class Operation: function: str type: str value: Any @dataclass class StackItem: type: str value: Any @dataclass class RuntimeError: message: str @dataclass class TestCase: name: str code: str tokens: List[Dict[str, Any]] operations: Optional[List[Dict[str, Any]]] = None stack_final: Optional[List[Dict[str, Any]]] = None runtime_error: Optional[Dict[str, str]] = None def to_dict(obj) -> Dict[str, Any]: """Convert dataclass to dict, removing None values.""" if obj is None: raise ValueError("Obj cannot be None") d = asdict(obj) if hasattr(obj, '__dataclass_fields__') else obj return {k: v for k, v in d.items() if v is not None} class BaseTestGenerator(ABC): """ Abstract base class for test case generators. Provides common functionality for generating test cases including test creation, operation building, and error handling. """ ENABLE_UNICODE = False __generators: "ClassVar[List[Type[BaseTestGenerator]]]" = [] def __init_subclass__(cls): BaseTestGenerator.__generators.append(cls) def __init__(self): """Initialize the test generator with an empty test list.""" self.tests: List[Dict[str, Any]] = [] # ========================================================================= # Test Case Management # ========================================================================= def add_test(self, name: str, code: str, tokens: List[Token], operations: Optional[List[Operation]] = None, stack_final: Optional[List[StackItem]] = None, runtime_error: Optional[RuntimeError] = None): """ Add a test case to the test suite. Args: name: Descriptive name for the test code: Source code being tested tokens: List of expected tokens from lexing operations: Optional list of operations for evaluation phase stack_final: Optional final stack state after execution runtime_error: Optional runtime error if test should fail """ test = TestCase( name=name, code=code, tokens=[to_dict(t) for t in tokens], operations=[to_dict(o) for o in operations] if operations else None, stack_final=[to_dict(s) for s in stack_final] if stack_final else None, runtime_error=to_dict(runtime_error) if runtime_error else None ) self.tests.append(to_dict(test)) # ========================================================================= # Factory Methods # ========================================================================= def make_push_op(self, type_name: str, value: Any) -> Operation: """ Create a push operation. Args: type_name: Type of the value being pushed value: The value to push Returns: Operation object representing a push """ return Operation(function="push", type=type_name, value=value) def make_stack_item(self, type_name: str, value: Any) -> StackItem: """ Create a stack item. Args: type_name: Type of the stack item value: The value of the stack item Returns: StackItem object """ return StackItem(type=type_name, value=value) def make_error_token(self, message: str) -> Token: """ Create an error token. Args: message: Error message Returns: Token object with type "error" """ return Token(type="error", value=message) def make_runtime_error(self, message: str) -> RuntimeError: """ Create a runtime error. Args: message: Error message Returns: RuntimeError object """ return RuntimeError(message=message) # ========================================================================= # Convenience Test Creators # ========================================================================= def make_success_test(self, name: str, code: str, type_name: str, value: Any): """ Create a successful test case with standard push operation. Args: name: Test name code: Source code type_name: Type of the value value: The value """ token = Token(type=type_name, value=value) op = self.make_push_op(type_name, value) stack = self.make_stack_item(type_name, value) self.add_test(name, code, [token], [op], [stack]) def make_error_test(self, name: str, code: str, error_msg: str): """ Create an error test case (lexing error). Args: name: Test name code: Source code error_msg: Expected error message """ token = self.make_error_token(error_msg) self.add_test(name, code, [token]) def make_runtime_error_test(self, name: str, code: str, tokens: List[Token], operations: List[Operation], error_msg: str): """ Create a runtime error test case. Args: name: Test name code: Source code tokens: Tokens that were successfully lexed operations: Operations that led to the error error_msg: Expected runtime error message """ runtime_error = self.make_runtime_error(error_msg) self.add_test(name, code, tokens, operations, None, runtime_error) def make_empty_test(self, name: str, code: str): """ Create a test case with empty result (e.g., comments, whitespace). Args: name: Test name code: Source code """ self.add_test(name, code, [], [], []) # ========================================================================= # Multi-Value Test Helpers # ========================================================================= def make_multi_value_test(self, name: str, code: str, values: List[tuple[str, Any]]): """ Create a test with multiple values on the stack. Args: name: Test name code: Source code values: List of (type_name, value) tuples in stack order """ tokens = [Token(type=t, value=v) for t, v in values] operations = [self.make_push_op(t, v) for t, v in values] stack = [self.make_stack_item(t, v) for t, v in values] self.add_test(name, code, tokens, operations, stack) # ========================================================================= # Test Suite Generation # ========================================================================= @abstractmethod def generate_all_tests(self) -> List[Dict[str, Any]]: """ Generate all test cases for this generator. Must be implemented by subclasses to define their specific test suite. Returns: List of test case dictionaries ready for serialization """ pass def get_tests(self) -> List[Dict[str, Any]]: """ Get the current list of tests. Returns: List of test case dictionaries """ return self.tests def clear_tests(self): """Clear all tests from the generator.""" self.tests = [] def test_count(self) -> int: """ Get the number of tests generated. Returns: Number of tests """ return len(self.tests) @classmethod def generate_tests(cls) -> List[Dict[str, Any]]: gen = cls() tests = gen.generate_all_tests() gen.print_statistics() return tests @classmethod def run_all_generators(cls) -> List[Dict[str, Any]]: tests = [] for sub_cls in cls.__generators: tests += sub_cls.generate_tests() return tests # ========================================================================= # Test Organization Helpers # ========================================================================= def add_test_group_comment(self, comment: str): """ Add a comment to organize test groups (for documentation). Note: This doesn't add an actual test, just tracks organization in subclass implementations. Args: comment: Comment describing the test group """ # Subclasses can override to add metadata or logging pass # ========================================================================= # Validation Helpers # ========================================================================= def validate_test_names_unique(self) -> bool: """ Check if all test names are unique. Returns: True if all test names are unique, False otherwise """ names = [test['name'] for test in self.tests] return len(names) == len(set(names)) def get_duplicate_test_names(self) -> List[str]: """ Get list of duplicate test names. Returns: List of test names that appear more than once """ names = [test['name'] for test in self.tests] seen = set() duplicates = set() for name in names: if name in seen: duplicates.add(name) seen.add(name) return list(duplicates) # ========================================================================= # Statistics # ========================================================================= def get_test_statistics(self) -> Dict[str, int]: """ Get statistics about the generated tests. Returns: Dictionary with test statistics """ stats = { 'total': len(self.tests), 'success': 0, 'lex_error': 0, 'runtime_error': 0, 'empty': 0, } for test in self.tests: if test.get('runtime_error'): stats['runtime_error'] += 1 elif test.get('tokens') and test['tokens'][0].get('type') == 'error': stats['lex_error'] += 1 elif not test.get('tokens'): stats['empty'] += 1 else: stats['success'] += 1 return stats def print_statistics(self): """Print test statistics to console.""" stats = self.get_test_statistics() print(f"Test Statistics for {self.__class__.__name__}:") print(f" Total tests: {stats['total']}") print(f" Success tests: {stats['success']}") print(f" Lex error tests: {stats['lex_error']}") print(f" Runtime error tests: {stats['runtime_error']}") print(f" Empty tests: {stats['empty']}") if not self.validate_test_names_unique(): duplicates = self.get_duplicate_test_names() print(f" WARNING: Duplicate test names found: {duplicates}")