pytest-plugin-utils 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_plugin_utils-0.1.0 → pytest_plugin_utils-0.2.0}/PKG-INFO +43 -3
- {pytest_plugin_utils-0.1.0 → pytest_plugin_utils-0.2.0}/README.md +42 -1
- {pytest_plugin_utils-0.1.0 → pytest_plugin_utils-0.2.0}/pyproject.toml +4 -4
- {pytest_plugin_utils-0.1.0 → pytest_plugin_utils-0.2.0}/pytest_plugin_utils/__init__.py +0 -1
- pytest_plugin_utils-0.2.0/pytest_plugin_utils/artifacts.py +64 -0
- {pytest_plugin_utils-0.1.0 → pytest_plugin_utils-0.2.0}/pytest_plugin_utils/config.py +37 -15
- pytest_plugin_utils-0.1.0/pytest_plugin_utils/artifacts.py +0 -119
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: pytest-plugin-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Reusable configuration and artifact utilities for building pytest plugins
|
|
5
5
|
Keywords: pytest,plugin,testing,utilities
|
|
6
6
|
Author: Michael Bianco
|
|
7
7
|
Author-email: Michael Bianco <mike@mikebian.co>
|
|
8
|
-
Requires-Dist: structlog-config>=0.10.0
|
|
9
8
|
Requires-Python: >=3.12
|
|
10
9
|
Project-URL: Repository, https://github.com/iloveitaly/pytest-plugin-utils
|
|
11
10
|
Description-Content-Type: text/markdown
|
|
@@ -31,7 +30,9 @@ uv add pytest-plugin-utils
|
|
|
31
30
|
|
|
32
31
|
### Configuration Options
|
|
33
32
|
|
|
34
|
-
Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference
|
|
33
|
+
Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference.
|
|
34
|
+
|
|
35
|
+
#### For Plugin Authors
|
|
35
36
|
|
|
36
37
|
```python
|
|
37
38
|
from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
|
|
@@ -55,6 +56,38 @@ def pytest_configure(config):
|
|
|
55
56
|
api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
|
|
56
57
|
```
|
|
57
58
|
|
|
59
|
+
#### For Plugin Users
|
|
60
|
+
|
|
61
|
+
Once a plugin has registered options using this package, users can configure them in three ways (in order of precedence):
|
|
62
|
+
|
|
63
|
+
1. **Command Line** (highest priority):
|
|
64
|
+
```bash
|
|
65
|
+
pytest --api-url=https://prod.example.com
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
2. **INI Configuration** (medium priority):
|
|
69
|
+
|
|
70
|
+
In `pytest.ini`:
|
|
71
|
+
```ini
|
|
72
|
+
[pytest]
|
|
73
|
+
api_url = https://staging.example.com
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Or in `pyproject.toml`:
|
|
77
|
+
```toml
|
|
78
|
+
[tool.pytest.ini_options]
|
|
79
|
+
api_url = "https://staging.example.com"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
3. **Runtime/Programmatic** (via conftest.py):
|
|
83
|
+
```python
|
|
84
|
+
def pytest_configure(config):
|
|
85
|
+
# Override at runtime
|
|
86
|
+
config.option.api_url = "https://custom.example.com"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The value resolution follows this precedence chain, with each level overriding the next: Runtime > CLI > INI > Default.
|
|
90
|
+
|
|
58
91
|
### Artifact Directory Management
|
|
59
92
|
|
|
60
93
|
Create per-test artifact directories with sanitized names:
|
|
@@ -81,6 +114,13 @@ def pytest_runtest_setup(item):
|
|
|
81
114
|
* Per-test artifact directory creation and resolution
|
|
82
115
|
* Type-safe configuration retrieval with warnings on mismatches
|
|
83
116
|
|
|
117
|
+
## Related Projects
|
|
118
|
+
|
|
119
|
+
* [pytest-playwright-visual-snapshot](https://github.com/iloveitaly/pytest-playwright-visual-snapshot): Easy pytest visual regression testing using playwright.
|
|
120
|
+
* [pytest-line-runner](https://github.com/iloveitaly/pytest-line-runner): Run pytest tests by line number instead of exact test name.
|
|
121
|
+
* [pytest-celery-utils](https://github.com/iloveitaly/pytest-celery-utils): Pytest plugin for inspecting Celery task queues in Redis during tests.
|
|
122
|
+
* [pytest-playwright-artifacts](https://github.com/iloveitaly/pytest-playwright-artifacts): Pytest plugin that captures HTML, screenshots, and console logs on Playwright test failures.
|
|
123
|
+
|
|
84
124
|
## [MIT License](LICENSE.md)
|
|
85
125
|
|
|
86
126
|
---
|
|
@@ -19,7 +19,9 @@ uv add pytest-plugin-utils
|
|
|
19
19
|
|
|
20
20
|
### Configuration Options
|
|
21
21
|
|
|
22
|
-
Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference
|
|
22
|
+
Register pytest options with automatic precedence handling (runtime > CLI > INI > defaults) and type inference.
|
|
23
|
+
|
|
24
|
+
#### For Plugin Authors
|
|
23
25
|
|
|
24
26
|
```python
|
|
25
27
|
from pytest_plugin_utils import set_pytest_option, register_pytest_options, get_pytest_option
|
|
@@ -43,6 +45,38 @@ def pytest_configure(config):
|
|
|
43
45
|
api_url = get_pytest_option(__package__, config, "api_url", type_hint=str)
|
|
44
46
|
```
|
|
45
47
|
|
|
48
|
+
#### For Plugin Users
|
|
49
|
+
|
|
50
|
+
Once a plugin has registered options using this package, users can configure them in three ways (in order of precedence):
|
|
51
|
+
|
|
52
|
+
1. **Command Line** (highest priority):
|
|
53
|
+
```bash
|
|
54
|
+
pytest --api-url=https://prod.example.com
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. **INI Configuration** (medium priority):
|
|
58
|
+
|
|
59
|
+
In `pytest.ini`:
|
|
60
|
+
```ini
|
|
61
|
+
[pytest]
|
|
62
|
+
api_url = https://staging.example.com
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or in `pyproject.toml`:
|
|
66
|
+
```toml
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
api_url = "https://staging.example.com"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
3. **Runtime/Programmatic** (via conftest.py):
|
|
72
|
+
```python
|
|
73
|
+
def pytest_configure(config):
|
|
74
|
+
# Override at runtime
|
|
75
|
+
config.option.api_url = "https://custom.example.com"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The value resolution follows this precedence chain, with each level overriding the next: Runtime > CLI > INI > Default.
|
|
79
|
+
|
|
46
80
|
### Artifact Directory Management
|
|
47
81
|
|
|
48
82
|
Create per-test artifact directories with sanitized names:
|
|
@@ -69,6 +103,13 @@ def pytest_runtest_setup(item):
|
|
|
69
103
|
* Per-test artifact directory creation and resolution
|
|
70
104
|
* Type-safe configuration retrieval with warnings on mismatches
|
|
71
105
|
|
|
106
|
+
## Related Projects
|
|
107
|
+
|
|
108
|
+
* [pytest-playwright-visual-snapshot](https://github.com/iloveitaly/pytest-playwright-visual-snapshot): Easy pytest visual regression testing using playwright.
|
|
109
|
+
* [pytest-line-runner](https://github.com/iloveitaly/pytest-line-runner): Run pytest tests by line number instead of exact test name.
|
|
110
|
+
* [pytest-celery-utils](https://github.com/iloveitaly/pytest-celery-utils): Pytest plugin for inspecting Celery task queues in Redis during tests.
|
|
111
|
+
* [pytest-playwright-artifacts](https://github.com/iloveitaly/pytest-playwright-artifacts): Pytest plugin that captures HTML, screenshots, and console logs on Playwright test failures.
|
|
112
|
+
|
|
72
113
|
## [MIT License](LICENSE.md)
|
|
73
114
|
|
|
74
115
|
---
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "pytest-plugin-utils"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Reusable configuration and artifact utilities for building pytest plugins"
|
|
5
5
|
keywords = ["pytest", "plugin", "testing", "utilities"]
|
|
6
6
|
readme = "README.md"
|
|
7
7
|
requires-python = ">=3.12"
|
|
8
|
-
dependencies = [
|
|
8
|
+
dependencies = []
|
|
9
9
|
authors = [{ name = "Michael Bianco", email = "mike@mikebian.co" }]
|
|
10
10
|
urls = { "Repository" = "https://github.com/iloveitaly/pytest-plugin-utils" }
|
|
11
11
|
|
|
12
12
|
# additional packaging information: https://packaging.python.org/en/latest/specifications/core-metadata/#license
|
|
13
13
|
|
|
14
14
|
[build-system]
|
|
15
|
-
requires = ["uv_build>=0.10.0"]
|
|
15
|
+
requires = ["uv_build>=0.10.0,<0.11"]
|
|
16
16
|
build-backend = "uv_build"
|
|
17
17
|
|
|
18
18
|
[tool.uv.build-backend]
|
|
@@ -30,7 +30,7 @@ dev = [
|
|
|
30
30
|
]
|
|
31
31
|
|
|
32
32
|
[tool.pyright]
|
|
33
|
-
exclude = ["examples/", "playground/", "tmp/", ".venv/"
|
|
33
|
+
exclude = ["examples/", "playground/", "tmp/", ".venv/"]
|
|
34
34
|
|
|
35
35
|
[tool.pytest.ini_options]
|
|
36
36
|
addopts = "--cov --cov-report=term-missing --cov-report=html:tmp/htmlcov"
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from pytest_plugin_utils.artifacts import (
|
|
2
2
|
get_artifact_dir as get_artifact_dir,
|
|
3
3
|
sanitize_for_artifacts as sanitize_for_artifacts,
|
|
4
|
-
set_artifact_dir_option as set_artifact_dir_option,
|
|
5
4
|
)
|
|
6
5
|
from pytest_plugin_utils.config import (
|
|
7
6
|
get_pytest_option as get_pytest_option,
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Path handling utilities for pytest artifact management.
|
|
3
|
+
|
|
4
|
+
This module contains logic for determining where artifacts should be stored
|
|
5
|
+
for individual tests, including sanitization of test names and resolution
|
|
6
|
+
of output directories.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def sanitize_for_artifacts(text: str) -> str:
|
|
16
|
+
"""
|
|
17
|
+
Sanitize a test nodeid or name for use as a directory name.
|
|
18
|
+
|
|
19
|
+
This function replaces characters that are not alphanumeric or hyphens
|
|
20
|
+
with a single hyphen, and removes leading/trailing hyphens. This ensures
|
|
21
|
+
that the resulting string is safe to use as a directory name on most
|
|
22
|
+
file systems.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> sanitize_for_artifacts("test_file.py::test_func[param]")
|
|
26
|
+
'test-file-py-test-func-param'
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
text: The text to sanitize (e.g., a test nodeid).
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
A sanitized string safe for use as a directory name.
|
|
33
|
+
"""
|
|
34
|
+
sanitized = re.sub(r"[^A-Za-z0-9]+", "-", text)
|
|
35
|
+
sanitized = re.sub(r"-+", "-", sanitized).strip("-")
|
|
36
|
+
return sanitized or "unknown-test"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_artifact_dir(
|
|
40
|
+
item: pytest.Item, base_dir: Path, *, create: bool = False
|
|
41
|
+
) -> Path:
|
|
42
|
+
"""
|
|
43
|
+
Get or create the artifact directory for a specific test item.
|
|
44
|
+
|
|
45
|
+
This function determines the subdirectory for the specific test item
|
|
46
|
+
using its sanitized nodeid, relative to the provided base_dir.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
item: The pytest.Item (test case) for which to get the directory.
|
|
50
|
+
base_dir: The root output directory for artifacts.
|
|
51
|
+
create: If True, creates the artifact directory and its parents if they do not exist.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A pathlib.Path object pointing to the specific test's artifact directory.
|
|
55
|
+
"""
|
|
56
|
+
if create:
|
|
57
|
+
base_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
per_test_dir = base_dir / sanitize_for_artifacts(item.nodeid)
|
|
60
|
+
|
|
61
|
+
if create:
|
|
62
|
+
per_test_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
return per_test_dir
|
|
@@ -10,11 +10,11 @@ import warnings
|
|
|
10
10
|
from dataclasses import dataclass
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import logging
|
|
14
14
|
from _pytest.config import Config
|
|
15
15
|
from _pytest.config.argparsing import Parser
|
|
16
16
|
|
|
17
|
-
log =
|
|
17
|
+
log = logging.getLogger(__package__)
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
@@ -136,7 +136,12 @@ def register_pytest_options(namespace: str, parser: Parser) -> None:
|
|
|
136
136
|
if opt.available in ("all", "cli_option"):
|
|
137
137
|
cli_name = f"--{opt.name.replace('_', '-')}"
|
|
138
138
|
# CRITICAL: We set default=None here so CLI allows fallback to INI/Runtime
|
|
139
|
-
|
|
139
|
+
if opt.type_hint is bool:
|
|
140
|
+
parser.addoption(
|
|
141
|
+
cli_name, action="store_true", default=None, help=help_text
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
parser.addoption(cli_name, action="store", default=None, help=help_text)
|
|
140
145
|
|
|
141
146
|
# INI Registration
|
|
142
147
|
if opt.available in ("all", "ini"):
|
|
@@ -150,7 +155,7 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
|
|
|
150
155
|
This handles cases where CLI arguments (always strings) need conversion,
|
|
151
156
|
or where default values might not match the strict type.
|
|
152
157
|
"""
|
|
153
|
-
log.debug("casting value",
|
|
158
|
+
log.debug("casting value raw_value=%s target_type=%s", value, type_hint)
|
|
154
159
|
|
|
155
160
|
if type_hint is None:
|
|
156
161
|
return value
|
|
@@ -173,14 +178,14 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
|
|
|
173
178
|
# Casting logic for strings (from CLI or raw defaults)
|
|
174
179
|
if type_hint is bool and isinstance(value, str):
|
|
175
180
|
result = value.lower() in ("true", "1", "yes", "on")
|
|
176
|
-
log.debug("converted string to bool",
|
|
181
|
+
log.debug("converted string to bool converted_value=%s", result)
|
|
177
182
|
return result
|
|
178
183
|
|
|
179
184
|
if origin is list and isinstance(value, str):
|
|
180
185
|
# list("foo") produces ['f', 'o', 'o'], so handle string-to-list specially
|
|
181
186
|
# by splitting on newlines (CLI args or raw strings from config)
|
|
182
187
|
result = [v.strip() for v in value.splitlines() if v.strip()]
|
|
183
|
-
log.debug("converted string to list",
|
|
188
|
+
log.debug("converted string to list converted_value=%s", result)
|
|
184
189
|
return result
|
|
185
190
|
|
|
186
191
|
# Generic fallback: call type_hint(value) as constructor
|
|
@@ -189,18 +194,30 @@ def _smart_cast[T](value: t.Any, type_hint: type[T] | None) -> T | t.Any:
|
|
|
189
194
|
result = t.cast(type, origin)(value)
|
|
190
195
|
else:
|
|
191
196
|
result = t.cast(type, type_hint)(value)
|
|
192
|
-
log.debug("converted using type constructor",
|
|
197
|
+
log.debug("converted using type constructor converted_value=%s", result)
|
|
193
198
|
return result
|
|
194
199
|
except (TypeError, ValueError) as e:
|
|
195
|
-
log.debug("failed to convert value",
|
|
200
|
+
log.debug("failed to convert value error=%s", str(e))
|
|
196
201
|
raise TypeError(
|
|
197
202
|
f"Cannot cast value of type {type(value)} to {type_hint}"
|
|
198
203
|
) from e
|
|
199
204
|
|
|
200
205
|
|
|
206
|
+
@t.overload
|
|
201
207
|
def get_pytest_option[T](
|
|
202
|
-
namespace: str, config: Config, key: str, *, type_hint: type[T]
|
|
203
|
-
) -> T |
|
|
208
|
+
namespace: str, config: Config, key: str, *, type_hint: type[T]
|
|
209
|
+
) -> T | None: ...
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@t.overload
|
|
213
|
+
def get_pytest_option(
|
|
214
|
+
namespace: str, config: Config, key: str, *, type_hint: None = None
|
|
215
|
+
) -> t.Any | None: ...
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_pytest_option(
|
|
219
|
+
namespace: str, config: Config, key: str, *, type_hint: t.Any | None = None
|
|
220
|
+
) -> t.Any | None:
|
|
204
221
|
"""
|
|
205
222
|
Retrieve a configuration value from runtime overrides, CLI, or INI files.
|
|
206
223
|
|
|
@@ -219,7 +236,10 @@ def get_pytest_option[T](
|
|
|
219
236
|
The resolved value, optionally casted. Returns None if not found.
|
|
220
237
|
"""
|
|
221
238
|
log.debug(
|
|
222
|
-
"getting pytest option
|
|
239
|
+
"getting pytest option namespace=%s key=%s type_hint=%s",
|
|
240
|
+
namespace,
|
|
241
|
+
key,
|
|
242
|
+
type_hint,
|
|
223
243
|
)
|
|
224
244
|
|
|
225
245
|
normalized_key = key.replace("-", "_")
|
|
@@ -261,7 +281,7 @@ def get_pytest_option[T](
|
|
|
261
281
|
val = opt.default
|
|
262
282
|
source = "default"
|
|
263
283
|
|
|
264
|
-
log.debug("resolved raw value", key
|
|
284
|
+
log.debug("resolved raw value key=%s raw_value=%s source=%s", key, val, source)
|
|
265
285
|
|
|
266
286
|
# Determine effective type hint
|
|
267
287
|
effective_type_hint = type_hint
|
|
@@ -272,16 +292,18 @@ def get_pytest_option[T](
|
|
|
272
292
|
if val is not None and effective_type_hint is not None:
|
|
273
293
|
try:
|
|
274
294
|
result = _smart_cast(val, effective_type_hint)
|
|
275
|
-
log.debug(
|
|
295
|
+
log.debug(
|
|
296
|
+
"returning converted value key=%s converted_value=%s", key, result
|
|
297
|
+
)
|
|
276
298
|
return result
|
|
277
299
|
except TypeError as e:
|
|
278
300
|
# warning? or just return val?
|
|
279
301
|
# Let's log a warning and return val to be safe
|
|
280
302
|
warnings.warn(f"Failed to cast option '{key}': {e}")
|
|
281
303
|
log.debug(
|
|
282
|
-
"returning raw value after conversion failure", key
|
|
304
|
+
"returning raw value after conversion failure key=%s value=%s", key, val
|
|
283
305
|
)
|
|
284
306
|
return val
|
|
285
307
|
|
|
286
|
-
log.debug("returning raw value", key
|
|
308
|
+
log.debug("returning raw value key=%s value=%s", key, val)
|
|
287
309
|
return val
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Path handling utilities for pytest artifact management.
|
|
3
|
-
|
|
4
|
-
This module contains logic for determining where artifacts should be stored
|
|
5
|
-
for individual tests, including sanitization of test names and resolution
|
|
6
|
-
of output directories. The artifact directory option name can be customized
|
|
7
|
-
via set_artifact_dir_option().
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import re
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
import pytest
|
|
14
|
-
|
|
15
|
-
from .config import get_pytest_option
|
|
16
|
-
|
|
17
|
-
_artifact_dir_options: dict[str, str] = {}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def set_artifact_dir_option(namespace: str, option_name: str) -> None:
|
|
21
|
-
"""
|
|
22
|
-
Set the pytest option name used for the artifact output directory.
|
|
23
|
-
|
|
24
|
-
This function should typically be called in pytest_configure() to customize
|
|
25
|
-
the option name before any tests run. It allows this module to be reused
|
|
26
|
-
by other pytest plugins that need different option names.
|
|
27
|
-
|
|
28
|
-
Example:
|
|
29
|
-
# In your conftest.py or plugin module:
|
|
30
|
-
from pytest_plugin_utils.artifacts import set_artifact_dir_option
|
|
31
|
-
from pytest_plugin_utils.config import set_pytest_option
|
|
32
|
-
|
|
33
|
-
def pytest_configure(config):
|
|
34
|
-
# Register your custom option
|
|
35
|
-
set_pytest_option(
|
|
36
|
-
__package__,
|
|
37
|
-
"my_artifacts_output",
|
|
38
|
-
default="my-test-results",
|
|
39
|
-
help="Directory for test artifacts",
|
|
40
|
-
available="cli_option",
|
|
41
|
-
type_hint=str,
|
|
42
|
-
)
|
|
43
|
-
# Configure paths module to use it
|
|
44
|
-
set_artifact_dir_option(__package__, "my_artifacts_output")
|
|
45
|
-
|
|
46
|
-
Args:
|
|
47
|
-
namespace: Unique namespace for this plugin (typically __package__).
|
|
48
|
-
option_name: The pytest option name (without '--' prefix, with underscores).
|
|
49
|
-
"""
|
|
50
|
-
_artifact_dir_options[namespace] = option_name
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_artifact_dir_option(namespace: str) -> str:
|
|
54
|
-
"""
|
|
55
|
-
Get the currently configured artifact directory option name.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
namespace: Unique namespace for this plugin (typically __package__).
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
The pytest option name used for the artifact output directory.
|
|
62
|
-
"""
|
|
63
|
-
assert namespace in _artifact_dir_options, (
|
|
64
|
-
f"call set_artifact_dir_option({namespace!r}, ...) before using get_artifact_dir_option()"
|
|
65
|
-
)
|
|
66
|
-
return _artifact_dir_options[namespace]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def sanitize_for_artifacts(text: str) -> str:
|
|
70
|
-
"""
|
|
71
|
-
Sanitize a test nodeid or name for use as a directory name.
|
|
72
|
-
|
|
73
|
-
This function replaces characters that are not alphanumeric or hyphens
|
|
74
|
-
with a single hyphen, and removes leading/trailing hyphens. This ensures
|
|
75
|
-
that the resulting string is safe to use as a directory name on most
|
|
76
|
-
file systems.
|
|
77
|
-
|
|
78
|
-
Example:
|
|
79
|
-
>>> sanitize_for_artifacts("test_file.py::test_func[param]")
|
|
80
|
-
'test-file-py-test-func-param'
|
|
81
|
-
|
|
82
|
-
Args:
|
|
83
|
-
text: The text to sanitize (e.g., a test nodeid).
|
|
84
|
-
|
|
85
|
-
Returns:
|
|
86
|
-
A sanitized string safe for use as a directory name.
|
|
87
|
-
"""
|
|
88
|
-
sanitized = re.sub(r"[^A-Za-z0-9]+", "-", text)
|
|
89
|
-
sanitized = re.sub(r"-+", "-", sanitized).strip("-")
|
|
90
|
-
return sanitized or "unknown-test"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def get_artifact_dir(namespace: str, item: pytest.Item) -> Path:
|
|
94
|
-
"""
|
|
95
|
-
Get or create the artifact directory for a specific test item.
|
|
96
|
-
|
|
97
|
-
This function determines the root output directory based on the configured
|
|
98
|
-
artifact directory option (see set_artifact_dir_option). It then creates
|
|
99
|
-
a subdirectory for the specific test item using its sanitized nodeid.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
namespace: Unique namespace for this plugin (typically __package__).
|
|
103
|
-
item: The pytest.Item (test case) for which to get the directory.
|
|
104
|
-
|
|
105
|
-
Returns:
|
|
106
|
-
A pathlib.Path object pointing to the specific test's artifact directory.
|
|
107
|
-
The directory and its parents are created if they do not exist.
|
|
108
|
-
"""
|
|
109
|
-
assert namespace in _artifact_dir_options, (
|
|
110
|
-
f"call set_artifact_dir_option({namespace!r}, ...) before using get_artifact_dir()"
|
|
111
|
-
)
|
|
112
|
-
option_name = _artifact_dir_options[namespace]
|
|
113
|
-
output_path = get_pytest_option(namespace, item.config, option_name, type_hint=Path)
|
|
114
|
-
assert output_path
|
|
115
|
-
output_path.mkdir(parents=True, exist_ok=True)
|
|
116
|
-
|
|
117
|
-
per_test_dir = output_path / sanitize_for_artifacts(item.nodeid)
|
|
118
|
-
per_test_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
-
return per_test_dir
|