You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/06/30 01:59:48 UTC

(grails-core) 01/01: Stabilize recurring CI flaky tests in functional/example apps

This is an automated email from the ASF dual-hosted git repository.

jamesfredley pushed a commit to branch fix/flaky-tests
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 2acc785e57f6f876b549da3b89d0a80647bf2ad9
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Mon Jun 29 21:58:50 2026 -0400

    Stabilize recurring CI flaky tests in functional/example apps
    
    Mined ~100 recent CI failures and stabilized the recurring, environment/
    timing-sensitive flakes using the established Spock @Retry convention plus
    root-cause synchronization hardening where feasible:
    
    - Scaffolding Geb auth flake (geb.waiting.WaitTimeoutException): harden
      LoginPage.login()/LogoutPage.logout() to wait for a definitive
      authenticated/logged-out signal (login form gone/present) instead of a
      transient title change, and add a class-level @Retry backstop scoped to
      WaitTimeoutException on UserControllerSpec and UserCommunityControllerSpec.
    
    - AsyncPromiseSpec intermittent HTTP 500 (app1 + hibernate7/app1 mirrors):
      add @Retry scoped to AssertionError on the affected method. The JDK
      HttpClient does not throw on non-2xx, so a transient 500 surfaces as an
      assertJson assertion failure.
    
    - GreetingControllerFunctionalSpec Geb render timing: replace the eager
      pageSource assertion with waitFor { pageSource.contains('Hello') } and add
      a WaitTimeoutException-scoped @Retry backstop.
    
    These tests are ContainerGebSpec/functional/integration tests that require
    Docker and a full app boot and are intermittent by nature, so confirmation of
    the fixes is CI-bound and cannot be proven locally.
    
    Assisted-by: claude-code:claude-4.8-opus
---
 .../groovy/functionaltests/async/AsyncPromiseSpec.groovy          | 2 ++
 .../groovy/functionaltests/async/AsyncPromiseSpec.groovy          | 2 ++
 .../groovy/com/example/UserCommunityControllerSpec.groovy         | 4 ++++
 .../integrationTest/groovy/com/example/UserControllerSpec.groovy  | 4 ++++
 .../src/integrationTest/groovy/com/example/pages/LoginPage.groovy | 4 +++-
 .../integrationTest/groovy/com/example/pages/LogoutPage.groovy    | 4 +++-
 .../groovy/testphases/GreetingControllerFunctionalSpec.groovy     | 8 ++++++--
 7 files changed, 24 insertions(+), 4 deletions(-)

diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
index 0e090a1282..2ad124f0e2 100644
--- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
+++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
@@ -21,6 +21,7 @@ package functionaltests.async
 import java.util.concurrent.TimeUnit
 
 import functionaltests.services.AsyncProcessingService
+import spock.lang.Retry
 import spock.lang.Specification
 import spock.lang.Tag
 import spock.lang.Unroll
@@ -99,6 +100,7 @@ class AsyncPromiseSpec extends Specification implements HttpClientSupport {
 
     // ========== Error Handling Tests ==========
 
+    @Retry(count = 2, delay = 500, exceptions = [AssertionError])
     def "async task handles success without error"() {
         when: "calling task that should succeed"
         def response = http('/asyncTest/taskWithError?fail=false')
diff --git a/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy b/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
index 0e090a1282..2ad124f0e2 100644
--- a/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
+++ b/grails-test-examples/hibernate7/app1/src/integration-test/groovy/functionaltests/async/AsyncPromiseSpec.groovy
@@ -21,6 +21,7 @@ package functionaltests.async
 import java.util.concurrent.TimeUnit
 
 import functionaltests.services.AsyncProcessingService
+import spock.lang.Retry
 import spock.lang.Specification
 import spock.lang.Tag
 import spock.lang.Unroll
@@ -99,6 +100,7 @@ class AsyncPromiseSpec extends Specification implements HttpClientSupport {
 
     // ========== Error Handling Tests ==========
 
+    @Retry(count = 2, delay = 500, exceptions = [AssertionError])
     def "async task handles success without error"() {
         when: "calling task that should succeed"
         def response = http('/asyncTest/taskWithError?fail=false')
diff --git a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserCommunityControllerSpec.groovy b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserCommunityControllerSpec.groovy
index 2534c5f4b0..530ffb116c 100644
--- a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserCommunityControllerSpec.groovy
+++ b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserCommunityControllerSpec.groovy
@@ -25,7 +25,11 @@ import com.example.pages.CommunityUserListPage
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 
+import geb.waiting.WaitTimeoutException
+import spock.lang.Retry
+
 @Integration
+@Retry(count = 2, delay = 1000, exceptions = [WaitTimeoutException])
 class UserCommunityControllerSpec extends ContainerGebSpec {
 
     void setup() {
diff --git a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy
index a76cc1e013..9e1c6ab459 100644
--- a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy
+++ b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/UserControllerSpec.groovy
@@ -25,7 +25,11 @@ import com.example.pages.UserListPage
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 
+import geb.waiting.WaitTimeoutException
+import spock.lang.Retry
+
 @Integration
+@Retry(count = 2, delay = 1000, exceptions = [WaitTimeoutException])
 class UserControllerSpec extends ContainerGebSpec {
 
     void setup() {
diff --git a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LoginPage.groovy b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LoginPage.groovy
index f7158f36d8..c0360d6ca1 100644
--- a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LoginPage.groovy
+++ b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LoginPage.groovy
@@ -36,6 +36,8 @@ class LoginPage extends Page {
         this.username = username
         this.password = password
         loginButton.click()
-        waitFor(30) { title != pageTitle }
+        // Wait for a definitive authenticated signal: the login page must be fully replaced
+        // (title changed AND the login form is gone), not merely a transient title change.
+        waitFor(30) { title != pageTitle && $('input', name: 'username').empty }
     }
 }
diff --git a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LogoutPage.groovy b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LogoutPage.groovy
index e099989d93..b8874b7d2b 100644
--- a/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LogoutPage.groovy
+++ b/grails-test-examples/scaffolding/src/integrationTest/groovy/com/example/pages/LogoutPage.groovy
@@ -32,6 +32,8 @@ class LogoutPage extends Page {
 
     void logout() {
         logoutButton.click()
-        waitFor(30) { title != pageTitle }
+        // Wait for a definitive logged-out signal: the confirm page is replaced and the login
+        // form is present again, not merely a transient title change.
+        waitFor(30) { title != pageTitle && !$('input', name: 'username').empty }
     }
 }
diff --git a/grails-test-examples/test-phases/src/functional-test/groovy/testphases/GreetingControllerFunctionalSpec.groovy b/grails-test-examples/test-phases/src/functional-test/groovy/testphases/GreetingControllerFunctionalSpec.groovy
index caf8f1bb57..2cd7d679ce 100644
--- a/grails-test-examples/test-phases/src/functional-test/groovy/testphases/GreetingControllerFunctionalSpec.groovy
+++ b/grails-test-examples/test-phases/src/functional-test/groovy/testphases/GreetingControllerFunctionalSpec.groovy
@@ -21,15 +21,19 @@ package testphases
 import grails.plugin.geb.ContainerGebSpec
 import grails.testing.mixin.integration.Integration
 
+import geb.waiting.WaitTimeoutException
+import spock.lang.Retry
+
 @Integration
+@Retry(count = 2, delay = 1000, exceptions = [WaitTimeoutException])
 class GreetingControllerFunctionalSpec extends ContainerGebSpec {
 
     void "test greeting controller renders response"() {
         when: 'navigating to the greeting controller'
         go('/greeting/index')
 
-        then: 'the page contains the greeting message'
-        pageSource.contains('Hello')
+        then: 'the page eventually renders the greeting message'
+        waitFor { pageSource.contains('Hello') }
     }
 
 }