Skip to content

Commit 3afd227

Browse files
committed
Add free-threading safety tests and CI integration
- Introduced `test-freethreaded` command for thread safety testing with `pytest-freethreaded`. - Added `ci-checks-ft` for comprehensive CI checks including free-threading tests. - Updated dependencies to include `pytest-freethreaded` and `pytest-timeout`. - Enhanced documentation with guidelines for thread safety testing and Python 3.14t usage. - Updated workflows to run free-threading safety checks during CI.
1 parent 818eef7 commit 3afd227

6 files changed

Lines changed: 165 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ jobs:
1818
run: |
1919
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
2020
21-
- name: Run CI checks
22-
run: just ci-checks
21+
- name: Run CI checks (including free-threading safety)
22+
run: just ci-checks-ft

Justfile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ test *ARGS:
2626
test-parallel *ARGS:
2727
uv run pytest -n auto {{ ARGS }}
2828

29+
# Run tests with free-threading safety checks (parallel threads + iterations)
30+
test-freethreaded *ARGS:
31+
uv run pytest --threads=8 --iterations=10 --require-gil-disabled {{ ARGS }}
32+
2933
# Lint code (check for issues)
3034
lint *ARGS:
3135
uv run ruff check {{ ARGS }} .
@@ -63,6 +67,10 @@ clean:
6367
ci-checks:
6468
just install && just lint && just fmt-check && just typecheck && just test-parallel
6569

70+
# Run all checks + free-threading safety tests
71+
ci-checks-ft:
72+
just ci-checks && just test-freethreaded
73+
6674
# Enable pre-push hook to run ci-checks before pushing
6775
enable-pre-push:
6876
@echo "Installing pre-push hook..."

docs/benchmark.md

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -410,17 +410,53 @@ With Python 3.14's free-threaded build (no GIL):
410410

411411
### Running with Free-Threaded Python
412412

413-
To test with Python 3.14's free-threaded build:
413+
aria-testing uses Python 3.14t (free-threaded build) by default and includes specialized testing to detect thread safety issues.
414+
415+
#### Standard Testing
416+
417+
```bash
418+
# Regular parallel tests (pytest-xdist)
419+
just test-parallel
420+
421+
# All quality checks (lint, format, typecheck, tests)
422+
just ci-checks
423+
```
424+
425+
#### Free-Threading Safety Testing
426+
427+
Uses `pytest-freethreaded` to run tests multiple times across multiple threads simultaneously:
428+
429+
```bash
430+
# Run tests with thread safety detection
431+
just test-freethreaded
432+
433+
# This runs: pytest --threads=8 --iterations=10 --require-gil-disabled
434+
# - 8 threads running tests in parallel
435+
# - 10 iterations of each test
436+
# - Requires GIL to be disabled (fails if not using 3.14t)
437+
```
438+
439+
**What This Detects:**
440+
- Race conditions from concurrent access
441+
- Deadlocks and hangs (via timeouts)
442+
- Issues with global mutable state
443+
- Non-deterministic behavior
444+
445+
**Timeouts Configured:**
446+
- `timeout = 60` - Test timeout (detects hangs)
447+
- `faulthandler_timeout = 120` - Dump stack traces on timeout
448+
449+
#### Manual Free-Threading Testing
414450

415451
```bash
416-
# Install free-threaded Python 3.14
417-
# (Build with --disable-gil or use python3.14t)
452+
# Verify Python is free-threaded
453+
python -c "import sys; print(f'Free-threaded: {not sys._is_gil_enabled()}')"
418454

419-
# Run tests with free-threading enabled
420-
PYTHON_GIL=0 pytest -n auto
455+
# Run with custom thread/iteration counts
456+
pytest --threads=16 --iterations=50 --require-gil-disabled tests/test_concurrency.py
421457

422-
# Verify no race conditions with longer runs
423-
PYTHON_GIL=0 pytest -n auto --count=100
458+
# Run specific stress test
459+
pytest tests/test_concurrency.py::TestThreadSafetyStress -v
424460
```
425461

426462
### Thread-Safety Guarantees

docs/contributing.md

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Guide for developers contributing to aria-testing.
66

77
### Prerequisites
88

9-
- Python 3.14+
9+
- Python 3.14t (free-threaded build)
1010
- [uv](https://docs.astral.sh/uv/) package manager
1111
- [just](https://just.systems/) command runner
1212

@@ -45,11 +45,13 @@ just fmt-check # Check formatting without changes
4545
just typecheck # Run type checking
4646

4747
# Testing
48-
just test # Run tests (sequential)
49-
just test-parallel # Run tests (parallel)
48+
just test # Run tests (sequential)
49+
just test-parallel # Run tests (parallel with pytest-xdist)
50+
just test-freethreaded # Run thread safety tests (8 threads x 10 iterations)
5051

5152
# Quality checks
5253
just ci-checks # Run all checks (lint, format, typecheck, tests)
54+
just ci-checks-ft # All checks + free-threading safety tests
5355

5456
# Documentation
5557
just docs # Build documentation
@@ -283,6 +285,80 @@ just test-parallel
283285
just test --cov
284286
```
285287

288+
### Thread Safety Testing
289+
290+
aria-testing is designed for Python 3.14's free-threaded mode (no GIL). All code must be thread-safe.
291+
292+
#### Testing for Thread Safety
293+
294+
Use `pytest-freethreaded` to detect race conditions and threading issues:
295+
296+
```bash
297+
# Run thread safety tests (8 threads, 10 iterations)
298+
just test-freethreaded
299+
300+
# Custom thread/iteration counts
301+
pytest --threads=16 --iterations=50 --require-gil-disabled tests/
302+
303+
# Test specific concurrency tests
304+
pytest tests/test_concurrency.py -v
305+
```
306+
307+
#### What Gets Tested
308+
309+
The `--threads` and `--iterations` options:
310+
- Run each test multiple times (`--iterations=10`)
311+
- Run tests concurrently across threads (`--threads=8`)
312+
- Expose race conditions, deadlocks, and non-deterministic behavior
313+
314+
Example:
315+
```bash
316+
# This test will run 80 times total (8 threads × 10 iterations)
317+
pytest tests/test_queries.py::test_get_by_role --threads=8 --iterations=10
318+
```
319+
320+
#### Writing Thread-Safe Tests
321+
322+
**✅ Good - No shared mutable state:**
323+
```python
324+
def test_concurrent_queries():
325+
# Each test creates its own container - thread-safe
326+
doc = html(t'<div><button>Click</button></div>')
327+
button = get_by_role(doc, "button")
328+
assert button.tag == "button"
329+
```
330+
331+
**⚠️ Careful - Shared containers are OK if read-only:**
332+
```python
333+
# Module-level container (created once)
334+
SAMPLE_DOC = html(t'<div><button>Click</button></div>')
335+
336+
def test_read_only_access():
337+
# Read-only access to shared container - thread-safe
338+
button = get_by_role(SAMPLE_DOC, "button")
339+
assert button.tag == "button"
340+
```
341+
342+
**❌ Bad - Shared mutable state:**
343+
```python
344+
# Module-level mutable list - NOT thread-safe!
345+
results = []
346+
347+
def test_with_shared_state():
348+
# Multiple threads modifying same list - race condition!
349+
element = get_by_role(doc, "button")
350+
results.append(element) # ❌ NOT THREAD-SAFE
351+
```
352+
353+
#### Thread Safety Guidelines
354+
355+
1. **No global mutable state** - Use function-local variables
356+
2. **Immutable data structures** - Use `MappingProxyType`, tuples, frozensets
357+
3. **No caching without locks** - Caching creates shared mutable state
358+
4. **Document thread-safety** - Mark functions as thread-safe in docstrings
359+
360+
See `tests/test_concurrency.py` for examples of proper thread-safe testing.
361+
286362
## Adding New Features
287363

288364
### Adding a New Query Type

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Homepage = "https://github.com/t-strings/aria-testing"
2121
testpaths = ["tests"]
2222
python_files = ["test_*.py"]
2323
addopts = "-p no:doctest"
24+
# Free-threading safety: timeout tests to detect hangs/deadlocks
25+
timeout = 60
26+
faulthandler_timeout = 120
2427

2528
[build-system]
2629
requires = ["uv_build>=0.8.17,<0.9.0"]
@@ -37,4 +40,6 @@ dev = [
3740
"sphinx-autobuild>=2024.9.3",
3841
"ty>=0.0.1a25",
3942
"coverage>=7.11.2",
43+
"pytest-freethreaded>=0.1.0",
44+
"pytest-timeout>=2.4.0",
4045
]

uv.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)