io.openvidu.test.OpenViduClientBrowserTest.java Source code

Java tutorial

Introduction

Here is the source code for io.openvidu.test.OpenViduClientBrowserTest.java

Source

/*
 * (C) Copyright 2017-2019 OpenVidu (https://openvidu.io/)
 *
 * 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 io.openvidu.test;

import static io.openvidu.test.config.RoomTestConfiguration.ROOM_APP_CLASSNAME_DEFAULT;
import static io.openvidu.test.config.RoomTestConfiguration.ROOM_APP_CLASSNAME_PROP;
import static org.junit.Assert.fail;
import static org.kurento.commons.PropertiesManager.getProperty;
import static org.kurento.test.config.TestConfiguration.SELENIUM_SCOPE_PROPERTY;
import static org.kurento.test.config.TestConfiguration.TEST_URL_TIMEOUT_DEFAULT;
import static org.kurento.test.config.TestConfiguration.TEST_URL_TIMEOUT_PROPERTY;

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.kurento.commons.PropertiesManager;
import org.kurento.jsonrpc.JsonUtils;
import org.kurento.test.base.BrowserTest;
import org.kurento.test.browser.Browser;
import org.kurento.test.browser.BrowserType;
import org.kurento.test.browser.WebPage;
import org.kurento.test.browser.WebPageType;
import org.kurento.test.config.BrowserScope;
import org.kurento.test.config.TestScenario;
import org.kurento.test.services.KmsService;
import org.kurento.test.services.Service;
import org.kurento.test.services.TestService;
import org.kurento.test.services.WebServerService;
import org.openqa.selenium.By;
import org.openqa.selenium.Dimension;
import org.openqa.selenium.ElementNotVisibleException;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.Point;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

import com.google.gson.JsonArray;

/**
 * Base for Kurento Room tests with browsers.
 *
 * @author Radu Tom Vlad (rvlad@naevatec.com)
 * @since 6.2.1
 */
public abstract class OpenViduClientBrowserTest<W extends WebPage> extends BrowserTest<W> {

    public interface UserLifecycle {
        public void run(int numUser, int iteration) throws Exception;
    }

    public interface Task {
        public void exec(int numTask) throws Exception;
    }

    static class KmsUriSetterService extends TestService {

        private KmsService kms;

        public KmsUriSetterService(KmsService kms) {
            this.kms = kms;
        }

        @Override
        public TestServiceScope getScope() {
            return kms.getScope();
        }

        @Override
        public void start() {
            super.start();
            String uri = kms.getWsUri();
            System.setProperty("kms.uris", "[\"" + uri + "\"]");
            System.setProperty("kms.uri", uri);
            log.debug("Set system properties 'kms.uri' to {} & 'kms.uris' to [{}] ", uri, uri);
        }

    }

    public static long TASKS_TIMEOUT_IN_MINUTES = 15 * 60;

    public static int POLLING_LATENCY = 100;
    public static int WEB_TEST_MAX_WIDTH = 1200;
    public static int WEB_TEST_LEFT_BAR_WIDTH = 60;
    public static int WEB_TEST_TOP_BAR_WIDTH = 30;
    public static int WEB_TEST_BROWSER_WIDTH = 500;
    public static int WEB_TEST_BROWSER_HEIGHT = 400;

    public static String ROOM_PREFIX = "room";
    public static String USER_BROWSER_PREFIX = "browser";
    public static String USER_FAKE_PREFIX = "user";

    public static @Service(1) KmsService kms = new KmsService();
    public static @Service(2) KmsUriSetterService kmsUriSetter = new KmsUriSetterService(kms);

    public static BrowserScope testScope = BrowserScope.LOCAL;
    public static Class<?> webServerClass;

    static {
        loadWebServerClass();
        if (webServerClass == null) {
            Assert.fail("Unable to load any of the provided classnames for the web server test service");
        }

        String scopeProp = PropertiesManager.getProperty(SELENIUM_SCOPE_PROPERTY);
        if (scopeProp != null) {
            testScope = BrowserScope.valueOf(scopeProp.toUpperCase());
        }
    }

    public static @Service(99) WebServerService webServer = new WebServerService(webServerClass);

    public static int testTimeout;

    public static SecureRandom random = new SecureRandom();

    public WebPageType webPageType = WebPageType.ROOM;

    public int PLAY_TIME = 5; // seconds

    public int ITERATIONS = 2;

    public Object browsersLock = new Object();

    public String roomName = ROOM_PREFIX;

    public Map<String, Exception> execExceptions = new HashMap<String, Exception>();

    public boolean failed = false;

    public OpenViduClientBrowserTest() {
        setDeleteLogsIfSuccess(false); // always keep the logs
    }

    @BeforeClass
    public static void beforeClass() {
        testTimeout = getProperty(TEST_URL_TIMEOUT_PROPERTY, TEST_URL_TIMEOUT_DEFAULT);
        log.debug("Test timeout: {}", testTimeout);
    }

    @Override
    public void setupBrowserTest() throws InterruptedException {
        super.setupBrowserTest();

        execExceptions.clear();

        if (testScenario != null && testScenario.getBrowserMap() != null
                && testScenario.getBrowserMap().size() > 0) {
            int row = 0;
            int col = 0;
            for (final String browserKey : testScenario.getBrowserMap().keySet()) {
                Browser browser = getPage(browserKey).getBrowser();
                browser.getWebDriver().manage().window()
                        .setSize(new Dimension(WEB_TEST_BROWSER_WIDTH, WEB_TEST_BROWSER_HEIGHT));
                browser.getWebDriver().manage().window()
                        .setPosition(new Point(col * WEB_TEST_BROWSER_WIDTH + WEB_TEST_LEFT_BAR_WIDTH,
                                row * WEB_TEST_BROWSER_HEIGHT + WEB_TEST_TOP_BAR_WIDTH));
                col++;
                if (col * WEB_TEST_BROWSER_WIDTH + WEB_TEST_LEFT_BAR_WIDTH > WEB_TEST_MAX_WIDTH) {
                    col = 0;
                    row++;
                }
            }
        }
    }

    @Override
    public void teardownBrowserTest() {
        super.teardownBrowserTest();
        failWithExceptions();
    }

    public static Collection<Object[]> localChromes(String caller, int browsers, WebPageType pageType) {
        TestScenario test = new TestScenario();
        for (int i = 0; i < browsers; i++) {
            Browser browser = new Browser.Builder().webPageType(pageType).browserType(BrowserType.CHROME)
                    .scope(BrowserScope.LOCAL).build();
            test.addBrowser(getBrowserKey(i), browser);
        }
        log.debug("{}: Web browsers: {}, webPageType: {}, test scope: {}, Browsers map keySet: {}", caller,
                browsers, pageType, testScope.toString(), test.getBrowserMap().keySet());
        return Arrays.asList(new Object[][] { { test } });
    }

    public static void loadWebServerClass() {
        try {
            List<String> auxList = JsonUtils.toStringList(PropertiesManager.getPropertyJson(ROOM_APP_CLASSNAME_PROP,
                    ROOM_APP_CLASSNAME_DEFAULT, JsonArray.class));

            for (String aux : auxList) {
                log.info("Loading class '{}' as the test's web server service", aux);
                try {
                    webServerClass = Class.forName(aux);
                    break;
                } catch (Exception e) {
                    log.warn("Couldn't load web server class '{}': {}", aux, e.getMessage());
                    log.debug("Couldn't load web server class '{}'", aux, e);
                }
            }
        } catch (Exception e) {
            log.error("Incorrect value for property '{}'", ROOM_APP_CLASSNAME_PROP, e);
        }
    }

    public static void sleep(long seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            log.warn("Interrupted while sleeping {}seconds", seconds, e);
        }
    }

    public static String getBrowserKey(int index) {
        return USER_BROWSER_PREFIX + index;
    }

    public static String getBrowserStreamName(int index) {
        return getBrowserKey(index) + "_webcam";
    }

    public static String getBrowserVideoStreamName(int index) {
        return "video-" + getBrowserStreamName(index);
    }

    public static String getBrowserNativeStreamName(int index) {
        return "native-" + getBrowserVideoStreamName(index);
    }

    public static String getFakeKey(int index) {
        return USER_FAKE_PREFIX + index;
    }

    public static String getFakeStreamName(int index) {
        return getFakeKey(index) + "_webcam";
    }

    public static String getFakeStreamName(String userName) {
        return userName + "_webcam";
    }

    public static String getFakeVideoStreamName(int index) {
        return "video-" + getFakeStreamName(index);
    }

    public static String getFakeNativeStreamName(int index) {
        return "native-" + getFakeVideoStreamName(index);
    }

    public static CountDownLatch[] createCdl(int numLatches, int numUsers) {
        final CountDownLatch[] cdl = new CountDownLatch[numLatches];
        for (int i = 0; i < numLatches; i++) {
            cdl[i] = new CountDownLatch(numUsers);
        }
        return cdl;
    }

    public void iterParallelUsers(int numUsers, int iterations, final UserLifecycle user)
            throws InterruptedException, ExecutionException, TimeoutException {

        int totalExecutions = iterations * numUsers;
        ExecutorService threadPool = Executors.newFixedThreadPool(totalExecutions);
        ExecutorCompletionService<Void> exec = new ExecutorCompletionService<>(threadPool);
        List<Future<Void>> futures = new ArrayList<>();

        try {
            for (int j = 0; j < iterations; j++) {
                final int it = j;
                log.info("it#{}: Starting execution of {} users", it, numUsers);
                for (int i = 0; i < numUsers; i++) {
                    final int numUser = i;
                    final Browser browser = getPage(getBrowserKey(i)).getBrowser();
                    futures.add(exec.submit(new Callable<Void>() {
                        @Override
                        public Void call() throws Exception {
                            Thread.currentThread().setName("it" + it + "|browser" + numUser);
                            if (it > 0) {
                                log.debug("Page reloaded");
                                browser.reload();
                            }
                            user.run(numUser, it);
                            return null;
                        }
                    }));
                }

                for (int i = 0; i < numUsers; i++) {
                    try {
                        exec.take().get();
                    } catch (ExecutionException e) {
                        log.error("Execution exception", e);
                        throw e;
                    }
                }
                log.info("it#{}: Finished execution of {} users", it, numUsers);
            }
        } finally {
            threadPool.shutdownNow();
        }
    }

    public void parallelTasks(int numThreads, final String thPrefix, String taskName, final Task task) {
        ExecutorService threadPool = Executors.newFixedThreadPool(numThreads);
        ExecutorCompletionService<Void> exec = new ExecutorCompletionService<>(threadPool);
        try {
            for (int i = 0; i < numThreads; i++) {
                final int numTask = i;
                exec.submit(new Callable<Void>() {
                    @Override
                    public Void call() throws Exception {
                        String thname = Thread.currentThread().getName();
                        Thread.currentThread().setName(thPrefix + numTask);
                        task.exec(numTask);
                        Thread.currentThread().setName(thname);
                        return null;
                    }
                });
            }
            for (int i = 0; i < numThreads; i++) {
                String thTask = taskName + "-" + thPrefix + i;
                try {
                    log.debug("Waiting for the {} execution to complete ({}/{})", thTask, i + 1, numThreads);
                    exec.take().get();
                    log.debug("Job {} completed ({}/{})", thTask, i + 1, numThreads);
                } catch (ExecutionException e) {
                    log.debug("Execution exception of {} ({}/{})", thTask, i + 1, numThreads, e);
                    execExceptions.put(taskName + "-" + thPrefix + i, e);
                } catch (InterruptedException e) {
                    log.error("Interrupted while waiting for execution of task{}", i, e);
                }
            }
        } finally {
            threadPool.shutdown();
            try {
                threadPool.awaitTermination(TASKS_TIMEOUT_IN_MINUTES, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
                log.warn("Tasks were executed more than {} minutes. Stopping it", TASKS_TIMEOUT_IN_MINUTES);
                threadPool.shutdownNow();
            }

        }
    }

    public void joinToRoom(int pageIndex, String userName, String roomName) {
        Browser userBrowser = getPage(getBrowserKey(pageIndex)).getBrowser();
        WebElement nameInput = findElement(userName, userBrowser, "name");
        nameInput.clear();
        nameInput.sendKeys(userName);
        WebElement roomInput = findElement(userName, userBrowser, "roomName");
        roomInput.clear();
        roomInput.sendKeys(roomName);
        findElement(userName, userBrowser, "joinBtn").submit();
        log.debug("Clicked on 'joinBtn' in {}", userName);
    }

    public void exitFromRoom(int pageIndex, String userName) {
        Browser userBrowser = getPage(getBrowserKey(pageIndex)).getBrowser();
        try {
            Actions actions = new Actions(userBrowser.getWebDriver());
            actions.click(findElement(userName, userBrowser, "buttonLeaveRoom")).perform();
            log.debug("'buttonLeaveRoom' clicked on in {}", userName);
        } catch (ElementNotVisibleException e) {
            log.warn("Button 'buttonLeaveRoom' is not visible. Session can't be closed");
        }
    }

    public void unsubscribe(int pageIndex, int unsubscribeFromIndex) {
        String clickableVideoTagId = getBrowserVideoStreamName(unsubscribeFromIndex);
        selectVideoTag(pageIndex, clickableVideoTagId);

        WebDriver userWebDriver = getPage(getBrowserKey(pageIndex)).getBrowser().getWebDriver();
        try {
            userWebDriver.findElement(By.id("buttonDisconnect")).click();
        } catch (ElementNotVisibleException e) {
            String msg = "Button 'buttonDisconnect' is not visible. Can't unsubscribe from media.";
            log.warn(msg);
            fail(msg);
        }
    }

    public void selectVideoTag(int pageIndex, String targetVideoTagId) {
        WebDriver userWebDriver = getPage(getBrowserKey(pageIndex)).getBrowser().getWebDriver();
        try {
            WebElement element = userWebDriver.findElement(By.id(targetVideoTagId));
            Actions actions = new Actions(userWebDriver);
            actions.moveToElement(element).click().perform();
        } catch (ElementNotVisibleException e) {
            String msg = "Video tag '" + targetVideoTagId + "' is not visible, thus not selectable.";
            log.warn(msg);
            fail(msg);
        }
    }

    protected void unpublish(int pageIndex) {
        WebDriver userWebDriver = getPage(getBrowserKey(pageIndex)).getBrowser().getWebDriver();
        try {
            userWebDriver.findElement(By.id("buttonDisconnect")).click();
        } catch (ElementNotVisibleException e) {
            log.warn("Button 'buttonDisconnect' is not visible. Can't unpublish media.");
        }
    }

    public WebElement findElement(String label, Browser browser, String id) {
        try {
            return new WebDriverWait(browser.getWebDriver(), testTimeout, POLLING_LATENCY)
                    .until(ExpectedConditions.presenceOfElementLocated(By.id(id)));
        } catch (TimeoutException e) {
            log.warn("Timeout when waiting for element {} to exist in browser {}", id, label);
            int originalTimeout = 60;
            try {
                originalTimeout = browser.getTimeout();
                log.debug("Original browser timeout (s): {}, set to 10", originalTimeout);
                browser.setTimeout(10);
                browser.changeTimeout(10);
                WebElement elem = browser.getWebDriver().findElement(By.id(id));
                log.info("Additional findElement call was able to locate {} in browser {}", id, label);
                return elem;
            } catch (NoSuchElementException e1) {
                log.debug("Additional findElement call couldn't locate {} in browser {} ({})", id, label,
                        e1.getMessage());
                throw new NoSuchElementException("Element with id='" + id + "' not found after " + testTimeout
                        + " seconds in browser " + label);
            } finally {
                browser.setTimeout(originalTimeout);
                browser.changeTimeout(originalTimeout);
            }
        }
    }

    public void waitWhileElement(String label, Browser browser, String id) throws TimeoutException {
        int originalTimeout = 60;
        try {
            originalTimeout = browser.getTimeout();
            log.debug("Original browser timeout (s): {}, set to 1", originalTimeout);
            browser.setTimeout(1);
            browser.changeTimeout(1);
            new WebDriverWait(browser.getWebDriver(), testTimeout, POLLING_LATENCY)
                    .until(ExpectedConditions.invisibilityOfElementLocated(By.id(id)));
        } catch (org.openqa.selenium.TimeoutException e) {
            log.warn("Timeout when waiting for element {} to disappear in browser {}", id, label, e);
            throw new TimeoutException(
                    "Element with id='" + id + "' is present in page after " + testTimeout + " seconds");
        } finally {
            browser.setTimeout(originalTimeout);
            browser.changeTimeout(originalTimeout);
        }
    }

    /**
     * Wait for stream of another browser user.
     *
     * @param index
     *          own user index
     * @param label
     *          browser name (user name)
     * @param targetIndex
     *          the target user index
     */
    public void waitForStream(int index, String label, int targetIndex) {
        waitForStream(index, label, getBrowserNativeStreamName(targetIndex));
    }

    /**
     * Wait for stream of a fake user.
     *
     * @param index
     *          own user index
     * @param label
     *          browser name (user name)
     * @param targetIndex
     *          the target user index
     */
    public void waitForStreamFake(int index, String label, int targetIndex) {
        waitForStream(index, label, getFakeNativeStreamName(targetIndex));
    }

    /**
     * Wait for stream of user whose video tag has already been generated.
     *
     * @param index
     *          own user index
     * @param label
     *          browser name (user name)
     * @param targetVideoTagId
     *          expected video tag id
     */
    public void waitForStream(int index, String label, String targetVideoTagId) {
        String hostKey = getBrowserKey(index);
        Browser browser = getPage(hostKey).getBrowser();
        int i = 0;
        for (; i < testTimeout; i++) {
            WebElement video = findElement(label, browser, targetVideoTagId);
            String srcAtt = video.getAttribute("src");
            if (srcAtt != null && srcAtt.startsWith("blob")) {
                break;
            }
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        if (i == testTimeout) {
            Assert.fail("Video tag '" + targetVideoTagId + "' is not playing media after " + testTimeout
                    + " seconds in '" + hostKey + "'");
        }
    }

    public void verify(boolean[] activeBrowserUsers) {
        verify(activeBrowserUsers, null);
    }

    public void verify(boolean[] activeBrowserUsers, Map<String, Boolean> activeFakeUsers) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < activeBrowserUsers.length; i++) {
            if (activeBrowserUsers[i]) {
                sb.append(USER_BROWSER_PREFIX + i + ": active, ");
            } else {
                sb.append(USER_BROWSER_PREFIX + i + ": not, ");
            }
        }
        log.debug("Checking Browser users (active or not): [{}]", sb);
        if (activeFakeUsers != null && !activeFakeUsers.isEmpty()) {
            log.debug("Checking Fake users (active or not): {}", activeFakeUsers);
        }

        long startTime = System.nanoTime();

        for (int i = 0; i < activeBrowserUsers.length; i++) {

            if (activeBrowserUsers[i]) {
                Browser browser = getPage(getBrowserKey(i)).getBrowser();
                String browserLabel = getBrowserStreamName(i);
                String browserUsername = USER_BROWSER_PREFIX + i;

                for (int j = 0; j < activeBrowserUsers.length; j++) {
                    String videoElementId = "video-" + getBrowserStreamName(j);
                    verifyVideoInBrowser(browser, browserLabel, browserUsername, videoElementId,
                            activeBrowserUsers[j]);
                }

                if (activeFakeUsers != null) {
                    for (Entry<String, Boolean> fakeEntry : activeFakeUsers.entrySet()) {
                        String videoElementId = "video-" + getFakeStreamName(fakeEntry.getKey());
                        verifyVideoInBrowser(browser, browserLabel, browserUsername, videoElementId,
                                fakeEntry.getValue());
                    }
                }
            }
        }
        long endTime = System.nanoTime();
        double duration = ((double) endTime - startTime) / 1_000_000;
        log.debug("Checked active users: [{}] {} - in {} millis", sb,
                activeFakeUsers != null && !activeFakeUsers.isEmpty() ? "& " + activeFakeUsers : "", duration);
    }

    public void verifyVideoInBrowser(Browser browser, String browserLabel, String chromeName, String videoElementId,
            boolean isActive) {
        if (isActive) {
            log.debug("Verifing element {} exists in browser of {}", videoElementId, chromeName);
            try {
                WebElement video = findElement(browserLabel, browser, videoElementId);
                if (video == null) {
                    fail("Video element " + videoElementId + " was not found in browser of " + chromeName);
                }
            } catch (NoSuchElementException e) {
                fail(e.getMessage());
            }
            log.debug("OK - element {} found in browser of {}", videoElementId, chromeName);
        } else {
            log.debug("Verifing element {} is missing from browser of {}", videoElementId, chromeName);
            try {
                waitWhileElement(browserLabel, browser, videoElementId);
            } catch (TimeoutException e) {
                fail(e.getMessage());
            }
            log.debug("OK - element {} is missing from browser of {}", videoElementId, chromeName);
        }
    }

    public void failWithExceptions() {
        if (!failed && !execExceptions.isEmpty()) {
            failed = true;
            StringBuffer sb = new StringBuffer();
            log.warn("\n+-------------------------------------------------------+\n"
                    + "|   Failing because of the following test errors:       |\n"
                    + "+-------------------------------------------------------+");
            for (String exKey : execExceptions.keySet()) {
                Exception e = execExceptions.get(exKey);
                log.warn("Error on '{}'", exKey, e);
                sb.append(exKey).append(" - ").append(e.getMessage()).append("\n");
            }
            sb.append("Check logs for more details");
            log.warn("\n+-------------------------------------------------------+\n"
                    + "|   End of errors list                                  |\n"
                    + "+-------------------------------------------------------+");
            Assert.fail(sb.toString());
        }
    }
}