Skip to content

Instantly share code, notes, and snippets.

@nipafx
Last active August 21, 2018 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nipafx/138606fc0f3aeca1feae52805b540aa9 to your computer and use it in GitHub Desktop.
Save nipafx/138606fc0f3aeca1feae52805b540aa9 to your computer and use it in GitHub Desktop.
Running Selenium tests once per grid node
@SeleniumGridConfiguration(url = "http://0.0.0.0:4444")
class GridDemoTest {
@SeleniumGridTest
fun demo(driver: WebDriver) {
driver.get("https://blog.codefx.org")
val homeUrl = driver.findElement(By.className("img-hyperlink")).getAttribute("href")
assertThat(homeUrl).contains("blog.codefx.org")
}
}
class SeleniumGrid {
// a line looks like this:
// <p>capabilities: Capabilities {applicationName: , browserName: chrome, [...] }</p>
private static final Pattern CAPABILITIES_LINE = Pattern.compile("<p>[^{]*\\{(.*?)}</p>");
private static final ConcurrentMap<String, ResolvedGrid> GRIDS = new ConcurrentHashMap<>();
public static Stream<SeleniumNode> nodes(String url, TestReporter reporter) {
return GRIDS
.computeIfAbsent(url, u -> resolve(u, reporter))
.nodes().stream();
}
private static ResolvedGrid resolve(String url, TestReporter reporter) {
try {
URL hubUrl = new URL(url + "/wd/hub");
String consoleUrl = url + "/grid/console";
List<SeleniumNode> nodes = parseCapabilities(consoleUrl)
.peek(capabilities -> reporter.publishEntry("node", capabilities.toString()))
.map(capabilities -> new SeleniumNode(hubUrl, capabilities))
.collect(toList());
return ResolvedGrid.resolvedToNodes(nodes);
} catch (Exception ex) {
return ResolvedGrid.resolvedToError(ex);
}
}
private static Stream<Capabilities> parseCapabilities(String consoleUrl) throws IOException {
Document doc = Jsoup.connect(consoleUrl).get();
Elements leftColumn = doc.select("#left-column > div > div.content > div[type=\"config\"]");
Elements rightColumn = doc.select("#right-column > div > div.content > div[type=\"config\"]");
return concat(leftColumn.stream(), rightColumn.stream())
.map(SeleniumGrid::parseCapabilities);
}
private static Capabilities parseCapabilities(Element element) {
MutableCapabilities capabilities = new MutableCapabilities();
String capabilitiesLine = element.select("p").stream()
.filter(p -> p.toString().contains("capabilities"))
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"Selenium node has no capabilities\n" + element.toString()))
.toString();
// e.g. "<p>capabilities: Capabilities {applicationName: , browserName: chrome, [...] }</p>"
Matcher matcher = CAPABILITIES_LINE.matcher(capabilitiesLine);
if (!matcher.find())
throw new IllegalStateException("Selenium node has malformed capabilities: \"" + capabilitiesLine + "\"");
// e.g. "capabilities: Capabilities {applicationName: , browserName: chrome, [...]"
stream(matcher.group(1).split(","))
.map(String::trim)
// e.g. "applicationName: " or "browserName: chrome"
.map(pair -> pair.split(":"))
// some pairs have a key, but no value - remove them
.filter(pair -> pair.length == 2)
.forEach(pair -> capabilities.setCapability(pair[0].trim(), pair[1].trim()));
return capabilities;
}
private static class ResolvedGrid {
private final Optional<Exception> error;
private final List<SeleniumNode> nodes;
private ResolvedGrid(
Optional<Exception> error, List<SeleniumNode> nodes) {
this.error = requireNonNull(error);
this.nodes = requireNonNull(nodes);
}
static ResolvedGrid resolvedToNodes(List<SeleniumNode> nodes) {
return new ResolvedGrid(empty(), nodes);
}
static ResolvedGrid resolvedToError(Exception error) {
return new ResolvedGrid(of(error), emptyList());
}
List<SeleniumNode> nodes() throws IllegalArgumentException {
if (error.isPresent())
throw new IllegalArgumentException("Grid resolution failed", error.get());
return nodes;
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@ExtendWith(SeleniumGridExtension.class)
public @interface SeleniumGridConfiguration {
String url();
}
public class SeleniumGridExtension implements TestTemplateInvocationContextProvider {
public static final String SELENIUM_GRID_URL = "selenium.grid.url";
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return stream(context.getRequiredTestMethod().getParameters())
.anyMatch(SeleniumGridExtension::isWebDriver);
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
String url = SeleniumGridUrlFinder.find(context);
return SeleniumGrid
.nodes(url, context::publishReportEntry)
.map(SeleniumGridInvocationContext::new);
}
private static boolean isWebDriver(Parameter parameter) {
return WebDriver.class.isAssignableFrom(parameter.getType());
}
private static class SeleniumGridUrlFinder {
private final ExtensionContext context;
private SeleniumGridUrlFinder(ExtensionContext context) {
this.context = requireNonNull(context);
}
public static String find(ExtensionContext context) {
return new SeleniumGridUrlFinder(context).find();
}
private String find() {
return Stream.of(findOnParameter(), findOnMethod(), findOnClass(), findInConfiguration())
.flatMap(optional -> optional.map(Stream::of).orElseGet(Stream::empty))
.findFirst()
.orElseThrow(() -> new IllegalStateException("No Selenium grid URL defined"));
}
private Optional<String> findOnParameter() {
return stream(context.getRequiredTestMethod().getParameters())
.filter(SeleniumGridExtension::isWebDriver)
.flatMap(this::findOnElement)
.findFirst();
}
private Optional<String> findOnMethod() {
return findOnElement(context.getRequiredTestMethod()).findFirst();
}
private Optional<String> findOnClass() {
return findOnElement(context.getRequiredTestClass()).findFirst();
}
private Optional<String> findInConfiguration() {
return context.getConfigurationParameter(SELENIUM_GRID_URL);
}
private Stream<String> findOnElement(AnnotatedElement element) {
return AnnotationSupport
.findAnnotation(element, SeleniumGridConfiguration.class)
.map(SeleniumGridConfiguration::url)
.map(Stream::of)
.orElse(Stream.empty());
}
}
private static class SeleniumGridInvocationContext implements TestTemplateInvocationContext, ParameterResolver {
private final SeleniumNode node;
SeleniumGridInvocationContext(SeleniumNode node) {
this.node = requireNonNull(node);
}
@Override
public String getDisplayName(int invocationIndex) {
return node.name();
}
@Override
public List<Extension> getAdditionalExtensions() {
return singletonList(this);
}
@Override
public boolean supportsParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return isWebDriver(parameterContext.getParameter());
}
@Override
public Object resolveParameter(
ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return node.connect();
}
}
}
@TestTemplate
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@ExtendWith(SeleniumGridExtension.class)
public @interface SeleniumGridTest { }
class SeleniumNode {
private final URL gridUrl;
private final Capabilities capabilities;
public SeleniumNode(URL gridUrl, Capabilities capabilities) {
this.gridUrl = requireNonNull(gridUrl);
this.capabilities = requireNonNull(capabilities);
}
public String name() {
return capabilities.getBrowserName() + " : " + capabilities.getVersion();
}
public RemoteWebDriver connect() {
return new RemoteWebDriver(gridUrl, capabilities);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment