YREA-SLS/SLS_Tests/generate_tests/base_tests.py

368 lines
11 KiB
Python

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
ENABLE_EXPONENTIAL_LITERALS = 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}")