This is an automated email from the ASF dual-hosted git repository.
dsoumis pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new f5d35f3f8e Add full-suite file-level coverage based on the Apache
nightly produced jacoco.xml
f5d35f3f8e is described below
commit f5d35f3f8e1e3cc56587da48266af06eca6efd9e
Author: Dimitris Soumis <[email protected]>
AuthorDate: Wed Oct 21 14:48:49 2026 +0300
Add full-suite file-level coverage based on the Apache nightly produced
jacoco.xml
---
.github/scripts/coverage-file-level.py | 88 +++++++++++++++++++++++++++++
.github/scripts/coverage-file-level.test.sh | 83 +++++++++++++++++++++++++++
.github/scripts/select-tests.sh | 4 ++
.github/scripts/select-tests.test.sh | 20 +++++++
.github/workflows/coverage.yml | 39 +++++++++++--
5 files changed, 230 insertions(+), 4 deletions(-)
diff --git a/.github/scripts/coverage-file-level.py
b/.github/scripts/coverage-file-level.py
new file mode 100755
index 0000000000..1ac177b82a
--- /dev/null
+++ b/.github/scripts/coverage-file-level.py
@@ -0,0 +1,88 @@
+#!/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.
+#
+# Reads a JaCoCo XML report (argv[1]) and a list of changed repo-relative paths
+# on stdin, and prints a Markdown table of per-file LINE coverage for the
changed
+# java/ source files. Files present in the report show covered/total and a
+# round-half-up percentage; files with no executable lines (interfaces, marker
+# types) are labelled "no executable lines"; files absent from the report (new
+# files, or never exercised) show "not in report". Non-java/ or non-.java paths
+# are ignored. Prints nothing if the report cannot be parsed or no changed file
+# qualifies, so a bad/empty nightly report degrades gracefully and never fails
+# the PR check.
+#
+# Usage: coverage-file-level.py <jacoco.xml> < changed-files.txt
+import sys
+import xml.etree.ElementTree as ET
+
+# Sentinel: a source file present in the report but with no executable lines
+# (e.g. an interface or annotation type) -- distinct from "absent from report".
+NO_EXECUTABLE_LINES = object()
+
+
+def line_coverage(xml_path):
+ """Map repo path -> (covered, total), or NO_EXECUTABLE_LINES for files with
+ no executable lines. Returns None if the report cannot be parsed."""
+ try:
+ root = ET.parse(xml_path).getroot()
+ except (ET.ParseError, OSError) as exc:
+ sys.stderr.write(
+ "coverage-file-level: could not parse {}: {}\n".format(xml_path,
exc))
+ return None
+ result = {}
+ for package in root.iter("package"):
+ pkg = package.get("name")
+ for sf in package.findall("sourcefile"):
+ repo_path = "java/{}/{}".format(pkg, sf.get("name"))
+ line = sf.find("counter[@type='LINE']")
+ if line is None:
+ result[repo_path] = NO_EXECUTABLE_LINES
+ continue
+ covered = int(line.get("covered"))
+ total = covered + int(line.get("missed"))
+ result[repo_path] = (covered, total) if total else
NO_EXECUTABLE_LINES
+ return result
+
+
+def main():
+ xml_path = sys.argv[1]
+ changed = [ln.strip() for ln in sys.stdin if ln.strip()]
+ cov = line_coverage(xml_path)
+ if cov is None:
+ return # unparseable report: emit nothing, never fail the step
+
+ rows = []
+ for path in changed:
+ if not (path.startswith("java/") and path.endswith(".java")):
+ continue
+ if path not in cov:
+ rows.append("| `{}` | — | not in report |".format(path))
+ elif cov[path] is NO_EXECUTABLE_LINES:
+ rows.append("| `{}` | — | no executable lines |".format(path))
+ else:
+ covered, total = cov[path]
+ pct = int(100.0 * covered / total + 0.5) # round half up
+ rows.append("| `{}` | {}/{} | {}% |".format(path, covered, total,
pct))
+
+ if not rows:
+ return
+ print("| File | Lines covered | Full-suite coverage |")
+ print("| --- | --- | --- |")
+ print("\n".join(rows))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/.github/scripts/coverage-file-level.test.sh
b/.github/scripts/coverage-file-level.test.sh
new file mode 100644
index 0000000000..58f9e66792
--- /dev/null
+++ b/.github/scripts/coverage-file-level.test.sh
@@ -0,0 +1,83 @@
+#!/usr/bin/env bash
+
+# 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.
+
+set -euo pipefail
+
+script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+script="${script_dir}/coverage-file-level.py"
+
+fixture="$(mktemp -d)"
+trap 'rm -rf "$fixture"' EXIT
+
+cat > "$fixture/jacoco.xml" <<'XML'
+<?xml version="1.0" encoding="UTF-8"?>
+<report name="Test">
+ <package name="org/apache/tomcat/util/http/parser">
+ <sourcefile name="MediaType.java">
+ <counter type="LINE" missed="3" covered="9"/>
+ </sourcefile>
+ <sourcefile name="HttpParser.java">
+ <counter type="LINE" missed="0" covered="10"/>
+ </sourcefile>
+ <sourcefile name="Rounding.java">
+ <counter type="LINE" missed="3" covered="5"/>
+ </sourcefile>
+ </package>
+ <package name="org/apache/tomcat">
+ <sourcefile name="Marker.java">
+ <counter type="CLASS" missed="0" covered="1"/>
+ </sourcefile>
+ </package>
+</report>
+XML
+
+out="$(printf '%s\n' \
+ 'java/org/apache/tomcat/util/http/parser/MediaType.java' \
+ 'java/org/apache/tomcat/util/http/parser/Rounding.java' \
+ 'java/org/apache/tomcat/Marker.java' \
+ 'java/org/apache/catalina/Newbie.java' \
+ 'docs/foo.xml' \
+ | python3 "$script" "$fixture/jacoco.xml")"
+
+echo "--- output ---"; echo "$out"; echo "--------------"
+
+fail=0
+check() { if echo "$out" | grep -qF "$1"; then echo "ok - $2"; else echo
"FAIL - $2"; fail=1; fi }
+reject() { if echo "$out" | grep -qF "$1"; then echo "FAIL - $2"; fail=1; else
echo "ok - $2"; fi }
+
+check '9/12 | 75%' "MediaType computed as 9/12 = 75%"
+check '5/8 | 63%' "Rounding uses round-half-up (5/8 -> 63%, not
62%)"
+check 'no executable lines' "sourcefile with no LINE counter labelled
distinctly"
+check 'not in report' "file absent from report is flagged"
+check 'Newbie.java' "absent file still listed"
+reject 'docs/foo.xml' "non-java path ignored"
+reject 'HttpParser.java' "unchanged file not listed"
+
+# A malformed / truncated report must degrade gracefully: no crash, no table,
exit 0
+# (the feature's guarantee is "report-only, never fail the PR").
+printf '%s' '<report><package name="org/apache/foo"><sourcefile name="Bar.jav'
> "$fixture/bad.xml"
+if bad_out="$(printf 'java/org/apache/foo/Bar.java\n' | python3 "$script"
"$fixture/bad.xml" 2>/dev/null)"; then
+ if [ -z "$bad_out" ]; then
+ echo "ok - malformed XML degrades gracefully (empty output, exit 0)"
+ else
+ echo "FAIL - malformed XML should produce no table"; fail=1
+ fi
+else
+ echo "FAIL - malformed XML must not crash the step (expected exit 0)"; fail=1
+fi
+
+exit "$fail"
diff --git a/.github/scripts/select-tests.sh b/.github/scripts/select-tests.sh
index 78691bd0a4..0705218a5d 100755
--- a/.github/scripts/select-tests.sh
+++ b/.github/scripts/select-tests.sh
@@ -36,6 +36,10 @@ while IFS= read -r path; do
*) continue ;;
esac
+ case "$path" in
+ *[!A-Za-z0-9/_.-]*) continue ;;
+ esac
+
# Package dir: strip the leading java/ and the trailing /<Class>.java.
pkg="$(dirname "${path#java/}")"
[ "$pkg" = "." ] && continue
diff --git a/.github/scripts/select-tests.test.sh
b/.github/scripts/select-tests.test.sh
index 3dde33164c..562a4cbaea 100644
--- a/.github/scripts/select-tests.test.sh
+++ b/.github/scripts/select-tests.test.sh
@@ -15,6 +15,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+# Hermetic tests for select-tests.sh using a temporary fixture tree.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -23,9 +24,16 @@ script="${script_dir}/select-tests.sh"
fixture="$(mktemp -d)"
trap 'rm -rf "$fixture"' EXIT
+# Mirror test packages for the happy-path and multi-package cases.
mkdir -p "$fixture/test/org/apache/tomcat/util/http/parser"
touch "$fixture/test/org/apache/tomcat/util/http/parser/TestHttpParser.java"
touch "$fixture/test/org/apache/tomcat/util/http/parser/TestMediaType.java"
+mkdir -p "$fixture/test/org/apache/catalina/connector"
+touch "$fixture/test/org/apache/catalina/connector/TestRequest.java"
+mkdir -p "$fixture/test/org/apache/tomcat/util/buf"
+touch "$fixture/test/org/apache/tomcat/util/buf/TestByteChunk.java"
+mkdir -p "$fixture/test/org/apache/dum;my"
+touch "$fixture/test/org/apache/dum;my/TestX.java"
run() { SELECT_TESTS_ROOT="$fixture" bash "$script"; }
@@ -63,4 +71,16 @@ assert_eq "ignores non-java-source paths" "" "$out"
out="$(printf '' | run)"
assert_eq "empty input yields empty output" "" "$out"
+# 6. Multiple distinct packages -> sorted, comma-joined.
+out="$(printf '%s\n' \
+ 'java/org/apache/catalina/connector/Request.java' \
+ 'java/org/apache/tomcat/util/buf/ByteChunk.java' \
+ 'java/org/apache/tomcat/util/http/parser/MediaType.java' | run)"
+assert_eq "joins multiple packages sorted and comma-separated" \
+
"org/apache/catalina/connector/Test*.java,org/apache/tomcat/util/buf/Test*.java,org/apache/tomcat/util/http/parser/Test*.java"
"$out"
+
+# 7. Paths with unsafe characters are rejected even when a mirror exists
+out="$(printf '%s\n' 'java/org/apache/dum;my/X.java' | run)"
+assert_eq "rejects paths with unsafe characters" "" "$out"
+
exit "$fail"
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index d839843b71..980721eb12 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -62,7 +62,11 @@ jobs:
run: |
includes="$(git diff --name-only --diff-filter=d "${{
steps.base.outputs.ref }}"...HEAD \
| bash .github/scripts/select-tests.sh)"
- echo "includes=$includes" >> "$GITHUB_OUTPUT"
+ {
+ echo "includes<<SELECT_TESTS_EOF"
+ echo "$includes"
+ echo "SELECT_TESTS_EOF"
+ } >> "$GITHUB_OUTPUT"
if [ -z "$includes" ]; then
echo "No targeted tests for the changed files; skipping coverage."
>> "$GITHUB_STEP_SUMMARY"
else
@@ -71,12 +75,13 @@ jobs:
- name: Run targeted tests with coverage
if: steps.select.outputs.includes != ''
+ env:
+ INCLUDES: ${{ steps.select.outputs.includes }}
+ ANT_OPTS: -Dtest.openssl.exists=false -Dtest.excludePerformance=true
run: |
ant -noinput deploy test-nio coverage-report \
-Dtest.coverage=true \
- -Dtest.name="${{ steps.select.outputs.includes }}"
- env:
- ANT_OPTS: -Dtest.openssl.exists=false -Dtest.excludePerformance=true
+ -Dtest.name="$INCLUDES"
- name: Report diff coverage
if: steps.select.outputs.includes != ''
@@ -89,3 +94,29 @@ jobs:
echo '## Diff coverage (targeted tests)'
cat diff-cover.md
} >> "$GITHUB_STEP_SUMMARY"
+
+ - name: Full-suite coverage of changed files
+ if: always()
+ run: |
+ # Only fetch the (large) nightly report if the PR changed java/
source.
+ changed="$(git diff --name-only --diff-filter=d "${{
steps.base.outputs.ref }}"...HEAD)"
+ java_changed="$(printf '%s\n' "$changed" | grep -E '^java/.*\.java$'
|| true)"
+ if [ -z "$java_changed" ]; then
+ exit 0
+ fi
+
nightly_url="https://nightlies.apache.org/tomcat/tomcat-12.0.x/coverage/jacoco.xml"
+ if ! curl -fsS --connect-timeout 10 --max-time 60 --retry 2
"$nightly_url" -o nightly-jacoco.xml; then
+ echo "No nightly full-suite coverage data available."
+ echo "_No nightly full-suite coverage data available._" >>
"$GITHUB_STEP_SUMMARY"
+ exit 0
+ fi
+ table="$(printf '%s\n' "$java_changed" | python3
.github/scripts/coverage-file-level.py nightly-jacoco.xml)"
+ if [ -n "$table" ]; then
+ # Print to the step log (visible when the step is opened) and the
run Summary.
+ echo 'Full-suite coverage of changed files (overall, not your
diff):'
+ printf '%s\n' "$table"
+ {
+ echo '## Full-suite coverage of changed files (overall, not your
diff)'
+ echo "$table"
+ } >> "$GITHUB_STEP_SUMMARY"
+ fi
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]