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:06 UTC

(tapestry-5) 01/03: TAP5-2809: Gradle integration test convention use mutex

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)
+    }
+}