def _long_request_check_status(self, response, request_id): if ( response.status_code == requests.codes.ok or "Location" not in response.headers ): # request ends successful or it doesn't support Long request return response elif response.status_code == requests.codes.accepted: # long request here; calling received url to know that request was processed url = response.headers["Location"] response = self._long_request_loop(url, request_id) return self._long_request_check_status(response, request_id) elif response.status_code == requests.codes.created: # delete url that was used before url = response.headers["Location"] return self.request( "delete", url, request_id=request_id, headers={"Eyes-Date": datetime_utils.current_time_in_rfc1123()}, ) elif response.status_code == requests.codes.gone: raise EyesError("The server task has gone.") else: raise EyesError("Unknown error during long request: {}".format(response))
def render_status_by_id(self, *render_ids): # type: (*Text) -> List[RenderStatusResults] argument_guard.not_none(render_ids) if self._render_info is None: raise EyesError("render_info must be fetched first") headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/json" headers["X-Auth-Token"] = self._render_info.access_token url = urljoin(self._render_info.service_url, self.RENDER_STATUS) response = self._com.request( requests.post, url, use_api_key=False, headers=headers, data=json.dumps(render_ids), ) if not response.ok: raise EyesError( "Error getting server status, {} {}".format( response.status_code, response.content ) ) # TODO: improve parser to handle similar names return json_utils.attr_from_response(response, RenderStatusResults)
def render_put_resource(self, running_render, resource): # type: (RunningRender, VGResource) -> Text argument_guard.not_none(running_render) argument_guard.not_none(resource) if self._render_info is None: raise EyesError("render_info must be fetched first") content = resource.content argument_guard.not_none(content) logger.debug("resource hash: {} url: {} render id: {}" "".format(resource.hash, resource.url, running_render.render_id)) headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = resource.content_type headers["X-Auth-Token"] = self._render_info.access_token url = urljoin(self._render_info.service_url, self.RESOURCES_SHA_256 + resource.hash) response = self._com.request( requests.put, url, use_api_key=False, headers=headers, data=content, params={"render-id": running_render.render_id}, ) logger.debug("ServerConnector.put_resource - request succeeded") if not response.ok: raise EyesError("Error putting resource: {}, {}".format( response.status_code, response.content)) return resource.hash
def stop_session(self, running_session, is_aborted, save): # type: (RunningSession, bool, bool) -> TestResults """ Stops a running session in the Eyes server. :param running_session: The session to stop. :param is_aborted: Whether the server should mark this session as aborted. :param save: Whether the session should be automatically saved if it is not aborted. :return: Test results of the stopped session. """ logger.debug("stop_session called.") if not self.is_session_started: raise EyesError("Session not started") params = {"aborted": is_aborted, "updateBaseline": save} response = self._com.long_request( requests.delete, url_resource=urljoin(self.API_SESSIONS_RUNNING, running_session.id), params=params, headers=ServerConnector.DEFAULT_HEADERS, ) test_results = json_utils.attr_from_response(response, TestResults) logger.debug("stop_session(): parsed response: {}".format(test_results)) # mark that session isn't started self._is_session_started = False return test_results
def match_window(self, running_session, match_data): # type: (RunningSession, MatchWindowData) -> MatchResult """ Matches the current window to the immediate expected window in the Eyes server. Notice that a window might be matched later at the end of the test, even if it was not immediately matched in this call. :param running_session: The current session that is running. :param match_data: The data for the requests.post. :return: The parsed response. """ logger.debug("match_window called. {}".format(running_session)) # logger.debug("Data length: %d, data: %s" % (len(data), repr(data))) if not self.is_session_started: raise EyesError("Session not started") data = prepare_match_data(match_data) # Using the default headers, but modifying the "content type" to binary headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/octet-stream" # TODO: allow to send images as base64 response = self._com.long_request( requests.post, url_resource=urljoin(self.API_SESSIONS_RUNNING, running_session.id), data=data, headers=headers, ) match_result = json_utils.attr_from_response(response, MatchResult) return match_result
def post_dom_snapshot(self, dom_json): # type: (Text) -> Optional[Text] """ Upload the DOM of the tested page. Return an URL of uploaded resource which should be posted to :py: `AppOutput`. """ logger.debug("post_dom_snapshot called.") if not self.is_session_started: raise EyesError("Session not started") headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/octet-stream" dom_bytes = gzip_compress(dom_json.encode("utf-8")) response = self._com.request( requests.post, url_resource=urljoin(self.API_SESSIONS_RUNNING, "data"), data=dom_bytes, headers=headers, ) dom_url = None if response.ok: dom_url = response.headers["Location"] return dom_url
def _open_base(self): if self.configure.is_disabled: logger.debug("open_base(): ignored (disabled)") return logger.open_() self._log_open_base() retry = 0 while retry < self._MAX_ITERATIONS: try: self._validate_session_open() self._init_providers() self._is_viewport_size_set = False self._before_open() try: if self.configure.viewport_size: self._ensure_running_session() except Exception as e: logger.exception(e) retry += 1 continue self._is_opened = True self._after_open() return None except EyesError as e: logger.exception(e) logger.close() raise e raise EyesError("eyes.open_base() failed")
def _upload_data( self, data_bytes, rendering_info, target_url, content_type, media_type ): # type: (bytes, RenderingInfo, Text, Text, Text) -> bool headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = content_type headers["Content-Length"] = str(len(data_bytes)) headers["Media-Type"] = media_type headers["X-Auth-Token"] = rendering_info.access_token headers["x-ms-blob-type"] = "BlockBlob" timeout_sec = datetime_utils.to_sec(self._com.timeout_ms) response = self.client_session.request( "put", target_url, data=data_bytes, headers=headers, timeout=timeout_sec, verify=False, ) if response.status_code in [requests.codes.ok, requests.codes.created]: logger.debug("Upload Status Code: {}".format(response.status_code)) return True raise EyesError( "Failed to Upload. Status Code: {}".format(response.status_code) )
def update_config(self, conf): if conf.api_key is None: raise EyesError( "API key not set! Log in to https://applitools.com to obtain your" " API Key and use 'api_key' to set it.") self._com.server_url = conf.server_url self._com.api_key = conf.api_key self._com.timeout_ms = conf._timeout
def get_image_part(image, region): # type: (Image.Image, Region) -> Image.Image """ Get a copy of the part of the image given by region. :return: The part of the image. """ if region.is_empty: raise EyesError("region is empty!") return image.crop(box=(region.left, region.top, region.right, region.bottom))
def match_window(self, running_session, match_data): # type: (RunningSession, MatchWindowData) -> MatchResult """ Matches the current window to the immediate expected window in the Eyes server. Notice that a window might be matched later at the end of the test, even if it was not immediately matched in this call. :param running_session: The current session that is running. :param match_data: The data for the requests.post. :return: The parsed response. """ logger.debug("match_window called. {}".format(running_session)) # logger.debug("Data length: %d, data: %s" % (len(data), repr(data))) if not self.is_session_started: raise EyesError("Session not started") app_output = match_data.app_output # when screenshot_url is present we don't need to upload again if app_output.screenshot_url is None and app_output.screenshot_bytes: app_output.screenshot_url = self.try_upload_image( match_data.app_output.screenshot_bytes) if app_output.screenshot_url is None: raise EyesError( "MatchWindow failed: could not upload image to storage service." ) logger.info("Screenshot image URL: {}".format( app_output.screenshot_url)) data = json_utils.to_json(match_data) headers = ServerConnector.DEFAULT_HEADERS.copy() response = self._com.long_request( "post", url_resource=urljoin(self.API_SESSIONS_RUNNING, running_session.id), data=data, headers=headers, ) return json_utils.attr_from_response(response, MatchResult)
def post_dom_capture(self, dom_json): # type: (Text) -> Optional[Text] """ Upload the DOM of the tested page. Return an URL of uploaded resource which should be posted to :py: `AppOutput`. """ logger.debug("post_dom_snapshot called.") if not self.is_session_started: raise EyesError("Session not started") dom_bytes = gzip_compress(dom_json.encode("utf-8")) return self._try_upload_data(dom_bytes, "application/octet-stream", "application/json")
def update_config(self, conf, full_agent_id, render_info=None, ua_string=None): if conf.api_key is None: raise EyesError( "API key not set! Log in to https://applitools.com to obtain your" " API Key and use 'api_key' to set it." ) self._com.server_url = conf.server_url self._com.api_key = conf.api_key self._com.timeout_ms = conf._timeout if render_info: self._render_info = render_info if ua_string: self._ua_string = ua_string self.DEFAULT_HEADERS["x-applitools-eyes-client"] = full_agent_id
def render_info(self): # type: () -> Optional[RenderingInfo] logger.debug("render_info() called.") headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/json" response = self._com.long_request("get", self.RENDER_INFO_PATH, headers=headers) if not response.ok: raise EyesError( "Cannot get render info: \n Status: {}, Content: {}".format( response.status_code, response.content ) ) self._render_info = json_utils.attr_from_response(response, RenderingInfo) return self._render_info
def render(self, *render_requests): # type: (*RenderRequest) -> List[RunningRender] logger.debug("render called with {}".format(render_requests)) if self._render_info is None: raise EyesError("render_info must be fetched first") url = urljoin(self._render_info.service_url, self.RENDER) headers = ServerConnector.DEFAULT_HEADERS.copy() headers["Content-Type"] = "application/json" headers["X-Auth-Token"] = self._render_info.access_token data = json_utils.to_json(render_requests) response = self._com.request( requests.post, url, use_api_key=False, headers=headers, data=data ) if response.ok or response.status_code == requests.codes.not_found: return json_utils.attr_from_response(response, RunningRender) raise EyesError( "ServerConnector.render - unexpected status ({})\n\tcontent{}".format( response.status_code, response.content ) )
def close(self, raise_ex=True): # type: (bool) -> Optional[TestResults] """ Ends the test. :param raise_ex: If true, an exception will be raised for failed/new tests. :return: The test results. """ if self.configure.is_disabled: logger.debug("close(): ignored (disabled)") return None try: logger.debug("close({})".format(raise_ex)) if not self._is_opened: raise EyesError("Eyes not open") self._is_opened = False self._reset_last_screenshot() self._init_providers(hard_reset=True) # If there's no running session, we simply return the default test results. if not self._running_session: logger.debug("close(): Server session was not started") logger.info("close(): --- Empty test ended.") return TestResults() is_new_session = self._running_session.is_new_session results_url = self._running_session.url logger.info("close(): Ending server session...") should_save = (is_new_session and self.configure.save_new_tests) or ( (not is_new_session) and self.configure.save_failed_tests) logger.debug("close(): automatically save session? %s" % should_save) results = self._server_connector.stop_session( self._running_session, False, should_save) results.is_new = is_new_session results.url = results_url self.log_session_results_and_raise_exception(raise_ex, results) return results finally: self._running_session = None logger.close()
def open_base( self, app_name, # type: Text test_name, # type: Text viewport_size=None, # type: Optional[ViewPort] session_type=SessionType.SEQUENTIAL, # type: SessionType ): # type: (...) -> None """ Starts a test. :param app_name: The name of the application under test. :param test_name: The test name. :param viewport_size: The client's viewport size (i.e., the visible part of the document's body) or None to allow any viewport size. :param session_type: The type of test (e.g., Progression for timing tests) or Sequential by default. :return: An updated web driver :raise EyesError: If the session was already open. """ logger.open_() if self.configuration.is_disabled: logger.debug("open_base(): ignored (disabled)") return if self._server_connector is None: raise EyesError("Server connector not set.") # If there's no default application name, one must be provided for the current test. if self.configuration.app_name is None: argument_guard.not_none(app_name) self.configuration.app_name = app_name argument_guard.not_none(test_name) self.configuration.test_name = test_name logger.info("\nAgent: {}\n".format(self.full_agent_id)) logger.info( "open_base(%s, %s, %s, %s)" % (app_name, test_name, viewport_size, self.configuration.failure_reports) ) self.configuration.session_type = session_type self.configuration.viewport_size = viewport_size self._open_base()
def __ensure_viewport_size(self): # type: () -> None """ Assign the viewport size we need to be in the default content frame. """ if self._is_viewport_size_set: return try: if self.configure.viewport_size is None: # TODO: ignore if viewport_size settled explicitly target_size = self._get_viewport_size() self.configure.viewport_size = target_size else: target_size = self.configure.viewport_size self._set_viewport_size(target_size) self._is_viewport_size_set = True except Exception as e: self._is_viewport_size_set = False raise_from(EyesError("Viewport has not been setup"), e)
def get_text_in_running_session_image(self, data): # type: (TextSettingsData) -> List[Text] logger.debug( "call", _class=self.__class__.__name__, _method="extract_text", text_region_data=data, ) resp = self._com.long_request( "post", urljoin(self.API_SESSIONS_RUNNING, "images/text"), data=json_utils.to_json(data), ) if resp.ok: return resp.json() raise EyesError( "ServerConnector.extract_text - unexpected status {}".format( resp.status_code ) )
def get_text_regions_in_running_session_image(self, data): # type: (TextSettingsData) -> PATTERN_TEXT_REGIONS logger.debug( "call", _class=self.__class__.__name__, _method="extract_text_regions", text_region_data=data, ) resp = self._com.long_request( "post", urljoin(self.API_SESSIONS_RUNNING, "images/textregions"), data=json_utils.to_json(data), ) if resp.ok: return { pattern: json_utils.attr_from_dict(regions, TextRegion) for pattern, regions in iteritems(resp.json()) } raise EyesError( "ServerConnector.extract_text_regions - unexpected status {}".format( resp.status_code ) )
def limit_parallel_retries(self, method): deadline = time() + self._max_retry_time with self._condvar: while self._retrying: self._condvar.wait() try: return method() except EyesServiceUnavailableError: with self._condvar: while self._retrying: self._condvar.wait() self._retrying = True try: while True: try: return method() except EyesServiceUnavailableError: if time() > deadline: raise EyesError("Session opening timeout reached") sleep(self._sleep_time) finally: with self._condvar: self._retrying = False self._condvar.notify_all()
def _validate_session_open(self): if self.is_open: self.abort() raise EyesError("A test is already running")
def __setstate__(self, state): raise EyesError("Cannot create MouseTrigger instance from dict!")
def try_upload_image(self, data): # type: (bytes) -> Optional[Text] try: return self._try_upload_data(data, "image/png", "image/png") except EyesError as e: raise_from(EyesError("Failed to Upload Image"), e)