All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kurento.test.grid.GridHandler Maven / Gradle / Ivy

Go to download

This project contains test cases for testing Kurento Java Client and Kurento Media Server.

There is a newer version: 6.18.0
Show newest version
/*

 * (C) Copyright 2015 Kurento (http://kurento.org/)
 *
 * Licensed 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.kurento.test.grid;

import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_ADDRESS;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_ADDRESS_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_PORT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_HUB_PORT_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_FILE_LIST_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_LIST_DEFAULT;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_LIST_PROPERTY;
import static org.kurento.test.config.TestConfiguration.SELENIUM_NODES_URL_PROPERTY;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Writer;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServlet;

import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.io.FileUtils;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.Assert;
import org.kurento.test.base.PerformanceTest;
import org.kurento.test.browser.BrowserType;
import org.kurento.test.config.TestScenario;
import org.kurento.test.utils.Randomizer;
import org.kurento.test.utils.Shell;
import org.kurento.test.utils.SshConnection;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.net.NetworkUtils;
import org.openqa.selenium.support.events.WebDriverEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Charsets;
import com.google.common.collect.ImmutableList;
import com.google.common.io.CharStreams;
import com.google.gson.JsonElement;

import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * Singleton handler for Selenium Grid infrastructure.
 *
 * @author Boni Garcia ([email protected])
 * @since 5.1.1
 * @see Selenium
 */
public class GridHandler {

  public static Logger log = LoggerFactory.getLogger(GridHandler.class);

  public static final String REMOTE_FOLDER = ".kurento-test";
  public static final String REMOTE_PID_FILE = "node-pid";
  public static final String IPS_REGEX =
      "(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)";

  private static final int TIMEOUT_NODE = 300; // seconds
  private static final String LAUNCH_SH = "launch-node.sh";

  private static GridHandler instance = null;

  private GridHub hub;
  private String hubAddress = getProperty(SELENIUM_HUB_ADDRESS, SELENIUM_HUB_ADDRESS_DEFAULT);
  private int hubPort = getProperty(SELENIUM_HUB_PORT_PROPERTY, SELENIUM_HUB_PORT_DEFAULT);
  private CountDownLatch countDownLatch;
  private Map nodes = new ConcurrentHashMap<>();
  private List nodeList;
  private boolean hubStarted = false;
  private boolean nodeListFiltered = false;

  protected GridHandler() {
    String nodesListProp = System.getProperty(SELENIUM_NODES_LIST_PROPERTY);
    String nodesListFileProp = System.getProperty(SELENIUM_NODES_FILE_LIST_PROPERTY);
    String nodesListUrlProp = System.getProperty(SELENIUM_NODES_URL_PROPERTY);

    if (nodesListUrlProp != null) {
      if (nodeList == null) {
        nodeList = new ArrayList<>();
        try {
          log.trace("Reading node list from URL {}", nodesListUrlProp);
          String contents = readContents(nodesListUrlProp);
          Pattern p = Pattern.compile(IPS_REGEX);
          Matcher m = p.matcher(contents);
          while (m.find()) {
            nodeList.add(m.group());
          }
        } catch (IOException e) {
          Assert.fail("Exception reading URL " + nodesListUrlProp + " : " + e.getMessage());
        }
      }

    } else if (nodesListFileProp != null) {
      log.trace("Reading node list from file {}", nodesListFileProp);
      try {
        nodeList = FileUtils.readLines(new File(nodesListFileProp), Charset.defaultCharset());
      } catch (IOException e) {
        Assert.fail("Exception reading node list file: " + e.getMessage());
      }

    } else if (nodesListProp != null) {
      log.trace("Reading node list from property {}", nodesListProp);
      nodeList = new ArrayList<>(Arrays.asList(nodesListProp.split(";")));

    } else {
      log.trace("Using default node list {}", SELENIUM_NODES_LIST_DEFAULT);
      InputStream inputStream =
          PerformanceTest.class.getClassLoader().getResourceAsStream(SELENIUM_NODES_LIST_DEFAULT);

      try {
        nodeList = CharStreams.readLines(new InputStreamReader(inputStream, Charsets.UTF_8));
      } catch (IOException e) {
        Assert.fail("Exception reading node-list.txt: " + e.getMessage());
      }
    }
  }

  public static synchronized GridHandler getInstance() {
    if (instance == null) {
      instance = new GridHandler();
    }
    return instance;
  }

  public synchronized void stopGrid() {
    log.debug("Stopping Selenium Grid");
    try {
      // Stop Hub
      if (hub != null) {
        log.debug("Stopping Hub");
        hub.stop();
        hubStarted = false;
      }

      // Stop Nodes
      if (nodes != null) {
        log.debug("Number of nodes: {}", nodes.size());

        for (GridNode node : nodes.values()) {
          log.debug("Stopping Node {}", node.getHost());
          stopNode(node);
        }
      }
      nodes.clear();
    } catch (Exception e) {
      throw new RuntimeException(e);
    }

  }

  public synchronized void startHub() {
    try {
      if (hubAddress != null && !hubStarted) {
        hub = new GridHub(hubPort);
        hub.start();
        hubStarted = true;
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void startNodes() {
    try {
      countDownLatch = new CountDownLatch(nodes.size());
      ExecutorService exec = Executors.newFixedThreadPool(nodes.size());

      for (final GridNode n : nodes.values()) {
        Thread t = new Thread() {
          @Override
          public void run() {
            startNode(n);
          }
        };
        exec.execute(t);
      }

      if (!countDownLatch.await(TIMEOUT_NODE, TimeUnit.SECONDS)) {
        Assert.fail("Timeout waiting nodes (" + TIMEOUT_NODE + " seconds)");
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void startNode(GridNode node) {
    try {
      countDownLatch = new CountDownLatch(1);
      log.debug("Launching node {}", node.getHost());
      node.startSsh();

      final String chromeDriverSource = System.getProperty("webdriver.chrome.driver");

      final Class[] classpath = { ImmutableList.class,
          NetworkUtils.class, WebDriverException.class, LogFactory.class, HttpServlet.class,
          ChromeDriver.class, FirefoxDriver.class, JsonElement.class, HttpEntity.class,
          HttpClient.class, WebDriverEventListener.class, ExecuteWatchdog.class };

      // OverThere SCP need absolute path, so home path must be known
      String remoteHome = node.getHome();

      final String remoteFolder = remoteHome + "/" + REMOTE_FOLDER;
      final String remoteChromeDriver = remoteFolder + "/chromedriver";
      final String remoteScript = node.getTmpFolder() + "/" + LAUNCH_SH;
      final String remotePort = String.valueOf(node.getSshConnection().getFreePort());

      if (!node.getSshConnection().exists(remoteFolder) || node.isOverwrite()) {
        node.getSshConnection().execAndWaitCommand("mkdir", "-p", remoteFolder);
      }
      if (!node.getSshConnection().exists(remoteChromeDriver) || node.isOverwrite()) {
        node.getSshConnection().scp(chromeDriverSource, remoteChromeDriver);
        node.getSshConnection().execAndWaitCommand("chmod", "+x", remoteChromeDriver);
      }

      String cp = "";
      for (Class clazz : classpath) {
        if (!cp.isEmpty()) {
          cp += ":";
        }
        String jarSource = getJarPath(clazz).getAbsolutePath();
        String remoteSeleniumJar = remoteFolder + "/" + getJarPath(clazz).getName();
        cp += remoteSeleniumJar;

        if (!node.getSshConnection().exists(remoteSeleniumJar) || node.isOverwrite()) {
          node.getSshConnection().scp(jarSource, remoteSeleniumJar);
        }
      }

      // Script is always overwritten
      createRemoteScript(node, remotePort, remoteScript, remoteFolder, remoteChromeDriver, cp,
          node.getBrowserType(), node.getMaxInstances());

      // Launch node
      node.getSshConnection().execCommand(remoteScript);

      // Wait to be available for Hub
      waitForNode(node.getHost(), remotePort);

      // Set started flag to true
      node.setStarted(true);
    } catch (Exception e) {
      e.printStackTrace();
      throw new RuntimeException(e);
    }
  }

  private File getJarPath(Class aclass) {
    URL url;
    try {
      url = aclass.getProtectionDomain().getCodeSource().getLocation();
    } catch (SecurityException ex) {
      url = aclass.getResource(aclass.getSimpleName() + ".class");
    }
    try {
      return new File(url.toURI());
    } catch (URISyntaxException ex) {
      return new File(url.getPath());
    }
  }

  private void createRemoteScript(GridNode node, String remotePort, String remoteScript,
      String remoteFolder, String remoteChromeDriver, String classpath, BrowserType browser,
      int maxInstances) throws IOException {

    Map data = new HashMap();
    data.put("remotePort", String.valueOf(remotePort));
    data.put("maxInstances", String.valueOf(maxInstances));
    data.put("hubIp", hubAddress);
    data.put("hubPort", String.valueOf(hubPort));
    data.put("tmpFolder", node.getTmpFolder());
    data.put("remoteChromeDriver", remoteChromeDriver);
    data.put("classpath", classpath);
    data.put("pidFile", REMOTE_PID_FILE);
    data.put("browser", browser);

    // Create script for Node
    Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
    cfg.setClassForTemplateLoading(PerformanceTest.class, "/templates/");

    String tmpScript = node.getTmpFolder() + LAUNCH_SH;
    try {
      Template template = cfg.getTemplate(LAUNCH_SH + ".ftl");
      Writer writer = new FileWriter(new File(tmpScript));
      template.process(data, writer);
      writer.flush();
      writer.close();

    } catch (Exception e) {
      throw new RuntimeException("Exception while creating file from template", e);
    }

    // Copy script to remote node
    node.getSshConnection().scp(tmpScript, remoteScript);
    node.getSshConnection().execAndWaitCommand("chmod", "+x", remoteScript);
    Shell.runAndWait("rm", tmpScript);
  }

  public void copyRemoteVideo(GridNode node, String video) {
    try {
      // Copy video in remote host if necessary
      if (!node.getSshConnection().exists(node.getRemoteVideo(video)) || node.isOverwrite()) {
        node.getSshConnection().scp(video, node.getRemoteVideo(video));
      }
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  private void waitForNode(String node, String port) {
    log.debug("Waiting for node {} to be ready...", node);
    int responseStatusCode = 0;
    HttpClient client = HttpClientBuilder.create().build();
    HttpGet httpGet =
        new HttpGet("http://" + node + ":" + port + "/wd/hub/static/resource/hub.html");

    // Wait for a max of TIMEOUT_NODE seconds
    long maxSystemTime = System.currentTimeMillis() + TIMEOUT_NODE * 1000;
    do {
      try {
        HttpResponse response = client.execute(httpGet);
        responseStatusCode = response.getStatusLine().getStatusCode();
      } catch (Exception e) {
        try {
          Thread.sleep(100);
        } catch (InterruptedException ie) {
          // Intentionally left blank
        }
        if (System.currentTimeMillis() > maxSystemTime) {
          log.error("Timeout ({} sec) waiting for node {}", TIMEOUT_NODE, node);
        }
      }
    } while (responseStatusCode != HttpStatus.SC_OK);

    if (responseStatusCode == HttpStatus.SC_OK) {
      log.debug("Node {} ready (responseStatus {})", node, responseStatusCode);
      countDownLatch.countDown();
    }
  }

  public synchronized void filterValidNodes() {
    if (!nodeListFiltered) {
      log.debug("Node availables in the node list: {}", nodeList.size());
      int nodeListSize = nodeList.size();
      ExecutorService executor = Executors.newFixedThreadPool(nodeListSize);
      final CountDownLatch latch = new CountDownLatch(nodeListSize);

      for (final String nodeCandidate : nodeList) {
        executor.execute(new Runnable() {
          @Override
          public void run() {
            if (!nodeIsValid(nodeCandidate)) {
              nodeList.remove(nodeCandidate);
            }
            latch.countDown();
          }
        });
      }

      try {
        latch.await();
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
      nodeListFiltered = true;
      log.debug("Node availables in the node list after filtering: {}", nodeList.size());
    }
  }

  public boolean nodeIsValid(String nodeCandidate) {
    boolean valid = false;
    log.debug("Node candidate {}", nodeCandidate);

    if (SshConnection.ping(nodeCandidate)) {
      SshConnection remoteHost = new SshConnection(nodeCandidate);
      try {
        remoteHost.start();
        int xvfb = remoteHost.runAndWaitCommand("xvfb-run");
        if (xvfb != 2) {
          log.debug("Node {} has no Xvfb", nodeCandidate);
        } else {
          valid = true;
        }
      } catch (Exception e) {
        log.debug("Invalid credentials to access node {} ", nodeCandidate);
      } finally {
        remoteHost.stop();
      }

    } else {
      log.debug("Node {} seems to be down", nodeCandidate);
    }
    return valid;
  }

  public synchronized GridNode getRandomNodeFromList(String browserKey, BrowserType browserType,
      int browserPerInstance) {

    log.debug("getRandomNodeFromList for browser {}", browserKey);

    GridNode node = browserPerInstance > 1 ? existsNode(browserKey) : null;
    if (node == null) {
      try {

        String nodeCandidate = nodeList.get(Randomizer.getInt(0, nodeList.size()));
        log.debug("######## Creating node {} in host {}", browserKey, nodeCandidate);
        node = new GridNode(nodeCandidate, browserType, browserPerInstance);
        addNode(browserKey, node);
        nodeList.remove(nodeCandidate);
        log.debug(">>>> Using node {} for browser '{}'", node.getHost(), browserKey);

      } catch (IllegalArgumentException e) {
        throw new RuntimeException("No valid available node(s) to perform Selenim Grid test");
      }

    } else {
      log.debug(">>>> Re-using node {} for browser '{}'", node.getHost(), browserKey);
      node.setStarted(true);
    }
    return node;

  }

  private synchronized GridNode existsNode(String browserKey) {
    GridNode gridNode = null;
    int indexOfSeparator = browserKey.lastIndexOf(TestScenario.INSTANCES_SEPARATOR);

    if (indexOfSeparator != -1) {
      String browserPreffix = browserKey.substring(0, indexOfSeparator + 1);
      log.debug("browserPreffix {}", browserPreffix);

      for (String node : nodes.keySet()) {
        if (node.startsWith(browserPreffix)) {
          gridNode = nodes.get(node);
          break;
        }
      }
    }
    log.debug("Exists node {} = {}", browserKey, gridNode != null);

    return gridNode;
  }

  private void stopNode(GridNode node) throws IOException {
    if (node.getSshConnection().isStarted()) {
      node.getSshConnection().execCommand("kill", "-9", "-1");
      node.stopSsh();
    }
  }

  public void runParallel(List nodeList, Runnable myFunc)
      throws InterruptedException, ExecutionException {
    ExecutorService exec = Executors.newFixedThreadPool(nodes.size());
    List> results = new ArrayList<>();
    for (int i = 0; i < nodes.size(); i++) {
      results.add(exec.submit(myFunc));
    }
    for (Future r : results) {
      r.get();
    }
  }

  public String getHubHost() {
    return hubAddress;
  }

  public int getHubPort() {
    return hubPort;
  }

  public GridNode getNode(String browserKey) {
    return nodes.get(browserKey);
  }

  public synchronized void addNode(String browserKey, GridNode node) {
    log.debug("Adding node {} ({}) to map", browserKey, node.getHost());
    nodes.put(browserKey, node);
  }

  public boolean useRemoteNodes() {
    return !nodes.isEmpty();
  }

  public void logNodeList() {
    String nodeListStr = "";
    for (GridNode node : nodes.values()) {
      nodeListStr += node.getHost() + " ";
    }
    log.debug("Node list: {}", nodeListStr);
  }

  public GridNode getFirstNode(String browserKey) {
    if (nodes.containsKey(browserKey)) {
      return nodes.get(browserKey);
    } else {
      return nodes.get(browserKey.substring(0, browserKey.indexOf("-") + 1) + 0);
    }
  }

  public synchronized boolean containsSimilarBrowserKey(String browserKey) {
    boolean constainsSimilarBrowser = false;
    int index = browserKey.indexOf("-");
    String browser = null;
    if (index != -1) {
      String prefix = browserKey.substring(0, browserKey.indexOf("-"));
      for (String key : nodes.keySet()) {
        constainsSimilarBrowser |= key.startsWith(prefix);
        if (constainsSimilarBrowser) {
          browser = key;
          break;
        }
      }
    }

    if (constainsSimilarBrowser && !nodes.keySet().contains(browserKey) && browser != null) {
      addNode(browserKey, nodes.get(browser));
    }
    return constainsSimilarBrowser;
  }

  public void setHubAddress(String hubAddress) {
    this.hubAddress = hubAddress;
  }

  public static String readContents(String address) throws IOException {
    StringBuilder contents = new StringBuilder(2048);
    BufferedReader br = null;
    try {
      URL url = new URL(address);
      br = new BufferedReader(new InputStreamReader(url.openStream()));
      String line = "";
      while (line != null) {
        line = br.readLine();
        contents.append(line);
      }
    } finally {
      if (br != null) {
        br.close();
      }
    }
    return contents.toString();
  }

  public List getNodeList() {
    return nodeList;
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy