Added base test generator class

This commit is contained in:
Kyler Olsen 2025-11-12 13:11:40 -07:00
parent cf5f51ccd8
commit 375c3f2422
6 changed files with 483 additions and 286 deletions

View File

@ -3,35 +3,22 @@ Test case generator for integer literals in the Stack Language.
Generates comprehensive test cases for all integer types and bases. Generates comprehensive test cases for all integer types and bases.
""" """
from .integer_tests import generate_integer_literal_tests from .general_tests import GeneralTestGenerator
from .float_tests import generate_float_literal_tests from .integer_tests import IntegerTestGenerator
from .float_tests import FloatTestGenerator
if __name__ == "__main__": if __name__ == "__main__":
# import json
import yaml import yaml
# Generate tests # Generate tests
tests = [] tests = []
tests += generate_integer_literal_tests() tests += GeneralTestGenerator.generate_tests()
tests += generate_float_literal_tests() tests += IntegerTestGenerator.generate_tests()
tests += FloatTestGenerator.generate_tests()
# Print summary # Print summary
print(f"Generated {len(tests)} test cases") print(f"Generated {len(tests)} test cases")
# # Save as JSON
# with open("integer_literal_tests.json", "w") as f:
# json.dump(tests, f, indent=2)
# print("Saved to integer_literal_tests.json")
# # Save as YAML
# with open("integer_literal_tests.yaml", "w") as f:
# yaml.dump(tests, f, default_flow_style=False, sort_keys=False)
# print("Saved to integer_literal_tests.yaml")
# Save as YAML # Save as YAML
with open("cases.yaml", "w") as f: with open("cases.yaml", "w") as f:
yaml.dump(tests, f, default_flow_style=False, sort_keys=False) yaml.dump(tests, f, default_flow_style=False, sort_keys=False)
# Print first few tests as example
# print("\nFirst 3 test cases:")
# print(yaml.dump(tests[:3], default_flow_style=False, sort_keys=False))

View File

@ -0,0 +1,352 @@
from typing import List, Dict, Any, Optional
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.
"""
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
# =========================================================================
# 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}")

View File

@ -1,7 +1,7 @@
from typing import List, Dict, Any, Optional from typing import List, Dict, Any
from .utils import Token, Operation, StackItem, RuntimeError, TestCase, to_dict from .base_tests import BaseTestGenerator
class FloatTestGenerator: class FloatTestGenerator(BaseTestGenerator):
"""Generate test cases for floating point literals.""" """Generate test cases for floating point literals."""
# Special float values # Special float values
@ -20,48 +20,6 @@ class FloatTestGenerator:
} }
} }
def __init__(self):
self.tests = []
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."""
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))
def make_push_op(self, type_name: str, value: Any) -> Operation:
"""Create a push operation."""
return Operation(function="push", type=type_name, value=value)
def make_stack_item(self, type_name: str, value: Any) -> StackItem:
"""Create a stack item."""
return StackItem(type=type_name, value=value)
def make_error_token(self, message: str) -> Token:
"""Create an error token."""
return Token(type="error", value=message)
def make_success_test(self, name: str, code: str, type_name: str, value: float):
"""Create a successful test case."""
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."""
token = self.make_error_token(error_msg)
self.add_test(name, code, [token])
def generate_basic_tests(self): def generate_basic_tests(self):
"""Generate basic test cases.""" """Generate basic test cases."""
# Simple default floats (f64) # Simple default floats (f64)
@ -354,14 +312,3 @@ class FloatTestGenerator:
self.generate_signed_zero_tests() self.generate_signed_zero_tests()
return self.tests return self.tests
def generate_float_literal_tests() -> List[Dict[str, Any]]:
"""
Main function to generate all floating point literal test cases.
Returns:
List of test case dictionaries ready for serialization
"""
generator = FloatTestGenerator()
return generator.generate_all_tests()

View File

@ -0,0 +1,11 @@
from typing import List, Dict, Any
from .base_tests import BaseTestGenerator
class GeneralTestGenerator(BaseTestGenerator):
def generate_all_tests(self) -> List[Dict[str, Any]]:
"""Generate all test cases."""
self.add_test("Empty_Statement", "", [], [], [])
return self.tests

View File

@ -1,7 +1,7 @@
from typing import List, Dict, Any, Optional from typing import List, Dict, Any
from .utils import Token, Operation, StackItem, RuntimeError, TestCase, to_dict from .base_tests import BaseTestGenerator
class IntegerTestGenerator: class IntegerTestGenerator(BaseTestGenerator):
"""Generate test cases for integer literals.""" """Generate test cases for integer literals."""
# Type ranges # Type ranges
@ -16,52 +16,8 @@ class IntegerTestGenerator:
'u64': (0, 18446744073709551615), 'u64': (0, 18446744073709551615),
} }
def __init__(self):
self.tests = []
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."""
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))
def make_push_op(self, type_name: str, value: Any) -> Operation:
"""Create a push operation."""
return Operation(function="push", type=type_name, value=value)
def make_stack_item(self, type_name: str, value: Any) -> StackItem:
"""Create a stack item."""
return StackItem(type=type_name, value=value)
def make_error_token(self, message: str) -> Token:
"""Create an error token."""
return Token(type="error", value=message)
def make_success_test(self, name: str, code: str, type_name: str, value: Any):
"""Create a successful test case."""
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."""
token = self.make_error_token(error_msg)
self.add_test(name, code, [token])
def generate_basic_tests(self): def generate_basic_tests(self):
"""Generate basic test cases.""" """Generate basic test cases."""
# Empty statement
self.add_test("Empty_Statement", "", [], [], [])
# Simple default integers # Simple default integers
self.make_success_test("Integer Default Decimal 0", "0", "i64", 0) self.make_success_test("Integer Default Decimal 0", "0", "i64", 0)
@ -265,14 +221,3 @@ class IntegerTestGenerator:
self.generate_underscore_tests() self.generate_underscore_tests()
return self.tests return self.tests
def generate_integer_literal_tests() -> List[Dict[str, Any]]:
"""
Main function to generate all integer literal test cases.
Returns:
List of test case dictionaries ready for serialization
"""
generator = IntegerTestGenerator()
return generator.generate_all_tests()

View File

@ -1,45 +0,0 @@
from typing import List, Dict, Any, Optional
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):
"""Convert dataclass to dict, removing None values."""
if obj is None:
return 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}