@@ -2,9 +2,6 @@ name: Code Coverage
22
33on :
44 pull_request :
5- branches :
6- - develop
7- - main
85 push :
96 branches :
107 - develop
4643 coverage : xdebug # Use Xdebug for coverage (PCOV was causing PHP to crash)
4744 tools : composer
4845
49- - name : Install SVN
50- run : sudo apt-get install subversion
46+ - name : Install SVN and XML tools
47+ run : |
48+ sudo apt-get update
49+ sudo apt-get install -y subversion libxml2-utils
5150
5251 - name : Install Composer dependencies
5352 uses : ramsey/composer-install@v2
@@ -74,13 +73,23 @@ jobs:
7473 # Run PHPUnit with coverage - allow test failures but ensure coverage is generated
7574 # test-class-security.php is excluded via phpunit.xml.dist to avoid output contamination
7675 set +e
76+ # Run PHPUnit and capture both test output and coverage text separately
7777 php -d memory_limit=512M -d max_execution_time=300 \
7878 vendor/bin/phpunit --configuration phpunit.xml.dist \
7979 --coverage-clover=coverage.xml \
80- --coverage-text 2>&1 | tee phpunit-output .log
80+ --coverage-text --colors=never > phpunit-with-coverage .log 2>&1
8181 PHPUNIT_EXIT=$?
8282 set -e
8383
84+ # Extract test output (everything before coverage section) for debugging
85+ # Coverage section typically starts with a line like "Code Coverage Report:" or summary table
86+ # Extract everything up to (but not including) the coverage section
87+ awk '/Code Coverage Report:|^Summary|^ Classes:|^ Methods:|^ Lines:/{exit} {print}' phpunit-with-coverage.log > phpunit-output.log || cat phpunit-with-coverage.log > phpunit-output.log
88+
89+ # Extract coverage text output (the coverage section)
90+ # Coverage section starts with summary or "Code Coverage Report"
91+ awk '/Code Coverage Report:|^Summary|^ Classes:|^ Methods:|^ Lines:/{flag=1} flag' phpunit-with-coverage.log > current-coverage-full.txt || tail -200 phpunit-with-coverage.log > current-coverage-full.txt
92+
8493 echo "End time: $(date)"
8594 echo "Memory after: $(free -h | grep Mem)"
8695 echo "=== Debug: PHPUnit exit code: $PHPUNIT_EXIT ==="
@@ -117,14 +126,36 @@ jobs:
117126 - name : Generate coverage report summary
118127 id : coverage
119128 run : |
120- # Generate coverage text report and save it
121- vendor/bin/phpunit --coverage-text --colors=never > current-coverage-full.txt 2>&1 || true
129+ # Extract overall coverage from coverage.xml (Clover format)
130+ # This avoids running PHPUnit twice - we already have coverage.xml from the first run
131+ if [ -f coverage.xml ]; then
132+ # Extract metrics from Clover XML using xmllint
133+ # Fallback to Python if xmllint fails
134+ STATEMENTS=$(xmllint --xpath 'string(//project/metrics/@statements)' coverage.xml 2>/dev/null || python3 -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); metrics = tree.find('.//project/metrics'); print(metrics.get('statements', '0') if metrics is not None else '0')" 2>/dev/null || echo "0")
135+ COVERED_STATEMENTS=$(xmllint --xpath 'string(//project/metrics/@coveredstatements)' coverage.xml 2>/dev/null || python3 -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); metrics = tree.find('.//project/metrics'); print(metrics.get('coveredstatements', '0') if metrics is not None else '0')" 2>/dev/null || echo "0")
136+
137+ # Calculate coverage percentage
138+ if [ "$STATEMENTS" != "0" ] && [ -n "$STATEMENTS" ] && [ -n "$COVERED_STATEMENTS" ]; then
139+ COVERAGE=$(echo "scale=2; ($COVERED_STATEMENTS * 100) / $STATEMENTS" | bc)
140+ else
141+ COVERAGE="0"
142+ fi
143+
144+ echo "current_coverage=$COVERAGE" >> $GITHUB_OUTPUT
145+ echo "Current code coverage: $COVERAGE% (from coverage.xml)"
146+ echo "Statements: $COVERED_STATEMENTS / $STATEMENTS"
147+ else
148+ echo "ERROR: coverage.xml not found!"
149+ echo "current_coverage=0" >> $GITHUB_OUTPUT
150+ exit 1
151+ fi
122152
123- # Extract overall coverage from the text report (the summary line, not per-class lines)
124- # Look for the line that starts with " Lines:" (two spaces at the start)
125- COVERAGE=$(grep "^ Lines:" current-coverage-full.txt | tail -1 | awk '{print $2}' | sed 's/%//' || echo "0")
126- echo "current_coverage=$COVERAGE" >> $GITHUB_OUTPUT
127- echo "Current code coverage: $COVERAGE%"
153+ # Coverage text output was already extracted from phpunit-with-coverage.log in the previous step
154+ # If extraction failed, try to generate it again as fallback
155+ if [ ! -s current-coverage-full.txt ]; then
156+ echo "Warning: Could not extract coverage text from phpunit-with-coverage.log, generating separately..."
157+ vendor/bin/phpunit --configuration phpunit.xml.dist --coverage-text --colors=never > current-coverage-full.txt 2>&1 || true
158+ fi
128159
129160 # Save detailed per-file coverage for later comparison
130161 # PHPUnit outputs class name on one line, stats on the next line
@@ -162,12 +193,34 @@ jobs:
162193 if : github.event_name == 'pull_request'
163194 id : base_coverage
164195 run : |
165- # Generate coverage for base branch
166- vendor/bin/phpunit --coverage-text --colors=never > base-coverage-full.txt 2>&1 || true
167- # Extract overall coverage (summary line starting with " Lines:")
168- BASE_COVERAGE=$(grep "^ Lines:" base-coverage-full.txt | tail -1 | awk '{print $2}' | sed 's/%//' || echo "0")
169- echo "base_coverage=$BASE_COVERAGE" >> $GITHUB_OUTPUT
170- echo "Base branch code coverage: $BASE_COVERAGE%"
196+ # Generate coverage for base branch (including coverage.xml)
197+ vendor/bin/phpunit --configuration phpunit.xml.dist \
198+ --coverage-clover=base-coverage.xml \
199+ --coverage-text --colors=never > base-coverage-full.txt 2>&1 || true
200+
201+ # Extract overall coverage from base-coverage.xml (Clover format)
202+ if [ -f base-coverage.xml ]; then
203+ # Extract metrics from Clover XML using xmllint
204+ # Fallback to Python if xmllint fails
205+ STATEMENTS=$(xmllint --xpath 'string(//project/metrics/@statements)' base-coverage.xml 2>/dev/null || python3 -c "import xml.etree.ElementTree as ET; tree = ET.parse('base-coverage.xml'); metrics = tree.find('.//project/metrics'); print(metrics.get('statements', '0') if metrics is not None else '0')" 2>/dev/null || echo "0")
206+ COVERED_STATEMENTS=$(xmllint --xpath 'string(//project/metrics/@coveredstatements)' base-coverage.xml 2>/dev/null || python3 -c "import xml.etree.ElementTree as ET; tree = ET.parse('base-coverage.xml'); metrics = tree.find('.//project/metrics'); print(metrics.get('coveredstatements', '0') if metrics is not None else '0')" 2>/dev/null || echo "0")
207+
208+ # Calculate coverage percentage
209+ if [ "$STATEMENTS" != "0" ] && [ -n "$STATEMENTS" ] && [ -n "$COVERED_STATEMENTS" ]; then
210+ BASE_COVERAGE=$(echo "scale=2; ($COVERED_STATEMENTS * 100) / $STATEMENTS" | bc)
211+ else
212+ BASE_COVERAGE="0"
213+ fi
214+
215+ echo "base_coverage=$BASE_COVERAGE" >> $GITHUB_OUTPUT
216+ echo "Base branch code coverage: $BASE_COVERAGE% (from base-coverage.xml)"
217+ echo "Statements: $COVERED_STATEMENTS / $STATEMENTS"
218+ else
219+ # Fallback to text extraction if XML not available
220+ BASE_COVERAGE=$(grep "^ Lines:" base-coverage-full.txt | tail -1 | awk '{print $2}' | sed 's/%//' || echo "0")
221+ echo "base_coverage=$BASE_COVERAGE" >> $GITHUB_OUTPUT
222+ echo "Base branch code coverage: $BASE_COVERAGE% (from text fallback)"
223+ fi
171224
172225 # Extract per-file coverage for comparison
173226 # PHPUnit outputs class name on one line, stats on the next line
0 commit comments