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/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)