sparkless-testing 0.1.0__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.
- sparkless_testing/__init__.py +87 -0
- sparkless_testing/conftest.py +39 -0
- sparkless_testing/engine.py +306 -0
- sparkless_testing/parametrize.py +150 -0
- sparkless_testing/protocols.py +145 -0
- sparkless_testing/py.typed +0 -0
- sparkless_testing/pytest_fixtures.py +235 -0
- sparkless_testing/session.py +262 -0
- sparkless_testing/utils.py +209 -0
- sparkless_testing-0.1.0.dist-info/METADATA +445 -0
- sparkless_testing-0.1.0.dist-info/RECORD +14 -0
- sparkless_testing-0.1.0.dist-info/WHEEL +5 -0
- sparkless_testing-0.1.0.dist-info/licenses/LICENSE +22 -0
- sparkless_testing-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sparkless-testing - Easy testing with sparkless or PySpark.
|
|
3
|
+
|
|
4
|
+
A Python package that simplifies running tests with either sparkless (mock)
|
|
5
|
+
or PySpark (real) engines on demand.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
__author__ = "Odos Matthews"
|
|
10
|
+
__email__ = "odosmatthews@gmail.com"
|
|
11
|
+
|
|
12
|
+
# Core engine configuration
|
|
13
|
+
from .engine import (
|
|
14
|
+
EngineConfig,
|
|
15
|
+
EngineType,
|
|
16
|
+
auto_configure_engine,
|
|
17
|
+
configure_engine,
|
|
18
|
+
detect_available_engines,
|
|
19
|
+
get_engine,
|
|
20
|
+
reset_engine_state,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Protocols (for type checking)
|
|
24
|
+
from .protocols import (
|
|
25
|
+
AnalysisExceptionProtocol,
|
|
26
|
+
ColumnProtocol,
|
|
27
|
+
DataFrameProtocol,
|
|
28
|
+
FunctionsProtocol,
|
|
29
|
+
SparkSessionProtocol,
|
|
30
|
+
TypesProtocol,
|
|
31
|
+
WindowProtocol,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Session factory
|
|
35
|
+
from .session import (
|
|
36
|
+
SessionConfig,
|
|
37
|
+
create_mock_session,
|
|
38
|
+
create_pyspark_session,
|
|
39
|
+
create_session,
|
|
40
|
+
stop_all_sessions,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Test utilities
|
|
44
|
+
from .utils import (
|
|
45
|
+
create_test_dataframe,
|
|
46
|
+
detect_spark_type,
|
|
47
|
+
get_app_name_prefix,
|
|
48
|
+
get_spark_mode,
|
|
49
|
+
get_warehouse_dir,
|
|
50
|
+
is_dataframe_like,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
__all__ = [
|
|
54
|
+
# Package metadata
|
|
55
|
+
"__version__",
|
|
56
|
+
"__author__",
|
|
57
|
+
"__email__",
|
|
58
|
+
# Engine configuration
|
|
59
|
+
"EngineType",
|
|
60
|
+
"EngineConfig",
|
|
61
|
+
"configure_engine",
|
|
62
|
+
"get_engine",
|
|
63
|
+
"reset_engine_state",
|
|
64
|
+
"detect_available_engines",
|
|
65
|
+
"auto_configure_engine",
|
|
66
|
+
# Session factory
|
|
67
|
+
"SessionConfig",
|
|
68
|
+
"create_mock_session",
|
|
69
|
+
"create_pyspark_session",
|
|
70
|
+
"create_session",
|
|
71
|
+
"stop_all_sessions",
|
|
72
|
+
# Test utilities
|
|
73
|
+
"detect_spark_type",
|
|
74
|
+
"is_dataframe_like",
|
|
75
|
+
"create_test_dataframe",
|
|
76
|
+
"get_spark_mode",
|
|
77
|
+
"get_warehouse_dir",
|
|
78
|
+
"get_app_name_prefix",
|
|
79
|
+
# Protocols
|
|
80
|
+
"ColumnProtocol",
|
|
81
|
+
"DataFrameProtocol",
|
|
82
|
+
"FunctionsProtocol",
|
|
83
|
+
"TypesProtocol",
|
|
84
|
+
"WindowProtocol",
|
|
85
|
+
"AnalysisExceptionProtocol",
|
|
86
|
+
"SparkSessionProtocol",
|
|
87
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Auto-configure pytest fixtures for sparkless-testing.
|
|
3
|
+
|
|
4
|
+
This module automatically registers pytest fixtures and hooks when imported.
|
|
5
|
+
Import this in your conftest.py or test files to enable sparkless-testing fixtures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# Import parametrization hooks
|
|
11
|
+
from .parametrize import ( # noqa: F401
|
|
12
|
+
both_engines,
|
|
13
|
+
parametrize_engines,
|
|
14
|
+
pytest_configure,
|
|
15
|
+
pytest_generate_tests,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Import fixtures to register them
|
|
19
|
+
from .pytest_fixtures import ( # noqa: F401
|
|
20
|
+
mock_spark_session,
|
|
21
|
+
pyspark_session,
|
|
22
|
+
spark_engine_type,
|
|
23
|
+
spark_functions,
|
|
24
|
+
spark_session,
|
|
25
|
+
spark_types,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"spark_session",
|
|
30
|
+
"mock_spark_session",
|
|
31
|
+
"pyspark_session",
|
|
32
|
+
"spark_functions",
|
|
33
|
+
"spark_types",
|
|
34
|
+
"spark_engine_type",
|
|
35
|
+
"both_engines",
|
|
36
|
+
"parametrize_engines",
|
|
37
|
+
"pytest_configure",
|
|
38
|
+
"pytest_generate_tests",
|
|
39
|
+
]
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Engine detection and configuration for sparkless-testing.
|
|
3
|
+
|
|
4
|
+
This module provides centralized engine detection and configuration,
|
|
5
|
+
allowing tests to work with either sparkless (mock) or PySpark (real) engines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import threading
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .protocols import (
|
|
16
|
+
AnalysisExceptionProtocol,
|
|
17
|
+
FunctionsProtocol,
|
|
18
|
+
TypesProtocol,
|
|
19
|
+
WindowProtocol,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class EngineType(Enum):
|
|
24
|
+
"""Engine type enumeration."""
|
|
25
|
+
|
|
26
|
+
MOCK = "mock"
|
|
27
|
+
PYSPARK = "pyspark"
|
|
28
|
+
AUTO = "auto"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class EngineConfig:
|
|
33
|
+
"""Configuration for a Spark engine."""
|
|
34
|
+
|
|
35
|
+
functions: FunctionsProtocol
|
|
36
|
+
types: TypesProtocol
|
|
37
|
+
analysis_exception: type[BaseException] | AnalysisExceptionProtocol
|
|
38
|
+
window: WindowProtocol | None = None
|
|
39
|
+
desc: Any | None = None
|
|
40
|
+
engine_name: str = "unknown"
|
|
41
|
+
engine_type: EngineType = EngineType.AUTO
|
|
42
|
+
dataframe_cls: Any | None = None
|
|
43
|
+
spark_session_cls: Any | None = None
|
|
44
|
+
column_cls: Any | None = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Global engine state (for backward compatibility)
|
|
48
|
+
_engine: EngineConfig | None = None
|
|
49
|
+
|
|
50
|
+
# Thread-local engine state (for parallel test isolation)
|
|
51
|
+
_thread_local = threading.local()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def configure_engine(
|
|
55
|
+
*,
|
|
56
|
+
functions: FunctionsProtocol,
|
|
57
|
+
types: TypesProtocol,
|
|
58
|
+
analysis_exception: type[BaseException] | AnalysisExceptionProtocol,
|
|
59
|
+
window: WindowProtocol | None = None,
|
|
60
|
+
desc: Any | None = None,
|
|
61
|
+
engine_name: str = "unknown",
|
|
62
|
+
engine_type: EngineType = EngineType.AUTO,
|
|
63
|
+
dataframe_cls: Any | None = None,
|
|
64
|
+
spark_session_cls: Any | None = None,
|
|
65
|
+
column_cls: Any | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Inject engine components.
|
|
69
|
+
|
|
70
|
+
Sets both thread-local and global engine state for backward compatibility.
|
|
71
|
+
Thread-local state takes precedence in get_engine() for parallel test isolation.
|
|
72
|
+
|
|
73
|
+
**Thread-Local vs Global State:**
|
|
74
|
+
|
|
75
|
+
- **Thread-local state**: Used for parallel test execution (pytest-xdist).
|
|
76
|
+
Each thread maintains its own engine configuration, ensuring test isolation.
|
|
77
|
+
This is checked first by get_engine().
|
|
78
|
+
|
|
79
|
+
- **Global state**: Used for backward compatibility and single-threaded execution.
|
|
80
|
+
Falls back to this if thread-local state is not set.
|
|
81
|
+
|
|
82
|
+
When configure_engine() is called, it sets BOTH thread-local and global state.
|
|
83
|
+
This ensures compatibility with both parallel and sequential test execution.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
functions: Functions module (F) from the engine
|
|
87
|
+
types: Types module from the engine
|
|
88
|
+
analysis_exception: AnalysisException class from the engine
|
|
89
|
+
window: Window class from the engine (optional)
|
|
90
|
+
desc: desc function from the engine (optional)
|
|
91
|
+
engine_name: Name of the engine (e.g., "mock", "pyspark")
|
|
92
|
+
engine_type: EngineType enum value
|
|
93
|
+
dataframe_cls: DataFrame class from the engine (optional)
|
|
94
|
+
spark_session_cls: SparkSession class from the engine (optional)
|
|
95
|
+
column_cls: Column class from the engine (optional)
|
|
96
|
+
"""
|
|
97
|
+
global _engine
|
|
98
|
+
engine_config = EngineConfig(
|
|
99
|
+
functions=functions,
|
|
100
|
+
types=types,
|
|
101
|
+
analysis_exception=analysis_exception,
|
|
102
|
+
window=window,
|
|
103
|
+
desc=desc,
|
|
104
|
+
engine_name=engine_name,
|
|
105
|
+
engine_type=engine_type,
|
|
106
|
+
dataframe_cls=dataframe_cls,
|
|
107
|
+
spark_session_cls=spark_session_cls,
|
|
108
|
+
column_cls=column_cls,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Set global state (for backward compatibility)
|
|
112
|
+
_engine = engine_config
|
|
113
|
+
|
|
114
|
+
# Set thread-local state (for parallel test isolation)
|
|
115
|
+
_thread_local.engine = engine_config
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_engine() -> EngineConfig:
|
|
119
|
+
"""
|
|
120
|
+
Get the current engine config, raising if not configured.
|
|
121
|
+
|
|
122
|
+
**Priority Order:**
|
|
123
|
+
1. Thread-local state (checked first for parallel test isolation)
|
|
124
|
+
2. Global state (fallback for backward compatibility)
|
|
125
|
+
|
|
126
|
+
This ensures that in parallel test execution, each thread uses its own
|
|
127
|
+
engine configuration, while single-threaded execution uses the global state.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
EngineConfig instance
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
RuntimeError: If engine is not configured in either thread-local or global state
|
|
134
|
+
"""
|
|
135
|
+
# Try thread-local first (for parallel test isolation)
|
|
136
|
+
if hasattr(_thread_local, "engine") and _thread_local.engine is not None:
|
|
137
|
+
return _thread_local.engine # type: ignore[no-any-return]
|
|
138
|
+
|
|
139
|
+
# Fallback to global state (for backward compatibility)
|
|
140
|
+
if _engine is None:
|
|
141
|
+
raise RuntimeError(
|
|
142
|
+
"Engine not configured. Call configure_engine(...) before using sparkless-testing."
|
|
143
|
+
)
|
|
144
|
+
return _engine
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def reset_engine_state() -> None:
|
|
148
|
+
"""
|
|
149
|
+
Reset thread-local engine state.
|
|
150
|
+
|
|
151
|
+
This is useful for test isolation - clears the thread-local engine
|
|
152
|
+
so the next get_engine() call will use global state or raise an error.
|
|
153
|
+
|
|
154
|
+
**Note:** This only resets thread-local state, not global state.
|
|
155
|
+
After calling this, get_engine() will fall back to global state if available,
|
|
156
|
+
or raise RuntimeError if global state is also not configured.
|
|
157
|
+
"""
|
|
158
|
+
if hasattr(_thread_local, "engine"):
|
|
159
|
+
delattr(_thread_local, "engine")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def detect_available_engines() -> set[EngineType]:
|
|
163
|
+
"""
|
|
164
|
+
Detect which engines are available (installed).
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Set of available EngineType values
|
|
168
|
+
"""
|
|
169
|
+
available: set[EngineType] = set()
|
|
170
|
+
|
|
171
|
+
# Check for sparkless (mock)
|
|
172
|
+
try:
|
|
173
|
+
import sparkless # noqa: F401
|
|
174
|
+
|
|
175
|
+
available.add(EngineType.MOCK)
|
|
176
|
+
except ImportError:
|
|
177
|
+
pass
|
|
178
|
+
|
|
179
|
+
# Check for PySpark
|
|
180
|
+
try:
|
|
181
|
+
import pyspark # noqa: F401
|
|
182
|
+
|
|
183
|
+
available.add(EngineType.PYSPARK)
|
|
184
|
+
except ImportError:
|
|
185
|
+
pass
|
|
186
|
+
|
|
187
|
+
return available
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def auto_configure_engine(
|
|
191
|
+
preferred_engine: EngineType | None = None,
|
|
192
|
+
) -> EngineConfig:
|
|
193
|
+
"""
|
|
194
|
+
Automatically configure engine based on availability and preference.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
preferred_engine: Preferred engine type (defaults to MOCK if available)
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
EngineConfig instance
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
RuntimeError: If no engines are available
|
|
204
|
+
"""
|
|
205
|
+
available = detect_available_engines()
|
|
206
|
+
|
|
207
|
+
if not available:
|
|
208
|
+
raise RuntimeError("No Spark engines available. Install either 'sparkless' or 'pyspark'.")
|
|
209
|
+
|
|
210
|
+
# Determine which engine to use
|
|
211
|
+
if preferred_engine is None:
|
|
212
|
+
# Default preference: MOCK > PYSPARK
|
|
213
|
+
if EngineType.MOCK in available:
|
|
214
|
+
preferred_engine = EngineType.MOCK
|
|
215
|
+
elif EngineType.PYSPARK in available:
|
|
216
|
+
preferred_engine = EngineType.PYSPARK
|
|
217
|
+
else:
|
|
218
|
+
# Use first available
|
|
219
|
+
preferred_engine = next(iter(available))
|
|
220
|
+
|
|
221
|
+
if preferred_engine not in available:
|
|
222
|
+
raise RuntimeError(
|
|
223
|
+
f"Preferred engine '{preferred_engine.value}' is not available. "
|
|
224
|
+
f"Available engines: {[e.value for e in available]}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Configure based on engine type
|
|
228
|
+
if preferred_engine == EngineType.MOCK:
|
|
229
|
+
return _configure_mock_engine()
|
|
230
|
+
elif preferred_engine == EngineType.PYSPARK:
|
|
231
|
+
return _configure_pyspark_engine()
|
|
232
|
+
else:
|
|
233
|
+
raise RuntimeError(f"Unknown engine type: {preferred_engine}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _configure_mock_engine() -> EngineConfig:
|
|
237
|
+
"""Configure sparkless (mock) engine."""
|
|
238
|
+
try:
|
|
239
|
+
from sparkless import AnalysisException as MockAnalysisException
|
|
240
|
+
from sparkless import Column as MockColumn
|
|
241
|
+
from sparkless import DataFrame as MockDataFrame
|
|
242
|
+
from sparkless import SparkSession as MockSparkSession
|
|
243
|
+
from sparkless import Window as MockWindow
|
|
244
|
+
from sparkless import functions as mock_functions
|
|
245
|
+
from sparkless import spark_types as mock_types
|
|
246
|
+
from sparkless.functions import desc as mock_desc
|
|
247
|
+
except ImportError as e:
|
|
248
|
+
raise RuntimeError(
|
|
249
|
+
"sparkless is not installed. Install it with: pip install sparkless"
|
|
250
|
+
) from e
|
|
251
|
+
|
|
252
|
+
configure_engine(
|
|
253
|
+
functions=mock_functions,
|
|
254
|
+
types=mock_types,
|
|
255
|
+
analysis_exception=MockAnalysisException,
|
|
256
|
+
window=MockWindow,
|
|
257
|
+
desc=mock_desc,
|
|
258
|
+
engine_name="mock",
|
|
259
|
+
engine_type=EngineType.MOCK,
|
|
260
|
+
dataframe_cls=MockDataFrame,
|
|
261
|
+
spark_session_cls=MockSparkSession,
|
|
262
|
+
column_cls=MockColumn,
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return get_engine()
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _configure_pyspark_engine() -> EngineConfig:
|
|
269
|
+
"""Configure PySpark engine."""
|
|
270
|
+
try:
|
|
271
|
+
from pyspark.sql import Column as PySparkColumn
|
|
272
|
+
from pyspark.sql import DataFrame as PySparkDataFrame
|
|
273
|
+
from pyspark.sql import SparkSession as PySparkSparkSession
|
|
274
|
+
from pyspark.sql import functions as pyspark_functions
|
|
275
|
+
from pyspark.sql import types as pyspark_types
|
|
276
|
+
from pyspark.sql.functions import desc as pyspark_desc
|
|
277
|
+
from pyspark.sql.utils import AnalysisException as PySparkAnalysisException
|
|
278
|
+
from pyspark.sql.window import Window as PySparkWindow
|
|
279
|
+
except ImportError as e:
|
|
280
|
+
raise RuntimeError("PySpark is not installed. Install it with: pip install pyspark") from e
|
|
281
|
+
|
|
282
|
+
configure_engine(
|
|
283
|
+
functions=pyspark_functions,
|
|
284
|
+
types=pyspark_types,
|
|
285
|
+
analysis_exception=PySparkAnalysisException,
|
|
286
|
+
window=PySparkWindow,
|
|
287
|
+
desc=pyspark_desc,
|
|
288
|
+
engine_name="pyspark",
|
|
289
|
+
engine_type=EngineType.PYSPARK,
|
|
290
|
+
dataframe_cls=PySparkDataFrame,
|
|
291
|
+
spark_session_cls=PySparkSparkSession,
|
|
292
|
+
column_cls=PySparkColumn,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
return get_engine()
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
__all__ = [
|
|
299
|
+
"EngineType",
|
|
300
|
+
"EngineConfig",
|
|
301
|
+
"configure_engine",
|
|
302
|
+
"get_engine",
|
|
303
|
+
"reset_engine_state",
|
|
304
|
+
"detect_available_engines",
|
|
305
|
+
"auto_configure_engine",
|
|
306
|
+
]
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Parametrization support for running tests with both engines.
|
|
3
|
+
|
|
4
|
+
This module provides utilities for running tests with both sparkless (mock)
|
|
5
|
+
and PySpark (real) engines automatically.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import Generator
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
from .engine import EngineType, auto_configure_engine, detect_available_engines
|
|
18
|
+
from .session import SessionConfig, create_session
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pytest_configure(config: Any) -> None:
|
|
22
|
+
"""Register custom pytest markers."""
|
|
23
|
+
config.addinivalue_line(
|
|
24
|
+
"markers",
|
|
25
|
+
"spark_engine(engine): Mark test to use specific engine (mock or pyspark)",
|
|
26
|
+
)
|
|
27
|
+
config.addinivalue_line(
|
|
28
|
+
"markers",
|
|
29
|
+
"parametrize_engines: Run test with both available engines",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture(scope="function") # type: ignore[untyped-decorator]
|
|
34
|
+
def both_engines(request: pytest.FixtureRequest) -> Generator[tuple[Any, EngineType], None, None]:
|
|
35
|
+
"""
|
|
36
|
+
Fixture that yields both available engines via parametrization.
|
|
37
|
+
|
|
38
|
+
This fixture is automatically parametrized to run with both engines.
|
|
39
|
+
The test will be called twice - once with mock engine, once with PySpark.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
def test_both_engines(both_engines):
|
|
43
|
+
spark, engine_type = both_engines
|
|
44
|
+
df = spark.createDataFrame([{"id": 1}])
|
|
45
|
+
assert df.count() == 1
|
|
46
|
+
"""
|
|
47
|
+
# Get the parametrized engine type
|
|
48
|
+
engine_type = getattr(request, "param", None)
|
|
49
|
+
if engine_type is None:
|
|
50
|
+
# If not parametrized, check availability and use default
|
|
51
|
+
available = detect_available_engines()
|
|
52
|
+
if EngineType.MOCK not in available or EngineType.PYSPARK not in available:
|
|
53
|
+
pytest.skip(f"Both engines not available. Available: {[e.value for e in available]}")
|
|
54
|
+
# Default to MOCK if not parametrized
|
|
55
|
+
engine_type = EngineType.MOCK
|
|
56
|
+
|
|
57
|
+
# Configure and create session for the specified engine
|
|
58
|
+
# Disable Delta Lake for parallel testing compatibility
|
|
59
|
+
unique_id = f"{os.getpid()}-{uuid.uuid4().hex[:8]}"
|
|
60
|
+
config = SessionConfig(app_name=f"test-{unique_id}", enable_delta=False)
|
|
61
|
+
auto_configure_engine(engine_type)
|
|
62
|
+
spark = create_session(engine_type=engine_type, config=config)
|
|
63
|
+
|
|
64
|
+
yield spark, engine_type
|
|
65
|
+
|
|
66
|
+
# Cleanup
|
|
67
|
+
try:
|
|
68
|
+
spark.stop()
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Auto-parametrize both_engines fixture if both engines are available
|
|
74
|
+
def pytest_generate_tests(metafunc: Any) -> None:
|
|
75
|
+
"""Generate test parameters for parametrize_engines marker and both_engines fixture."""
|
|
76
|
+
# Handle both_engines fixture parametrization
|
|
77
|
+
if "both_engines" in metafunc.fixturenames:
|
|
78
|
+
available = detect_available_engines()
|
|
79
|
+
if EngineType.MOCK in available and EngineType.PYSPARK in available:
|
|
80
|
+
# Parametrize the both_engines fixture
|
|
81
|
+
metafunc.parametrize(
|
|
82
|
+
"both_engines",
|
|
83
|
+
[EngineType.MOCK, EngineType.PYSPARK],
|
|
84
|
+
ids=["mock", "pyspark"],
|
|
85
|
+
indirect=True,
|
|
86
|
+
)
|
|
87
|
+
elif EngineType.MOCK in available or EngineType.PYSPARK in available:
|
|
88
|
+
# Only one engine available, parametrize with just that one
|
|
89
|
+
available_list = list(available)
|
|
90
|
+
metafunc.parametrize(
|
|
91
|
+
"both_engines",
|
|
92
|
+
available_list,
|
|
93
|
+
ids=[e.value for e in available_list],
|
|
94
|
+
indirect=True,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Handle spark_session fixture parametrization for parametrize_engines marker
|
|
98
|
+
if "spark_session" in metafunc.fixturenames:
|
|
99
|
+
# Check if test has parametrize_engines marker
|
|
100
|
+
marker = metafunc.definition.get_closest_marker("parametrize_engines")
|
|
101
|
+
if marker:
|
|
102
|
+
available = detect_available_engines()
|
|
103
|
+
|
|
104
|
+
if EngineType.MOCK in available and EngineType.PYSPARK in available:
|
|
105
|
+
# Parametrize the spark_session fixture
|
|
106
|
+
metafunc.parametrize(
|
|
107
|
+
"spark_session",
|
|
108
|
+
[EngineType.MOCK, EngineType.PYSPARK],
|
|
109
|
+
ids=["mock", "pyspark"],
|
|
110
|
+
indirect=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parametrize_engines(*args: Any, **kwargs: Any) -> Any:
|
|
115
|
+
"""
|
|
116
|
+
Decorator to parametrize a test to run with both engines.
|
|
117
|
+
|
|
118
|
+
This is a convenience decorator that uses pytest.mark.parametrize
|
|
119
|
+
to run the test with both mock and pyspark engines.
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
@parametrize_engines
|
|
123
|
+
def test_my_function(spark_session):
|
|
124
|
+
df = spark_session.createDataFrame([{"id": 1}])
|
|
125
|
+
assert df.count() == 1
|
|
126
|
+
|
|
127
|
+
Note: This decorator requires both engines to be available.
|
|
128
|
+
"""
|
|
129
|
+
available = detect_available_engines()
|
|
130
|
+
|
|
131
|
+
if EngineType.MOCK not in available or EngineType.PYSPARK not in available:
|
|
132
|
+
# Skip parametrization if both engines not available
|
|
133
|
+
return pytest.mark.skip(
|
|
134
|
+
f"Both engines not available. Available: {[e.value for e in available]}"
|
|
135
|
+
)(*args, **kwargs)
|
|
136
|
+
|
|
137
|
+
# Create parametrize marker
|
|
138
|
+
return pytest.mark.parametrize(
|
|
139
|
+
"engine_type",
|
|
140
|
+
[EngineType.MOCK, EngineType.PYSPARK],
|
|
141
|
+
ids=["mock", "pyspark"],
|
|
142
|
+
)(*args, **kwargs)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
__all__ = [
|
|
146
|
+
"pytest_configure",
|
|
147
|
+
"both_engines",
|
|
148
|
+
"parametrize_engines",
|
|
149
|
+
"pytest_generate_tests",
|
|
150
|
+
]
|