From 5289e9779a6594dcada5f5095e81ba8eafe62110 Mon Sep 17 00:00:00 2001 From: Krishnan Mahadevan Date: Wed, 11 Jul 2018 00:12:35 +0530 Subject: [PATCH] Enriching Hub Status to include Node info (#6127) Currently the end-point /grid/api/hub/status does not provide information about the nodes attached to it and also the busy/free status per browser flavor on each of the nodes. Enriching the end-point to provide node info when invoked with the option /grid/api/hub/status?configuration=nodes Sample payload as below: { "nodes": [ { "Id": "http://192.168.1.6:5555", "browsers": [ { "browser": "safari", "slots": { "busy": 0, "total": 1 } }, { "browser": "chrome", "slots": { "busy": 0, "total": 5 } }, { "browser": "firefox", "slots": { "busy": 0, "total": 5 } } ] } ], "success": true } --- .../grid/web/servlet/HubStatusServlet.java | 113 +++++++++++++--- .../grid/web/servlet/BaseServletTest.java | 20 ++- .../grid/web/servlet/GridServletTests.java | 3 +- .../web/servlet/HubStatusServletTest.java | 121 ++++++++++++++++++ .../servlet/RegistrationAwareServletTest.java | 38 ++++++ .../web/servlet/RegistrationServletTest.java | 18 +-- .../testing/FakeHttpServletRequest.java | 26 +--- 7 files changed, 278 insertions(+), 61 deletions(-) create mode 100644 java/server/test/org/openqa/grid/web/servlet/HubStatusServletTest.java create mode 100644 java/server/test/org/openqa/grid/web/servlet/RegistrationAwareServletTest.java diff --git a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java index 1b39543db32a7..2d11733cf1dd0 100644 --- a/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java +++ b/java/server/src/org/openqa/grid/web/servlet/HubStatusServlet.java @@ -19,24 +19,32 @@ import static org.openqa.selenium.json.Json.MAP_TYPE; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSortedMap; +import com.google.common.collect.Lists; import org.openqa.grid.internal.GridRegistry; import org.openqa.grid.internal.RemoteProxy; +import org.openqa.grid.internal.TestSlot; import org.openqa.selenium.json.Json; import org.openqa.selenium.json.JsonException; import org.openqa.selenium.json.JsonInput; import org.openqa.selenium.json.JsonOutput; +import org.openqa.selenium.remote.CapabilityType; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.io.Writer; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.stream.Collector; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.reducing; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -63,10 +71,18 @@ */ public class HubStatusServlet extends RegistryBasedServlet { + private static final String SUCCESS = "success"; + private static final String CONFIGURATION = "configuration"; + private static final String FREE = "free"; + private static final String BUSY = "busy"; + private static final String NEW_SESSION_REQUEST_COUNT = "newSessionRequestCount"; + private static final String SLOT_COUNTS = "slotCounts"; + private static final String NODES = "nodes"; + private static final String TOTAL = "total"; private final Json json = new Json(); public HubStatusServlet() { - super(null); + this(null); } public HubStatusServlet(GridRegistry registry) { @@ -76,7 +92,7 @@ public HubStatusServlet(GridRegistry registry) { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { - process(request, response, new HashMap()); + process(request, response, new HashMap<>()); } @Override @@ -109,35 +125,41 @@ private Map getResponse( HttpServletRequest request, Map requestJSON) { Map res = new TreeMap<>(); - res.put("success", true); + res.put(SUCCESS, true); try { - List keysToReturn = null; + String configuration = request.getParameter(CONFIGURATION); - if (request.getParameter("configuration") != null && !"".equals(request.getParameter("configuration"))) { - keysToReturn = Arrays.asList(request.getParameter("configuration").split(",")); - } else if (requestJSON != null && requestJSON.containsKey("configuration")) { - //noinspection unchecked - keysToReturn = (List) requestJSON.get("configuration"); + if (Strings.isNullOrEmpty(configuration)) { + configuration = ""; + if (requestJSON.containsKey(CONFIGURATION)) { + //noinspection unchecked + configuration = requestJSON.get(CONFIGURATION).toString(); + } } + List keysToReturn = Splitter.on(",").omitEmptyStrings().splitToList(configuration); + GridRegistry registry = getRegistry(); Map config = registry.getHub().getConfiguration().toJson(); for (Map.Entry entry : config.entrySet()) { - if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains(entry.getKey())) { + if (isKeyPresentIn(keysToReturn, entry.getKey())) { res.put(entry.getKey(), entry.getValue()); } } - if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("newSessionRequestCount")) { - res.put("newSessionRequestCount", registry.getNewSessionRequestCount()); + if (isKeyPresentIn(keysToReturn, NEW_SESSION_REQUEST_COUNT)) { + res.put(NEW_SESSION_REQUEST_COUNT, registry.getNewSessionRequestCount()); } - if (keysToReturn == null || keysToReturn.isEmpty() || keysToReturn.contains("slotCounts")) { - res.put("slotCounts", getSlotCounts()); + if (isKeyPresentIn(keysToReturn, SLOT_COUNTS)) { + res.put(SLOT_COUNTS, getSlotCounts()); + } + if (keysToReturn != null && keysToReturn.contains(NODES)) { + res.put(NODES, getNodesInfo()); } } catch (Exception e) { - res.remove("success"); - res.put("success", false); + res.remove(SUCCESS); + res.put(SUCCESS, false); res.put("msg", e.getMessage()); } return res; @@ -154,8 +176,8 @@ private Map getSlotCounts() { } return ImmutableSortedMap.of( - "free", totalSlots - usedSlots, - "total", totalSlots); + FREE, totalSlots - usedSlots, + TOTAL, totalSlots); } private Map getRequestJSON(HttpServletRequest request) throws IOException { @@ -167,4 +189,57 @@ private Map getRequestJSON(HttpServletRequest request) throws IO throw new IOException(e); } } + + private static boolean isKeyPresentIn(List keys, String key) { + return keys == null || keys.isEmpty() || keys.contains(key); + } + + private List> getNodesInfo() { + List proxies = getRegistry().getAllProxies().getSorted(); + return proxies.stream().map(this::getNodeInfo).collect(toList()); + } + + private Map getNodeInfo(RemoteProxy remoteProxy) { + return ImmutableSortedMap.of( + "id", remoteProxy.getId(), + "browsers", getInfoFromAllSlotsInNode(remoteProxy.getTestSlots()) + ); + } + + private List> getInfoFromAllSlotsInNode(List slots) { + List> browsers = Lists.newArrayList(); + Map> + slotsInfo = slots.stream().collect(groupingBy(HubStatusServlet::getBrowser)); + for (Map.Entry> each : slotsInfo.entrySet()) { + String key = each.getKey(); + Map value = getSlotInfoPerBrowserFlavor(each.getValue()); + browsers.add(ImmutableSortedMap.of("browser", key, "slots", value)); + } + return browsers; + } + + private Map getSlotInfoPerBrowserFlavor(List slots) { + Map byStatus = slots.stream().collect(groupingBy(this::status, counting())); + int busy = byStatus.computeIfAbsent(BUSY, status -> 0); + int free = byStatus.computeIfAbsent(FREE, status -> 0); + int total = busy + free; + + return ImmutableSortedMap.of(TOTAL, total, BUSY, busy); + } + + private String status(TestSlot slot) { + if (slot.getSession() == null) { + return FREE; + } + return BUSY; + } + + private static String getBrowser(TestSlot slot) { + return slot.getCapabilities().get(CapabilityType.BROWSER_NAME).toString(); + } + + private static Collector counting() { + return reducing(0, e -> 1, Integer::sum); + } + } diff --git a/java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java b/java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java index 41b8dcdbcf1d1..faa7bd6a83900 100644 --- a/java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java +++ b/java/server/test/org/openqa/grid/web/servlet/BaseServletTest.java @@ -23,6 +23,7 @@ import org.openqa.testing.UrlInfo; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; @@ -41,15 +42,30 @@ protected static UrlInfo createUrl(String path) { protected FakeHttpServletResponse sendCommand(String method, String commandPath) throws IOException, ServletException { - return sendCommand(method, commandPath, (Map) null); + return sendCommand(method, commandPath, null); } protected FakeHttpServletResponse sendCommand( String method, String commandPath, Map parameters) throws IOException, ServletException { + return sendCommand(this.servlet,method, commandPath, parameters); + } + + protected static FakeHttpServletResponse sendCommand( + HttpServlet servlet, + String method, + String commandPath, + Map parameters) throws IOException, ServletException { FakeHttpServletRequest request = new FakeHttpServletRequest(method, createUrl(commandPath)); - if (parameters != null) { + if ("get".equalsIgnoreCase(method) && parameters != null) { + Map params = new HashMap<>(); + for (Map.Entry parameter : parameters.entrySet()) { + params.put(parameter.getKey(), parameter.getValue().toString()); + } + request.setParameters(params); + } + if ("post".equalsIgnoreCase(method) && parameters != null) { request.setBody(new Json().toJson(parameters)); } FakeHttpServletResponse response = new FakeHttpServletResponse(); diff --git a/java/server/test/org/openqa/grid/web/servlet/GridServletTests.java b/java/server/test/org/openqa/grid/web/servlet/GridServletTests.java index dd6174970925a..9a296eb92779d 100644 --- a/java/server/test/org/openqa/grid/web/servlet/GridServletTests.java +++ b/java/server/test/org/openqa/grid/web/servlet/GridServletTests.java @@ -25,7 +25,8 @@ DisplayHelpServletTest.class, ResourceServletTest.class, ConsoleServletTest.class, - RegistrationServletTest.class + RegistrationServletTest.class, + HubStatusServletTest.class }) public class GridServletTests { } diff --git a/java/server/test/org/openqa/grid/web/servlet/HubStatusServletTest.java b/java/server/test/org/openqa/grid/web/servlet/HubStatusServletTest.java new file mode 100644 index 0000000000000..a4acc2701dc3e --- /dev/null +++ b/java/server/test/org/openqa/grid/web/servlet/HubStatusServletTest.java @@ -0,0 +1,121 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.grid.web.servlet; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableMap; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.openqa.grid.common.RegistrationRequest; +import org.openqa.grid.internal.DefaultGridRegistry; +import org.openqa.grid.internal.GridRegistry; +import org.openqa.grid.internal.utils.configuration.GridHubConfiguration; +import org.openqa.grid.internal.utils.configuration.GridNodeConfiguration; +import org.openqa.grid.web.Hub; +import org.openqa.selenium.json.Json; +import org.openqa.selenium.json.JsonInput; +import org.openqa.testing.FakeHttpServletResponse; +import org.seleniumhq.jetty9.server.handler.ContextHandler; + +import java.io.IOException; +import java.io.StringReader; +import java.util.List; +import java.util.Map; + +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; + +@RunWith(JUnit4.class) +public class HubStatusServletTest extends RegistrationAwareServletTest { + + private static final GridRegistry registry = DefaultGridRegistry + .newInstance(new Hub(new GridHubConfiguration())); + + @Before + public void setUp() throws Exception { + servlet = new HubStatusServlet() { + @Override + public ServletContext getServletContext() { + final ContextHandler.Context servletContext = new ContextHandler().getServletContext(); + servletContext.setAttribute(GridRegistry.KEY, registry); + return servletContext; + } + }; + servlet.init(); + } + + @Test + public void testGetConfiguration() throws IOException, ServletException { + Map map = invokeCommand("post", null); + assertTrue("capabilityMatcher should be present", map.containsKey("capabilityMatcher")); + } + + @Test + public void testSelectiveGetConfiguration() throws IOException, ServletException { + Map map = invokeCommand("get", + ImmutableMap.of("configuration", "debug")); + assertEquals("There should be only 2 keys in the map", 2, map.size()); + assertTrue("debug should be present", map.containsKey("debug")); + } + + @Test + public void testGetNodeInformation() throws Exception { + wireInNode(); + Map map = invokeCommand("get", + ImmutableMap.of("configuration", "nodes")); + assertFalse("Node configuration should not be empty", map.isEmpty()); + List nodes = (List) map.get("nodes"); + assertEquals("Exactly 1 node info should be present", 1, nodes.size()); + Map node = (Map) nodes.get(0); + assertEquals("Two keys should be present per node", 2, node.keySet().size()); + } + + private void wireInNode() throws Exception { + final GridNodeConfiguration config = new GridNodeConfiguration(); + config.id = "http://dummynode:3456"; + final RegistrationRequest request = RegistrationRequest.build(config); + request.getConfiguration().proxy = null; + HttpServlet servlet = new RegistrationServlet() { + @Override + public ServletContext getServletContext() { + final ContextHandler.Context servletContext = new ContextHandler().getServletContext(); + servletContext.setAttribute(GridRegistry.KEY, registry); + return servletContext; + } + }; + servlet.init(); + sendCommand(servlet, "POST", "/", request.toJson()); + waitForServletToAddProxy(); + } + + private Map invokeCommand(String method, Map params) + throws IOException, ServletException { + FakeHttpServletResponse fakeResponse = sendCommand(method, "/", params); + Json json = new Json(); + JsonInput jin = json.newInput(new StringReader(fakeResponse.getBody())); + return jin.read(Json.MAP_TYPE); + } + +} diff --git a/java/server/test/org/openqa/grid/web/servlet/RegistrationAwareServletTest.java b/java/server/test/org/openqa/grid/web/servlet/RegistrationAwareServletTest.java new file mode 100644 index 0000000000000..3230e3e725590 --- /dev/null +++ b/java/server/test/org/openqa/grid/web/servlet/RegistrationAwareServletTest.java @@ -0,0 +1,38 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package org.openqa.grid.web.servlet; + +public class RegistrationAwareServletTest extends BaseServletTest { + + /** + * Gives the servlet some time to add the proxy -- which happens on a separate thread. + */ + protected void waitForServletToAddProxy() throws Exception { + int tries = 0; + int size = 0; + while (tries < 10) { + size = ((RegistryBasedServlet) servlet).getRegistry().getAllProxies().size(); + if (size > 0) { + break; + } + Thread.sleep(1000); + tries += 1; + } + } + +} diff --git a/java/server/test/org/openqa/grid/web/servlet/RegistrationServletTest.java b/java/server/test/org/openqa/grid/web/servlet/RegistrationServletTest.java index d060d69ba2834..c56feed4ff93d 100644 --- a/java/server/test/org/openqa/grid/web/servlet/RegistrationServletTest.java +++ b/java/server/test/org/openqa/grid/web/servlet/RegistrationServletTest.java @@ -44,7 +44,7 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; -public class RegistrationServletTest extends BaseServletTest { +public class RegistrationServletTest extends RegistrationAwareServletTest { private Map requestWithoutConfig; private Map grid2Request; @@ -85,21 +85,7 @@ public ServletContext getServletContext() { servlet.init(); } - /** - * Gives the servlet some time to add the proxy -- which happens on a separate thread. - */ - private void waitForServletToAddProxy() throws Exception { - int tries = 0; - int size; - while (tries < 10) { - size = ((RegistrationServlet) servlet).getRegistry().getAllProxies().size(); - if (size > 0) { - break; - } - Thread.sleep(1000); - tries += 1; - } - } + /** * Tests that the registration request servlet throws an error for a request without a proxy diff --git a/java/server/test/org/openqa/testing/FakeHttpServletRequest.java b/java/server/test/org/openqa/testing/FakeHttpServletRequest.java index ce7da36fc68fc..5a96ba89d3710 100644 --- a/java/server/test/org/openqa/testing/FakeHttpServletRequest.java +++ b/java/server/test/org/openqa/testing/FakeHttpServletRequest.java @@ -59,35 +59,15 @@ public class FakeHttpServletRequest extends HeaderContainer private final Map parameters; private final String method; - private ServletInputStream inputStream = new ServletInputStream() { - @Override - public boolean isFinished() { - return false; - } - - @Override - public boolean isReady() { - return true; - } - - @Override - public void setReadListener(ReadListener readListener) { - throw new UnsupportedOperationException(); - } - - @Override - public int read() throws IOException { - return 0; - } - }; + private ServletInputStream inputStream; public FakeHttpServletRequest(String method, UrlInfo requestUrl) { this.attributes = new HashMap<>(); this.parameters = new HashMap<>(); this.method = method.toUpperCase(); this.requestUrl = requestUrl; - - setBody(""); + //Input stream should only be constructed when there's a valid body set from outside. + this.inputStream = null; } public void setParameters(Map parameters) {