rustest 0.5.0__cp312-cp312-win_amd64.whl → 0.7.0__cp312-cp312-win_amd64.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.
Potentially problematic release.
This version of rustest might be problematic. Click here for more details.
- rustest/cli.py +39 -1
- rustest/core.py +6 -0
- rustest/decorators.py +114 -0
- rustest/rust.cp312-win_amd64.pyd +0 -0
- rustest/rust.pyi +2 -0
- {rustest-0.5.0.dist-info → rustest-0.7.0.dist-info}/METADATA +25 -1
- rustest-0.7.0.dist-info/RECORD +16 -0
- {rustest-0.5.0.dist-info → rustest-0.7.0.dist-info}/WHEEL +1 -1
- rustest-0.5.0.dist-info/RECORD +0 -16
- {rustest-0.5.0.dist-info → rustest-0.7.0.dist-info}/entry_points.txt +0 -0
- {rustest-0.5.0.dist-info → rustest-0.7.0.dist-info}/licenses/LICENSE +0 -0
rustest/cli.py
CHANGED
|
@@ -93,7 +93,35 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
93
93
|
action="store_false",
|
|
94
94
|
help="Disable code block tests from markdown files.",
|
|
95
95
|
)
|
|
96
|
-
_ = parser.
|
|
96
|
+
_ = parser.add_argument(
|
|
97
|
+
"--lf",
|
|
98
|
+
"--last-failed",
|
|
99
|
+
action="store_true",
|
|
100
|
+
dest="last_failed",
|
|
101
|
+
help="Rerun only the tests that failed in the last run.",
|
|
102
|
+
)
|
|
103
|
+
_ = parser.add_argument(
|
|
104
|
+
"--ff",
|
|
105
|
+
"--failed-first",
|
|
106
|
+
action="store_true",
|
|
107
|
+
dest="failed_first",
|
|
108
|
+
help="Run previously failed tests first, then all other tests.",
|
|
109
|
+
)
|
|
110
|
+
_ = parser.add_argument(
|
|
111
|
+
"-x",
|
|
112
|
+
"--exitfirst",
|
|
113
|
+
action="store_true",
|
|
114
|
+
dest="fail_fast",
|
|
115
|
+
help="Exit instantly on first error or failed test.",
|
|
116
|
+
)
|
|
117
|
+
_ = parser.set_defaults(
|
|
118
|
+
capture_output=True,
|
|
119
|
+
color=True,
|
|
120
|
+
enable_codeblocks=True,
|
|
121
|
+
last_failed=False,
|
|
122
|
+
failed_first=False,
|
|
123
|
+
fail_fast=False,
|
|
124
|
+
)
|
|
97
125
|
return parser
|
|
98
126
|
|
|
99
127
|
|
|
@@ -105,6 +133,14 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
105
133
|
if not args.color:
|
|
106
134
|
Colors.disable()
|
|
107
135
|
|
|
136
|
+
# Determine last_failed_mode
|
|
137
|
+
if args.last_failed:
|
|
138
|
+
last_failed_mode = "only"
|
|
139
|
+
elif args.failed_first:
|
|
140
|
+
last_failed_mode = "first"
|
|
141
|
+
else:
|
|
142
|
+
last_failed_mode = "none"
|
|
143
|
+
|
|
108
144
|
report = run(
|
|
109
145
|
paths=list(args.paths),
|
|
110
146
|
pattern=args.pattern,
|
|
@@ -112,6 +148,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
112
148
|
workers=args.workers,
|
|
113
149
|
capture_output=args.capture_output,
|
|
114
150
|
enable_codeblocks=args.enable_codeblocks,
|
|
151
|
+
last_failed_mode=last_failed_mode,
|
|
152
|
+
fail_fast=args.fail_fast,
|
|
115
153
|
)
|
|
116
154
|
_print_report(report, verbose=args.verbose, ascii_mode=args.ascii)
|
|
117
155
|
return 0 if report.failed == 0 else 1
|
rustest/core.py
CHANGED
|
@@ -16,6 +16,8 @@ def run(
|
|
|
16
16
|
workers: int | None = None,
|
|
17
17
|
capture_output: bool = True,
|
|
18
18
|
enable_codeblocks: bool = True,
|
|
19
|
+
last_failed_mode: str = "none",
|
|
20
|
+
fail_fast: bool = False,
|
|
19
21
|
) -> RunReport:
|
|
20
22
|
"""Execute tests and return a rich report.
|
|
21
23
|
|
|
@@ -26,6 +28,8 @@ def run(
|
|
|
26
28
|
workers: Number of worker slots to use (experimental)
|
|
27
29
|
capture_output: Whether to capture stdout/stderr during test execution
|
|
28
30
|
enable_codeblocks: Whether to enable code block tests from markdown files
|
|
31
|
+
last_failed_mode: Last failed mode: "none", "only", or "first"
|
|
32
|
+
fail_fast: Exit instantly on first error or failed test
|
|
29
33
|
"""
|
|
30
34
|
raw_report = rust.run(
|
|
31
35
|
paths=list(paths),
|
|
@@ -34,5 +38,7 @@ def run(
|
|
|
34
38
|
workers=workers,
|
|
35
39
|
capture_output=capture_output,
|
|
36
40
|
enable_codeblocks=enable_codeblocks,
|
|
41
|
+
last_failed_mode=last_failed_mode,
|
|
42
|
+
fail_fast=fail_fast,
|
|
37
43
|
)
|
|
38
44
|
return RunReport.from_py(raw_report)
|
rustest/decorators.py
CHANGED
|
@@ -162,8 +162,122 @@ class MarkGenerator:
|
|
|
162
162
|
@mark.skipif(condition, *, reason="...")
|
|
163
163
|
@mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
|
|
164
164
|
@mark.usefixtures("fixture1", "fixture2")
|
|
165
|
+
@mark.asyncio(loop_scope="function")
|
|
165
166
|
"""
|
|
166
167
|
|
|
168
|
+
def asyncio(
|
|
169
|
+
self,
|
|
170
|
+
func: Callable[..., Any] | None = None,
|
|
171
|
+
*,
|
|
172
|
+
loop_scope: str = "function",
|
|
173
|
+
) -> Callable[..., Any]:
|
|
174
|
+
"""Mark an async test function to be executed with asyncio.
|
|
175
|
+
|
|
176
|
+
This decorator allows you to write async test functions that will be
|
|
177
|
+
automatically executed in an asyncio event loop. The loop_scope parameter
|
|
178
|
+
controls the scope of the event loop used for execution.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
func: The function to decorate (when used without parentheses)
|
|
182
|
+
loop_scope: The scope of the event loop. One of:
|
|
183
|
+
- "function": New loop for each test function (default)
|
|
184
|
+
- "class": Shared loop across all test methods in a class
|
|
185
|
+
- "module": Shared loop across all tests in a module
|
|
186
|
+
- "session": Shared loop across all tests in the session
|
|
187
|
+
|
|
188
|
+
Usage:
|
|
189
|
+
@mark.asyncio
|
|
190
|
+
async def test_async_function():
|
|
191
|
+
result = await some_async_operation()
|
|
192
|
+
assert result == expected
|
|
193
|
+
|
|
194
|
+
@mark.asyncio(loop_scope="module")
|
|
195
|
+
async def test_with_module_loop():
|
|
196
|
+
await another_async_operation()
|
|
197
|
+
|
|
198
|
+
Note:
|
|
199
|
+
This decorator should only be applied to async functions (coroutines).
|
|
200
|
+
Applying it to regular functions will raise a TypeError.
|
|
201
|
+
"""
|
|
202
|
+
import asyncio
|
|
203
|
+
import inspect
|
|
204
|
+
from functools import wraps
|
|
205
|
+
|
|
206
|
+
valid_scopes = {"function", "class", "module", "session"}
|
|
207
|
+
if loop_scope not in valid_scopes:
|
|
208
|
+
valid = ", ".join(sorted(valid_scopes))
|
|
209
|
+
msg = f"Invalid loop_scope '{loop_scope}'. Must be one of: {valid}"
|
|
210
|
+
raise ValueError(msg)
|
|
211
|
+
|
|
212
|
+
def decorator(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
213
|
+
# Handle class decoration - apply mark to all async methods
|
|
214
|
+
if inspect.isclass(f):
|
|
215
|
+
# Apply the mark to the class itself
|
|
216
|
+
mark_decorator = MarkDecorator("asyncio", (), {"loop_scope": loop_scope})
|
|
217
|
+
marked_class = mark_decorator(f)
|
|
218
|
+
|
|
219
|
+
# Wrap all async methods in the class
|
|
220
|
+
for name, method in inspect.getmembers(
|
|
221
|
+
marked_class, predicate=inspect.iscoroutinefunction
|
|
222
|
+
):
|
|
223
|
+
wrapped_method = _wrap_async_function(method, loop_scope)
|
|
224
|
+
setattr(marked_class, name, wrapped_method)
|
|
225
|
+
return marked_class
|
|
226
|
+
|
|
227
|
+
# Validate that the function is a coroutine
|
|
228
|
+
if not inspect.iscoroutinefunction(f):
|
|
229
|
+
msg = f"@mark.asyncio can only be applied to async functions or test classes, but '{f.__name__}' is not async"
|
|
230
|
+
raise TypeError(msg)
|
|
231
|
+
|
|
232
|
+
# Store the asyncio mark
|
|
233
|
+
mark_decorator = MarkDecorator("asyncio", (), {"loop_scope": loop_scope})
|
|
234
|
+
marked_f = mark_decorator(f)
|
|
235
|
+
|
|
236
|
+
# Wrap the async function to run it synchronously
|
|
237
|
+
return _wrap_async_function(marked_f, loop_scope)
|
|
238
|
+
|
|
239
|
+
def _wrap_async_function(f: Callable[..., Any], loop_scope: str) -> Callable[..., Any]:
|
|
240
|
+
"""Wrap an async function to run it synchronously in an event loop."""
|
|
241
|
+
|
|
242
|
+
@wraps(f)
|
|
243
|
+
def sync_wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
244
|
+
# Get or create event loop based on scope
|
|
245
|
+
# For now, we'll always create a new loop - scope handling will be
|
|
246
|
+
# implemented in a future enhancement via fixtures
|
|
247
|
+
loop = asyncio.new_event_loop()
|
|
248
|
+
asyncio.set_event_loop(loop)
|
|
249
|
+
try:
|
|
250
|
+
# Run the coroutine in the event loop
|
|
251
|
+
# Get the original async function
|
|
252
|
+
original_func = getattr(f, "__wrapped__", f)
|
|
253
|
+
coro = original_func(*args, **kwargs)
|
|
254
|
+
return loop.run_until_complete(coro)
|
|
255
|
+
finally:
|
|
256
|
+
# Clean up the loop
|
|
257
|
+
try:
|
|
258
|
+
# Cancel any pending tasks
|
|
259
|
+
pending = asyncio.all_tasks(loop)
|
|
260
|
+
for task in pending:
|
|
261
|
+
task.cancel()
|
|
262
|
+
# Run the loop one more time to let tasks finish cancellation
|
|
263
|
+
if pending:
|
|
264
|
+
loop.run_until_complete(
|
|
265
|
+
asyncio.gather(*pending, return_exceptions=True)
|
|
266
|
+
)
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
finally:
|
|
270
|
+
loop.close()
|
|
271
|
+
|
|
272
|
+
# Store reference to original async function
|
|
273
|
+
sync_wrapper.__wrapped__ = f
|
|
274
|
+
return sync_wrapper
|
|
275
|
+
|
|
276
|
+
# Support both @mark.asyncio and @mark.asyncio(loop_scope="...")
|
|
277
|
+
if func is not None:
|
|
278
|
+
return decorator(func)
|
|
279
|
+
return decorator
|
|
280
|
+
|
|
167
281
|
def skipif(
|
|
168
282
|
self,
|
|
169
283
|
condition: bool | str,
|
rustest/rust.cp312-win_amd64.pyd
CHANGED
|
Binary file
|
rustest/rust.pyi
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rustest
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Classifier: Development Status :: 3 - Alpha
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -18,6 +18,8 @@ Requires-Dist: maturin>=1.4,<2 ; extra == 'dev'
|
|
|
18
18
|
Requires-Dist: poethepoet>=0.22 ; extra == 'dev'
|
|
19
19
|
Requires-Dist: pre-commit>=3.5 ; extra == 'dev'
|
|
20
20
|
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=1.2.0 ; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-codeblocks>=0.17.0 ; extra == 'dev'
|
|
21
23
|
Requires-Dist: ruff>=0.1.9 ; extra == 'dev'
|
|
22
24
|
Requires-Dist: mkdocs>=1.5.0 ; extra == 'docs'
|
|
23
25
|
Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'docs'
|
|
@@ -44,6 +46,7 @@ Rustest (pronounced like Russ-Test) is a Rust-powered test runner that aims to p
|
|
|
44
46
|
|
|
45
47
|
- 🚀 **About 2x faster** than pytest on the rustest integration test suite
|
|
46
48
|
- ✅ Familiar `@fixture`, `@parametrize`, `@skip`, and `@mark` decorators
|
|
49
|
+
- 🔄 **Built-in async support** with `@mark.asyncio` (like pytest-asyncio)
|
|
47
50
|
- 🔍 Automatic test discovery (`test_*.py` and `*_test.py` files)
|
|
48
51
|
- 📝 **Built-in markdown code block testing** (like pytest-codeblocks, but faster)
|
|
49
52
|
- 🎯 Simple, clean API—if you know pytest, you already know rustest
|
|
@@ -76,6 +79,7 @@ With **10,000 parametrized invocations**:
|
|
|
76
79
|
|
|
77
80
|
Rustest supports Python **3.10 through 3.14**.
|
|
78
81
|
|
|
82
|
+
<!--pytest.mark.skip-->
|
|
79
83
|
```bash
|
|
80
84
|
# Using pip
|
|
81
85
|
pip install rustest
|
|
@@ -94,6 +98,7 @@ Create a file `test_math.py`:
|
|
|
94
98
|
|
|
95
99
|
```python
|
|
96
100
|
from rustest import fixture, parametrize, mark, approx, raises
|
|
101
|
+
import asyncio
|
|
97
102
|
|
|
98
103
|
@fixture
|
|
99
104
|
def numbers() -> list[int]:
|
|
@@ -111,6 +116,13 @@ def test_expensive_operation() -> None:
|
|
|
111
116
|
result = sum(range(1000000))
|
|
112
117
|
assert result > 0
|
|
113
118
|
|
|
119
|
+
@mark.asyncio
|
|
120
|
+
async def test_async_operation() -> None:
|
|
121
|
+
# Example async operation
|
|
122
|
+
await asyncio.sleep(0.001)
|
|
123
|
+
result = 42
|
|
124
|
+
assert result == 42
|
|
125
|
+
|
|
114
126
|
def test_division_by_zero() -> None:
|
|
115
127
|
with raises(ZeroDivisionError, match="division by zero"):
|
|
116
128
|
1 / 0
|
|
@@ -118,6 +130,7 @@ def test_division_by_zero() -> None:
|
|
|
118
130
|
|
|
119
131
|
### 2. Run Your Tests
|
|
120
132
|
|
|
133
|
+
<!--pytest.mark.skip-->
|
|
121
134
|
```bash
|
|
122
135
|
# Run all tests
|
|
123
136
|
rustest
|
|
@@ -133,6 +146,16 @@ rustest -m "slow" # Run only slow tests
|
|
|
133
146
|
rustest -m "not slow" # Skip slow tests
|
|
134
147
|
rustest -m "slow and integration" # Run tests with both marks
|
|
135
148
|
|
|
149
|
+
# Rerun only failed tests
|
|
150
|
+
rustest --lf # Last failed only
|
|
151
|
+
rustest --ff # Failed first, then all others
|
|
152
|
+
|
|
153
|
+
# Exit on first failure
|
|
154
|
+
rustest -x # Fail fast
|
|
155
|
+
|
|
156
|
+
# Combine options
|
|
157
|
+
rustest --ff -x # Run failed tests first, stop on first failure
|
|
158
|
+
|
|
136
159
|
# Show output during execution
|
|
137
160
|
rustest --no-capture
|
|
138
161
|
```
|
|
@@ -186,6 +209,7 @@ We welcome contributions! See the [Development Guide](https://apex-engineers-inc
|
|
|
186
209
|
|
|
187
210
|
Quick reference:
|
|
188
211
|
|
|
212
|
+
<!--pytest.mark.skip-->
|
|
189
213
|
```bash
|
|
190
214
|
# Setup
|
|
191
215
|
git clone https://github.com/Apex-Engineers-Inc/rustest.git
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rustest-0.7.0.dist-info/METADATA,sha256=t6zLLc3Y-0qY2KKh3xCdVK50GM1B1ZNugDVGt4AChzI,9011
|
|
2
|
+
rustest-0.7.0.dist-info/WHEEL,sha256=M1DN_cdEL9MiMVOnA_mgpqWn-huMwVTFfEx_RmZww1E,97
|
|
3
|
+
rustest-0.7.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
|
|
4
|
+
rustest-0.7.0.dist-info/licenses/LICENSE,sha256=Ci0bB0T1ZGkqIV237Zp_Bv8LIJJ0Vxwc-AhLhgDgAoQ,1096
|
|
5
|
+
rustest/__init__.py,sha256=LL9UloOClzeNO6A-iMkEFtHDrBTAhRLko3sXy55H0QA,542
|
|
6
|
+
rustest/__main__.py,sha256=yMhaWvxGAV46BYY8fB6GoRy9oh8Z8YrS9wlZI3LmoyY,188
|
|
7
|
+
rustest/approx.py,sha256=sGaH15n3vSSv84dmR_QAIDV-xwaUrX-MqwpWIp5ihjk,5904
|
|
8
|
+
rustest/cli.py,sha256=6etivUqjdnHgYShXobtrEFFhjW2PuetWuj8hwM4gRuI,10496
|
|
9
|
+
rustest/core.py,sha256=ORTAlkeuu6nC2Q43hg-VJY6PN1HUiaMoP1vu_WH-8cs,1505
|
|
10
|
+
rustest/decorators.py,sha256=mUrnY09mn7x0--3OAB47WQRR2f8NtexVSUWAUGcHvyc,19240
|
|
11
|
+
rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
rustest/reporting.py,sha256=g4jdlwjFoKF_fWA_sKfeDwmhDeHxPJQUqypa_E2XQlc,1686
|
|
13
|
+
rustest/rust.cp312-win_amd64.pyd,sha256=PUQBX9EY49xlHJ6zyxKz6yDayGOLDA5Gc8OBdeA2xow,1411584
|
|
14
|
+
rustest/rust.py,sha256=N_1C-uXRiC2qkV7ecKVcb51-XXyfhYNepd5zs-RIYOo,682
|
|
15
|
+
rustest/rust.pyi,sha256=ltvSC9_ZUpuRgq9cShN93YLxaxt9c1B2AqBswpk1nfY,849
|
|
16
|
+
rustest-0.7.0.dist-info/RECORD,,
|
rustest-0.5.0.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
rustest-0.5.0.dist-info/METADATA,sha256=SkRHcV4Pv7s5Hfo9ywd77-b3wuJtgbPvtrYY6LQoaYk,8225
|
|
2
|
-
rustest-0.5.0.dist-info/WHEEL,sha256=n8ZdhGDUio-M1d1jdBhS3kD6MKEN1SRxo8BOjWwZdsg,96
|
|
3
|
-
rustest-0.5.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
|
|
4
|
-
rustest-0.5.0.dist-info/licenses/LICENSE,sha256=Ci0bB0T1ZGkqIV237Zp_Bv8LIJJ0Vxwc-AhLhgDgAoQ,1096
|
|
5
|
-
rustest/__init__.py,sha256=LL9UloOClzeNO6A-iMkEFtHDrBTAhRLko3sXy55H0QA,542
|
|
6
|
-
rustest/__main__.py,sha256=yMhaWvxGAV46BYY8fB6GoRy9oh8Z8YrS9wlZI3LmoyY,188
|
|
7
|
-
rustest/approx.py,sha256=sGaH15n3vSSv84dmR_QAIDV-xwaUrX-MqwpWIp5ihjk,5904
|
|
8
|
-
rustest/cli.py,sha256=Y7I3sLMA7icsHFLdcGJMy0LElEcoqzrVrTkedE_vGTs,9474
|
|
9
|
-
rustest/core.py,sha256=2_kav-3XPhrtXnayQBDSc_tCaaaEj07rFHq5ln4Em8Q,1227
|
|
10
|
-
rustest/decorators.py,sha256=mCiqNJXYpbQMCMaVEdeiebN9KWLvgtRmqZ6nbUXpm6E,14138
|
|
11
|
-
rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
rustest/reporting.py,sha256=g4jdlwjFoKF_fWA_sKfeDwmhDeHxPJQUqypa_E2XQlc,1686
|
|
13
|
-
rustest/rust.cp312-win_amd64.pyd,sha256=RFiwEJBXWgLvajST9qNaP7RV5QbL7L6BWIMPAPgOAYQ,1341440
|
|
14
|
-
rustest/rust.py,sha256=N_1C-uXRiC2qkV7ecKVcb51-XXyfhYNepd5zs-RIYOo,682
|
|
15
|
-
rustest/rust.pyi,sha256=D2PL2GamLpcwdBDZxFeWC1ZgJ01CkvWycU62LnVTMD0,799
|
|
16
|
-
rustest-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|