Problem/Motivation
When running PHPUnit tests concurrently across multiple worker processes (for example, using https://github.com/Pronovix/testrunner, ParaTest, or other parallel test runners), test runs intermittently fail during bootstrap with a fatal ParseError:
Error in bootstrap script: ParseError: syntax error, unexpected string content "Passing an argument of type DO..." #0 core/tests/Drupal/TestTools/PhpUnitCompatibility/ClassWriter.php(36): Drupal\TestTools\PhpUnitCompatibility\ClassWriter::alterAssert() #1 core/tests/bootstrap.php(145): Drupal\TestTools\PhpUnitCompatibility\ClassWriter::mutateTestBase() #2 vendor/phpunit/phpunit/src/Util/FileLoader.php(66): include_once('...')
Root Cause:
\Drupal\TestTools\PhpUnitCompatibility\ClassWriter::flushAlteredCodeToFile() modifies PHPUnit\Framework\Assert and PHPUnit\Framework\TestCase to inject polyfill traits and writes the modified code to sites/simpletest/Assert.php and sites/simpletest/TestCase.php via file_put_contents() before including them.
Because file_put_contents() is not atomic, a race condition occurs when multiple parallel workers bootstrap simultaneously without pre-existing files:
- Worker 1 finds that the file does not exist and starts writing to
sites/simpletest/Assert.phpusingfile_put_contents()(which opens the file and begins streaming bytes). - Worker 2 executes concurrently, sees that
file_exists()is true, and attempts toincludethe file while Worker 1 has only partially written it. - Worker 2 fails with a fatal
ParseErroron truncated PHP code.
A previous related concurrency issue at this layer was resolved in #3221507: mkdir can fail in Drupal\TestTools\PhpUnitCompatibility\PhpUnit8::flushAlteredCodeToFile() because of a race condition (handling race conditions during directory creation), but the non-atomic file writing itself remains susceptible to race conditions.
Additional Context: In our environment, we use https://github.com/Pronovix/testrunner for running PHPUnit tests in parallel, but the same issue can occur with ParaTest and any other multi-process parallel PHPUnit runners sharing the codebase.
Steps to reproduce
- Ensure target mutated files do not exist:
rm -f sites/simpletest/Assert.php sites/simpletest/TestCase.php - Run parallel test workers (e.g., via
Pronovix/testrunner,paratest -c core --processes=8+, or concurrent CLI workers):
for i in {1..30}; do (php -r 'require "core/tests/bootstrap.php";' &); done; wait - Under sufficient concurrency or I/O load, observe intermittent
ParseError: syntax error, unexpected string content ...during bootstrap due to reading the partially-written file.
Proposed resolution
Suggested Fix:
Update ClassWriter::flushAlteredCodeToFile() to perform atomic file writes. Write the generated code to a unique temporary file in the target directory (e.g. using tempnam() with LOCK_EX) and replace the target destination atomically via rename().
$temp_path = tempnam($directory, 'class_writer_'); file_put_contents($temp_path, $altered_code, LOCK_EX); rename($temp_path, $full_path);
Temporary Workaround:
In CI pipelines or test runner scripts, pre-warm the mutated classes by running a single-threaded bootstrap before spawning parallel worker processes:
php -r 'require "core/tests/bootstrap.php";'Remaining tasks
- Create and submit a merge request / patch with the atomic write implementation.
- Review and verify concurrency safety.
User interface changes
None.
Introduced terminology
None.
API changes
None.
Data model changes
None.
Release notes snippet
Fixed a race condition in ClassWriter that caused intermittent ParseError failures during parallel PHPUnit test execution.
Issue fork drupal-3625260
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
- 3625260-
changes, plain diff MR !17220
- 3625260-classwriter-race-condition
compare
Comments
Comment #2
mxr576This is a long standing issue for us, but I only had time to investigate it and report it with an assistance of my coding agent.
Comment #3
mxr576Hm, so ClassWriter is gone since https://git.drupalcode.org/issue/drupal-3625260/-/commit/2d3e0bd3fb8f897...
Even better :) But Drupal 10 is still supported, so may worth exploring and fixing.
Comment #5
mxr576Comment #7
smustgrave commentedProbably need a test case showing the issue, even though we got like 2 months left of 10 ;)