-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathconftest.py
More file actions
220 lines (173 loc) · 7.73 KB
/
conftest.py
File metadata and controls
220 lines (173 loc) · 7.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""
Pytest configuration and fixtures for TRS-80 Color Computer BASIC Emulator tests.
This file provides common fixtures and configuration that will be available
to all test files automatically.
"""
import pytest
import sys
import os
import tempfile
import shutil
from typing import Dict, List, Any
# Add project root to Python path
sys.path.insert(0, os.path.dirname(__file__))
from emulator.core import CoCoBasic
@pytest.fixture
def basic():
"""
Provide a fresh CoCoBasic instance for each test.
This fixture automatically creates a new emulator instance and cleans
up after each test to ensure test isolation.
"""
emulator = CoCoBasic()
emulator.process_command('NEW') # Clear any existing state
yield emulator
# Cleanup happens automatically when the fixture goes out of scope
@pytest.fixture
def basic_with_program(basic):
"""
Provide a CoCoBasic instance with a simple test program loaded.
Useful for tests that need a basic program structure.
"""
program_lines = [
'10 PRINT "HELLO"',
'20 A = 5',
'30 PRINT A',
'40 END'
]
for line in program_lines:
basic.process_command(line)
return basic
@pytest.fixture
def graphics_basic(basic):
"""
Provide a CoCoBasic instance with graphics mode initialized.
Useful for graphics-related tests.
"""
basic.process_command('PMODE 4,1')
basic.process_command('PCLS')
return basic
# Helper functions available to all tests
class TestHelpers:
"""Collection of helper methods for tests"""
@staticmethod
def get_text_output(results: List[Dict[str, Any]]) -> List[str]:
"""Extract text output from command results"""
return [item['text'] for item in results if item.get('type') == 'text']
@staticmethod
def get_error_messages(results: List[Dict[str, Any]]) -> List[str]:
"""Extract error messages from command results"""
return [item['message'] for item in results if item.get('type') == 'error']
@staticmethod
def get_graphics_output(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Extract graphics output from command results"""
graphics_types = {'pmode', 'pset', 'preset', 'line', 'circle', 'draw', 'pcls',
'set_pmode', 'clear_graphics', 'turtle_graphics', 'paint'}
return [item for item in results if item.get('type') in graphics_types]
@staticmethod
def assert_variable_equals(basic: CoCoBasic, var_name: str, expected_value: Any):
"""Assert that a variable has the expected value"""
actual_value = basic.variables.get(var_name.upper())
assert actual_value == expected_value, f"Variable {var_name}: expected {expected_value}, got {actual_value}"
@staticmethod
def assert_array_element_equals(basic: CoCoBasic, array_name: str, indices: List[int], expected_value: Any):
"""Assert that an array element has the expected value"""
array_name = array_name.upper()
if array_name in basic.arrays:
try:
actual_value = basic.arrays[array_name]
for index in indices:
actual_value = actual_value[index]
assert actual_value == expected_value, f"Array {array_name}[{indices}]: expected {expected_value}, got {actual_value}"
except (IndexError, KeyError):
pytest.fail(f"Array element {array_name}[{indices}] does not exist")
else:
pytest.fail(f"Array {array_name} does not exist")
@staticmethod
def assert_error_output(basic: CoCoBasic, command: str, expected_error: str = None):
"""Assert that a command produces an error"""
result = basic.process_command(command)
if not result or result[0].get('type') != 'error':
pytest.fail(f"Expected error from command '{command}', got: {result}")
if expected_error is not None:
actual_error = result[0].get('message', '')
if expected_error not in actual_error:
pytest.fail(f"Expected error containing '{expected_error}', got '{actual_error}'")
@staticmethod
def load_program(basic: CoCoBasic, program_lines: List[str]):
"""Load a program from a list of line strings"""
basic.process_command('NEW')
for line in program_lines:
basic.process_command(line)
@staticmethod
def execute_program(basic: CoCoBasic, program_lines: List[str]) -> List[Dict[str, Any]]:
"""Load and run a program, returning the execution results"""
TestHelpers.load_program(basic, program_lines)
return basic.process_command('RUN')
@pytest.fixture
def helpers():
"""Provide the TestHelpers class to tests"""
return TestHelpers
# Pytest hooks and configuration
def pytest_configure(config):
"""Configure pytest with custom settings"""
# Register custom markers
config.addinivalue_line("markers", "unit: Unit tests for individual components")
config.addinivalue_line("markers", "integration: Integration tests for component interactions")
config.addinivalue_line("markers", "e2e: End-to-end tests")
config.addinivalue_line("markers", "slow: Tests that take longer than 1 second")
config.addinivalue_line("markers", "graphics: Tests that involve graphics operations")
config.addinivalue_line("markers", "io: Tests that involve input/output operations")
config.addinivalue_line("markers", "cli: Tests that involve CLI interactions")
config.addinivalue_line("markers", "websocket: Tests that involve WebSocket functionality")
config.addinivalue_line("markers", "regression: Regression tests for specific bugs")
def pytest_collection_modifyitems(config, items):
"""Modify test items during collection"""
# Add markers based on test file paths
for item in items:
# Add unit marker to tests in tests/unit/
if "tests/unit/" in str(item.fspath):
item.add_marker(pytest.mark.unit)
# Add integration marker to tests in tests/integration/
elif "tests/integration/" in str(item.fspath):
item.add_marker(pytest.mark.integration)
# Add e2e marker to tests in tests/integration/e2e/
if "tests/integration/e2e/" in str(item.fspath):
item.add_marker(pytest.mark.e2e)
# Add cli marker to tests in tests/integration/cli/
if "tests/integration/cli/" in str(item.fspath):
item.add_marker(pytest.mark.cli)
# Add markers based on test names
if "graphics" in item.name.lower():
item.add_marker(pytest.mark.graphics)
if "input" in item.name.lower() or "output" in item.name.lower():
item.add_marker(pytest.mark.io)
if "websocket" in item.name.lower():
item.add_marker(pytest.mark.websocket)
@pytest.fixture
def temp_programs_dir():
"""Provide a temporary directory with a programs/ subdirectory.
Autouse in test classes that test file operations (SAVE, LOAD, FILES, KILL, CD).
Changes cwd to the temp directory and restores it on teardown.
"""
test_directory = tempfile.mkdtemp(prefix='basicoco_test_')
original_cwd = os.getcwd()
os.chdir(test_directory)
programs_dir = os.path.join(test_directory, 'programs')
os.makedirs(programs_dir, exist_ok=True)
yield programs_dir
os.chdir(original_cwd)
if os.path.exists(test_directory):
shutil.rmtree(test_directory)
@pytest.fixture(scope="session")
def test_data_dir():
"""Provide path to test data directory"""
return os.path.join(os.path.dirname(__file__), "tests", "data")
# Performance fixtures
@pytest.fixture
def benchmark_basic(basic):
"""
Provide a basic instance for performance benchmarking.
Can be used with pytest-benchmark if installed.
"""
return basic