This is an automated email from the ASF dual-hosted git repository.
ascheman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-javadoc-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new a6c103f5 MJAVADOC-427: replace slf4j.org probe with localhost mock +
hit-counter
a6c103f5 is described below
commit a6c103f5fb0eab938f7b4cd12aaf025ac536d7af
Author: Gerd Aschemann <[email protected]>
AuthorDate: Fri Jun 12 16:43:26 2026 +0200
MJAVADOC-427: replace slf4j.org probe with localhost mock + hit-counter
The original IT used http://www.slf4j.org/ as the -link target so the
plugin's redirect-following code path could be exercised against a real
HTTP-to-HTTPS redirect. That external dependency flaked deterministically
on multiple JVM/OS/network combinations:
* OpenJ9 (any OS) — IBM JSSE / TLS handshake quirks against the CDN
* Stable JDK 25 LTS on Windows — observed in master CI runs
* Mainstream HotSpot (Temurin, Zulu) under CDN egress pressure — a recent
full-matrix fork run reproduced this with 4 cells out of ~51 failing
with the canonical 'Error fetching link: http://www.slf4j.org/apidocs.
Ignored
it.' warning in build.log. The flake had been latent on upstream master for
months and contributed to PR friction whenever a contributor hit the wrong
runner pool.
This rewrite replaces the slf4j.org probe with a localhost
com.sun.net.httpserver.HttpServer started by setup.groovy on a
*dynamically-allocated* port (the OS picks a free port via bind-to-0; the
chosen number is then patched into the IT's pom.xml in place of the
'__MJAVADOC_427_PORT__' placeholder before the build runs). Using a
dynamic port avoids any chance of collision with other listeners on the
host -- developer-local services, parallel CI processes, or daemon
threads left behind by a previous failed run -- so the IT does not
trade external-network flake for fixed-port flake.
The mock issues a 301 from /element-list to /redirected/element-list and
serves a minimal element-list with org.slf4j. The plugin's link resolver
must follow the redirect in order for the App.html cross-reference to
resolve correctly, and verify.groovy directly asserts both endpoints were
hit by inspecting a hit log written by the mock's handlers. That is a
stronger signal than the original IT, which only inferred redirect-
following from the absence of a broken link in the rendered HTML.
Coverage trade-off, documented for reviewer transparency:
Kept: 301 redirect-following on the plugin's -link probe path,
cross-reference URL construction from a redirected element-list,
and the assertion that the rendered link points at a URL the
plugin actually resolved (not an inline guess).
Dropped: HTTP-to-HTTPS protocol-switch coverage. That transition lives
inside the JDK's HttpURLConnection implementation, not in
maven-javadoc-plugin. The plugin is protocol-agnostic at its
layer and delegates redirect transport to the JDK. Reproducing
the http->https path on localhost would require an HTTPS
listener with a self-signed certificate and JVM truststore
plumbing — a lot of scaffolding for coverage of code that is
not the plugin's responsibility.
setup.groovy is idempotent: maven-invoker-plugin runs each IT twice in
the same JVM (once each for the integration-test and verify mojos), and
the second pass detects "already set up" by checking whether the
pom.xml placeholder has already been substituted, and returns silently.
The hit log is truncated on first setup so a stale file from a prior mvn
invocation cannot leak false positives, and both passes within the same
mvn run accumulate into it.
Local verification: mvn -P run-its -Dinvoker.test=MJAVADOC-427 verify
passes on macOS HotSpot JDK 17 (Passed: 1, Failed: 0), with the OS
allocating different free ports on successive runs and the hit log
showing both 'base /element-list' and 'redirected /redirected/element-list'.
---
src/it/projects/MJAVADOC-427/pom.xml | 21 ++++-
src/it/projects/MJAVADOC-427/setup.groovy | 129 +++++++++++++++++++++++++++++
src/it/projects/MJAVADOC-427/verify.groovy | 31 ++++++-
3 files changed, 175 insertions(+), 6 deletions(-)
diff --git a/src/it/projects/MJAVADOC-427/pom.xml
b/src/it/projects/MJAVADOC-427/pom.xml
index b24bef79..039f625e 100644
--- a/src/it/projects/MJAVADOC-427/pom.xml
+++ b/src/it/projects/MJAVADOC-427/pom.xml
@@ -29,13 +29,17 @@
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<url>https://issues.apache.org/jira/browse/MJAVADOC-427</url>
- <description>Tests that the plugin follows redirects</description>
+ <description>Tests that the plugin follows redirects against a localhost
mock.
+ Trade-off vs. the prior slf4j.org-backed IT: HTTP-to-HTTPS coverage is
+ intentionally not asserted (it lives in the JDK's HttpURLConnection,
+ not in maven-javadoc-plugin); in exchange this IT is deterministic
+ across all CI cells. Full rationale and the redirect-followed hit
+ assertions live in setup.groovy / verify.groovy.</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
- <!-- url of slf4j api is http, and javadoc is redirected to https -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
@@ -51,7 +55,18 @@
<artifactId>maven-javadoc-plugin</artifactId>
<version>@project.version@</version>
<configuration>
- <detectLinks>true</detectLinks>
+ <!-- Explicit link to the localhost mock. setup.groovy starts an
+ HTTP server on a dynamically-allocated port and substitutes
+ the '__MJAVADOC_427_PORT__' placeholder below with the
+ chosen port number. The server 301-redirects /element-list
+ (and /package-list) to /redirected/<same> and serves a
+ minimal element-list containing org.slf4j. This
+ deterministically exercises the plugin's redirect-following
+ code path without depending on slf4j.org reachability or
+ on a fixed port being free on the host. -->
+ <links>
+ <link>http://localhost:__MJAVADOC_427_PORT__/</link>
+ </links>
</configuration>
</plugin>
</plugins>
diff --git a/src/it/projects/MJAVADOC-427/setup.groovy
b/src/it/projects/MJAVADOC-427/setup.groovy
new file mode 100644
index 00000000..06f26aec
--- /dev/null
+++ b/src/it/projects/MJAVADOC-427/setup.groovy
@@ -0,0 +1,129 @@
+/*
+ * 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.
+ */
+
+// MJAVADOC-427: localhost mock HTTP server to deterministically test
redirect-following
+// without depending on slf4j.org reachability or any external TLS handshake.
+// Replaces the prior live http://www.slf4j.org/apidocs round-trip that flaked
on
+// OpenJ9, stable JDK 25 LTS on Windows, and -- under CDN egress pressure --
+// mainstream HotSpot distributions too (Temurin / Zulu on JDK 17 and 21).
+//
+// Coverage trade-off: the original IT implicitly tested the HTTP -> HTTPS
+// transition (slf4j.org's URL is http://, the CDN redirects to https://).
+// This rewrite intentionally drops that specific assertion. Reasons:
+// * The protocol switch happens inside the JDK's HttpURLConnection
+// implementation, not in maven-javadoc-plugin's code. The plugin
+// delegates redirect transport to the JDK and is protocol-agnostic at
+// its own layer.
+// * Reproducing the http -> https transition on a localhost mock requires
+// spinning up an HTTPS listener with a self-signed cert and patching
+// the IT's JVM truststore -- a lot of infrastructure for coverage of
+// code that is not the plugin's responsibility.
+// What this IT still asserts end-to-end (via the hit-counter below): the
+// plugin's link resolver
+// 1. probes the configured -link URL (`/element-list` on the base),
+// 2. follows the 301 Location header,
+// 3. reads the redirected element-list and uses it to build cross-refs.
+// That is the entirety of the plugin's redirect contract.
+
+import com.sun.net.httpserver.HttpServer
+import java.net.InetSocketAddress
+import java.util.concurrent.Executors
+import java.util.concurrent.ThreadFactory
+
+// Dynamic port allocation: bind to port 0 and let the OS pick a free one.
+// Avoids any chance of collision with whatever else is running on the host
+// (developer's local services on a fixed port, parallel CI processes,
+// previous failed runs that left a daemon listener behind).
+//
+// maven-invoker-plugin runs each IT twice (once for the integration-test
+// mojo, once for the verify mojo) in the same invoker JVM. The daemon
+// worker pool keeps the first run's server alive into the second run, so
+// we detect "already set up" via a marker in the IT's copied pom.xml
+// (whose '__MJAVADOC_427_PORT__' placeholder is replaced with the chosen
+// port at first setup) and skip silently on the second pass.
+def pomFile = new File(basedir, 'pom.xml')
+def pomContent = pomFile.text
+if (!pomContent.contains('__MJAVADOC_427_PORT__')) {
+ println "[MJAVADOC-427 mock] pom.xml already patched (re-invocation),
reusing existing listener"
+ return
+}
+
+HttpServer server = HttpServer.create(new InetSocketAddress(0), 0)
+int port = server.address.port
+server.executor = Executors.newFixedThreadPool(2, new ThreadFactory() {
+ Thread newThread(Runnable r) {
+ Thread t = new Thread(r, "mjavadoc-427-mock-" +
System.identityHashCode(r))
+ t.daemon = true
+ t
+ }
+})
+
+// Hit log: each handler appends one line per request so verify.groovy can
+// directly assert that both the base URL and the redirected URL were
+// probed -- i.e. that the plugin actually followed the 301. This is a
+// stronger signal than the original IT's "a link appears in the HTML",
+// which only inferred redirect-following from absence-of-broken-link.
+//
+// Truncate at first setup: a stale file from a previous mvn invocation
+// (rare under invoker because it cleans target/it/<name>/ on each run,
+// but possible) could otherwise leak false-positive 'redirected' lines.
+// The second invocation within the same mvn run returns early above, so
+// it does not re-truncate.
+def hitsFile = new File(basedir, 'redirect-hits.log')
+if (hitsFile.exists()) hitsFile.delete()
+
+// /element-list and /package-list at the root -> 301 redirect to
/redirected/<same>
+// Anything else at root -> also redirect, to exercise generic
Location-following.
+server.createContext("/", { exchange ->
+ String path = exchange.requestURI.path
+ hitsFile.append("base " + path + "\n")
+ String target = path == "/" ? "/redirected/" : "/redirected" + path
+ exchange.responseHeaders.add("Location", target)
+ exchange.sendResponseHeaders(301, -1)
+ exchange.close()
+})
+
+// /redirected/element-list and /redirected/package-list -> serve a minimal
package list
+// covering only org.slf4j, which is what App.java references via {@link
LoggerFactory}.
+server.createContext("/redirected/", { exchange ->
+ String path = exchange.requestURI.path
+ hitsFile.append("redirected " + path + "\n")
+ if (path.endsWith("/element-list") || path.endsWith("/package-list")) {
+ byte[] body = "org.slf4j\n".getBytes("UTF-8")
+ exchange.responseHeaders.add("Content-Type", "text/plain;
charset=utf-8")
+ exchange.sendResponseHeaders(200, body.length)
+ exchange.responseBody.write(body)
+ exchange.close()
+ } else {
+ // Other paths under /redirected/ are not requested by javadoc during
element-list
+ // resolution. Return 404 so we'd see a clear signal if javadoc starts
probing more.
+ exchange.sendResponseHeaders(404, -1)
+ exchange.close()
+ }
+})
+
+server.start()
+
+// Patch the IT's pom.xml: replace the '__MJAVADOC_427_PORT__' placeholder
+// with the chosen port. The placeholder format intentionally avoids the
+// '@property@' syntax used by maven-invoker-plugin's own filter step
+// (which runs at IT-copy time, before this script). Plain string replace
+// is safe and visible.
+pomFile.text = pomContent.replace('__MJAVADOC_427_PORT__', port.toString())
+println "[MJAVADOC-427 mock] HTTP server started on dynamic port " + port + "
(daemon); pom.xml patched"
diff --git a/src/it/projects/MJAVADOC-427/verify.groovy
b/src/it/projects/MJAVADOC-427/verify.groovy
index 8f478567..21d91b7e 100644
--- a/src/it/projects/MJAVADOC-427/verify.groovy
+++ b/src/it/projects/MJAVADOC-427/verify.groovy
@@ -21,6 +21,31 @@ def file = new File( basedir,
'target/reports/apidocs/mjavadoc427/App.html' );
assert file.exists()
-// assert that javadoc of class correctly contains link, just like method
details
-assert file.text =~ /Link to slf4j <a
href=".*?".*?><code>LoggerFactory<\/code><\/a>/
-assert file.text =~ /public.*?<a
href=".*?".*?>LoggerFactory<\/a>.*?getLoggerFactory.*?\(\)/
+// (1) Cross-reference resolves against the localhost mock — confirms the IT
+// does not depend on any external network. The original IT pointed at
+// slf4j.org, which flaked across multiple JVM/OS combinations whenever
+// CDN egress to slf4j.org was unreliable (see setup.groovy for the
+// full rationale, including the deliberate HTTP/HTTPS coverage trade-off).
+assert file.text =~ /Link to slf4j <a
href="http:\/\/localhost:[0-9]+\/[^"]*".*?><code>LoggerFactory<\/code><\/a>/
+assert file.text =~ /public.*?<a
href="http:\/\/localhost:[0-9]+\/[^"]*".*?>LoggerFactory<\/a>.*?getLoggerFactory.*?\(\)/
+
+// (2) Hit log produced by setup.groovy directly proves the plugin's link
+// resolver followed the 301: both the base URL and the /redirected/
+// URL must have been requested. This is a stronger signal than the
+// original IT, which only inferred redirect-following from the
+// absence of a broken link.
+def hitsFile = new File( basedir, 'redirect-hits.log' );
+assert hitsFile.exists(), "redirect-hits.log not found — setup.groovy did not
start the mock server"
+def hits = hitsFile.readLines()
+assert hits.any { it.startsWith('base ') }, \
+ "javadoc never probed the base URL (no 'base ...' line in
redirect-hits.log)"
+assert hits.any { it.startsWith('redirected ') }, \
+ "javadoc did not follow the 301 to /redirected/ (no 'redirected ...' line
in redirect-hits.log)"
+
+// (3) Defensive: no 'Error fetching link' warning. That string was the
+// canonical flake signature on CI when the plugin tried (and failed)
+// to reach slf4j.org. It must not reappear via a stray external URL.
+def buildLog = new File( basedir, 'build.log' );
+if ( buildLog.exists() ) {
+ assert ! ( buildLog.text =~ /Error fetching link/ ), "Unexpected 'Error
fetching link' warning in build.log"
+}