-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathconftest.py
More file actions
312 lines (242 loc) · 10.2 KB
/
Copy pathconftest.py
File metadata and controls
312 lines (242 loc) · 10.2 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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""
Root-level conftest.py for NeMo Platform
This file contains shared fixtures and configuration that are available to all tests
in the repository. Individual packages and services can add their own conftest.py
files for more specific fixtures.
"""
import logging
import os
import tempfile
from pathlib import Path
from typing import Generator
import pytest
from nmp.testing.pytest_outcomes import pytest_skip as skip_test
from tests.discovery_exclusions import TEST_DISCOVERY_EXCLUSIONS
# Set test environment variables BEFORE any imports
# This must happen at module level, before pytest even starts processing
if "MODE" not in os.environ:
os.environ["MODE"] = "development"
# Silence config warnings during tests - services don't need the config file
# when using create_test_client() which programmatically sets configuration overrides
if "NMP_CONFIG_WARNINGS_DISABLED" not in os.environ:
os.environ["NMP_CONFIG_WARNINGS_DISABLED"] = "1"
# ============================================================================
# Suppress httpcore debug logging during cleanup
# This prevents "I/O operation on closed file" errors when huggingface_hub's
# atexit handler tries to log debug messages after pytest closes stdout/stderr
# ============================================================================
_httpcore_logger = logging.getLogger("httpcore")
_httpcore_logger.setLevel(logging.WARNING)
_httpcore_logger.propagate = False # Prevent any handlers from receiving these messages
_httpx_logger = logging.getLogger("httpx")
_httpx_logger.setLevel(logging.WARNING)
_httpx_logger.propagate = False
# ============================================================================
# Session-level fixtures
# ============================================================================
@pytest.fixture(scope="session", autouse=True)
def cleanup_huggingface_hub_session():
"""
Clean up huggingface_hub HTTP sessions before pytest shuts down.
This prevents "I/O operation on closed file" logging errors that occur when
huggingface_hub's atexit handler tries to close HTTP connections after
pytest has already closed stdout/stderr.
"""
yield
# Suppress httpcore logging during cleanup to prevent errors on closed streams
# This must be done again here in case new loggers were created during tests
logging.getLogger("httpcore").setLevel(logging.CRITICAL)
logging.getLogger("httpcore").propagate = False
logging.getLogger("httpcore").disabled = True
# Clean up huggingface_hub's HTTP client before pytest closes streams
try:
from huggingface_hub.utils import _http
# Close the session if it exists
if hasattr(_http, "get_session"):
# Newer versions use get_session()
try:
session = _http.get_session()
if session is not None:
session.close()
except Exception:
pass
# Also try the module-level close function
if hasattr(_http, "close_session"):
try:
_http.close_session()
except Exception:
pass
except (ImportError, AttributeError):
pass # huggingface_hub not installed or structure changed
@pytest.fixture(scope="session")
def test_data_dir() -> Path:
"""Return the path to the test data directory."""
return Path(__file__).parent / "tests" / "data"
@pytest.fixture(scope="session")
def repo_root() -> Path:
"""Return the path to the repository root."""
return Path(__file__).parent
# ============================================================================
# Function-level fixtures
# ============================================================================
@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""
Create a temporary directory for a test.
The directory is automatically cleaned up after the test completes.
"""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def temp_file() -> Generator[Path, None, None]:
"""
Create a temporary file for a test.
The file is automatically cleaned up after the test completes.
"""
with tempfile.NamedTemporaryFile(mode="w", delete=False) as tmpfile:
tmpfile_path = Path(tmpfile.name)
yield tmpfile_path
# Cleanup
if tmpfile_path.exists():
tmpfile_path.unlink()
@pytest.fixture
def mock_env_vars(monkeypatch) -> dict:
"""
Provide a way to set environment variables for a test.
Usage:
def test_something(mock_env_vars):
mock_env_vars['MY_VAR'] = 'value'
# Test code that reads MY_VAR
"""
env_vars = {}
def set_env(key: str, value: str):
env_vars[key] = value
monkeypatch.setenv(key, value)
# Return a dict-like object that sets env vars when assigned
class EnvVarSetter(dict):
def __setitem__(self, key, value):
set_env(key, value)
super().__setitem__(key, value)
return EnvVarSetter()
# ============================================================================
# Pytest hooks
# ============================================================================
def pytest_ignore_collect(collection_path: Path, config) -> bool:
"""Skip test trees with explicit temporary root-CI discovery exclusions."""
try:
relative_path = collection_path.relative_to(Path(__file__).parent)
except ValueError:
return False
return any(
relative_path == excluded_path or excluded_path in relative_path.parents
for excluded_path in TEST_DISCOVERY_EXCLUSIONS
)
def pytest_load_initial_conftests(early_config, parser, args):
"""
Called before any conftest files are loaded.
This is the earliest hook that runs, making it ideal for setting
environment variables that are needed by module-level code in conftest files.
"""
# Set default test environment variables BEFORE any imports happen
# This is necessary because some modules create singleton configs at import time
if "MODE" not in os.environ:
os.environ["MODE"] = "development"
def pytest_configure(config):
"""
Configure pytest with custom settings.
This runs once at the start of the test session.
"""
config.option.importmode = "importlib"
def pytest_collection_modifyitems(config, items):
"""
Modify test items during collection.
Auto-marks tests based on their location:
- Tests in /integration/ directories get the 'integration' marker
- Tests without category markers get the 'unit' marker
"""
# Category markers that determine test type
category_markers = {
"unit",
"e2e",
"smoke_gpu_tasks",
"smoke_nmp_automodel_tasks",
"smoke_nmp_automodel_training",
"integration",
"regression",
"canary",
"slow",
"skip_in_ci",
}
for item in items:
# Get current marker names
marker_names = {marker.name for marker in item.iter_markers()}
fspath_str = str(item.fspath)
# Auto-mark tests in e2e directories
if "/e2e/" in fspath_str:
if "e2e" not in marker_names:
item.add_marker(pytest.mark.e2e)
marker_names.add("e2e")
# Auto-mark integration tests (e.g., /services/core/jobs/tests/integration/)
elif "/integration/" in fspath_str:
if "integration" not in marker_names:
item.add_marker(pytest.mark.integration)
marker_names.add("integration")
# Auto-mark async tests that might be slow
if "async" in item.name and item.get_closest_marker("timeout") is None:
# Give async tests a longer timeout by default
item.add_marker(pytest.mark.timeout(600))
# Auto-mark tests without category markers as unit tests
if not marker_names.intersection(category_markers):
item.add_marker(pytest.mark.unit)
# ============================================================================
# Pytest command-line options
# ============================================================================
def pytest_addoption(parser):
"""
Add custom command-line options for pytest.
"""
parser.addoption(
"--run-slow",
action="store_true",
default=False,
help="Run slow tests (skipped by default)",
)
parser.addoption(
"--run-e2e",
action="store_true",
default=False,
help="Run end-to-end tests (skipped by default in quick test runs)",
)
parser.addoption(
"--run-integration",
action="store_true",
default=True,
help="Run integration tests (enabled by default)",
)
def pytest_runtest_setup(item):
"""
Run before each test to check if it should be skipped based on command-line options.
"""
if "slow" in [marker.name for marker in item.iter_markers()]:
if not item.config.getoption("--run-slow"):
skip_test("Skipping slow test (use --run-slow to run)")
if "e2e" in [marker.name for marker in item.iter_markers()]:
if not item.config.getoption("--run-e2e"):
skip_test("Skipping e2e test (use --run-e2e to run)")
if "subprocess_only" in [marker.name for marker in item.iter_markers()]:
if os.environ.get("NMP_BASE_URL"):
skip_test("Skipping subprocess-only test (NMP_BASE_URL is set)")
if "container_only" in [marker.name for marker in item.iter_markers()]:
if not os.environ.get("NMP_BASE_URL"):
skip_test("Skipping container-only test (requires NMP_BASE_URL)")
from xdist.scheduler.loadscope import LoadScopeScheduling # noqa: E402
# Temporary workaround for https://github.com/pytest-dev/pytest-xdist/issues/1189
# Remove once pytest-xdist > 3.8.0 is released with the fix from PR #1299
_original_reschedule = LoadScopeScheduling._reschedule
def _patched_reschedule(self, node):
if node not in self.registered_collections:
return
_original_reschedule(self, node)
LoadScopeScheduling._reschedule = _patched_reschedule # type: ignore[invalid-assignment]