Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
package grails.plugin.geb

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
Expand All @@ -33,6 +35,7 @@ import org.spockframework.runtime.model.IterationInfo
* @author James Daugherty
* @since 4.1
*/
@Slf4j
@CompileStatic
class GebRecordingTestListener extends AbstractRunListener {

Expand All @@ -45,10 +48,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.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)
} else {
// Re-throw if it's a different type of NotFoundException
throw e
}
}
errorInfo = null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class GrailsContainerGebExtension implements IGlobalExtension {
}

spec.allFeatures*.addIterationInterceptor { invocation ->
holder.restartVncRecordingContainer()
holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName)
try {
invocation.proceed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class GrailsGebSettings {
String tracingEnabled
String recordingDirectoryName
String reportingDirectoryName
boolean restartRecordingContainerPerTest
VncRecordingMode recordingMode
VncRecordingFormat recordingFormat
LocalDateTime startTime
Expand All @@ -64,6 +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', '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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@
*/
package grails.plugin.geb

import java.lang.reflect.Field
import java.time.Duration
import java.time.temporal.ChronoUnit
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
Expand All @@ -39,6 +41,7 @@ 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 grails.plugin.geb.serviceloader.ServiceRegistry
Expand All @@ -51,6 +54,7 @@ import grails.plugin.geb.serviceloader.ServiceRegistry
* @author James Daugherty
* @since 4.1
*/
@Slf4j
@CompileStatic
class WebDriverContainerHolder {

Expand Down Expand Up @@ -252,4 +256,40 @@ 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.restartRecordingContainerPerTest || !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) {
// 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
}
}
}
17 changes: 14 additions & 3 deletions grails-test-examples/geb/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,20 @@ 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')
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 rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* 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 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 '(setup) running a test to create a recording'() {
when: 'visiting the home page'
to HomePage

then: 'the page loads correctly'
title == 'Welcome to Grails'
}

void '(setup) running a second test to create another recording'() {
when: 'visiting another page than the previous test'
to UploadPage

and: 'pausing to ensure the recorded file size is different'
Thread.sleep(1000)

then: 'the page loads correctly'
title == 'Upload Test'
}

void 'the recordings of the previous two tests are different'() {
when: 'getting the configured base recording directory'
// Logic from GrailsGebSettings
def recordingDirectoryName = System.getProperty(
'grails.geb.recording.directory',
'build/gebContainer/recordings'
)
def baseRecordingDir = new File(recordingDirectoryName)

then: 'the base recording directory exists'
baseRecordingDir.exists()

when: 'getting the most recent recording directory'
// Find the timestamped recording directory (should be the most recent one)
File recordingDir = null
def timestampedDirs = baseRecordingDir.listFiles({ File dir ->
dir.isDirectory() && dir.name ==~ /^\d{8}_\d{6}$/
} as FileFilter)

if (timestampedDirs) {
// Get the most recent directory
recordingDir = timestampedDirs.sort { it.name }.last()
}

then: 'the recording directory should be found'
recordingDir != null

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: 'the recording files should have different content (different sizes)'
// Sort by last modified time to get the most recent files
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'))
}
}
Original file line number Diff line number Diff line change
@@ -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' }

}
Loading