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}")
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
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}]" )
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
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, )
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
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
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
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)
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")
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")
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
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
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
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
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
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
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