rustest 0.3.0__cp311-cp311-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 +160 -0
- rustest/cli.py +259 -0
- rustest/core.py +21 -0
- rustest/decorators.py +350 -0
- rustest/py.typed +0 -0
- rustest/reporting.py +63 -0
- rustest/rust.cp311-win_amd64.pyd +0 -0
- rustest/rust.py +23 -0
- rustest/rust.pyi +35 -0
- rustest-0.3.0.dist-info/METADATA +658 -0
- rustest-0.3.0.dist-info/RECORD +16 -0
- rustest-0.3.0.dist-info/WHEEL +4 -0
- rustest-0.3.0.dist-info/entry_points.txt +2 -0
- rustest-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rustest
|
|
3
|
+
Version: 0.3.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 Classes
|
|
445
|
+
|
|
446
|
+
Rustest supports pytest-style test classes, allowing you to organize related tests together:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
from rustest import fixture, parametrize, mark
|
|
450
|
+
|
|
451
|
+
class TestBasicMath:
|
|
452
|
+
"""Group related tests in a class."""
|
|
453
|
+
|
|
454
|
+
def test_addition(self):
|
|
455
|
+
assert 1 + 1 == 2
|
|
456
|
+
|
|
457
|
+
def test_subtraction(self):
|
|
458
|
+
assert 5 - 3 == 2
|
|
459
|
+
|
|
460
|
+
def test_multiplication(self):
|
|
461
|
+
assert 3 * 4 == 12
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Using Fixtures in Test Classes:**
|
|
465
|
+
|
|
466
|
+
Test methods can inject fixtures just like standalone test functions:
|
|
467
|
+
|
|
468
|
+
```python
|
|
469
|
+
from rustest import fixture
|
|
470
|
+
|
|
471
|
+
@fixture
|
|
472
|
+
def calculator():
|
|
473
|
+
return {"add": lambda x, y: x + y, "multiply": lambda x, y: x * y}
|
|
474
|
+
|
|
475
|
+
class TestCalculator:
|
|
476
|
+
"""Test class using fixtures."""
|
|
477
|
+
|
|
478
|
+
def test_addition(self, calculator):
|
|
479
|
+
assert calculator["add"](2, 3) == 5
|
|
480
|
+
|
|
481
|
+
def test_multiplication(self, calculator):
|
|
482
|
+
assert calculator["multiply"](4, 5) == 20
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
**Class-Scoped Fixtures:**
|
|
486
|
+
|
|
487
|
+
Class-scoped fixtures are shared across all test methods in the same class, perfect for expensive setup operations:
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
from rustest import fixture
|
|
491
|
+
|
|
492
|
+
@fixture(scope="class")
|
|
493
|
+
def database():
|
|
494
|
+
"""Expensive setup shared across all tests in a class."""
|
|
495
|
+
db = {"connection": "db://test", "data": []}
|
|
496
|
+
return db
|
|
497
|
+
|
|
498
|
+
class TestDatabase:
|
|
499
|
+
"""All tests share the same database fixture instance."""
|
|
500
|
+
|
|
501
|
+
def test_connection(self, database):
|
|
502
|
+
assert database["connection"] == "db://test"
|
|
503
|
+
|
|
504
|
+
def test_add_data(self, database):
|
|
505
|
+
database["data"].append("item1")
|
|
506
|
+
assert len(database["data"]) >= 1
|
|
507
|
+
|
|
508
|
+
def test_data_persists(self, database):
|
|
509
|
+
# Same database instance, so previous test's data is still there
|
|
510
|
+
assert len(database["data"]) >= 1
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Fixture Methods Within Test Classes:**
|
|
514
|
+
|
|
515
|
+
You can define fixtures as methods inside test classes, providing class-specific setup:
|
|
516
|
+
|
|
517
|
+
```python
|
|
518
|
+
from rustest import fixture
|
|
519
|
+
|
|
520
|
+
class TestWithFixtureMethod:
|
|
521
|
+
"""Test class with its own fixture methods."""
|
|
522
|
+
|
|
523
|
+
@fixture(scope="class")
|
|
524
|
+
def class_resource(self):
|
|
525
|
+
"""Fixture method shared across tests in this class."""
|
|
526
|
+
resource = {"value": 42, "name": "test_resource"}
|
|
527
|
+
yield resource
|
|
528
|
+
# Teardown happens after all tests in class
|
|
529
|
+
resource["closed"] = True
|
|
530
|
+
|
|
531
|
+
@fixture
|
|
532
|
+
def per_test_data(self, class_resource):
|
|
533
|
+
"""Fixture method that depends on another fixture."""
|
|
534
|
+
return {"id": id(self), "resource": class_resource}
|
|
535
|
+
|
|
536
|
+
def test_uses_class_resource(self, class_resource):
|
|
537
|
+
assert class_resource["value"] == 42
|
|
538
|
+
|
|
539
|
+
def test_uses_per_test_data(self, per_test_data):
|
|
540
|
+
assert "resource" in per_test_data
|
|
541
|
+
assert per_test_data["resource"]["value"] == 42
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
**Class Variables and Instance Variables:**
|
|
545
|
+
|
|
546
|
+
Test classes can use class variables for shared state and instance variables for per-test isolation:
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
class TestWithVariables:
|
|
550
|
+
"""Test class with class and instance variables."""
|
|
551
|
+
|
|
552
|
+
class_variable = "shared_data" # Shared across all tests
|
|
553
|
+
|
|
554
|
+
def test_class_variable(self):
|
|
555
|
+
# Access class variable
|
|
556
|
+
assert self.class_variable == "shared_data"
|
|
557
|
+
assert TestWithVariables.class_variable == "shared_data"
|
|
558
|
+
|
|
559
|
+
def test_instance_variable(self):
|
|
560
|
+
# Each test gets a fresh instance
|
|
561
|
+
self.instance_var = "test_specific"
|
|
562
|
+
assert self.instance_var == "test_specific"
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Parametrized Test Methods:**
|
|
566
|
+
|
|
567
|
+
Use `@parametrize` on class methods just like regular test functions:
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
from rustest import parametrize
|
|
571
|
+
|
|
572
|
+
class TestParametrized:
|
|
573
|
+
"""Test class with parametrized methods."""
|
|
574
|
+
|
|
575
|
+
@parametrize("value,expected", [(2, 4), (3, 9), (4, 16)])
|
|
576
|
+
def test_square(self, value, expected):
|
|
577
|
+
assert value ** 2 == expected
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### Test Output
|
|
581
|
+
|
|
582
|
+
When you run rustest, you'll see clean, informative output:
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
PASSED 0.001s test_simple_assertion
|
|
586
|
+
PASSED 0.002s test_string_operations
|
|
587
|
+
PASSED 0.001s test_database_query
|
|
588
|
+
PASSED 0.003s test_square[two]
|
|
589
|
+
PASSED 0.001s test_square[three]
|
|
590
|
+
PASSED 0.002s test_square[four]
|
|
591
|
+
SKIPPED 0.000s test_future_feature
|
|
592
|
+
FAILED 0.005s test_broken_feature
|
|
593
|
+
----------------------------------------
|
|
594
|
+
AssertionError: Expected 5, got 4
|
|
595
|
+
at test_example.py:42
|
|
596
|
+
|
|
597
|
+
8 tests: 6 passed, 1 failed, 1 skipped in 0.015s
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
## Feature Comparison with pytest
|
|
601
|
+
|
|
602
|
+
Rustest aims to provide the most commonly-used pytest features with dramatically better performance. Here's how the two compare:
|
|
603
|
+
|
|
604
|
+
| Feature | pytest | rustest | Notes |
|
|
605
|
+
|---------|--------|---------|-------|
|
|
606
|
+
| **Core Test Discovery** |
|
|
607
|
+
| `test_*.py` / `*_test.py` files | โ
| โ
| Rustest uses Rust for dramatically faster discovery |
|
|
608
|
+
| Test function detection (`test_*`) | โ
| โ
| |
|
|
609
|
+
| Test class detection (`Test*`) | โ
| โ
| Full pytest-style class support with fixture methods |
|
|
610
|
+
| Pattern-based filtering | โ
| โ
| `-k` pattern matching |
|
|
611
|
+
| **Fixtures** |
|
|
612
|
+
| `@fixture` decorator | โ
| โ
| Rust-based dependency resolution |
|
|
613
|
+
| Fixture dependency injection | โ
| โ
| Much faster in rustest |
|
|
614
|
+
| Fixture scopes (function/class/module/session) | โ
| โ
| Full support for all scopes |
|
|
615
|
+
| Yield fixtures (setup/teardown) | โ
| โ
| Full support with cleanup |
|
|
616
|
+
| Fixture methods within test classes | โ
| โ
| Define fixtures as class methods |
|
|
617
|
+
| Fixture parametrization | โ
| ๐ง | Planned |
|
|
618
|
+
| **Parametrization** |
|
|
619
|
+
| `@parametrize` decorator | โ
| โ
| Full support with custom IDs |
|
|
620
|
+
| Multiple parameter sets | โ
| โ
| |
|
|
621
|
+
| Parametrize with fixtures | โ
| โ
| |
|
|
622
|
+
| **Marks** |
|
|
623
|
+
| `@mark.skip` / `@skip` | โ
| โ
| Skip tests with reasons |
|
|
624
|
+
| Custom marks (`@mark.slow`, etc.) | โ
| โ
| Just added! |
|
|
625
|
+
| Mark with arguments | โ
| โ
| `@mark.timeout(30)` |
|
|
626
|
+
| Selecting tests by mark (`-m`) | โ
| ๐ง | Mark metadata collected, filtering planned |
|
|
627
|
+
| **Test Execution** |
|
|
628
|
+
| Detailed assertion introspection | โ
| โ | Uses standard Python assertions |
|
|
629
|
+
| Parallel execution | โ
(`pytest-xdist`) | ๐ง | Planned (Rust makes this easier) |
|
|
630
|
+
| Test isolation | โ
| โ
| |
|
|
631
|
+
| Stdout/stderr capture | โ
| โ
| |
|
|
632
|
+
| **Reporting** |
|
|
633
|
+
| Pass/fail/skip summary | โ
| โ
| |
|
|
634
|
+
| Failure tracebacks | โ
| โ
| Full Python traceback support |
|
|
635
|
+
| Duration reporting | โ
| โ
| Per-test timing |
|
|
636
|
+
| JUnit XML output | โ
| ๐ง | Planned |
|
|
637
|
+
| HTML reports | โ
(`pytest-html`) | ๐ง | Planned |
|
|
638
|
+
| **Advanced Features** |
|
|
639
|
+
| Plugins | โ
| โ | Not planned (keeps rustest simple) |
|
|
640
|
+
| Hooks | โ
| โ | Not planned |
|
|
641
|
+
| Custom collectors | โ
| โ | Not planned |
|
|
642
|
+
| `conftest.py` | โ
| โ
| Shared fixtures across test files |
|
|
643
|
+
| **Developer Experience** |
|
|
644
|
+
| Fully typed Python API | โ ๏ธ | โ
| rustest uses `basedpyright` strict mode |
|
|
645
|
+
| Fast CI/CD runs | โ ๏ธ | โ
| 78x faster = dramatically shorter feedback loops |
|
|
646
|
+
|
|
647
|
+
**Legend:**
|
|
648
|
+
- โ
Fully supported
|
|
649
|
+
- ๐ง Planned or in progress
|
|
650
|
+
- โ ๏ธ Partial support
|
|
651
|
+
- โ Not planned
|
|
652
|
+
|
|
653
|
+
**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.
|
|
654
|
+
|
|
655
|
+
## License
|
|
656
|
+
|
|
657
|
+
rustest is distributed under the terms of the MIT license. See [LICENSE](LICENSE).
|
|
658
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
rustest-0.3.0.dist-info/METADATA,sha256=esXlixx0RIYl37WTKb13SkHQ2HrtF7LJDM698ymGoo8,20976
|
|
2
|
+
rustest-0.3.0.dist-info/WHEEL,sha256=bNaa2-XeaoMXnkzV391Sm2NgCjpJ3A2VmfN6ZUnNTZA,96
|
|
3
|
+
rustest-0.3.0.dist-info/entry_points.txt,sha256=7fUa3LO8vudQ4dKG1sTRaDnxcMdBSZsWs9EyuxFQ7Lk,48
|
|
4
|
+
rustest-0.3.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=-y-NDR9ndxK21Rlunm-Z5I0hPn95SdZ6RqHRG25KNVI,8958
|
|
9
|
+
rustest/core.py,sha256=oMNUz_9oa3j3FmKn41ttZxcS3IaOjRglYEKo46zifms,506
|
|
10
|
+
rustest/decorators.py,sha256=21PNOMl9nZoAm8kUnms9_tkhX5gAGNvr7xYG8kwESts,11723
|
|
11
|
+
rustest/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
+
rustest/reporting.py,sha256=g4jdlwjFoKF_fWA_sKfeDwmhDeHxPJQUqypa_E2XQlc,1686
|
|
13
|
+
rustest/rust.cp311-win_amd64.pyd,sha256=kRwTphiGE2mF4hT7zZ_suOr9d03h92AHGfpu4DvGCWc,1303040
|
|
14
|
+
rustest/rust.py,sha256=N_1C-uXRiC2qkV7ecKVcb51-XXyfhYNepd5zs-RIYOo,682
|
|
15
|
+
rustest/rust.pyi,sha256=rV1Msn2dGUeQ_GuPSoKjkgcFaoEehXD_QtzCY4-77_M,741
|
|
16
|
+
rustest-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Apex Engineers Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|