Merge pull request #1553 from apache/add-code-coverage
Add JaCoCo coverage measurement and Codecov reporting
diff --git a/.github/scripts/check-coverage-report.py b/.github/scripts/check-coverage-report.py
new file mode 100755
index 0000000..6c3e343
--- /dev/null
+++ b/.github/scripts/check-coverage-report.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Sanity-check the aggregated JaCoCo report before it is published.
+
+Most Wicket tests live in a module other than the code they exercise, so the
+report depends on the dependency scopes declared in wicket-coverage/pom.xml:
+compile contributes classes, test contributes execution data only. If someone
+overrides maven-surefire-plugin's <argLine> without keeping the
+@{jacoco.argLine} placeholder, or changes those scopes, coverage silently drops
+to zero instead of failing the build. These assertions are that tripwire.
+
+This deliberately checks structure, not a coverage percentage. It is not a
+quality gate: it only fails when the measurement itself is broken.
+"""
+import sys
+import xml.etree.ElementTree as ET
+
+REPORT = 'wicket-coverage/target/site/jacoco-aggregate/jacoco.xml'
+
+# Every module listed at compile scope in wicket-coverage/pom.xml.
+EXPECTED_MODULES = {
+ 'wicket-auth-roles', 'wicket-bean-validation', 'wicket-cdi', 'wicket-core',
+ 'wicket-devutils', 'wicket-extensions', 'wicket-extensions-tester',
+ 'wicket-guice', 'wicket-ioc', 'wicket-jmx', 'wicket-native-websocket-core',
+ 'wicket-native-websocket-javax', 'wicket-native-websocket-tester',
+ 'wicket-request', 'wicket-spring', 'wicket-tester', 'wicket-util',
+ 'wicket-velocity',
+}
+
+# Modules whose tests live elsewhere. Zero here means cross-module attribution
+# has broken, which is the failure this script exists to catch.
+MUST_BE_COVERED = ('wicket-core', 'wicket-tester', 'wicket-cdi')
+
+
+def instructions(group):
+ for counter in group.findall('counter'):
+ if counter.get('type') == 'INSTRUCTION':
+ return int(counter.get('missed')), int(counter.get('covered'))
+ return 0, 0
+
+
+def main():
+ try:
+ root = ET.parse(REPORT).getroot()
+ except (OSError, ET.ParseError) as e:
+ sys.exit('cannot read %s: %s' % (REPORT, e))
+
+ groups = {g.get('name'): g for g in root.findall('group')}
+ failures = []
+
+ for name in sorted(groups):
+ missed, covered = instructions(groups[name])
+ total = missed + covered
+ pct = (100.0 * covered / total) if total else 0.0
+ print('%-34s %8d / %8d instructions (%5.1f%%)' % (name, covered, total, pct))
+ print()
+
+ missing = EXPECTED_MODULES - set(groups)
+ extra = set(groups) - EXPECTED_MODULES
+ if missing:
+ failures.append('missing from the report: %s' % ', '.join(sorted(missing)))
+ if extra:
+ failures.append('unexpectedly present: %s -- update EXPECTED_MODULES here and '
+ 'the dependency list in wicket-coverage/pom.xml together'
+ % ', '.join(sorted(extra)))
+
+ for name in MUST_BE_COVERED:
+ if name in groups and instructions(groups[name])[1] == 0:
+ failures.append('%s has zero coverage: its tests live in another module, so '
+ 'this means the JaCoCo agent did not attach or a dependency '
+ 'scope in wicket-coverage/pom.xml is wrong' % name)
+
+ if failures:
+ for f in failures:
+ print('FAIL: %s' % f, file=sys.stderr)
+ return 1
+ print('OK: %d modules reported, cross-module attribution intact' % len(groups))
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 27c8891..015890e 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -53,4 +53,28 @@
- name: Build with Maven
run: |
java -version
- ./apache-maven-${MAVEN_VERSION}/bin/mvn --show-version clean verify -Pjs-test
+ ./apache-maven-${MAVEN_VERSION}/bin/mvn --show-version \
+ clean verify -Pjs-test \
+ ${{ matrix.java == '21' && '-Pcoverage' || '' }}
+
+ # Fails if the measurement itself has broken - for instance if a future
+ # <argLine> override drops the @{jacoco.argLine} placeholder, which would
+ # silently zero a module's coverage rather than failing the build.
+ - name: Check coverage report
+ if: matrix.java == '21'
+ run: python3 .github/scripts/check-coverage-report.py
+
+ # Coverage is measured on one JDK only: a single number is all that is needed,
+ # and this leaves the other legs' timings untouched. The aggregated report is
+ # produced by the wicket-coverage module; see the 'coverage' profile in pom.xml.
+ - name: Upload coverage to Codecov
+ # v7.0.0 - external actions must be pinned to a commit SHA:
+ # https://infra.apache.org/github-actions-policy.html
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f
+ if: matrix.java == '21' && github.repository_owner == 'apache'
+ with:
+ files: ./wicket-coverage/target/site/jacoco-aggregate/jacoco.xml
+ # Empty for pull requests from forks, which upload tokenlessly. Do NOT switch
+ # to pull_request_target to get at the secret; ASF policy forbids it.
+ token: ${{ secrets.CODECOV_TOKEN }}
+ disable_search: true
diff --git a/README.md b/README.md
index b6b89b1..fb2234a 100644
--- a/README.md
+++ b/README.md
@@ -200,6 +200,29 @@
creates wicket-(subproject)-x.y.z.jar(s) in according target subdirectories and
installs the jar files into your local Maven repository for use in other projects.
+Code coverage
+-------------
+
+Coverage is measured with JaCoCo and aggregated into a single report, because most
+Wicket tests live in a module other than the code they exercise (the tests for
+wicket-core are in wicket-core-tests, for example). To produce it:
+
+ - mvn clean verify -Pcoverage
+
+ writes the aggregated report to
+ wicket-coverage/target/site/jacoco-aggregate/index.html
+
+To build only the modules that feed the report, which is considerably faster:
+
+ - mvn clean verify -Pcoverage -pl wicket-coverage -am
+
+Note that a build without "clean" merges the previous run's data into the new report,
+because the JaCoCo agent appends by default.
+
+Every push and pull request also uploads this report to
+https://app.codecov.io/gh/apache/wicket. Coverage is reported there, never enforced:
+no coverage check can fail a build or block a merge.
+
Migrating from 9.x
------------------
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000..a5b8b91
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Coverage is reported, never enforced. 'informational' keeps the project and patch
+# checks visible on a pull request, showing the real numbers, while making them
+# incapable of failing a build or blocking a merge.
+coverage:
+ status:
+ project:
+ default:
+ informational: true
+ patch:
+ default:
+ informational: true
+
+comment:
+ layout: "condensed_header, diff, flags, components"
+ # Stay quiet on pull requests that do not move coverage, which is most of them.
+ require_changes: true
diff --git a/pom.xml b/pom.xml
index 9c5e0f5..d162101 100644
--- a/pom.xml
+++ b/pom.xml
@@ -124,6 +124,8 @@
<module>wicket-migration</module>
<module>wicket-tester</module>
<module>wicket-extensions-tester</module>
+ <!-- must stay last: aggregates the JaCoCo data of every module above -->
+ <module>wicket-coverage</module>
</modules>
<properties>
<!-- Encoding -->
@@ -132,6 +134,17 @@
<project.build.outputTimestamp>2026-01-31T19:00:35Z</project.build.outputTimestamp>
+ <!--
+ Late-bound placeholder for the JaCoCo agent's -javaagent argument, consumed
+ by maven-surefire-plugin's <argLine> as @{jacoco.argLine}.
+
+ This property MUST stay declared, even though it is empty: surefire only
+ substitutes @{x} for properties that actually exist, otherwise it passes the
+ literal "@{x}" to the forked JVM and every test module fails with
+ "Unrecognized option". jacoco:prepare-agent overwrites the value; see
+ <propertyName> in the 'coverage' profile.
+ -->
+ <jacoco.argLine />
<javadoc.additionalJOption />
<javadoc.jdk.apidocs.link>https://docs.oracle.com/en/java/javase/${java.specification.version}/docs/api/</javadoc.jdk.apidocs.link>
@@ -1123,7 +1136,14 @@
<includes>
<include>**/*Test.java</include>
</includes>
- <argLine>--add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED
+ <!--
+ @{jacoco.argLine} is replaced by surefire at fork time, and is empty
+ unless -Pcoverage is active. Any future override of <argLine> (in a
+ profile or a module) MUST keep this placeholder, or coverage for that
+ module silently drops to zero.
+ -->
+ <argLine>@{jacoco.argLine}
+ --add-opens java.base/jdk.internal.loader=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED
--add-modules=ALL-SYSTEM</argLine>
<useModulePath>false</useModulePath>
@@ -1254,6 +1274,11 @@
</configuration>
</plugin>
<plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <version>${jacoco.version}</version>
+ </plugin>
+ <plugin>
<groupId>org.primefaces.extensions</groupId>
<artifactId>resources-optimizer-maven-plugin</artifactId>
<version>${resources-optimizer-maven-plugin.version}</version>
@@ -1357,30 +1382,37 @@
</profile>
<profile>
+ <!--
+ Attaches the JaCoCo agent to every surefire fork by setting the
+ 'jacoco.argLine' property, which surefire's <argLine> consumes as
+ @{jacoco.argLine}.
+
+ The aggregated report is produced by the wicket-coverage module, which
+ declares a profile with this same id. Deliberately NO per-module 'report'
+ goal: most tests live in a module other than the code they exercise
+ (wicket-core has no tests of its own, wicket-core-tests has ~500), so
+ per-module reports would show 0% for wicket-core.
+
+ Usage: mvn clean verify -Pcoverage
+ Result: wicket-coverage/target/site/jacoco-aggregate/{index.html,jacoco.xml}
+ -->
<id>coverage</id>
<build>
<plugins>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
- <version>${jacoco.version}</version>
-
<executions>
<execution>
<id>jacoco-initialize</id>
<goals>
<goal>prepare-agent</goal>
</goals>
- </execution>
- <execution>
- <id>jacoco-site</id>
- <phase>package</phase>
- <goals>
- <goal>report</goal>
- </goals>
+ <configuration>
+ <propertyName>jacoco.argLine</propertyName>
+ </configuration>
</execution>
</executions>
-
</plugin>
</plugins>
</build>
diff --git a/wicket-coverage/pom.xml b/wicket-coverage/pom.xml
new file mode 100644
index 0000000..bac3a56
--- /dev/null
+++ b/wicket-coverage/pom.xml
@@ -0,0 +1,209 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-parent</artifactId>
+ <version>11.0.0-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <artifactId>wicket-coverage</artifactId>
+ <packaging>pom</packaging>
+ <name>Wicket Code Coverage</name>
+ <description>
+ Build-only module that aggregates the JaCoCo execution data of all Wicket
+ modules into a single report. Not released, and does nothing unless the
+ 'coverage' profile is active.
+
+ The dependency list below IS the configuration. jacoco:report-aggregate reads
+ the scope of each dependency:
+ compile -> the module's classes and sources appear in the report, and its
+ own target/jacoco.exec is read;
+ test -> only the module's target/jacoco.exec is read. Used for the
+ test-only modules that hold the tests of a *different* module.
+ </description>
+ <properties>
+ <japicmp.skip>true</japicmp.skip>
+ <maven.deploy.skip>true</maven.deploy.skip> <!-- this module is not released -->
+ </properties>
+ <dependencies>
+ <!--
+ compile scope: classes + sources + own execution data are reported.
+ Scopes are stated explicitly on purpose, because the whole design hinges
+ on Dependency.getScope().
+ -->
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-auth-roles</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-bean-validation</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-cdi</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-core</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-devutils</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-extensions</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-extensions-tester</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-guice</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-ioc</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-jmx</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-native-websocket-core</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-native-websocket-javax</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-native-websocket-tester</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-request</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-spring</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <!-- managed to 'test' in wicket-parent; forced to compile so that its classes are reported -->
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-tester</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-util</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-velocity</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ <!--
+ test scope: execution data only, no classes or sources. These modules have
+ no main sources at all; they hold the tests for the modules above.
+ wicket-core-tests -> the tests of wicket-core AND wicket-tester
+ wicket-cdi-tests -> the only tests wicket-cdi has
+
+ Deliberately omitted, and why:
+ wicket, wicket-experimental, wicket-native-websocket
+ aggregator POMs, no classes
+ wicket-examples demo webapp, not a released library; including it
+ would swamp the framework signal and drag jetty,
+ weld and httpunit into this POM's compile graph.
+ To let its tests count towards wicket-core, add it
+ here as <scope>test</scope><type>war</type> and
+ re-check dependency convergence.
+ wicket-user-guide no Java sources (asciidoctor documentation)
+ archetypes/quickstart no Java sources (packaging=maven-archetype)
+ wicket-migration no main Java sources (OpenRewrite recipes)
+ wicket-metrics no tests, so it would only contribute a 0% bundle
+ wicket-objectsizeof-agent a -javaagent JAR, not a classpath artifact
+ wicket-common-tests its single test only opens jar files off the
+ classpath; no meaningful coverage
+ wicket-js-tests surefire is skipped there, so no jacoco.exec exists
+ -->
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-cdi-tests</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.wicket</groupId>
+ <artifactId>wicket-core-tests</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <profiles>
+ <profile>
+ <!--
+ Same id as the profile in wicket-parent that attaches the agent, so a
+ single -Pcoverage switches on both halves of the feature.
+
+ Gated on the profile rather than unconditional on purpose: with no
+ execution data present, report-aggregate still emits a well-formed 0%
+ jacoco.xml, which would be a trap for anything consuming that path.
+ -->
+ <id>coverage</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.jacoco</groupId>
+ <artifactId>jacoco-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <!-- report-aggregate has no default phase; binding is mandatory -->
+ <id>jacoco-aggregate-report</id>
+ <phase>verify</phase>
+ <goals>
+ <goal>report-aggregate</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>