Java tutorial
/* * (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()); } } }