From fbc1a929a6fbd017ea27960490ce56d68bb10f1f Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 10 Oct 2026 00:37:40 +0400 Subject: [PATCH 1/2] test: run command tests in their own working directory The commands skip config generation when the working directory already has a .rr.yaml. Under Infection a mutant wrote one into the project root, which then failed the config tests of every later run. Assisted-By: Claude Opus 5.5 --- tests/Unit/GetBinaryCommandTest.php | 9 +++++++++ tests/Unit/MakeConfigCommandTest.php | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/tests/Unit/GetBinaryCommandTest.php b/tests/Unit/GetBinaryCommandTest.php index 02fae4a..779a89a 100644 --- a/tests/Unit/GetBinaryCommandTest.php +++ b/tests/Unit/GetBinaryCommandTest.php @@ -29,17 +29,26 @@ final class GetBinaryCommandTest /** @var list */ private array $downloads = []; + private string $cwd; + + /** + * The command also looks for ".rr.yaml" in the working directory, so each test gets its own. + */ #[BeforeTest] public function createDirectory(): void { $this->dir = TempDirectory::create(); $this->target = $this->dir . '/bin'; \mkdir($this->target); + $this->cwd = (string) \getcwd(); + \mkdir($this->dir . '/cwd'); + \chdir($this->dir . '/cwd'); } #[AfterTest] public function removeDirectory(): void { + \chdir($this->cwd); TempDirectory::remove($this->dir); } diff --git a/tests/Unit/MakeConfigCommandTest.php b/tests/Unit/MakeConfigCommandTest.php index 435d991..5255dd5 100644 --- a/tests/Unit/MakeConfigCommandTest.php +++ b/tests/Unit/MakeConfigCommandTest.php @@ -18,16 +18,24 @@ final class MakeConfigCommandTest { private string $dir; + private string $cwd; + /** + * The command also looks for ".rr.yaml" in the working directory, so each test gets its own. + */ #[BeforeTest] public function createDirectory(): void { $this->dir = TempDirectory::create(); + $this->cwd = (string) \getcwd(); + \mkdir($this->dir . '/cwd'); + \chdir($this->dir . '/cwd'); } #[AfterTest] public function removeDirectory(): void { + \chdir($this->cwd); TempDirectory::remove($this->dir); } @@ -74,6 +82,16 @@ public function keepsExistingConfiguration(): void Assert::same(\file_get_contents($this->dir . '/.rr.yaml'), 'existing'); } + public function keepsConfigurationOfWorkingDirectory(): void + { + \file_put_contents($this->dir . '/cwd/.rr.yaml', 'existing'); + + $status = $this->run(['--location' => $this->dir]); + + Assert::same($status, Command::FAILURE); + Assert::false(\is_file($this->dir . '/.rr.yaml')); + } + /** * @param array $input */ From 9f5abf879f45d0701a69efa9072c169124e1c5ef Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sat, 10 Oct 2026 00:37:43 +0400 Subject: [PATCH 2/2] ci: run mutation tests with Infection Infection is installed in the workflow rather than from the phar: the Testo adapter bundled in the 0.35.6 phar writes a bootstrap that calls the unscoped `\Infection\StreamWrapper\IncludeInterceptor`, while the phar only ships `Infected\...`, so every mutant errors. It stays out of require-dev because it needs PHP 8.3+. Local run (Windows): 568 mutants, covered MSI 65%; CI (Linux): 569 mutants, covered MSI 63%, so both thresholds are 60. Assisted-By: Claude Opus 5.5 --- .gitattributes | 1 + .github/workflows/mutation.yml | 51 ++++++++++++++++++++++++++++++++++ README.md | 1 + infection.json5 | 16 +++++++++++ 4 files changed, 69 insertions(+) create mode 100644 .github/workflows/mutation.yml create mode 100644 infection.json5 diff --git a/.gitattributes b/.gitattributes index 478a2db..cb1b6e5 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ /*.lock export-ignore /*.dist export-ignore /*.php export-ignore +/infection.json5 export-ignore diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml new file mode 100644 index 0000000..9484c92 --- /dev/null +++ b/.github/workflows/mutation.yml @@ -0,0 +1,51 @@ +--- + +name: Mutation Tests + +on: # yamllint disable-line rule:truthy + pull_request: + push: + branches: [2.x] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + mutation: + name: Infection + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + persist-credentials: false + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: xdebug + ini-values: memory_limit=-1 + + - name: Install Composer dependencies + uses: ramsey/composer-install@v3 + with: + dependency-versions: highest + + # The Infection phar is unusable with Testo: its bundled adapter calls the + # unscoped \Infection\StreamWrapper\IncludeInterceptor. Infection requires + # PHP 8.3+, so it is installed here instead of in require-dev. + - name: Install Infection + run: | + composer config allow-plugins.infection/extension-installer true + composer require --dev --no-interaction --no-progress -W infection/infection testo/bridge-infection + + - name: Run Infection + run: vendor/bin/infection --threads=max --logger-github --show-mutations=0 --min-msi=60 --min-covered-msi=60 + env: + STRYKER_DASHBOARD_API_KEY: ${{ github.event_name == 'push' && secrets.STRYKER_DASHBOARD_API_KEY || '' }} diff --git a/README.md b/README.md index 7f04bb4..fcf4bb9 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ [![Psalm Level](https://shepherd.dev/github/roadrunner-php/cli/level.svg)](https://shepherd.dev/github/roadrunner-php/cli) [![Type Coverage](https://shepherd.dev/github/roadrunner-php/cli/coverage.svg)](https://shepherd.dev/github/roadrunner-php/cli) +[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Froadrunner-php%2Fcli%2F2.x)](https://dashboard.stryker-mutator.io/reports/github.com/roadrunner-php/cli/2.x) diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 0000000..b6307ae --- /dev/null +++ b/infection.json5 @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/infection/infection/0.35.6/resources/schema.json", + "source": { + "directories": ["src"] + }, + "testFramework": "testo", + "tmpDir": "runtime", + "timeout": 10, + "logs": { + "text": "runtime/infection.log", + "github": true, + "stryker": { + "report": "2.x" + } + } +}