pestphp / pestphp/pest

[Bug]: arch() tests still break TestCase::createApplication for subsequent tests when the codebase references Rector (reproducible follow-up to #1423)

Open
#1,732 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
11.7k
Forks
538
Avg merge
4d 11h
Merged PRs (30d)
8

Description

What Happened

This is a follow-up to #1423, which was closed as not reproducible. The bug still reproduces on current versions (Pest v4.7.0, PHP 8.3.30, Laravel v13.12.0, rector/rector 2.4.5), and after the closure @InvisibleSmiley confirmed it independently and identified the condition that makes reproduction hit-or-miss — the arch test must run before the app-booting tests in the same process (the default Laravel phpunit.xml suite order, Unit → Feature, does exactly that when the arch test lives in tests/Unit). Filing fresh with the load-bearing conditions spelled out, the root cause traced into vendor code, and a verified workaround.

When any arch() expectation runs in a project that contains a class referencing Rector classes (e.g. a custom Rector rule under app/), every subsequent test in the same process that boots Laravel fails with:

FAILED  Tests\Feature\… > …                                                Error
Failed opening required '/path/to/app/vendor/rector/rector/bootstrap/app.php' (include_path='.:/usr/share/php')
Root cause (traced)

Three steps, each verifiable:

  1. The arch scan autoloads the scanned classes. Loading the custom rule (… extends Rector\Rector\AbstractRector) pulls in rector/rector, whose scoped autoloader registers itself as an additional Composer ClassLoader. After the arch test, array_keys(ClassLoader::getRegisteredLoaders()) is:
    [0] => /path/to/app/vendor/rector/rector/vendor
    [1] => /path/to/app/vendor
    
  2. Illuminate\Foundation\Testing\TestCase::createApplication() resolves the app via require Application::inferBasePath().'/bootstrap/app.php' (src/Illuminate/Foundation/Testing/TestCase.php:47).
  3. Application::inferBasePath() takes dirname() of the first registered loader path (src/Illuminate/Foundation/Application.php):
    default => dirname(array_values(array_filter(
        array_keys(ClassLoader::getRegisteredLoaders()),
        fn ($path) => ! str_starts_with($path, 'phar://'),
    ))[0]),
    
    With rector's loader registered first, the inferred base path becomes vendor/rector/rector, and the require of its nonexistent bootstrap/app.php fails.

So the arch run permanently perturbs global autoloader state for the rest of the PHPUnit process; any package that registers a scoped ClassLoader when one of its classes is touched by the scan (rector is the common case) will break it.

How to Reproduce

Three ingredients are load-bearing (missing any one of them makes it pass, which is likely why #1423 didn't reproduce):

  1. a class in the scanned code referencing Rector classes;
  2. any arch() test — a preset or a single targeted expectation, both reproduce;
  3. a Laravel-booting test running after the arch test in the same process (default suite order Unit → Feature suffices).
laravel new app   # prompts: no starter kit, Pest, SQLite
cd app
composer require --dev rector/rector

mkdir -p app/Rector/Rules
cat > app/Rector/Rules/SomeCustomRector.php <<'PHP'
<?php

declare(strict_types=1);

namespace App\Rector\Rules;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\Trait_;
use Rector\Contract\Rector\ConfigurableRectorInterface;
use Rector\Rector\AbstractRector;

final class SomeCustomRector extends AbstractRector implements ConfigurableRectorInterface
{
    public function configure(array $configuration): void {}

    /** @return array<class-string<Node>> */
    public function getNodeTypes(): array
    {
        return [Trait_::class, Class_::class];
    }

    public function refactor(Node $node): ?Node
    {
        return $node;
    }
}
PHP

cat > tests/Unit/PresetTest.php <<'PHP'
<?php

declare(strict_types=1);

arch()->preset()->php();
PHP

vendor/bin/pest

Result: Tests\Unit\PresetTest passes, then Tests\Feature\ExampleTest fails with the Failed opening required '…/vendor/rector/rector/bootstrap/app.php' error above. The same happens in our real application with a single targeted expectation (arch()->expect(SomeTrait::class)->toOnlyBeUsedIn(SomeClass::class)) — it is not preset-specific.

Sample Repository

https://github.com/alexkart-examples/pest-rector-bug — the sample repo from #1423 (built on Pest 3.8.2; the script above is the same recipe). The bug reproduces identically on Pest 4.7.0, so it has persisted across the major version.

Verified workaround (confirms the diagnosis)

Pinning the base path so inferBasePath() never consults the loaders fixes it:

APP_BASE_PATH=$(pwd) vendor/bin/pest   # all tests pass, arch test included

(Equivalently, $_ENV['APP_BASE_PATH'] = dirname(__DIR__); at the top of tests/Pest.php.)

Suggested direction

Since the arch layer is what perturbs the process-global loader state, it seems like its responsibility to contain that — e.g. snapshot ClassLoader::getRegisteredLoaders() before the scan and unregister newcomers afterwards; alternatively the Laravel plugin could pin APP_BASE_PATH from the already-booted application before arch expectations run. Happy to test a patch — the sample repository above is ready to clone.

Versions
Pest v4.7.0
PHP 8.3.30
Laravel v13.12.0
rector/rector 2.4.5
OS Linux (Ubuntu)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Reproduce with the sample repository and vendor/bin/pest, then inspect the arch scan alongside Illuminate\Foundation\Testing\TestCase::createApplication() and Application::inferBasePath() in the named Laravel source files. Verify the registered-loader order before and after scanning. Done means a regression case covers a Rector-referencing class followed by a Laravel-booting test, and both pass without setting APP_BASE_PATH.

Written by the indexing model from the issue text.

Assessment

Tech stack
laravel, php
Domain
backend, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.