diff options
| author | Michael Peter Christen <mc@yacy.net> | 2026-07-12 00:49:49 +0200 |
|---|---|---|
| committer | Michael Peter Christen <mc@yacy.net> | 2026-07-12 00:49:49 +0200 |
| commit | b2e9142ea9a61af8c6e55a39a392136306dc0312 (patch) | |
| tree | 918e2fc6308b9ed040d8470c21a4731e9766a0a2 /test | |
| parent | bfddfe5e7ed90a80b4acf9a71a567b61b77cfcc4 (diff) | |
towards a Jetty 9 decoupling baseline for the Jetty 12 migration
Diffstat (limited to 'test')
| -rw-r--r-- | test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java | 32 | ||||
| -rw-r--r-- | test/java/net/yacy/http/HttpServerBootstrapConfigTest.java | 25 | ||||
| -rw-r--r-- | test/java/net/yacy/http/Jetty9LoggingFacadeTest.java | 16 | ||||
| -rw-r--r-- | test/java/net/yacy/http/ProxyAccessPolicyTest.java | 20 | ||||
| -rw-r--r-- | test/java/net/yacy/http/Slf4jJulBridgeTest.java | 59 | ||||
| -rwxr-xr-x | test/jetty-auth-smoke-test.sh | 66 | ||||
| -rwxr-xr-x | test/jetty-peer-start-smoke-test.sh | 49 | ||||
| -rwxr-xr-x | test/jetty-smoke-test.sh | 109 | ||||
| -rwxr-xr-x | test/jetty-solr-dependency-guard.sh | 72 | ||||
| -rw-r--r-- | test/jetty/RelocateJettyPackages.java | 65 | ||||
| -rwxr-xr-x | test/proxy-smoke-test.sh | 28 | ||||
| -rwxr-xr-x | test/remote-solr-smoke-test.sh | 30 |
12 files changed, 536 insertions, 35 deletions
diff --git a/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java b/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java new file mode 100644 index 000000000..480579150 --- /dev/null +++ b/test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java @@ -0,0 +1,32 @@ +package net.yacy.cora.federate.solr.connector; + +import org.apache.solr.client.solrj.response.QueryResponse; +import org.apache.solr.common.params.ModifiableSolrParams; + +import net.yacy.cora.federate.solr.instance.RemoteInstance; + +/** Command-line integration probe for YaCY's Apache-based remote Solr path. */ +public final class RemoteSolrSmoke { + + private RemoteSolrSmoke() { + } + + public static void main(final String[] args) throws Exception { + if (args.length != 1) { + throw new IllegalArgumentException("usage: RemoteSolrSmoke SOLR_BASE_URL"); + } + final RemoteInstance instance = new RemoteInstance( + args[0], null, "collection1", 10_000, false, Long.MAX_VALUE, false); + try { + final RemoteSolrConnector connector = new RemoteSolrConnector(instance, false); + final QueryResponse response = connector.getResponseByParams( + new ModifiableSolrParams().set("q", "*:*").set("rows", 0)); + if (response.getResults() == null) { + throw new IllegalStateException("remote Solr response has no result list"); + } + System.out.println("Remote Solr numFound=" + response.getResults().getNumFound()); + } finally { + instance.close(); + } + } +} diff --git a/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java new file mode 100644 index 000000000..7f2f71120 --- /dev/null +++ b/test/java/net/yacy/http/HttpServerBootstrapConfigTest.java @@ -0,0 +1,25 @@ +package net.yacy.http; + +import org.junit.Assert; +import org.junit.Test; + +public class HttpServerBootstrapConfigTest { + + @Test + public void testAcceptorCountIsClamped() { + Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(1)); + Assert.assertEquals(1, HttpServerBootstrapConfig.acceptorCountFor(2)); + Assert.assertEquals(2, HttpServerBootstrapConfig.acceptorCountFor(4)); + Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(8)); + Assert.assertEquals(4, HttpServerBootstrapConfig.acceptorCountFor(64)); + } + + @Test + public void testFixedConnectorLimits() { + Assert.assertEquals(16_384, HttpServerBootstrapConfig.REQUEST_HEADER_SIZE); + Assert.assertEquals(9_000L, HttpServerBootstrapConfig.CONNECTOR_IDLE_TIMEOUT_MILLIS); + Assert.assertEquals(128, HttpServerBootstrapConfig.ACCEPT_QUEUE_SIZE); + Assert.assertEquals(4_096, HttpServerBootstrapConfig.REQUEST_INFLATE_BUFFER_SIZE); + Assert.assertEquals(-1, HttpServerBootstrapConfig.MAX_FORM_CONTENT_SIZE); + } +} diff --git a/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java b/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java new file mode 100644 index 000000000..bba2b6924 --- /dev/null +++ b/test/java/net/yacy/http/Jetty9LoggingFacadeTest.java @@ -0,0 +1,16 @@ +package net.yacy.http; + +import static org.junit.Assert.assertEquals; + +import org.eclipse.jetty.util.log.Log; +import org.junit.Test; + +/** Jetty 9 baseline only; replace this test when Jetty9HttpServerImpl is removed. */ +public class Jetty9LoggingFacadeTest { + + @Test + public void jetty9UsesItsSlf4jFacade() { + assertEquals("org.eclipse.jetty.util.log.Slf4jLog", + Log.getLogger("org.eclipse.jetty.yacy.logging.test").getClass().getName()); + } +} diff --git a/test/java/net/yacy/http/ProxyAccessPolicyTest.java b/test/java/net/yacy/http/ProxyAccessPolicyTest.java new file mode 100644 index 000000000..ce67c6ce3 --- /dev/null +++ b/test/java/net/yacy/http/ProxyAccessPolicyTest.java @@ -0,0 +1,20 @@ +package net.yacy.http; + +import org.junit.Assert; +import org.junit.Test; + +public class ProxyAccessPolicyTest { + + @Test + public void testConfiguredClientPatterns() { + Assert.assertTrue(ProxyAccessPolicy.isClientAllowed("*", "198.51.100.7")); + Assert.assertTrue(ProxyAccessPolicy.isClientAllowed( + "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "127.0.0.1")); + Assert.assertTrue(ProxyAccessPolicy.isClientAllowed( + "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "192.168.2.15")); + Assert.assertFalse(ProxyAccessPolicy.isClientAllowed( + "localhost,127\\.0\\.0\\.1,192\\.168\\..*", "198.51.100.7")); + Assert.assertFalse(ProxyAccessPolicy.isClientAllowed(null, "127.0.0.1")); + Assert.assertFalse(ProxyAccessPolicy.isClientAllowed("127\\.0\\.0\\.1", null)); + } +} diff --git a/test/java/net/yacy/http/Slf4jJulBridgeTest.java b/test/java/net/yacy/http/Slf4jJulBridgeTest.java new file mode 100644 index 000000000..b1448eae6 --- /dev/null +++ b/test/java/net/yacy/http/Slf4jJulBridgeTest.java @@ -0,0 +1,59 @@ +package net.yacy.http; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; +import java.util.logging.Logger; + +import org.junit.Test; +import org.slf4j.LoggerFactory; + +/** Version-neutral contract for the public SLF4J 2 to JUL logging path. */ +public class Slf4jJulBridgeTest { + + @Test + public void publicSlf4jProviderRoutesJettyNamespaceToJul() { + assertEquals("org.slf4j.jul.JDK14LoggerFactory", + LoggerFactory.getILoggerFactory().getClass().getName()); + + final String loggerName = "org.eclipse.jetty.yacy.logging.test"; + final Logger julLogger = Logger.getLogger(loggerName); + final Level previousLevel = julLogger.getLevel(); + final boolean previousUseParentHandlers = julLogger.getUseParentHandlers(); + final AtomicReference<LogRecord> received = new AtomicReference<>(); + final Handler capture = new Handler() { + @Override + public void publish(final LogRecord record) { + received.set(record); + } + + @Override + public void flush() { + } + + @Override + public void close() { + } + }; + + try { + julLogger.setUseParentHandlers(false); + julLogger.setLevel(Level.INFO); + capture.setLevel(Level.ALL); + julLogger.addHandler(capture); + + LoggerFactory.getLogger(loggerName).info("Jetty SLF4J to JUL bridge test"); + + assertTrue("SLF4J log record did not reach java.util.logging", received.get() != null); + assertEquals("Jetty SLF4J to JUL bridge test", received.get().getMessage()); + } finally { + julLogger.removeHandler(capture); + julLogger.setLevel(previousLevel); + julLogger.setUseParentHandlers(previousUseParentHandlers); + } + } +} diff --git a/test/jetty-auth-smoke-test.sh b/test/jetty-auth-smoke-test.sh new file mode 100755 index 000000000..4970a7e47 --- /dev/null +++ b/test/jetty-auth-smoke-test.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env sh + +# Authentication contract for the embedded server. A remote URL must really +# reach the peer through a non-loopback socket; forwarded headers are not used. + +set -eu + +base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090} +remote_base_url=${YACY_SMOKE_REMOTE_BASE_URL:-} +protected_path=${YACY_SMOKE_PROTECTED_PATH:-/ConfigAccounts_p.html} +conf=${YACY_SMOKE_CONF:-DATA/SETTINGS/yacy.conf} +timeout=${YACY_SMOKE_TIMEOUT:-30} +require_complete=${YACY_SMOKE_REQUIRE_COMPLETE:-false} + +case "$require_complete" in + true|false) ;; + *) echo "FAIL: YACY_SMOKE_REQUIRE_COMPLETE must be true or false" >&2; exit 2 ;; +esac + +[ -f "$conf" ] || { echo "FAIL: missing peer configuration: $conf" >&2; exit 2; } + +status() { + curl --silent --show-error --max-time "$timeout" --output /dev/null \ + --write-out '%{http_code}' "$@" +} + +localhost_access=$(sed -n 's/^adminAccountForLocalhost=//p' "$conf" | head -n 1) +local_status=$(status "$base_url$protected_path") +if [ "$localhost_access" = "true" ]; then + [ "$local_status" = 200 ] || { echo "FAIL: localhost bypass returned $local_status" >&2; exit 1; } +else + [ "$local_status" = 401 ] || { echo "FAIL: protected localhost request returned $local_status" >&2; exit 1; } +fi +echo "ok 1 - configured localhost access rule" + +YACY_DATA_PATH=$(CDPATH= cd -- "$(dirname "$conf")/.." && pwd) +export YACY_DATA_PATH +bin/apicall.sh 'ConfigAccounts_p.html' >/dev/null +echo "ok 2 - bin/apicall.sh localhost authentication" + +if [ -n "${YACY_SMOKE_ADMIN_USER:-}" ] && [ -n "${YACY_SMOKE_ADMIN_PASSWORD:-}" ]; then + authenticated_status=$(status --user "$YACY_SMOKE_ADMIN_USER:$YACY_SMOKE_ADMIN_PASSWORD" \ + "$base_url$protected_path") + [ "$authenticated_status" = 200 ] || { echo "FAIL: administrator login returned $authenticated_status" >&2; exit 1; } + echo "ok 3 - administrator credentials" +else + if [ "$require_complete" = true ]; then + echo "FAIL: complete acceptance requires YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD" >&2 + exit 1 + fi + echo "ok 3 # SKIP - set YACY_SMOKE_ADMIN_USER and YACY_SMOKE_ADMIN_PASSWORD" +fi + +if [ -n "$remote_base_url" ]; then + remote_status=$(status "$remote_base_url$protected_path") + [ "$remote_status" = 401 ] || { echo "FAIL: remote protected request returned $remote_status" >&2; exit 1; } + echo "ok 4 - remote request does not receive localhost bypass" +else + if [ "$require_complete" = true ]; then + echo "FAIL: complete acceptance requires YACY_SMOKE_REMOTE_BASE_URL over a real non-loopback path" >&2 + exit 1 + fi + echo "ok 4 # SKIP - set YACY_SMOKE_REMOTE_BASE_URL to a real non-loopback path" +fi + +echo "PASS: embedded-server authentication contract" diff --git a/test/jetty-peer-start-smoke-test.sh b/test/jetty-peer-start-smoke-test.sh new file mode 100755 index 000000000..148eb8678 --- /dev/null +++ b/test/jetty-peer-start-smoke-test.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env sh + +# Lifecycle gate owned by an external isolated-peer harness. Commands are +# explicit because this script must never start or stop the developer's normal +# DATA directory by guessing. + +set -eu + +[ -n "${YACY_SMOKE_START_COMMAND:-}" ] && [ -n "${YACY_SMOKE_STOP_COMMAND:-}" ] || { + echo "SKIP: set YACY_SMOKE_START_COMMAND and YACY_SMOKE_STOP_COMMAND for an isolated DATA directory" >&2 + exit 2 +} + +base_url=${YACY_SMOKE_BASE_URL:-http://127.0.0.1:8090} +timeout=${YACY_SMOKE_TIMEOUT:-60} +started=false +cleanup() { + if [ "$started" = true ]; then + sh -c "$YACY_SMOKE_STOP_COMMAND" + fi +} +trap cleanup EXIT HUP INT TERM + +sh -c "$YACY_SMOKE_START_COMMAND" +started=true + +attempt=0 +while [ "$attempt" -lt "$timeout" ]; do + if curl --silent --fail --max-time 2 "$base_url/api/version.xml" >/dev/null 2>&1; then + break + fi + attempt=$((attempt + 1)) + sleep 1 +done +[ "$attempt" -lt "$timeout" ] || { echo "FAIL: isolated peer did not start" >&2; exit 1; } + +test/jetty-smoke-test.sh "$base_url" +curl --silent --show-error --fail --max-time 30 \ + "$base_url/solr/collection1/select?q=*:*&rows=0&wt=json" \ + | grep -F 'numFound' >/dev/null || { + echo "FAIL: embedded Solr query failed" >&2 + exit 1 + } +echo "ok - embedded Solr query" + +cleanup +started=false +trap - EXIT HUP INT TERM +echo "PASS: isolated peer start, embedded Solr and clean stop" diff --git a/test/jetty-smoke-test.sh b/test/jetty-smoke-test.sh index ab53a0d8c..0ff61ccc2 100755 --- a/test/jetty-smoke-test.sh +++ b/test/jetty-smoke-test.sh @@ -53,21 +53,35 @@ fail() { request() { method=$1 path=$2 - shift 2 + expected_status=$3 + shift 3 : > "$response_headers" : > "$response_body" - status=$(curl --silent --show-error \ - --max-time "$curl_timeout" \ - --request "$method" \ - --dump-header "$response_headers" \ - --output "$response_body" \ - --write-out '%{http_code}' \ - "$@" \ - "$base_url$path") || fail "$method $path could not be requested" + if [ "$method" = HEAD ]; then + status=$(curl --silent --show-error \ + --max-time "$curl_timeout" \ + --head \ + --dump-header "$response_headers" \ + --output /dev/null \ + --write-out '%{http_code}' \ + "$@" \ + "$base_url$path") || fail "$method $path could not be requested" + : > "$response_body" + else + status=$(curl --silent --show-error \ + --max-time "$curl_timeout" \ + --request "$method" \ + --dump-header "$response_headers" \ + --output "$response_body" \ + --write-out '%{http_code}' \ + "$@" \ + "$base_url$path") || fail "$method $path could not be requested" + fi - [ "$status" = "200" ] || fail "$method $path returned HTTP $status" + [ "$status" = "$expected_status" ] || \ + fail "$method $path returned HTTP $status (expected $expected_status)" } assert_header_contains() { @@ -109,38 +123,101 @@ while [ "$attempt" -lt 30 ]; do done [ "$attempt" -lt 30 ] || fail "YaCy did not become ready within 30 seconds" -request GET /api/version.xml +request GET /api/version.xml 200 assert_header_contains Content-Type text/xml assert_body_contains '<version>' assert_body_contains '<buildVersion>' pass "reflection-backed XML endpoint" -request GET /env/grafics/YaCyLogo2012.svg +request GET /env/grafics/YaCyLogo2012.svg 200 assert_header_contains Content-Type image/svg+xml assert_body_contains '<svg' pass "static resource" -request GET /index.html +cp "$response_body" "$work_dir/static-full" +static_length=$(wc -c < "$work_dir/static-full" | tr -d ' ') +request HEAD /env/grafics/YaCyLogo2012.svg 200 +assert_header_contains Content-Type image/svg+xml +assert_header_contains Content-Length "$static_length" +[ ! -s "$response_body" ] || fail "HEAD response contains a body" +pass "HEAD without response body" + +request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-9' +assert_header_contains Content-Range "bytes 0-9/$static_length" +[ "$(wc -c < "$response_body" | tr -d ' ')" -eq 10 ] || fail "range body is not 10 bytes" +head -c 10 "$work_dir/static-full" > "$work_dir/static-prefix" +cmp "$work_dir/static-prefix" "$response_body" >/dev/null 2>&1 || fail "range body has unexpected bytes" +pass "single byte range" + +request GET /env/grafics/YaCyLogo2012.svg 206 --header 'Range: bytes=0-4,48-51' +assert_header_contains Content-Type 'multipart/byteranges; boundary=' +assert_body_contains "Content-Range: bytes 0-4/$static_length" +assert_body_contains "Content-Range: bytes 48-51/$static_length" +assert_body_contains '<?xml' +assert_body_contains 'YaCy' +multipart_boundary=$(tr -d '\r' < "$response_headers" | awk -F 'boundary=' ' + tolower($0) ~ /^content-type: multipart\/byteranges/ { print $2 } +' | tail -n 1) +[ -n "$multipart_boundary" ] || fail "multipart range response has no boundary" +tail -c 80 "$response_body" | grep -F -- "--$multipart_boundary--" >/dev/null 2>&1 || \ + fail "multipart range response has no closing boundary" +pass "multipart byte ranges" + +request GET /env/grafics/YaCyLogo2012.svg 416 --header "Range: bytes=$static_length-" +assert_header_contains Content-Range "bytes */$static_length" +pass "unsatisfiable byte range" + +# YaCy deliberately suppresses Last-Modified on static responses to control its +# cache policy, but still implements If-Modified-Since against the resource. +request GET /env/grafics/YaCyLogo2012.svg 304 \ + --header 'If-Modified-Since: Thu, 31 Dec 2099 23:59:59 GMT' +[ ! -s "$response_body" ] || fail "304 response contains a body" +pass "If-Modified-Since" + +request GET /index.html 200 assert_header_contains Content-Type text/html assert_body_contains '<!DOCTYPE html>' assert_body_contains '<html' pass "rendered HTML template" -request GET '/suggest.json?q=jetty-smoke' +request GET '/suggest.json?q=jetty-smoke' 200 assert_header_contains Content-Type application/json assert_header_contains Access-Control-Allow-Origin '*' assert_body_contains '["jetty-smoke",[' pass "reflection-backed JSON endpoint and CORS header" -request POST /api/version.xml \ +request POST /api/version.xml 200 \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data 'smoke=post' assert_header_contains Content-Type text/xml assert_body_contains '<version>' pass "URL-encoded POST dispatch" -request OPTIONS /api/version.xml +request OPTIONS /api/version.xml 200 assert_header_contains Allow 'GET,HEAD,POST,OPTIONS' pass "OPTIONS method contract" +request GET /this-resource-must-not-exist-yacy-jetty-smoke 404 +assert_header_contains Content-Type text/html +assert_body_contains 'YaCy ' +pass "YaCy 404 error page" + +request GET /env/grafics/YaCyLogo2012.svg 200 \ + --header 'Accept-Encoding: gzip' \ + --raw +assert_header_contains Content-Encoding gzip +gzip -dc "$response_body" > "$work_dir/gzip-decoded" || fail "gzip response cannot be decompressed" +cmp "$work_dir/static-full" "$work_dir/gzip-decoded" >/dev/null 2>&1 || \ + fail "decompressed response differs from the uncompressed resource" +pass "gzip response compression" + +printf 'q=jetty-gzip-smoke' > "$work_dir/gzip-request-form" +gzip -c "$work_dir/gzip-request-form" > "$work_dir/gzip-request-body" +request POST /suggest.json 200 \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --header 'Content-Encoding: gzip' \ + --data-binary "@$work_dir/gzip-request-body" +assert_body_contains '["jetty-gzip-smoke",[' +pass "gzip request decompression" + echo "PASS: $checks embedded-server checks succeeded." diff --git a/test/jetty-solr-dependency-guard.sh b/test/jetty-solr-dependency-guard.sh index 0f2fad8a2..21e208194 100755 --- a/test/jetty-solr-dependency-guard.sh +++ b/test/jetty-solr-dependency-guard.sh @@ -42,14 +42,72 @@ for artifact in http2-client http2-common http2-http-client-transport; do fail "$artifact must only be an input of the Solr 9 bridge" done +for artifact in slf4j-api slf4j-jdk14; do + grep -E "name=\"$artifact\" rev=\"1.7.36\" conf=\"solr9-bridge->master\"" ivy.xml >/dev/null 2>&1 || \ + fail "$artifact 1.7.36 must only be an input of the Solr 9 bridge" +done + +grep -E 'org="org.eclipse.jetty.toolchain" name="jetty-servlet-api" rev="4.0.9"' ivy.xml >/dev/null 2>&1 || \ + fail "Jetty's EE8 Servlet 4 API must be an explicit dependency" +grep -E 'exclude org="javax.servlet" module="javax.servlet-api"' ivy.xml >/dev/null 2>&1 || \ + fail "transitive javax.servlet-api artifacts must be excluded" + +expected_jetty_version=$(sed -n \ + 's/.*org="org.eclipse.jetty" name="jetty-server" rev="\([^"]*\)".*/\1/p' \ + ivy.xml) +[ -n "$expected_jetty_version" ] || \ + fail "could not determine the public Jetty version from jetty-server in ivy.xml" +[ "$(printf '%s\n' "$expected_jetty_version" | wc -l | tr -d ' ')" -eq 1 ] || \ + fail "jetty-server must declare exactly one public Jetty version" + if [ -d lib ]; then + public_jetty_count=0 + for artifact in lib/jetty-*.jar; do + [ -e "$artifact" ] || continue + case $(basename "$artifact") in + jetty-servlet-api-*.jar) + # Jetty's Servlet 4 toolchain has its own version line. + continue + ;; + esac + public_jetty_count=$((public_jetty_count + 1)) + case $(basename "$artifact") in + *-"$expected_jetty_version".jar) ;; + *) fail "public Jetty artifact is not on version $expected_jetty_version: $artifact" ;; + esac + done + [ "$public_jetty_count" -gt 0 ] || \ + fail "no public Jetty $expected_jetty_version artifacts found" + + case "$expected_jetty_version" in + 12.*) + for artifact in lib/jetty-continuation-*.jar; do + [ -e "$artifact" ] || continue + fail "Jetty 9-only artifact remains on the Jetty 12 classpath: $artifact" + done + ;; + esac + + servlet_api_count=0 + for artifact in lib/*servlet-api-*.jar; do + [ -e "$artifact" ] || continue + servlet_api_count=$((servlet_api_count + 1)) + [ "$(basename "$artifact")" = "jetty-servlet-api-4.0.9.jar" ] || \ + fail "unexpected Servlet API artifact: $artifact" + done + [ "$servlet_api_count" -eq 1 ] || \ + fail "expected exactly one public Servlet API artifact, found $servlet_api_count" + jar tf lib/jetty-servlet-api-4.0.9.jar | grep '^javax/servlet/resources/web-app_4_0.xsd$' >/dev/null 2>&1 || \ + fail "Jetty Servlet API is missing the EE8 web.xml schema" + for pattern in \ 'jetty-deploy-*.jar' \ 'jetty-jmx-*.jar' \ 'solr-core-*.jar' \ 'solr-solrj-*.jar' \ 'solr-scripting-*.jar' \ - 'http2-*.jar'; do + 'http2-*.jar' \ + 'slf4j-*-1.7.36.jar'; do for artifact in lib/$pattern; do [ -e "$artifact" ] || continue fail "forbidden resolved artifact: $artifact" @@ -66,7 +124,9 @@ if [ -d lib ]; then jetty-util-9.4.58.v20250814 \ http2-client-9.4.58.v20250814 \ http2-common-9.4.58.v20250814 \ - http2-http-client-transport-9.4.58.v20250814; do + http2-http-client-transport-9.4.58.v20250814 \ + slf4j-api-1.7.36 \ + slf4j-jdk14-1.7.36; do jar="lib/solr9-bridge-$artifact.jar" [ -f "$jar" ] || fail "missing generated bridge artifact: $jar" if jar tf "$jar" | grep '^org/eclipse/jetty/' >/dev/null 2>&1; then @@ -75,7 +135,13 @@ if [ -d lib ]; then if zipgrep -a -E 'org(/|\.)eclipse(/|\.)jetty' "$jar" >/dev/null 2>&1; then fail "unrelocated Jetty reference in $jar" fi + if jar tf "$jar" | grep '^org/slf4j/' >/dev/null 2>&1; then + fail "unrelocated SLF4J class in $jar" + fi + if zipgrep -a -E 'org(/|\.)slf4j' "$jar" >/dev/null 2>&1; then + fail "unrelocated SLF4J reference in $jar" + fi done fi -echo "PASS: Solr 9 uses only the relocated Jetty client island." +echo "PASS: Solr 9 uses only the relocated Jetty client and SLF4J 1.7 island." diff --git a/test/jetty/RelocateJettyPackages.java b/test/jetty/RelocateJettyPackages.java index 0766388e5..01939f5d2 100644 --- a/test/jetty/RelocateJettyPackages.java +++ b/test/jetty/RelocateJettyPackages.java @@ -1,9 +1,10 @@ /* - * Proof-of-concept bytecode relocator for the Jetty 12 migration. + * Build-time bytecode relocator for the Jetty 12 migration. * * This is deliberately a small build tool, not YaCy runtime code. It keeps * Solr's public packages unchanged while moving its private Jetty 9 linkage - * below net.yacy.solr9.jetty. + * below net.yacy.solr9.jetty and its SLF4J 1.7 linkage below + * net.yacy.solr9.slf4j. */ package net.yacy.test.jetty; @@ -26,10 +27,12 @@ import org.objectweb.asm.commons.Remapper; public final class RelocateJettyPackages { - private static final String SOURCE_INTERNAL = "org/eclipse/jetty"; - private static final String TARGET_INTERNAL = "net/yacy/solr9/jetty"; - private static final String SOURCE_BINARY = "org.eclipse.jetty"; - private static final String TARGET_BINARY = "net.yacy.solr9.jetty"; + private static final Relocation[] RELOCATIONS = { + new Relocation("org/eclipse/jetty", "net/yacy/solr9/jetty", + "org.eclipse.jetty", "net.yacy.solr9.jetty"), + new Relocation("org/slf4j", "net/yacy/solr9/slf4j", + "org.slf4j", "net.yacy.solr9.slf4j") + }; private RelocateJettyPackages() { } @@ -43,7 +46,7 @@ public final class RelocateJettyPackages { private static void relocate(final Path input, final Path output) throws IOException { Files.createDirectories(output.toAbsolutePath().getParent()); - final Remapper remapper = new JettyRemapper(); + final Remapper remapper = new BridgeRemapper(); try (JarFile source = new JarFile(input.toFile()); JarOutputStream target = new JarOutputStream(Files.newOutputStream(output))) { @@ -62,8 +65,7 @@ public final class RelocateJettyPackages { if (entry.getName().endsWith(".class")) { target.write(relocateClass(content, remapper)); } else if (entry.getName().startsWith("META-INF/services/")) { - target.write(new String(content, StandardCharsets.UTF_8) - .replace(SOURCE_BINARY, TARGET_BINARY) + target.write(relocateText(new String(content, StandardCharsets.UTF_8)) .getBytes(StandardCharsets.UTF_8)); } else { target.write(content); @@ -74,28 +76,42 @@ public final class RelocateJettyPackages { } } - private static final class JettyRemapper extends Remapper { + private static final class BridgeRemapper extends Remapper { - private JettyRemapper() { + private BridgeRemapper() { super(Opcodes.ASM9); } @Override public String map(final String internalName) { - return replacePrefix(internalName, SOURCE_INTERNAL, TARGET_INTERNAL); + return relocateInternalName(internalName); } @Override public Object mapValue(final Object value) { if (value instanceof String) { final String text = (String) value; - return text.replace(SOURCE_BINARY, TARGET_BINARY) - .replace(SOURCE_INTERNAL, TARGET_INTERNAL); + return relocateText(text); } return super.mapValue(value); } } + private static final class Relocation { + private final String sourceInternal; + private final String targetInternal; + private final String sourceBinary; + private final String targetBinary; + + private Relocation(final String sourceInternal, final String targetInternal, + final String sourceBinary, final String targetBinary) { + this.sourceInternal = sourceInternal; + this.targetInternal = targetInternal; + this.sourceBinary = sourceBinary; + this.targetBinary = targetBinary; + } + } + private static byte[] relocateClass(final byte[] content, final Remapper remapper) { final ClassReader reader = new ClassReader(content); final ClassWriter writer = new ClassWriter(0); @@ -120,13 +136,30 @@ public final class RelocateJettyPackages { } private static String relocateEntryName(final String name) { - final String internalName = replacePrefix(name, SOURCE_INTERNAL, TARGET_INTERNAL); + final String internalName = relocateInternalName(name); if (internalName.startsWith("META-INF/services/")) { - return internalName.replace(SOURCE_BINARY, TARGET_BINARY); + return relocateText(internalName); } return internalName; } + private static String relocateInternalName(final String name) { + String relocated = name; + for (final Relocation relocation : RELOCATIONS) { + relocated = replacePrefix(relocated, relocation.sourceInternal, relocation.targetInternal); + } + return relocated; + } + + private static String relocateText(final String text) { + String relocated = text; + for (final Relocation relocation : RELOCATIONS) { + relocated = relocated.replace(relocation.sourceBinary, relocation.targetBinary) + .replace(relocation.sourceInternal, relocation.targetInternal); + } + return relocated; + } + private static byte[] readAll(final InputStream stream) throws IOException { final ByteArrayOutputStream output = new ByteArrayOutputStream(); stream.transferTo(output); diff --git a/test/proxy-smoke-test.sh b/test/proxy-smoke-test.sh new file mode 100755 index 000000000..ccae2227b --- /dev/null +++ b/test/proxy-smoke-test.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env sh + +# Live HTTP proxy and CONNECT contract. The peer must be started with the +# transparent proxy enabled and both targets must be controlled test services. + +set -eu + +proxy=${YACY_SMOKE_PROXY:-} +http_target=${YACY_SMOKE_PROXY_HTTP_TARGET:-} +https_target=${YACY_SMOKE_PROXY_HTTPS_TARGET:-} +[ -n "$proxy" ] && [ -n "$http_target" ] && [ -n "$https_target" ] || { + echo "SKIP: set YACY_SMOKE_PROXY, YACY_SMOKE_PROXY_HTTP_TARGET and YACY_SMOKE_PROXY_HTTPS_TARGET" >&2 + exit 2 +} + +timeout=${YACY_SMOKE_TIMEOUT:-30} +curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \ + --noproxy '' \ + "$http_target" >/dev/null +echo "ok 1 - HTTP proxy traffic" + +# An HTTPS request through an HTTP proxy necessarily establishes a CONNECT tunnel. +curl --silent --show-error --fail --max-time "$timeout" --proxy "$proxy" \ + --noproxy '' \ + "$https_target" >/dev/null +echo "ok 2 - HTTPS CONNECT tunnel" + +echo "PASS: proxy and CONNECT contract" diff --git a/test/remote-solr-smoke-test.sh b/test/remote-solr-smoke-test.sh new file mode 100755 index 000000000..84bea117a --- /dev/null +++ b/test/remote-solr-smoke-test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env sh + +# Exercises YaCy's Apache-HttpClient-backed RemoteInstance against an external +# Solr collection. This is an opt-in integration gate. + +set -eu + +[ -n "${YACY_REMOTE_SOLR_URL:-}" ] || { + echo "SKIP: set YACY_REMOTE_SOLR_URL (for example http://host:8983/solr/)" >&2 + exit 2 +} + +repo_root=$(CDPATH= cd -- "$(dirname "$0")/.." && pwd) +cd "$repo_root" +[ -d build/classes/java/main ] || { echo "FAIL: run ant compile first" >&2; exit 1; } + +work=$(mktemp -d "${TMPDIR:-/tmp}/yacy-remote-solr-smoke.XXXXXX") +trap 'rm -rf "$work"' EXIT HUP INT TERM +classpath=build/classes/java/main +for archive in lib/*.jar; do + case $(basename "$archive") in yacycore.jar) continue ;; esac + classpath="$classpath:$archive" +done + +javac --release 17 -cp "$classpath" -d "$work" \ + test/java/net/yacy/cora/federate/solr/connector/RemoteSolrSmoke.java +java -cp "$work:$classpath" net.yacy.cora.federate.solr.connector.RemoteSolrSmoke \ + "$YACY_REMOTE_SOLR_URL" + +echo "PASS: remote Solr request through YaCy RemoteInstance" |
