Last active
August 21, 2018 14:00
-
-
Save nipafx/138606fc0f3aeca1feae52805b540aa9 to your computer and use it in GitHub Desktop.
Running Selenium tests once per grid node
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@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") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) | |
@ExtendWith(SeleniumGridExtension.class) | |
public @interface SeleniumGridConfiguration { | |
String url(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@TestTemplate | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) | |
@ExtendWith(SeleniumGridExtension.class) | |
public @interface SeleniumGridTest { } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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