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:

  1. Worker 1 finds that the file does not exist and starts writing to sites/simpletest/Assert.php using file_put_contents() (which opens the file and begins streaming bytes).
  2. Worker 2 executes concurrently, sees that file_exists() is true, and attempts to include the file while Worker 1 has only partially written it.
  3. Worker 2 fails with a fatal ParseError on 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

  1. Ensure target mutated files do not exist:
    rm -f sites/simpletest/Assert.php sites/simpletest/TestCase.php
  2. 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
  3. 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

Command icon 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:

Comments

mxr576 created an issue. See original summary.

mxr576’s picture

This 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.

mxr576’s picture

Version: main » 10.6.x-dev

Hm, 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.

mxr576 changed the visibility of the branch 3625260-classwriter-race-condition to hidden.

mxr576’s picture

Status: Active » Needs review

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

Probably need a test case showing the issue, even though we got like 2 months left of 10 ;)