rustest 0.8.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.
- rustest/__init__.py +28 -0
- rustest/__main__.py +10 -0
- rustest/approx.py +176 -0
- rustest/builtin_fixtures.py +309 -0
- rustest/cli.py +311 -0
- rustest/core.py +44 -0
- rustest/decorators.py +549 -0
- rustest/py.typed +0 -0
- rustest/reporting.py +63 -0
- rustest/rust.cp312-win_amd64.pyd +0 -0
- rustest/rust.py +23 -0
- rustest/rust.pyi +39 -0
- rustest-0.8.0.dist-info/METADATA +270 -0
- rustest-0.8.0.dist-info/RECORD +17 -0
- rustest-0.8.0.dist-info/WHEEL +4 -0
- rustest-0.8.0.dist-info/entry_points.txt +2 -0
- rustest-0.8.0.dist-info/licenses/LICENSE +21 -0
rustest/cli.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Command line interface helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from collections.abc import Sequence
|
|
7
|
+
|
|
8
|
+
from .reporting import RunReport, TestResult
|
|
9
|
+
from .core import run
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ANSI color codes
|
|
13
|
+
class _ColorsNamespace:
|
|
14
|
+
"""Namespace for ANSI color codes."""
|
|
15
|
+
|
|
16
|
+
def __init__(self) -> None: # pyright: ignore[reportMissingSuperCall]
|
|
17
|
+
self.green = "\033[92m"
|
|
18
|
+
self.red = "\033[91m"
|
|
19
|
+
self.yellow = "\033[93m"
|
|
20
|
+
self.cyan = "\033[96m"
|
|
21
|
+
self.bold = "\033[1m"
|
|
22
|
+
self.dim = "\033[2m"
|
|
23
|
+
self.reset = "\033[0m"
|
|
24
|
+
|
|
25
|
+
def disable(self) -> None:
|
|
26
|
+
"""Disable all colors."""
|
|
27
|
+
self.green = ""
|
|
28
|
+
self.red = ""
|
|
29
|
+
self.yellow = ""
|
|
30
|
+
self.cyan = ""
|
|
31
|
+
self.bold = ""
|
|
32
|
+
self.dim = ""
|
|
33
|
+
self.reset = ""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
Colors = _ColorsNamespace()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
40
|
+
parser = argparse.ArgumentParser(
|
|
41
|
+
prog="rustest",
|
|
42
|
+
description="Run Python tests at blazing speed with a Rust powered core.",
|
|
43
|
+
)
|
|
44
|
+
_ = parser.add_argument(
|
|
45
|
+
"paths",
|
|
46
|
+
nargs="*",
|
|
47
|
+
default=(".",),
|
|
48
|
+
help="Files or directories to collect tests from.",
|
|
49
|
+
)
|
|
50
|
+
_ = parser.add_argument(
|
|
51
|
+
"-k",
|
|
52
|
+
"--pattern",
|
|
53
|
+
help="Substring to filter tests by (case insensitive).",
|
|
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
|
+
)
|
|
61
|
+
_ = parser.add_argument(
|
|
62
|
+
"-n",
|
|
63
|
+
"--workers",
|
|
64
|
+
type=int,
|
|
65
|
+
help="Number of worker slots to use (experimental).",
|
|
66
|
+
)
|
|
67
|
+
_ = parser.add_argument(
|
|
68
|
+
"--no-capture",
|
|
69
|
+
dest="capture_output",
|
|
70
|
+
action="store_false",
|
|
71
|
+
help="Do not capture stdout/stderr during test execution.",
|
|
72
|
+
)
|
|
73
|
+
_ = parser.add_argument(
|
|
74
|
+
"-v",
|
|
75
|
+
"--verbose",
|
|
76
|
+
action="store_true",
|
|
77
|
+
help="Show verbose output with hierarchical test structure.",
|
|
78
|
+
)
|
|
79
|
+
_ = parser.add_argument(
|
|
80
|
+
"--ascii",
|
|
81
|
+
action="store_true",
|
|
82
|
+
help="Use ASCII characters instead of Unicode symbols for output.",
|
|
83
|
+
)
|
|
84
|
+
_ = parser.add_argument(
|
|
85
|
+
"--no-color",
|
|
86
|
+
dest="color",
|
|
87
|
+
action="store_false",
|
|
88
|
+
help="Disable colored output.",
|
|
89
|
+
)
|
|
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
|
+
)
|
|
125
|
+
return parser
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
129
|
+
parser = build_parser()
|
|
130
|
+
args = parser.parse_args(argv)
|
|
131
|
+
|
|
132
|
+
# Disable colors if requested
|
|
133
|
+
if not args.color:
|
|
134
|
+
Colors.disable()
|
|
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
|
+
|
|
144
|
+
report = run(
|
|
145
|
+
paths=list(args.paths),
|
|
146
|
+
pattern=args.pattern,
|
|
147
|
+
mark_expr=args.mark_expr,
|
|
148
|
+
workers=args.workers,
|
|
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,
|
|
153
|
+
)
|
|
154
|
+
_print_report(report, verbose=args.verbose, ascii_mode=args.ascii)
|
|
155
|
+
return 0 if report.failed == 0 else 1
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _print_report(report: RunReport, verbose: bool = False, ascii_mode: bool = False) -> None:
|
|
159
|
+
"""Print test report with configurable output format.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
report: The test run report
|
|
163
|
+
verbose: If True, show hierarchical verbose output (vitest-style)
|
|
164
|
+
ascii_mode: If True, use ASCII characters instead of Unicode symbols
|
|
165
|
+
"""
|
|
166
|
+
if verbose:
|
|
167
|
+
_print_verbose_report(report, ascii_mode)
|
|
168
|
+
else:
|
|
169
|
+
_print_default_report(report, ascii_mode)
|
|
170
|
+
|
|
171
|
+
# Print summary line with colors
|
|
172
|
+
passed_str = (
|
|
173
|
+
f"{Colors.green}{report.passed} passed{Colors.reset}"
|
|
174
|
+
if report.passed > 0
|
|
175
|
+
else f"{report.passed} passed"
|
|
176
|
+
)
|
|
177
|
+
failed_str = (
|
|
178
|
+
f"{Colors.red}{report.failed} failed{Colors.reset}"
|
|
179
|
+
if report.failed > 0
|
|
180
|
+
else f"{report.failed} failed"
|
|
181
|
+
)
|
|
182
|
+
skipped_str = (
|
|
183
|
+
f"{Colors.yellow}{report.skipped} skipped{Colors.reset}"
|
|
184
|
+
if report.skipped > 0
|
|
185
|
+
else f"{report.skipped} skipped"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
summary = (
|
|
189
|
+
f"\n{Colors.bold}{report.total} tests:{Colors.reset} "
|
|
190
|
+
f"{passed_str}, "
|
|
191
|
+
f"{failed_str}, "
|
|
192
|
+
f"{skipped_str} in {Colors.dim}{report.duration:.3f}s{Colors.reset}"
|
|
193
|
+
)
|
|
194
|
+
print(summary)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _print_default_report(report: RunReport, ascii_mode: bool) -> None:
|
|
198
|
+
"""Print pytest-style progress indicators followed by failure details."""
|
|
199
|
+
# Define symbols
|
|
200
|
+
if ascii_mode:
|
|
201
|
+
# pytest-style: . (pass), F (fail), s (skip)
|
|
202
|
+
pass_symbol = "."
|
|
203
|
+
fail_symbol = "F"
|
|
204
|
+
skip_symbol = "s"
|
|
205
|
+
else:
|
|
206
|
+
# Unicode symbols (no spaces, with colors)
|
|
207
|
+
pass_symbol = f"{Colors.green}✓{Colors.reset}"
|
|
208
|
+
fail_symbol = f"{Colors.red}✗{Colors.reset}"
|
|
209
|
+
skip_symbol = f"{Colors.yellow}⊘{Colors.reset}"
|
|
210
|
+
|
|
211
|
+
# Print progress indicators
|
|
212
|
+
for result in report.results:
|
|
213
|
+
if result.status == "passed":
|
|
214
|
+
print(pass_symbol, end="")
|
|
215
|
+
elif result.status == "failed":
|
|
216
|
+
print(fail_symbol, end="")
|
|
217
|
+
elif result.status == "skipped":
|
|
218
|
+
print(skip_symbol, end="")
|
|
219
|
+
print() # Newline after progress indicators
|
|
220
|
+
|
|
221
|
+
# Print failure details
|
|
222
|
+
failures = [r for r in report.results if r.status == "failed"]
|
|
223
|
+
if failures:
|
|
224
|
+
print(f"\n{Colors.red}{'=' * 70}")
|
|
225
|
+
print(f"{Colors.bold}FAILURES{Colors.reset}")
|
|
226
|
+
print(f"{Colors.red}{'=' * 70}{Colors.reset}")
|
|
227
|
+
for result in failures:
|
|
228
|
+
print(
|
|
229
|
+
f"\n{Colors.bold}{result.name}{Colors.reset} ({Colors.cyan}{result.path}{Colors.reset})"
|
|
230
|
+
)
|
|
231
|
+
print(f"{Colors.red}{'-' * 70}{Colors.reset}")
|
|
232
|
+
if result.message:
|
|
233
|
+
print(result.message.rstrip())
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _print_verbose_report(report: RunReport, ascii_mode: bool) -> None:
|
|
237
|
+
"""Print vitest-style hierarchical output with nesting and timing."""
|
|
238
|
+
# Define symbols
|
|
239
|
+
if ascii_mode:
|
|
240
|
+
pass_symbol = "PASS"
|
|
241
|
+
fail_symbol = "FAIL"
|
|
242
|
+
skip_symbol = "SKIP"
|
|
243
|
+
else:
|
|
244
|
+
pass_symbol = f"{Colors.green}✓{Colors.reset}"
|
|
245
|
+
fail_symbol = f"{Colors.red}✗{Colors.reset}"
|
|
246
|
+
skip_symbol = f"{Colors.yellow}⊘{Colors.reset}"
|
|
247
|
+
|
|
248
|
+
# Group tests by file path and organize hierarchically
|
|
249
|
+
from collections import defaultdict
|
|
250
|
+
|
|
251
|
+
tests_by_file: dict[str, list[TestResult]] = defaultdict(list)
|
|
252
|
+
for result in report.results:
|
|
253
|
+
tests_by_file[result.path].append(result)
|
|
254
|
+
|
|
255
|
+
# Print hierarchical structure
|
|
256
|
+
for file_path in sorted(tests_by_file.keys()):
|
|
257
|
+
print(f"\n{Colors.bold}{file_path}{Colors.reset}")
|
|
258
|
+
|
|
259
|
+
# Group tests by class within this file
|
|
260
|
+
tests_by_class: dict[str | None, list[tuple[TestResult, str | None]]] = defaultdict(list)
|
|
261
|
+
for result in tests_by_file[file_path]:
|
|
262
|
+
# Parse test name to extract class if present
|
|
263
|
+
# Format can be: "test_name" or "ClassName.test_name" or "module::Class::test"
|
|
264
|
+
class_name: str | None
|
|
265
|
+
if "::" in result.name:
|
|
266
|
+
parts = result.name.split("::")
|
|
267
|
+
class_name = "::".join(parts[:-1]) if len(parts) > 1 else None
|
|
268
|
+
elif "." in result.name:
|
|
269
|
+
parts = result.name.split(".")
|
|
270
|
+
class_name = parts[0] if len(parts) > 1 else None
|
|
271
|
+
else:
|
|
272
|
+
class_name = None
|
|
273
|
+
tests_by_class[class_name].append((result, class_name))
|
|
274
|
+
|
|
275
|
+
# Print tests organized by class
|
|
276
|
+
for class_name in sorted(tests_by_class.keys(), key=lambda x: (x is None, x)):
|
|
277
|
+
# Print class name if present
|
|
278
|
+
if class_name:
|
|
279
|
+
print(f" {Colors.cyan}{class_name}{Colors.reset}")
|
|
280
|
+
|
|
281
|
+
for result, _ in tests_by_class[class_name]:
|
|
282
|
+
# Get symbol based on status
|
|
283
|
+
if result.status == "passed":
|
|
284
|
+
symbol = pass_symbol
|
|
285
|
+
elif result.status == "failed":
|
|
286
|
+
symbol = fail_symbol
|
|
287
|
+
elif result.status == "skipped":
|
|
288
|
+
symbol = skip_symbol
|
|
289
|
+
else:
|
|
290
|
+
symbol = "?"
|
|
291
|
+
|
|
292
|
+
# Extract just the test method name
|
|
293
|
+
if "::" in result.name:
|
|
294
|
+
display_name = result.name.split("::")[-1]
|
|
295
|
+
elif "." in result.name:
|
|
296
|
+
display_name = result.name.split(".")[-1]
|
|
297
|
+
else:
|
|
298
|
+
display_name = result.name
|
|
299
|
+
|
|
300
|
+
# Indent based on whether it's in a class
|
|
301
|
+
indent = " " if class_name else " "
|
|
302
|
+
|
|
303
|
+
# Print with symbol, name, and timing
|
|
304
|
+
duration_str = f"{Colors.dim}{result.duration * 1000:.0f}ms{Colors.reset}"
|
|
305
|
+
print(f"{indent}{symbol} {display_name} {duration_str}")
|
|
306
|
+
|
|
307
|
+
# Show error message for failures
|
|
308
|
+
if result.status == "failed" and result.message:
|
|
309
|
+
error_lines = result.message.rstrip().split("\n")
|
|
310
|
+
for line in error_lines:
|
|
311
|
+
print(f"{indent} {line}")
|
rustest/core.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""High level Python API wrapping the Rust extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from . import rust
|
|
8
|
+
from .reporting import RunReport
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run(
|
|
12
|
+
*,
|
|
13
|
+
paths: Sequence[str],
|
|
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,
|
|
21
|
+
) -> RunReport:
|
|
22
|
+
"""Execute tests and return a rich report.
|
|
23
|
+
|
|
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
|
+
)
|
|
44
|
+
return RunReport.from_py(raw_report)
|