예제 #1
0
    def create_addon_locator(by: By, by_value: str):
        """Creates and returns an locator used in an addon based on a locator strategy

        Args:
            by (By): The element locator strategy to be used
            by_value (str): The associated element locator strategy value

        Returns:
            dict: object representing the element locator strategy to use in the addon
        """

        if by == By.ID:
            return {"id": by_value}
        elif by == By.NAME:
            return {"name": by_value}
        elif by == By.XPATH:
            return {"xpath", by_value}
        elif by == By.CLASS_NAME:
            return {"className": by_value}
        elif by == By.CSS_SELECTOR:
            return {"cssSelector": by_value}
        elif by == By.LINK_TEXT:
            return {"linkText": by_value}
        elif by == By.PARTIAL_LINK_TEXT:
            return {"partialLinkText": by_value}
        elif by == By.TAG_NAME:
            return {"tagName": by_value}
        else:
            raise SdkException(f"Did not recognize locator strategy {by}")
예제 #2
0
def get_sdk_version() -> str:
    """Returns the current SDK version

    Returns:
        str: The current SDK version read from package metadata or an environment variable
    """

    version = None

    try:
        sdk_metadata = metadata('testproject-python-sdk')
        version = sdk_metadata['Version']
    except PackageNotFoundError:
        # This is OK, it just means that there's no previously installed version available
        pass

    logging.debug(f"Version read from package metadata: {version}")

    if version is None:
        # we're not dealing with an installed package, build uses an environment variable
        version = os.environ.get("TP_SDK_VERSION")
        if version is None:
            raise SdkException(
                "No SDK version definition found in metadata or environment variable"
            )

        logging.debug(f"Version read from environment variable: {version}")

    return version
예제 #3
0
    def __handle_new_session_error(self, response: OperationResult):
        """ Handles errors occurring on creation of a new session with the Agent

        Args:
            response (OperationResult): response from the Agent
        """
        if response.status_code == HTTPStatus.UNAUTHORIZED:
            logging.error(
                "Failed to initialize a session with the Agent - invalid developer token supplied"
            )
            logging.error(
                "Get your developer token from https://app.testproject.io/#/integrations/sdk?lang=Python"
                " and set it in the TP_DEV_TOKEN environment variable")
            raise InvalidTokenException(response.message)
        elif response.status_code == HTTPStatus.NOT_FOUND:
            error_message = response.message if response.message else "Failed to start a new session!"
            raise SdkException(error_message)
        elif response.status_code == HTTPStatus.NOT_ACCEPTABLE:
            logging.error(
                f"Failed to initialize a session with the Agent - obsolete SDK version {ConfigHelper.get_sdk_version()}"
            )
            raise ObsoleteVersionException(response.message)
        else:
            logging.error("Failed to initialize a session with the Agent")
            raise AgentConnectException(
                f"Agent responded with HTTP status {response.status_code}: [{response.message}]"
            )
예제 #4
0
    def create_search_criteria(by: By, by_value: str):
        """Translator method to create element search criteria to send to the Agent

        Args:
            by (By): The element locator strategy to be used
            by_value (str): The associated element locator strategy value

        Returns:
            ElementSearchCriteria: object representing the element search criteria
        """

        if by == By.ID:
            return ElementSearchCriteria(FindByType.ID, by_value)
        elif by == By.NAME:
            return ElementSearchCriteria(FindByType.NAME, by_value)
        elif by == By.XPATH:
            return ElementSearchCriteria(FindByType.XPATH, by_value)
        elif by == By.CLASS_NAME:
            return ElementSearchCriteria(FindByType.CLASSNAME, by_value)
        elif by == By.CSS_SELECTOR:
            return ElementSearchCriteria(FindByType.CSSSELECTOR, by_value)
        elif by == By.LINK_TEXT:
            return ElementSearchCriteria(FindByType.LINKTEXT, by_value)
        elif by == By.PARTIAL_LINK_TEXT:
            return ElementSearchCriteria(FindByType.PARTIALLINKTEXT, by_value)
        elif by == By.TAG_NAME:
            return ElementSearchCriteria(FindByType.TAG_NAME, by_value)
        else:
            raise SdkException(f"Did not recognize locator strategy {by}")
def get_active_driver_instance():
    """Get the current driver instance in use (BaseDriver, Remote or Generic) """
    # Get the first driver instance that exists (not None) in the list of possible driver instances.
    driver = next((_driver for _driver in [BaseDriver.instance(), Remote.instance(), Generic.instance()] if _driver
                  is not None), None)
    if driver is None:
        raise SdkException("No active driver instance found for reporting")
    return driver
예제 #6
0
    def send_request(self,
                     method,
                     path,
                     body=None,
                     params=None) -> OperationResult:
        """Sends HTTP request to Agent

        Args:
            method (str): HTTP method (GET, POST, ...)
            path (str): Relative API route path
            body (dict): Request body
            params (dict): Request parameters

        Returns:
            OperationResult: contains result of the sent request
        """
        with requests.Session() as session:
            if params:
                session.params = params
            if method == "GET":
                response = session.get(path,
                                       headers={"Authorization": self._token})
            elif method == "POST":
                response = session.post(path,
                                        headers={"Authorization": self._token},
                                        json=body)
            elif method == "DELETE":
                response = session.delete(
                    path, headers={"Authorization": self._token})
            elif method == "PUT":
                response = session.put(path,
                                       headers={"Authorization": self._token},
                                       json=body)
            else:
                raise SdkException(
                    f"Unsupported HTTP method {method} in send_request()")

        response_json = {}
        # For some successful calls, the response body will be empty
        # Parsing it results in a ValueError, so we should handle this
        try:
            response_json = response.json()
        except ValueError:
            pass

        # Handling any HTTPError exceptions.
        try:
            response.raise_for_status()
            return OperationResult(True, response.status_code, "",
                                   response_json)
        except HTTPError as http_error:
            return OperationResult(
                False,
                response.status_code,
                response_json.get("message", str(http_error)),
                response_json if response_json else None,
            )
예제 #7
0
    def __init__(
        self,
        token: str = None,
        projectname: str = None,
        jobname: str = None,
        disable_reports: bool = False,
    ):
        if Generic.__instance is not None:
            raise SdkException("A driver session already exists")

        LoggingHelper.configure_logging()

        self._token = token if token is not None else ConfigHelper.get_developer_token(
        )

        agent_status_response: AgentStatusResponse = AgentClient.get_agent_version(
            self._token)

        if version.parse(agent_status_response.tag) < version.parse(
                Generic.MIN_GENERIC_DRIVER_SUPPORTED_VERSION):
            raise AgentConnectException(
                f"Your current Agent version {agent_status_response.tag} does not support the Generic driver. "
                f"Please upgrade your Agent to the latest version and try again"
            )
        else:
            logging.info(
                f"Current Agent version {agent_status_response.tag} does support Generic driver"
            )

        self.session_id = None

        if disable_reports:
            # Setting the project and job name to empty strings will cause the Agent to not initialize a report
            self._projectname = ""
            self._jobname = ""
        else:
            self._projectname = (projectname if projectname is not None else
                                 ReportHelper.infer_project_name())
            self._jobname = (jobname if jobname is not None else
                             ReportHelper.infer_job_name())

        reportsettings = ReportSettings(self._projectname, self._jobname)

        capabilities = {"platformName": "ANY"}

        self._agent_client: AgentClient = AgentClient(
            token=self._token,
            capabilities=capabilities,
            report_settings=reportsettings,
        )
        self._agent_session: AgentSession = self._agent_client.agent_session

        self.command_executor = GenericCommandExecutor(
            agent_client=self._agent_client)

        Generic.__instance = self
예제 #8
0
    def __init__(
        self,
        capabilities: dict,
        token: str,
        projectname: str,
        jobname: str,
        disable_reports: bool,
    ):

        if BaseDriver.__instance is not None:
            raise SdkException("A driver session already exists")

        LoggingHelper.configure_logging()

        if token is not None:
            logging.info(f"Token used as specified in constructor: {token}")

        self._token = token if token is not None else ConfigHelper.get_developer_token(
        )

        if disable_reports:
            # Setting the project and job name to empty strings will cause the Agent to not initialize a report
            self._projectname = ""
            self._jobname = ""
        else:
            self._projectname = (projectname if projectname is not None else
                                 ReportHelper.infer_project_name())
            self._jobname = (jobname if jobname is not None else
                             ReportHelper.infer_job_name())

        self._agent_client: AgentClient = AgentClient(
            token=self._token,
            capabilities=capabilities,
            reportsettings=ReportSettings(self._projectname, self._jobname),
        )
        self._agent_session: AgentSession = self._agent_client.agent_session
        self.w3c = True if self._agent_session.dialect == "W3C" else False

        # Create a custom command executor to enable:
        # - automatic logging capabilities
        # - customized reporting settings
        self.command_executor = CustomCommandExecutor(
            agent_client=self._agent_client,
            remote_server_addr=self._agent_session.remote_address,
        )

        self.command_executor.disable_reports = disable_reports

        RemoteWebDriver.__init__(
            self,
            command_executor=self.command_executor,
            desired_capabilities=self._agent_session.capabilities,
        )

        BaseDriver.__instance = self
예제 #9
0
    def __init__(
        self,
        desired_capabilities: dict = None,
        token: str = None,
        project_name: str = None,
        job_name: str = None,
        disable_reports: bool = False,
    ):
        if Remote.__instance is not None:
            raise SdkException("A driver session already exists")

        LoggingHelper.configure_logging()

        self._desired_capabilities = desired_capabilities

        self._token = token if token is not None else ConfigHelper.get_developer_token(
        )

        if disable_reports:
            # Setting the project and job name to empty strings will cause the Agent to not initialize a report
            self._project_name = ""
            self._job_name = ""
        else:
            self._project_name = (project_name if project_name is not None else
                                  ReportHelper.infer_project_name())
            self._job_name = (job_name if job_name is not None else
                              ReportHelper.infer_job_name())

        report_settings = ReportSettings(self._project_name, self._job_name)

        self._agent_client: AgentClient = AgentClient(
            token=self._token,
            capabilities=self._desired_capabilities,
            report_settings=report_settings,
        )
        self._agent_session: AgentSession = self._agent_client.agent_session
        self.w3c = True if self._agent_session.dialect == "W3C" else False

        AppiumWebDriver.__init__(
            self,
            command_executor=self._agent_session.remote_address,
            desired_capabilities=self._desired_capabilities,
        )

        self.command_executor = CustomAppiumCommandExecutor(
            agent_client=self._agent_client,
            remote_server_addr=self._agent_session.remote_address,
        )

        # this ensures that mobile-specific commands are also available for our command executor
        self._addCommands()

        Remote.__instance = self
def __get_active_driver_instance():
    """Get the current driver instance in use (BaseDriver, Remote or Generic) """
    driver = BaseDriver.instance()
    if driver is None:
        driver = Remote.instance()
        if driver is None:
            driver = Generic.instance()
            if driver is None:
                raise SdkException(
                    "No active driver instance found, so cannot report failed assertion"
                )

    return driver
예제 #11
0
    def get_agent_version(token: str):
        """Requests the current Agent status

        Args:
            token (str): The developer token used to communicate with the Agent

        Returns:
            AgentStatusResponse: contains the response to the sent Agent status request
        """

        with requests.Session() as session:
            response = session.get(
                urljoin(ConfigHelper.get_agent_service_address(),
                        Endpoint.GetStatus.value),
                headers={"Authorization": token},
            )

        try:
            response.raise_for_status()
            try:
                response_json = response.json()
                agent_version = response_json["tag"]
            except ValueError:
                raise SdkException(
                    "Could not parse Agent status response: no JSON response body present"
                )
            except KeyError:
                raise SdkException(
                    "Could not parse Agent status response: element 'tag' not found in JSON response body"
                )
        except HTTPError:
            raise AgentConnectException(
                f"Agent returned HTTP {response.status_code} when trying to retrieve Agent status"
            )

        return AgentStatusResponse(agent_version)
예제 #12
0
    def __handle_new_session_error(response: OperationResult):
        """ Handles errors occurring on creation of a new session with the Agent

        Args:
            response (OperationResult): response from the Agent
        """
        if response.status_code == 401:
            logging.error("Invalid developer token supplied")
            logging.error(
                "Get your developer token from https://app.testproject.io/#/integrations/sdk?lang=Python"
                " and set it in the TP_DEV_TOKEN environment variable")
            logging.error(f"Response from Agent: {response.message}")
            raise SdkException("Invalid developer token supplied")
        elif response.status_code == 406:
            logging.error(
                f"This SDK version ({ConfigHelper.get_sdk_version()}) is incompatible with your Agent version."
            )
            logging.error(f"Response from Agent: {response.message}")
            raise SdkException(
                f"Invalid SDK version {ConfigHelper.get_sdk_version()}")
        else:
            logging.error("Failed to initialize a session with the Agent")
            logging.error(f"Response from Agent: {response.message}")
            raise SdkException("Failed to initialize a session with the Agent")
예제 #13
0
    def __init__(self, token: str, capabilities: dict,
                 report_settings: ReportSettings):
        self._remote_address = ConfigHelper.get_agent_service_address()
        self._capabilities = capabilities
        self._agent_session = None
        self._token = token
        self._report_settings = report_settings
        self._queue = queue.Queue()

        self._running = True
        self._reporting_thread = threading.Thread(target=self.__report_worker,
                                                  daemon=True)
        self._reporting_thread.start()

        if not self.__start_session():
            raise SdkException("Failed to start development mode session")
예제 #14
0
    def get_developer_token() -> str:
        """Returns the TestProject developer token as defined in the TP_DEV_TOKEN environment variable

        Returns:
            str: the developer token
        """
        token = os.getenv("TP_DEV_TOKEN")
        if token is None:
            logging.error(
                "No developer token was found, did you set it in the TP_DEV_TOKEN environment variable?"
            )
            logging.error(
                "You can get a developer token from https://app.testproject.io/#/integrations/sdk?lang=Python"
            )
            raise SdkException(
                "No development token defined in TP_DEV_TOKEN environment variable"
            )
        return token
예제 #15
0
    def execute(self,
                action: ActionProxy,
                by: By = None,
                by_value: str = None) -> ActionProxy:

        # Set the locator properties
        action.proxydescriptor.by = by
        action.proxydescriptor.by_value = by_value

        # Set the list of parameters for the action
        for param in action.__dict__:
            # Skip the _proxydescriptor attribute itself
            if param not in ["_proxydescriptor"]:
                action.proxydescriptor.parameters[param] = action.__dict__[
                    param]

        response: AddonExecutionResponse = self._agent_client.execute_proxy(
            action)
        if response.executionresulttype != ExecutionResultType.Passed:
            raise SdkException(
                f"Error occurred during addon action execution: {response.message}"
            )

        for field in response.fields:

            # skip non-output fields
            if not field.is_output:
                continue

            # check if action has an attribute with the name of the field
            if not hasattr(action, field.name):
                logging.warning(
                    f"Action '{action.proxydescriptor.guid}' does not have a field named '{field.name}'"
                )
                continue

            # update the attribute value with the value from the response
            setattr(action, field.name, field.value)

        return action
예제 #16
0
    def _request_session_from_agent(self) -> SessionResponse:
        """Creates and sends a session request object

        Returns:
            SessionResponse: object containing the response to the session request
        """
        session_request = SessionRequest(self._capabilities,
                                         self._reportsettings)

        logging.info(f"Session request: {session_request.to_json()}")

        try:
            response = self.send_request(
                "POST",
                f"{self._remote_address}{Endpoint.DevelopmentSession.value}",
                session_request.to_json(),
            )
        except requests.exceptions.ConnectionError:
            logging.error(
                f"Could not start new session on {self._remote_address}. Is your Agent running?"
            )
            logging.error(
                "You can download the TestProject Agent from https://app.testproject.io/#/agents"
            )
            raise SdkException(
                f"Connection error trying to connect to Agent on {self._remote_address}"
            )

        if not response.passed:
            self.__handle_new_session_error(response)

        start_session_response = SessionResponse(
            dev_socket_port=response.data["devSocketPort"],
            server_address=response.data["serverAddress"],
            session_id=response.data["sessionId"],
            dialect=response.data["dialect"],
            capabilities=response.data["capabilities"],
        )
        return start_session_response
예제 #17
0
    def create_connection(socket_address: str, socket_port: int) -> socket:
        """Parses the agent service address and attempts to create a socket connection

            Args:
                socket_address (str): The address for the socket
                socket_port (int): The development socket port to connect to

            Returns:
                socket: Socket object that has been created and connected to
        """
        host = urlparse(socket_address).hostname

        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
        sock.connect((host, socket_port))

        if not SocketHelper.is_socket_connected(sock):
            raise SdkException("Error occurred connecting to development socket")

        logging.info(f"Socket connection to {host}:{socket_port} established successfully")

        return sock
예제 #18
0
    def __init__(
        self,
        capabilities: dict,
        token: str,
        project_name: str,
        job_name: str,
        disable_reports: bool,
        report_type: ReportType,
    ):

        if BaseDriver.__instance is not None:
            raise SdkException("A driver session already exists")

        LoggingHelper.configure_logging()

        if token is not None:
            logging.info(f"Token used as specified in constructor: {token}")

        self._token = token if token is not None else ConfigHelper.get_developer_token(
        )

        if disable_reports:
            # Setting the project and job name to empty strings will cause the Agent to not initialize a report
            self._project_name = ""
            self._job_name = ""
        else:
            self._project_name = project_name if project_name is not None else ReportHelper.infer_project_name(
            )

            if job_name:
                self._job_name = job_name
            else:
                self._job_name = ReportHelper.infer_job_name()
                # Can update job name at runtime if not specified.
                os.environ[
                    EnvironmentVariable.TP_UPDATE_JOB_NAME.value] = "True"

        self._agent_client: AgentClient = AgentClient(
            token=self._token,
            capabilities=capabilities,
            report_settings=ReportSettings(self._project_name, self._job_name,
                                           report_type),
        )
        self._agent_session: AgentSession = self._agent_client.agent_session
        self.w3c = True if self._agent_session.dialect == "W3C" else False

        # Create a custom command executor to enable:
        # - automatic logging capabilities
        # - customized reporting settings
        self.command_executor = CustomCommandExecutor(
            agent_client=self._agent_client,
            remote_server_addr=self._agent_session.remote_address,
        )

        self.command_executor.disable_reports = disable_reports

        # Disable automatic command and test reports if Behave reporting is enabled.
        if os.getenv("TP_DISABLE_AUTO_REPORTING") == "True":
            self.command_executor.disable_command_reports = True
            self.command_executor.disable_auto_test_reports = True

        RemoteWebDriver.__init__(
            self,
            command_executor=self.command_executor,
            desired_capabilities=self._agent_session.capabilities,
        )

        BaseDriver.__instance = self
예제 #19
0
    def execute(self,
                action: ActionProxy,
                by: By = None,
                by_value: str = None) -> ActionProxy:
        # Set the locator properties
        action.proxydescriptor.by = by
        action.proxydescriptor.by_value = by_value

        # Set the list of parameters for the action
        for param in action.__dict__:
            # Skip the _proxydescriptor attribute itself
            if param not in ["_proxydescriptor"]:
                action.proxydescriptor.parameters[param] = action.__dict__[
                    param]

        # Objects for handling any StepSettings
        settings = self._command_executor.settings
        step_helper = self._command_executor.step_helper

        # Handling driver timeout
        step_helper.handle_timeout(settings.timeout)
        # Handling sleep before execution
        step_helper.handle_sleep(sleep_timing_type=settings.sleep_timing_type,
                                 sleep_time=settings.sleep_time)

        # Execute the action
        response: AddonExecutionResponse = self._agent_client.execute_proxy(
            action)

        # Handling sleep after execution
        step_helper.handle_sleep(sleep_timing_type=settings.sleep_timing_type,
                                 sleep_time=settings.sleep_time,
                                 step_executed=True)

        if response.execution_result_type is not ExecutionResultType.Passed and not settings.invert_result:
            raise SdkException(
                f"Error occurred during addon action execution: {response.message}"
            )

        # Update attributes value from response
        for field in response.fields:

            # skip non-output fields
            if not field.is_output:
                continue

            # check if action has an attribute with the name of the field
            if not hasattr(action, field.name):
                logging.warning(
                    f"Action '{action.proxydescriptor.guid}' does not have a field named '{field.name}'"
                )
                continue

            # update the attribute value with the value from the response
            setattr(action, field.name, field.value)

        # Extract result from response result.
        result = True if response.execution_result_type is ExecutionResultType.Passed else False
        result, step_message = step_helper.handle_step_result(
            step_result=result,
            base_msg=response.message,
            invert_result=settings.invert_result,
            always_pass=settings.always_pass)

        # Handle screenshot condition
        screenshot = step_helper.take_screenshot(settings.screenshot_condition,
                                                 result)

        # Getting the addon name from its proxy descriptor class name.
        # For example:
        #   action.proxydescriptor.classname = io.testproject.something.i.dont.care.TypeRandomPhoneNumber
        #   description is 'Execute TypeRandomPhoneNumber'.
        description = f'Execute \'{action.proxydescriptor.classname.split(".")[-1]}\''

        element = None
        # If proxy descriptor has the by property and the by property is implemented by TestProject's FindByType...
        if action.proxydescriptor.by and FindByType.has_value(
                action.proxydescriptor.by):
            element = ElementSearchCriteria(
                find_by_type=FindByType(action.proxydescriptor.by),
                by_value=action.proxydescriptor.by_value,
                index=-1)
        # Creating input/output fields
        input_fields = {
            f.name: f.value
            for f in response.fields if not f.is_output
        }
        output_fields = {
            f.name: f.value
            for f in response.fields if f.is_output
        }
        # Manually reporting the addon step with all the information.
        Reporter(command_executor=self._command_executor).step(
            description=description,
            message=f'{step_message}{os.linesep}',
            element=element,
            inputs=input_fields,
            outputs=output_fields,
            passed=result,
            screenshot=screenshot)
        return action
예제 #20
0
    def __init__(
        self,
        desired_capabilities: dict = None,
        token: str = None,
        project_name: str = None,
        job_name: str = None,
        disable_reports: bool = False,
    ):
        if Remote.__instance is not None:
            raise SdkException("A driver session already exists")

        LoggingHelper.configure_logging()

        self._desired_capabilities = desired_capabilities

        self._token = token if token is not None else ConfigHelper.get_developer_token(
        )

        if disable_reports:
            # Setting the project and job name to empty strings will cause the Agent to not initialize a report
            self._project_name = ""
            self._job_name = ""
        else:
            self._project_name = (project_name if project_name is not None else
                                  ReportHelper.infer_project_name())

            if job_name:
                self._job_name = job_name
            else:
                self._job_name = ReportHelper.infer_job_name()
                # Can update job name at runtime if not specified.
                os.environ[
                    EnvironmentVariable.TP_UPDATE_JOB_NAME.value] = "True"

        report_settings = ReportSettings(self._project_name, self._job_name)

        self._agent_client: AgentClient = AgentClient(
            token=self._token,
            capabilities=self._desired_capabilities,
            report_settings=report_settings,
        )
        self._agent_session: AgentSession = self._agent_client.agent_session
        self.w3c = True if self._agent_session.dialect == "W3C" else False

        AppiumWebDriver.__init__(
            self,
            command_executor=self._agent_session.remote_address,
            desired_capabilities=self._desired_capabilities,
        )

        self.command_executor = CustomAppiumCommandExecutor(
            agent_client=self._agent_client,
            remote_server_addr=self._agent_session.remote_address,
        )

        # this ensures that mobile-specific commands are also available for our command executor
        self._addCommands()

        # Disable automatic command and test reports if Behave reporting is enabled.
        if os.getenv("TP_DISABLE_AUTO_REPORTING") == "True":
            self.command_executor.disable_command_reports = True
            self.command_executor.disable_auto_test_reports = True

        Remote.__instance = self