Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature #79 - recording support #80

Merged
merged 19 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ This requires a [compatible container runtime](https://java.testcontainers.org/s
If you choose to use the `ContainerGebSpec` class, as long as you have a compatible container runtime installed, you don't need to do anything else.
Just run `./gradlew integrationTest` and a container will be started and configured to start a browser that can access your application under test.

#### Custom Host Configuration

The annotation `ContainerGebConfiguration` exists to customize the connection the container will use to access the application under test. The annotation is not required and `ContainerGebSpec` will use the default values in this annotation if it's not present.

#### Recording
By default, all failed tests will generate a video recording of the test execution. These recordings are saved to a `recordings` directory under the project's `build` directory.

The following system properties can be change to configure the recording behavior:

* `grails.geb.recording.mode`
* purpose: which tests to record
* possible values: `SKIP`, `RECORD_ALL`, or `RECORD_FAILING`
* defaults to `RECORD_FAILING`


* `grails.geb.recording.directory`
* purpose: the directory to save the recordings relative to the project directory
* defaults to `build/recordings`


* `grails.geb.recording.format`
* purpose: sets the format of the recording
* possible values are `FLV` or `MP4`
* defaults to `MP4`

### GebSpec

If you choose to extend `GebSpec`, you will need to have a [Selenium WebDriver](https://www.selenium.dev/documentation/webdriver/browsers/) installed that matches a browser you have installed on your system.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import java.util.regex.Pattern
* setups, ensuring the host network context is used for download requests.</p>
*
* @author Mattias Reichel
* @since 5.0.0
* @since 5.0
*/
@CompileStatic
@SelfType(ContainerGebSpec)
Expand All @@ -45,6 +45,7 @@ trait ContainerAwareDownloadSupport implements DownloadSupport {
private final DownloadSupport downloadSupport = new LocalhostDownloadSupport(browser, this)

abstract Browser getBrowser()

abstract String getHostNameFromHost()

private static class LocalhostDownloadSupport extends DefaultDownloadSupport {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

/**
* Can be used to configure the protocol and hostname that the container's browser will use
*
* @author James Daugherty
* @since 5.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface ContainerGebConfiguration {

static final String DEFAULT_HOSTNAME_FROM_CONTAINER = 'host.testcontainers.internal'
static final String DEFAULT_PROTOCOL = 'http'

/**
* The protocol that the container's browser will use to access the server under test.
* <p>Defaults to {@code http}.
*/
String protocol() default DEFAULT_PROTOCOL

/**
* The hostname that the container's browser will use to access the server under test.
* <p>Defaults to {@code host.testcontainers.internal}.
* <p>This is useful when the server under test needs to be accessed with a certain hostname.
*/
String hostName() default DEFAULT_HOSTNAME_FROM_CONTAINER
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import grails.testing.mixin.integration.Integration
import groovy.transform.CompileStatic
import groovy.transform.TailRecursive
import groovy.util.logging.Slf4j
import org.spockframework.runtime.extension.IGlobalExtension
import org.spockframework.runtime.model.SpecInfo

import java.time.LocalDateTime

/**
* A Spock Extension that manages the Testcontainers lifecycle for a {@link grails.plugin.geb.ContainerGebSpec}
*
* @author James Daugherty
* @since 5.0
*/
@Slf4j
@CompileStatic
class ContainerGebRecordingExtension implements IGlobalExtension {
WebDriverContainerHolder holder
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved

@Override
void start() {
holder = new WebDriverContainerHolder(new RecordingSettings())
addShutdownHook {
holder.stop()
}
}

@Override
void visitSpec(SpecInfo spec) {
if (isContainerGebSpec(spec)) {
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
validateContainerGebSpec(spec)

ContainerGebTestListener listener = new ContainerGebTestListener(holder, spec, LocalDateTime.now())
spec.addSetupInterceptor {
holder.reinitialize(it)
(it.sharedInstance as ContainerGebSpec).webDriverContainer = holder.current
}

spec.addListener(listener)
}
}

@TailRecursive
private boolean isContainerGebSpec(SpecInfo spec) {
if (spec != null) {
if (spec.filename.startsWith('ContainerGebSpec.')) {
return true
}
return isContainerGebSpec(spec.superSpec)
}
return false
}

private static void validateContainerGebSpec(SpecInfo specInfo) {
if (!specInfo.annotations.find { it.annotationType() == Integration }) {
throw new IllegalArgumentException("ContainerGebSpec classes must be annotated with @Integration")
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

87 changes: 8 additions & 79 deletions src/testFixtures/groovy/grails/plugin/geb/ContainerGebSpec.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,17 @@ import java.time.Duration
* </li>
* </ul>
*
* @see grails.plugin.geb.ContainerGebConfiguration for how to customize the container's connection information
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
*
* @author Søren Berg Glasius
* @author Mattias Reichel
* @since 5.0.0
* @author James Daugherty
* @since 5.0
*/
@DynamicallyDispatchesToBrowser
abstract class ContainerGebSpec extends Specification implements ManagedGebTest, ContainerAwareDownloadSupport {

private static final String DEFAULT_HOSTNAME_FROM_CONTAINER = 'host.testcontainers.internal'
private static final String DEFAULT_HOSTNAME_FROM_HOST = 'localhost'
private static final String DEFAULT_PROTOCOL = 'http'

private String hostNameFromContainer = DEFAULT_HOSTNAME_FROM_CONTAINER

boolean reportingEnabled = false

@Override
Expand All @@ -70,34 +68,6 @@ abstract class ContainerGebSpec extends Specification implements ManagedGebTest,
@Shared
BrowserWebDriverContainer webDriverContainer

@PackageScope
void initialize() {
webDriverContainer = new BrowserWebDriverContainer()
Testcontainers.exposeHostPorts(port)
webDriverContainer.tap {
addExposedPort(this.port)
withAccessToHost(true)
start()
}
if (hostNameChanged) {
webDriverContainer.execInContainer('/bin/sh', '-c', "echo '$hostIp\t$hostName' | sudo tee -a /etc/hosts")
}
WebDriver driver = new RemoteWebDriver(webDriverContainer.seleniumAddress, new ChromeOptions())
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(30))
browser.driver = driver
}

void setup() {
if (notInitialized) {
initialize()
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
browser.baseUrl = "$protocol://$hostName:$port"
}

def cleanupSpec() {
webDriverContainer?.stop()
}

/**
* Get access to container running the web-driver, for convenience to execInContainer, copyFileToContainer etc.
*
Expand All @@ -110,62 +80,21 @@ abstract class ContainerGebSpec extends Specification implements ManagedGebTest,
return webDriverContainer
}

/**
* Returns the protocol that the browser will use to access the server under test.
* <p>Defaults to {@code http}.
*
* @return the protocol for accessing the server under test
*/
String getProtocol() {
return DEFAULT_PROTOCOL
}

/**
* Returns the hostname that the browser will use to access the server under test.
* <p>Defaults to {@code host.testcontainers.internal}.
* <p>This is useful when the server under test needs to be accessed with a certain hostname.
*
* @return the hostname for accessing the server under test
*/
String getHostName() {
return hostNameFromContainer
}

void setHostName(String hostName) {
hostNameFromContainer = hostName
}

/**
* Returns the hostname that the server under test is available on from the host.
* <p>This is useful when using any of the {@code download*()} methods as they will connect from the host,
* and not from within the container.
* <p>Defaults to {@code localhost}. If the value returned by {@code getHostName()}
* is different from the default, this method will return the same value same as {@code getHostName()}.
* <p>Defaults to {@code localhost}. If the value returned by {@code webDriverContainer.getHost()}
* is different from the default, this method will return the same value same as {@code webDriverContainer.getHost()}.
*
* @return the hostname for accessing the server under test from the host
*/
@Override
String getHostNameFromHost() {
return hostNameChanged ? hostName : DEFAULT_HOSTNAME_FROM_HOST
}

int getPort() {
try {
return (int) getProperty('serverPort')
} catch (Exception ignore) {
throw new IllegalStateException('Test class must be annotated with @Integration for serverPort to be injected')
}
}

private static String getHostIp() {
PortForwardingContainer.INSTANCE.network.get().ipAddress
return hostNameChanged ? webDriverContainer.host : DEFAULT_HOSTNAME_FROM_HOST
}

private boolean isHostNameChanged() {
return hostNameFromContainer != DEFAULT_HOSTNAME_FROM_CONTAINER
}

private boolean isNotInitialized() {
webDriverContainer == null
return webDriverContainer.host != ContainerGebConfiguration.DEFAULT_HOSTNAME_FROM_CONTAINER
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright 2024 original author or authors
*
* Licensed 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 grails.plugin.geb

import groovy.transform.CompileStatic
import org.spockframework.runtime.model.IterationInfo
import org.testcontainers.lifecycle.TestDescription

import java.time.LocalDateTime
import java.time.format.DateTimeFormatter

/**
* Implements {@link org.testcontainers.lifecycle.TestDescription} to customize recording names.
*
* @see org.testcontainers.lifecycle.TestDescription
* @author James Daugherty
* @since 5.0
*/
@CompileStatic
class ContainerGebTestDescription implements TestDescription {

String testId
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
String filesystemFriendlyName

ContainerGebTestDescription(IterationInfo testInfo, LocalDateTime runDate) {
testId = testInfo.displayName
String safeName = testId.replaceAll('\\W+', '_')
filesystemFriendlyName = "${DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss").format(runDate)}_${safeName}"
jdaugherty marked this conversation as resolved.
Show resolved Hide resolved
}
}
Loading
Loading