def _debug_screenshot_provided(self, save): # type: (bool) -> None prev = self._debug_screenshot_provider if save: self._debug_screenshot_provider = FileDebugScreenshotProvider( prev.prefix, prev.path) else: self._debug_screenshot_provider = NullDebugScreenshotProvider()
def __init__(self): """ Creates a new (possibly disabled) Eyes instance that interacts with the Eyes server. """ super(EyesBase, self).__init__() self._server_connector = ServerConnector() # type: ServerConnector self._user_inputs = [] # type: UserInputs self._debug_screenshot_provider = NullDebugScreenshotProvider()
def _init_providers(self, hard_reset=False): if hard_reset: self._scale_provider = NullScaleProvider() self._position_provider = InvalidPositionProvider() self._cut_provider = NullCutProvider() self._debug_screenshot_provider = NullDebugScreenshotProvider() if self._scale_provider is None: self._scale_provider = NullScaleProvider() if self._position_provider is None: self._position_provider = InvalidPositionProvider() if self._cut_provider is None: self._cut_provider = NullCutProvider()
class EyesBase(EyesConfigurationMixin, _EyesBaseAbstract, ABC): _MAX_ITERATIONS = 10 _running_session = None # type: Optional[RunningSession] _session_start_info = None # type: Optional[SessionStartInfo] _last_screenshot = None # type: Optional[EyesScreenshot] _scale_provider = None # type: Optional[ScaleProvider] _dom_url = None # type: Optional[Text] _position_provider = None # type: Optional[PositionProvider] _is_viewport_size_set = False # type: bool _should_match_once_on_timeout = False # type: bool _is_opened = False # type: bool _render_info = None # type: Optional[RenderingInfo] _render = False _cut_provider = None _should_get_title = False # type: bool _config_cls = Configuration # TODO: make it run with no effect to other pices of code # def set_explicit_viewport_size(self, size): # """ # Define the viewport size as {@code size} without doing any actual action on the # # :param size: The size of the viewport. # """ # if not size: # self.viewport_size = None # self._is_viewport_size_set = False # return None # logger.info("Viewport size explicitly set to {}".format(size)) # self.viewport_size = RectangleSize(size["width"], size["height"]) # self._is_viewport_size_set = True def __init__(self): """ Creates a new (possibly disabled) Eyes instance that interacts with the Eyes server. """ super(EyesBase, self).__init__() self._server_connector = ServerConnector() # type: ServerConnector self._user_inputs = [] # type: UserInputs self._debug_screenshot_provider = NullDebugScreenshotProvider() @property def is_cut_provider_explicitly_set(self): return self._cut_provider and not (isinstance(self._cut_provider, NullCutProvider)) @property def debug_screenshot_provider(self): return self._debug_screenshot_provider @property def cut_provider(self): # type: () -> Union[FixedCutProvider, UnscaledFixedCutProvider, NullCutProvider] return self._cut_provider @cut_provider.setter def cut_provider(self, cutprovider): # type: (Union[FixedCutProvider,UnscaledFixedCutProvider,NullCutProvider])->None argument_guard.is_in( cutprovider, [FixedCutProvider, UnscaledFixedCutProvider, NullCutProvider]) self._cut_provider = cutprovider @property def _debug_screenshot_provided(self): # type: () -> bool """True if screenshots saving enabled.""" return isinstance(self._debug_screenshot_provider, FileDebugScreenshotProvider) @_debug_screenshot_provided.setter def _debug_screenshot_provided(self, save): # type: (bool) -> None prev = self._debug_screenshot_provider if save: self._debug_screenshot_provider = FileDebugScreenshotProvider( prev.prefix, prev.path) else: self._debug_screenshot_provider = NullDebugScreenshotProvider() @property def _environment(self): # type: () -> AppEnvironment """ Application environment is the environment (e.g., the host OS) which runs the application under test. :return: The current application environment. """ app_env = AppEnvironment( os=self.configure.host_os, hosting_app=self.configure.host_app, display_size=self.configure.viewport_size, inferred=self._inferred_environment, ) return app_env @property def scale_ratio(self): # type: () -> float return self._scale_provider.scale_ratio @scale_ratio.setter def scale_ratio(self, scale_ratio): # type: (float) -> None if scale_ratio: self._scale_provider = FixedScaleProvider(scale_ratio) else: self._scale_provider = NullScaleProvider() @property def position_provider(self): # type: () -> PositionProvider return self._position_provider @position_provider.setter def position_provider(self, provider): # type: (PositionProvider) -> None if isinstance(provider, PositionProvider): self._position_provider = provider else: self._position_provider = InvalidPositionProvider() @property def full_agent_id(self): # type: () -> Text """ Gets the agent id, which identifies the current library using the SDK. :return: The agent id. """ if self.configure.agent_id is None: return self.base_agent_id return "{0} [{1}]".format(self.configure.agent_id, self.base_agent_id) @property def agent_setup(self): return None def add_property(self, name, value): # type: (Text, Text) -> None """ Associates a key/value pair with the test. This can be used later for filtering. :param name: (string) The property name. :param value: (string) The property value """ self.configure.properties.append({"name": name, "value": value}) def clear_properties(self): del self.configure.properties[:] @property def is_open(self): # type: () -> bool """ Returns whether the session is currently running. """ return self._is_opened @staticmethod def log_session_results_and_raise_exception(raise_ex, results): logger.info("close(): %s" % results) results_url = results.url scenario_id_or_name = results.name app_id_or_name = results.app_name if results.is_unresolved: if results.is_new: logger.info( "--- New test ended. \n\tPlease approve the new baseline at {}" .format(results_url)) if raise_ex: raise NewTestError(results, scenario_id_or_name, app_id_or_name) else: logger.info( "--- Failed test ended. \n\tSee details at {}".format( results_url)) if raise_ex: raise DiffsFoundError(results, scenario_id_or_name, app_id_or_name) elif results.is_failed: logger.info("--- Failed test ended. \n\tSee details at {}".format( results_url)) if raise_ex: raise TestFailedError(results, scenario_id_or_name, app_id_or_name) # Test passed logger.info( "--- Test passed. \n\tSee details at {}".format(results_url)) 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 abort(self): # type: () -> Optional[TestResults] """ If a test is running, aborts it. Otherwise, does nothing. """ if self.configure.is_disabled: logger.debug("abort(): ignored (disabled)") return try: self._reset_last_screenshot() if self._running_session: results_url = self._running_session.url logger.debug("abort(): Aborting session...") try: logger.info( "--- Test aborted. \n\tSee details at {}".format( results_url)) results = self._server_connector.stop_session( self._running_session, True, False) results.url = results_url return results except EyesError as e: logger.info("Failed to abort server session: %s " % e) pass finally: self._running_session = None finally: logger.close() def abort_if_not_closed(self): logger.deprecation("Use `abort()` instead") self.abort() 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.configure.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.configure.app_name is None: argument_guard.not_none(app_name) self.configure.app_name = app_name argument_guard.not_none(test_name) self.configure.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.configure.failure_reports)) self.configure.session_type = session_type self.configure.viewport_size = viewport_size self._open_base() def _before_open(self): pass def _after_open(self): pass def _init_providers(self, hard_reset=False): if hard_reset: self._scale_provider = NullScaleProvider() self._position_provider = InvalidPositionProvider() self._cut_provider = NullCutProvider() self._debug_screenshot_provider = NullDebugScreenshotProvider() if self._scale_provider is None: self._scale_provider = NullScaleProvider() if self._position_provider is None: self._position_provider = InvalidPositionProvider() if self._cut_provider is None: self._cut_provider = NullCutProvider() 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 _validate_session_open(self): if self.is_open: self.abort() raise EyesError("A test is already running") def _log_open_base(self): logger.debug("Eyes server URL is '{}'".format( self.configure.server_url)) logger.debug("Timeout = {} ms".format(self.configure._timeout)) logger.debug("match_timeout = {} ms".format( self.configure.match_timeout)) logger.debug("Default match settings = '{}' ".format( self.configure.default_match_settings)) logger.debug("FailureReports = '{}' ".format( self.configure.failure_reports)) def _create_session_start_info(self): # type: () -> None self._session_start_info = SessionStartInfo( agent_id=self.full_agent_id, session_type=self.configure.session_type, app_id_or_name=self.configure.app_name, ver_id=None, scenario_id_or_name=self.configure.test_name, batch_info=self.configure.batch, baseline_env_name=self.configure.baseline_env_name, environment_name=self.configure.environment_name, environment=self._environment, default_match_settings=self.configure.default_match_settings, branch_name=self.configure.branch_name, parent_branch_name=self.configure.parent_branch_name, baseline_branch_name=self.configure.baseline_branch_name, save_diffs=self.configure.save_diffs, render=self._render, properties=self.configure.properties, ) def _start_session(self): # type: () -> None logger.debug("_start_session()") self.__ensure_viewport_size() # initialization of Eyes parameters if empty from ENV variables if self.configure.batch is None: logger.info("No Batch set") self.configure.batch = BatchInfo() else: logger.info("Batch is {}".format(self.configure.batch)) self._server_connector.update_config(self.get_configuration()) self._create_session_start_info() # Actually start the session. self._running_session = self._server_connector.start_session( self._session_start_info) self._should_match_once_on_timeout = self._running_session.is_new_session def _reset_last_screenshot(self): # type: () -> None self._last_screenshot = None del self._user_inputs[:] def _ensure_running_session(self): if self._running_session: logger.debug("Session already running.") return logger.debug("No running session, calling start session...") self._start_session() output_provider = AppOutputProvider( self._get_app_output_with_screenshot) self._match_window_task = MatchWindowTask( self._server_connector, self._running_session, self.configure.match_timeout, eyes=self, app_output_provider=output_provider, ) def _get_app_output_with_screenshot(self, region, last_screenshot, check_settings): # type: (Region, EyesScreenshot, CheckSettings) -> AppOutputWithScreenshot logger.info("getting screenshot...") screenshot = self._get_screenshot() logger.info("Done getting screenshot!") if not region.is_size_empty: screenshot = screenshot.sub_screenshot(region) self._debug_screenshot_provider.save(screenshot.image, "sub_screenshot") if not self._dom_url and (self.configure.send_dom or check_settings.values.send_dom): dom_json = self._try_capture_dom() self._dom_url = self._try_post_dom_snapshot(dom_json) logger.info("dom_url: {}".format(self._dom_url)) app_output = AppOutput(title=self._title, screenshot64=None, dom_url=self._dom_url) result = AppOutputWithScreenshot(app_output, screenshot) logger.info("Done") return result def _before_match_window(self): """ Allow to add custom behavior after receiving response from the server """ def _after_match_window(self): """ Allow to add custom behavior before sending data to the server """ def _check_window_base(self, region_provider, tag=None, ignore_mismatch=False, check_settings=None): # type: (RegionProvider, Optional[Text], bool, CheckSettings) -> MatchResult if self.configure.is_disabled: logger.info("check_window(%s): ignored (disabled)" % tag) return MatchResult(as_expected=True) self._ensure_running_session() self._before_match_window() tag = tag if tag is not None else "" result = self._match_window(region_provider, tag, ignore_mismatch, check_settings) self._after_match_window() self._handle_match_result(result, tag) return result def _handle_match_result(self, result, tag): # type: (MatchResult, Text) -> None self._last_screenshot = result.screenshot as_expected = result.as_expected self._user_inputs = [] if not as_expected: self._should_match_once_on_timeout = True if self._running_session and not self._running_session.is_new_session: logger.info("Window mismatch %s" % tag) if self.configure.failure_reports == FailureReports.IMMEDIATE: raise TestFailedError("Mismatch found in '%s' of '%s'" % ( self._session_start_info.scenario_id_or_name, self._session_start_info.app_id_or_name, )) def _try_post_dom_snapshot(self, dom_json): # type: (Text) -> Optional[Text] """ In case DOM data is valid uploads it to the server and return URL where it stored. """ if dom_json is None: return None try: return self._server_connector.post_dom_snapshot(dom_json) except Exception as e: logger.warning( "Couldn't send DOM Json. Passing...\n Got next error: {}". format(e)) return None def _match_window(self, region_provider, tag, ignore_mismatch, check_settings): # type: (RegionProvider, Text, bool, CheckSettings) -> MatchResult # Update retry timeout if it wasn't specified. retry_timeout_ms = -1 # type: Num if check_settings: retry_timeout_ms = check_settings.values.timeout check_settings = self._process_check_settings_values(check_settings) region = region_provider.get_region() logger.debug("params: ([{}], {}, {} ms)".format( region, tag, retry_timeout_ms)) result = self._match_window_task.match_window( self._user_inputs, region, tag, self._should_match_once_on_timeout, ignore_mismatch, check_settings, retry_timeout_ms, ) return result def _process_check_settings_values(self, check_settings): get_config_value = general_utils.use_default_if_none_factory( self.configure.default_match_settings, self.configure) # Set defaults if necessary if check_settings.values.match_level is None: check_settings = check_settings.match_level( get_config_value("match_level")) if check_settings.values.ignore_caret is None: check_settings = check_settings.ignore_caret( get_config_value("ignore_caret")) if check_settings.values.send_dom is None: check_settings = check_settings.send_dom( get_config_value("send_dom")) if check_settings.values.use_dom is None: check_settings = check_settings.use_dom( get_config_value("use_dom")) if check_settings.values.enable_patterns is None: check_settings = check_settings.enable_patterns( get_config_value("enable_patterns")) if check_settings.values.ignore_displacements is None: check_settings = check_settings.ignore_displacements( self.configure.default_match_settings.ignore_displacements) return check_settings def __ensure_viewport_size(self): # type: () -> None """ Assign the viewport size we need to be in the default content frame. """ if not self._is_viewport_size_set: 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: logger.warning("Viewport has not been setup. {}".format(e)) self._is_viewport_size_set = False raise e