You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2026/10/03 12:12:05 UTC
(tapestry-5) branch javax updated (85f956e3c -> c613524d3)
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a change to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
from 85f956e3c TAP5-2836: update require.js/underscore
new c7937aa2e TAP5-2809: Gradle integration test convention use mutex
new 260c892ec TAP5-2817: SeleniumTestCase improve error handling during testStartup
new c613524d3 JettyRunner allow for re-bind attempts
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
buildSrc/README.md | 21 ++
.../groovy/t5build/IntegrationTestMutex.groovy | 20 ++
.../groovy/tapestry.testing-base-convention.gradle | 13 ++
.../main/groovy/tapestry.testng-convention.gradle | 15 ++
.../org/apache/tapestry5/test/JettyRunner.java | 82 +++++++-
.../apache/tapestry5/test/SeleniumTestCase.java | 216 ++++++++++++---------
6 files changed, 274 insertions(+), 93 deletions(-)
create mode 100644 buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy
(tapestry-5) 02/03: TAP5-2817: SeleniumTestCase improve error handling during testStartup
Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit 260c892ecb74d4c4f522a9827cdfba35f5c99579
Author: Ben Weidig <be...@netzgut.net>
AuthorDate: Sun Jul 26 14:31:17 2026 +0200
TAP5-2817: SeleniumTestCase improve error handling during testStartup
---
.../apache/tapestry5/test/SeleniumTestCase.java | 216 ++++++++++++---------
1 file changed, 125 insertions(+), 91 deletions(-)
diff --git a/tapestry-test/src/main/java/org/apache/tapestry5/test/SeleniumTestCase.java b/tapestry-test/src/main/java/org/apache/tapestry5/test/SeleniumTestCase.java
index 84884abfa..f03e8c9e0 100644
--- a/tapestry-test/src/main/java/org/apache/tapestry5/test/SeleniumTestCase.java
+++ b/tapestry-test/src/main/java/org/apache/tapestry5/test/SeleniumTestCase.java
@@ -246,128 +246,162 @@ public abstract class SeleniumTestCase extends Assert implements Selenium
final Runnable stopWebServer = launchWebServer(container, webAppFolder, contextPath, port, sslPort);
- FirefoxDriverManager.firefoxdriver().setup();
+ FirefoxDriver driver = null;
- DesiredCapabilities desiredCapabilities = new DesiredCapabilities();
- FirefoxOptions options = new FirefoxOptions(desiredCapabilities);
-
- // TAP5-2819: Run headless on CI
- if (Boolean.parseBoolean(System.getProperty("ci", "false")))
+ try
{
- options.addArguments("-headless", "--width=1920", "--height=1080");
- }
+ FirefoxDriverManager.firefoxdriver().setup();
- File ffProfileTemplate = new File(TapestryRunnerConstants.MODULE_BASE_DIR, "src/test/conf/ff_profile_template");
- // From https://forums.parasoft.com/discussion/5682/using-selenium-with-firefox-snap-ubuntu
- String osName = System.getProperty("os.name");
- String snapProfileRoot = osName.contains("Linux") && new File("/snap/firefox").exists()
- ? createProfileRootInUserHome()
- : null;
+ DesiredCapabilities desiredCapabilities = new DesiredCapabilities();
+ FirefoxOptions options = new FirefoxOptions(desiredCapabilities);
- FirefoxProfile profile;
- if (ffProfileTemplate.isDirectory())
- {
- LOGGER.info("Loading Firefox profile from: {}", ffProfileTemplate);
- profile = new FirefoxProfile(ffProfileTemplate);
- }
- else if (snapProfileRoot != null)
- {
- File snapSafeDir = new File(snapProfileRoot, "tmp-profile");
- snapSafeDir.mkdirs();
- LOGGER.info("Creating Snap-compatible Firefox profile in: {}", snapSafeDir);
+ // TAP5-2819: Run headless on CI
+ if (Boolean.parseBoolean(System.getProperty("ci", "false")))
+ {
+ options.addArguments("-headless", "--width=1920", "--height=1080");
+ }
- profile = new FirefoxProfile(snapSafeDir);
- profile.setPreference("intl.accept_languages", "en,fr,de");
- }
- else
- {
- LOGGER.info("Using default Firefox profile");
- profile = new FirefoxProfile();
- profile.setPreference("intl.accept_languages", "en,fr,de");
- }
+ File ffProfileTemplate = new File(TapestryRunnerConstants.MODULE_BASE_DIR, "src/test/conf/ff_profile_template");
+ // From https://forums.parasoft.com/discussion/5682/using-selenium-with-firefox-snap-ubuntu
+ String osName = System.getProperty("os.name");
+ String snapProfileRoot = osName.contains("Linux") && new File("/snap/firefox").exists()
+ ? createProfileRootInUserHome()
+ : null;
- options.setProfile(profile);
+ FirefoxProfile profile;
+ if (ffProfileTemplate.isDirectory())
+ {
+ LOGGER.info("Loading Firefox profile from: {}", ffProfileTemplate);
+ profile = new FirefoxProfile(ffProfileTemplate);
+ }
+ else if (snapProfileRoot != null)
+ {
+ File snapSafeDir = new File(snapProfileRoot, "tmp-profile");
+ snapSafeDir.mkdirs();
+ LOGGER.info("Creating Snap-compatible Firefox profile in: {}", snapSafeDir);
- FirefoxDriver driver = snapProfileRoot != null
- ? new FirefoxDriver(createGeckoDriverService(snapProfileRoot), options)
- : new FirefoxDriver(options);
+ profile = new FirefoxProfile(snapSafeDir);
+ profile.setPreference("intl.accept_languages", "en,fr,de");
+ }
+ else
+ {
+ LOGGER.info("Using default Firefox profile");
+ profile = new FirefoxProfile();
+ profile.setPreference("intl.accept_languages", "en,fr,de");
+ }
- // Implicit waiting can interfere with WebDriverWait
- // driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10L));
+ options.setProfile(profile);
- CommandProcessor webDriverCommandProcessor = new WebDriverCommandProcessor(baseURL, driver);
+ driver = snapProfileRoot != null
+ ? new FirefoxDriver(createGeckoDriverService(snapProfileRoot), options)
+ : new FirefoxDriver(options);
- final ErrorReporterImpl errorReporter = new ErrorReporterImpl(driver, testContext);
+ // Implicit waiting can interfere with WebDriverWait
+ // driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10L));
- ErrorReportingCommandProcessor commandProcessor = new ErrorReportingCommandProcessor(webDriverCommandProcessor,
- errorReporter);
+ CommandProcessor webDriverCommandProcessor = new WebDriverCommandProcessor(baseURL, driver);
- Selenium selenium = new WebDriverBackedSelenium(driver, baseURL);
+ final ErrorReporterImpl errorReporter = new ErrorReporterImpl(driver, testContext);
- testContext.setAttribute(TapestryTestConstants.BASE_URL_ATTRIBUTE, baseURL);
- testContext.setAttribute(TapestryTestConstants.SELENIUM_ATTRIBUTE, selenium);
- testContext.setAttribute(TapestryTestConstants.ERROR_REPORTER_ATTRIBUTE, errorReporter);
- testContext.setAttribute(TapestryTestConstants.COMMAND_PROCESSOR_ATTRIBUTE, commandProcessor);
+ ErrorReportingCommandProcessor commandProcessor = new ErrorReportingCommandProcessor(webDriverCommandProcessor,
+ errorReporter);
- testContext.setAttribute(TapestryTestConstants.SHUTDOWN_ATTRIBUTE, new Runnable()
- {
- @Override
- public void run()
+ Selenium selenium = new WebDriverBackedSelenium(driver, baseURL);
+
+ testContext.setAttribute(TapestryTestConstants.BASE_URL_ATTRIBUTE, baseURL);
+ testContext.setAttribute(TapestryTestConstants.SELENIUM_ATTRIBUTE, selenium);
+ testContext.setAttribute(TapestryTestConstants.ERROR_REPORTER_ATTRIBUTE, errorReporter);
+ testContext.setAttribute(TapestryTestConstants.COMMAND_PROCESSOR_ATTRIBUTE, commandProcessor);
+
+ testContext.setAttribute(TapestryTestConstants.SHUTDOWN_ATTRIBUTE, new Runnable()
{
- try
+ @Override
+ public void run()
{
- LOGGER.info("Shutting down selenium client ...");
-
try
{
- selenium.stop();
- } catch (RuntimeException e)
- {
- LOGGER.error("Selenium client shutdown failure.", e);
- }
+ LOGGER.info("Shutting down selenium client ...");
+
+ try
+ {
+ selenium.stop();
+ } catch (RuntimeException e)
+ {
+ LOGGER.error("Selenium client shutdown failure.", e);
+ }
- LOGGER.info("Shutting down webdriver ...");
+ LOGGER.info("Shutting down webdriver ...");
- try
- {
- if (webDriver != null) { // is sometimes null... but why?
- webDriver.quit();
+ try
+ {
+ if (webDriver != null) { // is sometimes null... but why?
+ webDriver.quit();
+ }
+ } catch (RuntimeException e)
+ {
+ LOGGER.error("Webdriver shutdown failure.", e);
}
- } catch (RuntimeException e)
- {
- LOGGER.error("Webdriver shutdown failure.", e);
- }
- LOGGER.info("Shutting down selenium server ...");
+ LOGGER.info("Shutting down selenium server ...");
- LOGGER.info("Shutting web server ...");
+ LOGGER.info("Shutting web server ...");
- try
- {
- stopWebServer.run();
- } catch (RuntimeException e)
+ try
+ {
+ stopWebServer.run();
+ } catch (RuntimeException e)
+ {
+ LOGGER.error("Web server shutdown failure.", e);
+ }
+
+ // Output, at the end of the Test, any html capture or screen shots (this makes it much easier
+ // to locate them at the end of the run; there's such a variance on where they end up based
+ // on whether the tests are running from inside an IDE or via one of the command line
+ // builds.
+
+ errorReporter.writeOutputPaths();
+ } finally
{
- LOGGER.error("Web server shutdown failure.", e);
+ testContext.removeAttribute(TapestryTestConstants.BASE_URL_ATTRIBUTE);
+ testContext.removeAttribute(TapestryTestConstants.SELENIUM_ATTRIBUTE);
+ testContext.removeAttribute(TapestryTestConstants.ERROR_REPORTER_ATTRIBUTE);
+ testContext.removeAttribute(TapestryTestConstants.COMMAND_PROCESSOR_ATTRIBUTE);
+ testContext.removeAttribute(TapestryTestConstants.SHUTDOWN_ATTRIBUTE);
+ testContext.removeAttribute(TapestryTestConstants.PAGE_LOAD_TIMEOUT_PARAMETER);
+ testContext.removeAttribute(TapestryTestConstants.WEBDRIVER_WAIT_TIMEOUT_PARAMETER);
}
+ }
+ });
+ } catch (Exception ex)
+ {
+ // Nothing has been stored in the test context yet, so testShutdown() has no way
+ // of cleaning up after a failure here. Release the browser and the web server now; otherwise
+ // the port stays bound for the rest of the JVM's life and every later <test> in the suite
+ // fails with "Failed to bind".
- // Output, at the end of the Test, any html capture or screen shots (this makes it much easier
- // to locate them at the end of the run; there's such a variance on where they end up based
- // on whether the tests are running from inside an IDE or via one of the command line
- // builds.
+ LOGGER.error("Startup of the integration test environment failed; releasing resources.", ex);
- errorReporter.writeOutputPaths();
- } finally
+ if (driver != null)
+ {
+ try
+ {
+ driver.quit();
+ } catch (RuntimeException e)
{
- testContext.removeAttribute(TapestryTestConstants.BASE_URL_ATTRIBUTE);
- testContext.removeAttribute(TapestryTestConstants.SELENIUM_ATTRIBUTE);
- testContext.removeAttribute(TapestryTestConstants.ERROR_REPORTER_ATTRIBUTE);
- testContext.removeAttribute(TapestryTestConstants.COMMAND_PROCESSOR_ATTRIBUTE);
- testContext.removeAttribute(TapestryTestConstants.SHUTDOWN_ATTRIBUTE);
- testContext.removeAttribute(TapestryTestConstants.PAGE_LOAD_TIMEOUT_PARAMETER);
- testContext.removeAttribute(TapestryTestConstants.WEBDRIVER_WAIT_TIMEOUT_PARAMETER);
+ LOGGER.error("Webdriver shutdown failure.", e);
}
}
- });
+
+ try
+ {
+ stopWebServer.run();
+ } catch (RuntimeException e)
+ {
+ LOGGER.error("Web server shutdown failure.", e);
+ }
+
+ throw ex;
+ }
}
private static String createProfileRootInUserHome() {
(tapestry-5) 03/03: JettyRunner allow for re-bind attempts
Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit c613524d33d2d4e8433e6fa36b0a38b9daab4d2e
Author: Ben Weidig <be...@netzgut.net>
AuthorDate: Sun Jul 26 14:35:34 2026 +0200
JettyRunner allow for re-bind attempts
To reduce failing tests due to test-unrelated issues like a port not
being free, the JettyRunner now tries 5 times with a 500ms delay to
reacquire the port.
This isn't bullet proof, but should provide a little more leeway in case
a previous integration test app is still holding on to a port.
---
.../org/apache/tapestry5/test/JettyRunner.java | 82 +++++++++++++++++++++-
1 file changed, 80 insertions(+), 2 deletions(-)
diff --git a/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java b/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
index f6da5112f..35e89f435 100644
--- a/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
+++ b/tapestry-runner/src/main/java/org/apache/tapestry5/test/JettyRunner.java
@@ -1,4 +1,4 @@
-// Copyright 2009, 2010, 2011, 2013 The Apache Software Foundation
+// Copyright 2009, 2010, 2011, 2013, 20026 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -22,12 +22,29 @@ import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.eclipse.jetty.webapp.WebAppContext;
import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ServerSocket;
/**
* Launches an instance of Jetty.
*/
public class JettyRunner implements ServletContainerRunner
{
+ /**
+ * Number of attempts made to acquire the server's ports before giving up.
+ *
+ * @since 5.10
+ */
+ private static final int BIND_ATTEMPTS = 5;
+
+ /**
+ * Delay, in milliseconds, between attempts to acquire the server's ports.
+ *
+ * @since 5.10
+ */
+ private static final long BIND_RETRY_DELAY = 500;
+
private Server jettyServer;
private String description;
@@ -36,6 +53,8 @@ public class JettyRunner implements ServletContainerRunner
private int sslPort;
+ private boolean sslEnabled;
+
public JettyRunner()
{
// un-configured runner
@@ -65,7 +84,9 @@ public class JettyRunner implements ServletContainerRunner
// SSL support
File keystoreFile = new File(TapestryRunnerConstants.MODULE_BASE_DIR, "src/test/conf/keystore");
- if (keystoreFile.exists())
+ sslEnabled = keystoreFile.exists();
+
+ if (sslEnabled)
{
SslContextFactory sslContextFactory = new SslContextFactory();
@@ -88,6 +109,13 @@ public class JettyRunner implements ServletContainerRunner
public void start() throws Exception
{
+ awaitPortAvailable(port);
+
+ if (sslEnabled)
+ {
+ awaitPortAvailable(sslPort);
+ }
+
jettyServer.start();
}
@@ -103,6 +131,10 @@ public class JettyRunner implements ServletContainerRunner
{
// Stop immediately and not gracefully.
jettyServer.stop();
+
+ // Release the connectors and the thread pool, rather than leaving them to be
+ // collected at some later point.
+ jettyServer.destroy();
} catch (Exception ex)
{
throw new RuntimeException("Error stopping Jetty instance: " + ex.toString(), ex);
@@ -111,6 +143,52 @@ public class JettyRunner implements ServletContainerRunner
System.out.println("Jetty instance has stopped.");
}
+ /**
+ * Waits, for a bounded amount of time, until the given port can be acquired.
+ *
+ * <p>A port might not always be available the instant the server holding it is stopped,
+ * so a suite that runs several test applications in sequence inside a single JVM could
+ * otherwise fail with "Failed to bind" IOException.
+ *
+ * @param portToAcquire
+ * the port the server is about to bind
+ * @throws IOException
+ * if the port is still held after {@link #BIND_ATTEMPTS} attempts
+ * @since 5.10
+ */
+ private static void awaitPortAvailable(int portToAcquire) throws IOException, InterruptedException
+ {
+ IOException failure = null;
+
+ for (int attempt = 0; attempt < BIND_ATTEMPTS; attempt++)
+ {
+ if (attempt > 0)
+ {
+ Thread.sleep(BIND_RETRY_DELAY);
+ }
+
+ // Jetty's connectors set SO_REUSEADDR, so this probe has to as well for the result to
+ // mean the same thing.
+ try (ServerSocket probe = new ServerSocket())
+ {
+ probe.setReuseAddress(true);
+ probe.bind(new InetSocketAddress(portToAcquire));
+
+ return;
+ } catch (IOException ex)
+ {
+ failure = ex;
+
+ System.out.printf("Port %d is not available yet (attempt %d of %d).%n",
+ portToAcquire, attempt + 1, BIND_ATTEMPTS);
+ }
+ }
+
+ throw new IOException(String.format(
+ "Port %d is still in use after %d attempts over %d ms; a previous server instance, or another process, has not released it.",
+ portToAcquire, BIND_ATTEMPTS, (BIND_ATTEMPTS - 1) * BIND_RETRY_DELAY), failure);
+ }
+
public Server getServer()
{
return jettyServer;
(tapestry-5) 01/03: TAP5-2809: Gradle integration test convention use mutex
Posted by th...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
thiagohp pushed a commit to branch javax
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
commit c7937aa2e65a3d205144b3564f2e7e003253a070
Author: Ben Weidig <be...@netzgut.net>
AuthorDate: Sun Jul 26 14:29:24 2026 +0200
TAP5-2809: Gradle integration test convention use mutex
---
buildSrc/README.md | 21 +++++++++++++++++++++
.../main/groovy/t5build/IntegrationTestMutex.groovy | 20 ++++++++++++++++++++
.../groovy/tapestry.testing-base-convention.gradle | 13 +++++++++++++
.../main/groovy/tapestry.testng-convention.gradle | 15 +++++++++++++++
4 files changed, 69 insertions(+)
diff --git a/buildSrc/README.md b/buildSrc/README.md
index d5c83b7f8..e09ca7200 100644
--- a/buildSrc/README.md
+++ b/buildSrc/README.md
@@ -46,6 +46,16 @@ Configures every `Test` task in the project via `tasks.withType(Test)`:
* Standard system properties: file encoding, locale, CI flag, Selenium wait timeout
* Consistent test logging: full exception format, pass/skip/fail events, progress counter
+Integration tests bind 9090/8443 by default (`@TapestryTestConfiguration`).
+To move a single run off those ports, when something else on the machine already holds them:
+
+```bash
+./gradlew :tapestry-jpa:testNG -Dtapestry.port=9191 -Dtapestry.ssl-port=8544
+```
+
+Gradle's `-D` only reaches the daemon, so the convention forwards these two to the test workers.
+`SeleniumTestCase` reads them as a fallback when `testng.xml` declares no such parameter.
+
### `tapestry.junit5-convention`
Use when the module's test sources use JUnit 5 Jupiter (`@Test`, `@ExtendWith`, ...).
@@ -131,6 +141,17 @@ The `testng.xml` suite file for these modules should contain **only** integratio
Unit tests are handled by the `test` task via testng-engine by Jupiter and must not appear in `testng.xml`.
+The task **must** be named `testNG`: the convention gives every task with that name a permit on the
+`integrationTestMutex` build service, described below.
+A differently named task gets no permit and will race the others for the ports.
+
+#### Serializing integration tests
+
+Every Selenium suite binds the same ports, so no two may run at once.
+Rather than disabling parallel builds (not a simple flag on project-level), the convention registers
+an `IntegrationTestMutex` build service with `maxParallelUsages = 1` and declares it on each `testNG` task.
+Gradle then admits one integration test task at a time while compilation, asset generation and the unit test suites still run in parallel.
+
---
## Helper classes (`t5build` package)
diff --git a/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy b/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy
new file mode 100644
index 000000000..245bcc743
--- /dev/null
+++ b/buildSrc/src/main/groovy/t5build/IntegrationTestMutex.groovy
@@ -0,0 +1,20 @@
+package t5build
+
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+
+/**
+ * A build service with no behaviour, used purely as a mutex.
+ *
+ * <p>Selenium integration tests bind fixed ports (9090/8443 by default, see
+ * {@code @TapestryTestConfiguration}), so two modules testing at the same time fight over one
+ * socket and the loser dies with "Failed to bind".
+ * <p>Registering this service with {@code maxParallelUsages = 1} and declaring it on
+ * the integration test tasks tells Gradle to allow only one of them at a time, whereas
+ * compilation, asset generation and the unit test suites carry on in parallel.
+ *
+ * @see tapestry.testng-convention.gradle
+ */
+abstract class IntegrationTestMutex implements BuildService<BuildServiceParameters.None>
+{
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle b/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle
index c4f1a1f06..6c3595490 100644
--- a/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle
+++ b/buildSrc/src/main/groovy/tapestry.testing-base-convention.gradle
@@ -38,6 +38,19 @@ tasks.withType(Test).configureEach { testTask ->
'selenium.wait.timeout': System.getProperty('selenium.wait.timeout', '15')
)
+ // Integration tests default to 9090/8443 (see @TapestryTestConfiguration).
+ // The integration test mutex in tapestry.testng-convention keeps two suites from wanting them at the same time.
+ // These let a single run move off the defaults when something else on the machine holds them:
+ // ./gradlew :tapestry-jpa:testNG -Dtapestry.port=9191 -Dtapestry.ssl-port=8544
+ // Gradle's -D only reaches the daemon, so they have to be forwarded to the workers explicitly.
+ // SeleniumTestCase reads them as a fallback when testng.xml declares no such parameter.
+ ['tapestry.port', 'tapestry.ssl-port'].each { key ->
+ def value = System.getProperty(key)
+ if (value != null) {
+ systemProperty key, value
+ }
+ }
+
environment.LANG = 'en_US.UTF-8'
testLogging {
diff --git a/buildSrc/src/main/groovy/tapestry.testng-convention.gradle b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle
index 083069931..70d0c7636 100644
--- a/buildSrc/src/main/groovy/tapestry.testng-convention.gradle
+++ b/buildSrc/src/main/groovy/tapestry.testng-convention.gradle
@@ -14,6 +14,8 @@
// tapestry.testing-base-convention platform runner, BOM, logging
// └─ tapestry.testng-convention + TestNG + EasyMock + testng-engine (this file)
+import t5build.IntegrationTestMutex
+
plugins {
id 'tapestry.testing-base-convention'
}
@@ -23,3 +25,16 @@ dependencies {
testImplementation libs.easymock
testRuntimeOnly libs.testng.engine
}
+
+// Every Selenium suite binds the same ports, so no two of them may run at once.
+// Gradle's build services express exactly that: one permit, handed to one integration
+// test task at a time, while everything else in the build still runs in parallel.
+def integrationTestMutex = gradle.sharedServices.registerIfAbsent('integrationTestMutex', IntegrationTestMutex) {
+ maxParallelUsages = 1
+}
+
+tasks.withType(Test).configureEach {
+ if (name == 'testNG') {
+ usesService(integrationTestMutex)
+ }
+}