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) {

Reply via email to