Skip to content

wpmedia-phpunit binary breaks @runInSeparateProcess testsΒ #51

Description

@remyperona

πŸ€– AI-generated β€” created with Claude Code, reviewed by @Tabrisrp. Discovered while migrating wp-media/mcp-oauth to wp-media/phpunit v3.2.

Summary

The wpmedia-phpunit binary is incompatible with PHPUnit's @runInSeparateProcess isolation: all @runInSeparateProcess tests fail when run through the binary, while identical tests pass under a direct vendor/bin/phpunit invocation with the same arguments.

Steps to reproduce / Evidence

Reproduction in a consumer plugin (wp-media/mcp-oauth):

  • A test class with 9 @runInSeparateProcess + @preserveGlobalState disabled data sets
    • Via composer test-unit (runs wpmedia-phpunit unit): 0/9 pass (Test was run in child process and ended unexpectedly)
    • Via direct vendor/bin/phpunit --testsuite unit --colors=always --configuration Tests/Unit/phpunit.xml.dist: 9/9 pass
    • Same effective arguments; identical test code

PHPUnit instrumentation evidence:

  • Instrumenting vendor/phpunit/phpunit/src/Util/PHP/Template/TestCaseMethod.tpl with counters showed the isolated child dies inside the {included_files} block: the included-file count grows from 58 β†’ 142 in the passing (direct) run, whereas the binary-run child never survives that block to reach the configured bootstrap require.

Root cause

The binary (wpmedia-phpunit at repo root) and src/BootstrapManager.php bootstrap PHPUnit by require-ing vendor/bin/phpunit into the currently running PHP process (see BootstrapManager::runTestSuite(), the require_once "{$root}/bin/phpunit" line).

The failure chain:

  1. wpmedia-phpunit and src/BootstrapManager.php are added to get_included_files() when loaded into the parent process.
  2. For @runInSeparateProcess tests, PHPUnit serializes the parent's included-files list into each child and re-requires them (PHPUnit\Util\GlobalState::getIncludedFilesAsString()).
  3. The child re-requires wpmedia-phpunit.
  4. Because that script begins with a #!/usr/bin/env php shebang and, when included, self-executes (it calls BootstrapManager::setupConstants() and BootstrapManager::runTestSuite() at the top level), re-including it in the child re-runs the whole bootstrap manager and emits the shebang line to the child's STDOUT.
  5. That corrupts the serialized child result the parent reads back β†’ Test was run in child process and ended unexpectedly.
  6. Every isolated data set fails.

A direct vendor/bin/phpunit run passes because its included files are all inert library files β€” harmless to re-require.

Proposed fix

Two options; the second is recommended as the robust, long-term solution.

Option 1 β€” Minimal / targeted

In BootstrapManager, before running the suite, register the binary entrypoints into PHPUnit's isolation exclusion list so they are not re-required in isolated children:

$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'][] = realpath( $wpmedia_phpunit_binary_path );
$GLOBALS['__PHPUNIT_ISOLATION_EXCLUDE_LIST'][] = realpath( __DIR__ . '/BootstrapManager.php' );

PHPUnit already honors this list in vendor/phpunit/phpunit/src/Util/GlobalState.php (getIncludedFilesAsString() skips listed files). Composer's bin-proxy already populates it for the real phpunit entry; the binary's own entrypoints simply need to be added.

  • Pros: minimal change; reuses PHPUnit's existing mechanism.
  • Cons: does not address the structural issue of loading PHPUnit into-process.

Option 2 β€” Structural / robust (recommended)

Run PHPUnit as a genuine subprocess (proc_open/passthru) instead of require-ing it into the runner process, so the binary and BootstrapManager never enter the test process's included-files set at all. This mirrors how native phpunit runs.

  • Pros: eliminates the root cause; robust against future PHPUnit versions and other edge cases.
  • Cons: larger refactor; requires careful subprocess lifecycle/error handling.

Impact

  • @runInSeparateProcess is common in WordPress plugin tests (e.g. tests that define() constants such as WP_DEBUG / WP_DEBUG_LOG). Affected consumers cannot use the README-recommended wpmedia-phpunit binary and must fall back to direct phpunit.
  • Dogfooding/README inconsistency worth noting: the README recommends "test-unit": "wpmedia-phpunit unit", but this library's own composer.json scripts invoke vendor/bin/phpunit --configuration ... directly β€” i.e. they sidestep the binary and therefore this bug.

Additional notes

Minor related papercut: BootstrapManager::getConfigArgv() hardcodes --colors=always, which emits raw ANSI escape codes when output is piped to a non-TTY (CI logs / file capture). Consider --colors=auto or making it overridable.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions