Exemplo n.º 1
0
 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()
Exemplo n.º 2
0
 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()
Exemplo n.º 3
0
    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()
Exemplo n.º 4
0
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