From 901b238d26c7ec5f412e9107eea2920f1cfd20cf Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 14:37:51 +0200 Subject: [PATCH 1/8] [CI] Use extracted tomasvotruba/fast-unit package instead of inline Go runner --- .github/workflows/benchmark_phpunit.yaml | 15 +- composer.json | 7 + utils-tests-runner/.gitignore | 2 - utils-tests-runner/README.md | 93 ------------ utils-tests-runner/go.mod | 3 - utils-tests-runner/main.go | 181 ----------------------- 6 files changed, 15 insertions(+), 286 deletions(-) delete mode 100644 utils-tests-runner/.gitignore delete mode 100644 utils-tests-runner/README.md delete mode 100644 utils-tests-runner/go.mod delete mode 100644 utils-tests-runner/main.go diff --git a/.github/workflows/benchmark_phpunit.yaml b/.github/workflows/benchmark_phpunit.yaml index 5e8af57a633..a84367dfd68 100644 --- a/.github/workflows/benchmark_phpunit.yaml +++ b/.github/workflows/benchmark_phpunit.yaml @@ -1,7 +1,12 @@ name: Benchmark PHPUnit +<<<<<<< HEAD # A/B wall-time benchmark: serial phpunit vs the Go parallel runner. # Scheduled only (not on pull requests); runs every 2 hours on ubuntu. +======= +# A/B wall-time benchmark: serial phpunit vs the fast-unit parallel runner. +# Scheduled only (not on pull requests); runs every 2 hours on ubuntu + windows. +>>>>>>> 52991250dc ([CI] Use extracted tomasvotruba/fast-unit package instead of inline Go runner) # Reminder: cron fires only from the default branch, so this starts running # once the file is on `main`. on: @@ -36,10 +41,6 @@ jobs: - uses: "ramsey/composer-install@v4" - - uses: actions/setup-go@v5 - with: - go-version: 'stable' - - name: Serial phpunit shell: bash run: | @@ -48,9 +49,9 @@ jobs: vendor/bin/phpunit echo "| ${{ matrix.os }} | ${{ matrix.php-versions }} | serial | $((SECONDS - start))s |" >> "$GITHUB_STEP_SUMMARY" - - name: Go parallel runner + - name: fast-unit runner shell: bash run: | start=$SECONDS - go run ./utils-tests-runner/main.go - echo "| ${{ matrix.os }} | ${{ matrix.php-versions }} | go-runner | $((SECONDS - start))s |" >> "$GITHUB_STEP_SUMMARY" + vendor/bin/fastunit + echo "| ${{ matrix.os }} | ${{ matrix.php-versions }} | fast-unit | $((SECONDS - start))s |" >> "$GITHUB_STEP_SUMMARY" diff --git a/composer.json b/composer.json index 7f3c636669d..f14d76d35f3 100644 --- a/composer.json +++ b/composer.json @@ -57,6 +57,7 @@ "symplify/phpstan-rules": "^14.12", "symplify/vendor-patches": "^11.5", "tomasvotruba/class-leak": "^2.1", + "tomasvotruba/fast-unit": "^0.1", "tomasvotruba/type-coverage": "^2.3", "tomasvotruba/unused-public": "^2.2", "tracy/tracy": "^2.12" @@ -64,6 +65,12 @@ "replace": { "rector/rector": "self.version" }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/tomasvotruba/fast-unit.git" + } + ], "autoload": { "psr-4": { "Rector\\": [ diff --git a/utils-tests-runner/.gitignore b/utils-tests-runner/.gitignore deleted file mode 100644 index e8d9d4d50e0..00000000000 --- a/utils-tests-runner/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/fast-phpunit -/fast-phpunit.exe diff --git a/utils-tests-runner/README.md b/utils-tests-runner/README.md deleted file mode 100644 index 4b930c5113a..00000000000 --- a/utils-tests-runner/README.md +++ /dev/null @@ -1,93 +0,0 @@ -# fast-phpunit (proof of concept) - -Run the PHPUnit suite **~4x faster** by splitting test classes across parallel -workers that each boot PHP **once** and run many classes in a single process. - -## Speed — CI, per platform (GitHub runners, 4 vCPU, full suite) - -Serial `vendor/bin/phpunit` vs this runner, measured on the CI run step: - -| Platform | Serial | Go runner | Speedup | -| --- | --- | --- | --- | -| ubuntu-latest | 31s | **16s** | ~1.9x | -| windows-latest | **116s** | **67s** | **~1.7x** | - -Windows is the slow platform (~3.7x slower than Ubuntu serial); the runner cuts -~49s off it. Both run the full suite and pass. - -## Speed — local (24-core host, full suite) - -Full suite: **685 test classes / 5833 fixtures**. - -| Mode | Wall time | vs serial | -| --- | --- | --- | -| Serial `vendor/bin/phpunit` (1 process) | **38.3s** | 1.0x | -| `fast-phpunit -p 8` | ~11s | ~3.5x | -| `fast-phpunit -p 12` | **~9s** | **~4.2x** | -| `fast-phpunit -p 24` | ~9.8s | ~3.9x | - -Subset — `rules-tests/CodeQuality` (86 classes / 771 fixtures): - -| Mode | Wall time | -| --- | --- | -| Serial (1 process) | 5.71s | -| Process-per-class, `xargs -P8` | 8.55s (**slower than serial**) | -| `fast-phpunit -p 8` | **2.43s** | - -Sweet spot is ~12 workers; more does not help, because the heaviest chunk -bounds wall time and 20+ concurrent PHP processes start contending. - -## Why it is faster - -Bootstrap dominates a Rector test class, not the assertions: - -| Step | Time | -| --- | --- | -| Boot only (container build, 0 tests) | ~0.23s — fixed, per process | -| One class, 27 fixtures (warm) | 0.92s → ~26ms/fixture | - -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 slower than serial -(see subset table). - -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. - -## Usage - -```bash -cd utils-tests-runner && go build -o fast-phpunit . -cd .. -utils-tests-runner/fast-phpunit -p 12 # whole suite -utils-tests-runner/fast-phpunit -p 8 rules-tests/CodeQuality # a subtree -``` - -Flags: `-p` workers (default = CPU count), `-bin` phpunit path. - -## Isolation — required to make it correct - -Two shared-state issues surface when many classes share a process; both are -handled so any chunking is safe. - -1. **Shared temp cache (cross-process).** Rector caches parsed files under - `sys_get_temp_dir()/rector_cached_files`; parallel processes racing that - directory throw `Failed to open directory` / `Directory not empty`. Each - worker gets its own `TMPDIR`. - -2. **Leaked `phpVersion()` (in-process) — a latent bug, fixed here.** - `phpVersion(...)` is stored in the static `SimpleParameterProvider` and was - never reset between classes, so a version-bound class leaks its version into - the next class in the same process — a version-less class then sees, e.g., - PHP 8.1 instead of the test default (`PhpVersion::PHP_10`) and produces wrong - output. The serial suite passes only because of its class ordering; any - reshuffle (this tool **or** paratest) can trigger it. Fixed in - `AbstractRectorTestCase::tearDownAfterClass()` by resetting - `PHP_VERSION_FEATURES` to the test default. - -## Status - -Proof of concept. Standalone Go helper that shells out to the existing -`vendor/bin/phpunit`, so it does not change how tests are written or how CI -runs. Pure Go, no `.php`, so it is invisible to ECS / PHPStan / Rector. diff --git a/utils-tests-runner/go.mod b/utils-tests-runner/go.mod deleted file mode 100644 index 067b5c90707..00000000000 --- a/utils-tests-runner/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module rector/fast-phpunit - -go 1.26.4 diff --git a/utils-tests-runner/main.go b/utils-tests-runner/main.go deleted file mode 100644 index ee03c9d4195..00000000000 --- a/utils-tests-runner/main.go +++ /dev/null @@ -1,181 +0,0 @@ -// Command fast-phpunit runs the PHPUnit suite in parallel by splitting test -// classes into N balanced "warm" chunks: each worker boots PHP once and runs -// many test classes in a single process, so the container is built N times -// instead of once per class (as tools that spawn a process per chunk do). -// -// Balancing is by fixture count, since Rector rule tests iterate one assertion -// per .php.inc fixture, so fixture count approximates a class's runtime. -package main - -import ( - "flag" - "fmt" - "os" - "os/exec" - "path/filepath" - "regexp" - "runtime" - "sort" - "strings" - "sync" - "time" -) - -type testClass struct { - path string - weight int // fixture count, min 1 -} - -var fixtureCountRe = regexp.MustCompile(`\.php\.inc$`) - -func main() { - workers := flag.Int("p", runtime.NumCPU(), "number of parallel workers") - php := flag.String("php", "php", "php interpreter") - // the real PHP entry script (runs cross-platform via `php`); vendor/bin/phpunit - // is a shell/batch proxy on Windows and cannot be passed to php directly. - phpunit := flag.String("bin", "vendor/phpunit/phpunit/phpunit", "phpunit entry script") - flag.Parse() - - dirs := flag.Args() - if len(dirs) == 0 { - dirs = []string{"rules-tests", "tests"} - } - - classes := discover(dirs) - if len(classes) == 0 { - fmt.Fprintln(os.Stderr, "no test classes found") - os.Exit(1) - } - - chunks := balance(classes, *workers) - - start := time.Now() - failed := run(chunks, *php, *phpunit, *workers) - elapsed := time.Since(start) - - totalFixtures := 0 - for _, c := range classes { - totalFixtures += c.weight - } - fmt.Printf("\n%d classes, %d fixtures, %d chunks, %d workers\n", - len(classes), totalFixtures, len(chunks), *workers) - fmt.Printf("wall time: %.2fs\n", elapsed.Seconds()) - - if failed > 0 { - fmt.Printf("FAILED chunks: %d\n", failed) - os.Exit(1) - } - fmt.Println("OK") -} - -// discover finds *Test.php files and weights each by sibling Fixture/ file count. -func discover(dirs []string) []testClass { - var classes []testClass - for _, dir := range dirs { - _ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() || !strings.HasSuffix(path, "Test.php") { - return nil - } - classes = append(classes, testClass{path: path, weight: fixtureWeight(path)}) - return nil - }) - } - return classes -} - -func fixtureWeight(testPath string) int { - fixtureDir := filepath.Join(filepath.Dir(testPath), "Fixture") - entries, err := os.ReadDir(fixtureDir) - if err != nil { - return 1 - } - count := 0 - for _, e := range entries { - if !e.IsDir() && fixtureCountRe.MatchString(e.Name()) { - count++ - } - } - if count < 1 { - return 1 - } - return count -} - -// balance greedily packs classes (heaviest first) into n bins, always adding to -// the lightest bin. Minimizes the heaviest chunk, so wall time is bounded by the -// slowest worker rather than by unlucky static splits. -func balance(classes []testClass, n int) [][]testClass { - sort.Slice(classes, func(i, j int) bool { - return classes[i].weight > classes[j].weight - }) - bins := make([][]testClass, n) - loads := make([]int, n) - for _, c := range classes { - min := 0 - for i := 1; i < n; i++ { - if loads[i] < loads[min] { - min = i - } - } - bins[min] = append(bins[min], c) - loads[min] += c.weight - } - var out [][]testClass - for _, b := range bins { - if len(b) > 0 { - out = append(out, b) - } - } - return out -} - -func run(chunks [][]testClass, php, phpunit string, workers int) int { - sem := make(chan struct{}, workers) - var wg sync.WaitGroup - var mu sync.Mutex - failed := 0 - - for idx, chunk := range chunks { - wg.Add(1) - go func(idx int, chunk []testClass) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() - - // Each worker gets its own temp dir so Rector's file cache - // (sys_get_temp_dir()/rector_cached_files) and the fixture temp - // dumper never race across processes. sys_get_temp_dir() reads - // TMPDIR on Linux/macOS and TMP/TEMP on Windows, so set all three. - tmp := filepath.Join(os.TempDir(), fmt.Sprintf("fast-phpunit-%d", idx)) - _ = os.MkdirAll(tmp, 0o755) - defer os.RemoveAll(tmp) - - // invoke via `php ` so it works uniformly on Windows, - // where vendor/bin/phpunit is not directly executable. - args := make([]string, 0, len(chunk)+1) - args = append(args, phpunit) - for _, c := range chunk { - args = append(args, c.path) - } - cmd := exec.Command(php, args...) - cmd.Env = append(os.Environ(), "TMPDIR="+tmp, "TMP="+tmp, "TEMP="+tmp) - out, err := cmd.CombinedOutput() - if err != nil { - mu.Lock() - failed++ - fmt.Printf("chunk FAILED (%d classes): %v\n%s\n", len(chunk), err, tail(string(out), 15)) - mu.Unlock() - } - }(idx, chunk) - } - wg.Wait() - return failed -} - -func tail(s string, lines int) string { - parts := strings.Split(strings.TrimRight(s, "\n"), "\n") - if len(parts) > lines { - parts = parts[len(parts)-lines:] - } - return strings.Join(parts, "\n") -} From bdc3985abfe5840a99c7497962fc4819103fcc77 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 14:41:59 +0200 Subject: [PATCH 2/8] Drop VCS repository entry, resolve fast-unit from Packagist --- composer.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/composer.json b/composer.json index f14d76d35f3..a85eedfe0e6 100644 --- a/composer.json +++ b/composer.json @@ -65,12 +65,6 @@ "replace": { "rector/rector": "self.version" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/tomasvotruba/fast-unit.git" - } - ], "autoload": { "psr-4": { "Rector\\": [ From 333f03614398f58e99f5273cb1f94149c62c5351 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 15:08:44 +0200 Subject: [PATCH 3/8] Use fast-unit as main test runner; make failing fixture path clickable - key fixture data provider by path, so a failure prints the exact clickable .php.inc file instead of 'data set #N' - tests.yaml runs vendor/bin/fastunit over tests + rules-tests + utils/phpstan/tests --- .github/workflows/tests.yaml | 2 +- src/Testing/Fixture/FixtureFileFinder.php | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index a3378b794f7..33930c13b9b 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -38,4 +38,4 @@ jobs: - uses: "ramsey/composer-install@v4" - - run: vendor/bin/phpunit --colors + - run: vendor/bin/fastunit tests rules-tests utils/phpstan/tests diff --git a/src/Testing/Fixture/FixtureFileFinder.php b/src/Testing/Fixture/FixtureFileFinder.php index 10e2288ceca..65c941b6db0 100644 --- a/src/Testing/Fixture/FixtureFileFinder.php +++ b/src/Testing/Fixture/FixtureFileFinder.php @@ -11,7 +11,7 @@ final class FixtureFileFinder { /** * @api used in tests - * @return Iterator> + * @return Iterator> */ public static function yieldDirectory(string $directory, string $suffix = '*.php.inc'): Iterator { @@ -22,7 +22,9 @@ public static function yieldDirectory(string $directory, string $suffix = '*.php ->sortByName(); foreach ($finder as $fileInfo) { - yield [$fileInfo->getRealPath()]; + // key the data set by fixture path, so a failure prints the exact + // clickable ".php.inc" file instead of an anonymous "data set #N" + yield $fileInfo->getRealPath() => [$fileInfo->getRealPath()]; } } } From 6bf42332f6eca913f61cdfb358d2dd0d0e00ecf4 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 16:38:42 +0200 Subject: [PATCH 4/8] Fix RectorConfigTest global-state leak under warm-process runner; add composer test script The parallel runner batches many test classes into one warm PHP process, which exposed a latent leak: RectorConfig::configure() marks only the first call in a process as 'root', so RectorConfigTest's root-rule assertions failed when another class configured first. Add RectorConfig::resetRecreated() and reset it (plus the registered-rule lists) in the test's setUp. Also add composer 'test' script running fastunit over the suite dirs. --- composer.json | 3 ++- src/Config/RectorConfig.php | 10 ++++++++++ tests/Config/RectorConfigTest.php | 14 ++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a85eedfe0e6..01997f5db17 100644 --- a/composer.json +++ b/composer.json @@ -104,8 +104,9 @@ "complete-check": [ "@check-cs", "@phpstan", - "phpunit" + "@test" ], + "test": "vendor/bin/fastunit tests rules-tests utils/phpstan/tests", "check-cs": "vendor/bin/ecs check --ansi", "fix-cs": "vendor/bin/ecs check --fix --ansi", "phpstan": "vendor/bin/phpstan analyse --ansi --memory-limit=512M", diff --git a/src/Config/RectorConfig.php b/src/Config/RectorConfig.php index ef492c30300..8ff649c093f 100644 --- a/src/Config/RectorConfig.php +++ b/src/Config/RectorConfig.php @@ -59,6 +59,16 @@ final class RectorConfig extends Container private static ?bool $recreated = null; + /** + * @internal Resets the root-config detection, so tests that assert on root + * rule registration behave the same whether run alone or batched into one + * warm process by a parallel runner. + */ + public static function resetRecreated(): void + { + self::$recreated = null; + } + public static function configure(): RectorConfigBuilder { if (self::$recreated === null) { diff --git a/tests/Config/RectorConfigTest.php b/tests/Config/RectorConfigTest.php index f26396d6288..0f054548c7c 100644 --- a/tests/Config/RectorConfigTest.php +++ b/tests/Config/RectorConfigTest.php @@ -4,6 +4,7 @@ namespace Rector\Tests\Config; +use Rector\Config\RectorConfig; use Rector\Configuration\Option; use Rector\Configuration\Parameter\SimpleParameterProvider; use Rector\Renaming\Rector\MethodCall\RenameMethodRector; @@ -17,6 +18,19 @@ final class RectorConfigTest extends AbstractLazyTestCase { + protected function setUp(): void + { + parent::setUp(); + + // these tests assert on root rule registration, which is decided by a + // static "first configure() in the process is root" flag; reset it and + // the registered-rule lists so the assertions hold whether this class + // runs alone or batched into one warm process by a parallel runner + RectorConfig::resetRecreated(); + SimpleParameterProvider::setParameter(Option::REGISTERED_RECTOR_RULES, []); + SimpleParameterProvider::setParameter(Option::ROOT_STANDALONE_REGISTERED_RULES, []); + } + public function test(): void { $rectorConfig = $this->getContainer(); From 7f25db56ff2c061830128618f6025e8c6880fea7 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 17:09:26 +0200 Subject: [PATCH 5/8] Add test-tia composer script; gitignore .fastunit-cache --- .gitignore | 3 +++ composer.json | 1 + 2 files changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 9c96bd27896..4eb499d7d8a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,9 @@ composer.lock .phpunit.cache .fastunit-cache +# fast-unit test impact analysis cache +/.fastunit-cache + # scoped & downgraded version php-scoper.phar box.phar diff --git a/composer.json b/composer.json index 01997f5db17..183e9a62790 100644 --- a/composer.json +++ b/composer.json @@ -107,6 +107,7 @@ "@test" ], "test": "vendor/bin/fastunit tests rules-tests utils/phpstan/tests", + "test-tia": "vendor/bin/fastunit -tia tests rules-tests utils/phpstan/tests", "check-cs": "vendor/bin/ecs check --ansi", "fix-cs": "vendor/bin/ecs check --fix --ansi", "phpstan": "vendor/bin/phpstan analyse --ansi --memory-limit=512M", From bc7fd22b2dff72bef87245c544c451feb5aa309a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 17:42:25 +0200 Subject: [PATCH 6/8] Resolve rebase conflicts: benchmark comment + dedup .fastunit-cache gitignore - benchmark: keep fast-unit wording, ubuntu-only (Windows dropped in #8352) - .gitignore: main already ignores .fastunit-cache; drop the duplicate --- .github/workflows/benchmark_phpunit.yaml | 7 +------ .gitignore | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/benchmark_phpunit.yaml b/.github/workflows/benchmark_phpunit.yaml index a84367dfd68..c510d82e0e1 100644 --- a/.github/workflows/benchmark_phpunit.yaml +++ b/.github/workflows/benchmark_phpunit.yaml @@ -1,12 +1,7 @@ name: Benchmark PHPUnit -<<<<<<< HEAD -# A/B wall-time benchmark: serial phpunit vs the Go parallel runner. -# Scheduled only (not on pull requests); runs every 2 hours on ubuntu. -======= # A/B wall-time benchmark: serial phpunit vs the fast-unit parallel runner. -# Scheduled only (not on pull requests); runs every 2 hours on ubuntu + windows. ->>>>>>> 52991250dc ([CI] Use extracted tomasvotruba/fast-unit package instead of inline Go runner) +# Scheduled only (not on pull requests); runs every 2 hours on ubuntu. # Reminder: cron fires only from the default branch, so this starts running # once the file is on `main`. on: diff --git a/.gitignore b/.gitignore index 4eb499d7d8a..9c96bd27896 100644 --- a/.gitignore +++ b/.gitignore @@ -12,9 +12,6 @@ composer.lock .phpunit.cache .fastunit-cache -# fast-unit test impact analysis cache -/.fastunit-cache - # scoped & downgraded version php-scoper.phar box.phar From a047466a7f348e6f0ba94e149779851c8a947b6e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 17:45:57 +0200 Subject: [PATCH 7/8] fix unique fixture name --- .../Fixture/final_private_constructor.php.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules-tests/Php80/Rector/ClassMethod/FinalPrivateToPrivateVisibilityRector/Fixture/final_private_constructor.php.inc b/rules-tests/Php80/Rector/ClassMethod/FinalPrivateToPrivateVisibilityRector/Fixture/final_private_constructor.php.inc index 069bf70a364..c795868a63f 100644 --- a/rules-tests/Php80/Rector/ClassMethod/FinalPrivateToPrivateVisibilityRector/Fixture/final_private_constructor.php.inc +++ b/rules-tests/Php80/Rector/ClassMethod/FinalPrivateToPrivateVisibilityRector/Fixture/final_private_constructor.php.inc @@ -2,7 +2,7 @@ namespace Rector\Tests\Php80\Rector\ClassMethod\FinalPrivateToPrivateVisibilityRector\Fixture; -abstract class FinalPrivate +abstract class FinalPrivateConstructor { final private function __construct() { From b80b60515a2a92b55b82f74d7f817e8b632eae6b Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Sat, 15 Aug 2026 20:53:58 +0200 Subject: [PATCH 8/8] Add optional -tia fast-feedback job on PRs Runs only tests whose static dependency closure changed, with a persisted .fastunit-cache via actions/cache. PR-only and non-blocking in intent: the full 'tests' job stays the authoritative gate, since -tia over-approximates but cannot see dynamic (reflection / class-string) dependencies. --- .github/workflows/tests.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 33930c13b9b..db0052313ec 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -39,3 +39,35 @@ jobs: - uses: "ramsey/composer-install@v4" - run: vendor/bin/fastunit tests rules-tests utils/phpstan/tests + + # optional fast feedback on pull requests: run only the tests whose static + # dependency closure changed. NOT a merge gate -- the full "tests" job above + # stays authoritative, since -tia over-approximates but cannot see dynamic + # (reflection / class-string) dependencies. + tests_tia: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 4 + + name: PHP 8.4 tests (impacted only) + steps: + - uses: actions/checkout@v5 + + # restore the newest cached hash snapshot; save a fresh one per commit + - uses: actions/cache@v4 + with: + path: .fastunit-cache + key: fastunit-tia-${{ github.sha }} + restore-keys: | + fastunit-tia- + + - + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + ini-values: zend.assertions=1 + + - uses: "ramsey/composer-install@v4" + + - run: vendor/bin/fastunit -tia tests rules-tests utils/phpstan/tests