Skip to content

Commit c3f99a7

Browse files
JonasPammermatreijamesfredley
authored
Restart Geb VNC Container for each Test in a Spec to get correct recordings (#14995)
* test: proof of bug test: proof of bug * chore: boolean toggle * fix using reflection * test: improve clarity of test And also, do not use an external url in the test. * fix: rename property for clarity * feat: default to restarting the recording container * chore: cleanup * 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. * 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. * Restore Slf4j logging to WebDriverContainerHolder Restore Slf4j logging lost in the merge --------- Co-authored-by: Mattias Reichel <[email protected]> Co-authored-by: James Fredley <[email protected]>
1 parent af5f2fd commit c3f99a7

File tree

7 files changed

+209
-7
lines changed

7 files changed

+209
-7
lines changed

grails-geb/src/testFixtures/groovy/grails/plugin/geb/GebRecordingTestListener.groovy

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
package grails.plugin.geb
2020

2121
import groovy.transform.CompileStatic
22+
import groovy.util.logging.Slf4j
2223

24+
import com.github.dockerjava.api.exception.NotFoundException
2325
import org.spockframework.runtime.AbstractRunListener
2426
import org.spockframework.runtime.model.ErrorInfo
2527
import org.spockframework.runtime.model.IterationInfo
@@ -33,6 +35,7 @@ import org.spockframework.runtime.model.IterationInfo
3335
* @author James Daugherty
3436
* @since 4.1
3537
*/
38+
@Slf4j
3639
@CompileStatic
3740
class GebRecordingTestListener extends AbstractRunListener {
3841

@@ -45,10 +48,23 @@ class GebRecordingTestListener extends AbstractRunListener {
4548

4649
@Override
4750
void afterIteration(IterationInfo iteration) {
48-
containerHolder.currentContainer.afterTest(
49-
new ContainerGebTestDescription(iteration),
50-
Optional.ofNullable(errorInfo?.exception)
51-
)
51+
try {
52+
containerHolder.currentContainer.afterTest(
53+
new ContainerGebTestDescription(iteration),
54+
Optional.ofNullable(errorInfo?.exception)
55+
)
56+
} catch (NotFoundException e) {
57+
// Handle the case where VNC recording container doesn't have a recording file
58+
// This can happen when per-test recording is enabled and a test doesn't use the browser
59+
if (containerHolder.grailsGebSettings.restartRecordingContainerPerTest &&
60+
e.message?.contains('/newScreen.mp4')) {
61+
log.debug("No VNC recording found for test '{}' - this is expected for tests that don't use the browser",
62+
iteration.displayName)
63+
} else {
64+
// Re-throw if it's a different type of NotFoundException
65+
throw e
66+
}
67+
}
5268
errorInfo = null
5369
}
5470

grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsContainerGebExtension.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class GrailsContainerGebExtension implements IGlobalExtension {
106106
}
107107

108108
spec.allFeatures*.addIterationInterceptor { invocation ->
109+
holder.restartVncRecordingContainer()
109110
holder.testManager.beforeTest(invocation.instance.getClass(), invocation.iteration.displayName)
110111
try {
111112
invocation.proceed()

grails-geb/src/testFixtures/groovy/grails/plugin/geb/GrailsGebSettings.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ class GrailsGebSettings {
4747
String tracingEnabled
4848
String recordingDirectoryName
4949
String reportingDirectoryName
50+
boolean restartRecordingContainerPerTest
5051
VncRecordingMode recordingMode
5152
VncRecordingFormat recordingFormat
5253
LocalDateTime startTime
@@ -64,6 +65,7 @@ class GrailsGebSettings {
6465
recordingFormat = VncRecordingFormat.valueOf(
6566
System.getProperty('grails.geb.recording.format', DEFAULT_RECORDING_FORMAT.name())
6667
)
68+
restartRecordingContainerPerTest = Boolean.parseBoolean(System.getProperty('grails.geb.recording.restartRecordingContainerPerTest', 'true'))
6769
implicitlyWait = getIntProperty('grails.geb.timeouts.implicitlyWait', DEFAULT_TIMEOUT_IMPLICITLY_WAIT)
6870
pageLoadTimeout = getIntProperty('grails.geb.timeouts.pageLoad', DEFAULT_TIMEOUT_PAGE_LOAD)
6971
scriptTimeout = getIntProperty('grails.geb.timeouts.script', DEFAULT_TIMEOUT_SCRIPT)

grails-geb/src/testFixtures/groovy/grails/plugin/geb/WebDriverContainerHolder.groovy

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@
1818
*/
1919
package grails.plugin.geb
2020

21+
import java.lang.reflect.Field
2122
import java.time.Duration
2223
import java.time.temporal.ChronoUnit
2324
import java.util.function.Supplier
2425

2526
import groovy.transform.CompileStatic
2627
import groovy.transform.EqualsAndHashCode
2728
import groovy.transform.PackageScope
29+
import groovy.util.logging.Slf4j
2830

2931
import com.github.dockerjava.api.model.ContainerNetwork
3032
import geb.Browser
@@ -39,6 +41,7 @@ import org.spockframework.runtime.model.SpecInfo
3941
import org.testcontainers.Testcontainers
4042
import org.testcontainers.containers.BrowserWebDriverContainer
4143
import org.testcontainers.containers.PortForwardingContainer
44+
import org.testcontainers.containers.VncRecordingContainer
4245
import org.testcontainers.images.PullPolicy
4346

4447
import grails.plugin.geb.serviceloader.ServiceRegistry
@@ -51,6 +54,7 @@ import grails.plugin.geb.serviceloader.ServiceRegistry
5154
* @author James Daugherty
5255
* @since 4.1
5356
*/
57+
@Slf4j
5458
@CompileStatic
5559
class WebDriverContainerHolder {
5660

@@ -252,4 +256,40 @@ class WebDriverContainerHolder {
252256
fileDetector = configuration?.fileDetector() ?: ContainerGebConfiguration.DEFAULT_FILE_DETECTOR
253257
}
254258
}
259+
260+
/**
261+
* Workaround for https://github.com/testcontainers/testcontainers-java/issues/3998
262+
* Restarts the VNC recording container to enable separate recording files for each test method.
263+
* This method uses reflection to access the VNC recording container field in BrowserWebDriverContainer.
264+
* Should be called BEFORE each test starts.
265+
*/
266+
void restartVncRecordingContainer() {
267+
if (!grailsGebSettings.recordingEnabled || !grailsGebSettings.restartRecordingContainerPerTest || !currentContainer) {
268+
return
269+
}
270+
try {
271+
// Use reflection to access the VNC recording container field
272+
Field vncRecordingContainerField = BrowserWebDriverContainer.class.getDeclaredField('vncRecordingContainer')
273+
vncRecordingContainerField.setAccessible(true)
274+
275+
VncRecordingContainer vncContainer = vncRecordingContainerField.get(currentContainer) as VncRecordingContainer
276+
277+
if (vncContainer) {
278+
// Stop the current VNC recording container
279+
vncContainer.stop()
280+
// Create and start a new VNC recording container for the next test
281+
VncRecordingContainer newVncContainer = new VncRecordingContainer(currentContainer)
282+
.withVncPassword('secret')
283+
.withVncPort(5900)
284+
.withVideoFormat(grailsGebSettings.recordingFormat)
285+
vncRecordingContainerField.set(currentContainer, newVncContainer)
286+
newVncContainer.start()
287+
288+
log.debug('Successfully restarted VNC recording container')
289+
}
290+
} catch (Exception e) {
291+
log.warn("Failed to restart VNC recording container: ${e.message}", e)
292+
// Don't throw the exception to avoid breaking the test execution
293+
}
294+
}
255295
}

grails-test-examples/geb/build.gradle

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,20 @@ dependencies {
7070
integrationTestImplementation testFixtures('org.apache.grails:grails-geb')
7171
}
7272

73-
//tasks.withType(Test).configureEach {
74-
// //systemProperty('grails.geb.recording.mode', 'RECORD_ALL')
75-
//}
73+
tasks.withType(Test).configureEach {
74+
systemProperty('grails.geb.recording.mode', 'RECORD_ALL')
75+
doLast {
76+
// Delete all but the two most recent recording directories to save space
77+
def baseDir = file(System.getProperty('grails.geb.recording.directory', 'build/gebContainer/recordings'))
78+
if (!baseDir.isDirectory()) return
79+
def dirs = (baseDir.listFiles() ?: [])
80+
.findAll { it.directory && it.name ==~ /^\d{8}_\d{6}$/ }
81+
.sort { it.name }
82+
if (dirs.size() > 2) {
83+
delete(dirs.dropRight(2)) // keep the two most recent
84+
}
85+
}
86+
}
7687

7788
apply {
7889
from rootProject.layout.projectDirectory.file('gradle/functional-test-config.gradle')
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.demo.spock
21+
22+
import spock.lang.Stepwise
23+
24+
import grails.plugin.geb.ContainerGebSpec
25+
import grails.testing.mixin.integration.Integration
26+
27+
import org.demo.spock.pages.HomePage
28+
import org.demo.spock.pages.UploadPage
29+
30+
@Stepwise
31+
@Integration
32+
class PerTestRecordingSpec extends ContainerGebSpec {
33+
34+
void '(setup) running a test to create a recording'() {
35+
when: 'visiting the home page'
36+
to HomePage
37+
38+
then: 'the page loads correctly'
39+
title == 'Welcome to Grails'
40+
}
41+
42+
void '(setup) running a second test to create another recording'() {
43+
when: 'visiting another page than the previous test'
44+
to UploadPage
45+
46+
and: 'pausing to ensure the recorded file size is different'
47+
Thread.sleep(1000)
48+
49+
then: 'the page loads correctly'
50+
title == 'Upload Test'
51+
}
52+
53+
void 'the recordings of the previous two tests are different'() {
54+
when: 'getting the configured base recording directory'
55+
// Logic from GrailsGebSettings
56+
def recordingDirectoryName = System.getProperty(
57+
'grails.geb.recording.directory',
58+
'build/gebContainer/recordings'
59+
)
60+
def baseRecordingDir = new File(recordingDirectoryName)
61+
62+
then: 'the base recording directory exists'
63+
baseRecordingDir.exists()
64+
65+
when: 'getting the most recent recording directory'
66+
// Find the timestamped recording directory (should be the most recent one)
67+
File recordingDir = null
68+
def timestampedDirs = baseRecordingDir.listFiles({ File dir ->
69+
dir.isDirectory() && dir.name ==~ /^\d{8}_\d{6}$/
70+
} as FileFilter)
71+
72+
if (timestampedDirs) {
73+
// Get the most recent directory
74+
recordingDir = timestampedDirs.sort { it.name }.last()
75+
}
76+
77+
then: 'the recording directory should be found'
78+
recordingDir != null
79+
80+
when: 'getting all video recording files (mp4 or flv) from the recording directory'
81+
def recordingFiles = recordingDir?.listFiles({ File file ->
82+
isVideoFile(file) && file.name.contains(this.class.simpleName)
83+
} as FileFilter)
84+
85+
then: 'recording files should exist for each test method'
86+
recordingFiles != null
87+
recordingFiles.length >= 2 // At least 2 files for the first two test methods
88+
89+
and: 'the recording files should have different content (different sizes)'
90+
// Sort by last modified time to get the most recent files
91+
def sortedFiles = recordingFiles.sort { it.lastModified() }
92+
def secondLastFile = sortedFiles[sortedFiles.length - 2]
93+
def lastFile = sortedFiles[sortedFiles.length - 1]
94+
95+
// Files should have different sizes (allowing for small variations due to timing)
96+
long sizeDifference = Math.abs(lastFile.length() - secondLastFile.length())
97+
sizeDifference > 1000 // Expect at least 1KB difference
98+
}
99+
100+
private static boolean isVideoFile(File file) {
101+
return file.isFile() && (file.name.endsWith('.mp4') || file.name.endsWith('.flv'))
102+
}
103+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* https://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.demo.spock.pages
21+
22+
import geb.Page
23+
24+
class HomePage extends Page {
25+
26+
static url = '/'
27+
static at = { title == 'Welcome to Grails' }
28+
29+
}

0 commit comments

Comments
 (0)