├── .gitignore ├── .travis.yml ├── Dockerfile ├── HowToSetupAVPC.md ├── README.md ├── license.txt ├── pom.xml ├── src ├── main │ ├── java │ │ └── com │ │ │ └── rmn │ │ │ └── qa │ │ │ ├── AutomationCapabilityMatcher.java │ │ │ ├── AutomationConstants.java │ │ │ ├── AutomationContext.java │ │ │ ├── AutomationDynamicNode.java │ │ │ ├── AutomationRequestMatcher.java │ │ │ ├── AutomationRunContext.java │ │ │ ├── AutomationRunRequest.java │ │ │ ├── AutomationUtils.java │ │ │ ├── NodesCouldNotBeStartedException.java │ │ │ ├── RegistryRetriever.java │ │ │ ├── RequestMatcher.java │ │ │ ├── aws │ │ │ ├── AwsTagReporter.java │ │ │ ├── AwsVmManager.java │ │ │ └── VmManager.java │ │ │ ├── servlet │ │ │ ├── AutomationStatusServlet.java │ │ │ ├── AutomationTestRunServlet.java │ │ │ └── StatusServlet.java │ │ │ └── task │ │ │ ├── AbstractAutomationCleanupTask.java │ │ │ ├── AutomationHubCleanupTask.java │ │ │ ├── AutomationNodeCleanupTask.java │ │ │ ├── AutomationNodeRegistryTask.java │ │ │ ├── AutomationReaperTask.java │ │ │ └── AutomationRunCleanupTask.java │ └── resources │ │ ├── .s3cfg │ │ ├── aws.properties.default │ │ ├── hub.static.json │ │ ├── logback.xml │ │ ├── node.linux.json │ │ ├── node.windows.json │ │ └── nodeConfigTemplate.json └── test │ └── java │ └── com │ └── rmn │ └── qa │ ├── AbstractAutomationCleanupTaskTest.java │ ├── AutomationCapabilityMatcherTest.java │ ├── AutomationDynamicNodeTest.java │ ├── AutomationRequestMatcherTest.java │ ├── AutomationRunContextTest.java │ ├── AutomationRunRequestTest.java │ ├── AutomationUtilsTest.java │ ├── MockHttpServletRequest.java │ ├── MockHttpServletResponse.java │ ├── MockRemoteProxy.java │ ├── MockRequestMatcher.java │ ├── MockVmManager.java │ ├── aws │ ├── AwsTagReporterTest.java │ ├── MockAmazonEc2Client.java │ ├── MockManageVm.java │ └── VmManagerTest.java │ ├── servlet │ ├── AutomationTestRunServletTest.java │ └── MockAutomationTestRunServlet.java │ └── task │ ├── AutomationHubCleanupTaskTest.java │ ├── AutomationNodeCleanupTaskTest.java │ ├── AutomationNodeRegistryTaskTest.java │ ├── AutomationReaperTaskTest.java │ ├── AutomationRunCleanupTaskTest.java │ ├── MockAutomationHubCleanupTask.java │ ├── MockAutomationNodeCleanupTask.java │ ├── MockAutomationNodeRegistryTask.java │ └── MockAutomationRunCleanupTask.java └── startGrid /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled source # 2 | ################### 3 | *.com 4 | *.class 5 | *.dll 6 | *.exe 7 | *.o 8 | *.so 9 | 10 | # Packages # 11 | ############ 12 | # it's better to unpack these files and commit the raw source 13 | # git has its own built in compression methods 14 | *.7z 15 | *.dmg 16 | *.gz 17 | *.iso 18 | *.jar 19 | *.rar 20 | *.tar 21 | *.zip 22 | 23 | # Logs, databases, and properties # 24 | ###################### 25 | *.log* 26 | *.sql 27 | *.sqlite 28 | *.properties 29 | 30 | # OS generated files # 31 | ###################### 32 | .DS_Store 33 | .DS_Store? 34 | ._* 35 | .Spotlight-V100 36 | .Trashes 37 | Icon? 38 | ehthumbs.db 39 | Thumbs.db 40 | 41 | # IntelliJ # 42 | ############ 43 | .idea 44 | *.iml 45 | 46 | # Eclipse cruft # 47 | ################# 48 | .project 49 | .metadata 50 | bin/** 51 | tmp/** 52 | tmp/**/* 53 | *.tmp 54 | *.bak 55 | *.swp 56 | *~.nib 57 | local.properties 58 | .classpath 59 | .settings/ 60 | .loadpath 61 | 62 | # External tool builders 63 | .externalToolBuilders/ 64 | 65 | # Locally stored "Eclipse launch configurations" 66 | *.launch 67 | 68 | # CDT-specific 69 | .cproject 70 | 71 | # PDT-specific 72 | .buildpath 73 | 74 | # Maven stuff 75 | target/ 76 | test-output/ 77 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk7 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:7-jdk 2 | ADD target /target 3 | ADD startGrid /usr/bin/startGrid 4 | RUN chmod +x /usr/bin/startGrid 5 | CMD ["/usr/bin/startGrid"] 6 | -------------------------------------------------------------------------------- /HowToSetupAVPC.md: -------------------------------------------------------------------------------- 1 | 1.) In your Amazon Web Services console, click the 'VPC' link. This should take you to the VPC Dashboard. 2 | 3 | 2.) Click 'Start VPC Wizard' at the top of the page. You should now be on the Step 1: Select a VPC Configuration page. 4 | 5 | 3.) Select the 'VPC with Public and Private Subnets' page. Click the 'Select' button on the right. You should now be on Step 2 6 | 7 | 4.) Give the VPC a name, such as "Selenium VPC". Name the Public subnet to be "Selenium Public Subnet" and the Private subnet to be "Selenium Private Subnet". Leave all other values as default. 8 | 9 | 5.) Click 'Create VPC' and then wait a few moments for the wizard to complete. Click the OK button after it finishes. 10 | 11 | 6.) In the VPC Dashboard, click on 'Your VPCs'. Find the VPC you created above, and note the VPC ID. 12 | 13 | 7.) In the VPC Dashboard, click on 'Subnets'. Find the private subnet you created above, and note its subnet ID (e.g. subnet-9ed94444). This will be the subnet you use to launch your nodes in in your properties file. The value will be for the \_linux\_node\_ami value, where is the name of your region, like east or west. So if you're in the 'east' region, the key would be east\_linux\_node\_ami. 14 | 15 | 8.) Select the public subnet you created named 'Selenium Public Subnet' and click on 'Inbound Rules'. You will want to add a rule by clicking 'Edit' and then 'Add another rule'. Type should be 'SSH (22)' for SSH access. If your externally facing IP address was 43.68.39.29, you would put 43.68.39.29/30 for the Source, so basically add "/30" to your externally facing IP address. If you need access to any additional ports, you should add them here. 16 | 17 | 9.) In the VPC Dashboard, click on 'Security Groups'. Find the security group for your VPC ID in step 6. This will be used for the \_security\_group value in your configuration file. 18 | 19 | 10.) Click 'Services' at the top, then click on 'EC2'. Now click on 'Instances' in the left hand side. Find the running instance that represents the NAT the wizard created (should be associated with your VPC ID from step 6). Select the instance, and there should be a public ip address under 'Public IP' in the 'Description' section at the bottom. **This will be the IP address that you must get whitelisted in your networking setup in order for the nodes to be able to hit your test environment. This is very important or your tests will not be able to run successfully.** -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Project has moved to https://github.com/mhardin/SeleniumGridScaler 2 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | com.retailmenot 6 | selenium-grid-scaler 7 | 0.9-SNAPSHOT 8 | 9 | 10 | 11 | Matthew Hardin 12 | mhardin.github@gmail.com 13 | RetailMeNot, inc 14 | https://github.com/mhardin 15 | 16 | 17 | 18 | 19 | 20 | GNU General Public License (GPL) 21 | http://www.gnu.org/licenses/gpl.txt 22 | 23 | 24 | 25 | 26 | 27 | org.seleniumhq.selenium 28 | selenium-server 29 | 2.42.2 30 | 31 | 32 | com.amazonaws 33 | aws-java-sdk 34 | 1.8.0 35 | 36 | 37 | 38 | org.apache.httpcomponents 39 | httpclient 40 | 41 | 42 | 43 | 44 | org.apache.commons 45 | commons-lang3 46 | 3.1 47 | 48 | 49 | commons-codec 50 | commons-codec 51 | 1.4 52 | 53 | 54 | junit 55 | junit 56 | 4.8.1 57 | test 58 | 59 | 60 | ch.qos.logback 61 | logback-classic 62 | 1.0.9 63 | 64 | 65 | ch.qos.logback 66 | logback-core 67 | 1.0.9 68 | 69 | 70 | org.slf4j 71 | slf4j-api 72 | 1.7.5 73 | 74 | 75 | nl.jqno.equalsverifier 76 | equalsverifier 77 | 1.4.1 78 | test 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-shade-plugin 88 | 1.7.1 89 | 90 | automation-grid 91 | false 92 | 93 | 94 | *:* 95 | 96 | META-INF/*.SF 97 | META-INF/*.DSA 98 | META-INF/*.RSA 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | package 107 | 108 | shade 109 | 110 | 111 | 112 | 113 | 114 | maven-assembly-plugin 115 | 2.3 116 | 117 | 118 | jar-with-dependencies 119 | 120 | 121 | 122 | 123 | 124 | org.apache.maven.plugins 125 | maven-surefire-plugin 126 | 2.12 127 | 128 | 129 | 130 | org.codehaus.mojo 131 | cobertura-maven-plugin 132 | 2.6 133 | 134 | 135 | 136 | 137 | com/rmn/qa/servlet/**/*StatusServlet.class 138 | 139 | 140 | 141 | true 142 | 143 | 144 | com.rmn.qa.task 145 | 80 146 | 80 147 | 148 | 149 | com.rmn.qa 150 | 80 151 | 80 152 | 153 | 154 | com.rmn.qa.aws 155 | 70 156 | 80 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | clean 165 | check 166 | 167 | 168 | 169 | 170 | 171 | 172 | org.apache.maven.plugins 173 | maven-compiler-plugin 174 | 2.4 175 | true 176 | 177 | 1.7 178 | 1.7 179 | 180 | 181 | 182 | 183 | org.apache.maven.plugins 184 | maven-source-plugin 185 | 2.2.1 186 | 187 | 188 | attach-sources 189 | 190 | jar 191 | 192 | 193 | 194 | 195 | 196 | org.apache.maven.plugins 197 | maven-javadoc-plugin 198 | 2.9 199 | 200 | 201 | attach-javadocs 202 | 203 | jar 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | org.codehaus.mojo 215 | cobertura-maven-plugin 216 | 2.5.1 217 | 218 | 219 | 220 | 221 | 222 | scm:git:git@github.com:RetailMeNot/SeleniumGridScaler.git 223 | scm:git:git@github.com:RetailMeNot/SeleniumGridScaler.git 224 | scm:git:git@github.com:RetailMeNot/SeleniumGridScaler.git 225 | 0.9 226 | 227 | 228 | 229 | 230 | ossrh 231 | https://oss.sonatype.org/content/repositories/snapshots 232 | 233 | 234 | ossrh 235 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationCapabilityMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa; 13 | 14 | import com.google.common.annotations.VisibleForTesting; 15 | import org.apache.commons.lang3.StringUtils; 16 | import org.openqa.grid.internal.utils.DefaultCapabilityMatcher; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | 20 | import java.util.HashSet; 21 | import java.util.Map; 22 | import java.util.Set; 23 | 24 | /** 25 | * Custom CapabilityMatcher which will not match a node that is marked as Expired/Terminated, which will happen 26 | * via any running {@link com.rmn.qa.task.AutomationNodeCleanupTask AutomationNodeCleanupTasks} 27 | * @author mhardin 28 | */ 29 | public class AutomationCapabilityMatcher extends DefaultCapabilityMatcher { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(AutomationCapabilityMatcher.class); 32 | 33 | // We can add additional capability keys we want to check here 34 | @VisibleForTesting 35 | final Set additionalConsiderations = new HashSet<>(); 36 | 37 | public AutomationCapabilityMatcher() { 38 | super(); 39 | String propertyValue = System.getProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME); 40 | if(!StringUtils.isEmpty(propertyValue)) { 41 | if(propertyValue.contains(",")) { 42 | String[] capabilities = propertyValue.split(","); 43 | for(String capability : capabilities) { 44 | log.info("Adding capability: " + capability); 45 | additionalConsiderations.add(capability); 46 | } 47 | } else { 48 | log.info("Adding capability from property value: " + propertyValue); 49 | additionalConsiderations.add(propertyValue); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public boolean matches(Map nodeCapability,Map requestedCapability) { 56 | // First we need to check any additional capabilities that may exist in the requested set. We're iterating over 57 | // additionalConsiderations as its likely to be a smaller collection for now than the requestedCapabilities 58 | for(String s : additionalConsiderations) { 59 | Object requestedCapabilityValue = requestedCapability.get(s); 60 | if(requestedCapabilityValue != null) { 61 | if(!requestedCapabilityValue.equals(nodeCapability.get(s))) { 62 | return false; 63 | } 64 | } 65 | } 66 | // If neither expected config value exists, go ahead and default to the default matching behavior 67 | // as this node is most likely not a dynamically started node 68 | if(!nodeCapability.containsKey(AutomationConstants.INSTANCE_ID)) { 69 | return super.matches(nodeCapability,requestedCapability); 70 | } 71 | String instanceId = (String)nodeCapability.get(AutomationConstants.INSTANCE_ID); 72 | AutomationRunContext context = AutomationContext.getContext(); 73 | // If the run that spun up these hubs is still happening, just perform the default matching behavior 74 | // as that run is the one that requested these nodes. 75 | AutomationDynamicNode node = context.getNode(instanceId); 76 | if(node != null && (node.getStatus() == AutomationDynamicNode.STATUS.EXPIRED || node.getStatus() == AutomationDynamicNode.STATUS.TERMINATED) ) { 77 | log.debug(String.format("Node [%s] will not be used to match a request as it is expired/terminated",instanceId)); 78 | // If the run that spun these hubs up is not in progress AND this node has been flagged to shutdown, 79 | // do not match this node up to fulfill a test request 80 | return false; 81 | } else { 82 | // If the node couldn't be retrieved or was not expired/terminated, then we should just use the default matching behavior 83 | return super.matches(nodeCapability,requestedCapability); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationConstants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa; 13 | 14 | /** 15 | * Constants in use across various hub/node configs 16 | * @author mhardin 17 | */ 18 | public interface AutomationConstants { 19 | 20 | // These NODE_CONFIG_* constants represent key names in the node configs 21 | String CONFIG_CREATED_DATE = "createdDate"; 22 | String CONFIG_MAX_SESSION = "maxSession"; 23 | String CONFIG_BROWSER = "createdBrowser"; 24 | String CONFIG_OS = "createdOs"; 25 | // This is the value that will be in the desired capabilities that our node registers with 26 | // Runtime value of the hub instance id that gets passed in as a system property. Also used in the node config 27 | String INSTANCE_ID = "instanceId"; 28 | // IP address of the hub that will be passed in as a system property 29 | String IP_ADDRESS = "ipAddress"; 30 | // Test run UUID 31 | String UUID = "uuid"; 32 | String WINDOWS_PROPERTY_NAME="node.windows.json"; 33 | String LINUX_PROPERTY_NAME="node.linux.json"; 34 | String EXTRA_CAPABILITIES_PROPERTY_NAME="extraCapabilities"; 35 | String AWS_ACCESS_KEY="awsAccessKey"; 36 | String AWS_PRIVATE_KEY="awsSecretKey"; 37 | String AWS_DEFAULT_RESOURCE_NAME= "aws.properties.default"; 38 | String REAPER_THREAD_CONFIG = "useReaperThread"; 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationContext.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa; 13 | 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | /** 18 | * Context to store a singleton {@link com.rmn.qa.AutomationRunContext AutomationRunContext} object 19 | * @author mhardin 20 | */ 21 | public class AutomationContext { 22 | 23 | private static final Logger log = LoggerFactory.getLogger(AutomationContext.class); 24 | private static final int DEFAULT_MAX_THREAD_COUNT = 150; 25 | private static AutomationRunContext context = new AutomationRunContext(); 26 | 27 | // Singleton to maintain a context object 28 | 29 | /** 30 | * Returns the singleton {@link com.rmn.qa.AutomationRunContext context} instance 31 | * @return 32 | */ 33 | public static AutomationRunContext getContext() { 34 | return AutomationContext.context; 35 | } 36 | 37 | /** 38 | * Clears out the previous context. Used for unit testing 39 | */ 40 | public static void refreshContext() { 41 | AutomationContext.context = new AutomationRunContext(); 42 | } 43 | 44 | static { 45 | String totalNodeCount = System.getProperty("totalNodeCount"); 46 | // Default to 150 if node count was not passed in 47 | if(totalNodeCount == null) { 48 | getContext().setTotalNodeCount(AutomationContext.DEFAULT_MAX_THREAD_COUNT); 49 | log.warn("Defaulting node count to " + AutomationContext.DEFAULT_MAX_THREAD_COUNT); 50 | } else { 51 | getContext().setTotalNodeCount(Integer.parseInt(totalNodeCount)); 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationDynamicNode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa; 13 | 14 | import java.util.Calendar; 15 | import java.util.Date; 16 | 17 | /** 18 | * Represents a dynamically started node that is used to run tests 19 | */ 20 | public final class AutomationDynamicNode implements Comparable { 21 | 22 | /**
 23 |      *  RUNNING    - node is running and no further action needs to be taken
 24 |      *  EXPIRED    - node has passed its expiration date and needs to be terminated.  A node will not
 25 |      *               be marked expired if there is sufficient load on the system to require the node resources
 26 |      *  TERMINATED - node has been successfully terminated through the EC2 API
 27 |      * 
28 | **/ 29 | public enum STATUS { RUNNING,EXPIRED,TERMINATED }; 30 | 31 | // This is used to set how far past the created date that the node will 32 | // be marked for termination 33 | private static final int NODE_INTERVAL_LIFETIME = 55; // 55 minutes 34 | 35 | // TODO: Refactor this to be AutomationRunRequest 36 | private final String uuid,instanceId,browser,os; 37 | private Date startDate,endDate; 38 | private final int nodeCapacity; 39 | private STATUS status; 40 | 41 | /** 42 | * Constructor to create a new node representing instance 43 | * @param uuid UUID of the test run that created this node 44 | * @param instanceId Instance ID of the instance this node represents 45 | * @param browser Requested browser of the test run that created this node 46 | * @param os Requested OS of the test run that created this node 47 | * @param startDate Date that this node was started 48 | * @param nodeCapacity Maximum test capacity that this node can run 49 | */ 50 | public AutomationDynamicNode(String uuid,String instanceId,String browser, String os,Date startDate,int nodeCapacity){ 51 | this.uuid = uuid; 52 | this.instanceId = instanceId; 53 | this.browser = browser; 54 | this.os = os; 55 | this.startDate = startDate; 56 | this.endDate = computeEndDate(startDate); 57 | this.nodeCapacity = nodeCapacity; 58 | this.status = STATUS.RUNNING; 59 | } 60 | 61 | /** 62 | * Updates the status of this node. 63 | * @param status 64 | */ 65 | public void updateStatus(STATUS status) { 66 | this.status = status; 67 | } 68 | 69 | /** 70 | * Computes the end date for this node based on the pre configured 71 | * end time 72 | * @param dateStarted 73 | * @return 74 | */ 75 | private Date computeEndDate(Date dateStarted) { 76 | Calendar c = Calendar.getInstance(); 77 | c.setTime(dateStarted); 78 | c.add(Calendar.MINUTE, AutomationDynamicNode.NODE_INTERVAL_LIFETIME); // number of days to add 79 | return c.getTime(); 80 | } 81 | 82 | /** 83 | * Returns the UUID for this node (will be the UUID of the run that 84 | * resulted in this node being started) 85 | * @return 86 | */ 87 | public String getUuid() { 88 | return uuid; 89 | } 90 | 91 | /** 92 | * Returns the instance id of this node 93 | * @return 94 | */ 95 | public String getInstanceId() { 96 | return instanceId; 97 | } 98 | 99 | /** 100 | * Returns the browser of this node. Will be the browser of the 101 | * run that resulted in this node being started 102 | * @return 103 | */ 104 | public String getBrowser() { 105 | return browser; 106 | } 107 | 108 | /** 109 | * Returns the OS of this node 110 | * @return 111 | */ 112 | public String getOs() { 113 | return os; 114 | } 115 | 116 | /** 117 | * Returns the date that his node was started. This will 118 | * be the time that the node was requested and not necessarily started 119 | * @return 120 | */ 121 | public Date getStartDate() { 122 | return startDate; 123 | } 124 | 125 | /** 126 | * Returns the currently scheduled end date of this node. Note 127 | * that this can change as this node end date gets pushed back. 128 | * @return 129 | */ 130 | public Date getEndDate() { 131 | return endDate; 132 | } 133 | 134 | /** 135 | * Sets the end date for this node. 136 | * @param endDate Date which will be set for the end date 137 | */ 138 | public void setEndDate(Date endDate) { 139 | this.endDate = endDate; 140 | } 141 | 142 | /** 143 | * Increments the end date by an hour. Useful for moving the end date into the next Amazon billing cycle 144 | */ 145 | public void incrementEndDateByOneHour() { 146 | // Add 60 minutes so we're as close to the hour as we can be instead of adding 55 again 147 | setEndDate(AutomationUtils.modifyDate(getEndDate(),60,Calendar.MINUTE)); 148 | } 149 | 150 | /** 151 | * Returns the total node capacity of this node (total number of browsers 152 | * that can run simultaneously) 153 | * @return 154 | */ 155 | public int getNodeCapacity() { 156 | return nodeCapacity; 157 | } 158 | 159 | /** 160 | * Returns the current status of this node. 161 | * @return 162 | */ 163 | public STATUS getStatus(){ 164 | return status; 165 | } 166 | 167 | @Override 168 | public int compareTo(AutomationDynamicNode o) { 169 | return this.startDate.compareTo(getStartDate()); 170 | } 171 | 172 | @Override 173 | public boolean equals(Object o) { 174 | if (this == o) { 175 | return true; 176 | } 177 | if (o == null || getClass() != o.getClass()) { 178 | return false; 179 | } 180 | 181 | AutomationDynamicNode that = (AutomationDynamicNode) o; 182 | 183 | if (!instanceId.equals(that.instanceId)) { 184 | return false; 185 | } 186 | if (!uuid.equals(that.uuid)) { 187 | return false; 188 | } 189 | 190 | return true; 191 | } 192 | 193 | @Override 194 | public int hashCode() { 195 | int result = uuid.hashCode(); 196 | result = 31 * result + instanceId.hashCode(); 197 | return result; 198 | } 199 | 200 | @Override 201 | public String toString() { 202 | return String.format("Node - UUID: %s Instance ID: %s Created Date: %s",uuid,instanceId, startDate); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationRequestMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa; 13 | 14 | import org.openqa.grid.internal.ProxySet; 15 | import org.openqa.grid.internal.RemoteProxy; 16 | import org.openqa.grid.internal.TestSession; 17 | import org.openqa.grid.internal.TestSlot; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import java.util.Calendar; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /** 26 | * Computes how many free/available resources there are for a given browser, browser version, and OS 27 | * @author mhardin 28 | */ 29 | public class AutomationRequestMatcher implements RequestMatcher { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(AutomationRequestMatcher.class); 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Override 37 | public int getNumFreeThreadsForParameters(ProxySet proxySet, AutomationRunRequest runRequest) { 38 | // This will keep a count of the number of instances that can run our requested test 39 | int totalFreeSlots = 0; 40 | // Current runs registered with the hub. Make a copy of the set so we don't muck with the original set of registered runs 41 | Map currentRuns = new HashMap<>(); 42 | // Add them all into our map so we can keep track of both the runs we need to delete from our free count as well as 43 | // tests that are in progress for the run. 44 | for(String key : AutomationContext.getContext().getRunUuids()) { 45 | currentRuns.put(key,0); 46 | } 47 | for(RemoteProxy proxy : proxySet) { 48 | int matchingCapableSlots = 0; 49 | int runningSessions = 0; 50 | int matchingRunningSessions = 0; 51 | int maxNodeThreadsAvailable = proxy.getMaxNumberOfConcurrentTestSessions(); 52 | Map config = proxy.getConfig(); 53 | String instanceId = null; 54 | if(config.containsKey(AutomationConstants.INSTANCE_ID)) { 55 | instanceId = (String)config.get(AutomationConstants.INSTANCE_ID); 56 | } 57 | boolean nodeMarkedForTermination = false; 58 | if(instanceId != null) { 59 | AutomationDynamicNode node = AutomationContext.getContext().getNode(instanceId); 60 | // If this node has been spun up and it is no longer in the running state, go to the next test slot 61 | // as we cannot consider this node to be a free resource 62 | if(node != null) {// There really shouldn't ever be a null node here but adding the check regardless 63 | if(node.getStatus() != AutomationDynamicNode.STATUS.RUNNING) { 64 | // If this is a dynamic node and its not in the running state, we should not be calculating its resources as available 65 | nodeMarkedForTermination = true; 66 | } 67 | } 68 | } 69 | if(instanceId != null) { 70 | log.info(String.format("Analyzing node %s...",instanceId)); 71 | } else { 72 | log.info("Analyzing node..."); 73 | } 74 | for (TestSlot testSlot : proxy.getTestSlots()) { 75 | //TODO Do selenium flavor of browsers to match here from RMN 76 | //TODO Better property matching 77 | TestSession session = testSlot.getSession(); 78 | Map testSlotCapabilities = testSlot.getCapabilities(); 79 | if(session != null) { 80 | Map sessionCapabilities = session.getRequestedCapabilities(); 81 | Object uuid = sessionCapabilities.get(AutomationConstants.UUID); 82 | // If the session has a UUID, go ahead and remove it from our runs that we're going to subtract from our available 83 | // node count as this means the run is under way 84 | if(uuid != null && currentRuns.containsKey(uuid)) { 85 | int previousCount = currentRuns.get(uuid); 86 | currentRuns.put((String)uuid,previousCount + 1); 87 | } 88 | if(runRequest.matchesCapabilities(testSlotCapabilities)) { 89 | matchingRunningSessions++; 90 | } 91 | runningSessions++; 92 | } 93 | if(runRequest.matchesCapabilities(testSlotCapabilities)) { 94 | matchingCapableSlots++; 95 | } 96 | } 97 | log.info(String.format("Node had %d matching running sessions and %d matching capable slots",matchingRunningSessions,matchingCapableSlots)); 98 | int nodeFreeSlots; 99 | // If the node is marked for termination, we need to subtract matching running sessions from our free count, and make sure to not add 100 | // any capable slots, as they're really not even 'capable' since the node will be shutdown 101 | if(nodeMarkedForTermination) { 102 | log.info(String.format("Node marked for termination. Subtracting %d sessions from %d total free slots",matchingRunningSessions,totalFreeSlots)); 103 | totalFreeSlots -= matchingRunningSessions; 104 | } else { 105 | // Decrement the running sessions only if running + free is more than the total threads the node can handle. This will handle 106 | // load from a capacity standpoint of a node 107 | if((runningSessions + matchingCapableSlots) > maxNodeThreadsAvailable ) { 108 | if(matchingCapableSlots < maxNodeThreadsAvailable) { 109 | log.debug(String.format("Subtracting %d running sessions from %d capable sessions",runningSessions,matchingCapableSlots)); 110 | nodeFreeSlots = matchingCapableSlots - runningSessions; 111 | } else { 112 | log.debug(String.format("Subtracting %d running sessions from %d maximum node thread limit",matchingCapableSlots,maxNodeThreadsAvailable)); 113 | nodeFreeSlots = maxNodeThreadsAvailable - runningSessions; 114 | } 115 | } else { 116 | log.debug(String.format("%d free node slots derived from matching capable slots",matchingCapableSlots)); 117 | nodeFreeSlots = matchingCapableSlots; 118 | // If there were any running sessions that match this browser, we need to subtract them from the capable sessions 119 | if(matchingRunningSessions != 0) { 120 | log.debug(String.format("%d matching running sessions will be subtracted from %d node free slots",matchingRunningSessions,nodeFreeSlots)); 121 | nodeFreeSlots -= matchingRunningSessions; 122 | } 123 | } 124 | // If nodeFreeSlots is negative, go ahead and subtract the capable sessions instead 125 | if(nodeFreeSlots < 0) { 126 | log.warn("The number of free node slots was less than 0. Resetting to 0."); 127 | nodeFreeSlots = 0; 128 | } 129 | totalFreeSlots += nodeFreeSlots; 130 | } 131 | } 132 | // Any runs still in this set means that run has not started yet, so we should consider this in our math 133 | for(String uuid : currentRuns.keySet()) { 134 | AutomationRunRequest request = AutomationContext.getContext().getRunRequest(uuid); 135 | // If we're not dealing with an old run request that just never started, go ahead and decrement 136 | // the value from available nodes on this hub 137 | if(request != null && !AutomationUtils.isCurrentTimeAfterDate(request.getCreatedDate(), 90, Calendar.SECOND) ) { 138 | if(!runRequest.matchesCapabilities(request)) { 139 | log.warn(String.format("Requested run %s did not match pending run %s so count will not be included",runRequest,request)); 140 | continue; 141 | } 142 | int currentlyRunningTestsForRun = currentRuns.get(uuid); 143 | // If some of the tests are underway, subtract the currently running tests from the number of total tests, and then subtract that 144 | // from the number of free slots. This way we're including tests that may not have started yet in our free resource check 145 | if(currentlyRunningTestsForRun < request.getThreadCount()) { 146 | int countToSubtract = request.getThreadCount() - currentlyRunningTestsForRun; 147 | log.debug(String.format("In progress run has %d threads that will be subtracted from our total free count %d.",countToSubtract,totalFreeSlots)); 148 | totalFreeSlots -= countToSubtract; 149 | } 150 | } 151 | } 152 | // Make sure we don't return a negative number to the caller 153 | if(totalFreeSlots < 0) { 154 | log.info("The number of total free node slots was less than 0. Resetting to 0."); 155 | totalFreeSlots = 0; 156 | } 157 | log.info(String.format("Returning %s free slots for request %s",totalFreeSlots,runRequest)); 158 | return totalFreeSlots; 159 | } 160 | 161 | /** 162 | * {@inheritDoc} 163 | */ 164 | @Override 165 | public int getNumInProgressTests(ProxySet proxySet, AutomationRunRequest runRequest) { 166 | int inProgressTests = 0; 167 | for(RemoteProxy proxy : proxySet) { 168 | for(TestSlot testSlot : proxy.getTestSlots() ) { 169 | TestSession session = testSlot.getSession(); 170 | if(session != null) { 171 | if(runRequest.matchesCapabilities(session.getRequestedCapabilities())) { 172 | inProgressTests++; 173 | } 174 | } 175 | } 176 | } 177 | return inProgressTests; 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationRunRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import com.google.common.annotations.VisibleForTesting; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.openqa.selenium.remote.CapabilityType; 18 | 19 | import java.util.Calendar; 20 | import java.util.Date; 21 | import java.util.Map; 22 | 23 | /** 24 | * Represents a run request which will typically be sent in by a test run requesting resources. Used 25 | * to encapsulate various run parameters 26 | * @author mhardin 27 | */ 28 | public final class AutomationRunRequest { 29 | 30 | private final String uuid; 31 | private final Integer threadCount; 32 | private final String browser; 33 | private final String browserVersion; 34 | private final String os; 35 | private final Date createdDate; 36 | 37 | // Require callers to have required variables through constructor below 38 | private AutomationRunRequest() { 39 | this(null); 40 | } 41 | 42 | /** 43 | * Constructs a run request instance for the specified browser 44 | * @param browser Browser for the requesting test run 45 | */ 46 | public AutomationRunRequest(String browser) { 47 | this(null,null,browser); 48 | } 49 | 50 | /** 51 | * Constructs a run request object 52 | * @param uuid UUID to represent the requesting test run 53 | * @param threadCount Number of threads for the requesting test run 54 | * @param browser Browser for the requesting test run 55 | */ 56 | public AutomationRunRequest(String uuid, Integer threadCount,String browser) { 57 | this(uuid,threadCount,browser,null,null); 58 | } 59 | 60 | /** 61 | * Constructs a run request object 62 | * @param runUuid UUID to represent the requesting test run 63 | * @param threadCount Number of threads for the requesting test run 64 | * @param browser Browser for the requesting test run 65 | * @param browserVersion Browser version for the requesting test run 66 | * @param os OS for the requesting test run 67 | */ 68 | public AutomationRunRequest(String runUuid, Integer threadCount, String browser, String browserVersion, String os) { 69 | this(runUuid, threadCount, browser, browserVersion, os, new Date()); 70 | } 71 | 72 | /** 73 | * Constructs a run request object 74 | * @param runUuid UUID to represent the requesting test run 75 | * @param threadCount Number of threads for the requesting test run 76 | * @param browser Browser for the requesting test run 77 | * @param browserVersion Browser version for the requesting test run 78 | * @param os OS for the requesting test run 79 | * @param createdDate Date that the test run request was received 80 | */ 81 | @VisibleForTesting 82 | public AutomationRunRequest(String runUuid, Integer threadCount, String browser,String browserVersion, String os, Date createdDate) { 83 | this.uuid = runUuid; 84 | this.threadCount = threadCount; 85 | this.browser = browser; 86 | this.browserVersion = browserVersion; 87 | this.os = os; 88 | this.createdDate = createdDate; 89 | } 90 | 91 | /** 92 | * Generates a AutomationRunRequest object from the capabilities passed in 93 | * @param capabilities 94 | * @return 95 | */ 96 | public static AutomationRunRequest requestFromCapabilities(Map capabilities) { 97 | String capabilityBrowser = (String)capabilities.get(CapabilityType.BROWSER_NAME); 98 | String capabilityBrowserVersion = null; 99 | if(capabilities.containsKey(CapabilityType.VERSION)) { 100 | capabilityBrowserVersion = (String)capabilities.get(CapabilityType.VERSION); 101 | } 102 | String capabilityOs = (String)capabilities.get(CapabilityType.PLATFORM); 103 | return new AutomationRunRequest(null,null,capabilityBrowser,capabilityBrowserVersion,capabilityOs); 104 | } 105 | 106 | /** 107 | * Returns the UUID for this run request 108 | * @return 109 | */ 110 | public String getUuid() { 111 | return uuid; 112 | } 113 | 114 | /** 115 | * Returns the thread count requested by this run 116 | * @return 117 | */ 118 | public int getThreadCount() { 119 | return threadCount; 120 | } 121 | 122 | /** 123 | * Returns the browser (e.g. 'chrome', 'firefox', etc) for this run request 124 | * @return 125 | */ 126 | public String getBrowser() { 127 | return browser; 128 | } 129 | 130 | /** 131 | * Returns the version of the browser (e.g. '27' for Firefox) 132 | * @return 133 | */ 134 | public String getBrowserVersion() { return browserVersion; } 135 | 136 | /** 137 | * Returns the OS (e.g. 'linux') 138 | * @return 139 | */ 140 | public String getOs() { return os; } 141 | /** 142 | * Returns the created date for this run request 143 | * @return 144 | */ 145 | public Date getCreatedDate() { 146 | return createdDate; 147 | } 148 | 149 | /** 150 | * Returns true if this run request is less than 2 minutes old, false otherwise 151 | * @return 152 | */ 153 | public boolean isNewRun() { 154 | Calendar c = Calendar.getInstance(); 155 | c.setTime(createdDate); 156 | c.add(Calendar.MINUTE,2); 157 | return new Date().before(c.getTime()); 158 | } 159 | 160 | @Override 161 | public String toString() { 162 | StringBuilder builder = new StringBuilder(); 163 | builder.append("Run Request"); 164 | if(!StringUtils.isEmpty(uuid)) { 165 | builder.append(" - UUID: ").append(uuid); 166 | } 167 | if(threadCount != null) { 168 | builder.append(" - Thread count: ").append(threadCount); 169 | } 170 | if(!StringUtils.isEmpty(browser)) { 171 | builder.append(" - Browser: ").append(browser); 172 | } 173 | if(!StringUtils.isEmpty(os)) { 174 | builder.append(" - OS: ").append(os); 175 | } 176 | return builder.toString(); 177 | } 178 | 179 | /** 180 | * Returns true if this run request matches the capabilities passed in. Includes browser, browser version, and OS 181 | * @param capabilities 182 | * @return 183 | */ 184 | public boolean matchesCapabilities(Map capabilities) { 185 | String capabilityBrowser = (String)capabilities.get(CapabilityType.BROWSER_NAME); 186 | String capabilityBrowserVersion = (String)capabilities.get(CapabilityType.VERSION); 187 | String capabilityOs = (String)capabilities.get(CapabilityType.PLATFORM); 188 | if(!AutomationUtils.lowerCaseMatch(browser, capabilityBrowser)) { 189 | return false; 190 | } 191 | if(browserVersion != null && !AutomationUtils.lowerCaseMatch(browserVersion,capabilityBrowserVersion)) { 192 | return false; 193 | } 194 | if(os != null && !AutomationUtils.lowerCaseMatch(os, capabilityOs)) { 195 | // If either OS has 'ANY' for the platform, that means it should be a match regardless and we don't have to count this as a non-match 196 | if(!AutomationUtils.lowerCaseMatch(os,"any") && !AutomationUtils.lowerCaseMatch(capabilityOs,"any")) { 197 | return false; 198 | } 199 | } 200 | return true; 201 | } 202 | 203 | /** 204 | * Returns true if this run request matches the run request passed in. Includes browser, browser version, and OS 205 | * @param otherRequest 206 | * @return 207 | */ 208 | public boolean matchesCapabilities(AutomationRunRequest otherRequest) { 209 | if(!AutomationUtils.lowerCaseMatch(browser, otherRequest.getBrowser())) { 210 | return false; 211 | } 212 | if(browserVersion != null && browserVersion != otherRequest.getBrowserVersion()) { 213 | return false; 214 | } 215 | if(os != null && !AutomationUtils.lowerCaseMatch(os, otherRequest.getOs())) { 216 | // If either OS has 'ANY' for the platform, that means it should be a match regardless and we don't have to count this as a non-match 217 | if(!AutomationUtils.lowerCaseMatch(os,"any") && !AutomationUtils.lowerCaseMatch(otherRequest.getOs(),"any")) { 218 | return false; 219 | } 220 | } 221 | return true; 222 | } 223 | 224 | @Override 225 | public boolean equals(Object o) { 226 | if (this == o) return true; 227 | if (o == null || getClass() != o.getClass()) return false; 228 | 229 | AutomationRunRequest that = (AutomationRunRequest) o; 230 | 231 | if (!browser.equals(that.browser)) return false; 232 | if (browserVersion != null ? !browserVersion.equals(that.browserVersion) : that.browserVersion != null) 233 | return false; 234 | if (os != null ? !os.equals(that.os) : that.os != null) return false; 235 | 236 | return true; 237 | } 238 | 239 | @Override 240 | public int hashCode() { 241 | int result = browser.hashCode(); 242 | result = 31 * result + (browserVersion != null ? browserVersion.hashCode() : 0); 243 | result = 31 * result + (os != null ? os.hashCode() : 0); 244 | return result; 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/AutomationUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import java.util.Calendar; 16 | import java.util.Date; 17 | 18 | /** 19 | * Util methods 20 | * @author mhardin 21 | */ 22 | public final class AutomationUtils { 23 | 24 | /** 25 | * Modifies the specified date 26 | * @param dateToModify Date to modify 27 | * @param unitsToModify Number of units to modify (e.g. 6 for 6 seconds) 28 | * @param unitType Measurement type (e.g. Calendar.SECONDS) 29 | * @return Modified date 30 | */ 31 | public static Date modifyDate(Date dateToModify,int unitsToModify,int unitType) { 32 | Calendar c = Calendar.getInstance(); 33 | c.setTime(dateToModify); 34 | // Add 60 seconds so we're as close to the hour as we can be instead of adding 55 again 35 | c.add(unitType,unitsToModify); 36 | return c.getTime(); 37 | } 38 | 39 | /** 40 | * Returns true if the current time is after the specified date, false otherwise 41 | * @param dateToCheck Date to check against the current time 42 | * @param unitsToCheckWith Number of units to add/subtract from dateToCheck 43 | * @param unitType Unit type (e.g. Calendar.MINUTES) 44 | * @return 45 | */ 46 | public static boolean isCurrentTimeAfterDate(Date dateToCheck,int unitsToCheckWith,int unitType) { 47 | Date targetDate = AutomationUtils.modifyDate(dateToCheck,unitsToCheckWith,unitType); 48 | return new Date().after(targetDate); 49 | } 50 | 51 | /** 52 | * Returns true if the strings are lower case equal 53 | * @param string1 First string to compare 54 | * @param string2 Second string to compare 55 | * @return 56 | */ 57 | public static boolean lowerCaseMatch(String string1, String string2) { 58 | string2 = string2.toLowerCase().replace(" ",""); 59 | return string2.equals(string1.toLowerCase().replace(" ", "")); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/NodesCouldNotBeStartedException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | public class NodesCouldNotBeStartedException extends Exception { 16 | public NodesCouldNotBeStartedException(String msg) { 17 | super(msg); 18 | } 19 | 20 | public NodesCouldNotBeStartedException(String msg, Throwable t) { 21 | super(msg,t); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/RegistryRetriever.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import org.openqa.grid.internal.Registry; 16 | 17 | /** 18 | * Wrapper interface used to retrieve a Selenium Registry object 19 | * @author mhardin 20 | */ 21 | public interface RegistryRetriever { 22 | 23 | /** 24 | * Retrieves a Selenium Registry 25 | * @return 26 | */ 27 | Registry retrieveRegistry(); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/RequestMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import org.openqa.grid.internal.ProxySet; 16 | 17 | /** 18 | * Interface used to determine current load 19 | */ 20 | public interface RequestMatcher { 21 | 22 | /** 23 | * Returns the number of free threads which satisfy the conditions of the parameters 24 | * @param proxySet Set of current registered proxy objects 25 | * @param runRequest Request used to match against 26 | * @return 27 | */ 28 | int getNumFreeThreadsForParameters(ProxySet proxySet, AutomationRunRequest runRequest); 29 | 30 | /** 31 | * Returns the number of in progress tests which match the request passed in 32 | * @param proxySet Set of current registered proxy objects 33 | * @param runRequest Request used to match against 34 | * @return 35 | */ 36 | int getNumInProgressTests(ProxySet proxySet, AutomationRunRequest runRequest); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/aws/AwsTagReporter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.aws; 13 | 14 | import com.amazonaws.services.ec2.AmazonEC2Client; 15 | import com.amazonaws.services.ec2.model.CreateTagsRequest; 16 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 17 | import com.amazonaws.services.ec2.model.DescribeInstancesResult; 18 | import com.amazonaws.services.ec2.model.Instance; 19 | import com.amazonaws.services.ec2.model.Tag; 20 | import com.google.common.annotations.VisibleForTesting; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | 24 | import java.util.ArrayList; 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | import java.util.List; 28 | import java.util.Properties; 29 | import java.util.Set; 30 | 31 | public class AwsTagReporter extends Thread { 32 | 33 | private static final Logger log = LoggerFactory.getLogger(AwsTagReporter.class); 34 | static int TIMEOUT_IN_SECONDS = 10 * 1000; 35 | 36 | private AmazonEC2Client ec2Client; 37 | private Collection instances; 38 | private Properties awsProperties; 39 | 40 | public AwsTagReporter(String testRunUuid, AmazonEC2Client ec2Client, Collection instances, Properties awsProperties) { 41 | this.ec2Client = ec2Client; 42 | this.instances = instances; 43 | this.awsProperties = awsProperties; 44 | this.setName("TagReporter-" + testRunUuid); 45 | } 46 | 47 | @Override 48 | public void run() { 49 | log.info("AwsTagReporter thread initialized"); 50 | DescribeInstancesRequest request = new DescribeInstancesRequest(); 51 | Collection instanceIds = new ArrayList<>(); 52 | for(Instance instance : instances) { 53 | instanceIds.add(instance.getInstanceId()); 54 | } 55 | request.withInstanceIds(instanceIds); 56 | long startTime = System.currentTimeMillis(); 57 | boolean instancesFound = false; 58 | do{ 59 | // Wait up to 10 seconds for the instances to exist with AWS 60 | if(System.currentTimeMillis() > startTime + AwsTagReporter.TIMEOUT_IN_SECONDS) { 61 | throw new RuntimeException("Error waiting for instances to exist to add tags"); 62 | } 63 | try{ 64 | DescribeInstancesResult existingInstances = ec2Client.describeInstances(request); 65 | if(existingInstances.getReservations().get(0).getInstances().size() == instances.size()) { 66 | log.info("Correct instances were found to add tags to!"); 67 | instancesFound = true; 68 | } 69 | } catch(Throwable t) { 70 | log.error("Error finding instances. Sleeping."); 71 | try { 72 | sleep(); 73 | } catch (InterruptedException e) { 74 | log.error("Error sleeping for adding tags", e); 75 | } 76 | } 77 | } while(!instancesFound); 78 | associateTags(instances); 79 | log.info("AwsTagReporter thread completed successfully"); 80 | } 81 | 82 | @VisibleForTesting 83 | void sleep() throws InterruptedException{ 84 | Thread.sleep(500); 85 | } 86 | 87 | /** 88 | * Associates the correct tags for each instance passed in 89 | * @param instances 90 | */ 91 | private void associateTags(Collection instances) { 92 | try{ 93 | for(Instance instance : instances) { 94 | log.info("Associating tags to instance: " + instance.getInstanceId()); 95 | String instanceId = instance.getInstanceId(); 96 | setTagsForInstance(instanceId); 97 | } 98 | log.info("Tags added!"); 99 | } catch(IndexOutOfBoundsException | ClassCastException e) { 100 | log.error("Error adding tags. Please make sure your tag syntax is correct (refer to the readme)",e); 101 | } 102 | } 103 | 104 | /** 105 | * Sets tags for the specified instance 106 | * @param instanceId 107 | * @return 108 | */ 109 | private void setTagsForInstance(String instanceId) { 110 | Set keys = awsProperties.keySet(); 111 | List tags = new ArrayList<>(); 112 | for(Object o : keys) { 113 | if(o instanceof String && ((String)o).startsWith("tag")) { 114 | String values = (String)awsProperties.get(o); 115 | String[] splitValues = values.split(","); 116 | String key = splitValues[0]; 117 | String value = splitValues[1]; 118 | Tag tagToAdd = new Tag(key,value); 119 | log.info("Adding tag: " + tagToAdd); 120 | tags.add(tagToAdd); 121 | } 122 | } 123 | // Including a hard coded tag here so we can track which resources originate from this plugin 124 | Tag nodeTag = new Tag("LaunchSource","SeleniumGridScalerPlugin"); 125 | log.info("Adding hard-coded tag: " + nodeTag); 126 | tags.add(nodeTag); 127 | CreateTagsRequest ctr = new CreateTagsRequest(Arrays.asList(instanceId),tags); 128 | ec2Client.createTags(ctr); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/aws/VmManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.aws; 13 | 14 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 15 | import com.amazonaws.services.ec2.model.Instance; 16 | import com.amazonaws.services.ec2.model.Reservation; 17 | import com.rmn.qa.NodesCouldNotBeStartedException; 18 | 19 | import java.util.List; 20 | 21 | public interface VmManager { 22 | 23 | /** 24 | * Launches the specified instances 25 | * @param uuid UUID of the requesting test run 26 | * @param os OS of the requesting test run 27 | * @param browser Browser of the requesting test run 28 | * @param hubHostName Hub host name for the nodes to register with 29 | * @param nodeCount Number of nodes to be started 30 | * @param maxSessions Number of max sessions per node 31 | * @return 32 | */ 33 | // TODO Refactor into AutomationRunRequest 34 | List launchNodes(String uuid, String os, String browser, String hubHostName, int nodeCount, int maxSessions) throws NodesCouldNotBeStartedException; 35 | 36 | /** 37 | * Terminates the specified instance 38 | * @param instanceId 39 | */ 40 | // TODO Rename to be node or instance in the name 41 | boolean terminateInstance(String instanceId); 42 | 43 | /** 44 | * Returns a list of reservations as defined in the {@link com.amazonaws.services.ec2.model.DescribeInstancesRequest DescribeInstancesRequest} 45 | * @param describeInstancesRequest 46 | * @return 47 | */ 48 | List describeInstances(DescribeInstancesRequest describeInstancesRequest); 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/servlet/AutomationStatusServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.servlet; 13 | 14 | import com.google.common.io.ByteStreams; 15 | import com.rmn.qa.*; 16 | import org.openqa.grid.internal.Registry; 17 | import org.openqa.grid.web.servlet.RegistryBasedServlet; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | 21 | import javax.servlet.ServletException; 22 | import javax.servlet.http.HttpServletRequest; 23 | import javax.servlet.http.HttpServletResponse; 24 | import java.io.ByteArrayInputStream; 25 | import java.io.IOException; 26 | import java.io.InputStream; 27 | 28 | /** 29 | * Legacy API to pull free threads for a given browser 30 | */ 31 | public class AutomationStatusServlet extends RegistryBasedServlet { 32 | 33 | private static final long serialVersionUID = 8484071790930378855L; 34 | private static final Logger log = LoggerFactory.getLogger(AutomationStatusServlet.class); 35 | private RequestMatcher requestMatcher; 36 | 37 | /** 38 | * Constructs a default status servlet 39 | */ 40 | public AutomationStatusServlet() { 41 | this(null); 42 | } 43 | 44 | /** 45 | * Constructs a default status servlet with the specified {@link org.openqa.grid.internal.Registry register} 46 | * @param registry 47 | */ 48 | public AutomationStatusServlet(Registry registry) { 49 | super(registry); 50 | this.requestMatcher = new AutomationRequestMatcher(); 51 | } 52 | 53 | /** 54 | * {@inheritDoc} 55 | */ 56 | @Override 57 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 58 | response.setContentType("text/html"); 59 | response.setCharacterEncoding("UTF-8"); 60 | 61 | String browserRequested = request.getParameter("browser"); 62 | 63 | // OS is optional 64 | String os = request.getParameter("os"); 65 | if (browserRequested == null) { 66 | response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Parameter 'browser' must be passed in as a query string parameter"); 67 | return; 68 | } 69 | AutomationRunRequest runRequest = new AutomationRunRequest(AutomationStatusServlet.class.getSimpleName(),null,browserRequested,null,os); 70 | log.info(String.format("Legacy server request received. Browser [%s]", browserRequested)); 71 | AutomationRunContext context = AutomationContext.getContext(); 72 | // If a run is already going on with this browser, return an error code 73 | if(context.hasRun(browserRequested)) { 74 | response.setStatus(400); 75 | return; 76 | } 77 | // Synchronize this block until we've added the run to our context for other potential threads to see 78 | int availableNodes = requestMatcher.getNumFreeThreadsForParameters(getRegistry().getAllProxies(),runRequest); 79 | response.setStatus(HttpServletResponse.SC_OK); 80 | // Add the browser so we know the nodes are occupied 81 | context.addRun(new AutomationRunRequest(browserRequested,availableNodes,browserRequested,null,os)); 82 | try (InputStream in = new ByteArrayInputStream(String.valueOf(availableNodes).getBytes("UTF-8"))){ 83 | ByteStreams.copy(in, response.getOutputStream()); 84 | } finally { 85 | response.flushBuffer(); 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/servlet/StatusServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.servlet; 13 | 14 | import com.google.common.io.ByteStreams; 15 | import com.rmn.qa.*; 16 | import org.openqa.grid.common.GridDocHelper; 17 | import org.openqa.grid.internal.Registry; 18 | import org.openqa.grid.internal.RemoteProxy; 19 | import org.openqa.grid.internal.TestSession; 20 | import org.openqa.grid.internal.TestSlot; 21 | import org.openqa.grid.internal.utils.GridHubConfiguration; 22 | import org.openqa.grid.selenium.proxy.DefaultRemoteProxy; 23 | import org.openqa.grid.web.servlet.RegistryBasedServlet; 24 | import org.openqa.grid.web.utils.BrowserNameUtils; 25 | import org.openqa.selenium.remote.BrowserType; 26 | import org.openqa.selenium.remote.CapabilityType; 27 | import org.openqa.selenium.remote.DesiredCapabilities; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import javax.servlet.ServletException; 32 | import javax.servlet.http.HttpServletRequest; 33 | import javax.servlet.http.HttpServletResponse; 34 | import java.io.ByteArrayInputStream; 35 | import java.io.IOException; 36 | import java.io.InputStream; 37 | import java.util.*; 38 | 39 | /** 40 | * Front end to monitor what is currently happening on the proxies. The display is defined by 41 | * HtmlRenderer returned by the RemoteProxy.getHtmlRenderer() method. 42 | */ 43 | public class StatusServlet extends RegistryBasedServlet { 44 | 45 | private static final long serialVersionUID = 8484071790930378855L; 46 | private static final Logger log = LoggerFactory.getLogger(StatusServlet.class); 47 | private static String coreVersion; 48 | private static String coreRevision; 49 | 50 | private AutomationRequestMatcher requestMatcher; 51 | 52 | /** 53 | * Constructs a servlet with default functionality 54 | */ 55 | public StatusServlet() { 56 | this(null); 57 | } 58 | 59 | /** 60 | * Constructs a servlet with the specified registry 61 | * @param registry 62 | */ 63 | public StatusServlet(Registry registry) { 64 | super(registry); 65 | getVersion(); 66 | requestMatcher = new AutomationRequestMatcher(); 67 | } 68 | 69 | @Override 70 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 71 | 72 | int refresh = -1; 73 | 74 | if (request.getParameter("refresh") != null) { 75 | try { 76 | refresh = Integer.parseInt(request.getParameter("refresh")); 77 | } catch (NumberFormatException e) { 78 | // ignore wrong param 79 | } 80 | 81 | } 82 | 83 | response.setContentType("text/html"); 84 | response.setCharacterEncoding("UTF-8"); 85 | response.setStatus(200); 86 | 87 | StringBuilder builder = new StringBuilder(); 88 | 89 | builder.append(""); 90 | builder.append(""); 91 | 92 | if (refresh != -1) { 93 | builder.append(String.format("", refresh)); 94 | } 95 | builder.append("Grid overview"); 96 | 97 | builder.append(""); 103 | builder.append(""); 104 | 105 | builder.append(""); 106 | builder.append("

Grid Hub "); 107 | builder.append(coreVersion).append(coreRevision); 108 | builder.append("

"); 109 | int chromeThreads = requestMatcher.getNumFreeThreadsForParameters(getRegistry().getAllProxies(),new AutomationRunRequest(StatusServlet.class.getSimpleName(),null, BrowserType.CHROME)); 110 | int firefoxThreads = requestMatcher.getNumFreeThreadsForParameters(getRegistry().getAllProxies(),new AutomationRunRequest(StatusServlet.class.getSimpleName(),null,BrowserType.FIREFOX)); 111 | int ieThreads = requestMatcher.getNumFreeThreadsForParameters(getRegistry().getAllProxies(),new AutomationRunRequest(StatusServlet.class.getSimpleName(),null,"internetexplorer")); 112 | builder.append("

Free Threads - Chrome: ").append(chromeThreads).append(" Firefox: ").append(firefoxThreads).append(" IE: ").append(ieThreads).append("

"); 113 | 114 | for (RemoteProxy proxy : getRegistry().getAllProxies()) { 115 | StringBuilder localBuilder = new StringBuilder(); 116 | 117 | localBuilder.append("
"); 118 | localBuilder.append("").append(proxy.getClass().getSimpleName()).append(""); 119 | localBuilder.append("listening on ").append(proxy.getRemoteHost()); 120 | 121 | Object instanceId = proxy.getConfig().get(AutomationConstants.INSTANCE_ID); 122 | if(instanceId != null) { 123 | AutomationDynamicNode node = AutomationContext.getContext().getNode((String)instanceId); 124 | if(node != null) { 125 | localBuilder.append("
EC2 dynamic node."); 126 | localBuilder.append("
Start time " + node.getStartDate()); 127 | localBuilder.append("
Current shutdown time ").append(node.getEndDate()); 128 | localBuilder.append("
Requested test run ").append(node.getUuid()); 129 | } 130 | } 131 | if (((DefaultRemoteProxy) proxy).isDown()) { 132 | localBuilder.append("(cannot be reached at the moment)"); 133 | } 134 | localBuilder.append("
"); 135 | if (proxy.getTimeOut() > 0) { 136 | int inSec = proxy.getTimeOut() / 1000; 137 | localBuilder.append("test session time out after ").append(inSec).append(" sec.
"); 138 | } 139 | 140 | localBuilder.append("Supports up to ").append(proxy.getMaxNumberOfConcurrentTestSessions()) 141 | .append(" concurrent tests from:
"); 142 | 143 | localBuilder.append(""); 144 | for (TestSlot slot : proxy.getTestSlots()) { 145 | TestSession session = slot.getSession(); 146 | 147 | String icon = getIcon(slot.getCapabilities(),proxy); 148 | if (icon != null) { 149 | localBuilder.append(""); 167 | } else { 168 | localBuilder.append(">"); 169 | localBuilder.append(slot.getCapabilities().get(CapabilityType.BROWSER_NAME)); 170 | localBuilder.append(""); 171 | } 172 | 173 | } 174 | localBuilder.append("
"); 175 | builder.append(localBuilder); 176 | } 177 | 178 | int numUnprocessedRequests = getRegistry().getNewSessionRequestCount(); 179 | 180 | if (numUnprocessedRequests > 0) { 181 | builder.append(String.format("%d requests waiting for a slot to be free.", numUnprocessedRequests)); 182 | } 183 | 184 | builder.append("
    "); 185 | for (DesiredCapabilities req : getRegistry().getDesiredCapabilities()) { 186 | builder.append("
  • ").append(req.asMap()).append("
  • "); 187 | } 188 | builder.append("
"); 189 | 190 | if (request.getParameter("config") != null) { 191 | builder.append(getConfigInfo(request.getParameter("configDebug") != null)); 192 | } else { 193 | builder.append("view config"); 194 | } 195 | 196 | builder.append(""); 197 | builder.append(""); 198 | 199 | try (InputStream in = new ByteArrayInputStream(builder.toString().getBytes("UTF-8"));) { 200 | ByteStreams.copy(in, response.getOutputStream()); 201 | } finally { 202 | response.flushBuffer(); 203 | } 204 | } 205 | 206 | /** 207 | * retracing how the hub config was built to help debugging. 208 | * 209 | * @return 210 | */ 211 | private String getConfigInfo(boolean verbose) { 212 | StringBuilder builder = new StringBuilder(); 213 | 214 | GridHubConfiguration config = getRegistry().getConfiguration(); 215 | builder.append("Config for the hub :
"); 216 | builder.append(prettyHtmlPrint(config)); 217 | 218 | if (verbose) { 219 | 220 | GridHubConfiguration tmp = new GridHubConfiguration(); 221 | tmp.loadDefault(); 222 | 223 | builder.append("Config details :
"); 224 | builder.append("hub launched with :"); 225 | for (int i = 0; i < config.getArgs().length; i++) { 226 | builder.append(config.getArgs()[i]).append(" "); 227 | } 228 | 229 | builder.append("
the final configuration comes from :
"); 230 | builder.append("the default :
"); 231 | builder.append(prettyHtmlPrint(tmp)); 232 | 233 | builder.append("updated with grid1 config :"); 234 | if (config.getGrid1Yml() != null) { 235 | builder.append(config.getGrid1Yml()).append("
"); 236 | tmp.loadFromGridYml(config.getGrid1Yml()); 237 | builder.append(prettyHtmlPrint(tmp)); 238 | } else { 239 | builder 240 | .append("No grid1 file specified. To specify one, use -grid1Yml XXX.yml where XXX.yml is a grid1 config file
"); 241 | } 242 | 243 | builder.append("
updated with grid2 config : "); 244 | if (config.getGrid2JSON() != null) { 245 | builder.append(config.getGrid2JSON()).append("
"); 246 | tmp.loadFromJSON(config.getGrid2JSON()); 247 | builder.append(prettyHtmlPrint(tmp)); 248 | } else { 249 | builder 250 | .append("No hub config file specified. To specify one, use -hubConfig XXX.json where XXX.json is a hub config file
"); 251 | } 252 | 253 | builder.append("
updated with params :
"); 254 | tmp.loadFromCommandLine(config.getArgs()); 255 | builder.append(prettyHtmlPrint(tmp)); 256 | } 257 | return builder.toString(); 258 | } 259 | 260 | private String key(String key) { 261 | return "" + key + " : "; 262 | } 263 | 264 | private String prettyHtmlPrint(GridHubConfiguration config) { 265 | StringBuilder b = new StringBuilder(); 266 | 267 | b.append(key("host")).append(config.getHost()).append("
"); 268 | b.append(key("port")).append(config.getPort()).append("
"); 269 | b.append(key("cleanUpCycle")).append(config.getCleanupCycle()).append("
"); 270 | b.append(key("timeout")).append(config.getTimeout()).append("
"); 271 | b.append(key("browserTimeout")).append(config.getBrowserTimeout()).append("
"); 272 | 273 | 274 | b.append(key("newSessionWaitTimeout")).append(config.getNewSessionWaitTimeout()) 275 | .append("
"); 276 | b.append(key("grid1Mapping")).append(config.getGrid1Mapping()).append("
"); 277 | b.append(key("throwOnCapabilityNotPresent")).append(config.isThrowOnCapabilityNotPresent()) 278 | .append("
"); 279 | 280 | b.append(key("capabilityMatcher")) 281 | .append( 282 | config.getCapabilityMatcher() == null ? "null" : config.getCapabilityMatcher() 283 | .getClass().getCanonicalName()).append("
"); 284 | b.append(key("prioritizer")) 285 | .append( 286 | config.getPrioritizer() == null ? "null" : config.getPrioritizer().getClass() 287 | .getCanonicalName()) 288 | .append("
"); 289 | b.append(key("servlets")); 290 | for (String s : config.getServlets()) { 291 | b.append(s.getClass().getCanonicalName()).append(","); 292 | } 293 | b.append("

"); 294 | b.append("all params :

"); 295 | List keys = new ArrayList(); 296 | keys.addAll(config.getAllParams().keySet()); 297 | Collections.sort(keys); 298 | for (String s : keys) { 299 | b.append(key(s.replaceFirst("-", ""))).append(config.getAllParams().get(s)).append("
"); 300 | } 301 | b.append("
"); 302 | return b.toString(); 303 | } 304 | 305 | private void getVersion() { 306 | final Properties p = new Properties(); 307 | 308 | InputStream stream = 309 | Thread.currentThread().getContextClassLoader().getResourceAsStream("VERSION.txt"); 310 | if (stream == null) { 311 | log.error("Couldn't determine version number"); 312 | return; 313 | } 314 | try { 315 | p.load(stream); 316 | } catch (IOException e) { 317 | log.error("Cannot load version from VERSION.txt" + e.getMessage()); 318 | } 319 | coreVersion = p.getProperty("selenium.core.version"); 320 | coreRevision = p.getProperty("selenium.core.revision"); 321 | if (coreVersion == null) { 322 | log.error("Cannot load selenium.core.version from VERSION.txt"); 323 | } 324 | } 325 | 326 | private String getIcon(Map capabilities,RemoteProxy proxy) { 327 | return BrowserNameUtils.getConsoleIconPath(new DesiredCapabilities(capabilities), 328 | proxy.getRegistry()); 329 | } 330 | 331 | } 332 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AbstractAutomationCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.task; 13 | 14 | import com.rmn.qa.RegistryRetriever; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | 18 | /** 19 | * Base task class that has exception/running handling for other tasks to extend with their 20 | * own implementations 21 | * @author mhardin 22 | */ 23 | public abstract class AbstractAutomationCleanupTask extends Thread { 24 | 25 | private static final Logger log = LoggerFactory.getLogger(AbstractAutomationCleanupTask.class); 26 | 27 | protected RegistryRetriever registryRetriever; 28 | private Throwable t; 29 | 30 | public AbstractAutomationCleanupTask(RegistryRetriever registryRetriever) { 31 | this.registryRetriever = registryRetriever; 32 | } 33 | 34 | @Override 35 | public void run() { 36 | try{ 37 | this.doWork(); 38 | }catch(Throwable t) { 39 | this.t = t; 40 | log.error(String.format("Error executing cleanup thread [%s]: %s", getDescription(), t), t); 41 | } 42 | } 43 | 44 | /** 45 | * Performs the work of the task 46 | */ 47 | public abstract void doWork(); 48 | 49 | /** 50 | * Returns the description of the task 51 | * @return 52 | */ 53 | public abstract String getDescription(); 54 | 55 | /** 56 | * Returns the exception of this task if any was encountered 57 | * @return 58 | */ 59 | public Throwable getThrowable() { 60 | return t; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AutomationHubCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.task; 13 | 14 | import com.google.common.annotations.VisibleForTesting; 15 | import com.rmn.qa.AutomationConstants; 16 | import com.rmn.qa.AutomationUtils; 17 | import com.rmn.qa.RegistryRetriever; 18 | import com.rmn.qa.aws.AwsVmManager; 19 | import com.rmn.qa.aws.VmManager; 20 | import org.openqa.grid.internal.ProxySet; 21 | import org.openqa.grid.internal.utils.GridHubConfiguration; 22 | import org.slf4j.Logger; 23 | import org.slf4j.LoggerFactory; 24 | 25 | import java.text.ParseException; 26 | import java.util.Calendar; 27 | import java.util.Date; 28 | 29 | /** 30 | * Task to shut down the hub if it was dynamically started. This task should only be started if this hub is a 31 | * should be terminated via {@link com.rmn.qa.aws.VmManager EC2} 32 | * @author mhardin 33 | */ 34 | public class AutomationHubCleanupTask extends AbstractAutomationCleanupTask { 35 | 36 | private static final Logger log = LoggerFactory.getLogger(AutomationHubCleanupTask.class); 37 | 38 | private VmManager ec2; 39 | private final String instanceId; 40 | @VisibleForTesting 41 | static final String NAME = "Hub Cleanup Task"; 42 | protected static Date createdDate = null; 43 | protected static Date endDate = null; 44 | static boolean errorEncountered = false; 45 | 46 | /** 47 | * Constructs a hub cleanup task with the specified parameters 48 | * @param registryRetriever Context retrieval mechanism to use 49 | * @param ec2 EC2 implementation to use for interaction with the hub 50 | * @param instanceId Instance ID of the hub to cleanup 51 | */ 52 | public AutomationHubCleanupTask(RegistryRetriever registryRetriever, VmManager ec2,String instanceId) { 53 | super(registryRetriever); 54 | this.ec2 = ec2; 55 | this.instanceId = instanceId; 56 | } 57 | 58 | /** 59 | * Returns the ProxySet to be used for cleanup purposes. 60 | * @return 61 | */ 62 | @VisibleForTesting 63 | protected ProxySet getProxySet() { 64 | return registryRetriever.retrieveRegistry().getAllProxies(); 65 | } 66 | 67 | @Override 68 | public String getDescription() { 69 | return AutomationHubCleanupTask.NAME; 70 | } 71 | 72 | /** 73 | * Returns the created date which is pulled from the grid configuration 74 | * @return 75 | */ 76 | protected Object getCreatedDate() { 77 | GridHubConfiguration config = registryRetriever.retrieveRegistry().getConfiguration(); 78 | Object createdDate = config.getAllParams().get(AutomationConstants.CONFIG_CREATED_DATE); 79 | return createdDate; 80 | } 81 | 82 | // We're going to continuously monitor this hub to see if we can shut it down. If were approaching the next 83 | // billing cycle and the hub has no nodes, we will terminate the hub. Also, if there is an error parsing the created date, 84 | // we will terminate it at our earliest convenience (no nodes) without regard to the creation time 85 | @Override 86 | public void doWork() { 87 | log.info("Performing cleanup on hub."); 88 | synchronized (AutomationHubCleanupTask.class) { 89 | if(createdDate == null) { 90 | Object createdDate = getCreatedDate(); 91 | try{ 92 | AutomationHubCleanupTask.createdDate = AwsVmManager.NODE_DATE_FORMAT.parse((String)createdDate); 93 | } catch(ParseException pe) { 94 | String message = "Error parsing created date for hub: " + pe; 95 | log.error(message); 96 | errorEncountered = true; 97 | } 98 | if(!errorEncountered) { 99 | // Set the end date to be 55 minutes + creation date 100 | AutomationHubCleanupTask.endDate = AutomationUtils.modifyDate(AutomationHubCleanupTask.createdDate, 55, Calendar.MINUTE); 101 | } 102 | } 103 | } 104 | Date currentTime = new Date(); 105 | if(errorEncountered || currentTime.after(AutomationHubCleanupTask.endDate)) { 106 | // If we're into the next billing cycle, don't shut the hub down 107 | if(errorEncountered && getProxySet().isEmpty()) { 108 | log.info("Error was encountered parsing the created date, so the hub will be shutdown as it is empty."); 109 | ec2.terminateInstance(instanceId); 110 | } else if(AutomationUtils.isCurrentTimeAfterDate(AutomationHubCleanupTask.endDate, 6, Calendar.MINUTE)) { 111 | log.info("Current date: " + new Date()); 112 | log.info("Current end date: " + AutomationHubCleanupTask.endDate); 113 | log.info(String.format("Hub [%s] ran into the next billing cycle. Increasing end date.", instanceId)); 114 | AutomationHubCleanupTask.endDate = AutomationUtils.modifyDate(AutomationHubCleanupTask.endDate,60,Calendar.MINUTE); 115 | return; 116 | }else if(getProxySet().isEmpty()){ 117 | log.warn("No running nodes found after hub expiration time -- terminating hub: " + instanceId); 118 | ec2.terminateInstance(instanceId); 119 | } else { 120 | // This means tests are still running and we cannot terminate yet 121 | log.info("Hub could not be shutdown yet."); 122 | return; 123 | } 124 | } 125 | 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AutomationNodeCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.task; 13 | 14 | import com.google.common.annotations.VisibleForTesting; 15 | import com.rmn.qa.*; 16 | import com.rmn.qa.aws.VmManager; 17 | import org.openqa.grid.internal.ProxySet; 18 | import org.openqa.grid.internal.RemoteProxy; 19 | import org.openqa.grid.internal.TestSlot; 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.util.*; 24 | 25 | /** 26 | * Cleanup task which moves {@link com.rmn.qa.AutomationDynamicNode nodes} into the correct status depending on their lifecycle. The purpose of this 27 | * task is to terminate nodes once current load is sufficient to allow for the node to be safely shutdown 28 | * @author mhardin 29 | */ 30 | public class AutomationNodeCleanupTask extends AbstractAutomationCleanupTask { 31 | 32 | private static final Logger log = LoggerFactory.getLogger(AutomationNodeCleanupTask.class); 33 | 34 | private VmManager ec2; 35 | private RequestMatcher requestMatcher; 36 | @VisibleForTesting 37 | static final String NAME = "Node Cleanup Task"; 38 | 39 | /** 40 | * Constructs a cleanup task with the specified options 41 | * @param registryRetriever Context retrieval mechanism 42 | * @param ec2 EC2 implementation 43 | * @param requestMatcher Request matcher implementation 44 | */ 45 | public AutomationNodeCleanupTask(RegistryRetriever registryRetriever,VmManager ec2,RequestMatcher requestMatcher) { 46 | super(registryRetriever); 47 | this.ec2 = ec2; 48 | this.requestMatcher = requestMatcher; 49 | } 50 | 51 | @Override 52 | public String getDescription() { 53 | return AutomationNodeCleanupTask.NAME; 54 | } 55 | 56 | /** 57 | * Returns the ProxySet to be used for cleanup purposes. 58 | * @return 59 | */ 60 | @VisibleForTesting 61 | protected ProxySet getProxySet() { 62 | return registryRetriever.retrieveRegistry().getAllProxies(); 63 | } 64 | 65 | // We're going to continuously iterate over registered nodes with the hub. If they're expired, we're going to mark them for removal. 66 | // If nodes marked for removal are used into the next billing cycle, then we're going to move their end date back again and put them back 67 | // into the running queue 68 | @Override 69 | public void doWork() { 70 | log.info("Performing cleanup on nodes."); 71 | AutomationRunContext context = AutomationContext.getContext(); 72 | Map nodes = context.getNodes(); 73 | synchronized (nodes) { 74 | Iterator iterator = nodes.keySet().iterator(); 75 | Date nowDate = new Date(); 76 | while(iterator.hasNext()) { 77 | String instanceId = iterator.next(); 78 | AutomationDynamicNode node = nodes.get(instanceId); 79 | AutomationDynamicNode.STATUS nodeStatus = node.getStatus(); 80 | // If the current time is after the scheduled end time for this node and the node is still running, go ahead and queue it to be removed 81 | if(nodeStatus == AutomationDynamicNode.STATUS.RUNNING && nowDate.after(node.getEndDate())) { 82 | if(canNodeShutDown(node)) { 83 | log.info(String.format("Updating node %s to 'EXPIRED' status. Start date [%s] End date [%s]",instanceId,node.getStartDate(),node.getEndDate())); 84 | node.updateStatus(AutomationDynamicNode.STATUS.EXPIRED); 85 | } 86 | } else if(nodeStatus == AutomationDynamicNode.STATUS.EXPIRED) { 87 | // See if we're in the next billing cycle (create + 55 + 6, which should equal 61 minutes and would safely be in the next billing cycle) 88 | if(AutomationUtils.isCurrentTimeAfterDate(node.getEndDate(), 6,Calendar.MINUTE)) { 89 | node.incrementEndDateByOneHour(); 90 | log.info(String.format("Node [%s] was still running after initial allotted time. Resetting status and increasing end date to %s.",instanceId, node.getEndDate())); 91 | node.updateStatus(AutomationDynamicNode.STATUS.RUNNING); 92 | } else if(isNodeCurrentlyEmpty(instanceId)) { 93 | log.info(String.format("Terminating node %s and updating status to 'TERMINATED'",instanceId)); 94 | // Delete node 95 | ec2.terminateInstance(instanceId); 96 | node.updateStatus(AutomationDynamicNode.STATUS.TERMINATED); 97 | } 98 | } else if(nodeStatus == AutomationDynamicNode.STATUS.TERMINATED) { 99 | // If the current time is more than 30 minutes after the node end date, we should remove it from being tracked 100 | if(System.currentTimeMillis() > node.getEndDate().getTime() + (30 * 60 * 1000) ) { 101 | // Remove it, and this will remove from tracking since we're referencing the collection 102 | log.info(String.format("Removing node [%s] from internal tracking set",instanceId)); 103 | iterator.remove(); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | /** 111 | * Returns true if the specified node can be safely shut down, false otherwise 112 | * @param node 113 | * @return 114 | */ 115 | // If free slots are greater than OR equal to our node capacity, that means we have enough wiggle room to go ahead and delete this node. 116 | // We also need to check to make sure there are no registered runs that have not yet started up 117 | private boolean canNodeShutDown(AutomationDynamicNode node) { 118 | // If a new run is queued up, we cannot shut down nodes until it starts, so short circuit out of here 119 | if(AutomationContext.getContext().isNewRunQueuedUp()) { 120 | log.warn(String.format("Node %s cannot be shutdown yet as a new run is queued up.", node.getInstanceId())); 121 | return false; 122 | } 123 | ProxySet proxySet = getProxySet(); 124 | // Iterate over our set of proxies until we find the proxy which represents the node we're trying to shut down 125 | Set existingSlots = new HashSet<>(); 126 | Map proxies = new HashMap<>(); 127 | for(RemoteProxy proxy : proxySet) { 128 | Object instanceId = proxy.getConfig().get(AutomationConstants.INSTANCE_ID); 129 | if(node.getInstanceId().equals(instanceId)) { 130 | // Associate this proxy with the instance id for use later 131 | proxies.put((String)instanceId,proxy); 132 | // Once we've found our target proxy, go ahead and compile a list of browsers this node uses 133 | for(TestSlot testSlot : proxy.getTestSlots()) { 134 | AutomationRunRequest requestRepresentation = AutomationRunRequest.requestFromCapabilities(testSlot.getCapabilities()); 135 | existingSlots.add(requestRepresentation); 136 | } 137 | } 138 | } 139 | if(existingSlots.size() == 0) { 140 | log.error("No browsers found for node: " + node.getInstanceId()); 141 | } 142 | for(AutomationRunRequest request : existingSlots) { 143 | int freeSlotsForBrowser = requestMatcher.getNumFreeThreadsForParameters(getProxySet(), request); 144 | if(freeSlotsForBrowser == 0) { 145 | log.info(String.format("No free slots exist so node will not be shutdown. Node: %s Request: %s Browser: %s",node.getInstanceId(),request,request.getBrowser())); 146 | return false; 147 | } 148 | // Once we find at least one browser that does not have enough capacity to shut this node down 149 | // we can return false to say we cannot shut this node down. 150 | RemoteProxy proxy = proxies.get(node.getInstanceId()); 151 | if(proxy == null) { 152 | log.error(String.format("Proxy was not found for node %s", node.getInstanceId())); 153 | return false; 154 | } 155 | int matchingSlots = 0; 156 | // Go over the test slots and get the number of browsers this node can run 157 | for(TestSlot testSlot : proxy.getTestSlots()) { 158 | // If this test slot matches the browser, increment our count 159 | if(request.matchesCapabilities(testSlot.getCapabilities())) { 160 | matchingSlots ++; 161 | } 162 | } 163 | log.info(String.format("%d matching slots were found for node %s",matchingSlots,node.getInstanceId())); 164 | // Get the lesser number between the total node capacity and browser specific capacity 165 | int finalNum = (matchingSlots < node.getNodeCapacity()) ? matchingSlots : node.getNodeCapacity(); 166 | if(freeSlotsForBrowser < finalNum) { 167 | // If there are no running tests which match the browser, we don't need to honor this browser for shutdown logic 168 | // as it is not currently needed 169 | int inProgressTests = requestMatcher.getNumInProgressTests(getProxySet(),request); 170 | if(inProgressTests != 0) { 171 | log.info(String.format("Current load will not allow for node to shutdown right now. Node: %s Request: %s Free Slots: %s Node Slots: %s",node.getInstanceId(),request,freeSlotsForBrowser,finalNum)); 172 | return false; 173 | } else { 174 | log.info(String.format("Tests are not in progress. Node: %s Request: %s Free Slots: %s Node Slots: %s",node.getInstanceId(),request,freeSlotsForBrowser,finalNum)); 175 | } 176 | } else { 177 | log.info(String.format("Load suitable for Request [%s]. Free slots [%s] Node slots [%s]",request,freeSlotsForBrowser,finalNum)); 178 | } 179 | } 180 | // If we iterated over every browser for the node and load was not heavy enough, we can safely shut this node down 181 | return true; 182 | } 183 | 184 | /** 185 | * Returns true if the specified node is empty and has no runs on it, and false otherwise 186 | * @param instanceToFind 187 | * @return 188 | */ 189 | public boolean isNodeCurrentlyEmpty(String instanceToFind) { 190 | ProxySet proxySet = getProxySet(); 191 | for(RemoteProxy proxy : proxySet){ 192 | List slots = proxy.getTestSlots(); 193 | Object instanceId = proxy.getConfig().get(AutomationConstants.INSTANCE_ID); 194 | // If the instance id's do not match, this means this is not the node we are looking for 195 | // and we should continue on to the next one 196 | if(!instanceToFind.equals(instanceId)) { 197 | continue; 198 | } 199 | // Now that we found the matching node, iterate over all its test slots to see if any sessions are running 200 | for (TestSlot testSlot : slots) { 201 | // If we find a running session, this means the node is occupied, so we should return false 202 | if(testSlot.getSession() != null) { 203 | return false; 204 | } 205 | } 206 | // If we reached this point, this means we found our target node AND it had no sessions, meaning the node was empty 207 | return true; 208 | } 209 | // If we didn't find a matching node, we're going to say the nodes is empty so we can terminate it 210 | log.warn("No matching node was found in the proxy set. Instance id: " + instanceToFind); 211 | return true; 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AutomationNodeRegistryTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.task; 13 | 14 | import com.google.common.annotations.VisibleForTesting; 15 | import com.rmn.qa.*; 16 | import com.rmn.qa.aws.AwsVmManager; 17 | import org.openqa.grid.internal.ProxySet; 18 | import org.openqa.grid.internal.RemoteProxy; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.text.ParseException; 23 | import java.util.Date; 24 | import java.util.Map; 25 | 26 | /** 27 | * Registry task which registers unregistered dynamic {@link com.rmn.qa.AutomationDynamicNode nodes}. This can happen if the hub process restarts for whatever reason 28 | * and loses track of previously registered nodes 29 | * @author mhardin 30 | */ 31 | public class AutomationNodeRegistryTask extends AbstractAutomationCleanupTask { 32 | 33 | private static final Logger log = LoggerFactory.getLogger(AutomationNodeRegistryTask.class); 34 | @VisibleForTesting 35 | static final String NAME = "Node Registry Task"; 36 | 37 | /** 38 | * Constructs a registry task with the specified context retrieval mechanism 39 | * @param registryRetriever Represents the retrieval mechanism you wish to use 40 | */ 41 | public AutomationNodeRegistryTask(RegistryRetriever registryRetriever) { 42 | super(registryRetriever); 43 | } 44 | 45 | /** 46 | * Returns the ProxySet to be used for cleanup purposes. 47 | * @return 48 | */ 49 | protected ProxySet getProxySet() { 50 | return registryRetriever.retrieveRegistry().getAllProxies(); 51 | } 52 | 53 | @Override 54 | public String getDescription() { 55 | return AutomationNodeRegistryTask.NAME; 56 | } 57 | 58 | // We're going to continuously iterate over registered nodes with the hub. If they're expired, we're going to mark them for removal. 59 | // If nodes marked for removal are used into the next billing cycle, then we're going to move their end date back again and put them back 60 | // into the running queue 61 | @Override 62 | public void doWork() { 63 | log.info("Looking for unregistered nodes"); 64 | ProxySet proxySet = getProxySet(); 65 | if(proxySet != null && proxySet.size() > 0) { 66 | for(RemoteProxy proxy : proxySet) { 67 | Map config = proxy.getConfig(); 68 | // If the config has an instanceId in it, this means this node was dynamically started and we should 69 | // track it if we are not already 70 | if(config.containsKey(AutomationConstants.INSTANCE_ID)) { 71 | String instanceId = (String)config.get(AutomationConstants.INSTANCE_ID); 72 | AutomationRunContext context = AutomationContext.getContext(); 73 | // If this node is already in our context, that means we are already tracking this node to terminate 74 | if(!context.nodeExists(instanceId)) { 75 | Date createdDate = getDate(config); 76 | // If we couldn't parse the date out, we are sort of out of luck 77 | if(createdDate == null) { 78 | break; 79 | } 80 | proxy.getConfig(); 81 | String uuid = (String)config.get(AutomationConstants.UUID); 82 | int threadCount = (Integer)config.get(AutomationConstants.CONFIG_MAX_SESSION); 83 | String browser = (String)config.get(AutomationConstants.CONFIG_BROWSER); 84 | String os = (String)config.get(AutomationConstants.CONFIG_OS); 85 | AutomationDynamicNode node = new AutomationDynamicNode(uuid,instanceId,browser,os,createdDate,threadCount); 86 | log.info("Unregistered dynamic node found: " + node); 87 | context.addNode(node); 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | /** 95 | * Attempts to parse the created date of the node from the capabilities object 96 | * @param capabilities 97 | * @return 98 | */ 99 | private Date getDate(Map capabilities) { 100 | String stringDate = (String)capabilities.get(AutomationConstants.CONFIG_CREATED_DATE); 101 | Date returnDate = null; 102 | try{ 103 | returnDate = AwsVmManager.NODE_DATE_FORMAT.parse(stringDate); 104 | } catch (ParseException pe) { 105 | log.error(String.format("Error trying to parse created date [%s]: %s", stringDate, pe)); 106 | } 107 | return returnDate; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AutomationReaperTask.java: -------------------------------------------------------------------------------- 1 | package com.rmn.qa.task; 2 | 3 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 4 | import com.amazonaws.services.ec2.model.Filter; 5 | import com.amazonaws.services.ec2.model.Instance; 6 | import com.amazonaws.services.ec2.model.Reservation; 7 | import com.google.common.annotations.VisibleForTesting; 8 | import com.rmn.qa.AutomationContext; 9 | import com.rmn.qa.AutomationUtils; 10 | import com.rmn.qa.RegistryRetriever; 11 | import com.rmn.qa.aws.VmManager; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | import java.util.Calendar; 16 | import java.util.Date; 17 | import java.util.List; 18 | 19 | public class AutomationReaperTask extends AbstractAutomationCleanupTask { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(AutomationReaperTask.class); 22 | @VisibleForTesting static final String NAME = "VM Reaper Task"; 23 | 24 | private VmManager ec2; 25 | 26 | /** 27 | * Constructs a registry task with the specified context retrieval mechanism 28 | * @param registryRetriever Represents the retrieval mechanism you wish to use 29 | */ 30 | public AutomationReaperTask(RegistryRetriever registryRetriever,VmManager ec2) { 31 | super(registryRetriever); 32 | this.ec2 = ec2; 33 | } 34 | @Override 35 | public void doWork() { 36 | log.info("Running " + AutomationReaperTask.NAME); 37 | DescribeInstancesRequest describeInstancesRequest = new DescribeInstancesRequest(); 38 | Filter filter = new Filter("tag:LaunchSource"); 39 | filter.withValues("SeleniumGridScalerPlugin"); 40 | describeInstancesRequest.withFilters(filter); 41 | List reservations = ec2.describeInstances(describeInstancesRequest); 42 | for(Reservation reservation : reservations) { 43 | for(Instance instance : reservation.getInstances()) { 44 | // Look for orphaned nodes 45 | Date threshold = AutomationUtils.modifyDate(new Date(),-30, Calendar.MINUTE); 46 | String instanceId = instance.getInstanceId(); 47 | // If we found a node old enough AND we're not internally tracking it, this means this is an orphaned node and we should terminate it 48 | if(threshold.after(instance.getLaunchTime()) && !AutomationContext.getContext().nodeExists(instanceId)) { 49 | log.info("Terminating orphaned node: " + instanceId); 50 | ec2.terminateInstance(instanceId); 51 | } 52 | } 53 | } 54 | } 55 | 56 | @Override 57 | public String getDescription() { 58 | return AutomationReaperTask.NAME; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/rmn/qa/task/AutomationRunCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | package com.rmn.qa.task; 13 | 14 | import com.google.common.annotations.VisibleForTesting; 15 | import com.rmn.qa.AutomationContext; 16 | import com.rmn.qa.AutomationRunContext; 17 | import com.rmn.qa.RegistryRetriever; 18 | import org.openqa.grid.internal.ProxySet; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | /** 23 | * Task to cleanup any registered {@link com.rmn.qa.AutomationRunRequest runs} that no longer have any running tests. 24 | * This will happen after the configured amount of time has passed since the run has started 25 | * @author mhardin 26 | */ 27 | public class AutomationRunCleanupTask extends AbstractAutomationCleanupTask { 28 | 29 | private static final Logger log = LoggerFactory.getLogger(AutomationRunCleanupTask.class); 30 | @VisibleForTesting 31 | static final String NAME = "Run Cleanup Task"; 32 | 33 | /** 34 | * Constructs a run cleanup task with the specified context retrieval mechanism 35 | * @param registryRetriever 36 | */ 37 | public AutomationRunCleanupTask(RegistryRetriever registryRetriever) { 38 | super(registryRetriever); 39 | } 40 | 41 | /** 42 | * {@inheritDoc} 43 | */ 44 | @Override 45 | public String getDescription() { 46 | return AutomationRunCleanupTask.NAME; 47 | } 48 | 49 | // Basically we want to continuously monitor all runs that are registered and ensure that at least 50 | // one test is being run that belongs to that test run. If there are any orphaned runs, we will 51 | // go ahead and remove it from AutomationRunContext providing they are old enough 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | @Override 56 | public void doWork() { 57 | log.info("Performing cleanup on runs."); 58 | AutomationRunContext context = AutomationContext.getContext(); 59 | context.cleanUpRunRequests(getProxySet()); 60 | } 61 | 62 | /** 63 | * Returns the {@link org.openqa.grid.internal.ProxySet ProxySet} to be used for cleanup purposes. 64 | * @return 65 | */ 66 | @VisibleForTesting 67 | protected ProxySet getProxySet() { 68 | return registryRetriever.retrieveRegistry().getAllProxies(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/resources/.s3cfg: -------------------------------------------------------------------------------- 1 | [default] 2 | access_key = 3 | secret_key = -------------------------------------------------------------------------------- /src/main/resources/aws.properties.default: -------------------------------------------------------------------------------- 1 | ########### Rename this file to 'aws.properties' to override default values 2 | 3 | # Default region 4 | region=us-east-1 5 | 6 | # Endpoints 7 | us-east-1_endpoint=https://us-east-1.ec2.amazonaws.com 8 | us-west-1_endpoint=https://us-west-1.ec2.amazonaws.com 9 | us-west-2_endpoint=https://ec2.us-west-2.amazonaws.com 10 | eu-west-1_endpoint=https://ec2.eu-west-1.amazonaws.com 11 | eu-central-1_endpoint=https://ec2.eu-central-1.amazonaws.com 12 | ap-southeast-2_endpoint=https://ec2.ap-southeast-2.amazonaws.com 13 | ap-northeast-1_endpoint=https://ec2.ap-northeast-1.amazonaws.com 14 | sa-east-1_endpoint=https://ec2.sa-east-1.amazonaws.com 15 | 16 | # Node AMI Info 17 | us-east-1_linux_node_ami=ami-d216a3ba 18 | node_instance_type_chrome=c3.large 19 | # Firefox and IE will only run in 1 browser per VM so we'll use micros for them 20 | node_instance_type_firefox=t2.micro 21 | node_instance_type_internetexplorer=t2.micro 22 | us-east-1_windows_node_ami=ami-dcd869b4 23 | 24 | 25 | # Security Group Info 26 | # us-east-1_security_group=sg-00000000 27 | # VPC Info 28 | # us-east-1_subnet_id=subnet-00000000 29 | # Fallback VPCs 30 | # us-east-1_subnet_fallback_id_1=subnet-11111111 31 | # us-east-1_subnet_fallback_id_2=subnet-22222222 32 | # us-east-1_subnet_fallback_id_3=subnet-33333333 33 | # Key 34 | # us-east-1_key_name=keyNameHere 35 | # Tags to be used 36 | # tagProduct=Department,QA 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/main/resources/hub.static.json: -------------------------------------------------------------------------------- 1 | { 2 | "host": null, 3 | "port": 4444, 4 | "newSessionWaitTimeout": -1, 5 | "prioritizer": null, 6 | "throwOnCapabilityNotPresent": false, 7 | "nodePolling": 5000, 8 | "unregisterIfStillDownAfter": 5000, 9 | "cleanUpCycle": 5000, 10 | "timeout": 80000, 11 | "servlets": ["com.rmn.qa.servlet.AutomationTestRunServlet","com.rmn.qa.servlet.StatusServlet"], 12 | "capabilityMatcher": "com.rmn.qa.AutomationCapabilityMatcher", 13 | "browserTimeout": 70000, 14 | "jettyMaxThreads":1024 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | 15 | 16 | 17 | %date{"MMM dd, yyyy HH:mm:ss:SSS aaa"} - %-5level [%thread] [%logger{0}] %msg%n 18 | 19 | 20 | 21 | 23 | 24 | ${logLocation:-log/automation.log} 25 | 26 | %date{"MMM dd, yyyy HH:mm:ss:SSS aaa"} - %-5level [%thread] [%logger{0}] %msg%n 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /src/main/resources/node.linux.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": 3 | [ 4 | { 5 | "browserName": "firefox", 6 | "maxInstances": , 7 | "instanceId": "", 8 | "seleniumProtocol": "WebDriver" 9 | }, 10 | { 11 | "browserName": "chrome", 12 | "maxInstances": , 13 | "instanceId": "", 14 | "seleniumProtocol": "WebDriver" 15 | }, 16 | ], 17 | "configuration": 18 | { 19 | "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy", 20 | "maxSession": , 21 | "uuid": "", 22 | "instanceId": "", 23 | "createdDate": "", 24 | "createdBrowser": "", 25 | "createdOs": "", 26 | "register": true, 27 | "registerCycle": 5000, 28 | "hubHost": "" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/resources/node.windows.json: -------------------------------------------------------------------------------- 1 | { 2 | "capabilities": 3 | [ 4 | { 5 | "browserName": "internet explorer", 6 | "maxInstances": , 7 | "instanceId": "", 8 | "seleniumProtocol": "WebDriver" 9 | }, 10 | { 11 | "browserName": "chrome", 12 | "maxInstances": , 13 | "instanceId": "", 14 | "seleniumProtocol": "WebDriver" 15 | }, 16 | { 17 | "browserName": "firefox", 18 | "maxInstances": , 19 | "instanceId": "", 20 | "seleniumProtocol": "WebDriver" 21 | }, 22 | ], 23 | "configuration": 24 | { 25 | "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy", 26 | "maxSession": , 27 | "uuid": "", 28 | "instanceId": "", 29 | "createdDate": "", 30 | "createdBrowser": "", 31 | "createdOs": "", 32 | "register": true, 33 | "registerCycle": 5000, 34 | "hubHost": "" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/resources/nodeConfigTemplate.json: -------------------------------------------------------------------------------- 1 | { "capabilities": 2 | [ 3 | { 4 | "browserName": "firefox", 5 | "maxInstances": , 6 | "instanceId": "", 7 | "seleniumProtocol": "WebDriver" 8 | }, 9 | { 10 | "browserName": "chrome", 11 | "maxInstances": , 12 | "instanceId": "", 13 | "seleniumProtocol": "WebDriver" 14 | }, 15 | ], 16 | "configuration": 17 | { 18 | "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy", 19 | "maxSession": , 20 | "uuid": "", 21 | "instanceId": "", 22 | "createdDate": "", 23 | "createdBrowser": "", 24 | "createdOs": "", 25 | "register": true, 26 | "registerCycle": 5000, 27 | "hubHost": "" 28 | } 29 | } -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AbstractAutomationCleanupTaskTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import com.rmn.qa.task.AbstractAutomationCleanupTask; 16 | import junit.framework.Assert; 17 | import org.junit.Test; 18 | 19 | /** 20 | * Created by mhardin on 4/25/14. 21 | */ 22 | public class AbstractAutomationCleanupTaskTest { 23 | 24 | @Test 25 | public void testExceptionHandled() { 26 | TestTask task = new TestTask(null); 27 | task.run(); 28 | Assert.assertEquals("Correct exception should have been thrown",task.getThrowable(),task.re); 29 | } 30 | 31 | private static class TestTask extends AbstractAutomationCleanupTask { 32 | 33 | private RuntimeException re = new RuntimeException("test exception"); 34 | 35 | public TestTask(RegistryRetriever retriever) { 36 | super(retriever); 37 | } 38 | 39 | @Override 40 | public void doWork() { 41 | throw re; 42 | } 43 | 44 | @Override 45 | public String getDescription() { 46 | return null; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AutomationCapabilityMatcherTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import com.google.common.collect.ImmutableMap; 16 | import junit.framework.Assert; 17 | import org.junit.After; 18 | import org.junit.Test; 19 | import org.openqa.selenium.remote.CapabilityType; 20 | 21 | import java.util.Date; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | /** 26 | * Created by mhardin on 4/25/14. 27 | */ 28 | public class AutomationCapabilityMatcherTest { 29 | 30 | @Test 31 | public void testMatches() { 32 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 33 | Map nodeCapability = new HashMap(); 34 | nodeCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 35 | Map testCapability = new HashMap(); 36 | testCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 37 | Assert.assertTrue("Capabilities should match as node is not dynamic",matcher.matches(nodeCapability,testCapability)); 38 | } 39 | 40 | @Test 41 | public void testNodeNotInContext() { 42 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 43 | Map nodeCapability = new HashMap(); 44 | nodeCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 45 | nodeCapability.put(AutomationConstants.INSTANCE_ID,"foo"); 46 | Map testCapability = new HashMap(); 47 | testCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 48 | AutomationDynamicNode node = new AutomationDynamicNode("uuid","id","browser","os", new Date(),10); 49 | node.updateStatus(AutomationDynamicNode.STATUS.EXPIRED); 50 | Assert.assertTrue("Capabilities should match as node is not in context",matcher.matches(nodeCapability,testCapability)); 51 | } 52 | 53 | @Test 54 | public void testExpiredNode() { 55 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 56 | Map nodeCapability = new HashMap(); 57 | nodeCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 58 | nodeCapability.put(AutomationConstants.INSTANCE_ID,"foo"); 59 | Map testCapability = new HashMap(); 60 | testCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 61 | AutomationDynamicNode node = new AutomationDynamicNode("uuid","foo","browser","os", new Date(),10); 62 | AutomationContext.getContext().addNode(node); 63 | node.updateStatus(AutomationDynamicNode.STATUS.EXPIRED); 64 | Assert.assertFalse("Capabilities should match as node is not in context", matcher.matches(nodeCapability, testCapability)); 65 | } 66 | 67 | @Test 68 | public void testTerminatedNode() { 69 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 70 | Map nodeCapability = new HashMap(); 71 | nodeCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 72 | nodeCapability.put(AutomationConstants.INSTANCE_ID,"foo"); 73 | Map testCapability = new HashMap(); 74 | testCapability.put(CapabilityType.BROWSER_NAME,"firefox"); 75 | AutomationDynamicNode node = new AutomationDynamicNode("uuid","foo","browser","os", new Date(),10); 76 | AutomationContext.getContext().addNode(node); 77 | node.updateStatus(AutomationDynamicNode.STATUS.TERMINATED); 78 | Assert.assertFalse("Capabilities should match as node is not in context",matcher.matches(nodeCapability,testCapability)); 79 | } 80 | 81 | @Test 82 | public void testSystemPropertyParsed() { 83 | String soleProperty = "foo"; 84 | System.setProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME,soleProperty); 85 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 86 | Assert.assertEquals("Only one property should have been added", 1, matcher.additionalConsiderations.size()); 87 | Assert.assertTrue("Contained property should be correct", matcher.additionalConsiderations.contains(soleProperty)); 88 | } 89 | 90 | @Test 91 | public void testMultipleSystemPropertiesParsed() { 92 | String firstProperty = "foo", secondProperty = "bar"; 93 | System.setProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME,firstProperty + "," + secondProperty); 94 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 95 | Assert.assertEquals("Only one property should have been added",2,matcher.additionalConsiderations.size()); 96 | Assert.assertTrue("Contained property should be correct", matcher.additionalConsiderations.contains(firstProperty)); 97 | Assert.assertTrue("Contained property should be correct", matcher.additionalConsiderations.contains(secondProperty)); 98 | } 99 | 100 | @Test 101 | public void testPropertyDoesntMatch() { 102 | String soleProperty = "foo"; 103 | System.setProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME,soleProperty); 104 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 105 | Map nodeCapability = ImmutableMap.of("foo", (Object)"bar"); 106 | Map requestedCapability = ImmutableMap.of("foo", (Object)"doesntMatch"); 107 | Assert.assertFalse("Capabilities should not match due to override", matcher.matches(nodeCapability, requestedCapability)); 108 | } 109 | 110 | @Test 111 | public void testPropertyNotPresentInCapabilities() { 112 | String soleProperty = "foo"; 113 | System.setProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME,soleProperty); 114 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 115 | Map nodeCapability = ImmutableMap.of("browser", (Object)"bar"); 116 | Map requestedCapability = ImmutableMap.of("browser", (Object)"bar"); 117 | Assert.assertTrue("Capabilities should not match due to override", matcher.matches(nodeCapability, requestedCapability)); 118 | } 119 | 120 | @Test 121 | public void testPropertyDoesMatch() { 122 | String soleProperty = "foo"; 123 | System.setProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME,soleProperty); 124 | AutomationCapabilityMatcher matcher = new AutomationCapabilityMatcher(); 125 | Map nodeCapability = ImmutableMap.of("foo", (Object)"bar"); 126 | Map requestedCapability = ImmutableMap.of("foo", (Object)"bar"); 127 | Assert.assertTrue("Capabilities should not match due to override",matcher.matches(nodeCapability,requestedCapability)); 128 | } 129 | 130 | @After 131 | public void clearSystemProperty() { 132 | System.clearProperty(AutomationConstants.EXTRA_CAPABILITIES_PROPERTY_NAME); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AutomationDynamicNodeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import nl.jqno.equalsverifier.EqualsVerifier; 16 | import nl.jqno.equalsverifier.Warning; 17 | import org.junit.Test; 18 | 19 | /** 20 | * Created by mhardin on 5/1/14. 21 | */ 22 | public class AutomationDynamicNodeTest { 23 | 24 | @Test 25 | public void equalsContract() { 26 | EqualsVerifier.forClass(AutomationDynamicNode.class).suppress(Warning.NULL_FIELDS,Warning.NONFINAL_FIELDS).verify(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AutomationRunContextTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import junit.framework.Assert; 16 | import nl.jqno.equalsverifier.EqualsVerifier; 17 | import nl.jqno.equalsverifier.Warning; 18 | import org.junit.After; 19 | import org.junit.Test; 20 | import org.openqa.grid.common.SeleniumProtocol; 21 | import org.openqa.grid.internal.ProxySet; 22 | import org.openqa.grid.internal.TestSlot; 23 | import org.openqa.grid.internal.utils.CapabilityMatcher; 24 | import org.openqa.selenium.remote.CapabilityType; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Calendar; 28 | import java.util.Date; 29 | import java.util.HashMap; 30 | import java.util.List; 31 | import java.util.Map; 32 | 33 | /** 34 | * Created by mhardin on 5/1/14. 35 | */ 36 | public class AutomationRunContextTest { 37 | 38 | @After() 39 | public void cleanUp() { 40 | AutomationContext.refreshContext(); 41 | } 42 | 43 | @Test 44 | // Tests that an old run gets cleaned up (removed) 45 | public void testOldRun() { 46 | AutomationRunRequest oldRequest = new AutomationRunRequest("uuid",10,"firefox","10","linux",AutomationUtils.modifyDate(new Date(),-5, Calendar.MINUTE)); 47 | AutomationRunContext context = AutomationContext.getContext(); 48 | context.addRun(oldRequest); 49 | 50 | Assert.assertTrue("Run should exist", context.hasRun(oldRequest.getUuid())); 51 | ProxySet proxySet = new ProxySet(false); 52 | proxySet.add(new MockRemoteProxy()); 53 | context.cleanUpRunRequests(proxySet); 54 | Assert.assertFalse("Run request should no longer exist as it should have been removed", context.hasRun(oldRequest.getUuid())); 55 | } 56 | 57 | @Test 58 | // Tests that a new run does not get cleaned up (removed) 59 | public void testNewRun() { 60 | AutomationRunRequest oldRequest = new AutomationRunRequest("uuid",10,"firefox"); 61 | AutomationRunContext context = AutomationContext.getContext(); 62 | context.addRun(oldRequest); 63 | 64 | Assert.assertTrue("Run should exist", context.hasRun(oldRequest.getUuid())); 65 | ProxySet proxySet = new ProxySet(false); 66 | proxySet.add(new MockRemoteProxy()); 67 | context.cleanUpRunRequests(proxySet); 68 | Assert.assertTrue("Run request should still exist as the run was new enough", context.hasRun(oldRequest.getUuid())); 69 | } 70 | 71 | @Test 72 | // Tests that a new run does not get cleaned up (removed) 73 | public void testNewRunIE() { 74 | AutomationRunRequest oldRequest = new AutomationRunRequest("uuid",10,"internetexplorer","10","linux",AutomationUtils.modifyDate(new Date(),-7, Calendar.MINUTE)); 75 | AutomationRunContext context = AutomationContext.getContext(); 76 | context.addRun(oldRequest); 77 | 78 | 79 | Assert.assertTrue("Run should exist", context.hasRun(oldRequest.getUuid())); 80 | ProxySet proxySet = new ProxySet(false); 81 | proxySet.add(new MockRemoteProxy()); 82 | context.cleanUpRunRequests(proxySet); 83 | Assert.assertTrue("Run request should still exist as the run was new enough", context.hasRun(oldRequest.getUuid())); 84 | } 85 | 86 | @Test 87 | // Tests that a new run does not get cleaned up (removed) 88 | public void testOldRunIE() { 89 | AutomationRunRequest oldRequest = new AutomationRunRequest("uuid",10,"internetexplorer","10","linux",AutomationUtils.modifyDate(new Date(),-15, Calendar.MINUTE)); 90 | AutomationRunContext context = AutomationContext.getContext(); 91 | context.addRun(oldRequest); 92 | 93 | 94 | Assert.assertTrue("Run should exist", context.hasRun(oldRequest.getUuid())); 95 | ProxySet proxySet = new ProxySet(false); 96 | proxySet.add(new MockRemoteProxy()); 97 | context.cleanUpRunRequests(proxySet); 98 | Assert.assertFalse("Run request should no longer exist as it should have been removed", context.hasRun(oldRequest.getUuid())); 99 | } 100 | 101 | @Test 102 | // Tests that a run with slots does not get removed 103 | public void testActiveSession() { 104 | String uuid = "uuid"; 105 | AutomationRunRequest request = new AutomationRunRequest(uuid,10,"firefox","10","linux",AutomationUtils.modifyDate(new Date(),-1, Calendar.HOUR)); 106 | AutomationRunContext context = AutomationContext.getContext(); 107 | context.addRun(request); 108 | 109 | Assert.assertTrue("Run should exist", context.hasRun(request.getUuid())); 110 | ProxySet proxySet = new ProxySet(false); 111 | MockRemoteProxy proxy = new MockRemoteProxy(); 112 | CapabilityMatcher matcher = new AutomationCapabilityMatcher(); 113 | proxy.setCapabilityMatcher(matcher); 114 | proxySet.add(proxy); 115 | Map config = new HashMap<>(); 116 | config.put(AutomationConstants.UUID,uuid); 117 | proxy.setConfig(config); 118 | List testSlots = new ArrayList<>(); 119 | TestSlot testSlot = new TestSlot(proxy,null,null,config); 120 | proxy.setTestSlots(testSlots); 121 | testSlot.getNewSession(config); 122 | testSlots.add(testSlot); 123 | proxySet.add(proxy); 124 | context.cleanUpRunRequests(proxySet); 125 | Assert.assertTrue("Run request should still exist as there were active sessions", context.hasRun(request.getUuid())); 126 | } 127 | 128 | @Test 129 | // Tests that a run with slots does not get removed 130 | public void testNoSessions() { 131 | String uuid = "uuid"; 132 | AutomationRunRequest request = new AutomationRunRequest(uuid,10,"firefox","10","linux",AutomationUtils.modifyDate(new Date(),-1, Calendar.HOUR)); 133 | AutomationRunContext context = AutomationContext.getContext(); 134 | context.addRun(request); 135 | 136 | Assert.assertTrue("Run should exist", context.hasRun(request.getUuid())); 137 | ProxySet proxySet = new ProxySet(false); 138 | MockRemoteProxy proxy = new MockRemoteProxy(); 139 | CapabilityMatcher matcher = new AutomationCapabilityMatcher(); 140 | proxy.setCapabilityMatcher(matcher); 141 | proxySet.add(proxy); 142 | Map config = new HashMap<>(); 143 | config.put(AutomationConstants.UUID,uuid); 144 | proxy.setConfig(config); 145 | List testSlots = new ArrayList<>(); 146 | TestSlot testSlot = new TestSlot(proxy,null,null,config); 147 | proxy.setTestSlots(testSlots); 148 | testSlots.add(testSlot); 149 | proxySet.add(proxy); 150 | context.cleanUpRunRequests(proxySet); 151 | Assert.assertFalse("Run request should not exist as there were no active sessions", context.hasRun(request.getUuid())); 152 | } 153 | 154 | @Test 155 | // Tests that a newly created run is considered a 'new' run 156 | public void testIsNewRun() { 157 | AutomationRunRequest first = new AutomationRunRequest("uuid",3,"firefox"); 158 | AutomationRunContext context = AutomationContext.getContext(); 159 | context.addRun(first); 160 | Assert.assertTrue("Run should be considered new", context.isNewRunQueuedUp()); 161 | } 162 | 163 | @Test 164 | // Tests that a newly created run is considered a 'new' run 165 | public void testNotNewRun() { 166 | AutomationRunRequest first = new AutomationRunRequest("uuid",null,null,null,null,AutomationUtils.modifyDate(new Date(),-3,Calendar.MINUTE)); 167 | AutomationRunContext context = AutomationContext.getContext(); 168 | context.addRun(first); 169 | Assert.assertFalse("Run should be considered new", context.isNewRunQueuedUp()); 170 | } 171 | 172 | @Test 173 | // Tests that equals and hash code are working as expected 174 | public void equalsContract() { 175 | EqualsVerifier.forClass(AutomationRunRequest.class).suppress(Warning.NULL_FIELDS).verify(); 176 | } 177 | 178 | @Test 179 | // Tests that the correct node count is returned when tests don't have a UUID 180 | public void testNodesFreeNoUuid() { 181 | AutomationRunContext runContext = new AutomationRunContext(); 182 | runContext.setTotalNodeCount(10); 183 | ProxySet proxySet = new ProxySet(false); 184 | MockRemoteProxy proxy = new MockRemoteProxy(); 185 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 186 | Map capabilities = new HashMap<>(); 187 | capabilities.put(CapabilityType.PLATFORM,"linux"); 188 | capabilities.put(CapabilityType.BROWSER_NAME,"chrome"); 189 | TestSlot testSlot = new TestSlot(proxy, SeleniumProtocol.WebDriver,null,capabilities); 190 | testSlot.getNewSession(capabilities); 191 | proxy.setMultipleTestSlots(testSlot,5); 192 | proxySet.add(proxy); 193 | int freeThreads = runContext.getTotalThreadsAvailable(proxySet); 194 | Assert.assertEquals(5,freeThreads); 195 | } 196 | 197 | @Test 198 | // Tests that the correct node count is returned when tests don't have a UUID 199 | public void testNodesFreeWithUuid() { 200 | AutomationRunContext runContext = new AutomationRunContext(); 201 | runContext.setTotalNodeCount(10); 202 | ProxySet proxySet = new ProxySet(false); 203 | MockRemoteProxy proxy = new MockRemoteProxy(); 204 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 205 | Map capabilities = new HashMap<>(); 206 | capabilities.put(CapabilityType.PLATFORM,"linux"); 207 | capabilities.put(CapabilityType.BROWSER_NAME,"chrome"); 208 | capabilities.put(AutomationConstants.UUID,"testUuid"); 209 | TestSlot testSlot = new TestSlot(proxy, SeleniumProtocol.WebDriver,null,capabilities); 210 | testSlot.getNewSession(capabilities); 211 | proxy.setMultipleTestSlots(testSlot,5); 212 | proxySet.add(proxy); 213 | int freeThreads = runContext.getTotalThreadsAvailable(proxySet); 214 | Assert.assertEquals(5,freeThreads); 215 | } 216 | 217 | @Test 218 | // Tests that a new run is counted instead of tests in progress 219 | public void testNewRunIsCounted() { 220 | String uuid = "testUuid"; 221 | AutomationRunContext runContext = new AutomationRunContext(); 222 | runContext.setTotalNodeCount(10); 223 | ProxySet proxySet = new ProxySet(false); 224 | MockRemoteProxy proxy = new MockRemoteProxy(); 225 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 226 | Map capabilities = new HashMap<>(); 227 | capabilities.put(CapabilityType.PLATFORM,"linux"); 228 | capabilities.put(CapabilityType.BROWSER_NAME,"chrome"); 229 | capabilities.put(AutomationConstants.UUID,uuid); 230 | AutomationRunRequest request = new AutomationRunRequest(uuid,10,"chrome"); 231 | runContext.addRun(request); 232 | TestSlot testSlot = new TestSlot(proxy, SeleniumProtocol.WebDriver,null,capabilities); 233 | testSlot.getNewSession(capabilities); 234 | proxy.setMultipleTestSlots(testSlot,5); 235 | proxySet.add(proxy); 236 | int freeThreads = runContext.getTotalThreadsAvailable(proxySet); 237 | Assert.assertEquals(0,freeThreads); 238 | } 239 | 240 | @Test 241 | // Tests that for an old run, the in progress tests are counted 242 | public void testOldRunInProgress() { 243 | String uuid = "testUuid"; 244 | AutomationRunContext runContext = new AutomationRunContext(); 245 | runContext.setTotalNodeCount(10); 246 | ProxySet proxySet = new ProxySet(false); 247 | MockRemoteProxy proxy = new MockRemoteProxy(); 248 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 249 | Map capabilities = new HashMap<>(); 250 | capabilities.put(CapabilityType.PLATFORM,"linux"); 251 | capabilities.put(CapabilityType.BROWSER_NAME,"chrome"); 252 | capabilities.put(AutomationConstants.UUID,uuid); 253 | AutomationRunRequest request = new AutomationRunRequest(uuid,10,"chrome","23","linux", AutomationUtils.modifyDate(new Date(),-5,Calendar.MINUTE)); 254 | runContext.addRun(request); 255 | TestSlot testSlot = new TestSlot(proxy, SeleniumProtocol.WebDriver,null,capabilities); 256 | testSlot.getNewSession(capabilities); 257 | int inProgressTests = 5; 258 | proxy.setMultipleTestSlots(testSlot,inProgressTests); 259 | proxySet.add(proxy); 260 | int freeThreads = runContext.getTotalThreadsAvailable(proxySet); 261 | Assert.assertEquals("Free threads should reflect in progress test count",inProgressTests,freeThreads); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AutomationRunRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import junit.framework.Assert; 16 | import org.junit.Test; 17 | import org.openqa.selenium.remote.CapabilityType; 18 | 19 | import java.util.Calendar; 20 | import java.util.Date; 21 | import java.util.HashMap; 22 | import java.util.Map; 23 | 24 | /** 25 | * Created by mhardin on 4/24/14. 26 | */ 27 | public class AutomationRunRequestTest { 28 | 29 | @Test 30 | // Tests that two run request objects match each other 31 | public void testMatchesOtherRunRequest() { 32 | String uuid = "testUuid"; 33 | String browser = "firefox"; 34 | String browserVersion = "20"; 35 | String os = "linux"; 36 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 37 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 38 | Assert.assertTrue("Run requests should match",first.matchesCapabilities(second)); 39 | } 40 | 41 | @Test 42 | // Tests that two run request objects do match each other due to mismatching browsers 43 | public void testMatchesOtherRunRequestBadBrowser() { 44 | String uuid = "testUuid"; 45 | String browser = "firefox"; 46 | String browserVersion = "20"; 47 | String os = "linux"; 48 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 49 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,"badBrowser",browserVersion,os); 50 | Assert.assertFalse("Run requests should NOT match due to browser",first.matchesCapabilities(second)); 51 | } 52 | @Test 53 | // Tests that two run request objects do match each other due to mismatching browser versions 54 | public void testMatchesOtherRunRequestBadBrowserVersion() { 55 | String uuid = "testUuid"; 56 | String browser = "firefox"; 57 | String browserVersion = "20"; 58 | String os = "linux"; 59 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 60 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,browser,"12432",os); 61 | Assert.assertFalse("Run requests should NOT match due to browser version", first.matchesCapabilities(second)); 62 | } 63 | @Test 64 | // Tests that two run request objects do match each other due to mismatching OS 65 | public void testMatchesOtherRunRequestBadOs() { 66 | String uuid = "testUuid"; 67 | String browser = "firefox"; 68 | String browserVersion = "20"; 69 | String os = "linux"; 70 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 71 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,browser,browserVersion,"badOs"); 72 | Assert.assertFalse("Run requests should NOT match due to browser",first.matchesCapabilities(second)); 73 | } 74 | 75 | @Test 76 | // Tests that two run request objects match each other when option fields are not set on the first object 77 | public void testMatchesOtherRunRequestOptionalParameters() { 78 | String uuid = "testUuid"; 79 | String browser = "firefox"; 80 | String browserVersion = null; 81 | String os = null; 82 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 83 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,browser,"20","linux"); 84 | Assert.assertTrue("Run requests should match",first.matchesCapabilities(second)); 85 | } 86 | 87 | @Test 88 | // Tests that two run request objects do NOT match each other since the optional fields are on the second object 89 | public void testDoesntMatchesOtherRunRequestNonOptionalParameters() { 90 | String uuid = "testUuid"; 91 | String browser = "firefox"; 92 | String browserVersion = null; 93 | String os = null; 94 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,"20","linux"); 95 | AutomationRunRequest second = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 96 | Assert.assertFalse("Run requests should match", first.matchesCapabilities(second)); 97 | } 98 | 99 | @Test 100 | // Tests that two run request objects match each other 101 | public void testMatchesCapabilities() { 102 | String uuid = "testUuid"; 103 | String browser = "firefox"; 104 | String browserVersion = "20"; 105 | String os = "linux"; 106 | Map map = new HashMap(); 107 | map.put(CapabilityType.BROWSER_NAME,browser); 108 | map.put(CapabilityType.VERSION,browserVersion); 109 | map.put(CapabilityType.PLATFORM,os); 110 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 111 | Assert.assertTrue("Capabilities should match",first.matchesCapabilities(map)); 112 | } 113 | 114 | @Test 115 | // Tests that two run request objects dont match each other due to incorrect browser 116 | public void testMatchesCapabilitiesBadBrowser() { 117 | String uuid = "testUuid"; 118 | String browser = "firefox"; 119 | String browserVersion = "20"; 120 | String os = "linux"; 121 | Map map = new HashMap(); 122 | map.put(CapabilityType.BROWSER_NAME,browser); 123 | map.put(CapabilityType.VERSION,browserVersion); 124 | map.put(CapabilityType.PLATFORM,os); 125 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,"badBrowser",browserVersion,os); 126 | Assert.assertFalse("Capabilities should match",first.matchesCapabilities(map)); 127 | } 128 | 129 | @Test 130 | // Tests that two run request objects dont match each other due to incorrect browser version 131 | public void testMatchesCapabilitiesBadVersion() { 132 | String uuid = "testUuid"; 133 | String browser = "firefox"; 134 | String browserVersion = "20"; 135 | String os = "linux"; 136 | Map map = new HashMap(); 137 | map.put(CapabilityType.BROWSER_NAME,browser); 138 | map.put(CapabilityType.VERSION,browserVersion); 139 | map.put(CapabilityType.PLATFORM,os); 140 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,"123",os); 141 | Assert.assertFalse("Capabilities should match",first.matchesCapabilities(map)); 142 | } 143 | 144 | @Test 145 | // Tests that two run request objects dont match each other due to incorrect os 146 | public void testMatchesCapabilitiesBadOs() { 147 | String uuid = "testUuid"; 148 | String browser = "firefox"; 149 | String browserVersion = "20"; 150 | String os = "linux"; 151 | Map map = new HashMap(); 152 | map.put(CapabilityType.BROWSER_NAME,browser); 153 | map.put(CapabilityType.VERSION,browserVersion); 154 | map.put(CapabilityType.PLATFORM,os); 155 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,"badOs"); 156 | Assert.assertFalse("Capabilities should match", first.matchesCapabilities(map)); 157 | } 158 | 159 | @Test 160 | // Tests that two run request objects match each other when option fields are not set on the first object 161 | public void testMatchesCapabilitiesOptionalParameters() { 162 | String uuid = "testUuid"; 163 | String browser = "firefox"; 164 | String browserVersion = null; 165 | String os = null; 166 | Map map = new HashMap(); 167 | map.put(CapabilityType.BROWSER_NAME,browser); 168 | map.put(CapabilityType.VERSION,"20"); 169 | map.put(CapabilityType.PLATFORM,"badOs"); 170 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 171 | Assert.assertTrue("Capabilities should match",first.matchesCapabilities(map)); 172 | } 173 | 174 | @Test 175 | // Tests that two run request objects do NOT match each other since the optional fields are on the second object 176 | public void testMatchesCapabilitiesNonOptionalParameters() { 177 | String uuid = "testUuid"; 178 | String browser = "firefox"; 179 | String browserVersion = "21"; 180 | String os = "linux"; 181 | Map map = new HashMap(); 182 | map.put(CapabilityType.BROWSER_NAME,browser); 183 | map.put(CapabilityType.VERSION,"20"); 184 | map.put(CapabilityType.PLATFORM,"badOs"); 185 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 186 | Assert.assertFalse("Capabilities should NOT match",first.matchesCapabilities(map)); 187 | } 188 | 189 | @Test 190 | // Tests that two run request objects do NOT match each other since the optional fields are on the second object 191 | public void testMatchesFactoryMethod() { 192 | String uuid = "testUuid"; 193 | String browser = "firefox"; 194 | String browserVersion = "25"; 195 | String os = "linux"; 196 | Map map = new HashMap(); 197 | map.put(CapabilityType.BROWSER_NAME,browser); 198 | map.put(CapabilityType.VERSION,browserVersion); 199 | map.put(CapabilityType.PLATFORM,os); 200 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,os); 201 | AutomationRunRequest second = AutomationRunRequest.requestFromCapabilities(map); 202 | Assert.assertEquals("Factory method should have generated an equal request", first, second); 203 | } 204 | 205 | @Test 206 | // Tests that a newly created run is considered a 'new' run 207 | public void testIsNewRun() { 208 | AutomationRunRequest first = new AutomationRunRequest(null); 209 | Assert.assertTrue("Run should be considered new", first.isNewRun()); 210 | } 211 | 212 | @Test 213 | // Test that an old run is not considered new 214 | public void testIsNotNewRun() { 215 | Date oldDate = AutomationUtils.modifyDate(new Date(), -121, Calendar.SECOND); 216 | AutomationRunRequest first = new AutomationRunRequest(null,null,null,null,null,oldDate); 217 | Assert.assertFalse("Run should be considered new", first.isNewRun()); 218 | } 219 | 220 | @Test 221 | // Tests that a platform of 'ANY' matches another otherwise non-matching OS 222 | public void testOsMatchesAnyRequests() { 223 | String uuid = "testUuid"; 224 | String browser = "firefox"; 225 | String browserVersion = "25"; 226 | String os = "ANY"; 227 | Map map = new HashMap<>(); 228 | map.put(CapabilityType.BROWSER_NAME,browser); 229 | map.put(CapabilityType.VERSION,browserVersion); 230 | map.put(CapabilityType.PLATFORM,os); 231 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,"linux"); 232 | AutomationRunRequest second = AutomationRunRequest.requestFromCapabilities(map); 233 | Assert.assertTrue("Requests should be equal", first.matchesCapabilities(second)); 234 | Assert.assertTrue("Requests should be equal", second.matchesCapabilities(first)); 235 | } 236 | 237 | @Test 238 | // Tests that a platform of 'ANY' matches another otherwise non-matching OS 239 | public void testOsMatchesAnyCapability() { 240 | String uuid = "testUuid"; 241 | String browser = "firefox"; 242 | String browserVersion = "25"; 243 | String os = "ANY"; 244 | Map capabilities = new HashMap<>(); 245 | capabilities.put(CapabilityType.BROWSER_NAME, browser); 246 | capabilities.put(CapabilityType.VERSION, browserVersion); 247 | capabilities.put(CapabilityType.PLATFORM, os); 248 | AutomationRunRequest first = new AutomationRunRequest(uuid,null,browser,browserVersion,"linux"); 249 | Assert.assertTrue("Requests should be equal", first.matchesCapabilities(capabilities)); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/AutomationUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import junit.framework.Assert; 16 | import org.junit.Test; 17 | 18 | import java.util.Calendar; 19 | import java.util.Date; 20 | 21 | public class AutomationUtilsTest { 22 | 23 | @Test 24 | public void testDifferentCasedEquals() { 25 | String case1 = "CASE"; 26 | String case2 = case1.toLowerCase(); 27 | Assert.assertTrue("Match should not be case sensitive", AutomationUtils.lowerCaseMatch(case1, case2)); 28 | } 29 | 30 | @Test 31 | public void testDifferentCasedReversedEquals() { 32 | String case1 = "CASE"; 33 | String case2 = case1.toLowerCase(); 34 | Assert.assertTrue("Match should not be case sensitive", AutomationUtils.lowerCaseMatch(case2, case1)); 35 | } 36 | 37 | @Test 38 | public void testWhiteSpaceSensitiveEquals() { 39 | String case1 = " foo fighters "; 40 | String case2 = "foofighters"; 41 | Assert.assertTrue("Whitespace should be ignored", AutomationUtils.lowerCaseMatch(case1, case2)); 42 | } 43 | 44 | @Test 45 | public void testDateIsAfter() { 46 | Date baseDate = new Date(); 47 | Calendar c = Calendar.getInstance(); 48 | c.setTime(baseDate); 49 | c.add(Calendar.SECOND,-10); 50 | Date afterDate = c.getTime(); 51 | Assert.assertTrue("Date should be considered after", AutomationUtils.isCurrentTimeAfterDate(afterDate,9,Calendar.SECOND)); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/MockHttpServletRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import javax.servlet.RequestDispatcher; 16 | import javax.servlet.ServletInputStream; 17 | import javax.servlet.http.Cookie; 18 | import javax.servlet.http.HttpServletRequest; 19 | import javax.servlet.http.HttpSession; 20 | import java.io.BufferedReader; 21 | import java.io.IOException; 22 | import java.io.UnsupportedEncodingException; 23 | import java.security.Principal; 24 | import java.util.Enumeration; 25 | import java.util.HashMap; 26 | import java.util.Locale; 27 | import java.util.Map; 28 | 29 | /** 30 | * Created by mhardin on 2/6/14. 31 | */ 32 | public class MockHttpServletRequest implements HttpServletRequest { 33 | 34 | private Map parameters = new HashMap(); 35 | 36 | @Override 37 | public String getAuthType() { 38 | return null; 39 | } 40 | 41 | @Override 42 | public Cookie[] getCookies() { 43 | return new Cookie[0]; 44 | } 45 | 46 | @Override 47 | public long getDateHeader(String name) { 48 | return 0; 49 | } 50 | 51 | @Override 52 | public String getHeader(String name) { 53 | return null; 54 | } 55 | 56 | @Override 57 | public Enumeration getHeaders(String name) { 58 | return null; 59 | } 60 | 61 | @Override 62 | public Enumeration getHeaderNames() { 63 | return null; 64 | } 65 | 66 | @Override 67 | public int getIntHeader(String name) { 68 | return 0; 69 | } 70 | 71 | @Override 72 | public String getMethod() { 73 | return null; 74 | } 75 | 76 | @Override 77 | public String getPathInfo() { 78 | return null; 79 | } 80 | 81 | @Override 82 | public String getPathTranslated() { 83 | return null; 84 | } 85 | 86 | @Override 87 | public String getContextPath() { 88 | return null; 89 | } 90 | 91 | @Override 92 | public String getQueryString() { 93 | return null; 94 | } 95 | 96 | @Override 97 | public String getRemoteUser() { 98 | return null; 99 | } 100 | 101 | @Override 102 | public boolean isUserInRole(String role) { 103 | return false; 104 | } 105 | 106 | @Override 107 | public Principal getUserPrincipal() { 108 | return null; 109 | } 110 | 111 | @Override 112 | public String getRequestedSessionId() { 113 | return null; 114 | } 115 | 116 | @Override 117 | public String getRequestURI() { 118 | return null; 119 | } 120 | 121 | @Override 122 | public StringBuffer getRequestURL() { 123 | return null; 124 | } 125 | 126 | @Override 127 | public String getServletPath() { 128 | return null; 129 | } 130 | 131 | @Override 132 | public HttpSession getSession(boolean create) { 133 | return null; 134 | } 135 | 136 | @Override 137 | public HttpSession getSession() { 138 | return null; 139 | } 140 | 141 | @Override 142 | public boolean isRequestedSessionIdValid() { 143 | return false; 144 | } 145 | 146 | @Override 147 | public boolean isRequestedSessionIdFromCookie() { 148 | return false; 149 | } 150 | 151 | @Override 152 | public boolean isRequestedSessionIdFromURL() { 153 | return false; 154 | } 155 | 156 | @Override 157 | public boolean isRequestedSessionIdFromUrl() { 158 | return false; 159 | } 160 | 161 | @Override 162 | public Object getAttribute(String name) { 163 | return null; 164 | } 165 | 166 | @Override 167 | public Enumeration getAttributeNames() { 168 | return null; 169 | } 170 | 171 | @Override 172 | public String getCharacterEncoding() { 173 | return null; 174 | } 175 | 176 | @Override 177 | public void setCharacterEncoding(String env) throws UnsupportedEncodingException { 178 | 179 | } 180 | 181 | @Override 182 | public int getContentLength() { 183 | return 0; 184 | } 185 | 186 | @Override 187 | public String getContentType() { 188 | return null; 189 | } 190 | 191 | @Override 192 | public ServletInputStream getInputStream() throws IOException { 193 | return null; 194 | } 195 | 196 | @Override 197 | public String getParameter(String name) { 198 | return parameters.get(name); 199 | } 200 | 201 | public void setParameter(String key, String value) { 202 | parameters.put(key,value); 203 | } 204 | 205 | @Override 206 | public Enumeration getParameterNames() { 207 | return null; 208 | } 209 | 210 | @Override 211 | public String[] getParameterValues(String name) { 212 | return new String[0]; 213 | } 214 | 215 | @Override 216 | public Map getParameterMap() { 217 | return null; 218 | } 219 | 220 | @Override 221 | public String getProtocol() { 222 | return null; 223 | } 224 | 225 | @Override 226 | public String getScheme() { 227 | return null; 228 | } 229 | 230 | @Override 231 | public String getServerName() { 232 | return null; 233 | } 234 | 235 | @Override 236 | public int getServerPort() { 237 | return 0; 238 | } 239 | 240 | @Override 241 | public BufferedReader getReader() throws IOException { 242 | return null; 243 | } 244 | 245 | @Override 246 | public String getRemoteAddr() { 247 | return null; 248 | } 249 | 250 | @Override 251 | public String getRemoteHost() { 252 | return null; 253 | } 254 | 255 | @Override 256 | public void setAttribute(String name, Object o) { 257 | 258 | } 259 | 260 | @Override 261 | public void removeAttribute(String name) { 262 | 263 | } 264 | 265 | @Override 266 | public Locale getLocale() { 267 | return null; 268 | } 269 | 270 | @Override 271 | public Enumeration getLocales() { 272 | return null; 273 | } 274 | 275 | @Override 276 | public boolean isSecure() { 277 | return false; 278 | } 279 | 280 | @Override 281 | public RequestDispatcher getRequestDispatcher(String path) { 282 | return null; 283 | } 284 | 285 | @Override 286 | public String getRealPath(String path) { 287 | return null; 288 | } 289 | 290 | @Override 291 | public int getRemotePort() { 292 | return 0; 293 | } 294 | 295 | @Override 296 | public String getLocalName() { 297 | return null; 298 | } 299 | 300 | @Override 301 | public String getLocalAddr() { 302 | return null; 303 | } 304 | 305 | @Override 306 | public int getLocalPort() { 307 | return 0; 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/MockHttpServletResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import javax.servlet.ServletOutputStream; 16 | import javax.servlet.http.Cookie; 17 | import javax.servlet.http.HttpServletResponse; 18 | import java.io.IOException; 19 | import java.io.PrintWriter; 20 | import java.util.Locale; 21 | 22 | /** 23 | * Created by mhardin on 2/6/14. 24 | */ 25 | public class MockHttpServletResponse implements HttpServletResponse { 26 | 27 | private int errorCode; 28 | private String errorMessage; 29 | private int statusCode; 30 | 31 | @Override 32 | public void addCookie(Cookie cookie) { 33 | 34 | } 35 | 36 | @Override 37 | public boolean containsHeader(String name) { 38 | return false; 39 | } 40 | 41 | @Override 42 | public String encodeURL(String url) { 43 | return null; 44 | } 45 | 46 | @Override 47 | public String encodeRedirectURL(String url) { 48 | return null; 49 | } 50 | 51 | @Override 52 | public String encodeUrl(String url) { 53 | return null; 54 | } 55 | 56 | @Override 57 | public String encodeRedirectUrl(String url) { 58 | return null; 59 | } 60 | 61 | @Override 62 | public void sendError(int sc, String msg) throws IOException { 63 | this.errorCode = sc; 64 | this.errorMessage = msg; 65 | } 66 | 67 | @Override 68 | public void sendError(int sc) throws IOException { 69 | this.errorCode = sc; 70 | } 71 | 72 | public int getErrorCode() { 73 | return errorCode; 74 | } 75 | 76 | public String getErrorMessage() { 77 | return errorMessage; 78 | } 79 | 80 | @Override 81 | public void sendRedirect(String location) throws IOException { 82 | 83 | } 84 | 85 | @Override 86 | public void setDateHeader(String name, long date) { 87 | 88 | } 89 | 90 | @Override 91 | public void addDateHeader(String name, long date) { 92 | 93 | } 94 | 95 | @Override 96 | public void setHeader(String name, String value) { 97 | 98 | } 99 | 100 | @Override 101 | public void addHeader(String name, String value) { 102 | 103 | } 104 | 105 | @Override 106 | public void setIntHeader(String name, int value) { 107 | 108 | } 109 | 110 | @Override 111 | public void addIntHeader(String name, int value) { 112 | 113 | } 114 | 115 | @Override 116 | public void setStatus(int sc) { 117 | this.statusCode = sc; 118 | } 119 | 120 | public int getStatusCode() { 121 | return this.statusCode; 122 | } 123 | 124 | @Override 125 | public void setStatus(int sc, String sm) { 126 | 127 | } 128 | 129 | @Override 130 | public String getCharacterEncoding() { 131 | return null; 132 | } 133 | 134 | @Override 135 | public String getContentType() { 136 | return null; 137 | } 138 | 139 | @Override 140 | public ServletOutputStream getOutputStream() throws IOException { 141 | return null; 142 | } 143 | 144 | @Override 145 | public PrintWriter getWriter() throws IOException { 146 | return null; 147 | } 148 | 149 | @Override 150 | public void setCharacterEncoding(String charset) { 151 | 152 | } 153 | 154 | @Override 155 | public void setContentLength(int len) { 156 | 157 | } 158 | 159 | @Override 160 | public void setContentType(String type) { 161 | 162 | } 163 | 164 | @Override 165 | public void setBufferSize(int size) { 166 | 167 | } 168 | 169 | @Override 170 | public int getBufferSize() { 171 | return 0; 172 | } 173 | 174 | @Override 175 | public void flushBuffer() throws IOException { 176 | 177 | } 178 | 179 | @Override 180 | public void resetBuffer() { 181 | 182 | } 183 | 184 | @Override 185 | public boolean isCommitted() { 186 | return false; 187 | } 188 | 189 | @Override 190 | public void reset() { 191 | 192 | } 193 | 194 | @Override 195 | public void setLocale(Locale loc) { 196 | 197 | } 198 | 199 | @Override 200 | public Locale getLocale() { 201 | return null; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/MockRemoteProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import org.json.JSONObject; 16 | import org.openqa.grid.common.RegistrationRequest; 17 | import org.openqa.grid.common.exception.GridException; 18 | import org.openqa.grid.internal.Registry; 19 | import org.openqa.grid.internal.RemoteProxy; 20 | import org.openqa.grid.internal.TestSession; 21 | import org.openqa.grid.internal.TestSlot; 22 | import org.openqa.grid.internal.utils.CapabilityMatcher; 23 | import org.openqa.grid.internal.utils.HtmlRenderer; 24 | import org.openqa.selenium.remote.internal.HttpClientFactory; 25 | 26 | import java.net.URL; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | /** 32 | * Created by mhardin on 2/6/14. 33 | */ 34 | public class MockRemoteProxy implements RemoteProxy { 35 | 36 | private List testSlots = new ArrayList();; 37 | private Map config; 38 | private CapabilityMatcher matcher; 39 | private int maxSessions; 40 | 41 | @Override 42 | public List getTestSlots() { 43 | return testSlots; 44 | } 45 | 46 | public void setTestSlots(List testSlots) { 47 | this.testSlots = testSlots; 48 | } 49 | 50 | public void setMultipleTestSlots(TestSlot testSlot, int count) { 51 | for(int i=0;i getConfig() { 87 | return config; 88 | } 89 | 90 | public void setConfig(Map config) { 91 | this.config = config; 92 | } 93 | 94 | @Override 95 | public RegistrationRequest getOriginalRegistrationRequest() { 96 | return null; 97 | } 98 | 99 | @Override 100 | public int getMaxNumberOfConcurrentTestSessions() { 101 | return maxSessions; 102 | } 103 | 104 | public void setMaxNumberOfConcurrentTestSessions(int sessions) { 105 | this.maxSessions = sessions; 106 | } 107 | 108 | @Override 109 | public URL getRemoteHost() { 110 | return null; 111 | } 112 | 113 | @Override 114 | public TestSession getNewSession(Map requestedCapability) { 115 | return null; 116 | } 117 | 118 | @Override 119 | public int getTotalUsed() { 120 | return 0; 121 | } 122 | 123 | @Override 124 | public HtmlRenderer getHtmlRender() { 125 | return null; 126 | } 127 | 128 | @Override 129 | public int getTimeOut() { 130 | return 0; 131 | } 132 | 133 | @Override 134 | public HttpClientFactory getHttpClientFactory() { 135 | return null; 136 | } 137 | 138 | @Override 139 | public JSONObject getStatus() throws GridException { 140 | return null; 141 | } 142 | 143 | @Override 144 | public boolean hasCapability(Map requestedCapability) { 145 | return false; 146 | } 147 | 148 | @Override 149 | public boolean isBusy() { 150 | return false; 151 | } 152 | 153 | @Override 154 | public float getResourceUsageInPercent() { 155 | return 0; 156 | } 157 | 158 | @Override 159 | public int compareTo(RemoteProxy remoteProxy) { 160 | return 0; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/MockRequestMatcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import org.openqa.grid.internal.ProxySet; 16 | 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | 20 | public class MockRequestMatcher implements RequestMatcher { 21 | 22 | private int threadsToReturn; 23 | private Map inProgressTests = new HashMap(); 24 | 25 | @Override 26 | public int getNumFreeThreadsForParameters(ProxySet proxySet, AutomationRunRequest request) { 27 | return threadsToReturn; 28 | } 29 | 30 | public void setThreadsToReturn(int threadsToReturn) { 31 | this.threadsToReturn = threadsToReturn; 32 | } 33 | 34 | @Override 35 | public int getNumInProgressTests(ProxySet proxySet, AutomationRunRequest request) { 36 | Integer returnValue = inProgressTests.get(request.getBrowser()); 37 | return (returnValue == null) ? 0 : returnValue; 38 | } 39 | 40 | public void setInProgressTests(String browser, int inProgressTests) { 41 | this.inProgressTests.put(browser,inProgressTests); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/MockVmManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa; 14 | 15 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 16 | import com.amazonaws.services.ec2.model.Instance; 17 | import com.amazonaws.services.ec2.model.Reservation; 18 | import com.rmn.qa.aws.VmManager; 19 | 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | public class MockVmManager implements VmManager { 24 | 25 | private boolean nodesLaunched = false; 26 | private int numberLaunched; 27 | private String browser; 28 | private boolean throwException = false; 29 | private boolean terminated = false; 30 | private List reservations; 31 | 32 | 33 | @Override 34 | public List launchNodes(String uuid, String os, String browser, String hubHostName, int nodeCount, int maxSessions) { 35 | if(throwException) { 36 | throw new RuntimeException("Can't start nodes"); 37 | } 38 | this.nodesLaunched = true; 39 | this.numberLaunched = nodeCount; 40 | this.browser = browser; 41 | Instance instance = new Instance(); 42 | instance.setInstanceId("instanceId"); 43 | List instances = new ArrayList(); 44 | instances.add(instance); 45 | return instances; 46 | } 47 | 48 | @Override 49 | public boolean terminateInstance(String instanceId) { 50 | terminated = true; 51 | return true; 52 | } 53 | 54 | @Override 55 | public List describeInstances(DescribeInstancesRequest describeInstancesRequest) { 56 | return reservations; 57 | } 58 | 59 | public boolean isNodesLaunched() { 60 | return nodesLaunched; 61 | } 62 | 63 | public String getBrowser() { 64 | return browser; 65 | } 66 | 67 | public int getNumberLaunched() { 68 | return numberLaunched; 69 | } 70 | 71 | public void setThrowException() { 72 | throwException = true; 73 | } 74 | 75 | public boolean isTerminated() { 76 | return terminated; 77 | } 78 | 79 | public void setReservations(List reservations) { 80 | this.reservations = reservations; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/aws/AwsTagReporterTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.aws; 14 | 15 | import com.amazonaws.services.ec2.model.DescribeInstancesResult; 16 | import com.amazonaws.services.ec2.model.Instance; 17 | import com.amazonaws.services.ec2.model.Reservation; 18 | import junit.framework.Assert; 19 | import org.junit.Test; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collection; 23 | import java.util.Properties; 24 | 25 | public class AwsTagReporterTest { 26 | 27 | @Test 28 | public void testTagsAssociated() { 29 | MockAmazonEc2Client client = new MockAmazonEc2Client(null); 30 | Collection instances = Arrays.asList(new Instance()); 31 | DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); 32 | Reservation reservation = new Reservation(); 33 | describeInstancesResult.setReservations(Arrays.asList(reservation)); 34 | reservation.setInstances(instances); 35 | client.setDescribeInstances(describeInstancesResult); 36 | Properties properties = new Properties(); 37 | properties.setProperty("tagAccounting","key,value"); 38 | properties.setProperty("function_tag","foo2"); 39 | properties.setProperty("product_tag","foo3"); 40 | AwsTagReporter reporter = new AwsTagReporter("testUuid",client,instances,properties); 41 | reporter.run(); 42 | } 43 | 44 | @Test 45 | public void testExceptionCaught() { 46 | MockAmazonEc2Client client = new MockAmazonEc2Client(null); 47 | Collection instances = Arrays.asList(new Instance()); 48 | DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); 49 | Reservation reservation = new Reservation(); 50 | describeInstancesResult.setReservations(Arrays.asList(reservation)); 51 | reservation.setInstances(instances); 52 | client.setDescribeInstances(describeInstancesResult); 53 | Properties properties = new Properties(); 54 | properties.setProperty("tagAccounting","key"); 55 | properties.setProperty("function_tag","foo2"); 56 | properties.setProperty("product_tag","foo3"); 57 | AwsTagReporter reporter = new AwsTagReporter("testUuid",client,instances,properties); 58 | reporter.run(); 59 | } 60 | 61 | @Test 62 | public void testClientThrowsErrors() { 63 | MockAmazonEc2Client client = new MockAmazonEc2Client(null); 64 | client.setDescribeInstancesToThrowError(); 65 | Collection instances = Arrays.asList(new Instance()); 66 | DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); 67 | Reservation reservation = new Reservation(); 68 | describeInstancesResult.setReservations(Arrays.asList(reservation)); 69 | reservation.setInstances(instances); 70 | client.setDescribeInstances(describeInstancesResult); 71 | Properties properties = new Properties(); 72 | properties.setProperty("accounting_tag","foo"); 73 | properties.setProperty("function_tag","foo2"); 74 | properties.setProperty("product_tag","foo3"); 75 | AwsTagReporter reporter = new AwsTagReporter("testUuid",client,instances,properties) { 76 | @Override 77 | void sleep() throws InterruptedException { 78 | // do nothing 79 | } 80 | }; 81 | reporter.run(); 82 | } 83 | 84 | @Test 85 | public void testSleepThrowsErrors() { 86 | MockAmazonEc2Client client = new MockAmazonEc2Client(null); 87 | client.setDescribeInstancesToThrowError(); 88 | Collection instances = Arrays.asList(new Instance()); 89 | DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); 90 | Reservation reservation = new Reservation(); 91 | describeInstancesResult.setReservations(Arrays.asList(reservation)); 92 | reservation.setInstances(instances); 93 | client.setDescribeInstances(describeInstancesResult); 94 | Properties properties = new Properties(); 95 | properties.setProperty("accounting_tag","foo"); 96 | properties.setProperty("function_tag","foo2"); 97 | properties.setProperty("product_tag","foo3"); 98 | AwsTagReporter reporter = new AwsTagReporter("testUuid",client,instances,properties) { 99 | @Override 100 | void sleep() throws InterruptedException { 101 | throw new InterruptedException(); 102 | } 103 | }; 104 | reporter.run(); 105 | } 106 | 107 | @Test() 108 | public void testThreadTimesOut() { 109 | MockAmazonEc2Client client = new MockAmazonEc2Client(null); 110 | Collection instances = Arrays.asList(new Instance()); 111 | DescribeInstancesResult describeInstancesResult = new DescribeInstancesResult(); 112 | Reservation reservation = new Reservation(); 113 | describeInstancesResult.setReservations(Arrays.asList(reservation)); 114 | // Make count mismatch 115 | reservation.setInstances(Arrays.asList(new Instance(),new Instance())); 116 | client.setDescribeInstances(describeInstancesResult); 117 | Properties properties = new Properties(); 118 | properties.setProperty("accounting_tag","foo"); 119 | properties.setProperty("function_tag","foo2"); 120 | properties.setProperty("product_tag","foo3"); 121 | AwsTagReporter reporter = new AwsTagReporter("testUuid",client,instances,properties); 122 | AwsTagReporter.TIMEOUT_IN_SECONDS = 1; 123 | try{ 124 | reporter.run(); 125 | } catch(RuntimeException e) { 126 | Assert.assertEquals("Error waiting for instances to exist to add tags",e.getMessage()); 127 | return; 128 | } 129 | Assert.fail("Exception should have been thrown since tags were never filed"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/aws/MockAmazonEc2Client.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.aws; 14 | 15 | import com.amazonaws.AmazonClientException; 16 | import com.amazonaws.AmazonServiceException; 17 | import com.amazonaws.auth.AWSCredentials; 18 | import com.amazonaws.services.ec2.AmazonEC2Client; 19 | import com.amazonaws.services.ec2.model.CreateTagsRequest; 20 | import com.amazonaws.services.ec2.model.DescribeInstancesRequest; 21 | import com.amazonaws.services.ec2.model.DescribeInstancesResult; 22 | import com.amazonaws.services.ec2.model.RunInstancesRequest; 23 | import com.amazonaws.services.ec2.model.RunInstancesResult; 24 | import com.amazonaws.services.ec2.model.TerminateInstancesRequest; 25 | import com.amazonaws.services.ec2.model.TerminateInstancesResult; 26 | 27 | public class MockAmazonEc2Client extends AmazonEC2Client { 28 | 29 | private DescribeInstancesResult describeInstancesResult; 30 | private RunInstancesResult runInstancesResult; 31 | private RunInstancesRequest runInstancesRequest; 32 | private boolean throwDescribeInstancesError = false; 33 | private AmazonClientException throwRunInstancesError; 34 | private boolean throwExceptionsInRunInstancesIndefinitely = false; 35 | private CreateTagsRequest tagsCreated; 36 | private TerminateInstancesResult terminateInstancesResult; 37 | private TerminateInstancesRequest terminateInstancesRequest; 38 | 39 | public MockAmazonEc2Client(AWSCredentials credentials) { 40 | super(credentials); 41 | } 42 | 43 | 44 | @Override 45 | public DescribeInstancesResult describeInstances(DescribeInstancesRequest describeInstancesRequest) throws AmazonClientException { 46 | if(throwDescribeInstancesError) { 47 | throwDescribeInstancesError = false; 48 | throw new AmazonClientException("testError"); 49 | } 50 | return describeInstancesResult; 51 | } 52 | 53 | public void setDescribeInstancesToThrowError() { 54 | throwDescribeInstancesError = true; 55 | } 56 | 57 | 58 | 59 | public void setDescribeInstances(DescribeInstancesResult resultToReturn) { 60 | this.describeInstancesResult = resultToReturn; 61 | } 62 | 63 | @Override 64 | public void createTags(CreateTagsRequest createTagsRequest) throws AmazonClientException { 65 | this.tagsCreated = createTagsRequest; 66 | } 67 | 68 | @Override 69 | public RunInstancesResult runInstances(RunInstancesRequest runInstancesRequest) throws AmazonServiceException, AmazonClientException { 70 | if(throwRunInstancesError != null) { 71 | AmazonClientException exceptionToThrow = throwRunInstancesError; 72 | if(!throwExceptionsInRunInstancesIndefinitely) { 73 | throwRunInstancesError = null; 74 | } 75 | throw exceptionToThrow; 76 | } 77 | this.runInstancesRequest = runInstancesRequest; 78 | return runInstancesResult; 79 | } 80 | 81 | public void setRunInstances(RunInstancesResult runInstancesResult) { 82 | this.runInstancesResult = runInstancesResult; 83 | } 84 | 85 | public RunInstancesRequest getRunInstancesRequest() { 86 | return runInstancesRequest; 87 | } 88 | 89 | @Override 90 | public TerminateInstancesResult terminateInstances(TerminateInstancesRequest terminateInstancesRequest) throws AmazonServiceException, AmazonClientException { 91 | this.terminateInstancesRequest = terminateInstancesRequest; 92 | return terminateInstancesResult; 93 | } 94 | 95 | public void setTerminateInstancesResult(TerminateInstancesResult terminateInstancesResult) { 96 | this.terminateInstancesResult = terminateInstancesResult; 97 | } 98 | 99 | public TerminateInstancesRequest getTerminateInstancesRequest() { 100 | return terminateInstancesRequest; 101 | } 102 | 103 | public void setThrowDescribeInstancesError(AmazonClientException t) { 104 | this.throwRunInstancesError = t; 105 | } 106 | 107 | public void setThrowExceptionsInRunInstancesIndefinitely() { 108 | throwExceptionsInRunInstancesIndefinitely = true; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/aws/MockManageVm.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.aws; 14 | 15 | import com.amazonaws.auth.BasicAWSCredentials; 16 | import com.amazonaws.services.ec2.AmazonEC2Client; 17 | 18 | import java.util.Properties; 19 | 20 | public class MockManageVm extends AwsVmManager { 21 | 22 | private String userData; 23 | 24 | public MockManageVm() { 25 | super(); 26 | } 27 | 28 | public MockManageVm(AmazonEC2Client client, Properties properties, String region) { 29 | super(client,properties,region); 30 | } 31 | 32 | public MockManageVm(AmazonEC2Client client, Properties properties, String region, BasicAWSCredentials credentials) { 33 | this(client, properties, region); 34 | this.credentials = credentials; 35 | } 36 | 37 | @Override 38 | String getUserData(String uuid, String hubHostName, String browser, String os, int maxSessions) { 39 | return userData; 40 | } 41 | 42 | public void setUserData(String userData) { 43 | this.userData = userData; 44 | } 45 | 46 | public void setCredentials(BasicAWSCredentials basicAWSCredentials) { 47 | this.credentials = basicAWSCredentials; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/servlet/MockAutomationTestRunServlet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.servlet; 14 | 15 | import com.rmn.qa.RequestMatcher; 16 | import com.rmn.qa.aws.VmManager; 17 | import org.openqa.grid.internal.ProxySet; 18 | import org.openqa.grid.internal.Registry; 19 | 20 | public class MockAutomationTestRunServlet extends AutomationTestRunServlet { 21 | 22 | private ProxySet proxySet; 23 | 24 | public MockAutomationTestRunServlet(Registry registry, boolean initThreads, VmManager ec2,RequestMatcher requestMatcher){ 25 | super(registry,initThreads,ec2,requestMatcher); 26 | } 27 | 28 | @Override 29 | protected ProxySet getProxySet() { 30 | return proxySet; 31 | } 32 | 33 | public void setProxySet(ProxySet proxySet) { 34 | this.proxySet = proxySet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/AutomationHubCleanupTaskTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.AutomationUtils; 16 | import com.rmn.qa.MockVmManager; 17 | import com.rmn.qa.MockRemoteProxy; 18 | import com.rmn.qa.aws.AwsVmManager; 19 | import junit.framework.Assert; 20 | import org.junit.After; 21 | import org.junit.Test; 22 | import org.openqa.grid.internal.ProxySet; 23 | 24 | import java.util.Calendar; 25 | import java.util.Date; 26 | 27 | public class AutomationHubCleanupTaskTest { 28 | 29 | @After 30 | public void afterTest() { 31 | // Make sure we clear the statically set start and end dates for the hub 32 | MockAutomationHubCleanupTask.clearDates(); 33 | AutomationHubCleanupTask.errorEncountered = false; 34 | } 35 | 36 | @Test 37 | // Tests that the hardcoded name of the task is correct 38 | public void testTaskName() { 39 | AutomationHubCleanupTask task = new AutomationHubCleanupTask(null,null,null); 40 | Assert.assertEquals("Name should be the same",AutomationHubCleanupTask.NAME, task.getDescription() ); 41 | } 42 | 43 | @Test 44 | // Tests that the hub terminates after the correct amount of time 45 | public void testHubTerminated() { 46 | MockVmManager ec2 = new MockVmManager(); 47 | MockAutomationHubCleanupTask task = new MockAutomationHubCleanupTask(null,ec2,"dummyId"); 48 | ProxySet proxySet = new ProxySet(false); 49 | task.setProxySet(proxySet); 50 | String createdDate = AwsVmManager.NODE_DATE_FORMAT.format(AutomationUtils.modifyDate(new Date(), -56, Calendar.MINUTE)); 51 | task.setCreatedDate(createdDate); 52 | 53 | task.run(); 54 | Assert.assertTrue("Hub should be terminated as it was empty",ec2.isTerminated()); 55 | } 56 | 57 | @Test 58 | // Tests that the cleanup task can handle an incorrectly formatted date (should shut down) 59 | public void testHubTerminatedBadCreatedDate() { 60 | MockVmManager ec2 = new MockVmManager(); 61 | MockAutomationHubCleanupTask task = new MockAutomationHubCleanupTask(null,ec2,"dummyId"); 62 | ProxySet proxySet = new ProxySet(false); 63 | task.setProxySet(proxySet); 64 | // Set the date to a format that will not be parseable 65 | String createdDate = "Mon Feb 17 20:56:48 UTC 2014"; 66 | task.setCreatedDate(createdDate); 67 | 68 | task.run(); 69 | Assert.assertTrue("Error should have been encountered",AutomationHubCleanupTask.errorEncountered); 70 | Assert.assertTrue("Hub should be terminated even though the date format parsing failed",ec2.isTerminated()); 71 | } 72 | 73 | @Test 74 | // Tests that the error field defaults to false 75 | public void testHubErrorDefaultsToFalse() { 76 | Assert.assertFalse("Error not have been configured by default",AutomationHubCleanupTask.errorEncountered); 77 | } 78 | 79 | 80 | @Test 81 | // Tests that if the hub still has nodes registered to it, it will not terminate 82 | public void testHubNotTerminatedExistingNodes() { 83 | MockVmManager ec2 = new MockVmManager(); 84 | MockAutomationHubCleanupTask task = new MockAutomationHubCleanupTask(null,ec2,"dummyId"); 85 | ProxySet proxySet = new ProxySet(false); 86 | proxySet.add(new MockRemoteProxy()); 87 | task.setProxySet(proxySet); 88 | String createdDate = AwsVmManager.NODE_DATE_FORMAT.format(AutomationUtils.modifyDate(new Date(),-56, Calendar.MINUTE)); 89 | task.setCreatedDate(createdDate); 90 | 91 | task.run(); 92 | Assert.assertFalse("Hub should not be terminated as nodes are still registered", ec2.isTerminated()); 93 | } 94 | 95 | @Test 96 | // Tests that if the hub is in the next billing cycle, its end date will reset 97 | // instead of terminating 98 | public void testHubNotTerminatedNextBillingCycle() { 99 | MockVmManager ec2 = new MockVmManager(); 100 | MockAutomationHubCleanupTask task = new MockAutomationHubCleanupTask(null,ec2,"dummyId"); 101 | ProxySet proxySet = new ProxySet(false); 102 | proxySet.add(new MockRemoteProxy()); 103 | task.setProxySet(proxySet); 104 | Date newStartDate = AutomationUtils.modifyDate(new Date(),-61, Calendar.MINUTE); 105 | String createdDate = AwsVmManager.NODE_DATE_FORMAT.format(newStartDate); 106 | task.setCreatedDate(createdDate); 107 | 108 | task.run(); 109 | Assert.assertFalse("Hub should not be terminated as its in the next billing cycle",ec2.isTerminated()); 110 | Assert.assertTrue("End date should have been increased", AutomationUtils.modifyDate(newStartDate, 116, Calendar.MINUTE).after(task.getEndDate())); // 60 minutes + 55 111 | Assert.assertTrue("End date should have been increased",AutomationUtils.modifyDate(newStartDate,114,Calendar.MINUTE).before(task.getEndDate())); // 60 minutes + 55 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/AutomationNodeRegistryTaskTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.*; 16 | import com.rmn.qa.aws.AwsVmManager; 17 | import junit.framework.Assert; 18 | import org.junit.After; 19 | import org.junit.Test; 20 | import org.openqa.grid.internal.ProxySet; 21 | 22 | import java.text.DateFormat; 23 | import java.text.SimpleDateFormat; 24 | import java.util.Date; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | /** 29 | * Created by mhardin on 4/24/14. 30 | */ 31 | public class AutomationNodeRegistryTaskTest { 32 | 33 | @After 34 | public void tearDown() { 35 | AutomationContext.refreshContext(); 36 | } 37 | 38 | @Test 39 | // Tests that the hardcoded name of the task is correct 40 | public void testTaskName() { 41 | AutomationNodeRegistryTask task = new AutomationNodeRegistryTask(null); 42 | Assert.assertEquals("Name should be the same", AutomationNodeRegistryTask.NAME, task.getDescription()); 43 | } 44 | 45 | @Test 46 | // Test that a node not in the context gets picked up and registered by the task 47 | public void testRegisterNewNode() { 48 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 49 | ProxySet proxySet = new ProxySet(false); 50 | MockRemoteProxy proxy = new MockRemoteProxy(); 51 | proxySet.add(proxy); 52 | Map config = new HashMap(); 53 | String instanceId = "instanceId"; 54 | String uuid="testUuid"; 55 | int threadCount = 10; 56 | String browser = "firefox"; 57 | String os = "linux"; 58 | config.put(AutomationConstants.INSTANCE_ID,instanceId); 59 | config.put(AutomationConstants.UUID,uuid); 60 | config.put(AutomationConstants.CONFIG_MAX_SESSION, threadCount); 61 | config.put(AutomationConstants.CONFIG_BROWSER, browser); 62 | config.put(AutomationConstants.CONFIG_OS, os); 63 | config.put(AutomationConstants.CONFIG_CREATED_DATE, AwsVmManager.NODE_DATE_FORMAT.format(new Date())); 64 | proxy.setConfig(config); 65 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 66 | task.setProxySet(proxySet); 67 | Assert.assertNull("Node should not be registered before the task runs",AutomationContext.getContext().getNode(instanceId)); 68 | task.run(); 69 | AutomationDynamicNode existingNode = AutomationContext.getContext().getNode(instanceId); 70 | Assert.assertNotNull("Node should exist after the task has run",existingNode); 71 | Assert.assertEquals("UUID should match", uuid,existingNode.getUuid()); 72 | Assert.assertEquals("Browser should match", browser,existingNode.getBrowser()); 73 | Assert.assertEquals("Thread count should match", threadCount,existingNode.getNodeCapacity()); 74 | Assert.assertEquals("OS should match", os, existingNode.getOs()); 75 | } 76 | 77 | @Test 78 | // Test if a node already exists in the context that the task does not override that node 79 | public void testNodeAlreadyExists() { 80 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 81 | ProxySet proxySet = new ProxySet(false); 82 | MockRemoteProxy proxy = new MockRemoteProxy(); 83 | proxySet.add(proxy); 84 | Map config = new HashMap(); 85 | String instanceId = "instanceId"; 86 | String uuid="testUuid"; 87 | int threadCount = 10; 88 | String browser = "firefox"; 89 | String os = "linux"; 90 | config.put(AutomationConstants.INSTANCE_ID,instanceId); 91 | config.put(AutomationConstants.UUID,"fake"); 92 | config.put(AutomationConstants.CONFIG_MAX_SESSION, 1); 93 | config.put(AutomationConstants.CONFIG_BROWSER, "fake"); 94 | config.put(AutomationConstants.CONFIG_OS, "fake"); 95 | config.put(AutomationConstants.CONFIG_CREATED_DATE, AwsVmManager.NODE_DATE_FORMAT.format(new Date())); 96 | proxy.setConfig(config); 97 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 98 | task.setProxySet(proxySet); 99 | AutomationDynamicNode existingNode = new AutomationDynamicNode(uuid,instanceId,browser,os,new Date(),threadCount); 100 | AutomationContext.getContext().addNode(existingNode); 101 | Assert.assertNotNull("Node should be registered before the task runs",AutomationContext.getContext().getNode(instanceId)); 102 | task.run(); 103 | AutomationDynamicNode newNode = AutomationContext.getContext().getNode(instanceId); 104 | Assert.assertNotNull("Node should still exist after the task has run",newNode); 105 | Assert.assertEquals("UUID should match the previously existing node", uuid, newNode.getUuid()); 106 | Assert.assertEquals("Browser should match the previously existing node", browser,newNode.getBrowser()); 107 | Assert.assertEquals("Thread count should match the previously existing node", threadCount, newNode.getNodeCapacity()); 108 | Assert.assertEquals("OS should match the previously existing node", os, newNode.getOs()); 109 | } 110 | 111 | @Test 112 | // Test that a node without an instance id does not get registered 113 | public void testRegisterNodeWithoutInstanceId() { 114 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 115 | ProxySet proxySet = new ProxySet(false); 116 | MockRemoteProxy proxy = new MockRemoteProxy(); 117 | proxySet.add(proxy); 118 | Map config = new HashMap(); 119 | String uuid="testUuid"; 120 | int threadCount = 10; 121 | String browser = "firefox"; 122 | String os = "linux"; 123 | config.put(AutomationConstants.UUID,uuid); 124 | config.put(AutomationConstants.CONFIG_MAX_SESSION, threadCount); 125 | config.put(AutomationConstants.CONFIG_BROWSER, browser); 126 | config.put(AutomationConstants.CONFIG_OS, os); 127 | config.put(AutomationConstants.CONFIG_CREATED_DATE, AwsVmManager.NODE_DATE_FORMAT.format(new Date())); 128 | proxy.setConfig(config); 129 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 130 | task.setProxySet(proxySet); 131 | Assert.assertEquals("Node should not be registered before the task runs",0,AutomationContext.getContext().getNodes().size()); 132 | task.run(); 133 | Assert.assertEquals("Node should still not be registered after the task runs",0,AutomationContext.getContext().getNodes().size()); 134 | } 135 | 136 | @Test 137 | // Test that an empty proxy set does not result in any added nodes after the register task runs 138 | public void testEmptyProxySet() { 139 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 140 | ProxySet proxySet = new ProxySet(false); 141 | task.setProxySet(proxySet); 142 | Assert.assertEquals("Node should not be registered before the task runs",0,AutomationContext.getContext().getNodes().size()); 143 | task.run(); 144 | Assert.assertEquals("Node should still not be registered after the task runs",0,AutomationContext.getContext().getNodes().size()); 145 | } 146 | 147 | @Test 148 | // Test that a null proxy set does not result in any added nodes 149 | public void testNullEmptyProxySet() { 150 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 151 | ProxySet proxySet = new ProxySet(false); 152 | task.setProxySet(proxySet); 153 | Assert.assertEquals("Node should not be registered before the task runs",0,AutomationContext.getContext().getNodes().size()); 154 | task.run(); 155 | Assert.assertEquals("Node should still not be registered after the task runs",0,AutomationContext.getContext().getNodes().size()); 156 | } 157 | 158 | @Test 159 | // Test that a node with a bad date does not register successfully after the task runs 160 | public void testBadDateFormat() { 161 | MockAutomationNodeRegistryTask task = new MockAutomationNodeRegistryTask(null); 162 | ProxySet proxySet = new ProxySet(false); 163 | MockRemoteProxy proxy = new MockRemoteProxy(); 164 | proxySet.add(proxy); 165 | Map config = new HashMap<>(); 166 | String instanceId = "instanceId"; 167 | String uuid="testUuid"; 168 | int threadCount = 10; 169 | String browser = "firefox"; 170 | String os = "linux"; 171 | config.put(AutomationConstants.INSTANCE_ID,instanceId); 172 | config.put(AutomationConstants.UUID,uuid); 173 | config.put(AutomationConstants.CONFIG_MAX_SESSION, threadCount); 174 | config.put(AutomationConstants.CONFIG_BROWSER, browser); 175 | config.put(AutomationConstants.CONFIG_OS, os); 176 | 177 | DateFormat badDateFormat = new SimpleDateFormat("MM HH:mm:ss"); 178 | config.put(AutomationConstants.CONFIG_CREATED_DATE, badDateFormat.format(new Date())); 179 | proxy.setConfig(config); 180 | proxy.setCapabilityMatcher(new AutomationCapabilityMatcher()); 181 | task.setProxySet(proxySet); 182 | Assert.assertNull("Node should not be registered before the task runs",AutomationContext.getContext().getNode(instanceId)); 183 | task.run(); 184 | Assert.assertNull("Node should still not be registered after the task runs",AutomationContext.getContext().getNode(instanceId)); 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/AutomationReaperTaskTest.java: -------------------------------------------------------------------------------- 1 | package com.rmn.qa.task; 2 | 3 | import com.amazonaws.services.ec2.model.Instance; 4 | import com.amazonaws.services.ec2.model.Reservation; 5 | import com.rmn.qa.AutomationContext; 6 | import com.rmn.qa.AutomationDynamicNode; 7 | import com.rmn.qa.AutomationUtils; 8 | import com.rmn.qa.MockVmManager; 9 | import junit.framework.Assert; 10 | import org.junit.Test; 11 | 12 | import java.util.Arrays; 13 | import java.util.Calendar; 14 | import java.util.Date; 15 | 16 | public class AutomationReaperTaskTest { 17 | 18 | @Test 19 | public void testShutdown() { 20 | MockVmManager ec2 = new MockVmManager(); 21 | Reservation reservation = new Reservation(); 22 | Instance instance = new Instance(); 23 | String instanceId = "foo"; 24 | instance.setInstanceId(instanceId); 25 | instance.setLaunchTime(AutomationUtils.modifyDate(new Date(),-5,Calendar.HOUR)); 26 | reservation.setInstances(Arrays.asList(instance)); 27 | ec2.setReservations(Arrays.asList(reservation)); 28 | AutomationReaperTask task = new AutomationReaperTask(null,ec2); 29 | task.run(); 30 | Assert.assertTrue("Node should be terminated as it was empty", ec2.isTerminated()); 31 | } 32 | 33 | @Test 34 | // Tests that a node that is not old enough is not terminated 35 | public void testNoShutdownTooRecent() { 36 | MockVmManager ec2 = new MockVmManager(); 37 | Reservation reservation = new Reservation(); 38 | Instance instance = new Instance(); 39 | String instanceId = "foo"; 40 | instance.setInstanceId(instanceId); 41 | instance.setLaunchTime(AutomationUtils.modifyDate(new Date(),-15,Calendar.MINUTE)); 42 | reservation.setInstances(Arrays.asList(instance)); 43 | ec2.setReservations(Arrays.asList(reservation)); 44 | AutomationReaperTask task = new AutomationReaperTask(null,ec2); 45 | task.run(); 46 | Assert.assertFalse("Node should NOT be terminated as it was not old", ec2.isTerminated()); 47 | } 48 | 49 | @Test 50 | // Tests that a node that is being tracked internally is not shut down 51 | public void testNoShutdownNodeTracked() { 52 | MockVmManager ec2 = new MockVmManager(); 53 | Reservation reservation = new Reservation(); 54 | Instance instance = new Instance(); 55 | String instanceId = "foo"; 56 | AutomationContext.getContext().addNode(new AutomationDynamicNode("faky",instanceId,null,null,new Date(),1)); 57 | instance.setInstanceId(instanceId); 58 | instance.setLaunchTime(AutomationUtils.modifyDate(new Date(),-5,Calendar.HOUR)); 59 | reservation.setInstances(Arrays.asList(instance)); 60 | ec2.setReservations(Arrays.asList(reservation)); 61 | AutomationReaperTask task = new AutomationReaperTask(null,ec2); 62 | task.run(); 63 | Assert.assertFalse("Node should NOT be terminated as it was tracked internally", ec2.isTerminated()); 64 | } 65 | 66 | @Test 67 | // Tests that the hardcoded name of the task is correct 68 | public void testTaskName() { 69 | AutomationReaperTask task = new AutomationReaperTask(null,null); 70 | Assert.assertEquals("Name should be the same",AutomationReaperTask.NAME, task.getDescription() ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/AutomationRunCleanupTaskTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.*; 16 | import junit.framework.Assert; 17 | import org.junit.Test; 18 | import org.openqa.grid.internal.ProxySet; 19 | 20 | import java.util.Calendar; 21 | import java.util.Date; 22 | 23 | /** 24 | * Created by mhardin on 5/1/14. 25 | */ 26 | public class AutomationRunCleanupTaskTest { 27 | 28 | @Test 29 | // Tests that an old run not in progress is no longer registered 30 | public void testCleanup() { 31 | AutomationRunRequest oldRequest = new AutomationRunRequest("uuid",10,"firefox","10","linux", AutomationUtils.modifyDate(new Date(), -1, Calendar.HOUR)); 32 | AutomationRunContext context = AutomationContext.getContext(); 33 | context.addRun(oldRequest); 34 | 35 | Assert.assertTrue("Run should exist", context.hasRun(oldRequest.getUuid())); 36 | ProxySet proxySet = new ProxySet(false); 37 | proxySet.add(new MockRemoteProxy()); 38 | context.cleanUpRunRequests(proxySet); 39 | MockAutomationRunCleanupTask task = new MockAutomationRunCleanupTask(null); 40 | task.setProxySet(proxySet); 41 | task.run(); 42 | Assert.assertFalse("Run request should no longer exist as it should have been removed",context.hasRun(oldRequest.getUuid())); 43 | } 44 | 45 | @Test 46 | // Tests that the task hard coded name matches the task name 47 | public void testTaskName() { 48 | AutomationRunCleanupTask task = new AutomationRunCleanupTask(null); 49 | Assert.assertEquals("Name should be the same",AutomationRunCleanupTask.NAME, task.getDescription() ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/MockAutomationHubCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.RegistryRetriever; 16 | import com.rmn.qa.aws.VmManager; 17 | import org.openqa.grid.internal.ProxySet; 18 | 19 | import java.util.Date; 20 | 21 | // We're extending the task so we can override the created date behavior 22 | public class MockAutomationHubCleanupTask extends AutomationHubCleanupTask { 23 | 24 | private Object createdDate; 25 | private ProxySet proxySet; 26 | 27 | public MockAutomationHubCleanupTask(RegistryRetriever retrieveContext, VmManager ec2, String instanceId) { 28 | super(retrieveContext, ec2, instanceId); 29 | } 30 | 31 | @Override 32 | protected Object getCreatedDate() { 33 | return createdDate; //To change body of overridden methods use File | Settings | File Templates. 34 | } 35 | 36 | @Override 37 | protected ProxySet getProxySet() { 38 | return proxySet; 39 | } 40 | 41 | static Date getEndDate() { 42 | return AutomationHubCleanupTask.endDate; 43 | } 44 | 45 | /** 46 | * Clears the start/end date 47 | */ 48 | static void clearDates() { 49 | AutomationHubCleanupTask.createdDate = null; 50 | AutomationHubCleanupTask.endDate = null; 51 | } 52 | 53 | public void setCreatedDate(Object createdDate) { 54 | this.createdDate = createdDate; 55 | } 56 | 57 | public void setProxySet(ProxySet proxySet) { 58 | this.proxySet = proxySet; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/MockAutomationNodeCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.RequestMatcher; 16 | import com.rmn.qa.RegistryRetriever; 17 | import com.rmn.qa.aws.VmManager; 18 | import org.openqa.grid.internal.ProxySet; 19 | 20 | public class MockAutomationNodeCleanupTask extends AutomationNodeCleanupTask { 21 | 22 | private ProxySet proxySet; 23 | 24 | public MockAutomationNodeCleanupTask(RegistryRetriever retrieveContext, VmManager ec2, RequestMatcher requestMatcher) { 25 | super(retrieveContext, ec2, requestMatcher); 26 | } 27 | 28 | @Override 29 | protected ProxySet getProxySet() { 30 | return proxySet; 31 | } 32 | 33 | public void setProxySet(ProxySet proxySet) { 34 | this.proxySet = proxySet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/MockAutomationNodeRegistryTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.RegistryRetriever; 16 | import org.openqa.grid.internal.ProxySet; 17 | 18 | /** 19 | * Created by mhardin on 4/24/14. 20 | */ 21 | public class MockAutomationNodeRegistryTask extends AutomationNodeRegistryTask { 22 | 23 | private ProxySet proxySet; 24 | 25 | public MockAutomationNodeRegistryTask(RegistryRetriever retrieveContext) { 26 | super(retrieveContext); 27 | } 28 | 29 | public void setProxySet(ProxySet proxySet) { 30 | this.proxySet = proxySet; 31 | } 32 | 33 | @Override 34 | public ProxySet getProxySet() { 35 | return proxySet; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/rmn/qa/task/MockAutomationRunCleanupTask.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 RetailMeNot, Inc. 3 | * This program is free software: you can redistribute it and/or modify 4 | * it under the terms of the GNU General Public License as published by 5 | * the Free Software Foundation, either version 3 of the License, or 6 | * (at your option) any later version. 7 | * This program is distributed in the hope that it will be useful, 8 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 9 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 | * GNU General Public License for more details. 11 | */ 12 | 13 | package com.rmn.qa.task; 14 | 15 | import com.rmn.qa.RegistryRetriever; 16 | import org.openqa.grid.internal.ProxySet; 17 | 18 | /** 19 | * Created by mhardin on 5/1/14. 20 | */ 21 | public class MockAutomationRunCleanupTask extends AutomationRunCleanupTask { 22 | 23 | private ProxySet proxySet; 24 | 25 | public MockAutomationRunCleanupTask(RegistryRetriever retrieveContext) { 26 | super(retrieveContext); 27 | } 28 | 29 | public ProxySet getProxySet() { 30 | return proxySet; 31 | } 32 | 33 | public void setProxySet(ProxySet proxySet) { 34 | this.proxySet = proxySet; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /startGrid: -------------------------------------------------------------------------------- 1 | # check required params 2 | [ -z ${AWS_ACCESS_KEY} ] && echo "AWS_ACCESS_KEY MUST BE SET!" && exit 1 3 | [ -z ${AWS_SECRET_KEY} ] && echo "AWS_SECRET_KEY MUST BE SET!" && exit 1 4 | 5 | # set defaults 6 | [ -z ${IP_ADDRESS} ] && IP_ADDRESS=10.10.10.10 7 | 8 | java -DawsAccessKey="${AWS_ACCESS_KEY}" \ 9 | -DawsSecretKey="${AWS_SECRET_KEY}" \ 10 | -DipAddress="${IP_ADDRESS}" \ 11 | -cp /target/automation-grid.jar org.openqa.grid.selenium.GridLauncher \ 12 | -role hub \ 13 | -servlets "com.rmn.qa.servlet.AutomationTestRunServlet","com.rmn.qa.servlet.StatusServlet" 14 | 15 | 16 | 17 | #-DPOOL_MAX=1024 18 | #-DawsAccessKey=foo 19 | #-DawsSecretKey=bar 20 | #-DtotalNodeCount=300 21 | #-DipAddress="10.10.10.10" 22 | #-hubConfig src/main/resources/hub.static.json 23 | #-log grid.log 24 | #-jettyMaxThreads 1024 25 | 26 | 27 | --------------------------------------------------------------------------------