Repository navigation
Expand file tree
/
Copy pathconftest.py
More file actions
257 lines (194 loc) · 7.68 KB
/
Copy pathconftest.py
File metadata and controls
257 lines (194 loc) · 7.68 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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
"""Test utilities."""
from __future__ import annotations
import os
import networkx as nx
import pytest
np = pytest.importorskip("numpy")
from tnfr.constants import inject_defaults
from tnfr.utils import cached_import, prune_failed_imports
STRUCTURAL_ATOL = 1e-12
STRUCTURAL_RTOL = 1e-10
def pytest_addoption(parser: pytest.Parser) -> None:
"""Expose CLI flag to force a specific mathematics backend."""
parser.addoption(
"--math-backend",
action="store",
default=None,
help="Force TNFR_MATH_BACKEND during the session (numpy, jax, torch).",
)
def pytest_configure(config: pytest.Config) -> None:
"""Propagate backend selection from CLI or environment before tests import."""
requested = config.getoption("math_backend")
env_override = os.getenv("TNFR_TEST_MATH_BACKEND")
choice = requested or env_override
if not choice:
return
os.environ["TNFR_MATH_BACKEND"] = choice
# Ensure stale caches do not override the requested backend.
from tnfr.mathematics import backend as backend_module # imported lazily
backend_module._BACKEND_CACHE.clear()
@pytest.fixture(scope="session")
def structural_tolerances() -> dict[str, float]:
"""Return the shared numerical comparison tolerances used in tests."""
return {"atol": STRUCTURAL_ATOL, "rtol": STRUCTURAL_RTOL}
@pytest.fixture
def source_tree_environment(pytestconfig: pytest.Config) -> dict[str, str]:
"""Give an opt-in child process the same configured source paths as pytest.
Pytest's pythonpath setting changes only the current process's sys.path.
Preserve the caller environment without mutating it or affecting tests of
installed distributions, which deliberately do not request this fixture.
"""
environment = os.environ.copy()
paths = [str(path) for path in pytestconfig.getini("pythonpath")]
inherited = environment.get("PYTHONPATH")
if inherited:
paths.append(inherited)
if paths:
environment["PYTHONPATH"] = os.pathsep.join(paths)
return environment
@pytest.fixture
def structural_rng() -> np.random.Generator:
"""Provide a reproducible RNG aligned with TNFR structural conventions."""
return np.random.default_rng(seed=0)
@pytest.fixture
def graph_canon():
"""Return a new graph with default attributes attached."""
def _factory():
G = nx.Graph()
inject_defaults(G)
return G
return _factory
@pytest.fixture(scope="module")
def reset_cached_import():
"""Provide a helper to reset cached import state for tests."""
def _reset() -> None:
cached_import.cache_clear()
prune_failed_imports()
_reset()
yield _reset
_reset()
@pytest.fixture(autouse=True)
def reset_global_state():
"""Reset the selected mutable process caches used by these tests.
Logging flags, callback limits, backend/cache managers, immutable-value
checks and RNG caches have explicit resets below.
Import caches are intentionally retained; reset_cached_import owns opt-in
clearing. Graph-owned callbacks, observers and integrators require fresh
graph fixtures. This is not a claim to reset every possible global object.
"""
# Reset state before test
_reset_all_state()
yield
# Reset state after test
_reset_all_state()
def _reset_all_state() -> None:
"""Apply the supported cache resets without importing retired subsystems."""
# Reset logging configured flag (but don't call _reset_logging_state as it may cause issues)
try:
from tnfr.utils import init as init_module
init_module._LOGGING_CONFIGURED = False
init_module._NP_MISSING_LOGGED = False
# Clear IMPORT_LOG to avoid test interference
if hasattr(init_module, "IMPORT_LOG"):
init_module.IMPORT_LOG.clear()
except (ImportError, AttributeError):
pass
# Reset callback manager
try:
from tnfr.utils.callbacks import callback_manager
# Reset error limit to default
if hasattr(callback_manager, "_error_limit"):
callback_manager._error_limit = 100
callback_manager._error_limit_cache = 100
# Note: Callbacks are stored in graph.graph['callbacks'], not in the manager
# So tests creating fresh graphs will have clean callback state
except ImportError:
pass
# Reset backend cache
try:
from tnfr.mathematics import backend as backend_module
backend_module._BACKEND_CACHE.clear()
except ImportError:
pass
# NOTE: We skip resetting import caches here because it can trigger
# module re-imports that affect logging state, causing test isolation issues.
# The reset_cached_import fixture provides this functionality for tests that need it.
# Reset global cache managers
try:
from tnfr.utils import cache as cache_module
# Reset global cache manager if it exists
cache_module._GLOBAL_CACHE_MANAGER = None
cache_module._GLOBAL_CACHE_LAYER_CONFIG.clear()
except (ImportError, AttributeError):
pass
# Reset immutable cache
try:
from tnfr import immutable as immutable_module
immutable_module._IMMUTABLE_CACHE.clear()
except (ImportError, AttributeError):
pass
# Reset RNG cache (seed_hash cache)
try:
from tnfr import rng as rng_module
if hasattr(rng_module, "seed_hash") and hasattr(
rng_module.seed_hash, "cache_clear"
):
rng_module.seed_hash.cache_clear()
# Reset RNG cache lock flag and cache
rng_module._CACHE_LOCKED = False
if hasattr(rng_module, "_seed_hash_cache"):
rng_module._seed_hash_cache.clear()
if hasattr(rng_module, "_RNG_CACHE_MANAGER"):
# Clear the cache manager layers
manager = rng_module._RNG_CACHE_MANAGER
if hasattr(manager, "clear_all"):
manager.clear_all()
except (ImportError, AttributeError):
pass
@pytest.fixture
def boundary_test_cases() -> dict[str, list[float]]:
"""Provide standard test cases for boundary testing.
Returns standard EPI values near boundaries and in safe ranges
for testing operator behavior at extremes.
Returns
-------
dict[str, list[float]]
Dictionary with 'upper_boundary', 'lower_boundary', and 'safe_values' keys
"""
return {
"upper_boundary": [0.95, 0.99, 0.999, 1.0 - 1e-10],
"lower_boundary": [-0.95, -0.99, -0.999, -1.0 + 1e-10],
"safe_values": [0.0, 0.5, -0.5, 0.8, -0.8],
}
def assert_epi_in_bounds(
epi_value: float, tolerance: float = 1e-9, abs_tol: float = 1e-12
) -> None:
"""Check the configured [-1, 1] EPI interval with numerical tolerance.
This helper uses math.isclose to handle floating-point precision issues
that may occur near boundaries.
Parameters
----------
epi_value : float
The EPI value to check
tolerance : float, default 1e-9
Relative tolerance for boundary comparisons
abs_tol : float, default 1e-12
Absolute tolerance for boundary comparisons
Raises
------
AssertionError
If EPI value is outside [-1.0, 1.0] beyond tolerance
"""
import math
# Check primary bounds
if -1.0 <= epi_value <= 1.0:
return
# Check with tolerance for floating point precision
if epi_value > 1.0:
assert math.isclose(
epi_value, 1.0, rel_tol=tolerance, abs_tol=abs_tol
), f"EPI {epi_value} exceeds upper boundary 1.0 beyond tolerance"
elif epi_value < -1.0:
assert math.isclose(
epi_value, -1.0, rel_tol=tolerance, abs_tol=abs_tol
), f"EPI {epi_value} falls below lower boundary -1.0 beyond tolerance"