From 2b9e084cb0bc8c5c56abbdc312eb2096f1928ae1 Mon Sep 17 00:00:00 2001 From: JonasPammer Date: Wed, 20 Aug 2025 11:42:54 +0200 Subject: [PATCH 01/10] test: proof of bug test: proof of bug --- .../demo/spock/PerTestRecordingSpec.groovy | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy diff --git a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy new file mode 100644 index 00000000000..93c555b9432 --- /dev/null +++ b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.demo.spock + +import grails.plugin.geb.ContainerGebSpec +import grails.testing.mixin.integration.Integration + +@Integration +class PerTestRecordingSpec extends ContainerGebSpec { + + void 'first test'() { + when: 'visiting the home page' + go('/') + + then: 'the page loads correctly' + title.contains('Welcome to Grails') + } + + void 'second test'() { + when: 'visiting another page' + go('https://grails.apache.org/') + + and: 'ensuring file size is different' + Thread.sleep(1000) + + then: 'the page loads correctly' + title.contains('Grails') + } + + void 'verify last recording directory'() { + when: + // Logic from GrailsGebSettings + String recordingDirectoryName = System.getProperty('grails.geb.recording.directory', 'build/gebContainer/recordings') + File baseRecordingDir = new File(recordingDirectoryName) + + then: 'base recording directory should exist' + baseRecordingDir.exists() + + when: 'get most recent recording directory' + // Find the timestamped recording directory (should be the most recent one) + File recordingDir = null + File[] timestampedDirs = baseRecordingDir.listFiles({ File dir -> + dir.isDirectory() && dir.name.matches('\\d{8}_\\d{6}') + } as FileFilter) + + if (timestampedDirs && timestampedDirs.length > 0) { + // Get the most recent directory + recordingDir = timestampedDirs.sort { it.name }.last() + } + + and: 'Get all recording files (mp4 or flv)' + File[] recordingFiles = recordingDir?.listFiles({ File file -> + file.isFile() && (file.name.endsWith('.mp4') || file.name.endsWith('.flv')) && file.name.contains(this.class.getSimpleName()) + } as FileFilter) + + then: 'recording directory should exist' + recordingDir != null + recordingDir.exists() + + and: 'recording files should be created for each test method' + recordingFiles != null + recordingFiles.length >= 2 // At least 2 files for the first two test methods + + and: 'recording files should have different content (different sizes)' + // Sort by last modified time to get the most recent files + File[] sortedFiles = recordingFiles.sort { it.lastModified() } + File secondLastFile = sortedFiles[sortedFiles.length - 2] + File lastFile = sortedFiles[sortedFiles.length - 1] + + // Files should have different sizes (allowing for small variations due to timing) + long sizeDifference = Math.abs(lastFile.length() - secondLastFile.length()) + sizeDifference > 1000 // Expect at least 1KB difference + } +} From 724036aeacabf80a5d2d425ca04dcee160a67a0d Mon Sep 17 00:00:00 2001 From: JonasPammer Date: Wed, 20 Aug 2025 13:10:59 +0200 Subject: [PATCH 02/10] chore: boolean toggle --- .../groovy/grails/plugin/geb/GrailsGebSettings.groovy | 2 ++ grails-test-examples/geb/build.gradle | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy index f3675e5094c..b7ce3404238 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy @@ -47,6 +47,7 @@ class GrailsGebSettings { String tracingEnabled String recordingDirectoryName String reportingDirectoryName + boolean recordingRestartPerTest VncRecordingMode recordingMode VncRecordingFormat recordingFormat LocalDateTime startTime @@ -64,6 +65,7 @@ class GrailsGebSettings { recordingFormat = VncRecordingFormat.valueOf( System.getProperty('grails.geb.recording.format', DEFAULT_RECORDING_FORMAT.name()) ) + recordingRestartPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartPerTest', false.toString())) implicitlyWait = getIntProperty('grails.geb.timeouts.implicitlyWait', DEFAULT_TIMEOUT_IMPLICITLY_WAIT) pageLoadTimeout = getIntProperty('grails.geb.timeouts.pageLoad', DEFAULT_TIMEOUT_PAGE_LOAD) scriptTimeout = getIntProperty('grails.geb.timeouts.script', DEFAULT_TIMEOUT_SCRIPT) diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index d6b4ffaf06f..be6594f6e49 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -70,9 +70,10 @@ dependencies { integrationTestImplementation testFixtures('org.apache.grails:grails-geb') } -//tasks.withType(Test).configureEach { -// //systemProperty('grails.geb.recording.mode', 'RECORD_ALL') -//} +tasks.withType(Test).configureEach { + systemProperty('grails.geb.recording.mode', 'RECORD_ALL') + systemProperty('grails.geb.recording.restartPerTest', 'true') +} apply { from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle') From 27388b00b36cc13aa5fe799a54af9db1e9adcdc3 Mon Sep 17 00:00:00 2001 From: JonasPammer Date: Wed, 20 Aug 2025 15:35:52 +0200 Subject: [PATCH 03/10] fix using reflection --- .../geb/GebRecordingTestListener.groovy | 24 ++++++++++-- .../geb/GrailsContainerGebExtension.groovy | 2 + .../geb/WebDriverContainerHolder.groovy | 39 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy index 8695b7b83ca..e21571e621b 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy @@ -18,7 +18,9 @@ */ package grails.plugin.geb +import com.github.dockerjava.api.exception.NotFoundException import groovy.transform.CompileStatic +import groovy.util.logging.Slf4j import org.spockframework.runtime.AbstractRunListener import org.spockframework.runtime.model.ErrorInfo import org.spockframework.runtime.model.IterationInfo @@ -32,6 +34,7 @@ import org.spockframework.runtime.model.IterationInfo * @author James Daugherty * @since 4.1 */ +@Slf4j @CompileStatic class GebRecordingTestListener extends AbstractRunListener { @@ -44,10 +47,23 @@ class GebRecordingTestListener extends AbstractRunListener { @Override void afterIteration(IterationInfo iteration) { - containerHolder.currentContainer.afterTest( - new ContainerGebTestDescription(iteration), - Optional.ofNullable(errorInfo?.exception) - ) + try { + containerHolder.currentContainer.afterTest( + new ContainerGebTestDescription(iteration), + Optional.ofNullable(errorInfo?.exception) + ) + } catch (NotFoundException e) { + // Handle the case where VNC recording container doesn't have a recording file + // This can happen when per-test recording is enabled and a test doesn't use the browser + if (containerHolder.grailsGebSettings.recordingRestartPerTest && + e.message?.contains('/newScreen.mp4')) { + log.debug("No VNC recording found for test '{}' - this is expected for tests that don't use the browser", + iteration.displayName) + } else { + // Re-throw if it's a different type of NotFoundException + throw e + } + } errorInfo = null } diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy index c30f89333cf..3055223cc23 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy @@ -106,6 +106,8 @@ class GrailsContainerGebExtension implements IGlobalExtension { } spec.allFeatures*.addIterationInterceptor { invocation -> + holder.restartVncRecordingContainer() + holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName) try { invocation.proceed() diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy index 2e9ffd1be4c..bedcf2e4703 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy @@ -36,8 +36,11 @@ import org.spockframework.runtime.model.SpecInfo import org.testcontainers.Testcontainers import org.testcontainers.containers.BrowserWebDriverContainer import org.testcontainers.containers.PortForwardingContainer +import org.testcontainers.containers.VncRecordingContainer import org.testcontainers.images.PullPolicy +import java.lang.reflect.Field + import java.time.Duration import java.time.temporal.ChronoUnit import java.util.function.Supplier @@ -250,5 +253,41 @@ class WebDriverContainerHolder { fileDetector = configuration?.fileDetector() ?: ContainerGebConfiguration.DEFAULT_FILE_DETECTOR } } + + /** + * Workaround for https://github.com/testcontainers/testcontainers-java/issues/3998 + * Restarts the VNC recording container to enable separate recording files for each test method. + * This method uses reflection to access the VNC recording container field in BrowserWebDriverContainer. + * Should be called BEFORE each test starts. + */ + void restartVncRecordingContainer() { + if (!grailsGebSettings.recordingEnabled || !grailsGebSettings.recordingRestartPerTest || !currentContainer) { + return + } + try { + // Use reflection to access the VNC recording container field + Field vncRecordingContainerField = BrowserWebDriverContainer.class.getDeclaredField('vncRecordingContainer') + vncRecordingContainerField.setAccessible(true) + + VncRecordingContainer vncContainer = vncRecordingContainerField.get(currentContainer) as VncRecordingContainer + + if (vncContainer != null) { + // Stop the current VNC recording container + vncContainer.stop() + // Create and start a new VNC recording container for the next test + VncRecordingContainer newVncContainer = new VncRecordingContainer(currentContainer) + .withVncPassword('secret') + .withVncPort(5900) + .withVideoFormat(grailsGebSettings.recordingFormat) + vncRecordingContainerField.set(currentContainer, newVncContainer) + newVncContainer.start() + + log.debug("Successfully restarted VNC recording container") + } + } catch (Exception e) { + log.warn("Failed to restart VNC recording container: ${e.message}", e) + // Don't throw the exception to avoid breaking the test execution + } + } } From 305e430500edd882071d36ba48f6095472320c7b Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 22 Aug 2025 17:12:08 +0200 Subject: [PATCH 04/10] test: improve clarity of test And also, do not use an external url in the test. --- .../demo/spock/PerTestRecordingSpec.groovy | 70 +++++++++++-------- .../org/demo/spock/pages/HomePage.groovy | 29 ++++++++ 2 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/HomePage.groovy diff --git a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy index 93c555b9432..8f82cdaafe5 100644 --- a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy +++ b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/PerTestRecordingSpec.groovy @@ -19,73 +19,85 @@ package org.demo.spock +import spock.lang.Stepwise + import grails.plugin.geb.ContainerGebSpec import grails.testing.mixin.integration.Integration +import org.demo.spock.pages.HomePage +import org.demo.spock.pages.UploadPage + +@Stepwise @Integration class PerTestRecordingSpec extends ContainerGebSpec { - void 'first test'() { + void '(setup) running a test to create a recording'() { when: 'visiting the home page' - go('/') + to HomePage then: 'the page loads correctly' - title.contains('Welcome to Grails') + title == 'Welcome to Grails' } - void 'second test'() { - when: 'visiting another page' - go('https://grails.apache.org/') + void '(setup) running a second test to create another recording'() { + when: 'visiting another page than the previous test' + to UploadPage - and: 'ensuring file size is different' + and: 'pausing to ensure the recorded file size is different' Thread.sleep(1000) then: 'the page loads correctly' - title.contains('Grails') + title == 'Upload Test' } - void 'verify last recording directory'() { - when: + void 'the recordings of the previous two tests are different'() { + when: 'getting the configured base recording directory' // Logic from GrailsGebSettings - String recordingDirectoryName = System.getProperty('grails.geb.recording.directory', 'build/gebContainer/recordings') - File baseRecordingDir = new File(recordingDirectoryName) + def recordingDirectoryName = System.getProperty( + 'grails.geb.recording.directory', + 'build/gebContainer/recordings' + ) + def baseRecordingDir = new File(recordingDirectoryName) - then: 'base recording directory should exist' + then: 'the base recording directory exists' baseRecordingDir.exists() - when: 'get most recent recording directory' + when: 'getting the most recent recording directory' // Find the timestamped recording directory (should be the most recent one) File recordingDir = null - File[] timestampedDirs = baseRecordingDir.listFiles({ File dir -> - dir.isDirectory() && dir.name.matches('\\d{8}_\\d{6}') + def timestampedDirs = baseRecordingDir.listFiles({ File dir -> + dir.isDirectory() && dir.name ==~ /^\d{8}_\d{6}$/ } as FileFilter) - if (timestampedDirs && timestampedDirs.length > 0) { + if (timestampedDirs) { // Get the most recent directory recordingDir = timestampedDirs.sort { it.name }.last() } - and: 'Get all recording files (mp4 or flv)' - File[] recordingFiles = recordingDir?.listFiles({ File file -> - file.isFile() && (file.name.endsWith('.mp4') || file.name.endsWith('.flv')) && file.name.contains(this.class.getSimpleName()) - } as FileFilter) - - then: 'recording directory should exist' + then: 'the recording directory should be found' recordingDir != null - recordingDir.exists() - and: 'recording files should be created for each test method' + when: 'getting all video recording files (mp4 or flv) from the recording directory' + def recordingFiles = recordingDir?.listFiles({ File file -> + isVideoFile(file) && file.name.contains(this.class.simpleName) + } as FileFilter) + + then: 'recording files should exist for each test method' recordingFiles != null recordingFiles.length >= 2 // At least 2 files for the first two test methods - and: 'recording files should have different content (different sizes)' + and: 'the recording files should have different content (different sizes)' // Sort by last modified time to get the most recent files - File[] sortedFiles = recordingFiles.sort { it.lastModified() } - File secondLastFile = sortedFiles[sortedFiles.length - 2] - File lastFile = sortedFiles[sortedFiles.length - 1] + def sortedFiles = recordingFiles.sort { it.lastModified() } + def secondLastFile = sortedFiles[sortedFiles.length - 2] + def lastFile = sortedFiles[sortedFiles.length - 1] // Files should have different sizes (allowing for small variations due to timing) long sizeDifference = Math.abs(lastFile.length() - secondLastFile.length()) sizeDifference > 1000 // Expect at least 1KB difference } + + private static boolean isVideoFile(File file) { + return file.isFile() && (file.name.endsWith('.mp4') || file.name.endsWith('.flv')) + } } diff --git a/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/HomePage.groovy b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/HomePage.groovy new file mode 100644 index 00000000000..4c4e5896461 --- /dev/null +++ b/grails-test-examples/geb/src/integration-test/groovy/org/demo/spock/pages/HomePage.groovy @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.demo.spock.pages + +import geb.Page + +class HomePage extends Page { + + static url = '/' + static at = { title == 'Welcome to Grails' } + +} \ No newline at end of file From 60cdc68a4e1bd4f115b7e938f7f145f4b162f8e6 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 22 Aug 2025 17:15:14 +0200 Subject: [PATCH 05/10] fix: rename property for clarity --- .../groovy/grails/plugin/geb/GebRecordingTestListener.groovy | 2 +- .../groovy/grails/plugin/geb/GrailsGebSettings.groovy | 4 ++-- .../groovy/grails/plugin/geb/WebDriverContainerHolder.groovy | 2 +- grails-test-examples/geb/build.gradle | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy index e21571e621b..907a773dca0 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy @@ -55,7 +55,7 @@ class GebRecordingTestListener extends AbstractRunListener { } catch (NotFoundException e) { // Handle the case where VNC recording container doesn't have a recording file // This can happen when per-test recording is enabled and a test doesn't use the browser - if (containerHolder.grailsGebSettings.recordingRestartPerTest && + if (containerHolder.grailsGebSettings.restartRecordingContainerPerTest && e.message?.contains('/newScreen.mp4')) { log.debug("No VNC recording found for test '{}' - this is expected for tests that don't use the browser", iteration.displayName) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy index b7ce3404238..61a89fa7fcd 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy @@ -47,7 +47,7 @@ class GrailsGebSettings { String tracingEnabled String recordingDirectoryName String reportingDirectoryName - boolean recordingRestartPerTest + boolean restartRecordingContainerPerTest VncRecordingMode recordingMode VncRecordingFormat recordingFormat LocalDateTime startTime @@ -65,7 +65,7 @@ class GrailsGebSettings { recordingFormat = VncRecordingFormat.valueOf( System.getProperty('grails.geb.recording.format', DEFAULT_RECORDING_FORMAT.name()) ) - recordingRestartPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartPerTest', false.toString())) + restartRecordingContainerPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartRecordingContainerPerTest', false.toString())) implicitlyWait = getIntProperty('grails.geb.timeouts.implicitlyWait', DEFAULT_TIMEOUT_IMPLICITLY_WAIT) pageLoadTimeout = getIntProperty('grails.geb.timeouts.pageLoad', DEFAULT_TIMEOUT_PAGE_LOAD) scriptTimeout = getIntProperty('grails.geb.timeouts.script', DEFAULT_TIMEOUT_SCRIPT) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy index bedcf2e4703..6355de6cb31 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy @@ -261,7 +261,7 @@ class WebDriverContainerHolder { * Should be called BEFORE each test starts. */ void restartVncRecordingContainer() { - if (!grailsGebSettings.recordingEnabled || !grailsGebSettings.recordingRestartPerTest || !currentContainer) { + if (!grailsGebSettings.recordingEnabled || !grailsGebSettings.restartRecordingContainerPerTest || !currentContainer) { return } try { diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index be6594f6e49..16cd2686a6f 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -72,7 +72,7 @@ dependencies { tasks.withType(Test).configureEach { systemProperty('grails.geb.recording.mode', 'RECORD_ALL') - systemProperty('grails.geb.recording.restartPerTest', 'true') + systemProperty('grails.geb.recording.restartRecordingContainerPerTest', 'true') } apply { From 0b1929fedb5222fee779a18b90aa8683363fdd37 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 22 Aug 2025 17:16:42 +0200 Subject: [PATCH 06/10] feat: default to restarting the recording container --- .../groovy/grails/plugin/geb/GrailsGebSettings.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy index 61a89fa7fcd..00212c8faae 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy @@ -65,7 +65,7 @@ class GrailsGebSettings { recordingFormat = VncRecordingFormat.valueOf( System.getProperty('grails.geb.recording.format', DEFAULT_RECORDING_FORMAT.name()) ) - restartRecordingContainerPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartRecordingContainerPerTest', false.toString())) + restartRecordingContainerPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartRecordingContainerPerTest', 'true')) implicitlyWait = getIntProperty('grails.geb.timeouts.implicitlyWait', DEFAULT_TIMEOUT_IMPLICITLY_WAIT) pageLoadTimeout = getIntProperty('grails.geb.timeouts.pageLoad', DEFAULT_TIMEOUT_PAGE_LOAD) scriptTimeout = getIntProperty('grails.geb.timeouts.script', DEFAULT_TIMEOUT_SCRIPT) From a27b47cd93498f36ba884bdc461204c665decb13 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Fri, 22 Aug 2025 17:17:12 +0200 Subject: [PATCH 07/10] chore: cleanup --- .../groovy/grails/plugin/geb/GebRecordingTestListener.groovy | 3 ++- .../grails/plugin/geb/GrailsContainerGebExtension.groovy | 1 - .../groovy/grails/plugin/geb/WebDriverContainerHolder.groovy | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy index 907a773dca0..74ac866f180 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy @@ -18,9 +18,10 @@ */ package grails.plugin.geb -import com.github.dockerjava.api.exception.NotFoundException import groovy.transform.CompileStatic import groovy.util.logging.Slf4j + +import com.github.dockerjava.api.exception.NotFoundException import org.spockframework.runtime.AbstractRunListener import org.spockframework.runtime.model.ErrorInfo import org.spockframework.runtime.model.IterationInfo diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy index 3055223cc23..b24ca2fb0eb 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy @@ -107,7 +107,6 @@ class GrailsContainerGebExtension implements IGlobalExtension { spec.allFeatures*.addIterationInterceptor { invocation -> holder.restartVncRecordingContainer() - holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName) try { invocation.proceed() diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy index 6355de6cb31..0afbadb5e04 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy @@ -271,7 +271,7 @@ class WebDriverContainerHolder { VncRecordingContainer vncContainer = vncRecordingContainerField.get(currentContainer) as VncRecordingContainer - if (vncContainer != null) { + if (vncContainer) { // Stop the current VNC recording container vncContainer.stop() // Create and start a new VNC recording container for the next test @@ -282,7 +282,7 @@ class WebDriverContainerHolder { vncRecordingContainerField.set(currentContainer, newVncContainer) newVncContainer.start() - log.debug("Successfully restarted VNC recording container") + log.debug('Successfully restarted VNC recording container') } } catch (Exception e) { log.warn("Failed to restart VNC recording container: ${e.message}", e) From 4eb7bc618dc6c232974a6fb234aa11bbdd53c060 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 25 Aug 2025 08:08:36 +0200 Subject: [PATCH 08/10] fix: remove redundant system property setting The system property `grails.geb.recording.restartRecordingContainerPerTest` was explicitly set to `true` for tests, but this is now the default behavior, so the explicit setting is no longer needed. --- grails-test-examples/geb/build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index 16cd2686a6f..a6e7ca536eb 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -72,7 +72,6 @@ dependencies { tasks.withType(Test).configureEach { systemProperty('grails.geb.recording.mode', 'RECORD_ALL') - systemProperty('grails.geb.recording.restartRecordingContainerPerTest', 'true') } apply { From e220055d2cab2411b1179fe21fa212afac744778 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 25 Aug 2025 08:47:43 +0200 Subject: [PATCH 09/10] test(geb): clean up old recording directories Delete all but the two most recent recording directories as part of the build to avoid unnecessary disk usage. --- grails-test-examples/geb/build.gradle | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/grails-test-examples/geb/build.gradle b/grails-test-examples/geb/build.gradle index a6e7ca536eb..c5b403de709 100644 --- a/grails-test-examples/geb/build.gradle +++ b/grails-test-examples/geb/build.gradle @@ -72,6 +72,17 @@ dependencies { tasks.withType(Test).configureEach { systemProperty('grails.geb.recording.mode', 'RECORD_ALL') + doLast { + // Delete all but the two most recent recording directories to save space + def baseDir = file(System.getProperty('grails.geb.recording.directory', 'build/gebContainer/recordings')) + if (!baseDir.isDirectory()) return + def dirs = (baseDir.listFiles() ?: []) + .findAll { it.directory && it.name ==~ /^\d{8}_\d{6}$/ } + .sort { it.name } + if (dirs.size() > 2) { + delete(dirs.dropRight(2)) // keep the two most recent + } + } } apply { From 2226151993ffd94cac57698883700a49a5be0c77 Mon Sep 17 00:00:00 2001 From: James Fredley Date: Thu, 28 Aug 2025 13:30:05 -0400 Subject: [PATCH 10/10] Restore Slf4j logging to WebDriverContainerHolder Restore Slf4j logging lost in the merge --- .../groovy/grails/plugin/geb/WebDriverContainerHolder.groovy | 2 ++ 1 file changed, 2 insertions(+) diff --git a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy index 0898adf848d..90c013f5dff 100644 --- a/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy +++ b/grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy @@ -26,6 +26,7 @@ import java.util.function.Supplier import groovy.transform.CompileStatic import groovy.transform.EqualsAndHashCode import groovy.transform.PackageScope +import groovy.util.logging.Slf4j import com.github.dockerjava.api.model.ContainerNetwork import geb.Browser @@ -53,6 +54,7 @@ import grails.plugin.geb.serviceloader.ServiceRegistry * @author James Daugherty * @since 4.1 */ +@Slf4j @CompileStatic class WebDriverContainerHolder {