rustest 0.2.0__cp313-cp313-macosx_10_12_x86_64.whl → 0.7.0__cp313-cp313-macosx_10_12_x86_64.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.
- rustest/__init__.py +9 -9
- rustest/__main__.py +1 -1
- rustest/{_cli.py → cli.py} +55 -3
- rustest/core.py +30 -7
- rustest/{_decorators.py → decorators.py} +187 -0
- rustest/py.typed +0 -0
- rustest/{_reporting.py → reporting.py} +3 -3
- rustest/rust.cpython-313-darwin.so +0 -0
- rustest/{_rust.py → rust.py} +1 -1
- rustest/{_rust.pyi → rust.pyi} +11 -7
- rustest-0.7.0.dist-info/METADATA +232 -0
- rustest-0.7.0.dist-info/RECORD +16 -0
- {rustest-0.2.0.dist-info → rustest-0.7.0.dist-info}/WHEEL +1 -1
- rustest/_rust.cpython-313-darwin.so +0 -0
- rustest-0.2.0.dist-info/METADATA +0 -521
- rustest-0.2.0.dist-info/RECORD +0 -15
- /rustest/{_approx.py → approx.py} +0 -0
- {rustest-0.2.0.dist-info → rustest-0.7.0.dist-info}/entry_points.txt +0 -0
- {rustest-0.2.0.dist-info → rustest-0.7.0.dist-info}/licenses/LICENSE +0 -0
rustest/__init__.py
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from . import
|
|
6
|
-
from .
|
|
7
|
-
from .
|
|
8
|
-
from .
|
|
5
|
+
from . import decorators
|
|
6
|
+
from .approx import approx
|
|
7
|
+
from .cli import main
|
|
8
|
+
from .reporting import RunReport, TestResult
|
|
9
9
|
from .core import run
|
|
10
10
|
|
|
11
|
-
fixture =
|
|
12
|
-
mark =
|
|
13
|
-
parametrize =
|
|
14
|
-
raises =
|
|
15
|
-
skip =
|
|
11
|
+
fixture = decorators.fixture
|
|
12
|
+
mark = decorators.mark
|
|
13
|
+
parametrize = decorators.parametrize
|
|
14
|
+
raises = decorators.raises
|
|
15
|
+
skip = decorators.skip
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"RunReport",
|
rustest/__main__.py
CHANGED
rustest/{_cli.py → cli.py}
RENAMED
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import argparse
|
|
6
6
|
from collections.abc import Sequence
|
|
7
7
|
|
|
8
|
-
from .
|
|
8
|
+
from .reporting import RunReport, TestResult
|
|
9
9
|
from .core import run
|
|
10
10
|
|
|
11
11
|
|
|
@@ -52,6 +52,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
52
52
|
"--pattern",
|
|
53
53
|
help="Substring to filter tests by (case insensitive).",
|
|
54
54
|
)
|
|
55
|
+
_ = parser.add_argument(
|
|
56
|
+
"-m",
|
|
57
|
+
"--marks",
|
|
58
|
+
dest="mark_expr",
|
|
59
|
+
help='Run tests matching the given mark expression (e.g., "slow", "not slow", "slow and integration").',
|
|
60
|
+
)
|
|
55
61
|
_ = parser.add_argument(
|
|
56
62
|
"-n",
|
|
57
63
|
"--workers",
|
|
@@ -81,7 +87,41 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
81
87
|
action="store_false",
|
|
82
88
|
help="Disable colored output.",
|
|
83
89
|
)
|
|
84
|
-
_ = parser.
|
|
90
|
+
_ = parser.add_argument(
|
|
91
|
+
"--no-codeblocks",
|
|
92
|
+
dest="enable_codeblocks",
|
|
93
|
+
action="store_false",
|
|
94
|
+
help="Disable code block tests from markdown files.",
|
|
95
|
+
)
|
|
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
|
+
)
|
|
85
125
|
return parser
|
|
86
126
|
|
|
87
127
|
|
|
@@ -93,11 +133,23 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
93
133
|
if not args.color:
|
|
94
134
|
Colors.disable()
|
|
95
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
|
+
|
|
96
144
|
report = run(
|
|
97
|
-
paths=
|
|
145
|
+
paths=list(args.paths),
|
|
98
146
|
pattern=args.pattern,
|
|
147
|
+
mark_expr=args.mark_expr,
|
|
99
148
|
workers=args.workers,
|
|
100
149
|
capture_output=args.capture_output,
|
|
150
|
+
enable_codeblocks=args.enable_codeblocks,
|
|
151
|
+
last_failed_mode=last_failed_mode,
|
|
152
|
+
fail_fast=args.fail_fast,
|
|
101
153
|
)
|
|
102
154
|
_print_report(report, verbose=args.verbose, ascii_mode=args.ascii)
|
|
103
155
|
return 0 if report.failed == 0 else 1
|
rustest/core.py
CHANGED
|
@@ -4,18 +4,41 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
from collections.abc import Sequence
|
|
6
6
|
|
|
7
|
-
from . import
|
|
8
|
-
from .
|
|
7
|
+
from . import rust
|
|
8
|
+
from .reporting import RunReport
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def run(
|
|
12
12
|
*,
|
|
13
13
|
paths: Sequence[str],
|
|
14
|
-
pattern: str | None,
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
pattern: str | None = None,
|
|
15
|
+
mark_expr: str | None = None,
|
|
16
|
+
workers: int | None = None,
|
|
17
|
+
capture_output: bool = True,
|
|
18
|
+
enable_codeblocks: bool = True,
|
|
19
|
+
last_failed_mode: str = "none",
|
|
20
|
+
fail_fast: bool = False,
|
|
17
21
|
) -> RunReport:
|
|
18
|
-
"""Execute tests and return a rich report.
|
|
22
|
+
"""Execute tests and return a rich report.
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
Args:
|
|
25
|
+
paths: Files or directories to collect tests from
|
|
26
|
+
pattern: Substring to filter tests by (case insensitive)
|
|
27
|
+
mark_expr: Mark expression to filter tests (e.g., "slow", "not slow", "slow and integration")
|
|
28
|
+
workers: Number of worker slots to use (experimental)
|
|
29
|
+
capture_output: Whether to capture stdout/stderr during test execution
|
|
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
|
|
33
|
+
"""
|
|
34
|
+
raw_report = rust.run(
|
|
35
|
+
paths=list(paths),
|
|
36
|
+
pattern=pattern,
|
|
37
|
+
mark_expr=mark_expr,
|
|
38
|
+
workers=workers,
|
|
39
|
+
capture_output=capture_output,
|
|
40
|
+
enable_codeblocks=enable_codeblocks,
|
|
41
|
+
last_failed_mode=last_failed_mode,
|
|
42
|
+
fail_fast=fail_fast,
|
|
43
|
+
)
|
|
21
44
|
return RunReport.from_py(raw_report)
|
|
@@ -157,8 +157,195 @@ class MarkGenerator:
|
|
|
157
157
|
@mark.slow
|
|
158
158
|
@mark.integration
|
|
159
159
|
@mark.timeout(seconds=30)
|
|
160
|
+
|
|
161
|
+
Standard marks:
|
|
162
|
+
@mark.skipif(condition, *, reason="...")
|
|
163
|
+
@mark.xfail(condition=None, *, reason=None, raises=None, run=True, strict=False)
|
|
164
|
+
@mark.usefixtures("fixture1", "fixture2")
|
|
165
|
+
@mark.asyncio(loop_scope="function")
|
|
160
166
|
"""
|
|
161
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
|
+
|
|
281
|
+
def skipif(
|
|
282
|
+
self,
|
|
283
|
+
condition: bool | str,
|
|
284
|
+
*,
|
|
285
|
+
reason: str | None = None,
|
|
286
|
+
) -> MarkDecorator:
|
|
287
|
+
"""Skip test if condition is true.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
condition: Boolean or string condition to evaluate
|
|
291
|
+
reason: Explanation for why the test is skipped
|
|
292
|
+
|
|
293
|
+
Usage:
|
|
294
|
+
@mark.skipif(sys.platform == "win32", reason="Not supported on Windows")
|
|
295
|
+
def test_unix_only():
|
|
296
|
+
pass
|
|
297
|
+
"""
|
|
298
|
+
return MarkDecorator("skipif", (condition,), {"reason": reason})
|
|
299
|
+
|
|
300
|
+
def xfail(
|
|
301
|
+
self,
|
|
302
|
+
condition: bool | str | None = None,
|
|
303
|
+
*,
|
|
304
|
+
reason: str | None = None,
|
|
305
|
+
raises: type[BaseException] | tuple[type[BaseException], ...] | None = None,
|
|
306
|
+
run: bool = True,
|
|
307
|
+
strict: bool = False,
|
|
308
|
+
) -> MarkDecorator:
|
|
309
|
+
"""Mark test as expected to fail.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
condition: Optional condition - if False, mark is ignored
|
|
313
|
+
reason: Explanation for why the test is expected to fail
|
|
314
|
+
raises: Expected exception type(s)
|
|
315
|
+
run: Whether to run the test (False means skip it)
|
|
316
|
+
strict: If True, passing test will fail the suite
|
|
317
|
+
|
|
318
|
+
Usage:
|
|
319
|
+
@mark.xfail(reason="Known bug in backend")
|
|
320
|
+
def test_known_bug():
|
|
321
|
+
assert False
|
|
322
|
+
|
|
323
|
+
@mark.xfail(sys.platform == "win32", reason="Not implemented on Windows")
|
|
324
|
+
def test_feature():
|
|
325
|
+
pass
|
|
326
|
+
"""
|
|
327
|
+
kwargs = {
|
|
328
|
+
"reason": reason,
|
|
329
|
+
"raises": raises,
|
|
330
|
+
"run": run,
|
|
331
|
+
"strict": strict,
|
|
332
|
+
}
|
|
333
|
+
args = () if condition is None else (condition,)
|
|
334
|
+
return MarkDecorator("xfail", args, kwargs)
|
|
335
|
+
|
|
336
|
+
def usefixtures(self, *names: str) -> MarkDecorator:
|
|
337
|
+
"""Use fixtures without explicitly requesting them as parameters.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
*names: Names of fixtures to use
|
|
341
|
+
|
|
342
|
+
Usage:
|
|
343
|
+
@mark.usefixtures("setup_db", "cleanup")
|
|
344
|
+
def test_with_fixtures():
|
|
345
|
+
pass
|
|
346
|
+
"""
|
|
347
|
+
return MarkDecorator("usefixtures", names, {})
|
|
348
|
+
|
|
162
349
|
def __getattr__(self, name: str) -> Any:
|
|
163
350
|
"""Create a mark decorator for the given name."""
|
|
164
351
|
# Return a callable that can be used as @mark.name or @mark.name(args)
|
rustest/py.typed
ADDED
|
File without changes
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
from collections.abc import Iterable
|
|
6
6
|
from dataclasses import dataclass
|
|
7
7
|
|
|
8
|
-
from . import
|
|
8
|
+
from . import rust
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
@dataclass(slots=True)
|
|
@@ -23,7 +23,7 @@ class TestResult:
|
|
|
23
23
|
stderr: str | None
|
|
24
24
|
|
|
25
25
|
@classmethod
|
|
26
|
-
def from_py(cls, result:
|
|
26
|
+
def from_py(cls, result: rust.PyTestResult) -> "TestResult":
|
|
27
27
|
return cls(
|
|
28
28
|
name=result.name,
|
|
29
29
|
path=result.path,
|
|
@@ -47,7 +47,7 @@ class RunReport:
|
|
|
47
47
|
results: tuple[TestResult, ...]
|
|
48
48
|
|
|
49
49
|
@classmethod
|
|
50
|
-
def from_py(cls, report:
|
|
50
|
+
def from_py(cls, report: rust.PyRunReport) -> "RunReport":
|
|
51
51
|
return cls(
|
|
52
52
|
total=report.total,
|
|
53
53
|
passed=report.passed,
|
|
Binary file
|
rustest/{_rust.py → rust.py}
RENAMED
|
@@ -19,5 +19,5 @@ def run(
|
|
|
19
19
|
"""Placeholder implementation that mirrors the extension signature."""
|
|
20
20
|
|
|
21
21
|
raise NotImplementedError(
|
|
22
|
-
"The rustest native extension is unavailable. Tests must patch rustest.
|
|
22
|
+
"The rustest native extension is unavailable. Tests must patch rustest.rust.run."
|
|
23
23
|
)
|
rustest/{_rust.pyi → rust.pyi}
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
"""Type stubs for the Rust extension module."""
|
|
1
|
+
"""Type stubs for the rustest Rust extension module."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from typing import Sequence
|
|
6
6
|
|
|
7
7
|
class PyTestResult:
|
|
8
|
-
"""
|
|
8
|
+
"""Individual test result from the Rust extension."""
|
|
9
9
|
|
|
10
10
|
name: str
|
|
11
11
|
path: str
|
|
@@ -16,20 +16,24 @@ class PyTestResult:
|
|
|
16
16
|
stderr: str | None
|
|
17
17
|
|
|
18
18
|
class PyRunReport:
|
|
19
|
-
"""
|
|
19
|
+
"""Test run report from the Rust extension."""
|
|
20
20
|
|
|
21
21
|
total: int
|
|
22
22
|
passed: int
|
|
23
23
|
failed: int
|
|
24
24
|
skipped: int
|
|
25
25
|
duration: float
|
|
26
|
-
results:
|
|
26
|
+
results: list[PyTestResult]
|
|
27
27
|
|
|
28
28
|
def run(
|
|
29
|
-
paths:
|
|
29
|
+
paths: Sequence[str],
|
|
30
30
|
pattern: str | None,
|
|
31
|
+
mark_expr: str | None,
|
|
31
32
|
workers: int | None,
|
|
32
33
|
capture_output: bool,
|
|
34
|
+
enable_codeblocks: bool,
|
|
35
|
+
last_failed_mode: str,
|
|
36
|
+
fail_fast: bool,
|
|
33
37
|
) -> PyRunReport:
|
|
34
|
-
"""
|
|
38
|
+
"""Execute tests and return a report."""
|
|
35
39
|
...
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rustest
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Classifier: Programming Language :: Rust
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Dist: typing-extensions>=4.15
|
|
16
|
+
Requires-Dist: basedpyright>=1.19 ; extra == 'dev'
|
|
17
|
+
Requires-Dist: maturin>=1.4,<2 ; extra == 'dev'
|
|
18
|
+
Requires-Dist: poethepoet>=0.22 ; extra == 'dev'
|
|
19
|
+
Requires-Dist: pre-commit>=3.5 ; extra == 'dev'
|
|
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'
|
|
23
|
+
Requires-Dist: ruff>=0.1.9 ; extra == 'dev'
|
|
24
|
+
Requires-Dist: mkdocs>=1.5.0 ; extra == 'docs'
|
|
25
|
+
Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'docs'
|
|
26
|
+
Requires-Dist: mkdocstrings[python]>=0.24.0 ; extra == 'docs'
|
|
27
|
+
Requires-Dist: mkdocs-autorefs>=0.5.0 ; extra == 'docs'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Provides-Extra: docs
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Summary: Rust powered pytest-compatible runner
|
|
32
|
+
Author: rustest contributors
|
|
33
|
+
Requires-Python: >=3.10
|
|
34
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
35
|
+
Project-URL: Homepage, https://github.com/Apex-Engineers-Inc/rustest
|
|
36
|
+
Project-URL: Repository, https://github.com/Apex-Engineers-Inc/rustest
|
|
37
|
+
Project-URL: Documentation, https://apex-engineers-inc.github.io/rustest
|
|
38
|
+
|
|
39
|
+
# rustest
|
|
40
|
+
|
|
41
|
+
Rustest (pronounced like Russ-Test) is a Rust-powered test runner that aims to provide the most common pytest ergonomics with a focus on raw performance. Get **~2x faster** test execution with familiar syntax and minimal setup.
|
|
42
|
+
|
|
43
|
+
📚 **[Full Documentation](https://apex-engineers-inc.github.io/rustest)** | [Getting Started](https://apex-engineers-inc.github.io/rustest/getting-started/quickstart/) | [User Guide](https://apex-engineers-inc.github.io/rustest/guide/writing-tests/) | [API Reference](https://apex-engineers-inc.github.io/rustest/api/overview/)
|
|
44
|
+
|
|
45
|
+
## Why rustest?
|
|
46
|
+
|
|
47
|
+
- 🚀 **About 2x faster** than pytest on the rustest integration test suite
|
|
48
|
+
- ✅ Familiar `@fixture`, `@parametrize`, `@skip`, and `@mark` decorators
|
|
49
|
+
- 🔄 **Built-in async support** with `@mark.asyncio` (like pytest-asyncio)
|
|
50
|
+
- 🔍 Automatic test discovery (`test_*.py` and `*_test.py` files)
|
|
51
|
+
- 📝 **Built-in markdown code block testing** (like pytest-codeblocks, but faster)
|
|
52
|
+
- 🎯 Simple, clean API—if you know pytest, you already know rustest
|
|
53
|
+
- 🧮 Built-in `approx()` helper for tolerant numeric comparisons
|
|
54
|
+
- 🪤 `raises()` context manager for precise exception assertions
|
|
55
|
+
- 📦 Easy installation with pip or uv
|
|
56
|
+
- ⚡ Low-overhead execution keeps small suites feeling instant
|
|
57
|
+
|
|
58
|
+
## Performance
|
|
59
|
+
|
|
60
|
+
Rustest is designed for speed. Our latest benchmarks on the rustest integration suite (~200 tests) show a consistent **2.1x wall-clock speedup** over pytest:
|
|
61
|
+
|
|
62
|
+
| Test Runner | Wall Clock | Speedup | Command |
|
|
63
|
+
|-------------|------------|---------|---------|
|
|
64
|
+
| pytest | 1.33–1.59s | 1.0x (baseline) | `pytest tests/ examples/tests/ -q` |
|
|
65
|
+
| rustest | 0.69–0.70s | **~2.1x faster** | `python -m rustest tests/ examples/tests/` |
|
|
66
|
+
|
|
67
|
+
### Large Parametrized Stress Test
|
|
68
|
+
|
|
69
|
+
With **10,000 parametrized invocations**:
|
|
70
|
+
|
|
71
|
+
| Test Runner | Avg. Wall Clock | Speedup | Command |
|
|
72
|
+
|-------------|-----------------|---------|---------|
|
|
73
|
+
| pytest | 9.72s | 1.0x | `pytest benchmarks/test_large_parametrize.py -q` |
|
|
74
|
+
| rustest | 0.41s | **~24x faster** | `python -m rustest benchmarks/test_large_parametrize.py` |
|
|
75
|
+
|
|
76
|
+
**[📊 View Detailed Performance Analysis →](https://apex-engineers-inc.github.io/rustest/advanced/performance/)**
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
Rustest supports Python **3.10 through 3.14**.
|
|
81
|
+
|
|
82
|
+
<!--pytest.mark.skip-->
|
|
83
|
+
```bash
|
|
84
|
+
# Using pip
|
|
85
|
+
pip install rustest
|
|
86
|
+
|
|
87
|
+
# Using uv
|
|
88
|
+
uv add rustest
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**[📖 Installation Guide →](https://apex-engineers-inc.github.io/rustest/getting-started/installation/)**
|
|
92
|
+
|
|
93
|
+
## Quick Start
|
|
94
|
+
|
|
95
|
+
### 1. Write Your Tests
|
|
96
|
+
|
|
97
|
+
Create a file `test_math.py`:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from rustest import fixture, parametrize, mark, approx, raises
|
|
101
|
+
import asyncio
|
|
102
|
+
|
|
103
|
+
@fixture
|
|
104
|
+
def numbers() -> list[int]:
|
|
105
|
+
return [1, 2, 3, 4, 5]
|
|
106
|
+
|
|
107
|
+
def test_sum(numbers: list[int]) -> None:
|
|
108
|
+
assert sum(numbers) == approx(15)
|
|
109
|
+
|
|
110
|
+
@parametrize("value,expected", [(2, 4), (3, 9), (4, 16)])
|
|
111
|
+
def test_square(value: int, expected: int) -> None:
|
|
112
|
+
assert value ** 2 == expected
|
|
113
|
+
|
|
114
|
+
@mark.slow
|
|
115
|
+
def test_expensive_operation() -> None:
|
|
116
|
+
result = sum(range(1000000))
|
|
117
|
+
assert result > 0
|
|
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
|
+
|
|
126
|
+
def test_division_by_zero() -> None:
|
|
127
|
+
with raises(ZeroDivisionError, match="division by zero"):
|
|
128
|
+
1 / 0
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### 2. Run Your Tests
|
|
132
|
+
|
|
133
|
+
<!--pytest.mark.skip-->
|
|
134
|
+
```bash
|
|
135
|
+
# Run all tests
|
|
136
|
+
rustest
|
|
137
|
+
|
|
138
|
+
# Run specific tests
|
|
139
|
+
rustest tests/
|
|
140
|
+
|
|
141
|
+
# Filter by test name pattern
|
|
142
|
+
rustest -k "test_sum"
|
|
143
|
+
|
|
144
|
+
# Filter by marks
|
|
145
|
+
rustest -m "slow" # Run only slow tests
|
|
146
|
+
rustest -m "not slow" # Skip slow tests
|
|
147
|
+
rustest -m "slow and integration" # Run tests with both marks
|
|
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
|
+
|
|
159
|
+
# Show output during execution
|
|
160
|
+
rustest --no-capture
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**[📖 Full Quick Start Guide →](https://apex-engineers-inc.github.io/rustest/getting-started/quickstart/)**
|
|
164
|
+
|
|
165
|
+
## Documentation
|
|
166
|
+
|
|
167
|
+
**[📚 Full Documentation](https://apex-engineers-inc.github.io/rustest)**
|
|
168
|
+
|
|
169
|
+
### Getting Started
|
|
170
|
+
- [Installation](https://apex-engineers-inc.github.io/rustest/getting-started/installation/)
|
|
171
|
+
- [Quick Start](https://apex-engineers-inc.github.io/rustest/getting-started/quickstart/)
|
|
172
|
+
|
|
173
|
+
### User Guide
|
|
174
|
+
- [Writing Tests](https://apex-engineers-inc.github.io/rustest/guide/writing-tests/)
|
|
175
|
+
- [Fixtures](https://apex-engineers-inc.github.io/rustest/guide/fixtures/)
|
|
176
|
+
- [Parametrization](https://apex-engineers-inc.github.io/rustest/guide/parametrization/)
|
|
177
|
+
- [Marks & Skipping](https://apex-engineers-inc.github.io/rustest/guide/marks/)
|
|
178
|
+
- [Test Classes](https://apex-engineers-inc.github.io/rustest/guide/test-classes/)
|
|
179
|
+
- [Assertion Helpers](https://apex-engineers-inc.github.io/rustest/guide/assertions/)
|
|
180
|
+
- [Markdown Testing](https://apex-engineers-inc.github.io/rustest/guide/markdown-testing/)
|
|
181
|
+
- [CLI Usage](https://apex-engineers-inc.github.io/rustest/guide/cli/)
|
|
182
|
+
- [Python API](https://apex-engineers-inc.github.io/rustest/guide/python-api/)
|
|
183
|
+
|
|
184
|
+
### API Reference
|
|
185
|
+
- [API Overview](https://apex-engineers-inc.github.io/rustest/api/overview/)
|
|
186
|
+
- [Decorators](https://apex-engineers-inc.github.io/rustest/api/decorators/)
|
|
187
|
+
- [Test Execution](https://apex-engineers-inc.github.io/rustest/api/core/)
|
|
188
|
+
- [Reporting](https://apex-engineers-inc.github.io/rustest/api/reporting/)
|
|
189
|
+
- [Assertion Utilities](https://apex-engineers-inc.github.io/rustest/api/approx/)
|
|
190
|
+
|
|
191
|
+
### Advanced Topics
|
|
192
|
+
- [Performance](https://apex-engineers-inc.github.io/rustest/advanced/performance/)
|
|
193
|
+
- [Comparison with pytest](https://apex-engineers-inc.github.io/rustest/advanced/comparison/)
|
|
194
|
+
- [Development Guide](https://apex-engineers-inc.github.io/rustest/advanced/development/)
|
|
195
|
+
|
|
196
|
+
## Feature Comparison with pytest
|
|
197
|
+
|
|
198
|
+
Rustest implements the 20% of pytest features that cover 80% of use cases, with a focus on raw speed and simplicity.
|
|
199
|
+
|
|
200
|
+
**[📋 View Full Feature Comparison →](https://apex-engineers-inc.github.io/rustest/advanced/comparison/)**
|
|
201
|
+
|
|
202
|
+
✅ **Supported:** Fixtures, parametrization, marks, test classes, conftest.py, markdown testing
|
|
203
|
+
🚧 **Planned:** Parallel execution, mark filtering, JUnit XML output
|
|
204
|
+
❌ **Not Planned:** Plugins, hooks, custom collectors (keeps rustest simple)
|
|
205
|
+
|
|
206
|
+
## Contributing
|
|
207
|
+
|
|
208
|
+
We welcome contributions! See the [Development Guide](https://apex-engineers-inc.github.io/rustest/advanced/development/) for setup instructions.
|
|
209
|
+
|
|
210
|
+
Quick reference:
|
|
211
|
+
|
|
212
|
+
<!--pytest.mark.skip-->
|
|
213
|
+
```bash
|
|
214
|
+
# Setup
|
|
215
|
+
git clone https://github.com/Apex-Engineers-Inc/rustest.git
|
|
216
|
+
cd rustest
|
|
217
|
+
uv sync --all-extras
|
|
218
|
+
uv run maturin develop
|
|
219
|
+
|
|
220
|
+
# Run tests
|
|
221
|
+
uv run poe pytests # Python tests
|
|
222
|
+
cargo test # Rust tests
|
|
223
|
+
|
|
224
|
+
# Format and lint
|
|
225
|
+
uv run pre-commit install # One-time setup
|
|
226
|
+
git commit -m "message" # Pre-commit hooks run automatically
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
## License
|
|
230
|
+
|
|
231
|
+
rustest is distributed under the terms of the MIT license. See [LICENSE](LICENSE).
|
|
232
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rustest-0.7.0.dist-info/METADATA,sha256=4oreZ3cw8nBFQuC5UMDhGkci1x-9p5KT6sz0YXkuhoA,8818
|
|
2
|
+
rustest-0.7.0.dist-info/WHEEL,sha256=Vw5ZLWgt_2Ah16i4JL1BF7IIc3gqArKE4ITLavudIhM,107
|
|
3
|
+
rustest-0.7.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
|
|
4
|
+
rustest-0.7.0.dist-info/licenses/LICENSE,sha256=s64ibUGtb6jEDBsYuxUFtMr_c4PaqYP-vj3YY6QtTGw,1075
|
|
5
|
+
rustest/__init__.py,sha256=0CkHfrmIjpGw6DMu2VcZLOUpBVtdwsN-aitn-RzOglo,514
|
|
6
|
+
rustest/__main__.py,sha256=bBvo5gsSluUzlDTDvn5bP_gZZEXMwJQZMqVA5W1M1v8,178
|
|
7
|
+
rustest/approx.py,sha256=MKmuorBBHqpH0h0QaIMVjbm3-mXJ0E90limEgSHHVfw,5744
|
|
8
|
+
rustest/cli.py,sha256=26zaX635WqW0q8B-alb0Kk0wzIDZVkF-y9ZqzKtHDbI,10185
|
|
9
|
+
rustest/core.py,sha256=FBnr3yzHQequ83R-pRlwlCdbcYlkjjucwtHpp0gnqdQ,1461
|
|
10
|
+
rustest/decorators.py,sha256=mZDieFSaUx5FIeDqsWO07H6E0urk70cOYtQIuCIjKzY,18703
|
|
11
|
+
rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
rustest/reporting.py,sha256=3-R8aljv2ZbmjOa9Q9KZeCPsMaitm8nZ96LoJS_NnUQ,1623
|
|
13
|
+
rustest/rust.cpython-313-darwin.so,sha256=6khWVzzAZDRmo6b8c2vlFC0T9JunQjVMLnZ8HnGE7pU,1668920
|
|
14
|
+
rustest/rust.py,sha256=tCIvjYd06VxoT_rKvv2o8CpXW_pFNua5VgcRDjLgU78,659
|
|
15
|
+
rustest/rust.pyi,sha256=bJDdaokbRZWPtTZ1bwsXWb43L9jwOm2gLkk7LzNlQcw,810
|
|
16
|
+
rustest-0.7.0.dist-info/RECORD,,
|
|
Binary file
|
rustest-0.2.0.dist-info/METADATA
DELETED
|
@@ -1,521 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: rustest
|
|
3
|
-
Version: 0.2.0
|
|
4
|
-
Classifier: Development Status :: 3 - Alpha
|
|
5
|
-
Classifier: Intended Audience :: Developers
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
|
8
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
-
Classifier: Programming Language :: Rust
|
|
14
|
-
Classifier: Topic :: Software Development :: Testing
|
|
15
|
-
Requires-Dist: typing-extensions>=4.15
|
|
16
|
-
Requires-Dist: basedpyright>=1.19 ; extra == 'dev'
|
|
17
|
-
Requires-Dist: maturin>=1.4,<2 ; extra == 'dev'
|
|
18
|
-
Requires-Dist: poethepoet>=0.22 ; extra == 'dev'
|
|
19
|
-
Requires-Dist: pre-commit>=3.5 ; extra == 'dev'
|
|
20
|
-
Requires-Dist: pytest>=7.0 ; extra == 'dev'
|
|
21
|
-
Requires-Dist: ruff>=0.1.9 ; extra == 'dev'
|
|
22
|
-
Provides-Extra: dev
|
|
23
|
-
License-File: LICENSE
|
|
24
|
-
Summary: Rust powered pytest-compatible runner
|
|
25
|
-
Author: rustest contributors
|
|
26
|
-
Requires-Python: >=3.10
|
|
27
|
-
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
28
|
-
Project-URL: Homepage, https://github.com/Apex-Engineers-Inc/rustest
|
|
29
|
-
Project-URL: Repository, https://github.com/Apex-Engineers-Inc/rustest
|
|
30
|
-
|
|
31
|
-
# rustest
|
|
32
|
-
|
|
33
|
-
Rustest (pronounced like Russ-Test) is a Rust-powered test runner that aims to provide the most common pytest ergonomics with a focus on raw performance. Get **~2x faster** test execution with familiar syntax and minimal setup.
|
|
34
|
-
|
|
35
|
-
## Why rustest?
|
|
36
|
-
|
|
37
|
-
- 🚀 **About 2x faster** than pytest on the rustest integration test suite
|
|
38
|
-
- ✅ Familiar `@fixture`, `@parametrize`, `@skip`, and `@mark` decorators
|
|
39
|
-
- 🔍 Automatic test discovery (`test_*.py` and `*_test.py` files)
|
|
40
|
-
- 🎯 Simple, clean API—if you know pytest, you already know rustest
|
|
41
|
-
- 🧮 Built-in `approx()` helper for tolerant numeric comparisons across scalars, collections, and complex numbers
|
|
42
|
-
- 🪤 `raises()` context manager for precise exception assertions with optional message matching
|
|
43
|
-
- 📦 Easy installation with pip or uv
|
|
44
|
-
- ⚡ Low-overhead execution keeps small suites feeling instant
|
|
45
|
-
|
|
46
|
-
## Performance
|
|
47
|
-
|
|
48
|
-
Rustest is designed for speed. Our latest benchmarks on the rustest integration suite (~200 tests) show a consistent **2.1x wall-clock speedup** over pytest:
|
|
49
|
-
|
|
50
|
-
| Test Runner | Reported Runtime† | Wall Clock‡ | Speedup (wall) | Command |
|
|
51
|
-
|-------------|------------------|-------------|----------------|---------|
|
|
52
|
-
| pytest | 0.43–0.59s | 1.33–1.59s | 1.0x (baseline) | `pytest tests/ examples/tests/ -q`
|
|
53
|
-
| rustest | 0.003s | 0.69–0.70s | **~2.1x faster** | `python -m rustest tests/ examples/tests/`§
|
|
54
|
-
|
|
55
|
-
### Large parametrized stress test
|
|
56
|
-
|
|
57
|
-
We also profiled an extreme case with **10,000 parametrized invocations** to ensure rustest scales on synthetic but heavy workloads. The test lives in [`benchmarks/test_large_parametrize.py`](benchmarks/test_large_parametrize.py) and simply asserts `value + value == 2 * value` across every case. Running the module on its own shows a dramatic gap:
|
|
58
|
-
|
|
59
|
-
| Test Runner | Avg. Wall Clock (3 runs) | Speedup | Command |
|
|
60
|
-
|-------------|--------------------------|---------|---------|
|
|
61
|
-
| pytest | 9.72s | 1.0x | `pytest benchmarks/test_large_parametrize.py -q`§
|
|
62
|
-
| rustest | 0.41s | **~24x** | `python -m rustest benchmarks/test_large_parametrize.py`§
|
|
63
|
-
|
|
64
|
-
† pytest and rustest both report only active test execution time; rustest's figure omits Python interpreter start-up overhead.
|
|
65
|
-
|
|
66
|
-
‡ Integration-suite wall-clock timing measured with the shell `time` builtin across two consecutive runs in the same environment.
|
|
67
|
-
|
|
68
|
-
§ Commands executed with `PYTHONPATH=python` in this repository checkout to exercise the local sources. Pytest relies on a small compatibility shim in [`benchmarks/conftest.py`](benchmarks/conftest.py) so it understands the rustest-style decorators. Large-parametrization timings come from averaging three `time.perf_counter()` measurements with output suppressed via `subprocess.DEVNULL`.
|
|
69
|
-
|
|
70
|
-
Rustest counts parametrized cases slightly differently than pytest, so you will see 199 executed cases vs. pytest's 201 discoveries on the same suite—the reported pass/skip counts still align.
|
|
71
|
-
|
|
72
|
-
**Why is rustest faster?**
|
|
73
|
-
- **Near-zero startup time**: Native Rust binary minimizes overhead before Python code starts running.
|
|
74
|
-
- **Rust-native test discovery**: Minimal imports until test execution keeps collection quick.
|
|
75
|
-
- **Optimized fixture resolution**: Efficient dependency graph resolution reduces per-test work.
|
|
76
|
-
- **Lean orchestration**: Rust handles scheduling and reporting so the Python interpreter focuses on running test bodies.
|
|
77
|
-
|
|
78
|
-
**Real-world impact:**
|
|
79
|
-
- **200 tests** (this repository): 1.46s → 0.70s (average wall-clock, ~0.76s saved per run)
|
|
80
|
-
- **1,000 tests** (projected): ~7.3s → ~3.4s assuming similar scaling
|
|
81
|
-
- **10,000 tests** (projected): ~73s → ~34s—minutes saved across CI runs
|
|
82
|
-
|
|
83
|
-
See [BENCHMARKS.md](BENCHMARKS.md) for detailed performance analysis and methodology.
|
|
84
|
-
|
|
85
|
-
## Installation
|
|
86
|
-
|
|
87
|
-
Rustest supports Python **3.10 through 3.14**.
|
|
88
|
-
|
|
89
|
-
### Using pip
|
|
90
|
-
```bash
|
|
91
|
-
pip install rustest
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
### Using uv
|
|
95
|
-
```bash
|
|
96
|
-
uv add rustest
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
### For Development
|
|
100
|
-
If you want to contribute to rustest, see [DEVELOPMENT.md](DEVELOPMENT.md) for setup instructions.
|
|
101
|
-
|
|
102
|
-
## Quick Start
|
|
103
|
-
|
|
104
|
-
### 1. Write Your Tests
|
|
105
|
-
|
|
106
|
-
Create a file `test_math.py`:
|
|
107
|
-
|
|
108
|
-
```python
|
|
109
|
-
from rustest import fixture, parametrize, mark, approx, raises
|
|
110
|
-
|
|
111
|
-
@fixture
|
|
112
|
-
def numbers() -> list[int]:
|
|
113
|
-
return [1, 2, 3, 4, 5]
|
|
114
|
-
|
|
115
|
-
def test_sum(numbers: list[int]) -> None:
|
|
116
|
-
assert sum(numbers) == approx(15)
|
|
117
|
-
|
|
118
|
-
@parametrize("value,expected", [(2, 4), (3, 9), (4, 16)])
|
|
119
|
-
def test_square(value: int, expected: int) -> None:
|
|
120
|
-
assert value ** 2 == expected
|
|
121
|
-
|
|
122
|
-
@mark.slow
|
|
123
|
-
def test_expensive_operation() -> None:
|
|
124
|
-
# This test is marked as slow for filtering
|
|
125
|
-
result = sum(range(1000000))
|
|
126
|
-
assert result > 0
|
|
127
|
-
|
|
128
|
-
def test_division_by_zero_is_reported() -> None:
|
|
129
|
-
with raises(ZeroDivisionError, match="division by zero"):
|
|
130
|
-
1 / 0
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
### 2. Run Your Tests
|
|
134
|
-
|
|
135
|
-
```bash
|
|
136
|
-
# Run all tests in the current directory
|
|
137
|
-
rustest
|
|
138
|
-
|
|
139
|
-
# Run tests in a specific directory
|
|
140
|
-
rustest tests/
|
|
141
|
-
|
|
142
|
-
# Run tests matching a pattern
|
|
143
|
-
rustest -k "test_sum"
|
|
144
|
-
|
|
145
|
-
# Show output during test execution
|
|
146
|
-
rustest --no-capture
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
## Usage Examples
|
|
150
|
-
|
|
151
|
-
### CLI Usage
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
# Run all tests in current directory
|
|
155
|
-
rustest
|
|
156
|
-
|
|
157
|
-
# Run tests in specific paths
|
|
158
|
-
rustest tests/ integration/
|
|
159
|
-
|
|
160
|
-
# Filter tests by name pattern
|
|
161
|
-
rustest -k "user" # Runs test_user_login, test_user_signup, etc.
|
|
162
|
-
rustest -k "auth" # Runs all tests with "auth" in the name
|
|
163
|
-
|
|
164
|
-
# Control output capture
|
|
165
|
-
rustest --no-capture # See print statements during test execution
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
### Python API Usage
|
|
169
|
-
|
|
170
|
-
You can also run rustest programmatically from Python:
|
|
171
|
-
|
|
172
|
-
```python
|
|
173
|
-
from rustest import run
|
|
174
|
-
|
|
175
|
-
# Basic usage
|
|
176
|
-
report = run(paths=["tests"])
|
|
177
|
-
print(f"Passed: {report.passed}, Failed: {report.failed}")
|
|
178
|
-
|
|
179
|
-
# With pattern filtering
|
|
180
|
-
report = run(paths=["tests"], pattern="user")
|
|
181
|
-
|
|
182
|
-
# Without output capture (see print statements)
|
|
183
|
-
report = run(paths=["tests"], capture_output=False)
|
|
184
|
-
|
|
185
|
-
# Access individual test results
|
|
186
|
-
for result in report.results:
|
|
187
|
-
print(f"{result.name}: {result.status} ({result.duration:.3f}s)")
|
|
188
|
-
if result.status == "failed":
|
|
189
|
-
print(f" Error: {result.message}")
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
### Writing Tests
|
|
193
|
-
|
|
194
|
-
#### Basic Test Functions
|
|
195
|
-
|
|
196
|
-
```python
|
|
197
|
-
def test_simple_assertion() -> None:
|
|
198
|
-
assert 1 + 1 == 2
|
|
199
|
-
|
|
200
|
-
def test_string_operations() -> None:
|
|
201
|
-
text = "hello world"
|
|
202
|
-
assert text.startswith("hello")
|
|
203
|
-
assert "world" in text
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
#### Using Fixtures
|
|
207
|
-
|
|
208
|
-
Fixtures provide reusable test data and setup:
|
|
209
|
-
|
|
210
|
-
```python
|
|
211
|
-
from rustest import fixture
|
|
212
|
-
|
|
213
|
-
@fixture
|
|
214
|
-
def database_connection() -> dict:
|
|
215
|
-
# Setup: create a connection
|
|
216
|
-
conn = {"host": "localhost", "port": 5432}
|
|
217
|
-
return conn
|
|
218
|
-
# Teardown happens automatically
|
|
219
|
-
|
|
220
|
-
@fixture
|
|
221
|
-
def sample_user() -> dict:
|
|
222
|
-
return {"id": 1, "name": "Alice", "email": "alice@example.com"}
|
|
223
|
-
|
|
224
|
-
def test_database_query(database_connection: dict) -> None:
|
|
225
|
-
assert database_connection["host"] == "localhost"
|
|
226
|
-
|
|
227
|
-
def test_user_email(sample_user: dict) -> None:
|
|
228
|
-
assert "@" in sample_user["email"]
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
#### Fixtures with Dependencies
|
|
232
|
-
|
|
233
|
-
Fixtures can depend on other fixtures:
|
|
234
|
-
|
|
235
|
-
```python
|
|
236
|
-
from rustest import fixture
|
|
237
|
-
|
|
238
|
-
@fixture
|
|
239
|
-
def api_url() -> str:
|
|
240
|
-
return "https://api.example.com"
|
|
241
|
-
|
|
242
|
-
@fixture
|
|
243
|
-
def api_client(api_url: str) -> dict:
|
|
244
|
-
return {"base_url": api_url, "timeout": 30}
|
|
245
|
-
|
|
246
|
-
def test_api_configuration(api_client: dict) -> None:
|
|
247
|
-
assert api_client["base_url"].startswith("https://")
|
|
248
|
-
assert api_client["timeout"] == 30
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
#### Assertion Helpers
|
|
252
|
-
|
|
253
|
-
Rustest ships helpers for expressive assertions:
|
|
254
|
-
|
|
255
|
-
```python
|
|
256
|
-
from rustest import approx, raises
|
|
257
|
-
|
|
258
|
-
def test_nearly_equal() -> None:
|
|
259
|
-
assert 0.1 + 0.2 == approx(0.3, rel=1e-9)
|
|
260
|
-
|
|
261
|
-
def test_raises_with_message() -> None:
|
|
262
|
-
with raises(ValueError, match="invalid configuration"):
|
|
263
|
-
raise ValueError("invalid configuration")
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
#### Yield Fixtures with Setup/Teardown
|
|
267
|
-
|
|
268
|
-
Fixtures can use `yield` to perform cleanup after tests:
|
|
269
|
-
|
|
270
|
-
```python
|
|
271
|
-
from rustest import fixture
|
|
272
|
-
|
|
273
|
-
@fixture
|
|
274
|
-
def database_connection():
|
|
275
|
-
# Setup: create connection
|
|
276
|
-
conn = create_db_connection()
|
|
277
|
-
print("Database connected")
|
|
278
|
-
|
|
279
|
-
yield conn
|
|
280
|
-
|
|
281
|
-
# Teardown: close connection
|
|
282
|
-
conn.close()
|
|
283
|
-
print("Database connection closed")
|
|
284
|
-
|
|
285
|
-
@fixture
|
|
286
|
-
def temp_file():
|
|
287
|
-
# Setup
|
|
288
|
-
file = open("temp.txt", "w")
|
|
289
|
-
file.write("test data")
|
|
290
|
-
|
|
291
|
-
yield file
|
|
292
|
-
|
|
293
|
-
# Teardown
|
|
294
|
-
file.close()
|
|
295
|
-
os.remove("temp.txt")
|
|
296
|
-
|
|
297
|
-
def test_database_query(database_connection):
|
|
298
|
-
result = database_connection.query("SELECT 1")
|
|
299
|
-
assert result is not None
|
|
300
|
-
```
|
|
301
|
-
|
|
302
|
-
#### Fixture Scopes
|
|
303
|
-
|
|
304
|
-
Fixtures support different scopes to control when they are created and destroyed:
|
|
305
|
-
|
|
306
|
-
```python
|
|
307
|
-
from rustest import fixture
|
|
308
|
-
|
|
309
|
-
@fixture # Default: function scope - new instance per test
|
|
310
|
-
def function_fixture() -> dict:
|
|
311
|
-
return {"value": "reset each test"}
|
|
312
|
-
|
|
313
|
-
@fixture(scope="class") # Shared across all tests in a class
|
|
314
|
-
def class_database() -> dict:
|
|
315
|
-
return {"connection": "db://test", "shared": True}
|
|
316
|
-
|
|
317
|
-
@fixture(scope="module") # Shared across all tests in a module
|
|
318
|
-
def module_config() -> dict:
|
|
319
|
-
return {"env": "test", "timeout": 30}
|
|
320
|
-
|
|
321
|
-
@fixture(scope="session") # Shared across entire test session
|
|
322
|
-
def session_cache() -> dict:
|
|
323
|
-
return {"global_cache": {}}
|
|
324
|
-
|
|
325
|
-
# Fixtures can depend on fixtures with different scopes
|
|
326
|
-
@fixture(scope="function")
|
|
327
|
-
def request_handler(module_config: dict, session_cache: dict) -> dict:
|
|
328
|
-
return {
|
|
329
|
-
"config": module_config, # module-scoped
|
|
330
|
-
"cache": session_cache, # session-scoped
|
|
331
|
-
"request_id": id(object()) # unique per test
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
**Scope Behavior:**
|
|
336
|
-
- `function` (default): New instance for each test function
|
|
337
|
-
- `class`: Shared across all test methods in a test class
|
|
338
|
-
- `module`: Shared across all tests in a Python module
|
|
339
|
-
- `session`: Shared across the entire test session
|
|
340
|
-
|
|
341
|
-
Scoped fixtures are especially useful for expensive setup operations like database connections, API clients, or configuration loading.
|
|
342
|
-
|
|
343
|
-
**Using conftest.py for Shared Fixtures:**
|
|
344
|
-
|
|
345
|
-
You can define fixtures in a `conftest.py` file to share them across multiple test files:
|
|
346
|
-
|
|
347
|
-
```python
|
|
348
|
-
# conftest.py
|
|
349
|
-
from rustest import fixture
|
|
350
|
-
|
|
351
|
-
@fixture(scope="session")
|
|
352
|
-
def database():
|
|
353
|
-
"""Shared database connection for all tests."""
|
|
354
|
-
db = setup_database()
|
|
355
|
-
yield db
|
|
356
|
-
db.cleanup()
|
|
357
|
-
|
|
358
|
-
@fixture(scope="module")
|
|
359
|
-
def api_client():
|
|
360
|
-
"""API client shared across a module."""
|
|
361
|
-
return create_api_client()
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
All test files in the same directory (and subdirectories) can use these fixtures automatically.
|
|
365
|
-
|
|
366
|
-
#### Parametrized Tests
|
|
367
|
-
|
|
368
|
-
Run the same test with different inputs:
|
|
369
|
-
|
|
370
|
-
```python
|
|
371
|
-
from rustest import parametrize
|
|
372
|
-
|
|
373
|
-
@parametrize("input,expected", [
|
|
374
|
-
(1, 2),
|
|
375
|
-
(2, 4),
|
|
376
|
-
(3, 6),
|
|
377
|
-
])
|
|
378
|
-
def test_double(input: int, expected: int) -> None:
|
|
379
|
-
assert input * 2 == expected
|
|
380
|
-
|
|
381
|
-
# With custom test IDs for better output
|
|
382
|
-
@parametrize("value,expected", [
|
|
383
|
-
(2, 4),
|
|
384
|
-
(3, 9),
|
|
385
|
-
(4, 16),
|
|
386
|
-
], ids=["two", "three", "four"])
|
|
387
|
-
def test_square(value: int, expected: int) -> None:
|
|
388
|
-
assert value ** 2 == expected
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
#### Combining Fixtures and Parameters
|
|
392
|
-
|
|
393
|
-
```python
|
|
394
|
-
from rustest import fixture, parametrize
|
|
395
|
-
|
|
396
|
-
@fixture
|
|
397
|
-
def multiplier() -> int:
|
|
398
|
-
return 10
|
|
399
|
-
|
|
400
|
-
@parametrize("value,expected", [
|
|
401
|
-
(1, 10),
|
|
402
|
-
(2, 20),
|
|
403
|
-
(3, 30),
|
|
404
|
-
])
|
|
405
|
-
def test_multiply(multiplier: int, value: int, expected: int) -> None:
|
|
406
|
-
assert multiplier * value == expected
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
#### Skipping Tests
|
|
410
|
-
|
|
411
|
-
```python
|
|
412
|
-
from rustest import skip, mark
|
|
413
|
-
|
|
414
|
-
@skip("Not implemented yet")
|
|
415
|
-
def test_future_feature() -> None:
|
|
416
|
-
assert False
|
|
417
|
-
|
|
418
|
-
@mark.skip(reason="Waiting for API update")
|
|
419
|
-
def test_deprecated_api() -> None:
|
|
420
|
-
assert False
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
#### Using Marks to Organize Tests
|
|
424
|
-
|
|
425
|
-
```python
|
|
426
|
-
from rustest import mark
|
|
427
|
-
|
|
428
|
-
@mark.unit
|
|
429
|
-
def test_calculation() -> None:
|
|
430
|
-
assert 2 + 2 == 4
|
|
431
|
-
|
|
432
|
-
@mark.integration
|
|
433
|
-
def test_database_integration() -> None:
|
|
434
|
-
# Integration test
|
|
435
|
-
pass
|
|
436
|
-
|
|
437
|
-
@mark.slow
|
|
438
|
-
@mark.integration
|
|
439
|
-
def test_full_workflow() -> None:
|
|
440
|
-
# This test has multiple marks
|
|
441
|
-
pass
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
### Test Output
|
|
445
|
-
|
|
446
|
-
When you run rustest, you'll see clean, informative output:
|
|
447
|
-
|
|
448
|
-
```
|
|
449
|
-
PASSED 0.001s test_simple_assertion
|
|
450
|
-
PASSED 0.002s test_string_operations
|
|
451
|
-
PASSED 0.001s test_database_query
|
|
452
|
-
PASSED 0.003s test_square[two]
|
|
453
|
-
PASSED 0.001s test_square[three]
|
|
454
|
-
PASSED 0.002s test_square[four]
|
|
455
|
-
SKIPPED 0.000s test_future_feature
|
|
456
|
-
FAILED 0.005s test_broken_feature
|
|
457
|
-
----------------------------------------
|
|
458
|
-
AssertionError: Expected 5, got 4
|
|
459
|
-
at test_example.py:42
|
|
460
|
-
|
|
461
|
-
8 tests: 6 passed, 1 failed, 1 skipped in 0.015s
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
## Feature Comparison with pytest
|
|
465
|
-
|
|
466
|
-
Rustest aims to provide the most commonly-used pytest features with dramatically better performance. Here's how the two compare:
|
|
467
|
-
|
|
468
|
-
| Feature | pytest | rustest | Notes |
|
|
469
|
-
|---------|--------|---------|-------|
|
|
470
|
-
| **Core Test Discovery** |
|
|
471
|
-
| `test_*.py` / `*_test.py` files | ✅ | ✅ | Rustest uses Rust for dramatically faster discovery |
|
|
472
|
-
| Test function detection (`test_*`) | ✅ | ✅ | |
|
|
473
|
-
| Test class detection (`Test*`) | ✅ | ✅ | via `unittest.TestCase` support |
|
|
474
|
-
| Pattern-based filtering | ✅ | ✅ | `-k` pattern matching |
|
|
475
|
-
| **Fixtures** |
|
|
476
|
-
| `@fixture` decorator | ✅ | ✅ | Rust-based dependency resolution |
|
|
477
|
-
| Fixture dependency injection | ✅ | ✅ | Much faster in rustest |
|
|
478
|
-
| Fixture scopes (function/class/module/session) | ✅ | ✅ | Full support for all scopes |
|
|
479
|
-
| Yield fixtures (setup/teardown) | ✅ | ✅ | Full support with cleanup |
|
|
480
|
-
| Fixture parametrization | ✅ | 🚧 | Planned |
|
|
481
|
-
| **Parametrization** |
|
|
482
|
-
| `@parametrize` decorator | ✅ | ✅ | Full support with custom IDs |
|
|
483
|
-
| Multiple parameter sets | ✅ | ✅ | |
|
|
484
|
-
| Parametrize with fixtures | ✅ | ✅ | |
|
|
485
|
-
| **Marks** |
|
|
486
|
-
| `@mark.skip` / `@skip` | ✅ | ✅ | Skip tests with reasons |
|
|
487
|
-
| Custom marks (`@mark.slow`, etc.) | ✅ | ✅ | Just added! |
|
|
488
|
-
| Mark with arguments | ✅ | ✅ | `@mark.timeout(30)` |
|
|
489
|
-
| Selecting tests by mark (`-m`) | ✅ | 🚧 | Mark metadata collected, filtering planned |
|
|
490
|
-
| **Test Execution** |
|
|
491
|
-
| Detailed assertion introspection | ✅ | ❌ | Uses standard Python assertions |
|
|
492
|
-
| Parallel execution | ✅ (`pytest-xdist`) | 🚧 | Planned (Rust makes this easier) |
|
|
493
|
-
| Test isolation | ✅ | ✅ | |
|
|
494
|
-
| Stdout/stderr capture | ✅ | ✅ | |
|
|
495
|
-
| **Reporting** |
|
|
496
|
-
| Pass/fail/skip summary | ✅ | ✅ | |
|
|
497
|
-
| Failure tracebacks | ✅ | ✅ | Full Python traceback support |
|
|
498
|
-
| Duration reporting | ✅ | ✅ | Per-test timing |
|
|
499
|
-
| JUnit XML output | ✅ | 🚧 | Planned |
|
|
500
|
-
| HTML reports | ✅ (`pytest-html`) | 🚧 | Planned |
|
|
501
|
-
| **Advanced Features** |
|
|
502
|
-
| Plugins | ✅ | ❌ | Not planned (keeps rustest simple) |
|
|
503
|
-
| Hooks | ✅ | ❌ | Not planned |
|
|
504
|
-
| Custom collectors | ✅ | ❌ | Not planned |
|
|
505
|
-
| `conftest.py` | ✅ | ✅ | Shared fixtures across test files |
|
|
506
|
-
| **Developer Experience** |
|
|
507
|
-
| Fully typed Python API | ⚠️ | ✅ | rustest uses `basedpyright` strict mode |
|
|
508
|
-
| Fast CI/CD runs | ⚠️ | ✅ | 78x faster = dramatically shorter feedback loops |
|
|
509
|
-
|
|
510
|
-
**Legend:**
|
|
511
|
-
- ✅ Fully supported
|
|
512
|
-
- 🚧 Planned or in progress
|
|
513
|
-
- ⚠️ Partial support
|
|
514
|
-
- ❌ Not planned
|
|
515
|
-
|
|
516
|
-
**Philosophy:** Rustest implements the 20% of pytest features that cover 80% of use cases, with a focus on raw speed and simplicity. If you need advanced pytest features like plugins or custom hooks, stick with pytest. If you want fast, straightforward testing with familiar syntax, rustest is for you.
|
|
517
|
-
|
|
518
|
-
## License
|
|
519
|
-
|
|
520
|
-
rustest is distributed under the terms of the MIT license. See [LICENSE](LICENSE).
|
|
521
|
-
|
rustest-0.2.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
rustest-0.2.0.dist-info/METADATA,sha256=zNdhsqtUfKsGrhvlI7csbjpmT6FFsb3T3kaE2a5Yx5c,16497
|
|
2
|
-
rustest-0.2.0.dist-info/WHEEL,sha256=73R9gF1Kx45mnq358tC7C3GKhsVvyIHwOBt6LvI35MM,106
|
|
3
|
-
rustest-0.2.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
|
|
4
|
-
rustest-0.2.0.dist-info/licenses/LICENSE,sha256=s64ibUGtb6jEDBsYuxUFtMr_c4PaqYP-vj3YY6QtTGw,1075
|
|
5
|
-
rustest/__init__.py,sha256=lXDAo5Ygm_sk-1Qu0M8kZnx3BR5AgSVDJWeb8IX2vYM,523
|
|
6
|
-
rustest/__main__.py,sha256=nqdz6DhrDze715SXxtzAYV2sie3CPoy7IvWCdcyHJEM,179
|
|
7
|
-
rustest/_approx.py,sha256=MKmuorBBHqpH0h0QaIMVjbm3-mXJ0E90limEgSHHVfw,5744
|
|
8
|
-
rustest/_cli.py,sha256=kq9LAwHaJmZ-gnAlTsz7Ov8r1fiDvNoLf4hEI3sxhng,8700
|
|
9
|
-
rustest/_decorators.py,sha256=nijzNG8NQXZd8kEfMjhSB-85gLoIjaIMxwZQNUNWgrE,11373
|
|
10
|
-
rustest/_reporting.py,sha256=6nVcccX1dgEBW72wCOeOIl5I-OE-ukjJD0VQs56pwjo,1626
|
|
11
|
-
rustest/_rust.cpython-313-darwin.so,sha256=htKOwWu0GWLOq_YHMlcMp32Qh2FQ9d_boi9Nm579y7Q,1540452
|
|
12
|
-
rustest/_rust.py,sha256=k3nXhGiehOVY_S6w28rIdrc0CEc3gFLgwWVOEMcPOZo,660
|
|
13
|
-
rustest/_rust.pyi,sha256=fDFLX0qj4G_bV1sHmTtRPI26grTDG_LFzPFEqp5vFGk,671
|
|
14
|
-
rustest/core.py,sha256=xmBUpuPs0r0HQthc9J5dCQYkZnXqfxqIfSGkHeoqQS4,488
|
|
15
|
-
rustest-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|