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

com.seleniumtests.connectors.selenium.SeleniumRobotSnapshotServerConnector Maven / Gradle / Ivy

There is a newer version: 4.23.18
Show newest version
/**
 * Orignal work: Copyright 2015 www.seleniumtests.com
 * Modified work: Copyright 2016 www.infotel.com
 * 				Copyright 2017-2019 B.Hecquet
 *
 * 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 com.seleniumtests.connectors.selenium;

import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;

import org.openqa.selenium.Rectangle;
import org.testng.ITestResult;

import com.seleniumtests.core.SeleniumTestsContextManager;
import com.seleniumtests.customexception.ConfigurationException;
import com.seleniumtests.customexception.SeleniumRobotServerException;
import com.seleniumtests.driver.BrowserType;
import com.seleniumtests.reporter.logger.Snapshot;
import com.seleniumtests.util.helper.WaitHelper;

import kong.unirest.HttpResponse;
import kong.unirest.MultipartBody;
import kong.unirest.UnirestException;
import kong.unirest.json.JSONException;
import kong.unirest.json.JSONObject;
import rp.com.google.common.io.Files;

public class SeleniumRobotSnapshotServerConnector extends SeleniumRobotServerConnector {
	
	private static final String FIELD_IMAGE = "image";
	private static final String FIELD_IS_OK_WITH_SNAPSHOTS = "isOkWithSnapshots";
	private static final String FIELD_COMPUTING_ERROR = "computingError";
	private static final String FIELD_STEP = "step";
	private static final String FIELD_TEST_STEPS = "testSteps";
	private static final String FIELD_SESSION = "session";
	private static final String FIELD_TEST_CASE = "testCase";
	public static final String SESSION_API_URL = "/snapshot/api/session/";
	public static final String TESTCASEINSESSION_API_URL = "/snapshot/api/testcaseinsession/";
	public static final String TESTSTEP_API_URL = "/snapshot/api/teststep/";
	public static final String STEPRESULT_API_URL = "/snapshot/api/stepresult/";
	public static final String EXCLUDE_API_URL = "/snapshot/api/exclude/";
	public static final String SNAPSHOT_API_URL = "/snapshot/upload/image";
	public static final String STEP_REFERENCE_API_URL = "/snapshot/stepReference/";
	private String sessionUUID;
	private static SeleniumRobotSnapshotServerConnector snapshotConnector;

	protected static final int MAX_TESTSESSION_NAME_LENGHT = 100;
	protected static final int MAX_TESTCASEINSESSION_NAME_LENGHT = 100;
	protected static final int MAX_SNAPSHOT_NAME_LENGHT = 100;
	protected static final int MAX_TESTSTEP_NAME_LENGHT = 100;
	
	public enum SnapshotComparisonResult {
		OK,
		KO,
		NOT_DONE
	}
	
	public static SeleniumRobotSnapshotServerConnector getInstance() {
		if (snapshotConnector == null) {
			snapshotConnector = new SeleniumRobotSnapshotServerConnector(
					SeleniumTestsContextManager.getGlobalContext().getSeleniumRobotServerActive(),
					SeleniumTestsContextManager.getGlobalContext().getSeleniumRobotServerUrl(),
					SeleniumTestsContextManager.getGlobalContext().getSeleniumRobotServerToken()
					);
		} 
		return snapshotConnector;
	}

	public SeleniumRobotSnapshotServerConnector(final boolean useRequested, final String url) {
		this(useRequested, url, null);
	}
	
	public SeleniumRobotSnapshotServerConnector(final boolean useRequested, final String url, String authToken) {
		super(useRequested, url, authToken);
		if (!active) {
			return; 
		}
		active = isAlive();
		if (active) {
			getInfoFromServer();
		}
	}
	
	@Override
	public boolean isAlive() {
		return isAlive("/snapshot/");
	}
	
	/**
	 * Create a test session
	 * @return
	 */
	public Integer createSession(String sessionName) {
		if (!active) {
			return null;
		}
		if (applicationId == null) {
			throw new SeleniumRobotServerException(String.format("Application %s has not been created", SeleniumTestsContextManager.getApplicationName()));
		}
		if (environmentId == null) {
			throw new SeleniumRobotServerException(String.format("Environment %s has not been created", SeleniumTestsContextManager.getGlobalContext().getTestEnv()));
		}
		if (versionId == null) {
			createVersion();
		}
		
		String strippedSessionName = sessionName.length() > MAX_TESTSESSION_NAME_LENGHT ? sessionName.substring(0, MAX_TESTSESSION_NAME_LENGHT): sessionName;
		
		try {
			BrowserType browser = SeleniumTestsContextManager.getGlobalContext().getBrowser();
			browser = browser == null ? BrowserType.NONE : browser;
			sessionUUID = UUID.randomUUID().toString(); // for uniqueness of the session
			
			JSONObject sessionJson = getJSonResponse(buildPostRequest(url + SESSION_API_URL)
					.field("sessionId", sessionUUID)
					.field("date", LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME))
					.field("browser", browser.getBrowserType())
					.field("environment", SeleniumTestsContextManager.getGlobalContext().getTestEnv())
					.field("version", versionId.toString())
					.field(FIELD_NAME, strippedSessionName)
					.field("compareSnapshot", String.valueOf(SeleniumTestsContextManager.getGlobalContext().getSeleniumRobotServerCompareSnapshot()))
					.field("ttl", String.format("%d days", SeleniumTestsContextManager.getGlobalContext().getSeleniumRobotServerCompareSnapshotTtl()))); // format is 'x days' as this is the way Django expect a duration in days
			return sessionJson.getInt("id");
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create session", e);
		}
	}
	
	/**
	 * Create link between test case and session
	 * @param sessionId		the sessionId which should have been created before
	 * @param testCaseId	the test case Id to link to this session
	 * @return	the id of the created testCaseInSession
	 * @deprecated use the same method with name parameter
	 */
	@Deprecated
	public Integer createTestCaseInSession(Integer sessionId, Integer testCaseId) {
		return createTestCaseInSession(sessionId, testCaseId, "");
	}
	
	/**
	 * Create link between test case and session
	 * @param sessionId		the sessionId which should have been created before
	 * @param testCaseId	the test case Id to link to this session
	 * @param name			name of the test case in this session. This is to distinguish the test case (e.g: 'test1') and its full name (e.g: 'test1-1'), when executed with dataprovider
	 * @return	the id of the created testCaseInSession
	 */
	public Integer createTestCaseInSession(Integer sessionId, Integer testCaseId, String name) {
		if (!active) {
			return null;
		}
		if (sessionId == null) {
			throw new ConfigurationException("testcaseInSessionId should not be null");
		}
		if (testCaseId == null) {
			throw new ConfigurationException("Test case must be previously defined");
		}
		
		String strippedName = name.length() > MAX_TESTCASEINSESSION_NAME_LENGHT ? name.substring(0, MAX_TESTCASEINSESSION_NAME_LENGHT): name;
		
		try {
			JSONObject testInSessionJson = getJSonResponse(buildPostRequest(url + TESTCASEINSESSION_API_URL)
					.field(FIELD_TEST_CASE, testCaseId)
					.field(FIELD_SESSION, sessionId.toString())
					.field(FIELD_NAME, strippedName));
			return testInSessionJson.getInt("id");
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create test case", e);
		}
	}
	
	/**
	 * Returns testStepName or a shorter version if it's too long
	 * @return
	 */
	private String getTestStepName(String testStepName) {
		return testStepName.length() > MAX_TESTSTEP_NAME_LENGHT ? testStepName.substring(0, MAX_TESTSTEP_NAME_LENGHT): testStepName;
	}
	

	/**
	 * Create test step and add it to the current test case
	 * @param testStep				name of the test step
	 * @param testCaseInSessionId	id of the test case in session, so that we can add this step to the test case
	 * @return	id of the created teststep
	 */
	public Integer createTestStep(String testStep, Integer testCaseInSessionId) {
		if (!active) {
			return null;
		}
		
		String strippedName = getTestStepName(testStep);
		
		try {
			JSONObject stepJson = getJSonResponse(buildPostRequest(url + TESTSTEP_API_URL)
					.field(FIELD_NAME, strippedName));
			Integer testStepId = stepJson.getInt("id");
			addCurrentTestStepToTestCase(testStepId, testCaseInSessionId);
			return testStepId;
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create test step", e);
		}
	}
	
	/**
	 * Get reference snapshot from server
	 * This is useful when a step fails and we want to get the reference to allow comparison
	 */
	public File getReferenceSnapshot(Integer stepResultId) {
		if (!active) {
			return null;
		}

		checkStepResult(stepResultId);
		
		try {

			File tmpFile = File.createTempFile("img", ".png");
			HttpResponse response = buildGetRequest(url + STEP_REFERENCE_API_URL + stepResultId + "/").asBytes();
			
			
			if (response.getStatus() == 200) {
				Files.write(response.getBody(), tmpFile);
				return tmpFile;
			} else {
				logger.warn("No reference found");
				return null;
			}
			
		} catch (UnirestException | SeleniumRobotServerException | IOException e) {
			throw new SeleniumRobotServerException("cannot get reference snapshot", e);
		}
	}

	/**
	 * @param stepResultId
	 */
	private void checkStepResult(Integer stepResultId) {
		if (stepResultId == null) {
			throw new ConfigurationException("Step result must be previously recorded");
		}
	}
	
	/**
	 * Create snapshot that shows the status of a step
	 */
	public void createStepReferenceSnapshot(Snapshot snapshot, Integer stepResultId) {
		if (!active) {
			return ;
		}

		checkStepResult(stepResultId);
		if (snapshot == null || snapshot.getScreenshot() == null || snapshot.getScreenshot().getFullImagePath() == null) {
			throw new SeleniumRobotServerException("Provided snapshot does not exist");
		}
		
		try {
			File pictureFile = new File(snapshot.getScreenshot().getFullImagePath());
			
			getJSonResponse(buildPostRequest(url + STEP_REFERENCE_API_URL)
					.field("stepResult", stepResultId)
					.field(FIELD_IMAGE, pictureFile)
					
					);
			
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create step reference snapshot", e);
		}
	}
	
	/**
	 * Send snapshot to server, for comparison, and check there is no difference with the reference picture
	 * This method will return true if
	 * - comparison is OK
	 * It returns null if:
	 * - server is inactive
	 * - computing error occurred
	 * - server side error ( e.g: if the server is not up to date)
	 */
	public SnapshotComparisonResult checkSnapshotHasNoDifferences(Snapshot snapshot, String testName, String stepName) {
		
		if (!active) {
			return SnapshotComparisonResult.NOT_DONE;
		}
		if (testName == null) {
			throw new ConfigurationException("testName must not be null");
		}
		if (stepName == null) {
			throw new ConfigurationException("stepName must not be null");
		}
		if (snapshot == null || snapshot.getScreenshot() == null || snapshot.getScreenshot().getFullImagePath() == null) {
			throw new SeleniumRobotServerException("Provided snapshot does not exist");
		}
		
		String snapshotName = snapshot.getName().length() > MAX_SNAPSHOT_NAME_LENGHT ? snapshot.getName().substring(0, MAX_SNAPSHOT_NAME_LENGHT): snapshot.getName(); 
		
		try {
			File pictureFile = new File(snapshot.getScreenshot().getFullImagePath());
			BrowserType browser = SeleniumTestsContextManager.getGlobalContext().getBrowser();
			browser = browser == null ? BrowserType.NONE : browser;
			String strippedTestName = getTestName(testName);
			String strippedStepName = getTestStepName(stepName);
			
			JSONObject snapshotJson = getJSonResponse(buildPutRequest(url + SNAPSHOT_API_URL)
					.socketTimeout(5000)
					.field(FIELD_IMAGE, pictureFile)
					.field(FIELD_NAME, snapshotName)
					.field("compare", snapshot.getCheckSnapshot().getName())
					.field("diffTolerance", String.valueOf(snapshot.getCheckSnapshot().getErrorThreshold()))
					.field("versionId", versionId.toString())
					.field("environmentId", environmentId.toString())
					.field("browser", browser.getBrowserType())
					.field("testCaseName", strippedTestName)
					.field("stepName", strippedStepName)
					);
			
			if (snapshotJson != null) {
				String computingError = snapshotJson.getString(FIELD_COMPUTING_ERROR);
				Float diffPixelPercentage = snapshotJson.getFloat("diffPixelPercentage");
				Boolean tooManyDiffs = snapshotJson.getBoolean("tooManyDiffs");
				
				if (!computingError.isEmpty()) {
					return SnapshotComparisonResult.NOT_DONE;
				} else if (Boolean.TRUE.equals(tooManyDiffs)) {
					logger.error(String.format("Snapshot comparison for %s has a difference of %.2f%% with reference", snapshot.getName(), diffPixelPercentage));
					return SnapshotComparisonResult.KO;
				} else {
					return SnapshotComparisonResult.OK;
				}
			} else {
				return SnapshotComparisonResult.NOT_DONE;
			}
			
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			// in case selenium server is not up to date, we shall not raise an error / retry
			logger.error("cannot send snapshot to server", e);
			return SnapshotComparisonResult.NOT_DONE;
		}
		
		
		
	}
	
	/**
	 * Create snapshot on server that will be used to show differences between 2 versions of the application
	 */
	public Integer createSnapshot(Snapshot snapshot, Integer sessionId, Integer testCaseInSessionId, Integer stepResultId) {
		if (!active) {
			return null;
		}
		if (sessionId == null) {
			throw new ConfigurationException("Session must be previously recorded");
		}
		if (testCaseInSessionId == null) {
			throw new ConfigurationException("TestCaseInSession must be previously recorded");
		}
		checkStepResult(stepResultId);
		if (snapshot == null || snapshot.getScreenshot() == null || snapshot.getScreenshot().getFullImagePath() == null) {
			throw new SeleniumRobotServerException("Provided snapshot does not exist");
		}
		
		String snapshotName = snapshot.getName().length() > MAX_SNAPSHOT_NAME_LENGHT ? snapshot.getName().substring(0, MAX_SNAPSHOT_NAME_LENGHT): snapshot.getName(); 
		
		try {
			File pictureFile = new File(snapshot.getScreenshot().getFullImagePath());
			
			JSONObject snapshotJson = getJSonResponse(buildPostRequest(url + SNAPSHOT_API_URL)
					.field("stepResult", stepResultId)
					.field("sessionId", sessionUUID)
					.field(FIELD_TEST_CASE, testCaseInSessionId.toString())
					.field(FIELD_IMAGE, pictureFile)
					.field(FIELD_NAME, snapshotName)
					.field("compare", snapshot.getCheckSnapshot().getName())
					.field("diffTolerance", String.valueOf(snapshot.getCheckSnapshot().getErrorThreshold()))
					);
			return snapshotJson.getInt("id");
			
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create test snapshot", e);
		}
	}
	
	/**
	 * Send exclude zones, stored in snapshot to the server
	 */
	public Integer createExcludeZones(Rectangle excludeZone, Integer snapshotId) {
		if (!active) {
			return null;
		}
		if (snapshotId == null) {
			throw new ConfigurationException("snapshotId must be provided");
		}

		try {
			JSONObject excludeJson = getJSonResponse(buildPostRequest(url + EXCLUDE_API_URL)
					.field("snapshot", snapshotId)
					.field("x", String.valueOf(excludeZone.x))
					.field("y", String.valueOf(excludeZone.y))
					.field("width", String.valueOf(excludeZone.width))
					.field("height", String.valueOf(excludeZone.height))
					);
			return excludeJson.getInt("id");
			
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create exclude zone", e);
		}
	}
	
	/**
	 * Record step result
	 * @param result	step result (true of false)
	 * @param logs		step details
	 * @param duration	step duration in milliseconds
	 * @return the stepResult id stored on server
	 */
	public Integer recordStepResult(Boolean result, String logs, long duration, Integer sessionId, Integer testCaseInSessionId, Integer testStepId) {
		if (!active) {
			return null;
		}
		if (sessionId == null) {
			throw new ConfigurationException("Test session must be previously defined");
		}
		if (testCaseInSessionId == null) {
			throw new ConfigurationException("TestCaseInSession must be previously defined");
		}
		if (testStepId == null) {
			throw new ConfigurationException("Test step and test case in session must be previously defined");
		}
		try {
			JSONObject resultJson = getJSonResponse(buildPostRequest(url + STEPRESULT_API_URL)
					.field(FIELD_STEP, testStepId)
					.field(FIELD_TEST_CASE, testCaseInSessionId.toString())
					.field("result", result.toString())
					.field("duration", String.valueOf(duration))
					.field("stacktrace", logs)
					);
			return resultJson.getInt("id");
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot create test snapshot", e);
		}
	}

	/**
	 * Returns list of test steps in a test case
	 * @return
	 */
	@SuppressWarnings("unchecked")
	public List getStepListFromTestCase(Integer testCaseInSessionId) {
		if (testCaseInSessionId == null) {
			return new ArrayList<>();
		}
		
		try {

			JSONObject sessionJson = getJSonResponse(buildGetRequest(url + TESTCASEINSESSION_API_URL + testCaseInSessionId));
			return (List) sessionJson.getJSONArray(FIELD_TEST_STEPS)
					.toList()
					.stream()
					.map(Object::toString)
					.collect(Collectors.toList());

		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot get test step list", e);
		}
	}
	
	/**
	 * Add the current test case (should have been previously created) to this test session
	 */
	public void addCurrentTestStepToTestCase(Integer testStepId, Integer testCaseInSessionId) {
		if (testStepId == null || testCaseInSessionId == null) {
			throw new ConfigurationException("Test step and Test case in session must be previously created");
		}
		
		try {
			// get list of tests associated to this session
			List testSteps = getStepListFromTestCase(testCaseInSessionId);
			if (!testSteps.contains(testStepId.toString())) {
				testSteps.add(testStepId.toString());
			}
			addTestStepsToTestCases(testSteps, testCaseInSessionId);
			
		} catch (UnirestException | JSONException | SeleniumRobotServerException e) {
			throw new SeleniumRobotServerException("cannot add test step to test case", e);
		}
	}
	
	public JSONObject addTestStepsToTestCases(List testSteps, Integer testCaseInSessionId)  {
		if (testSteps.isEmpty()) {
			return new JSONObject();
		}
		
		MultipartBody request = buildPatchRequest(url + TESTCASEINSESSION_API_URL + testCaseInSessionId + "/").field(FIELD_TEST_STEPS, testSteps.get(0));
		for (String tc: testSteps.subList(1, testSteps.size())) {
			request = request.field(FIELD_TEST_STEPS, tc);
		}
		return getJSonResponse(request);
	}
	
	public void addLogsToTestCaseInSession(Integer testCaseInSessionId, String logs) {
		if (testCaseInSessionId == null) {
			throw new ConfigurationException("testcaseInSessionId should not be null");
		}
		
		try {
			getJSonResponse(buildPatchRequest(url + TESTCASEINSESSION_API_URL + testCaseInSessionId + "/").field("stacktrace", logs));
		} catch (UnirestException e) {
			throw new SeleniumRobotServerException("cannot add logs to test case", e);
		}
	}
	
	public String getSessionUUID() {
		return sessionUUID;
	}
	
	/**
	 * Get the comparison result of snapshots. If we cannot get the information, return true
	 * @param testCaseInSessionId		id of the test case in this test sessions.
	 * @param errorMessage				the messages coming from server if some comparison error occurred (error during computing). 
	 * 									An empty errorMessage means all snapshots have been processed whatever the comparison result is
	 * @return							integer with test result. Values are the one from ITestResult
	 */
	public int getTestCaseInSessionComparisonResult(Integer testCaseInSessionId, StringBuilder errorMessage) {
		
		logger.info("Getting snapshot comparison result for " + testCaseInSessionId);
		try {
			JSONObject response = null;
			for (int i = 0; i < 5; i++) {
				response = getJSonResponse(buildGetRequest(url + TESTCASEINSESSION_API_URL + testCaseInSessionId));
				
				// 'isOkWithSnapshots' can take 3 values
				// - 'true' if all comparison are OK
				// - 'false' if at least 1 comparison fails
				// - 'null' if all comparison failed to be computed (e.g: due to bug on server) or at least one comparison is OK but all others are not computed
				// So if no computing error is returned, having 'null' means that no snapshot has been sent to server (according to 'snapshotServer/models.py' code)
				if (response.optBoolean("computed", false) && response.has(FIELD_IS_OK_WITH_SNAPSHOTS)) {
					return displaySnapshotComparisonError(response, errorMessage);
					
				} else {
					WaitHelper.waitForSeconds(1);
				}
			}
			if (response != null) {
				logger.info("Comparison result took too long to compute");
				return displaySnapshotComparisonError(response, errorMessage);
			} else {
				logger.error("Comparison result is null, setting to 'true'");
				return ITestResult.SKIP;
			}
			
		} catch (UnirestException e) {
			logger.error("Cannot get comparison result for this test case", e);
			return ITestResult.SKIP;
		}
		
	}
	
	private int displaySnapshotComparisonError(JSONObject response, StringBuilder errorMessage) {
		if (!response.optBoolean(FIELD_IS_OK_WITH_SNAPSHOTS, false) 
				&& response.optJSONArray(FIELD_COMPUTING_ERROR) != null 
				&& !response.optJSONArray(FIELD_COMPUTING_ERROR).isEmpty()) {
			logger.error("Errors while computing snapshot comparisons: \n" + response.optJSONArray(FIELD_COMPUTING_ERROR).join("\n"));
			errorMessage.append(response.optJSONArray(FIELD_COMPUTING_ERROR).join("\n"));
		}
		
		if (response.has(FIELD_IS_OK_WITH_SNAPSHOTS)) {
			if (response.isNull(FIELD_IS_OK_WITH_SNAPSHOTS)) {
				return ITestResult.SKIP;
			} else {
				return response.getBoolean(FIELD_IS_OK_WITH_SNAPSHOTS) ? ITestResult.SUCCESS: ITestResult.FAILURE;
			}
		} else {
			return ITestResult.SKIP;
		}
	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy