Skip to content

Add PHPUnit Coverage workflow #16

Add PHPUnit Coverage workflow

Add PHPUnit Coverage workflow #16

Workflow file for this run

name: Code Coverage
on:
pull_request:
branches:
- develop
- main
push:
branches:
- develop
- main
workflow_dispatch:
# Cancels all previous workflow runs for the same branch that have not yet completed.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
code-coverage:
name: Code Coverage Check
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ALLOW_EMPTY_PASSWORD: false
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: wordpress_tests
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=10s --health-retries=10
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history for accurate diff
- name: Install PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
ini-values: zend.assertions=1, error_reporting=-1, display_errors=On
coverage: pcov # Use PCOV for faster code coverage (5x faster than Xdebug)
tools: composer
- name: Install SVN
run: sudo apt-get install subversion
- name: "Composer: remove the PHP platform requirement"
run: composer config --unset platform.php
- name: Install Composer dependencies
uses: ramsey/composer-install@v2
with:
dependency-versions: "highest"
composer-options: "yoast/wp-test-utils --with-dependencies"
custom-cache-suffix: $(date -u -d "-0 month -$(($(date +%d)-1)) days" "+%F")
- name: Install WordPress Test Suite
shell: bash
run: tests/bin/install-wp-tests.sh wordpress_tests root root 127.0.0.1:3306 latest
- name: Generate code coverage report for current branch
run: |
echo "=== Debug: Check PCOV is loaded ==="
php -m | grep pcov || echo "PCOV not loaded"
php -r "var_dump(extension_loaded('pcov'));"
echo "=== Debug: Check PHPUnit version and php-code-coverage ==="
vendor/bin/phpunit --version
php -r "echo class_exists('SebastianBergmann\CodeCoverage\CodeCoverage') ? 'CodeCoverage class exists' : 'CodeCoverage class NOT found'; echo PHP_EOL;"
echo "=== Debug: Before running PHPUnit ==="
pwd
ls -la
echo "=== Running PHPUnit with coverage (without piping to tee) ==="
php -d pcov.enabled=1 -d pcov.directory=. vendor/bin/phpunit --dont-report-useless-tests --coverage-clover=coverage.xml --coverage-text 2>&1 | tail -100
PHPUNIT_EXIT=$?
echo "=== Debug: PHPUnit exit code: $PHPUNIT_EXIT ==="
echo "=== Debug: After running PHPUnit ==="
pwd
ls -la coverage* 2>/dev/null || echo "No coverage files in current directory"
echo "=== Searching entire workspace for coverage.xml ==="
find . -name "coverage.xml" -type f 2>/dev/null || echo "No coverage.xml found anywhere"
echo "=== Checking if coverage report was generated ==="
if [ -f coverage.xml ]; then
echo "SUCCESS: coverage.xml exists!"
ls -lh coverage.xml
else
echo "FAIL: coverage.xml was not generated"
echo "=== Checking stderr for errors ==="
php -d pcov.enabled=1 -d pcov.directory=. vendor/bin/phpunit --dont-report-useless-tests --coverage-clover=coverage.xml --coverage-text 2>&1 >/dev/null | grep -i "error\|warning\|coverage" || echo "No error messages found"
fi
continue-on-error: false
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
flags: unittests
name: codecov-progress-planner
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- name: Generate coverage report summary
id: coverage
run: |
# Debug: Check for coverage files
echo "=== Debug: Looking for coverage files ==="
ls -la coverage* 2>/dev/null || echo "No coverage files found in current directory"
echo "=== Debug: Current directory ==="
pwd
echo "=== Debug: Workspace directory ==="
echo "${{ github.workspace }}"
# Search for coverage.xml anywhere in the workspace
echo "=== Searching for coverage.xml recursively ==="
FOUND_FILES=$(find ${{ github.workspace }} -name "coverage.xml" -type f 2>/dev/null)
if [ -n "$FOUND_FILES" ]; then
echo "Found coverage.xml files:"
echo "$FOUND_FILES"
else
echo "No coverage.xml found anywhere in workspace"
fi
# Try to find coverage.xml in multiple locations
COVERAGE_FILE=""
if [ -f coverage.xml ]; then
COVERAGE_FILE="coverage.xml"
echo "Found coverage.xml in current directory"
elif [ -f ${{ github.workspace }}/coverage.xml ]; then
COVERAGE_FILE="${{ github.workspace }}/coverage.xml"
echo "Found coverage.xml in workspace root"
else
# Try to find it anywhere
COVERAGE_FILE=$(find ${{ github.workspace }} -name "coverage.xml" -type f 2>/dev/null | head -1)
if [ -n "$COVERAGE_FILE" ]; then
echo "Found coverage.xml at: $COVERAGE_FILE"
fi
fi
if [ -n "$COVERAGE_FILE" ]; then
echo "=== Debug: Found coverage file at $COVERAGE_FILE ==="
echo "=== Debug: First 50 lines of coverage.xml ==="
head -50 "$COVERAGE_FILE"
# Extract coverage using various possible attribute names
# Try 'statements' and 'coveredstatements'
LINES=$(grep -o 'statements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
COVERED=$(grep -o 'coveredstatements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
# Try alternative: 'elements' and 'coveredelements'
if [ -z "$LINES" ]; then
LINES=$(grep -o 'elements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
COVERED=$(grep -o 'coveredelements="[0-9]*"' "$COVERAGE_FILE" | head -1 | grep -o '[0-9]*')
fi
echo "=== Debug: LINES=$LINES, COVERED=$COVERED ==="
if [ -n "$LINES" ] && [ -n "$COVERED" ] && [ "$LINES" -gt 0 ]; then
COVERAGE=$(awk "BEGIN {printf \"%.2f\", ($COVERED/$LINES)*100}")
else
COVERAGE="0"
fi
else
echo "=== Debug: No coverage.xml file found ==="
COVERAGE="0"
fi
echo "current_coverage=$COVERAGE" >> $GITHUB_OUTPUT
echo "Current code coverage: $COVERAGE%"
- name: Checkout base branch for comparison
if: github.event_name == 'pull_request'
run: |
git fetch origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }}
- name: Install dependencies on base branch
if: github.event_name == 'pull_request'
run: |
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Generate coverage report for base branch
if: github.event_name == 'pull_request'
id: base_coverage
run: |
# Generate coverage for base branch
vendor/bin/phpunit --coverage-text --colors=never > base-coverage.txt 2>&1 || true
BASE_COVERAGE=$(cat base-coverage.txt | grep "Lines:" | awk '{print $2}' | sed 's/%//' || echo "0")
echo "base_coverage=$BASE_COVERAGE" >> $GITHUB_OUTPUT
echo "Base branch code coverage: $BASE_COVERAGE%"
continue-on-error: true
- name: Compare coverage and enforce threshold
if: github.event_name == 'pull_request'
run: |
CURRENT="${{ steps.coverage.outputs.current_coverage }}"
BASE="${{ steps.base_coverage.outputs.base_coverage }}"
# Default to 0 if base coverage couldn't be determined
BASE=${BASE:-0}
echo "Current Coverage: $CURRENT%"
echo "Base Coverage: $BASE%"
# Calculate the difference
DIFF=$(echo "$CURRENT - $BASE" | bc)
echo "Coverage Difference: $DIFF%"
# Check if coverage dropped by more than 0.5%
THRESHOLD=-0.5
if (( $(echo "$DIFF < $THRESHOLD" | bc -l) )); then
echo "❌ Code coverage dropped by ${DIFF}%, which exceeds the allowed threshold of ${THRESHOLD}%"
echo "Please add tests to maintain or improve code coverage."
exit 1
else
echo "✅ Code coverage check passed!"
echo "Coverage change: ${DIFF}%"
fi
- name: Comment PR with coverage
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const current = '${{ steps.coverage.outputs.current_coverage }}';
const base = '${{ steps.base_coverage.outputs.base_coverage }}' || '0';
const diff = (parseFloat(current) - parseFloat(base)).toFixed(2);
const emoji = diff >= 0 ? '📈' : '📉';
const status = diff >= -0.5 ? '✅' : '❌';
const comment = `## ${status} Code Coverage Report
| Metric | Value |
|--------|-------|
| Current Coverage | **${current}%** |
| Base Coverage | **${base}%** |
| Difference | ${emoji} **${diff}%** |
${diff < -0.5 ? '⚠️ **Warning:** Coverage dropped by more than 0.5%. Please add tests.' : ''}
${diff >= 0 ? '🎉 Great job maintaining/improving code coverage!' : ''}
`;
// Find existing coverage report comment
const {data: comments} = await github.rest.issues.listComments({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Code Coverage Report')
);
if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
comment_id: botComment.id,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} else {
// Create new comment
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
}
- name: Generate HTML coverage report
if: always()
run: |
vendor/bin/phpunit --coverage-html=coverage-html
continue-on-error: true
- name: Upload HTML coverage report as artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage-html/
retention-days: 30