This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 45110eccf8 [IT] figure out why selenium is failing (#8383)
45110eccf8 is described below
commit 45110eccf8f085cfa205c5ed5c049345d147ba76
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Sep 15 11:34:31 2026 +0200
[IT] figure out why selenium is failing (#8383)
---
Jenkinsfile.hop-web-selenium | 56 +++++++++++++++-
web-tests/pom.xml | 18 ++++-
.../org/apache/hop/web/it/HopWebEnvironment.java | 76 ++++++++++++++++++++--
3 files changed, 141 insertions(+), 9 deletions(-)
diff --git a/Jenkinsfile.hop-web-selenium b/Jenkinsfile.hop-web-selenium
index ffb22336b8..c5aba8f804 100644
--- a/Jenkinsfile.hop-web-selenium
+++ b/Jenkinsfile.hop-web-selenium
@@ -21,9 +21,16 @@ def AGENT_LABEL = env.AGENT_LABEL ?: 'ubuntu'
def JDK_NAME = env.JDK_NAME ?: 'jdk_21_latest'
def MAVEN_NAME = env.MAVEN_NAME ?: 'maven_3_latest'
-// The image the tests drive. Overridable so a release candidate can be
checked the same way.
+def MAVEN_PARAMS = "-T 2 -U -B -e -V -Dmaven.compiler.fork=true"
+
+// A published image to test instead of building one, e.g. a release
candidate. Only used when
+// BUILD_IMAGE is off.
def HOP_WEB_IMAGE = env.HOP_WEB_IMAGE ?: 'apache/hop-web:Development'
+// The image built from this checkout. A per-build tag, never pushed:
publishing stays with the
+// main job and its manual release process.
+def CI_IMAGE = "hop-web:selenium-${env.BUILD_NUMBER}"
+
pipeline {
agent {
@@ -54,7 +61,14 @@ pipeline {
logRotator(artifactNumToKeepStr: '5', numToKeepStr: '10')
)
disableConcurrentBuilds()
- timeout(time: 1, unit: 'HOURS')
+ timeout(time: 2, unit: 'HOURS')
+ }
+
+ // Compared against false rather than tested directly: on the first run
after this block is
+ // added Jenkins has not registered the parameter yet and
params.BUILD_IMAGE is null.
+ parameters {
+ booleanParam(name: 'BUILD_IMAGE', defaultValue: true,
+ description: 'Build Hop and its Web image from this checkout. Off:
test the published HOP_WEB_IMAGE instead.')
}
stages {
@@ -79,7 +93,32 @@ pipeline {
checkout scm
}
}
+ // Building here rather than pulling apache/hop-web:Development makes
the nightly test the
+ // code as it is now: a broken or late main build no longer leaves it
testing yesterday's
+ // image, or failing on a Docker Hub pull, and a failure points at a
commit range.
+ stage('Build Apache Hop') {
+ when { expression { params.BUILD_IMAGE != false } }
+ steps {
+ // fast-build: no tests, rat, spotless, checkstyle or javadoc
- the main build covers
+ // those. Only the assemblies the Web image is made of are
needed here.
+ sh "mvn $MAVEN_PARAMS -Pfast-build clean install"
+ sh "unzip -qo ./assemblies/client/target/hop-client-*.zip -d
./assemblies/client/target/"
+ // Same as the main job: the marketplace-only plugins go into
the image too
+ sh "./tools/install-wave1-plugins.sh
./assemblies/client/target/hop"
+ sh "unzip -qo ./assemblies/web/target/hop.war -d
./assemblies/web/target/webapp"
+ }
+ }
+ stage('Build Hop Web image') {
+ when { expression { params.BUILD_IMAGE != false } }
+ steps {
+ // The same Dockerfile the main job publishes from, so the
image under test is
+ // built the way the shipped one is. Current platform only, no
push.
+ sh "docker build . -f docker/web.Dockerfile -t ${CI_IMAGE}"
+ sh "docker image inspect ${CI_IMAGE} --format 'Testing
{{.RepoTags}} digest {{.Id}} created {{.Created}}'"
+ }
+ }
stage('Record image under test') {
+ when { expression { params.BUILD_IMAGE == false } }
steps {
// These tests drive a published image rather than this
checkout, so record which
// one actually ran. Without this a broken main build leaves
the job quietly
@@ -90,8 +129,14 @@ pipeline {
}
stage('Run Selenium tests'){
steps {
- sh "mvn -f web-tests/pom.xml -B -e clean test
-Dhopweb.image=${HOP_WEB_IMAGE}"
+ script {
+ // A locally built tag must not be pulled: it only exists
on this agent.
+ def imageArgs = params.BUILD_IMAGE != false
+ ? "-Dhopweb.image=${CI_IMAGE} -Dhopweb.pull=false"
+ : "-Dhopweb.image=${HOP_WEB_IMAGE}"
+ sh "mvn -f web-tests/pom.xml -B -e clean test ${imageArgs}"
}
+ }
post {
always {
// Screenshot and rendered DOM of every failed test.
@@ -104,6 +149,11 @@ pipeline {
}
post {
always {
+ // A new tag every night would fill the agent's disk.
Testcontainers removes its
+ // containers a few seconds after the JVM exits, which is after
this runs, so take
+ // any that still reference the image down first.
+ sh "docker ps -aq --filter ancestor=${CI_IMAGE} | xargs -r docker
rm -f || true"
+ sh "docker rmi ${CI_IMAGE} || true"
cleanWs()
}
// Only mail when there is something to act on: a nightly green build
does not need
diff --git a/web-tests/pom.xml b/web-tests/pom.xml
index e8e1e62271..33fcab02ce 100644
--- a/web-tests/pom.xml
+++ b/web-tests/pom.xml
@@ -36,12 +36,13 @@
<hopweb.headless>true</hopweb.headless>
<hopweb.image>apache/hop-web:Development</hopweb.image>
<hopweb.pull>true</hopweb.pull>
- <hopweb.startupTimeout>300</hopweb.startupTimeout>
+ <hopweb.startupTimeout>600</hopweb.startupTimeout>
<hopweb.url></hopweb.url>
<jacoco.skip>true</jacoco.skip>
<maven.deploy.skip>true</maven.deploy.skip>
<maven.install.skip>true</maven.install.skip>
<selenium.version>4.48.0</selenium.version>
+ <slf4j.version>2.0.18</slf4j.version>
<testcontainers.version>2.0.5</testcontainers.version>
</properties>
@@ -52,6 +53,21 @@
<version>${selenium.version}</version>
<scope>test</scope>
</dependency>
+ <!-- Testcontainers reports on SLF4J, including the log of a container
that failed to
+ start. Without a binding on the classpath that report goes
nowhere. Testcontainers
+ itself brings a 1.7 API, which cannot see a 2.x binding, so pin
the API to match. -->
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <version>${slf4j.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
diff --git
a/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
b/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
index 840746879f..21802d4346 100644
--- a/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
+++ b/web-tests/src/test/java/org/apache/hop/web/it/HopWebEnvironment.java
@@ -23,6 +23,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.ArrayList;
@@ -37,6 +38,7 @@ import org.openqa.selenium.logging.LoggingPreferences;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.testcontainers.Testcontainers;
import org.testcontainers.containers.BrowserWebDriverContainer;
+import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.Network;
import org.testcontainers.containers.output.ToStringConsumer;
@@ -66,6 +68,10 @@ import org.testcontainers.utility.MountableFile;
* <li>{@code hopweb.browser} - {@code auto} (default), {@code container} or
{@code local}
* <li>{@code hopweb.browserImage} - the browser image, for a containerised
browser
* <li>{@code hopweb.headless} - only meaningful for a local browser
+ * <li>{@code hopweb.javaOptions} - the JVM options Hop Web runs with,
replacing the image's
+ * {@code HOP_OPTIONS}. The image default is {@code
-XX:+AggressiveHeap}, which sizes the heap
+ * to the whole machine and is the wrong thing on a shared build agent.
+ * <li>{@code hopweb.startupTimeout} - seconds to wait for the container and
then the GUI
* </ul>
*/
public final class HopWebEnvironment {
@@ -89,8 +95,26 @@ public final class HopWebEnvironment {
/** Where the container keeps the configuration Hop Web reads at startup. */
private static final String CONFIG_PATH =
"/usr/local/tomcat/webapps/ROOT/config/hop-config.json";
+ /**
+ * A fixed heap instead of the image's {@code -XX:+AggressiveHeap}: that
flag hands the JVM most
+ * of the host's memory, and on a build agent shared with other jobs and
containers that is how
+ * Hop Web fails to come up at all. The RWT resource location is what the
image sets; keep it.
+ */
+ private static final String DEFAULT_JAVA_OPTIONS =
+ "-Xmx2g -Dorg.eclipse.rap.rwt.resourceLocation=/tmp/rwt-resources";
+
+ /** Written next to the screenshots when Hop Web does not start, so CI
archives it. */
+ private static final String STARTUP_LOG_FILE = "hop-web-startup.log";
+
private static HopWebEnvironment instance;
+ /**
+ * Why the environment could not be created, if it could not. Every test
class asks for the
+ * environment in its {@code @BeforeAll}; without this each of them would
wait the full startup
+ * timeout again for a Hop Web that already failed to come up once.
+ */
+ private static RuntimeException startupFailure;
+
private final String uiUrl;
private final WebDriver driver;
private final BrowserMode browserMode;
@@ -128,8 +152,17 @@ public final class HopWebEnvironment {
}
public static synchronized HopWebEnvironment get() {
+ if (startupFailure != null) {
+ throw new IllegalStateException(
+ "Hop Web did not start earlier in this run; not trying again",
startupFailure);
+ }
if (instance == null) {
- instance = new HopWebEnvironment();
+ try {
+ instance = new HopWebEnvironment();
+ } catch (RuntimeException e) {
+ startupFailure = e;
+ throw e;
+ }
// Testcontainers reaps what it started, but a locally launched
ChromeDriver would
// outlive the Surefire JVM and leave a browser behind on the build
agent.
Runtime.getRuntime().addShutdownHook(new Thread(instance::close));
@@ -244,22 +277,31 @@ public final class HopWebEnvironment {
}
private GenericContainer<?> startHopWeb(Network network) {
+ // Attached before start, not after: when the container fails to come up,
its log is the only
+ // thing that says why (a failed ROOT context leaves Tomcat serving 404 on
/ui, for example),
+ // and Testcontainers removes the container before anyone could read it
afterwards.
+ serverLog = new ToStringConsumer();
GenericContainer<?> container =
new GenericContainer<>(DockerImageName.parse(imageUnderTest()))
.withNetwork(network)
.withNetworkAliases(HOP_WEB_ALIAS)
.withExposedPorts(HOP_WEB_PORT)
+ .withEnv("HOP_OPTIONS", property("hopweb.javaOptions",
DEFAULT_JAVA_OPTIONS))
// Hop Web only reports the version through the GUI, so log which
image actually ran:
// a daily job testing a published tag must not go green against a
stale image.
.withImagePullPolicy(
shouldPull() ? PullPolicy.alwaysPull() :
PullPolicy.defaultPolicy())
.withCopyFileToContainer(
MountableFile.forClasspathResource("hop-config.json"),
CONFIG_PATH)
+ .withLogConsumer(serverLog)
.waitingFor(Wait.forHttp("/ui").forStatusCode(200))
.withStartupTimeout(Duration.ofSeconds(startupTimeoutSeconds()));
- container.start();
- serverLog = new ToStringConsumer();
- container.followOutput(serverLog);
+ try {
+ container.start();
+ } catch (ContainerLaunchException e) {
+ reportStartupFailure(container, e);
+ throw e;
+ }
System.out.println(
"Hop Web container started from image "
+ container.getDockerImageName()
@@ -269,6 +311,30 @@ public final class HopWebEnvironment {
return container;
}
+ /** Puts the container log in the build output and in the archived
artifacts. */
+ private void reportStartupFailure(GenericContainer<?> container,
ContainerLaunchException e) {
+ String log = serverLog.toUtf8String();
+ System.out.println(
+ "Hop Web did not start from image "
+ + container.getDockerImageName()
+ + ": "
+ + e.getMessage()
+ + System.lineSeparator()
+ + "----- container log -----"
+ + System.lineSeparator()
+ + (log.isBlank() ? "(the container printed nothing)" : log)
+ + System.lineSeparator()
+ + "----- end of container log -----");
+ Path directory = Path.of(property("hopweb.artifacts",
"target/hopweb-artifacts"));
+ try {
+ Files.createDirectories(directory);
+ Files.writeString(directory.resolve(STARTUP_LOG_FILE), log, UTF_8);
+ System.out.println("Container log written to " +
directory.resolve(STARTUP_LOG_FILE));
+ } catch (IOException ioException) {
+ System.out.println("Could not write the container log: " +
ioException.getMessage());
+ }
+ }
+
/**
* The image to test: the published one by default, or one built from this
working tree when
* {@code -Dhopweb.build=true} is given.
@@ -389,7 +455,7 @@ public final class HopWebEnvironment {
}
private static long startupTimeoutSeconds() {
- return Long.parseLong(property("hopweb.startupTimeout", "300"));
+ return Long.parseLong(property("hopweb.startupTimeout", "600"));
}
private static String property(String name, String defaultValue) {