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