├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── bin ├── lib.zip └── node_modules.zip ├── docs ├── README.md ├── _config.yml ├── java-tutorial.md ├── node-tutorial.md ├── node_quickstart.webm └── node_thumb.png ├── lambda-selenium-java ├── .gitignore ├── build.gradle ├── serverless.yml ├── settings.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── blackboard │ │ └── testing │ │ ├── common │ │ └── LambdaBaseTest.java │ │ ├── driver │ │ ├── LambdaWebDriverFactory.java │ │ └── LambdaWebDriverThreadLocalContainer.java │ │ ├── lambda │ │ ├── LambdaSeleniumService.java │ │ ├── LambdaTestHandler.java │ │ ├── LambdaTestSuite.java │ │ ├── MethodFilter.java │ │ ├── TestInvoker.java │ │ ├── TestRequest.java │ │ ├── TestResult.java │ │ ├── exceptions │ │ │ └── LambdaCodeMismatchException.java │ │ └── logger │ │ │ ├── Logger.java │ │ │ ├── LoggerContainer.java │ │ │ └── MockLambdaConsoleLogger.java │ │ ├── runner │ │ ├── ParallelParameterized.java │ │ └── ThreadPoolScheduler.java │ │ └── testcontext │ │ ├── EnvironmentDetector.java │ │ └── TestUUID.java │ └── test │ └── java │ └── com │ └── blackboard │ └── testing │ └── tests │ ├── ExampleTestSuite.java │ └── LambdaTest.java ├── lambda-selenium-node └── index.js └── serverless-task-runner └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ # 2 | .idea/ 3 | *.iml 4 | 5 | # Build Directory # 6 | build/ 7 | 8 | # OS X # 9 | .DS_Store 10 | 11 | # Gradle # 12 | .gradle/ 13 | 14 | # Serverless # 15 | .serverless/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | 5 | script: 6 | - unzip -q bin/lib.zip -d lambda-selenium-node/ 7 | - unzip -q bin/node_modules.zip -d lambda-selenium-node/node_modules 8 | - cd lambda-selenium-node 9 | - zip -q -r ../node_lambda_function.zip * 10 | - cd ../lambda-selenium-java 11 | - gradle clean unzipLibs shadowJar 12 | - mv build/libs/lambda-selenium-all.jar ../java_lambda_function.zip 13 | - cd ../ 14 | 15 | deploy: 16 | provider: releases 17 | api_key: $github_token 18 | file: 19 | - "node_lambda_function.zip" 20 | - "java_lambda_function.zip" 21 | skip_cleanup: true 22 | on: 23 | tags: true 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Blackboard, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Watch the SeleniumConf presentation! 2 | [Scaling Selenium to Infinity](https://youtu.be/pnYui-Ua-_s) 3 | 4 | # lambda-selenium [![Build Status](https://travis-ci.org/blackboard/lambda-selenium.svg?branch=master)](https://travis-ci.org/blackboard/lambda-selenium) 5 | 6 | Welcome to the lambda-selenium project! The purpose of this project is to show how to use Selenium Webdriver with Amazon Web Services (AWS) Lambda compute service. 7 | 8 | NEW Demo October 2018!: [Serverless Task Runner](serverless-task-runner) 9 | 10 | [![Basic Tutorial](https://blackboard.github.io/lambda-selenium/node_thumb.png)](https://blackboard.github.io/lambda-selenium/node_quickstart.webm "Basic Tutorial") 11 | 12 | The project examples currently support Chromium version 62 in headless only mode. However, we are working to provide support for the full versions of Google Chrome and Mozilla Firefox very soon. 13 | 14 | [Tutorials](https://blackboard.github.io/lambda-selenium/) 15 | - [Node.js](https://blackboard.github.io/lambda-selenium/node-tutorial.html) 16 | - [Java](https://blackboard.github.io/lambda-selenium/java-tutorial.html) 17 | 18 | **Need help or have a request?** Submit an issue and we would be happy to respond! This project is supported by a global team of software engineers that develop frameworks and scalable infrastructure for high scale end to end UI testing. 19 | 20 | ***If you've got an issue, we want to know about it!*** 21 | 22 | -------------------------------------------------------------------------------- /bin/lib.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackboard/lambda-selenium/22f077f60bc9560fb96cc274f9f2a8615380025f/bin/lib.zip -------------------------------------------------------------------------------- /bin/node_modules.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackboard/lambda-selenium/22f077f60bc9560fb96cc274f9f2a8615380025f/bin/node_modules.zip -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # lambda-selenium [![Build Status](https://travis-ci.org/blackboard/lambda-selenium.svg?branch=master)](https://travis-ci.org/blackboard/lambda-selenium) 2 | 3 | ## Watch the SeleniumConf presentation! 4 | [Scaling Selenium to Infinity](https://youtu.be/pnYui-Ua-_s) 5 | 6 | ## Getting Started 7 | 1. Download Latest Zip 8 | 2. Upload to AWS Lambda 9 | 3. Test the Function 10 | 11 | ### [Beginner: Node Tutorial](./node-tutorial.md) 12 | 13 | 14 | ### [Advanced: Java Tutorial](./java-tutorial.md) 15 | 16 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate 2 | show_downloads: false -------------------------------------------------------------------------------- /docs/java-tutorial.md: -------------------------------------------------------------------------------- 1 | # Lambda-Selenium: Java Tutorial [![Build Status](https://travis-ci.org/blackboard/lambda-selenium.svg?branch=master)](https://travis-ci.org/blackboard/lambda-selenium) 2 | 3 | #### Running The Example 4 | 5 | **_Install the serverless framework_** 6 | ```shell 7 | npm install serverless -g 8 | ``` 9 | 10 | **_Use shell commands to clone the repository and change the working directory to the Java example_** 11 | ```shell 12 | git clone git@github.com:blackboard/lambda-selenium.git 13 | cd lambda-selenium/lambda-selenium-java/ 14 | ``` 15 | 16 | #### Packaging A New Function 17 | 18 | **_To package the jar and deploy it, run the following command inside the lambda-selenium-java directory_** 19 | ```shell 20 | gradle clean unzipLibs shadowJar deploy 21 | ``` 22 | 23 | This will package all of the required dependencies and code within the jar file. 24 | The libraries that we included in the resources folder will also be included in this jar. 25 | Once the jar has been built, you can find it in the build/libs folder. 26 | This jar will then be deployed as a new lambda function using the [serverless framework](https://serverless.com/). 27 | 28 | Note: If you change the name to a different lambda function in the [serverless.yml](../lambda-selenium-java/serverless.yml), you must change the name of the 29 | function that is invoked in the code in the class com.blackboard.testing.lambda.LambdaSeleniumService 30 | 31 | Now the function is ready to be invoked. 32 | 33 | **_Next, run the test suite 'ExampleTestSuite' inside the same shell :_** 34 | ```shell 35 | gradle clean test 36 | ``` 37 | 38 | When the tests are ran, they will be executed in parallel by invoking the Lambda function that was created. 39 | Once the tests are complete, open folder './build/screenshots' to view screenshots taken inside the running tests. 40 | If a test fails, the exception will be thrown for the test case and logged to the console. 41 | -------------------------------------------------------------------------------- /docs/node-tutorial.md: -------------------------------------------------------------------------------- 1 | # Lambda-Selenium: NodeJS Tutorial [![Build Status](https://travis-ci.org/blackboard/lambda-selenium.svg?branch=master)](https://travis-ci.org/blackboard/lambda-selenium) 2 | 3 | [![Basic Tutorial](./node_thumb.png)](./node_quickstart.webm "Basic Tutorial") 4 | 5 | 6 | #### Running The Example 7 | 1. [Download the latest release - node_lambda_function.zip](https://github.com/blackboard/lambda-selenium/releases/latest) 8 | 2. Create new function in AWS console 9 | 3. Upload zip file and configure options 10 | 4. Execute! 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/node_quickstart.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackboard/lambda-selenium/22f077f60bc9560fb96cc274f9f2a8615380025f/docs/node_quickstart.webm -------------------------------------------------------------------------------- /docs/node_thumb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blackboard/lambda-selenium/22f077f60bc9560fb96cc274f9f2a8615380025f/docs/node_thumb.png -------------------------------------------------------------------------------- /lambda-selenium-java/.gitignore: -------------------------------------------------------------------------------- 1 | src/main/resources/ 2 | -------------------------------------------------------------------------------- /lambda-selenium-java/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.github.jengelman.gradle.plugins:shadow:2.0.1' 9 | } 10 | } 11 | 12 | apply plugin: 'com.github.johnrengelman.shadow' 13 | 14 | group = 'com.blackboard.testing' 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | test { 21 | include '**/ExampleTestSuite.class' 22 | } 23 | 24 | dependencies { 25 | compile group: 'com.codeborne', name: 'selenide', version: '4.8' 26 | compile group: 'com.amazonaws', name: 'aws-lambda-java-events', version: '1.1.0' 27 | compile group: 'com.amazonaws', name: 'aws-lambda-java-core', version: '1.1.0' 28 | compile group: 'com.amazonaws', name: 'aws-java-sdk-lambda', version: '1.11.208' 29 | compile group: 'com.amazonaws.serverless', name: 'aws-serverless-java-container-jersey', version: '0.7' 30 | compile group: 'org.reflections', name: 'reflections', version: '0.9.10' 31 | compile group: 'junit', name: 'junit', version:'4.12' 32 | } 33 | 34 | task unzipLibs(type: Copy) { 35 | def libs = file('../bin/lib.zip') 36 | def resources = file("${projectDir}/src/main/resources/") 37 | 38 | from zipTree(libs) 39 | into resources 40 | } 41 | 42 | task zipLibs(type: Zip) { 43 | from processResources 44 | archiveName 'lib.zip' 45 | destinationDir(file('../bin/')) 46 | } 47 | 48 | shadowJar { 49 | from sourceSets.test.output 50 | configurations = [ project.configurations.testRuntime ] 51 | } 52 | 53 | task deploy(type: Exec) { 54 | commandLine 'sls', 'deploy' 55 | } 56 | -------------------------------------------------------------------------------- /lambda-selenium-java/serverless.yml: -------------------------------------------------------------------------------- 1 | # serverless.yml 2 | service: lambda-selenium 3 | 4 | package: 5 | artifact: build/libs/lambda-selenium-all.jar 6 | 7 | provider: 8 | name: aws 9 | stage: ${opt:stage, 'dev'} 10 | runtime: java8 11 | memorySize: 3008 12 | timeout: 300 13 | versionFunctions: false 14 | 15 | functions: 16 | 'lambda-selenium-demo': 17 | handler: 'com.blackboard.testing.lambda.LambdaTestHandler::handleRequest' 18 | name: lambda-selenium-function 19 | description: Lambda Selenium Demo Tests 20 | -------------------------------------------------------------------------------- /lambda-selenium-java/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'lambda-selenium' -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/common/LambdaBaseTest.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.common; 2 | 3 | import com.blackboard.testing.driver.LambdaWebDriverThreadLocalContainer; 4 | import com.blackboard.testing.lambda.LambdaTestHandler; 5 | import com.blackboard.testing.testcontext.EnvironmentDetector; 6 | import com.codeborne.selenide.Configuration; 7 | import com.codeborne.selenide.Selenide; 8 | import com.codeborne.selenide.WebDriverRunner; 9 | import org.junit.BeforeClass; 10 | import org.openqa.selenium.OutputType; 11 | import org.openqa.selenium.TakesScreenshot; 12 | 13 | public abstract class LambdaBaseTest { 14 | 15 | @BeforeClass 16 | public static void baseTestBeforeClass() { 17 | Configuration.browser = "chrome"; 18 | Configuration.reopenBrowserOnFail = false; 19 | 20 | if (EnvironmentDetector.inLambda()) { 21 | WebDriverRunner.webdriverContainer = new LambdaWebDriverThreadLocalContainer(); 22 | } 23 | } 24 | 25 | protected void screenshot(String description) { 26 | if (EnvironmentDetector.inLambda()) { 27 | LambdaTestHandler.addAttachment(description + ".png", 28 | ((TakesScreenshot) WebDriverRunner.getWebDriver()) 29 | .getScreenshotAs(OutputType.BYTES)); 30 | } else { 31 | Selenide.screenshot(description); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/driver/LambdaWebDriverFactory.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.driver; 2 | 3 | import com.codeborne.selenide.webdriver.WebDriverFactory; 4 | import org.openqa.selenium.Proxy; 5 | import org.openqa.selenium.WebDriver; 6 | import org.openqa.selenium.chrome.ChromeDriver; 7 | import org.openqa.selenium.chrome.ChromeOptions; 8 | 9 | public class LambdaWebDriverFactory extends WebDriverFactory { 10 | 11 | public LambdaWebDriverFactory() { 12 | System.setProperty("webdriver.chrome.driver", getLibLocation("chromedriver")); 13 | } 14 | 15 | private ChromeOptions getLambdaChromeOptions() { 16 | ChromeOptions options = new ChromeOptions(); 17 | options.setBinary(getLibLocation("chrome")); 18 | options.addArguments("--disable-gpu"); 19 | options.addArguments("--headless"); 20 | options.addArguments("--window-size=1366,768"); 21 | options.addArguments("--single-process"); 22 | options.addArguments("--no-sandbox"); 23 | options.addArguments("--user-data-dir=/tmp/user-data"); 24 | options.addArguments("--data-path=/tmp/data-path"); 25 | options.addArguments("--homedir=/tmp"); 26 | options.addArguments("--disk-cache-dir=/tmp/cache-dir"); 27 | return options; 28 | } 29 | 30 | private String getLibLocation(String lib) { 31 | return String.format("%s/lib/%s", System.getenv("LAMBDA_TASK_ROOT"), lib); 32 | } 33 | 34 | @Override 35 | public WebDriver createWebDriver(Proxy proxy) { 36 | return new ChromeDriver(getLambdaChromeOptions()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/driver/LambdaWebDriverThreadLocalContainer.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.driver; 2 | 3 | import com.codeborne.selenide.impl.WebDriverThreadLocalContainer; 4 | 5 | public class LambdaWebDriverThreadLocalContainer extends WebDriverThreadLocalContainer { 6 | 7 | public LambdaWebDriverThreadLocalContainer() { 8 | factory = new LambdaWebDriverFactory(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/LambdaSeleniumService.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import com.amazonaws.services.lambda.invoke.LambdaFunction; 4 | 5 | public interface LambdaSeleniumService { 6 | @LambdaFunction(functionName = "lambda-selenium-function") 7 | TestResult runTest(TestRequest testRequest); 8 | } 9 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/LambdaTestHandler.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import static com.blackboard.testing.lambda.logger.LoggerContainer.LOGGER; 4 | import static java.util.Optional.ofNullable; 5 | 6 | import com.blackboard.testing.lambda.exceptions.LambdaCodeMismatchException; 7 | import com.blackboard.testing.lambda.logger.Logger; 8 | import com.blackboard.testing.lambda.logger.LoggerContainer; 9 | import com.amazonaws.services.lambda.runtime.Context; 10 | import com.amazonaws.services.lambda.runtime.RequestHandler; 11 | import java.util.Optional; 12 | import org.junit.runner.JUnitCore; 13 | import org.junit.runner.Result; 14 | import org.junit.runners.BlockJUnit4ClassRunner; 15 | 16 | public class LambdaTestHandler implements RequestHandler { 17 | 18 | private static TestResult testResult; 19 | 20 | public LambdaTestHandler() { 21 | testResult = new TestResult(); 22 | } 23 | 24 | public TestResult handleRequest(TestRequest testRequest, Context context) { 25 | LoggerContainer.LOGGER = new Logger(context.getLogger()); 26 | System.setProperty("target.test.uuid", testRequest.getTestRunUUID()); 27 | 28 | Optional result = Optional.empty(); 29 | try { 30 | BlockJUnit4ClassRunner runner = new BlockJUnit4ClassRunner(getTestClass(testRequest)); 31 | runner.filter(new MethodFilter(testRequest.getFrameworkMethod())); 32 | 33 | result = ofNullable(new JUnitCore().run(runner)); 34 | } catch (Exception e) { 35 | testResult.setThrowable(e); 36 | LOGGER.log(e); 37 | } 38 | 39 | if (result.isPresent()) { 40 | testResult.setRunCount(result.get().getRunCount()); 41 | testResult.setRunTime(result.get().getRunTime()); 42 | LOGGER.log("Run count: " + result.get().getRunCount()); 43 | result.get().getFailures().forEach(failure -> { 44 | LOGGER.log(failure.getException()); 45 | testResult.setThrowable(failure.getException()); 46 | }); 47 | } 48 | 49 | return testResult; 50 | } 51 | 52 | private Class getTestClass(TestRequest testRequest) { 53 | LOGGER.log("Running Test: %s::%s", testRequest.getTestClass(), testRequest.getFrameworkMethod()); 54 | try { 55 | LOGGER.log(testRequest.getTestClass()); 56 | LOGGER.log(testRequest.getFrameworkMethod()); 57 | return Class.forName(testRequest.getTestClass()); 58 | } catch (ClassNotFoundException e) { 59 | LOGGER.log(e); 60 | throw new LambdaCodeMismatchException(testRequest.getTestClass()); 61 | } 62 | } 63 | 64 | public static void addAttachment(String fileName, byte[] attachment) { 65 | testResult.getAttachments().put(fileName, attachment); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/LambdaTestSuite.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import static com.blackboard.testing.lambda.logger.LoggerContainer.LOGGER; 4 | import static java.util.Optional.ofNullable; 5 | 6 | import com.blackboard.testing.common.LambdaBaseTest; 7 | import com.blackboard.testing.runner.ParallelParameterized; 8 | import com.blackboard.testing.testcontext.TestUUID; 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.Set; 15 | import org.apache.commons.io.FileUtils; 16 | import org.junit.runner.RunWith; 17 | import org.junit.runner.manipulation.Filter; 18 | import org.junit.runners.BlockJUnit4ClassRunner; 19 | import org.junit.runners.model.InitializationError; 20 | import org.reflections.Reflections; 21 | 22 | @RunWith(ParallelParameterized.class) 23 | public class LambdaTestSuite { 24 | 25 | private static List getTestClasses(String folderName) { 26 | Reflections reflections = new Reflections(folderName); 27 | Set> allClasses = reflections.getSubTypesOf(LambdaBaseTest.class); 28 | List classes = new ArrayList<>(); 29 | classes.addAll(allClasses); 30 | return classes; 31 | } 32 | 33 | protected static List getTestRequests(String folderName, Filter filter) { 34 | List requests = new ArrayList<>(); 35 | getTestClasses(folderName).forEach(testClass -> { 36 | try { 37 | new BlockJUnit4ClassRunner(testClass).getDescription().getChildren() 38 | .forEach(description -> { 39 | if (filter.shouldRun(description)) { 40 | TestRequest request = new TestRequest(description); 41 | request.setTestRunUUID(TestUUID.getTestUUID()); 42 | requests.add(request); 43 | } 44 | }); 45 | } catch (InitializationError e) { 46 | LOGGER.log(e); 47 | } 48 | }); 49 | return requests; 50 | } 51 | 52 | protected void writeAttachments(Map attachments) { 53 | File outputDirectory = new File(System.getProperty("user.dir") + "/build/screenshots/"); 54 | outputDirectory.mkdirs(); 55 | 56 | attachments.forEach((fileName, bytes) -> { 57 | try { 58 | FileUtils.writeByteArrayToFile(new File(outputDirectory, fileName), bytes); 59 | } catch (IOException e) { 60 | LOGGER.log(e); 61 | } 62 | }); 63 | } 64 | 65 | protected void logTestResult(TestRequest request, TestResult result) { 66 | LOGGER.log("Test %s:%s completed.", request.getTestClass(), request.getFrameworkMethod()); 67 | if (ofNullable(result.getThrowable()).isPresent()) { 68 | throw new RuntimeException(result.getThrowable()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/MethodFilter.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import org.junit.runner.Description; 4 | import org.junit.runner.manipulation.Filter; 5 | 6 | public class MethodFilter extends Filter { 7 | 8 | private final String methodName; 9 | 10 | public MethodFilter(String methodName) { 11 | this.methodName = methodName; 12 | } 13 | 14 | @Override 15 | public boolean shouldRun(Description description) { 16 | return description.getMethodName().equals(methodName); 17 | } 18 | 19 | @Override 20 | public String describe() { 21 | return "Includes tests with method name: " + methodName; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/TestInvoker.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import com.amazonaws.services.lambda.AWSLambdaClientBuilder; 4 | import com.amazonaws.services.lambda.invoke.LambdaInvokerFactory; 5 | 6 | public class TestInvoker { 7 | 8 | private final TestRequest request; 9 | 10 | public TestInvoker(TestRequest request) { 11 | this.request = request; 12 | } 13 | 14 | public TestResult run() { 15 | final LambdaSeleniumService lambdaService = LambdaInvokerFactory.builder() 16 | .lambdaClient(AWSLambdaClientBuilder.defaultClient()) 17 | .build(LambdaSeleniumService.class); 18 | 19 | return lambdaService.runTest(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/TestRequest.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import static java.util.Optional.ofNullable; 4 | 5 | import org.junit.runner.Description; 6 | import org.junit.runners.model.FrameworkMethod; 7 | 8 | public class TestRequest { 9 | 10 | private String testClass; 11 | private String frameworkMethod; 12 | private String testRunUUID; 13 | 14 | public TestRequest() {} 15 | 16 | public TestRequest(Class clazz, FrameworkMethod method) { 17 | testClass = clazz.getCanonicalName(); 18 | frameworkMethod = method.getMethod().getName(); 19 | } 20 | 21 | public TestRequest(Description description) { 22 | testClass = description.getClassName(); 23 | frameworkMethod = description.getMethodName(); 24 | } 25 | 26 | public String getTestClass() { 27 | return testClass; 28 | } 29 | 30 | public void setTestClass(String testClass) { 31 | this.testClass = testClass; 32 | } 33 | 34 | public String getFrameworkMethod() { 35 | return frameworkMethod; 36 | } 37 | 38 | public void setFrameworkMethod(String method) { 39 | this.frameworkMethod = method; 40 | } 41 | 42 | public String getTestRunUUID() { 43 | return testRunUUID; 44 | } 45 | 46 | public void setTestRunUUID(String testRunUUID) { 47 | this.testRunUUID = testRunUUID; 48 | } 49 | 50 | @Override 51 | public String toString() { 52 | return ofNullable(testClass).orElse("null") + ":" + ofNullable(frameworkMethod).orElse("null"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/TestResult.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda; 2 | 3 | import static java.util.Optional.ofNullable; 4 | 5 | import com.fasterxml.jackson.annotation.JsonIgnore; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import org.junit.runner.Result; 9 | 10 | public class TestResult { 11 | 12 | private Throwable throwable; 13 | private int runCount; 14 | private int failureCount; 15 | private int ignoreCount; 16 | private long runTime; 17 | private Map attachments = new HashMap<>(); 18 | 19 | public TestResult() {} 20 | 21 | public TestResult(Result result) { 22 | runCount = result.getRunCount(); 23 | failureCount = result.getFailureCount(); 24 | ignoreCount = result.getIgnoreCount(); 25 | runTime = result.getRunTime(); 26 | if (!wasSuccessful()) { 27 | throwable = result.getFailures().get(0).getException(); 28 | } 29 | } 30 | 31 | public Throwable getThrowable() { 32 | return throwable; 33 | } 34 | 35 | public void setThrowable(Throwable throwable) { 36 | this.throwable = throwable; 37 | } 38 | 39 | public int getRunCount() { 40 | return runCount; 41 | } 42 | 43 | public void setRunCount(int runCount) { 44 | this.runCount = runCount; 45 | } 46 | 47 | public int getFailureCount() { 48 | return failureCount; 49 | } 50 | 51 | public void setFailureCount(int failureCount) { 52 | this.failureCount = failureCount; 53 | } 54 | 55 | public int getIgnoreCount() { 56 | return ignoreCount; 57 | } 58 | 59 | public void setIgnoreCount(int ignoreCount) { 60 | this.ignoreCount = ignoreCount; 61 | } 62 | 63 | public long getRunTime() { 64 | return runTime; 65 | } 66 | 67 | public void setRunTime(long runTime) { 68 | this.runTime = runTime; 69 | } 70 | 71 | public Map getAttachments() { 72 | return attachments; 73 | } 74 | 75 | public void setAttachments(Map attachments) { 76 | this.attachments = attachments; 77 | } 78 | 79 | @JsonIgnore 80 | public boolean wasSuccessful() { 81 | return !ofNullable(throwable).isPresent(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/exceptions/LambdaCodeMismatchException.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda.exceptions; 2 | 3 | public class LambdaCodeMismatchException extends RuntimeException { 4 | 5 | public LambdaCodeMismatchException(String className) { 6 | super(String.format("Test class %s was not found, the test code is likely newer than the code uploaded to lambda. Redeploy and try again", className)); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/logger/Logger.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda.logger; 2 | 3 | import static java.util.Optional.ofNullable; 4 | 5 | import com.amazonaws.services.lambda.runtime.LambdaLogger; 6 | import org.glassfish.jersey.internal.util.ExceptionUtils; 7 | 8 | public class Logger { 9 | 10 | private final LambdaLogger lambdaLogger; 11 | private boolean firstMessageLogged = false; 12 | 13 | public Logger(LambdaLogger lambdaLogger) { 14 | this.lambdaLogger = lambdaLogger; 15 | } 16 | 17 | private void logMessage(String message) { 18 | String newLine = ""; 19 | if (!firstMessageLogged) { 20 | newLine += "\n"; 21 | } else { 22 | firstMessageLogged = true; 23 | } 24 | lambdaLogger.log(newLine + ofNullable(message).orElse("null")); 25 | } 26 | 27 | public void log(Object message) { 28 | logMessage(String.format("%s: %s", message.getClass().getSimpleName(), ofNullable(message.toString()).orElse("null"))); 29 | } 30 | 31 | public void log(String message) { 32 | logMessage(message); 33 | } 34 | 35 | public void log(Throwable t) { 36 | logMessage(ExceptionUtils.exceptionStackTraceAsString(t)); 37 | } 38 | 39 | public void log(String format, Object... args) { 40 | logMessage(String.format(format, args)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/logger/LoggerContainer.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda.logger; 2 | 3 | public class LoggerContainer { 4 | 5 | public static Logger LOGGER = new Logger(new MockLambdaConsoleLogger()); 6 | } 7 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/lambda/logger/MockLambdaConsoleLogger.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.lambda.logger; 2 | 3 | import com.amazonaws.services.lambda.runtime.LambdaLogger; 4 | 5 | public class MockLambdaConsoleLogger implements LambdaLogger { 6 | 7 | @Override 8 | public void log(String string) { 9 | System.out.println(string); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/runner/ParallelParameterized.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.runner; 2 | 3 | import static java.lang.Integer.parseInt; 4 | 5 | import org.junit.runners.Parameterized; 6 | 7 | public class ParallelParameterized extends Parameterized { 8 | 9 | public ParallelParameterized(Class klass) throws Throwable { 10 | super(klass); 11 | 12 | String maxThreads = System.getProperty("testing.parallel.threads", "256"); 13 | String parallelTimeout = System.getProperty("testing.parallel.timeout", "10"); 14 | 15 | setScheduler(new ThreadPoolScheduler(parseInt(maxThreads), parseInt(parallelTimeout))); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/runner/ThreadPoolScheduler.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.runner; 2 | 3 | import java.util.concurrent.ExecutorService; 4 | import java.util.concurrent.Executors; 5 | import java.util.concurrent.TimeUnit; 6 | import org.junit.runners.model.RunnerScheduler; 7 | 8 | public class ThreadPoolScheduler implements RunnerScheduler { 9 | 10 | private int timeoutMinutes; 11 | private ExecutorService executor; 12 | 13 | public ThreadPoolScheduler(int maxThreads, int timeoutMinutes) { 14 | this.timeoutMinutes = timeoutMinutes; 15 | executor = Executors.newFixedThreadPool(maxThreads); 16 | } 17 | 18 | @Override 19 | public void schedule(Runnable childStatement) { 20 | executor.submit(childStatement); 21 | } 22 | 23 | @Override 24 | public void finished() { 25 | executor.shutdown(); 26 | try { 27 | executor.awaitTermination(timeoutMinutes, TimeUnit.MINUTES); 28 | } catch (InterruptedException e) { 29 | throw new RuntimeException(e); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/testcontext/EnvironmentDetector.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.testcontext; 2 | 3 | import static java.util.Optional.ofNullable; 4 | 5 | public class EnvironmentDetector { 6 | 7 | public static boolean inLambda() { 8 | return ofNullable(System.getenv("LAMBDA_RUNTIME_DIR")).isPresent(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/main/java/com/blackboard/testing/testcontext/TestUUID.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.testcontext; 2 | 3 | import java.util.UUID; 4 | 5 | public class TestUUID { 6 | 7 | private TestUUID() {} 8 | 9 | private static final String DEFAULT_TEST_UUID = UUID.randomUUID().toString(); 10 | 11 | public static String getTestUUID() { 12 | return System.getProperty("target.test.uuid", DEFAULT_TEST_UUID); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/test/java/com/blackboard/testing/tests/ExampleTestSuite.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.tests; 2 | 3 | import static com.blackboard.testing.lambda.logger.LoggerContainer.LOGGER; 4 | 5 | import com.blackboard.testing.lambda.LambdaTestSuite; 6 | import com.blackboard.testing.lambda.TestInvoker; 7 | import com.blackboard.testing.lambda.TestRequest; 8 | import com.blackboard.testing.lambda.TestResult; 9 | import java.util.Collection; 10 | import org.junit.Test; 11 | import org.junit.experimental.categories.Categories.CategoryFilter; 12 | import org.junit.runners.Parameterized.Parameters; 13 | 14 | public class ExampleTestSuite extends LambdaTestSuite { 15 | 16 | private static final CategoryFilter filter = CategoryFilter.include(Test.class); 17 | private TestRequest testRequest; 18 | 19 | public ExampleTestSuite(TestRequest testRequest) { 20 | this.testRequest = testRequest; 21 | } 22 | 23 | @Parameters(name = "{0}") 24 | public static Collection testRequests() { 25 | LOGGER.log("Running " + filter.describe()); 26 | 27 | return getTestRequests("com.blackboard.testing.tests", filter); 28 | } 29 | 30 | @Test 31 | public void runTest() { 32 | TestInvoker testInvoker = new TestInvoker(testRequest); 33 | TestResult testResult = testInvoker.run(); 34 | writeAttachments(testResult.getAttachments()); 35 | logTestResult(testRequest, testResult); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lambda-selenium-java/src/test/java/com/blackboard/testing/tests/LambdaTest.java: -------------------------------------------------------------------------------- 1 | package com.blackboard.testing.tests; 2 | 3 | import static com.codeborne.selenide.Selenide.open; 4 | import static com.codeborne.selenide.Selenide.title; 5 | import static org.hamcrest.CoreMatchers.containsString; 6 | import static org.junit.Assert.assertThat; 7 | 8 | import com.blackboard.testing.common.LambdaBaseTest; 9 | import org.junit.Test; 10 | import org.junit.experimental.categories.Category; 11 | 12 | @Category(Test.class) 13 | public class LambdaTest extends LambdaBaseTest { 14 | 15 | @Test 16 | public void googleTest() { 17 | open("http://www.google.com/"); 18 | screenshot("google-home-page"); 19 | assertThat(title(), containsString("Google")); 20 | } 21 | 22 | @Test 23 | public void blackboardTest() { 24 | open("http://www.blackboard.com/"); 25 | screenshot("blackboard-home-page"); 26 | assertThat(title(), containsString("Blackboard")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /lambda-selenium-node/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.handler = (event, context, callback) => { 4 | var webdriver = require('selenium-webdriver'); 5 | var chrome = require('selenium-webdriver/chrome'); 6 | var builder = new webdriver.Builder().forBrowser('chrome'); 7 | var chromeOptions = new chrome.Options(); 8 | const defaultChromeFlags = [ 9 | '--headless', 10 | '--disable-gpu', 11 | '--window-size=1280x1696', // Letter size 12 | '--no-sandbox', 13 | '--user-data-dir=/tmp/user-data', 14 | '--hide-scrollbars', 15 | '--enable-logging', 16 | '--log-level=0', 17 | '--v=99', 18 | '--single-process', 19 | '--data-path=/tmp/data-path', 20 | '--ignore-certificate-errors', 21 | '--homedir=/tmp', 22 | '--disk-cache-dir=/tmp/cache-dir' 23 | ]; 24 | 25 | chromeOptions.setChromeBinaryPath("/var/task/lib/chrome"); 26 | chromeOptions.addArguments(defaultChromeFlags); 27 | builder.setChromeOptions(chromeOptions); 28 | 29 | var driver = builder.build(); 30 | driver.get(event.url); 31 | driver.getTitle().then(function(title) { 32 | 33 | console.log("Page title for " + event.url + " is " + title) 34 | callback(null, 'Page title for ' + event.url + ' is ' + title); 35 | }); 36 | 37 | driver.quit(); 38 | }; 39 | 40 | 41 | -------------------------------------------------------------------------------- /serverless-task-runner/README.md: -------------------------------------------------------------------------------- 1 | Use Lambda and AWS managed services for massive parallel processing on the free tier! 100hrs of compute time per month free! 2 | Run Selenium tests or other workloads via a parameterized shell command for fast and cheap distributed computing. 3 | 4 | [Strafer-Duty](https://github.com/tsu-denim/strafer-duty) 5 | --------------------------------------------------------------------------------