Asynkron.TestRunner
A .NET global tool that wraps dotnet test, captures TRX results, tracks test history, and displays regression charts.
Features
- Wraps
dotnet test and streams output in real-time
- Automatically captures TRX test results
- Tracks test history per project and command signature
- Shows pass/fail bar charts across runs
- Detects regressions (tests that passed before but now fail)
- Detects fixes (tests that failed before but now pass)
- Supports color and non-color (CI/piped) output modes
- Default 20s per-test timeout (detects and reports hung tests)
- Automatic hang isolation - when a hang is detected, automatically drills down to find the culprit
- Displays test hierarchy tree using Spectre.Console for better visualization
- Easy filter syntax for running specific tests
Installation
dotnet tool install -g Asynkron.TestRunner
Usage
Run all tests
testrunner # Runs: dotnet test
Run filtered tests
testrunner "MyClass" # Runs: dotnet test --filter "FullyQualifiedName~MyClass"
testrunner "Namespace.Test" # Matches any test containing that pattern
Custom command
testrunner -- dotnet test ./tests/MyTests
testrunner -- dotnet test --filter "Category=Unit"
List tests (without running)
testrunner list # List all tests
testrunner list "Storage" # List tests matching 'Storage'
Options
testrunner --timeout 60 "SlowTests" # 60s hang timeout (default: 20s)
testrunner --timeout 0 # Disable hang detection
testrunner --help # Show help
View history
testrunner stats # Default command history
testrunner stats -- dotnet test ./tests/MyTests # Specific command history
testrunner stats --history 5 # Last 5 runs
Automatic hang isolation
When a test hang is detected during a normal run, testrunner automatically isolates and finds the hanging test:
testrunner "MyTests" # If hang detected, auto-isolates
The isolation process:
- Displays a tree of all matching tests
- Breaks the tree into branches with fewer than 100 leaf tests (or the full tree if smaller)
- Runs each branch separately with the hang timeout and reports failing or hanging branches
Manual isolate command
You can also manually trigger isolation:
testrunner isolate # Find hanging test in all tests
testrunner isolate "language" # Find within tests matching 'language'
testrunner isolate --timeout 60 # Use 60s timeout (default: 30s)
testrunner isolate --parallel 4 # Run up to 4 batches concurrently
testrunner isolate -p # Use all CPU cores for parallel batches
Parallel Isolation
For large test suites, parallel isolation can significantly speed up the process:
testrunner isolate -p 4 "Tests" # Run 4 test batches concurrently
testrunner isolate --parallel # Auto-detect parallelism (CPU count)
Parallel mode:
- Runs independent test batches concurrently
- Uses a semaphore to limit concurrent processes
- Thread-safe console output with progress indicators
- Falls back to sequential drilling when hanging tests are found
View regressions
testrunner regressions # Compare last 2 runs
Clear history
testrunner clear
Example Output
After a test run with regressions:
FAILED
──────────────────────────────────────────────────
Passed: 132
Failed: 4
Skipped: 0
Total: 136
Duration: 2.3s
Pass Rate: 97.1%
Regressions (3):
✗ MyTests.SomeTest.ThatUsedToPass
✗ MyTests.AnotherTest.NowFailing
✗ MyTests.ThirdTest.Broken
History chart:
Test History (4 runs)
──────────────────────────────────────────────────────────────────────
2025-12-30 10:04 █████████████████████████████X 135/136 (99.3%) ✗1
2025-12-30 10:03 █████████████████████████████X 132/136 (97.1%) ✗4
2025-12-30 10:00 █████████████████████████████X 135/136 (99.3%) ✗1
2025-12-30 10:00 █████████████████████████████X 135/136 (99.3%) ✗1
Configuration Options
Timeout Options
| Option | Description |
|---|
-t, --timeout <seconds> | Per-test hang timeout (default: 20s for run, 30s for isolate) |
--timeout 0 | Disable hang detection entirely |
Isolate Options
| Option | Description |
|---|
-t, --timeout <seconds> | Per-test timeout during isolation (default: 30s) |
-p, --parallel [N] | Run N batches in parallel (default: 1, or CPU count if no N) |
Filter Patterns
Filters use FullyQualifiedName~<pattern> matching: