This document describes the property-based testing and load/stress testing infrastructure added to the Rhiza project.
Rhiza now includes two additional types of testing:
- Property-Based Testing (using Hypothesis) - Tests that verify properties hold across a wide range of generated inputs
- Load/Stress Testing (using pytest-benchmark) - Tests that measure performance and verify stability under load
The test file .rhiza/tests/sync/test_readme_validation.py automatically executes every python
code block in README.md and checks that its output matches the adjacent ```result block. It
also syntax-checks every bash block using bash -n.
To exclude a specific code block from being executed or syntax-checked, append +RHIZA_SKIP to
the opening fence line:
```python +RHIZA_SKIP
# This block will NOT be executed or syntax-checked by the readme tests.
# Use it for illustrative examples, environment-specific code, or incomplete snippets.
from my_env import some_function
some_function()
```
```bash +RHIZA_SKIP
# This bash block will NOT be syntax-checked.
run-something --only-on-ci
```Markdown renderers (including GitHub) ignore everything after the first word on a fence line, so
the block still renders as a normal syntax-highlighted code block. All blocks that do not carry
+RHIZA_SKIP continue to be validated as before.
Property-based tests use the Hypothesis library to automatically generate test cases that verify certain properties always hold true.
In a standard Rhiza project, there are two relevant locations for property-based tests:
- Project property-based tests live in
tests/property/. These are part of your normal test suite and are discovered bypytestviapytest.ini:testpaths = tests. - Rhiza's own template/internal property-based tests (if present) live in
.rhiza/tests/property/. These are not part of your project's main test suite by default.
In a typical setup, the Make targets map to these suites as follows:
make test: runspytestover everything undertests/(includingtests/property/).make rhiza-test: runs Rhiza's internal tests under.rhiza/tests/(including.rhiza/tests/property/if any exist).
You can also invoke the corresponding pytest commands directly:
# Run all project property-based tests (what make test covers)
uv run pytest tests/property/ -v
# Run Rhiza's internal/template property-based tests (if you have any in .rhiza)
uv run pytest .rhiza/tests/property/ -v
# Run project property-based tests with more examples (increase coverage)
uv run pytest tests/property/ -v --hypothesis-max-examples=1000
# Run project property-based tests with verbose Hypothesis output
uv run pytest tests/property/ -v --hypothesis-verbosity=verboseBy default, the template disables live pytest CLI logging (log_cli = false) to keep normal test output concise.
When you need detailed live logs for debugging, enable them per-run:
uv run pytest -o log_cli=true --log-cli-level=DEBUGThe following property-based tests are included as examples:
- test_sort_correctness_using_properties: Verifies that sorted() correctly orders lists and preserves all elements including duplicates
Load and stress tests use pytest-benchmark to measure performance and verify system stability under load.
Benchmark and stress tests are located in tests/benchmarks/ (if present)
# Run all benchmarks
make benchmark
# Or with pytest directly
uv run pytest tests/benchmarks/ -v
# Run benchmarks and generate histogram
uv run pytest tests/benchmarks/ --benchmark-histogram=_tests/benchmarks/histogram
# Run benchmarks and save results
uv run pytest tests/benchmarks/ --benchmark-json=_tests/benchmarks/results.json
# Skip benchmarks (for CI)
uv run pytest tests/benchmarks/ --benchmark-skip
# Run only stress tests (note: these don't run with make benchmark by default)
uv run pytest tests/benchmarks/ -m stress -v
# Skip stress tests (run only performance benchmarks)
uv run pytest tests/benchmarks/ -m "not stress" -vNote: The make benchmark target runs with --benchmark-only, which means stress tests (that don't use the benchmark fixture) will be skipped. To run stress tests explicitly, use uv run pytest tests/benchmarks/ -m stress -v.
Tests that measure the performance of common Makefile operations:
test_help_target_performance- Measures help target execution timetest_print_variable_performance- Measures variable printing performancetest_dry_run_install_performance- Measures dry-run install performancetest_makefile_parsing_overhead- Measures Makefile parsing overhead
Tests that benchmark file system operations:
test_directory_traversal_performance- Measures directory traversal speedtest_file_reading_performance- Measures file reading performancetest_multiple_file_checks_performance- Measures file existence checking
Tests that measure subprocess creation overhead:
test_subprocess_creation_overhead- Measures subprocess creation timetest_git_command_performance- Measures git command execution time
Tests that verify stability under load (marked with @pytest.mark.stress):
test_repeated_help_invocations- Stress tests repeated help invocations (100 iterations)test_concurrent_print_variable_stress- Tests concurrent Makefile invocations (deterministic)test_file_system_stress- Tests rapid file creation/deletion (100 iterations)
Note: Stress tests can be slow and are marked with the stress marker. They don't use the benchmark fixture, so they won't run with make benchmark (which uses --benchmark-only). Use uv run pytest tests/benchmarks/ -m stress -v to run them explicitly.
Benchmark output includes:
- Min/Max: Minimum and maximum execution times
- Mean: Average execution time
- StdDev: Standard deviation (consistency)
- Median: Median execution time
- IQR: Interquartile range
- Outliers: Number of outlier measurements
- OPS: Operations per second (1/Mean)
Example output:
--------------------------------------------------- benchmark: 1 tests ---------------------------------------------------
Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations
--------------------------------------------------------------------------------------------------------------------------
test_help_target_performance 16.5255 18.0592 16.9294 0.3194 16.8354 0.4791 15;1 59.0689 55 1
--------------------------------------------------------------------------------------------------------------------------
The property-based tests run as part of the regular test suite:
# Run all tests including property-based tests
make testBenchmarks can be run separately or as part of validation:
# Run benchmarks
make benchmark- Focus on invariants: Test properties that should always hold true
- Use appropriate strategies: Choose Hypothesis strategies that generate realistic inputs
- Keep tests fast: Property tests run multiple times, so keep them quick
- Test edge cases: Use
@exampledecorator to test specific known edge cases
Example:
from hypothesis import given, strategies as st, example
@given(version=st.from_regex(r"^\d+\.\d+\.\d+$", fullmatch=True))
@example(version="0.0.0") # Test specific edge case
def test_version_parsing(version):
parts = version.split(".")
assert len(parts) == 3
assert all(p.isdigit() for p in parts)- Benchmark real operations: Test actual operations users will perform
- Use fixtures wisely: Use module or session-scoped fixtures for expensive setup
- Test multiple scenarios: Benchmark best case, average case, and worst case
- Monitor trends: Track benchmark results over time to detect regressions
Example:
def test_operation_performance(benchmark):
def run_operation():
# Operation to benchmark
return perform_operation()
result = benchmark(run_operation)
assert result is not None- Test realistic scenarios: Simulate real-world usage patterns
- Set reasonable thresholds: Allow small failure rates for resource contention
- Test concurrency: Use ThreadPoolExecutor or ProcessPoolExecutor for concurrent tests
- Monitor resource usage: Consider memory, CPU, and I/O in addition to time
Example:
def test_concurrent_operations(root):
import concurrent.futures
def operation():
# Operation to stress test
return perform_operation()
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
futures = [executor.submit(operation) for _ in range(100)]
results = [f.result() for f in concurrent.futures.as_completed(futures)]
success_rate = sum(results) / len(results)
assert success_rate == 1.0 # Rhiza template stress tests require 100% successThe following dependencies are required for property-based and load/stress testing:
# Property-based testing
hypothesis>=6.150.0
# Benchmarking and performance testing
pytest-benchmark>=5.2.3
pygal>=3.1.0
These are provisioned on the fly by the relevant make targets via uv run --with … (e.g. make test, make benchmark), so no separate install step is required.
If Hypothesis tests hang, you can:
- Reduce the number of examples:
pytest --hypothesis-max-examples=10 - Set a deadline:
pytest --hypothesis-deadline=1000 - Use the CI profile:
pytest --hypothesis-profile=ci
If benchmark results have high variance:
- Close other applications to reduce system load
- Increase the number of rounds:
pytest --benchmark-min-rounds=10 - Run on a consistent environment (CI is preferred for accurate benchmarks)
If stress tests fail occasionally:
- Check system resources (memory, CPU)
- Increase acceptable failure rate if resource contention is expected
- Reduce iteration count for local development