From 83282dbc6aa39cd2509809837a304d9f2ed1482c Mon Sep 17 00:00:00 2001 From: Thomas Hauschild <7961978+Morgy93@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:10:25 +0200 Subject: [PATCH 1/4] test: strict PHPUnit config, PCOV coverage and Infection mutation testing - phpunit.xml.dist: random execution order, fail on risky/warning/empty suite, strict output checks, details on deprecations/notices/warnings - PCOV in the DDEV web image (disabled by default, enabled per run to avoid the opcache JIT warning); `ddev phpunit --coverage` generates text, HTML and Clover reports - Infection mutation testing via `ddev infection` / composer test:mutation - CI: PHP 8.3/8.4 matrix, coverage summary + artifact, mutation job Co-Authored-By: Claude Fable 5 --- .ddev/commands/web/infection | 30 ++++++++++ .ddev/commands/web/phpunit | 32 ++++++++++- .ddev/php/pcov.ini | 4 ++ .ddev/web-build/Dockerfile.pcov | 3 + .github/workflows/phpunit.yml | 99 ++++++++++++++++++++++++++++++--- .gitignore | 1 + composer.json | 7 ++- infection.json5 | 16 ++++++ phpunit.xml.dist | 18 +++++- 9 files changed, 198 insertions(+), 12 deletions(-) create mode 100755 .ddev/commands/web/infection create mode 100644 .ddev/php/pcov.ini create mode 100644 .ddev/web-build/Dockerfile.pcov create mode 100644 infection.json5 diff --git a/.ddev/commands/web/infection b/.ddev/commands/web/infection new file mode 100755 index 00000000..ae063d41 --- /dev/null +++ b/.ddev/commands/web/infection @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +## Description: Run Infection mutation testing for the MageForge module +## Usage: infection [infection-options] +## Example: ddev infection +## Example: ddev infection --show-mutations +## Example: ddev infection --filter=src/Service/Hyva/ + +cd /var/www/html + +if [[ ! -x vendor/bin/infection ]]; then + echo "infection not found. Installing module dev dependencies..." + composer install --no-interaction +fi + +if ! php -m | grep -qi '^pcov$'; then + echo "PCOV extension not loaded. Run 'ddev restart' to rebuild the web image" >&2 + echo "(.ddev/web-build/Dockerfile.pcov installs it)." >&2 + exit 1 +fi + +echo "Mutation report: reports/infection/infection.html" +# The initial PHPUnit run needs PCOV for coverage (globally off, see +# .ddev/php/pcov.ini); opcache.jit=off avoids the JIT warning PCOV triggers. +exec vendor/bin/infection \ + --threads=max \ + --initial-tests-php-options="-d pcov.enabled=1 -d opcache.jit=off -d opcache.jit_buffer_size=0" \ + "$@" diff --git a/.ddev/commands/web/phpunit b/.ddev/commands/web/phpunit index 28b3c5b6..cdd482cb 100755 --- a/.ddev/commands/web/phpunit +++ b/.ddev/commands/web/phpunit @@ -3,10 +3,12 @@ set -euo pipefail ## Description: Run PHPUnit tests for the MageForge module -## Usage: phpunit [options] +## Usage: phpunit [--coverage] [phpunit-options] ## Example: ddev phpunit ## Example: ddev phpunit --testdox ## Example: ddev phpunit tests/Unit/Service/Hyva/ +## Example: ddev phpunit --coverage +## Example: ddev phpunit --coverage tests/Unit/Service/Hyva/ cd /var/www/html @@ -15,4 +17,30 @@ if [[ ! -x vendor/bin/phpunit ]]; then composer install --no-interaction fi -vendor/bin/phpunit "$@" +coverage=false +args=() +for arg in "$@"; do + if [[ "$arg" == "--coverage" ]]; then + coverage=true + else + args+=("$arg") + fi +done + +if [[ "$coverage" == true ]]; then + if ! php -m | grep -qi '^pcov$'; then + echo "PCOV extension not loaded. Run 'ddev restart' to rebuild the web image" >&2 + echo "(.ddev/web-build/Dockerfile.pcov installs it)." >&2 + exit 1 + fi + echo "Coverage reports: reports/coverage/index.html (HTML), reports/clover.xml (Clover)" + # pcov.enabled=1: opt in per-run (globally off, see .ddev/php/pcov.ini). + # opcache.jit=off: avoid the "JIT is incompatible" warning PCOV would trigger. + exec php -d pcov.enabled=1 -d opcache.jit=off -d opcache.jit_buffer_size=0 vendor/bin/phpunit \ + --coverage-text \ + --coverage-html reports/coverage \ + --coverage-clover reports/clover.xml \ + "${args[@]}" +fi + +exec vendor/bin/phpunit "${args[@]}" diff --git a/.ddev/php/pcov.ini b/.ddev/php/pcov.ini new file mode 100644 index 00000000..c027cda1 --- /dev/null +++ b/.ddev/php/pcov.ini @@ -0,0 +1,4 @@ +; PCOV stays disabled by default: its zend_execute_ex hook forces opcache to +; disable JIT and print a warning on every PHP invocation. +; `ddev phpunit --coverage` enables it per-run via -d pcov.enabled=1. +pcov.enabled=0 diff --git a/.ddev/web-build/Dockerfile.pcov b/.ddev/web-build/Dockerfile.pcov new file mode 100644 index 00000000..b1091bd8 --- /dev/null +++ b/.ddev/web-build/Dockerfile.pcov @@ -0,0 +1,3 @@ +# PCOV provides fast code coverage for PHPUnit (used by `ddev phpunit --coverage`). +# It is disabled by default via .ddev/php/pcov.ini and enabled per-run. +RUN sudo apt-get update && sudo apt-get install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests php${DDEV_PHP_VERSION}-pcov diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index baf97f7a..abbe5610 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -5,14 +5,19 @@ on: [pull_request] permissions: contents: read -env: - PHP_VERSION: "8.4" - jobs: phpunit: - name: Unit Tests + name: Unit Tests (PHP ${{ matrix.php-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ["8.3", "8.4"] + include: + - php-version: "8.4" + coverage: true + steps: - name: Checkout code uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 @@ -20,18 +25,98 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 with: - php-version: ${{ env.PHP_VERSION }} + php-version: ${{ matrix.php-version }} tools: composer:v2 + coverage: ${{ matrix.coverage && 'pcov' || 'none' }} - name: Cache Composer packages uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: ~/.composer/cache/files - key: ${{ runner.os }}-composer-phpunit-${{ hashFiles('composer.json') }} - restore-keys: ${{ runner.os }}-composer-phpunit + key: ${{ runner.os }}-composer-phpunit-${{ matrix.php-version }}-${{ hashFiles('composer.json') }} + restore-keys: ${{ runner.os }}-composer-phpunit-${{ matrix.php-version }} - name: Install dev dependencies run: composer install --no-interaction --no-progress - name: Run PHPUnit + if: ${{ !matrix.coverage }} run: vendor/bin/phpunit + + - name: Run PHPUnit with coverage + if: ${{ matrix.coverage }} + run: > + vendor/bin/phpunit + --coverage-text=reports/coverage.txt + --coverage-html reports/coverage + --coverage-clover reports/clover.xml + + - name: Publish coverage summary + if: ${{ matrix.coverage && always() }} + run: | + if [[ -f reports/coverage.txt ]]; then + { + echo '## Code Coverage' + echo '```' + cat reports/coverage.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload coverage report + if: ${{ matrix.coverage && always() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: coverage-report + path: | + reports/coverage + reports/clover.xml + if-no-files-found: ignore + + mutation: + name: Mutation Tests (Infection) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # v2 + with: + php-version: "8.4" + tools: composer:v2 + coverage: pcov + + - name: Cache Composer packages + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: ~/.composer/cache/files + key: ${{ runner.os }}-composer-phpunit-8.4-${{ hashFiles('composer.json') }} + restore-keys: ${{ runner.os }}-composer-phpunit-8.4 + + - name: Install dev dependencies + run: composer install --no-interaction --no-progress + + - name: Run Infection + run: vendor/bin/infection --threads=max --logger-github --no-progress + + - name: Publish mutation summary + if: always() + run: | + if [[ -f reports/infection/summary.log ]]; then + { + echo '## Mutation Testing' + echo '```' + cat reports/infection/summary.log + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload mutation report + if: always() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: mutation-report + path: reports/infection + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 1a5f69b8..bf161553 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # PHPUnit /.phpunit.cache/ +/reports/ .vscode /magento/ diff --git a/composer.json b/composer.json index 14317943..0a07d0cf 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ }, "require-dev": { "carthage-software/mago": "^1.30", + "infection/infection": "^0.34.0", "magento/magento-coding-standard": "^40", "phpunit/phpunit": "^11.0" }, @@ -37,6 +38,7 @@ "sort-packages": true, "allow-plugins": { "dealerdirect/phpcodesniffer-composer-installer": true, + "infection/extension-installer": true, "magento/composer-dependency-version-audit-plugin": true } }, @@ -45,6 +47,9 @@ "fmt": "mago fmt", "fmt:check": "mago fmt --dry-run", "phpcs": "phpcs -p -s", - "phpcbf": "phpcbf -p" + "phpcbf": "phpcbf -p", + "test": "phpunit", + "test:coverage": "phpunit --coverage-text --coverage-html reports/coverage --coverage-clover reports/clover.xml", + "test:mutation": "infection --threads=max --initial-tests-php-options='-d pcov.enabled=1'" } } diff --git a/infection.json5 b/infection.json5 new file mode 100644 index 00000000..d6aad063 --- /dev/null +++ b/infection.json5 @@ -0,0 +1,16 @@ +{ + "$schema": "vendor/infection/infection/resources/schema.json", + "source": { + "directories": ["src"], + "excludes": ["registration.php"], + }, + "timeout": 10, + "logs": { + "text": "reports/infection/infection.log", + "html": "reports/infection/infection.html", + "summary": "reports/infection/summary.log", + }, + "mutators": { + "@default": true, + }, +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e067aaea..3f29a067 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -3,15 +3,29 @@ xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd" bootstrap="vendor/autoload.php" colors="true" - cacheDirectory=".phpunit.cache"> + cacheDirectory=".phpunit.cache" + executionOrder="random" + failOnRisky="true" + failOnWarning="true" + failOnEmptyTestSuite="true" + beStrictAboutOutputDuringTests="true" + beStrictAboutChangesToGlobalState="true" + displayDetailsOnPhpunitDeprecations="true" + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true"> tests/Unit - + src + + src/registration.php + From b0b3d74991fef06ff1f14948cad81f1859ddddf5 Mon Sep 17 00:00:00 2001 From: Thomas Hauschild <7961978+Morgy93@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:18:49 +0200 Subject: [PATCH 2/4] fix: format file --- .ddev/commands/web/phpunit | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.ddev/commands/web/phpunit b/.ddev/commands/web/phpunit index cdd482cb..8907ae80 100755 --- a/.ddev/commands/web/phpunit +++ b/.ddev/commands/web/phpunit @@ -20,14 +20,14 @@ fi coverage=false args=() for arg in "$@"; do - if [[ "$arg" == "--coverage" ]]; then + if [[ $arg == "--coverage" ]]; then coverage=true else args+=("$arg") fi done -if [[ "$coverage" == true ]]; then +if [[ $coverage == true ]]; then if ! php -m | grep -qi '^pcov$'; then echo "PCOV extension not loaded. Run 'ddev restart' to rebuild the web image" >&2 echo "(.ddev/web-build/Dockerfile.pcov installs it)." >&2 From f00a7e53553acf189da09cb66023e73d88b06755 Mon Sep 17 00:00:00 2001 From: Thomas Hauschild <7961978+Morgy93@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:28:42 +0200 Subject: [PATCH 3/4] chore: fix trunk check findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shellcheck SC2250: brace all variable references in the phpunit command - hadolint: ignore .ddev/web-build/** — DDEV Dockerfile fragments have no FROM and use sudo by design - quote the apt package argument in Dockerfile.pcov Co-Authored-By: Claude Fable 5 --- .ddev/commands/web/phpunit | 6 +++--- .ddev/web-build/Dockerfile.pcov | 2 +- .trunk/trunk.yaml | 6 ++++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.ddev/commands/web/phpunit b/.ddev/commands/web/phpunit index 8907ae80..924fd720 100755 --- a/.ddev/commands/web/phpunit +++ b/.ddev/commands/web/phpunit @@ -20,14 +20,14 @@ fi coverage=false args=() for arg in "$@"; do - if [[ $arg == "--coverage" ]]; then + if [[ ${arg} == "--coverage" ]]; then coverage=true else - args+=("$arg") + args+=("${arg}") fi done -if [[ $coverage == true ]]; then +if [[ ${coverage} == true ]]; then if ! php -m | grep -qi '^pcov$'; then echo "PCOV extension not loaded. Run 'ddev restart' to rebuild the web image" >&2 echo "(.ddev/web-build/Dockerfile.pcov installs it)." >&2 diff --git a/.ddev/web-build/Dockerfile.pcov b/.ddev/web-build/Dockerfile.pcov index b1091bd8..246eb21d 100644 --- a/.ddev/web-build/Dockerfile.pcov +++ b/.ddev/web-build/Dockerfile.pcov @@ -1,3 +1,3 @@ # PCOV provides fast code coverage for PHPUnit (used by `ddev phpunit --coverage`). # It is disabled by default via .ddev/php/pcov.ini and enabled per-run. -RUN sudo apt-get update && sudo apt-get install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests php${DDEV_PHP_VERSION}-pcov +RUN sudo apt-get update && sudo apt-get install -y -o Dpkg::Options::="--force-confold" --no-install-recommends --no-install-suggests "php${DDEV_PHP_VERSION}-pcov" diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index 9c95b89d..f6ab98bf 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -30,6 +30,12 @@ lint: - linters: [checkov] paths: - .ddev/** + # DDEV web-build files are Dockerfile *fragments* concatenated into DDEV's + # generated Dockerfile: they have no FROM, and sudo is the documented DDEV + # mechanism since the build runs as the non-root web user. + - linters: [hadolint] + paths: + - .ddev/web-build/** enabled: - pinact@4.1.0 - svgo@4.0.1 From 8a2ed4ba896311ad0c9c5715d6bc3f90bdecdf25 Mon Sep 17 00:00:00 2001 From: Thomas Hauschild <7961978+Morgy93@users.noreply.github.com> Date: Sat, 4 Jul 2026 21:39:01 +0200 Subject: [PATCH 4/4] ci: create reports dir before coverage run PHPUnit's text report writes with a bare file_put_contents and only works if the Clover/HTML writers created reports/ first; drop that ordering dependence. Co-Authored-By: Claude Fable 5 --- .github/workflows/phpunit.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index abbe5610..8c9e9252 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -45,11 +45,14 @@ jobs: - name: Run PHPUnit with coverage if: ${{ matrix.coverage }} - run: > - vendor/bin/phpunit - --coverage-text=reports/coverage.txt - --coverage-html reports/coverage - --coverage-clover reports/clover.xml + # mkdir: the text report is written with a bare file_put_contents and + # relies on the Clover/HTML writers having created reports/ first. + run: | + mkdir -p reports + vendor/bin/phpunit \ + --coverage-text=reports/coverage.txt \ + --coverage-html reports/coverage \ + --coverage-clover reports/clover.xml - name: Publish coverage summary if: ${{ matrix.coverage && always() }}