This is an automated email from the ASF dual-hosted git repository.

jamesbognar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/juneau.git


The following commit(s) were added to refs/heads/master by this push:
     new 16b6f5a1a0 Fix JDK 25 test flush fragility in 
juneau-rest-client-classic; bump Micrometer to 1.15.12 (CVE-2026-40984); 
restore coverage.py --by-module
16b6f5a1a0 is described below

commit 16b6f5a1a0a7296851a6e5b12fd7c29d0da80949
Author: James Bognar <[email protected]>
AuthorDate: Wed Aug 12 12:25:19 2026 -0400

    Fix JDK 25 test flush fragility in juneau-rest-client-classic; bump 
Micrometer to 1.15.12 (CVE-2026-40984); restore coverage.py --by-module
---
 juneau-integration-tests/pom.xml                   |   4 +-
 .../classic/ResponseContent_Coverage_Test.java     |   4 +
 .../FluentResponseBodyAssertion_Test.java          |   4 +
 pom.xml                                            |   2 +-
 scripts/coverage.py                                | 148 ++++++++++++++++++++-
 5 files changed, 154 insertions(+), 8 deletions(-)

diff --git a/juneau-integration-tests/pom.xml b/juneau-integration-tests/pom.xml
index 800cec9ffd..7f64d59b7f 100644
--- a/juneau-integration-tests/pom.xml
+++ b/juneau-integration-tests/pom.xml
@@ -535,13 +535,13 @@
                <dependency>
                        <groupId>io.micrometer</groupId>
                        <artifactId>micrometer-core</artifactId>
-                       <version>1.13.6</version>
+                       <version>${micrometer.version}</version>
                        <scope>test</scope>
                </dependency>
                <dependency>
                        <groupId>io.micrometer</groupId>
                        <artifactId>micrometer-registry-prometheus</artifactId>
-                       <version>1.13.6</version>
+                       <version>${micrometer.version}</version>
                        <scope>test</scope>
                </dependency>
                <dependency>
diff --git 
a/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/ResponseContent_Coverage_Test.java
 
b/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/ResponseContent_Coverage_Test.java
index af6f2ad54d..2c51044b21 100644
--- 
a/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/ResponseContent_Coverage_Test.java
+++ 
b/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/ResponseContent_Coverage_Test.java
@@ -61,8 +61,12 @@ class ResponseContent_Coverage_Test {
                        exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
                        // Declares a larger Content-Length than the bytes 
actually written, so reading the full declared length
                        // triggers an IOException partway through -- exercises 
asString()/toString()'s catch(IOException) branch.
+                       // The explicit flush() forces the partial body onto 
the wire before close() (which itself throws
+                       // because the declared length wasn't fully written); 
without it, some JDKs (e.g. 25) tear the
+                       // connection down before anything is sent, producing 
NoHttpResponseException instead.
                        exchange.sendResponseHeaders(200, body.length + 1000);
                        exchange.getResponseBody().write(body);
+                       exchange.getResponseBody().flush();
                        exchange.close();
                });
                server.start();
diff --git 
a/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/assertion/FluentResponseBodyAssertion_Test.java
 
b/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/assertion/FluentResponseBodyAssertion_Test.java
index 5d76714fea..3b06b647bc 100644
--- 
a/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/assertion/FluentResponseBodyAssertion_Test.java
+++ 
b/juneau-rest/juneau-rest-client-classic/src/test/java/org/apache/juneau/rest/client/classic/assertion/FluentResponseBodyAssertion_Test.java
@@ -61,9 +61,13 @@ class FluentResponseBodyAssertion_Test {
                server.createContext("/broken", exchange -> {
                        // Declares far more bytes than are actually written, 
then closes the connection early so the
                        // client's read of the entity content fails with a 
connection-closed IOException mid-stream.
+                       // The explicit flush() forces the partial body onto 
the wire before close() (which itself throws
+                       // because the declared length wasn't fully written); 
without it, some JDKs (e.g. 25) tear the
+                       // connection down before anything is sent, producing 
NoHttpResponseException instead.
                        exchange.getResponseHeaders().add("Content-Type", 
"text/plain");
                        exchange.sendResponseHeaders(200, 10_000);
                        
exchange.getResponseBody().write("short".getBytes(StandardCharsets.UTF_8));
+                       exchange.getResponseBody().flush();
                        exchange.close();
                });
                server.start();
diff --git a/pom.xml b/pom.xml
index 80f6d1f130..48e0433c5e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -73,7 +73,7 @@
                
<sonar.coverage.exclusions>**/juneau-examples/**,**/juneau-petstore/**,**/*-microservice/**,**/juneau-sc-server/**,**/juneau-core/juneau-test-utils/**</sonar.coverage.exclusions>
                <jena.version>5.6.0</jena.version>
                <!-- Hoisted from owning modules so the juneau-bom + curated 
bundles can pin one coherent version source. -->
-               <micrometer.version>1.13.6</micrometer.version>
+               <micrometer.version>1.15.12</micrometer.version>
                <opentelemetry.version>1.63.0</opentelemetry.version>
                <jakarta.servlet-api.version>6.1.0</jakarta.servlet-api.version>
                
<jakarta.validation-api.version>3.0.2</jakarta.validation-api.version>
diff --git a/scripts/coverage.py b/scripts/coverage.py
index cc9130e7a5..e49c68ae9b 100755
--- a/scripts/coverage.py
+++ b/scripts/coverage.py
@@ -52,16 +52,24 @@ Arguments:
             Paths can be absolute or relative to the repo root.
 
 Options:
-    --run, -r       Re-run the owning module's tests before reporting
-                    (refreshes that module's .exec; other modules' existing
-                    execs are still merged in).
-    --branches, -b  Show only lines with missed branches (default: show all 
uncovered).
-    --help, -h      Show this help message.
+    --run, -r         Re-run the owning module's tests before reporting
+                      (refreshes that module's .exec; other modules' existing
+                      execs are still merged in).
+    --branches, -b    Show only lines with missed branches (default: show all 
uncovered).
+    --by-module       Print a per-module HTT-adjusted summary table (module, 
file
+                      count, branch %, missed branches, instruction %, missed
+                      instructions, HTT-excluded line count) sorted by branch %
+                      ascending, with a TOTAL row -- instead of reporting a 
single
+                      path. Uses whichever module target/jacoco.exec files 
already
+                      exist on disk; run the test suite first (e.g.
+                      `mvn clean test -Drat.skip=true`) if none are found.
+    --help, -h        Show this help message.
 
 Examples:
     ./scripts/coverage.py 
juneau-core/juneau-commons/src/main/java/org/apache/juneau/commons/conversion/
     ./scripts/coverage.py 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BitSetFormat.java
     ./scripts/coverage.py 
juneau-core/juneau-marshall/src/main/java/org/apache/juneau/marshall/BitSetFormat.java
 --run
+    ./scripts/coverage.py --by-module
 """
 
 from __future__ import annotations
@@ -486,6 +494,124 @@ def report(xml_path: Path, pkg_filter: str, file_filter: 
str | None, branches_on
         print()
 
 
+def index_module_sources(modules: list[Path]) -> dict[str, Path]:
+    """
+    Build a {"pkg/name/File.java": owning module Path} index across every 
given module's
+    src/main/java tree, used by by_module_report to attribute a JaCoCo 
package+sourcefile
+    entry (which carries no module identity of its own) back to its owning 
module.
+    """
+    index: dict[str, Path] = {}
+    for m in modules:
+        src = m / "src" / "main" / "java"
+        if not src.is_dir():
+            continue
+        for f in src.rglob("*.java"):
+            index[f.relative_to(src).as_posix()] = m
+    return index
+
+
+def by_module_report(xml_path: Path):  # NOSONAR python:S3776 -- Cognitive 
complexity is acceptable for XML report parsing and output formatting
+    """
+    Parse xml_path and print one HTT-adjusted coverage row per reactor module 
(module, file
+    count, branch %, missed branches, instruction %, missed instructions, 
HTT-excluded line
+    count), sorted by branch % ascending, followed by a TOTAL row. Reuses the 
same per-line
+    HTT exclusion (find_htt_lines) as the single-path report().
+    """
+    if not xml_path.exists():
+        die(f"JaCoCo report not found at {xml_path}. Run with --run to 
generate it.")
+
+    modules = discover_modules()
+    module_srcdirs = {m: m / "src" / "main" / "java" for m in modules if (m / 
"src" / "main" / "java").is_dir()}
+    owner_index = index_module_sources(list(module_srcdirs.keys()))
+
+    tree = ET.parse(xml_path)
+    root = tree.getroot()
+
+    # module -> [file count, mb, cb, mi, ci, htt-excluded line count]
+    stats: dict[Path, list[int]] = {}
+
+    for pkg in root.findall("package"):
+        pkg_name = pkg.get("name", "")
+        for sf in pkg.findall("sourcefile"):
+            fname = sf.get("name", "")
+            owner = owner_index.get(f"{pkg_name}/{fname}")
+            if owner is None:
+                continue  # not attributable to any in-scope module (e.g. 
excluded module)
+
+            mb = cb = mi = ci = 0
+            for ctr in sf.findall("counter"):
+                t = ctr.get("type")
+                m = int(ctr.get("missed", 0))
+                c = int(ctr.get("covered", 0))
+                if t == "BRANCH":
+                    mb, cb = m, c
+                elif t == "INSTRUCTION":
+                    mi, ci = m, c
+
+            htt_lines = find_htt_lines(pkg_name, fname, 
[module_srcdirs[owner]])
+            excluded = 0
+            for line in sf.findall("line"):
+                lmb = int(line.get("mb", 0))
+                lcb = int(line.get("cb", 0))
+                lmi = int(line.get("mi", 0))
+                lci = int(line.get("ci", 0))
+                if int(line.get("nr", 0)) in htt_lines and (lmb or lcb or lmi 
or lci):
+                    mb -= lmb
+                    cb -= lcb
+                    mi -= lmi
+                    ci -= lci
+                    excluded += 1
+
+            row = stats.setdefault(owner, [0, 0, 0, 0, 0, 0])
+            row[0] += 1
+            row[1] += mb
+            row[2] += cb
+            row[3] += mi
+            row[4] += ci
+            row[5] += excluded
+
+    if not stats:
+        print("No JaCoCo data found for any module.")
+        return
+
+    def branch_frac(row):
+        total = row[1] + row[2]
+        return row[2] / total if total else 1.0
+
+    def instr_frac(row):
+        total = row[3] + row[4]
+        return row[4] / total if total else 1.0
+
+    rows = sorted(
+        stats.items(),
+        key=lambda kv: (branch_frac(kv[1]), instr_frac(kv[1]), 
str(kv[0].relative_to(REPO_ROOT))),
+    )
+
+    names = [str(m.relative_to(REPO_ROOT)) for m, _ in rows] + ["TOTAL"]
+    name_w = max(len(n) for n in names)
+
+    header = f"{'Module':<{name_w}}  {'Files':>6}  {'Branch%':>7}  
{'BrMiss':>7}  {'Instr%':>7}  {'InstrMiss':>9}  {'HTT':>5}"
+    print(f"\nPer-module coverage (HTT-adjusted, sorted by branch % 
ascending):\n")
+    print(header)
+    print("-" * len(header))
+
+    total_files = total_mb = total_cb = total_mi = total_ci = total_htt = 0
+    for module, (files, mb, cb, mi, ci, htt) in rows:
+        name = str(module.relative_to(REPO_ROOT))
+        print(f"{name:<{name_w}}  {files:>6}  {pct(cb, mb + cb):>7}  {mb:>7}  
{pct(ci, mi + ci):>7}  {mi:>9}  {htt:>5}")
+        total_files += files
+        total_mb += mb
+        total_cb += cb
+        total_mi += mi
+        total_ci += ci
+        total_htt += htt
+
+    print("-" * len(header))
+    print(f"{'TOTAL':<{name_w}}  {total_files:>6}  {pct(total_cb, total_mb + 
total_cb):>7}  {total_mb:>7}  "
+          f"{pct(total_ci, total_mi + total_ci):>7}  {total_mi:>9}  
{total_htt:>5}")
+    print()
+
+
 def main():  # NOSONAR: always returns 0 by design — standard POSIX exit code 
for success
     args = sys.argv[1:]
     if not args or "--help" in args or "-h" in args:
@@ -495,17 +621,29 @@ def main():  # NOSONAR: always returns 0 by design — 
standard POSIX exit code
     path_arg = None
     do_run = False
     branches_only = False
+    by_module = False
 
     for arg in args:
         if arg in ("--run", "-r"):
             do_run = True
         elif arg in ("--branches", "-b"):
             branches_only = True
+        elif arg == "--by-module":
+            by_module = True
         elif arg.startswith("-"):
             die(f"Unknown option: {arg}")
         else:
             path_arg = arg
 
+    if by_module:
+        if do_run:
+            print("Note: --run has no effect with --by-module (it only 
refreshes a single module's "
+                  "tests). Run the full reactor test suite yourself first if 
needed, e.g. "
+                  "`mvn clean test -Drat.skip=true`.")
+        xml_path = generate_combined_report()
+        by_module_report(xml_path)
+        return 0
+
     if not path_arg:
         die("No path specified.")
 

Reply via email to