provide-testkit 0.0.0.dev0__py3-none-any.whl
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.
- provide/__init__.py +3 -0
- provide/testkit/__init__.py +248 -0
- provide/testkit/archive/__init__.py +24 -0
- provide/testkit/archive/fixtures.py +217 -0
- provide/testkit/cli.py +229 -0
- provide/testkit/common/__init__.py +32 -0
- provide/testkit/common/fixtures.py +234 -0
- provide/testkit/crypto.py +163 -0
- provide/testkit/environment.py +79 -0
- provide/testkit/file/__init__.py +40 -0
- provide/testkit/file/content_fixtures.py +275 -0
- provide/testkit/file/directory_fixtures.py +105 -0
- provide/testkit/file/fixtures.py +49 -0
- provide/testkit/file/special_fixtures.py +141 -0
- provide/testkit/fixtures.py +52 -0
- provide/testkit/harness.py +122 -0
- provide/testkit/hub.py +22 -0
- provide/testkit/logger/__init__.py +39 -0
- provide/testkit/logger/hooks.py +100 -0
- provide/testkit/logger/reset.py +230 -0
- provide/testkit/main.py +22 -0
- provide/testkit/mocking/__init__.py +46 -0
- provide/testkit/mocking/fixtures.py +340 -0
- provide/testkit/process/__init__.py +48 -0
- provide/testkit/process/async_fixtures.py +410 -0
- provide/testkit/process/fixtures.py +54 -0
- provide/testkit/process/subprocess_fixtures.py +208 -0
- provide/testkit/quality/__init__.py +101 -0
- provide/testkit/quality/artifacts.py +360 -0
- provide/testkit/quality/base.py +158 -0
- provide/testkit/quality/cli.py +394 -0
- provide/testkit/quality/complexity/__init__.py +30 -0
- provide/testkit/quality/complexity/analyzer.py +392 -0
- provide/testkit/quality/complexity/fixture.py +196 -0
- provide/testkit/quality/coverage/__init__.py +36 -0
- provide/testkit/quality/coverage/fixture.py +236 -0
- provide/testkit/quality/coverage/reporter.py +150 -0
- provide/testkit/quality/coverage/tracker.py +313 -0
- provide/testkit/quality/decorators.py +380 -0
- provide/testkit/quality/documentation/__init__.py +29 -0
- provide/testkit/quality/documentation/checker.py +361 -0
- provide/testkit/quality/documentation/fixture.py +187 -0
- provide/testkit/quality/profiling/__init__.py +30 -0
- provide/testkit/quality/profiling/fixture.py +332 -0
- provide/testkit/quality/profiling/profiler.py +428 -0
- provide/testkit/quality/report.py +266 -0
- provide/testkit/quality/runner.py +319 -0
- provide/testkit/quality/security/__init__.py +29 -0
- provide/testkit/quality/security/fixture.py +196 -0
- provide/testkit/quality/security/scanner.py +338 -0
- provide/testkit/streams.py +54 -0
- provide/testkit/threading/__init__.py +38 -0
- provide/testkit/threading/basic_fixtures.py +103 -0
- provide/testkit/threading/data_fixtures.py +101 -0
- provide/testkit/threading/execution_fixtures.py +268 -0
- provide/testkit/threading/fixtures.py +50 -0
- provide/testkit/threading/sync_fixtures.py +98 -0
- provide/testkit/time/__init__.py +32 -0
- provide/testkit/time/fixtures.py +416 -0
- provide/testkit/transport/__init__.py +30 -0
- provide/testkit/transport/fixtures.py +278 -0
- provide_testkit-0.0.0.dev0.dist-info/METADATA +145 -0
- provide_testkit-0.0.0.dev0.dist-info/RECORD +66 -0
- provide_testkit-0.0.0.dev0.dist-info/WHEEL +5 -0
- provide_testkit-0.0.0.dev0.dist-info/entry_points.txt +2 -0
- provide_testkit-0.0.0.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common Test Fixtures for Foundation.
|
|
3
|
+
|
|
4
|
+
Provides pytest fixtures for capturing output, setting up telemetry,
|
|
5
|
+
and other common testing scenarios across the Foundation test suite.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from collections.abc import Callable, Generator
|
|
9
|
+
import io
|
|
10
|
+
from typing import TextIO
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from provide.foundation import TelemetryConfig, get_hub
|
|
15
|
+
from provide.testkit.streams import set_log_stream_for_testing
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def captured_stderr_for_foundation() -> Generator[TextIO]:
|
|
20
|
+
"""
|
|
21
|
+
Fixture to capture stderr output from Foundation's logging system.
|
|
22
|
+
|
|
23
|
+
It redirects Foundation's log stream to an `io.StringIO` buffer, yields the buffer
|
|
24
|
+
to the test, and then restores the original stream.
|
|
25
|
+
"""
|
|
26
|
+
current_test_stream = io.StringIO()
|
|
27
|
+
set_log_stream_for_testing(current_test_stream)
|
|
28
|
+
yield current_test_stream
|
|
29
|
+
set_log_stream_for_testing(None)
|
|
30
|
+
current_test_stream.close()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture
|
|
34
|
+
def setup_foundation_telemetry_for_test(
|
|
35
|
+
captured_stderr_for_foundation: TextIO,
|
|
36
|
+
) -> Callable[[TelemetryConfig | None], None]:
|
|
37
|
+
"""
|
|
38
|
+
Fixture providing a function to set up Foundation Telemetry for tests.
|
|
39
|
+
|
|
40
|
+
This fixture captures stderr via `captured_stderr_for_foundation`
|
|
41
|
+
and provides a callable to configure telemetry with custom settings.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def _setup(config: TelemetryConfig | None = None) -> None:
|
|
45
|
+
if config is None:
|
|
46
|
+
config = TelemetryConfig()
|
|
47
|
+
|
|
48
|
+
# Use Hub API directly instead of deprecated setup_telemetry
|
|
49
|
+
hub = get_hub()
|
|
50
|
+
hub.initialize_foundation(config, force=True)
|
|
51
|
+
|
|
52
|
+
return _setup
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Harness Testing Utilities.
|
|
3
|
+
|
|
4
|
+
Provides utilities for testing CLI harnesses with artifact management.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from provide.foundation.process.runner import run_command
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HarnessRunner:
|
|
13
|
+
"""Test harness runner with artifact management."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, artifact_root: Path):
|
|
16
|
+
"""Initialize with artifact root directory."""
|
|
17
|
+
self.artifact_root = artifact_root
|
|
18
|
+
|
|
19
|
+
def run(
|
|
20
|
+
self,
|
|
21
|
+
command: list[str],
|
|
22
|
+
artifact_path: Path | str,
|
|
23
|
+
stdin: str | bytes | None = None,
|
|
24
|
+
timeout: float = 30.0,
|
|
25
|
+
cwd: Path | None = None,
|
|
26
|
+
) -> tuple[int, str, str]:
|
|
27
|
+
"""
|
|
28
|
+
Run command and return text output.
|
|
29
|
+
|
|
30
|
+
For binary output commands, use run_binary() instead.
|
|
31
|
+
"""
|
|
32
|
+
exit_code, stdout_str, stderr_str, _, _ = self._run_internal(
|
|
33
|
+
command, artifact_path, stdin, timeout, cwd
|
|
34
|
+
)
|
|
35
|
+
return exit_code, stdout_str, stderr_str
|
|
36
|
+
|
|
37
|
+
def run_binary(
|
|
38
|
+
self,
|
|
39
|
+
command: list[str],
|
|
40
|
+
artifact_path: Path | str,
|
|
41
|
+
stdin: str | bytes | None = None,
|
|
42
|
+
timeout: float = 30.0,
|
|
43
|
+
cwd: Path | None = None,
|
|
44
|
+
) -> tuple[int, bytes, bytes]:
|
|
45
|
+
"""
|
|
46
|
+
Run command and return binary output.
|
|
47
|
+
|
|
48
|
+
Use this for commands that output binary data.
|
|
49
|
+
"""
|
|
50
|
+
exit_code, _, _, stdout_bytes, stderr_bytes = self._run_internal(
|
|
51
|
+
command, artifact_path, stdin, timeout, cwd
|
|
52
|
+
)
|
|
53
|
+
return exit_code, stdout_bytes, stderr_bytes
|
|
54
|
+
|
|
55
|
+
def _run_internal(
|
|
56
|
+
self,
|
|
57
|
+
command: list[str],
|
|
58
|
+
artifact_path: Path | str,
|
|
59
|
+
stdin: str | bytes | None = None,
|
|
60
|
+
timeout: float = 30.0,
|
|
61
|
+
cwd: Path | None = None,
|
|
62
|
+
) -> tuple[int, str, str, bytes, bytes]:
|
|
63
|
+
"""
|
|
64
|
+
Internal run method that returns both text and binary outputs.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Tuple of (exit_code, stdout_str, stderr_str, stdout_bytes, stderr_bytes)
|
|
68
|
+
"""
|
|
69
|
+
# Create artifact directory
|
|
70
|
+
artifact_dir = self.artifact_root / artifact_path
|
|
71
|
+
artifact_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
|
|
73
|
+
# Save command and stdin
|
|
74
|
+
(artifact_dir / "cmd.txt").write_text(" ".join(command))
|
|
75
|
+
if stdin:
|
|
76
|
+
(artifact_dir / "stdin.txt").write_bytes(stdin.encode() if isinstance(stdin, str) else stdin)
|
|
77
|
+
|
|
78
|
+
# Run command in binary mode to handle binary outputs
|
|
79
|
+
result = run_command(
|
|
80
|
+
command,
|
|
81
|
+
cwd=cwd,
|
|
82
|
+
input=stdin,
|
|
83
|
+
timeout=timeout,
|
|
84
|
+
check=False, # Don't raise on non-zero exit
|
|
85
|
+
text=False, # Binary mode to handle binary data
|
|
86
|
+
env={"PROVIDE_TELEMETRY_DISABLED": "true"},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Handle binary outputs - return as bytes for binary data, decode for text
|
|
90
|
+
stdout_bytes = result.stdout if isinstance(result.stdout, bytes) else result.stdout.encode()
|
|
91
|
+
stderr_bytes = result.stderr if isinstance(result.stderr, bytes) else result.stderr.encode()
|
|
92
|
+
|
|
93
|
+
# Try to decode as text for logging/artifacts, but preserve original bytes
|
|
94
|
+
try:
|
|
95
|
+
stdout_str = stdout_bytes.decode("utf-8")
|
|
96
|
+
except UnicodeDecodeError:
|
|
97
|
+
# For binary output, save as hex dump for debugging
|
|
98
|
+
import base64
|
|
99
|
+
|
|
100
|
+
stdout_str = f"[Binary data - base64]: {base64.b64encode(stdout_bytes).decode('ascii')}"
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
stderr_str = stderr_bytes.decode("utf-8")
|
|
104
|
+
except UnicodeDecodeError:
|
|
105
|
+
stderr_str = f"[Binary stderr]: {stderr_bytes.decode('utf-8', errors='replace')}"
|
|
106
|
+
|
|
107
|
+
# Save outputs
|
|
108
|
+
(artifact_dir / "stdout.txt").write_text(stdout_str)
|
|
109
|
+
(artifact_dir / "stderr.txt").write_text(stderr_str)
|
|
110
|
+
(artifact_dir / "exitcode.txt").write_text(str(result.returncode))
|
|
111
|
+
|
|
112
|
+
# Save raw binary outputs for binary commands
|
|
113
|
+
if stdout_bytes:
|
|
114
|
+
(artifact_dir / "stdout.bin").write_bytes(stdout_bytes)
|
|
115
|
+
if stderr_bytes:
|
|
116
|
+
(artifact_dir / "stderr.bin").write_bytes(stderr_bytes)
|
|
117
|
+
|
|
118
|
+
# Return all formats
|
|
119
|
+
return result.returncode, stdout_str, stderr_str, stdout_bytes, stderr_bytes
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
__all__ = ["HarnessRunner"]
|
provide/testkit/hub.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Hub Testing Fixtures for Foundation.
|
|
3
|
+
|
|
4
|
+
Provides pytest fixtures for testing hub and component functionality,
|
|
5
|
+
including container directories and component registration scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from provide.foundation.file import temp_dir as foundation_temp_dir
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(scope="session")
|
|
14
|
+
def default_container_directory():
|
|
15
|
+
"""
|
|
16
|
+
Provides a default directory for container operations in tests.
|
|
17
|
+
|
|
18
|
+
This fixture is used by tests that need a temporary directory
|
|
19
|
+
for container-related operations.
|
|
20
|
+
"""
|
|
21
|
+
with foundation_temp_dir() as tmp_dir:
|
|
22
|
+
yield tmp_dir
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#
|
|
2
|
+
# __init__.py
|
|
3
|
+
#
|
|
4
|
+
"""
|
|
5
|
+
Logger Testing Utilities.
|
|
6
|
+
|
|
7
|
+
Provides utilities for logger testing, including state reset,
|
|
8
|
+
mock fixtures, and pytest hooks for managing noisy loggers.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# Import reset utilities
|
|
12
|
+
# Import hook utilities
|
|
13
|
+
from provide.testkit.logger.hooks import (
|
|
14
|
+
DEFAULT_NOISY_LOGGERS,
|
|
15
|
+
get_log_level_for_noisy_loggers,
|
|
16
|
+
get_noisy_loggers,
|
|
17
|
+
pytest_runtest_setup,
|
|
18
|
+
suppress_loggers,
|
|
19
|
+
)
|
|
20
|
+
from provide.testkit.logger.reset import (
|
|
21
|
+
mock_logger,
|
|
22
|
+
mock_logger_factory,
|
|
23
|
+
reset_foundation_setup_for_testing,
|
|
24
|
+
reset_foundation_state,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
# Reset utilities
|
|
29
|
+
"mock_logger",
|
|
30
|
+
"mock_logger_factory",
|
|
31
|
+
"reset_foundation_setup_for_testing",
|
|
32
|
+
"reset_foundation_state",
|
|
33
|
+
# Hook utilities
|
|
34
|
+
"DEFAULT_NOISY_LOGGERS",
|
|
35
|
+
"get_log_level_for_noisy_loggers",
|
|
36
|
+
"get_noisy_loggers",
|
|
37
|
+
"pytest_runtest_setup",
|
|
38
|
+
"suppress_loggers",
|
|
39
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#
|
|
2
|
+
# hooks.py
|
|
3
|
+
#
|
|
4
|
+
"""
|
|
5
|
+
Pytest Hooks for Logger Management.
|
|
6
|
+
|
|
7
|
+
Provides pytest hooks for suppressing noisy loggers during test runs.
|
|
8
|
+
This helps reduce test output noise from third-party libraries.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
# Default list of commonly noisy loggers to suppress during tests
|
|
17
|
+
DEFAULT_NOISY_LOGGERS = [
|
|
18
|
+
"markdown_it",
|
|
19
|
+
"asyncio",
|
|
20
|
+
"urllib3.connectionpool",
|
|
21
|
+
"requests.packages.urllib3.connectionpool",
|
|
22
|
+
"botocore",
|
|
23
|
+
"boto3.resources",
|
|
24
|
+
"websockets.protocol",
|
|
25
|
+
"websockets.server",
|
|
26
|
+
"websockets.client",
|
|
27
|
+
"httpx",
|
|
28
|
+
"httpcore",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_noisy_loggers() -> list[str]:
|
|
33
|
+
"""
|
|
34
|
+
Get the list of loggers to suppress during tests.
|
|
35
|
+
|
|
36
|
+
Can be customized via the TESTKIT_NOISY_LOGGERS environment variable,
|
|
37
|
+
which should be a comma-separated list of logger names.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
List of logger names to suppress.
|
|
41
|
+
"""
|
|
42
|
+
env_loggers = os.getenv("TESTKIT_NOISY_LOGGERS")
|
|
43
|
+
if env_loggers:
|
|
44
|
+
return [name.strip() for name in env_loggers.split(",") if name.strip()]
|
|
45
|
+
return DEFAULT_NOISY_LOGGERS
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_log_level_for_noisy_loggers() -> int:
|
|
49
|
+
"""
|
|
50
|
+
Get the log level to set for noisy loggers.
|
|
51
|
+
|
|
52
|
+
Can be customized via the TESTKIT_NOISY_LOG_LEVEL environment variable.
|
|
53
|
+
Defaults to WARNING level.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Logging level (int) to set for noisy loggers.
|
|
57
|
+
"""
|
|
58
|
+
level_name = os.getenv("TESTKIT_NOISY_LOG_LEVEL", "WARNING")
|
|
59
|
+
return getattr(logging, level_name.upper(), logging.WARNING)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@pytest.hookimpl(tryfirst=True)
|
|
63
|
+
def pytest_runtest_setup():
|
|
64
|
+
"""
|
|
65
|
+
Hook that runs before each test setup.
|
|
66
|
+
|
|
67
|
+
This forcefully sets the log level for noisy libraries to WARNING (or custom level),
|
|
68
|
+
overriding any configuration that may have happened at import time
|
|
69
|
+
(e.g., by Textual or the application itself).
|
|
70
|
+
"""
|
|
71
|
+
noisy_loggers = get_noisy_loggers()
|
|
72
|
+
log_level = get_log_level_for_noisy_loggers()
|
|
73
|
+
|
|
74
|
+
for logger_name in noisy_loggers:
|
|
75
|
+
logger = logging.getLogger(logger_name)
|
|
76
|
+
logger.setLevel(log_level)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def suppress_loggers(logger_names: list[str], level: int = logging.WARNING) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Utility function to suppress specific loggers to a given level.
|
|
82
|
+
|
|
83
|
+
Can be used directly in tests or conftest.py files for custom suppression.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
logger_names: List of logger names to suppress
|
|
87
|
+
level: Log level to set (defaults to WARNING)
|
|
88
|
+
"""
|
|
89
|
+
for logger_name in logger_names:
|
|
90
|
+
logger = logging.getLogger(logger_name)
|
|
91
|
+
logger.setLevel(level)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
"DEFAULT_NOISY_LOGGERS",
|
|
96
|
+
"get_log_level_for_noisy_loggers",
|
|
97
|
+
"get_noisy_loggers",
|
|
98
|
+
"pytest_runtest_setup",
|
|
99
|
+
"suppress_loggers",
|
|
100
|
+
]
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
#
|
|
2
|
+
# reset.py
|
|
3
|
+
#
|
|
4
|
+
"""
|
|
5
|
+
Logger Testing Utilities for Foundation.
|
|
6
|
+
|
|
7
|
+
Provides utilities for resetting logger state, managing configurations,
|
|
8
|
+
and ensuring test isolation for the Foundation logging system.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from unittest.mock import Mock
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
# Note: Removed module-level imports to avoid circular imports
|
|
16
|
+
# All Foundation imports will be done within functions when needed
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.fixture
|
|
20
|
+
def mock_logger():
|
|
21
|
+
"""
|
|
22
|
+
Comprehensive mock logger for testing.
|
|
23
|
+
|
|
24
|
+
Provides compatibility with both stdlib logging and structlog interfaces,
|
|
25
|
+
including method call tracking and common logger attributes.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Mock logger with debug, info, warning, error methods and structlog compatibility.
|
|
29
|
+
"""
|
|
30
|
+
logger = Mock()
|
|
31
|
+
logger.debug = Mock()
|
|
32
|
+
logger.info = Mock()
|
|
33
|
+
logger.warning = Mock()
|
|
34
|
+
logger.warn = Mock() # Alias for warning
|
|
35
|
+
logger.error = Mock()
|
|
36
|
+
logger.exception = Mock()
|
|
37
|
+
logger.critical = Mock()
|
|
38
|
+
logger.fatal = Mock() # Alias for critical
|
|
39
|
+
|
|
40
|
+
# Add common logger attributes
|
|
41
|
+
logger.name = "mock_logger"
|
|
42
|
+
logger.level = 10 # DEBUG level
|
|
43
|
+
logger.handlers = []
|
|
44
|
+
logger.disabled = False
|
|
45
|
+
|
|
46
|
+
# Add structlog compatibility methods
|
|
47
|
+
logger.bind = Mock(return_value=logger)
|
|
48
|
+
logger.unbind = Mock(return_value=logger)
|
|
49
|
+
logger.new = Mock(return_value=logger)
|
|
50
|
+
logger.msg = Mock() # Alternative to info
|
|
51
|
+
|
|
52
|
+
# Add trace method for Foundation's extended logging
|
|
53
|
+
logger.trace = Mock()
|
|
54
|
+
|
|
55
|
+
return logger
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def mock_logger_factory():
|
|
59
|
+
"""
|
|
60
|
+
Factory function to create mock loggers outside of pytest context.
|
|
61
|
+
|
|
62
|
+
Useful for unit tests that need a mock logger but aren't using pytest fixtures.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Mock logger with the same interface as the pytest fixture.
|
|
66
|
+
"""
|
|
67
|
+
logger = Mock()
|
|
68
|
+
logger.debug = Mock()
|
|
69
|
+
logger.info = Mock()
|
|
70
|
+
logger.warning = Mock()
|
|
71
|
+
logger.warn = Mock()
|
|
72
|
+
logger.error = Mock()
|
|
73
|
+
logger.exception = Mock()
|
|
74
|
+
logger.critical = Mock()
|
|
75
|
+
logger.fatal = Mock()
|
|
76
|
+
|
|
77
|
+
logger.name = "mock_logger"
|
|
78
|
+
logger.level = 10
|
|
79
|
+
logger.handlers = []
|
|
80
|
+
logger.disabled = False
|
|
81
|
+
|
|
82
|
+
logger.bind = Mock(return_value=logger)
|
|
83
|
+
logger.unbind = Mock(return_value=logger)
|
|
84
|
+
logger.new = Mock(return_value=logger)
|
|
85
|
+
logger.msg = Mock()
|
|
86
|
+
logger.trace = Mock()
|
|
87
|
+
|
|
88
|
+
return logger
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _reset_opentelemetry_providers() -> None:
|
|
92
|
+
"""
|
|
93
|
+
Reset OpenTelemetry providers to uninitialized state.
|
|
94
|
+
|
|
95
|
+
This prevents "Overriding of current TracerProvider/MeterProvider" warnings
|
|
96
|
+
and stream closure issues by properly resetting the global providers.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
# Reset tracing provider more thoroughly
|
|
100
|
+
import opentelemetry.trace as otel_trace
|
|
101
|
+
|
|
102
|
+
# Reset the Once flag to allow re-initialization
|
|
103
|
+
if hasattr(otel_trace, "_TRACER_PROVIDER_SET_ONCE"):
|
|
104
|
+
once_obj = otel_trace._TRACER_PROVIDER_SET_ONCE
|
|
105
|
+
if hasattr(once_obj, "_done"):
|
|
106
|
+
once_obj._done = False
|
|
107
|
+
if hasattr(once_obj, "_lock"):
|
|
108
|
+
with once_obj._lock:
|
|
109
|
+
once_obj._done = False
|
|
110
|
+
|
|
111
|
+
# Reset to NoOpTracerProvider
|
|
112
|
+
from opentelemetry.trace import NoOpTracerProvider
|
|
113
|
+
|
|
114
|
+
otel_trace.set_tracer_provider(NoOpTracerProvider())
|
|
115
|
+
|
|
116
|
+
except ImportError:
|
|
117
|
+
# OpenTelemetry tracing not available
|
|
118
|
+
pass
|
|
119
|
+
except Exception:
|
|
120
|
+
# Ignore errors during reset - better to continue than fail
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# Reset metrics provider more thoroughly
|
|
125
|
+
import opentelemetry.metrics as otel_metrics
|
|
126
|
+
import opentelemetry.metrics._internal as otel_metrics_internal
|
|
127
|
+
|
|
128
|
+
# Reset the Once flag to allow re-initialization
|
|
129
|
+
if hasattr(otel_metrics_internal, "_METER_PROVIDER_SET_ONCE"):
|
|
130
|
+
once_obj = otel_metrics_internal._METER_PROVIDER_SET_ONCE
|
|
131
|
+
if hasattr(once_obj, "_done"):
|
|
132
|
+
once_obj._done = False
|
|
133
|
+
if hasattr(once_obj, "_lock"):
|
|
134
|
+
with once_obj._lock:
|
|
135
|
+
once_obj._done = False
|
|
136
|
+
|
|
137
|
+
# Reset to NoOpMeterProvider
|
|
138
|
+
from opentelemetry.metrics import NoOpMeterProvider
|
|
139
|
+
|
|
140
|
+
otel_metrics.set_meter_provider(NoOpMeterProvider())
|
|
141
|
+
|
|
142
|
+
except ImportError:
|
|
143
|
+
# OpenTelemetry metrics not available
|
|
144
|
+
pass
|
|
145
|
+
except Exception:
|
|
146
|
+
# Ignore errors during reset - better to continue than fail
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def reset_foundation_state() -> None:
|
|
151
|
+
"""
|
|
152
|
+
Internal function to reset structlog and Foundation's state using Hub-based approach.
|
|
153
|
+
|
|
154
|
+
This resets:
|
|
155
|
+
- structlog configuration to defaults
|
|
156
|
+
- Foundation Hub state (which manages all Foundation components)
|
|
157
|
+
- Stream state back to defaults
|
|
158
|
+
- Lazy setup state tracking (if available)
|
|
159
|
+
- OpenTelemetry provider state (if available)
|
|
160
|
+
"""
|
|
161
|
+
# Use the new internal reset APIs from Foundation's testmode module
|
|
162
|
+
from provide.foundation.testmode.internal import (
|
|
163
|
+
reset_coordinator_state,
|
|
164
|
+
reset_eventsets_state,
|
|
165
|
+
reset_hub_state,
|
|
166
|
+
reset_logger_state,
|
|
167
|
+
reset_streams_state,
|
|
168
|
+
reset_structlog_state,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Reset in the proper order to avoid triggering reinitialization
|
|
172
|
+
reset_structlog_state()
|
|
173
|
+
reset_streams_state()
|
|
174
|
+
|
|
175
|
+
# Reset OpenTelemetry providers to avoid "Overriding" warnings and stream closure
|
|
176
|
+
# Note: OpenTelemetry providers are designed to prevent override for safety.
|
|
177
|
+
# In test environments, we suppress this reset to avoid hanging/blocking.
|
|
178
|
+
# The warnings are harmless in test context.
|
|
179
|
+
_reset_opentelemetry_providers()
|
|
180
|
+
|
|
181
|
+
# Reset lazy setup state FIRST to prevent hub operations from triggering setup
|
|
182
|
+
reset_logger_state()
|
|
183
|
+
|
|
184
|
+
# Clear Hub (this handles all Foundation state including logger instances)
|
|
185
|
+
reset_hub_state()
|
|
186
|
+
|
|
187
|
+
# Reset coordinator and event set state
|
|
188
|
+
reset_coordinator_state()
|
|
189
|
+
reset_eventsets_state()
|
|
190
|
+
|
|
191
|
+
# Final reset of logger state (after all operations that might trigger setup)
|
|
192
|
+
reset_logger_state()
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def reset_foundation_setup_for_testing() -> None:
|
|
196
|
+
"""
|
|
197
|
+
Public test utility to reset Foundation's internal state using Hub-based approach.
|
|
198
|
+
|
|
199
|
+
This function ensures clean test isolation by resetting all
|
|
200
|
+
Foundation state between test runs. Now uses Hub.clear_hub() which
|
|
201
|
+
properly resets all Foundation components.
|
|
202
|
+
"""
|
|
203
|
+
# Full reset with Hub-based state management
|
|
204
|
+
reset_foundation_state()
|
|
205
|
+
|
|
206
|
+
# Re-register HTTP transport for tests that need it
|
|
207
|
+
try:
|
|
208
|
+
from provide.foundation.transport.http import _register_http_transport
|
|
209
|
+
|
|
210
|
+
_register_http_transport()
|
|
211
|
+
except ImportError:
|
|
212
|
+
# Transport module not available
|
|
213
|
+
pass
|
|
214
|
+
|
|
215
|
+
# Final reset of lazy setup state (after transport registration)
|
|
216
|
+
try:
|
|
217
|
+
from provide.foundation.logger.core import _LAZY_SETUP_STATE
|
|
218
|
+
|
|
219
|
+
_LAZY_SETUP_STATE.update({"done": False, "error": None, "in_progress": False})
|
|
220
|
+
except ImportError:
|
|
221
|
+
# Legacy state not available, skip
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
__all__ = [
|
|
226
|
+
"mock_logger",
|
|
227
|
+
"mock_logger_factory",
|
|
228
|
+
"reset_foundation_setup_for_testing",
|
|
229
|
+
"reset_foundation_state",
|
|
230
|
+
]
|
provide/testkit/main.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Main CLI entry point for provide-testkit."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from .quality.cli import quality_cli
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
@click.version_option()
|
|
12
|
+
def main() -> None:
|
|
13
|
+
"""Provide Testkit - Testing utilities for the provide ecosystem."""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Add quality commands
|
|
18
|
+
main.add_command(quality_cli)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mocking utilities for the provide-io ecosystem.
|
|
3
|
+
|
|
4
|
+
Standardized mocking patterns, fixtures, and utilities to reduce
|
|
5
|
+
boilerplate and ensure consistent mocking across all tests.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from provide.testkit.mocking.fixtures import (
|
|
9
|
+
ANY,
|
|
10
|
+
AsyncMock,
|
|
11
|
+
MagicMock,
|
|
12
|
+
Mock,
|
|
13
|
+
PropertyMock,
|
|
14
|
+
assert_mock_calls,
|
|
15
|
+
async_mock_factory,
|
|
16
|
+
auto_patch,
|
|
17
|
+
call,
|
|
18
|
+
magic_mock_factory,
|
|
19
|
+
mock_factory,
|
|
20
|
+
mock_open_fixture,
|
|
21
|
+
patch,
|
|
22
|
+
patch_fixture,
|
|
23
|
+
patch_multiple_fixture,
|
|
24
|
+
property_mock_factory,
|
|
25
|
+
spy_fixture,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"ANY",
|
|
30
|
+
"AsyncMock",
|
|
31
|
+
"MagicMock",
|
|
32
|
+
"Mock",
|
|
33
|
+
"PropertyMock",
|
|
34
|
+
"assert_mock_calls",
|
|
35
|
+
"async_mock_factory",
|
|
36
|
+
"auto_patch",
|
|
37
|
+
"call",
|
|
38
|
+
"magic_mock_factory",
|
|
39
|
+
"mock_factory",
|
|
40
|
+
"mock_open_fixture",
|
|
41
|
+
"patch",
|
|
42
|
+
"patch_fixture",
|
|
43
|
+
"patch_multiple_fixture",
|
|
44
|
+
"property_mock_factory",
|
|
45
|
+
"spy_fixture",
|
|
46
|
+
]
|