Skip to content

Commit a074dd4

Browse files
authored
Adopt active local grid if found; upgrade to Selenium 4.32.0
1 parent 56ff845 commit a074dd4

26 files changed

+857
-234
lines changed

docs/ConfiguringProjectSettings.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,9 +93,7 @@ The **`GRID_PLUGINS`** setting specifies a semicolon-delimited list of fully-qua
9393

9494
* `getSeleniumGrid()` - Get an object that represents the active Selenium Grid. If indicated, this method wil launch a local Grid instance.
9595
* `getHubUrl()` - Get the URL for the configured Selenium Grid hub host. In local Grid configurations, this value will be populated when the Grid is launched.
96-
* `shutdownGrid(boolean localOnly)` - Shut down the active Selenium Grid. To enable shutdown of remote Grid instances, attached Grid nodes must install the **`LifecycleServlet`**:
97-
98-
> -servlets org.openqa.grid.web.servlet.LifecycleServlet
96+
* `shutdownGrid()` - Shut down the active Selenium Grid.
9997

10098
#### Grid Configuration for Selenium Foundation unit tests
10199

selenium4Deps.gradle

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ dependencies {
3232
api 'org.seleniumhq.selenium:selenium-firefox-driver:4.32.0'
3333
api 'org.seleniumhq.selenium:selenium-opera-driver:4.4.0'
3434
api 'org.seleniumhq.selenium:selenium-safari-driver:4.32.0'
35-
api 'com.nordstrom.ui-tools:htmlunit-remote:4.30.1'
36-
api 'org.seleniumhq.selenium:htmlunit3-driver:4.30.0'
37-
api 'org.htmlunit:htmlunit:4.11.1'
35+
api 'com.nordstrom.ui-tools:htmlunit-remote:4.32.0'
36+
api 'org.seleniumhq.selenium:htmlunit3-driver:4.32.0'
37+
api 'org.htmlunit:htmlunit:4.12.0'
3838
api 'com.codeborne:phantomjsdriver:1.5.0'
3939
api 'org.apache.httpcomponents:httpclient:4.5.14'
4040
api 'org.eclipse.jetty:jetty-servlet:9.4.57.v20241219'

src/main/java/com/nordstrom/automation/selenium/AbstractSeleniumConfig.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434

3535
import com.nordstrom.automation.selenium.core.FoundationSlotMatcher;
3636
import com.nordstrom.automation.selenium.core.GridUtility;
37+
import com.nordstrom.automation.selenium.core.LocalSeleniumGrid.LocalGridServer;
3738
import com.nordstrom.automation.selenium.core.SeleniumGrid;
3839
import com.nordstrom.automation.selenium.servlet.ExamplePageLauncher;
3940
import com.nordstrom.automation.selenium.servlet.ExamplePageServlet;
@@ -42,6 +43,7 @@
4243
import com.nordstrom.automation.selenium.servlet.ExamplePageServlet.FrameC_Servlet;
4344
import com.nordstrom.automation.selenium.servlet.ExamplePageServlet.FrameD_Servlet;
4445
import com.nordstrom.automation.selenium.support.SearchContextWait;
46+
import com.nordstrom.automation.selenium.utility.HostUtils;
4547
import com.nordstrom.automation.settings.SettingsCore;
4648
import com.nordstrom.common.base.UncheckedThrow;
4749
import com.nordstrom.common.file.PathUtils;
@@ -619,18 +621,29 @@ public String resolveString(String propertyName) {
619621

620622
/**
621623
* Get the URL for the configured Selenium Grid hub host.
624+
* <p>
625+
* <b>NOTE</b>: If hub host is unspecified, a local address is synthesized from hub port.
626+
* If hub port is also unspecified, this method returns {@code null}.
622627
*
623-
* @return {@link URL} for hub host; {@code null} if hub host is unspecified
628+
* @return {@link URL} for hub host; {@code null} if neither host nor port is unspecified
624629
*/
625630
public synchronized URL getHubUrl() {
626631
if (hubUrl == null) {
627632
String hostStr = getString(SeleniumSettings.HUB_HOST.key());
628633
if (hostStr != null) {
629634
try {
630635
hubUrl = URI.create(hostStr).toURL();
636+
LOGGER.debug("Specified hub URL: {}", hubUrl);
631637
} catch (MalformedURLException e) {
632638
throw UncheckedThrow.throwUnchecked(e);
633639
}
640+
} else {
641+
Integer hubPort = getInteger(SeleniumSettings.HUB_PORT.key(), -1);
642+
if (hubPort != -1) {
643+
String localHost = HostUtils.getLocalHost();
644+
hubUrl = LocalGridServer.getServerUrl(localHost, hubPort);
645+
LOGGER.debug("Synthesized hub URL: {}", hubUrl);
646+
}
634647
}
635648
}
636649
return hubUrl;
@@ -657,15 +670,16 @@ public SeleniumGrid getSeleniumGrid() {
657670
/**
658671
* Shutdown the active Selenium Grid.
659672
*
660-
* @param localOnly {@code true} to target only local Grid servers
661673
* @return {@code false} if non-local Grid server encountered; otherwise {@code true}
662674
* @throws InterruptedException if this thread was interrupted
663675
*/
664-
public boolean shutdownGrid(final boolean localOnly) throws InterruptedException {
676+
public boolean shutdownGrid() throws InterruptedException {
665677
boolean result = true;
666678
synchronized(SeleniumGrid.class) {
667-
if (seleniumGrid != null) {
668-
result = seleniumGrid.shutdown(localOnly);
679+
getSeleniumGrid(); // ensure local grid mappings are initialized
680+
// if grid hub is active
681+
if (seleniumGrid.getHubServer().isActive()) {
682+
result = seleniumGrid.shutdown();
669683
if (result) {
670684
ExamplePageLauncher.getLauncher().shutdown();
671685
seleniumGrid = null;

src/main/java/com/nordstrom/automation/selenium/core/DriverManager.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public static void onFinish() {
146146
SeleniumConfig config = SeleniumConfig.getConfig();
147147
if (config.getBoolean(SeleniumSettings.SHUTDOWN_GRID.key())) {
148148
try {
149-
config.shutdownGrid(true);
149+
config.shutdownGrid();
150150
} catch (InterruptedException e) {
151151
Thread.currentThread().interrupt();
152152
}
@@ -178,7 +178,6 @@ public static WebDriver injectDriver(TestBase instance, final Method method) {
178178
* @param driver driver object in which to configure timeout intervals
179179
* @param config configuration object that specifies timeout intervals
180180
*/
181-
@SuppressWarnings("deprecation")
182181
public static void setDriverTimeouts(final WebDriver driver, final SeleniumConfig config) {
183182
Timeouts timeouts = driver.manage().timeouts();
184183
timeouts.implicitlyWait(WaitType.IMPLIED.getInterval(config), TimeUnit.SECONDS);

src/main/java/com/nordstrom/automation/selenium/core/GridUtility.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
import org.openqa.selenium.Capabilities;
3939
import org.openqa.selenium.MutableCapabilities;
4040
import org.openqa.selenium.WebDriver;
41-
import org.openqa.selenium.net.NetworkUtils;
4241
import org.openqa.selenium.remote.RemoteWebDriver;
4342
import org.slf4j.Logger;
4443
import org.slf4j.LoggerFactory;
@@ -57,7 +56,6 @@
5756
public final class GridUtility {
5857

5958
private static final Logger LOGGER = LoggerFactory.getLogger(GridUtility.class);
60-
private static final NetworkUtils IDENTITY = new NetworkUtils();
6159

6260
/**
6361
* Private constructor to prevent instantiation.
@@ -152,6 +150,7 @@ public static WebDriver getDriver(URL remoteAddress, Capabilities desiredCapabil
152150
localGrid.activate();
153151
} catch (InterruptedException e) {
154152
Thread.currentThread().interrupt();
153+
throw new IllegalStateException("Interrupted activating local grid instance", e);
155154
} catch (IOException | TimeoutException e) {
156155
throw new IllegalStateException("Failed activating local grid instance", e);
157156
}
@@ -257,6 +256,8 @@ public static Map<String, Object> getNordOptions(Capabilities capabilities) {
257256
* @return 'true' if server is local host; otherwise 'false'
258257
*/
259258
public static boolean isLocalHost(URL host) {
259+
Objects.requireNonNull(host, "[host] must be non-null");
260+
260261
try {
261262
InetAddress addr = InetAddress.getByName(host.getHost());
262263
return (isThisMyIpAddress(addr));
@@ -304,15 +305,6 @@ public static HttpHost extractHost(URL url) {
304305
return null;
305306
}
306307

307-
/**
308-
* Get Internet protocol (IP) address for the machine we're running on.
309-
*
310-
* @return IP address for the machine we're running on (a.k.a. - 'localhost')
311-
*/
312-
public static String getLocalHost() {
313-
return IDENTITY.getIp4NonLoopbackAddressOfThisMachine().getHostAddress();
314-
}
315-
316308
/**
317309
* Get next configured output path for Grid server of specified role.
318310
*

src/main/java/com/nordstrom/automation/selenium/core/SeleniumGrid.java

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,9 @@ public SeleniumGrid(SeleniumConfig config, URL hubUrl) throws IOException {
9090
hubServer = new GridServer(hubUrl, true);
9191
List<URL> nodeEndpoints = GridServer.getGridProxies(config, hubUrl);
9292
if (nodeEndpoints.isEmpty()) {
93-
LOGGER.debug("Detected servlet container at: {}", hubUrl);
93+
LOGGER.debug("Detected existing servlet container at: {}", hubUrl);
9494
} else {
95-
LOGGER.debug("Mapping structure of grid at: {}", hubUrl);
95+
LOGGER.debug("Mapping structure of existing grid at: {}", hubUrl);
9696
for (URL nodeEndpoint : nodeEndpoints) {
9797
URI nodeUri = UriUtils.uriForPath(nodeEndpoint, GridServer.HUB_BASE);
9898
nodeServers.put(nodeEndpoint, new GridServer(nodeUri.toURL(), false));
@@ -118,17 +118,19 @@ public SeleniumGrid(SeleniumConfig config, URL hubUrl) throws IOException {
118118
*/
119119
public SeleniumGrid(SeleniumConfig config, LocalGridServer hubServer, LocalGridServer... nodeServers) throws IOException {
120120
this.hubServer = Objects.requireNonNull(hubServer, "[hubServer] must be non-null");
121-
if (nodeServers.length == 0) {
122-
LOGGER.debug("Defined servlet container at: {}", hubServer.getUrl());
123-
} else {
124-
LOGGER.debug("Assembling graph of grid at: {}", hubServer.getUrl());
121+
if (nodeServers.length > 0) {
122+
LOGGER.debug("Assembling graph of pending grid at: {}", hubServer.getUrl());
125123
for (LocalGridServer nodeServer : nodeServers) {
126124
String nodeEndpoint = nodeServer.getUrl().getProtocol() + "://" + nodeServer.getUrl().getAuthority();
127125
URL nodeUrl = URI.create(nodeEndpoint).toURL();
128126
this.nodeServers.put(nodeUrl, nodeServer);
129127
this.personalities.putAll(nodeServer.getPersonalities());
130128
}
131129
LOGGER.debug("{}: Personalities => {}", hubServer.getUrl(), personalities.keySet());
130+
} else if (config.getVersion() == 3) {
131+
LOGGER.debug("Queued up servlet container at: {}", hubServer.getUrl());
132+
} else {
133+
LOGGER.debug("Queued up hub without nodes at: {}", hubServer.getUrl());
132134
}
133135
}
134136

@@ -163,43 +165,49 @@ private void addNodePersonalities(SeleniumConfig config, URL hubUrl, URL nodeUrl
163165
* Grid instance and returns a {@link LocalSeleniumGrid} object.
164166
*
165167
* @param config {@link SeleniumConfig} object
166-
* @param hubUrl {@link URL} of hub host
168+
* @param hubUrl {@link URL} of hub host (may be {@code null})
167169
* @return {@link SeleniumGrid} object for the specified hub endpoint
168170
* @throws IOException if an I/O error occurs
169171
*/
170172
public static SeleniumGrid create(SeleniumConfig config, URL hubUrl) throws IOException {
171-
if ((hubUrl != null) && GridServer.isHubActive(hubUrl)) {
173+
Objects.requireNonNull(config, "[config] must be non-null");
174+
175+
// if URL is undefined or specifies 'localhost' address
176+
if (hubUrl == null || GridUtility.isLocalHost(hubUrl)) {
177+
// create/augment local grid instance
178+
return LocalSeleniumGrid.create(config, hubUrl);
179+
// otherwise, if URL responds to requests
180+
} else if (GridServer.isHubActive(hubUrl)) {
172181
// store hub host and hub port in system properties for subsequent retrieval
173182
System.setProperty(SeleniumSettings.HUB_HOST.key(), hubUrl.toExternalForm());
174183
System.setProperty(SeleniumSettings.HUB_PORT.key(), Integer.toString(hubUrl.getPort()));
184+
// build graph of existing grid
175185
return new SeleniumGrid(config, hubUrl);
176-
} else if ((hubUrl == null) || GridUtility.isLocalHost(hubUrl)) {
177-
return LocalSeleniumGrid.create(config, config.createHubConfig());
178186
}
187+
179188
throw new IllegalStateException("Specified remote hub URL '" + hubUrl + "' isn't active");
180189
}
181190

182191
/**
183192
* Shutdown the Selenium Grid represented by this object.
184193
*
185-
* @param localOnly {@code true} to target only local Grid servers
186194
* @return {@code false} if non-local Grid server encountered; otherwise {@code true}
187195
* @throws InterruptedException if this thread was interrupted
188196
*/
189-
public boolean shutdown(final boolean localOnly) throws InterruptedException {
197+
public boolean shutdown() throws InterruptedException {
190198
boolean result = true;
191199
Iterator<Entry<URL, GridServer>> iterator = nodeServers.entrySet().iterator();
192200

193201
while (iterator.hasNext()) {
194202
Entry<URL, GridServer> serverEntry = iterator.next();
195-
if (serverEntry.getValue().shutdown(localOnly)) {
203+
if (serverEntry.getValue().shutdown()) {
196204
iterator.remove();
197205
} else {
198206
result = false;
199207
}
200208
}
201209

202-
if (hubServer.shutdown(localOnly)) {
210+
if (hubServer.shutdown()) {
203211
hubServer = null;
204212
} else {
205213
result = false;
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package com.nordstrom.automation.selenium.core;
2+
3+
import java.io.BufferedReader;
4+
import java.io.IOException;
5+
import java.io.InputStreamReader;
6+
import java.util.Optional;
7+
8+
import com.nordstrom.common.file.OSInfo;
9+
10+
/**
11+
* This class uses OS utilities to identify the ID of the server process listening to the specified port.
12+
*/
13+
public class ServerPidFinder {
14+
15+
private enum PidFinder {
16+
WINDOWS("cmd.exe", "/c", "for /f \"tokens=5\" %%a in ('netstat -ano ^| findstr :%d ^| findstr LISTENING') do @echo %%a"),
17+
MAC_UNIX("sh", "-c", "lsof -i :%d -sTCP:LISTEN -t");
18+
19+
private String executable;
20+
private String commandOption;
21+
private String commandFormat;
22+
23+
PidFinder(String execuable, String commandOption, String commandFormat) {
24+
this.executable = execuable;
25+
this.commandOption = commandOption;
26+
this.commandFormat = commandFormat;
27+
}
28+
29+
String getExecutable() {
30+
return executable;
31+
}
32+
33+
String getOption() {
34+
return commandOption;
35+
}
36+
37+
String getCommand(int port) {
38+
return String.format(commandFormat, port);
39+
}
40+
}
41+
42+
/**
43+
* Private constructor to prevent instantiation.
44+
*/
45+
private ServerPidFinder() {
46+
throw new AssertionError("ServerPidFinder is a static utility class that cannot be instantiated");
47+
}
48+
49+
/**
50+
* Get the process ID of the server listening to the specified port.
51+
*
52+
* @param port {@code localhost} port to check
53+
* @return if found, ID of listening process; otherwise {@code null}
54+
*/
55+
public static String getPidOfServerAt(int port) {
56+
String pid = null;
57+
58+
try {
59+
PidFinder finder =
60+
OSInfo.getDefault().getType() == OSInfo.OSType.WINDOWS ? PidFinder.WINDOWS : PidFinder.MAC_UNIX;
61+
62+
ProcessBuilder pb = new ProcessBuilder(finder.getExecutable(), finder.getOption(), finder.getCommand(port));
63+
Process process = pb.start();
64+
65+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
66+
pid = Optional.ofNullable(reader.readLine()).filter(s -> s != null && !s.matches("\\s*")).map(String::trim).orElse(null);
67+
}
68+
69+
process.waitFor();
70+
} catch (IOException e) {
71+
// nothing to do here;
72+
} catch (InterruptedException e) {
73+
Thread.currentThread().interrupt();
74+
}
75+
76+
return pid;
77+
}
78+
}

src/main/java/com/nordstrom/automation/selenium/exceptions/VacationStackTrace.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import java.lang.reflect.Method;
44

5-
import com.nordstrom.automation.selenium.utility.ReflectUtil;
5+
import com.nordstrom.automation.selenium.utility.ReflectUtils;
66
import com.nordstrom.common.base.StackTrace;
77

88
/**
@@ -67,7 +67,7 @@ public Method getVacater() {
6767
*/
6868
private static String getMessage(final Method method, final String reason) {
6969
String className = method.getDeclaringClass().getSimpleName();
70-
String signature = ReflectUtil.getSignature(method);
70+
String signature = ReflectUtils.getSignature(method);
7171
String suffix = (reason != null) ? "\n" + reason : "";
7272
return PREAMBLE + className + ":" + signature + suffix;
7373
}

src/main/java/com/nordstrom/automation/selenium/plugins/RemoteWebDriverPlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ public LocalGridServer create(SeleniumConfig config, String launcherClassName, S
5656
Path nodeConfigPath = config.createNodeConfig(capabilities, hubUrl);
5757
String[] propertyNames = getPropertyNames(capabilities);
5858
return LocalSeleniumGrid.create(config, launcherClassName, combinedContexts,
59-
false, 0, nodeConfigPath, workingPath, outputPath, propertyNames);
59+
false, -1, nodeConfigPath, workingPath, outputPath, propertyNames);
6060
}
6161

6262
/**

0 commit comments

Comments
 (0)