Esempio n. 1
0
 def save_debug_screenshots(self, save):
     # type: (bool) -> None
     prev = self._debug_screenshots_provider
     if save:
         self._debug_screenshots_provider = FileDebugScreenshotsProvider(
             prev.prefix, prev.path)
     else:
         self._debug_screenshots_provider = NullDebugScreenshotsProvider()
Esempio 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_screenshots_provider = NullDebugScreenshotsProvider()
Esempio 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_screenshots_provider = NullDebugScreenshotsProvider()

        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()
Esempio n. 4
0
class EyesBase(EyesConfigurationMixin, DebugScreenshotsAbstract,
               _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_screenshots_provider = NullDebugScreenshotsProvider()

    @property
    def match_level(self):
        # type: () -> MatchLevel
        return self.configure.match_level

    @match_level.setter
    def match_level(self, match_level):
        # type: (MatchLevel) -> None
        self.configure.match_level = match_level

    @property
    def is_cut_provider_explicitly_set(self):
        return self._cut_provider and not (isinstance(self._cut_provider,
                                                      NullCutProvider))

    @property
    def server_connector(self):
        # type: () -> ServerConnector
        return self._server_connector

    @server_connector.setter
    def server_connector(self, server_connector):
        # type: (ServerConnector) -> None
        argument_guard.is_a(server_connector, ServerConnector)
        self._server_connector = server_connector

    @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_screenshots_provider(self):
        return self._debug_screenshots_provider

    @property
    def save_debug_screenshots(self):
        # type: () -> bool
        """True if screenshots saving enabled."""
        return isinstance(self._debug_screenshots_provider,
                          FileDebugScreenshotsProvider)

    @save_debug_screenshots.setter
    def save_debug_screenshots(self, save):
        # type: (bool) -> None
        prev = self._debug_screenshots_provider
        if save:
            self._debug_screenshots_provider = FileDebugScreenshotsProvider(
                prev.prefix, prev.path)
        else:
            self._debug_screenshots_provider = NullDebugScreenshotsProvider()

    @property
    def debug_screenshots_path(self):
        # type: () -> Optional[Text]
        return self._debug_screenshots_provider.path

    @debug_screenshots_path.setter
    def debug_screenshots_path(self, path_to_save):
        # type: (Text) -> None
        self._debug_screenshots_provider.path = path_to_save

    @property
    def debug_screenshots_prefix(self):
        # type: () -> Optional[Text]
        return self._debug_screenshots_provider.prefix

    @debug_screenshots_prefix.setter
    def debug_screenshots_prefix(self, prefix):
        # type: (Text) -> None
        self._debug_screenshots_provider.prefix = prefix

    @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):
        # TODO: Implement agent_setup
        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.add_property(name, value)

    def clear_properties(self):
        """
        Clears the list of custom properties.
        """
        self.configure.clear_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({}): {}".format(raise_ex, 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(
                    "--- Differences are found. \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)
        else:
            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(status="Failed")

            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.info("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
        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)
            finally:
                self._running_session = None

    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_screenshots_provider = NullDebugScreenshotsProvider()

        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.info("Running on: {} {} {}".format(
            platform.platform(),
            platform.python_implementation(),
            platform.python_version(),
        ))
        logger.info("Eyes server URL is '{}'".format(
            self.configure.server_url))
        logger.info("Timeout = {} ms".format(self.configure._timeout))
        logger.debug("match_timeout = {} ms".format(
            self.configure.match_timeout))
        logger.info("Default match settings = '{}' ".format(
            self.configure.default_match_settings))
        logger.info("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
        logger.info("Batch is {}".format(self.configure.batch))

        self._server_connector.update_config(self.get_configuration(),
                                             self.full_agent_id)
        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_screenshots_provider.save(screenshot.image,
                                                  "sub_screenshot")

        if not self._dom_url and (self.configure.send_dom
                                  or check_settings.values.send_dom):
            logger.info("Capturing DOM")
            dom_json = self._try_capture_dom()
            self._dom_url = self._try_post_dom_capture(dom_json)
            logger.info("Captured DOM URL: {}".format(self._dom_url))

        app_output = AppOutput(title=self._title,
                               screenshot_bytes=None,
                               dom_url=self._dom_url)
        result = AppOutputWithScreenshot(app_output, screenshot)
        logger.info("Done getting screenshot and DOM!")
        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,  # type: RegionProvider
            tag=None,  # type: Optional[Text]
            ignore_mismatch=False,  # type: bool
            check_settings=None,  # type: CheckSettings
            source=None,  # type: Optional[Text]
    ):
        # type: (...) -> 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, check_settings,
                                    source)

        if not ignore_mismatch:
            del self._user_inputs[:]
            self._last_screenshot = result.screenshot

        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_capture(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_capture(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, check_settings, source):
        # type: (RegionProvider, Text, CheckSettings, Optional[Text]) -> 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

        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,
            check_settings,
            retry_timeout_ms,
            source,
        )
        return result

    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)