[PoC] Parallel PHPUnit runner in Go (~4x faster) + fix phpVersion leak between test classes - #8349
Merged
Conversation
TomasVotruba
marked this pull request as ready for review
August 14, 2026 09:49
…k between test classes Add utils-tests-runner/: a Go orchestrator that splits test classes into N balanced warm chunks (one PHP boot per chunk, not per class), giving ~4x over serial (38s -> ~9s full suite). Fix a latent order-dependent leak in AbstractRectorTestCase: phpVersion() was stored in static SimpleParameterProvider and never reset between classes, so a version-bound class leaked its version into the next class in the same process. Reset PHP_VERSION_FEATURES to the test default in tearDownAfterClass.
go build -o writes the exact name (no auto .exe), so the run step could not find the binary on Windows. Suffix both build and run consistently.
TomasVotruba
force-pushed
the
poc-fast-phpunit-go
branch
from
August 14, 2026 11:07
f0ad6ff to
dbcacc5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parallel PHPUnit runner in Go — measured ~4x local
Proof of concept: run the suite faster by splitting test classes across parallel workers that each boot PHP once and run many classes in a single process.
CI-measured (GitHub runners, 4 vCPU, full suite)
Same suite, serial
vendor/bin/phpunitvs the Go runner, measured on the run step of this PR:Full suite (685 classes / 5833 fixtures) runs and passes. A
fast_testsCI job (added here) runs the runner so these numbers are reproducible on every push.Local speed (24-core host, full suite)
vendor/bin/phpunit(1 process)fast-phpunit -p 8fast-phpunit -p 12fast-phpunit -p 24Speedup scales with cores; it flattens once the heaviest chunk bounds wall time.
Why it is faster
Bootstrap dominates a Rector test class, not the assertions:
Average class has ~7 fixtures, so bootstrap is ~56% of an average class's run time. Any runner that spawns a fresh process per class pays that 0.23s boot 685 times — which is why process-per-class parallelism is actually slower than serial (5.71s serial vs 8.55s
xargs -P8on the CodeQuality subset). This runner splits classes into N chunks balanced by fixture count, and each worker runs its whole chunk in one warm process, so the container is built N times, not 685 times.Isolation — required to make it correct
Two shared-state issues surface when many classes share a process; both handled so any chunking is safe.
Shared temp cache (cross-process). Rector caches parsed files in
sys_get_temp_dir()/rector_cached_files; parallel processes racing that dir throwFailed to open directory/Directory not empty. Each worker gets its own temp dir.Leaked
phpVersion()(in-process) — a latent bug, fixed here.phpVersion(...)is stored in the staticSimpleParameterProviderand was never reset between classes. A version-bound class leaked its version into the next class in the same process:The serial suite passes only because of its class ordering; any reshuffle — this tool or paratest — can trigger it. Fixed in
AbstractRectorTestCase::tearDownAfterClass()by resettingPHP_VERSION_FEATURESto the test default. Useful on its own, independent of the runner.Scope
utils-tests-runner/is a standalone Go helper that shells out tophp vendor/phpunit/phpunit/phpunit. It does not change how tests are written; it is invoked via one extra CI job. Pure Go, no.php, so it is invisible to ECS / PHPStan / Rector.phpVersionreset inAbstractRectorTestCase. ECS + PHPStan level 8 pass; full suite passes under-p 24locally and on CI.Open questions
phpVersionreset into its own PR and drop the tool?testsjob with the runner once trusted, or keep both for a while?