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]

Reply via email to