import asyncio import base64 import logging import re import subprocess import sys from abc import ABCMeta, abstractmethod from http.client import RemoteDisconnected import pytest import requests from appium import webdriver from appium.options.common import AppiumOptions from appium.webdriver.common.mobileby import MobileBy from appium.webdriver.connectiontype import ConnectionType from selenium.common.exceptions import NoSuchElementException, WebDriverException from urllib3.exceptions import MaxRetryError, ProtocolError from support.api.network_api import NetworkApi from tests import test_suite_data, start_threads, appium_container, pytest_config_global, transl from tests.conftest import github_report, run_name, lambda_test_username, lambda_test_access_key executor_lambda_test = 'https://%s:%s@mobile-hub.lambdatest.com/wd/hub' % (lambda_test_username, lambda_test_access_key) executor_local = 'http://localhost:4723/wd/hub' implicit_wait = 5 def get_capabilities_local(): desired_caps = dict() if pytest_config_global['docker']: # apk is in shared volume directory apk = '/root/shared_volume/%s' % pytest_config_global['apk'] else: apk = pytest_config_global['apk'] desired_caps['app'] = apk desired_caps['deviceName'] = 'nexus_5' desired_caps['platformName'] = 'Android' desired_caps['appiumVersion'] = '1.9.1' desired_caps['platformVersion'] = '10.0' desired_caps['newCommandTimeout'] = 600 desired_caps['fullReset'] = False desired_caps['unicodeKeyboard'] = True desired_caps['automationName'] = 'UiAutomator2' desired_caps['setWebContentDebuggingEnabled'] = True return desired_caps def add_local_devices_to_capabilities(): updated_capabilities = list() raw_out = re.split(r'[\r\\n]+', str(subprocess.check_output(['adb', 'devices'])).rstrip()) for line in raw_out[1:]: serial = re.findall(r"(([\d.\d:]*\d+)|\bemulator-\d+)", line) if serial: capabilities = get_capabilities_local() capabilities['udid'] = serial[0][0] updated_capabilities.append(capabilities) return updated_capabilities def get_lambda_test_capabilities_real_device(): capabilities = { "lt:options": { "w3c": True, "platformName": "android", "deviceName": "Pixel 8", "platformVersion": "14", "app": pytest_config_global['lt_apk_url'], "devicelog": True, "visual": True, "video": True, "build": run_name, "name": test_suite_data.current_test.group_name, "idleTimeout": 1000, "isRealMobile": True } } options = AppiumOptions() options.load_capabilities(capabilities) return options def get_lambda_test_capabilities_emulator(): capabilities = { "lt:options": { "w3c": True, "platformName": "android", "deviceName": "Pixel 6", "appiumVersion": "2.1.3", "platformVersion": "14", "app": pytest_config_global['lt_apk_url'], "devicelog": True, "visual": True, "video": True, "build": run_name, "name": test_suite_data.current_test.group_name, "idleTimeout": 1000, # "enableImageInjection": True, # "uploadMedia": ["lt://MEDIA2b3e34e2b0ee4928b9fc38c603f98191",], # "lt://MEDIAcfc1b4f1af0740759254404186bbe4f1"] }, "appium:options": { "automationName": "UiAutomator2", "hideKeyboard": True } } options = AppiumOptions() options.load_capabilities(capabilities) return options def get_app_path(): app_folder = 'im.status.ethereum' apk = pytest_config_global['apk'] if re.findall(r'pr\d\d\d\d\d', apk) or re.findall(r'\d\d\d\d\d.apk', apk): app_folder += '.pr' app_path = '/storage/emulated/0/Android/data/%s/files/Download/' % app_folder return app_path def pull_geth(driver): result = driver.pull_file(get_app_path() + 'geth.log') return base64.b64decode(result) def pull_requests_log(driver): result = driver.pull_file(get_app_path() + 'api.log') return base64.b64decode(result) class AbstractTestCase: __metaclass__ = ABCMeta def print_lt_session_info(self, driver): sys.stdout = sys.stderr print("LambdaTestSessionID=%s job-name=%s" % (driver.session_id, run_name)) def get_translation_by_key(self, key): return transl[key] @abstractmethod def setup_method(self, method): raise NotImplementedError('Should be overridden from a child class') @abstractmethod def teardown_method(self, method): raise NotImplementedError('Should be overridden from a child class') @property def environment(self): return pytest_config_global['env'] network_api = NetworkApi() @staticmethod def get_alert_text(driver): try: return driver.find_element(MobileBy.ID, 'android:id/message').text except NoSuchElementException: return None def add_alert_text_to_report(self, driver): try: alert_text = self.get_alert_text(driver) if alert_text: test_suite_data.current_test.testruns[-1].error = "%s; also Unexpected Alert is shown: '%s'" % ( test_suite_data.current_test.testruns[-1].error, alert_text ) except (RemoteDisconnected, ProtocolError): test_suite_data.current_test.testruns[-1].error = "%s; \n RemoteDisconnected" % \ test_suite_data.current_test.testruns[-1].error class Driver(webdriver.Remote): @property def number(self): return test_suite_data.current_test.testruns[-1].jobs[self.session_id] def info(self, text: str, device=True): if device: text = 'Device %s: %s ' % (self.number, text) logging.info(text) test_suite_data.current_test.testruns[-1].steps.append(text) def fail(self, text: str): pytest.fail('Device %s: %s' % (self.number, text)) def update_lt_session_status(self, index, status): data = { "action": "setTestStatus", "arguments": { "status": status, "remark": "Device %s" % index } } self.execute_script("lambda-hook: %s" % str(data).replace("'", "\"")) class Errors(object): def __init__(self): self.errors = list() def append(self, text=str()): self.errors.append(text) def verify_no_errors(self): if self.errors: pytest.fail('\n '.join([self.errors.pop(0) for _ in range(len(self.errors))])) class SingleDeviceTestCase(AbstractTestCase): def setup_method(self, method, **kwargs): if pytest_config_global['docker']: appium_container.start_appium_container(pytest_config_global['docker_shared_volume']) appium_container.connect_device(pytest_config_global['device_ip']) # (executor, capabilities) = (executor_sauce_lab, get_capabilities_sauce_lab()) if \ # self.environment == 'sauce' else (executor_local, get_capabilities_local()) # for key, value in kwargs.items(): # capabilities[key] = value # self.driver = Driver(executor, capabilities) test_suite_data.current_test.testruns[-1].jobs[self.driver.session_id] = 1 self.driver.implicitly_wait(implicit_wait) self.errors = Errors() if pytest_config_global['docker']: appium_container.reset_battery_stats() def teardown_method(self, method): if self.environment == 'sauce': self.print_lt_session_info(self.driver) try: self.add_alert_text_to_report(self.driver) geth_content = pull_geth(self.driver) self.driver.quit() if pytest_config_global['docker']: appium_container.stop_container() except (WebDriverException, AttributeError): pass finally: github_report.save_test(test_suite_data.current_test, {'%s_geth.log' % test_suite_data.current_test.name: geth_content}) class LocalMultipleDeviceTestCase(AbstractTestCase): def setup_method(self, method): self.drivers = dict() self.errors = Errors() def create_drivers(self, quantity): capabilities = self.add_local_devices_to_capabilities() for driver in range(quantity): self.drivers[driver] = Driver(self.executor_local, capabilities[driver]) test_suite_data.current_test.testruns[-1].jobs[self.drivers[driver].session_id] = driver + 1 self.drivers[driver].implicitly_wait(self.implicitly_wait) def teardown_method(self, method): for driver in self.drivers: try: self.add_alert_text_to_report(self.drivers[driver]) self.drivers[driver].quit() except WebDriverException: pass def create_shared_drivers(quantity): drivers = dict() if pytest_config_global['env'] == 'local': capabilities = add_local_devices_to_capabilities() for i in range(quantity): driver = Driver(executor_local, capabilities[i]) test_suite_data.current_test.testruns[-1].jobs[driver.session_id] = i + 1 driver.implicitly_wait(implicit_wait) drivers[i] = driver loop = None return drivers, loop else: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) print('LT Executor: %s' % executor_lambda_test) try: drivers = loop.run_until_complete(start_threads(test_suite_data.current_test.name, quantity, Driver, drivers, command_executor=executor_lambda_test, options=get_lambda_test_capabilities_emulator())) if len(drivers) < quantity: test_suite_data.current_test.testruns[-1].error = "Not all %s drivers are created" % quantity for i in range(quantity): test_suite_data.current_test.testruns[-1].jobs[drivers[i].session_id] = i + 1 drivers[i].implicitly_wait(implicit_wait) drivers[i].update_settings({"enforceXPath1": True}) drivers[i].set_network_connection(ConnectionType.WIFI_ONLY) return drivers, loop except (MaxRetryError, AttributeError) as e: test_suite_data.current_test.testruns[-1].error = str(e) for i, driver in drivers.items(): try: driver.update_lt_session_status(i + 1, "failed") driver.quit() except (WebDriverException, AttributeError): pass raise e class LocalSharedMultipleDeviceTestCase(AbstractTestCase): def setup_method(self, method): jobs = test_suite_data.current_test.testruns[-1].jobs if not jobs: for index, driver in self.drivers.items(): jobs[driver.session_id] = index + 1 self.errors = Errors() def teardown_method(self, method): for driver in self.drivers: try: self.add_alert_text_to_report(self.drivers[driver]) except WebDriverException: pass @pytest.fixture(scope='class', autouse=True) def prepare(self, request): try: request.cls.prepare_devices(request) finally: for item, value in request.__dict__.items(): setattr(request.cls, item, value) @classmethod def teardown_class(cls): for driver in cls.drivers: try: cls.drivers[driver].quit() except WebDriverException: pass class LambdaTestSharedMultipleDeviceTestCase(AbstractTestCase): def setup_method(self, method): if not self.drivers: pytest.fail(test_suite_data.current_test.testruns[-1].error) for _, driver in self.drivers.items(): driver.execute_script("lambda-testCase-start=%s" % method.__name__) driver.log_event("appium", "Started %s" % method.__name__) jobs = test_suite_data.current_test.testruns[-1].jobs if not jobs: for index, driver in self.drivers.items(): jobs[driver.session_id] = index + 1 self.errors = Errors() test_suite_data.current_test.group_name = self.__class__.__name__ def teardown_method(self, method): log_names, log_contents = [], [] for driver in self.drivers: try: self.print_lt_session_info(self.drivers[driver]) self.add_alert_text_to_report(self.drivers[driver]) log_names.append( '%s_geth%s.log' % (test_suite_data.current_test.name, str(self.drivers[driver].number))) log_contents.append(pull_geth(self.drivers[driver])) log_names.append( '%s_requests%s.log' % (test_suite_data.current_test.name, str(self.drivers[driver].number))) log_contents.append(pull_requests_log(self.drivers[driver])) except (WebDriverException, AttributeError, RemoteDisconnected, ProtocolError): pass finally: try: logs = {log_names[i]: log_contents[i] for i in range(len(log_names))} test_suite_data.current_test.logs_paths = github_report.save_logs(logs) except IndexError: pass @pytest.fixture(scope='class', autouse=True) def prepare(self, request): test_suite_data.current_test.group_name = request.cls.__name__ try: request.cls.prepare_devices(request) finally: for item, value in request.__dict__.items(): setattr(request.cls, item, value) @classmethod def teardown_class(cls): requests_session = requests.Session() requests_session.auth = (lambda_test_username, lambda_test_access_key) if test_suite_data.tests[0].testruns[-1].error and 'setup failed' in test_suite_data.tests[0].testruns[ -1].error: group_setup_failed = True else: group_setup_failed = False log_contents, log_names = list(), list() try: for i, driver in cls.drivers.items(): if group_setup_failed: log_contents.append(pull_geth(driver=driver)) log_names.append('%s_geth%s.log' % (cls.__name__, i)) log_contents.append(pull_requests_log(driver=driver)) log_names.append('%s_requests%s.log' % (cls.__name__, i)) lt_session_status = "failed" else: lt_session_status = "passed" try: driver.update_lt_session_status(i + 1, lt_session_status) driver.quit() except (WebDriverException, RemoteDisconnected): pass # url = 'https://api.%s/rest/v1/%s/jobs/%s/assets/%s' % (apibase, sauce_username, session_id, "log.json") # try: # WebDriverWait(driver, 60, 2).until(lambda _: requests_session.get(url).status_code == 200) # commands = requests_session.get(url).json() # for command in commands: # try: # if command['message'].startswith("Started "): # for test in test_suite_data.tests: # if command['message'] == "Started %s" % test.name: # test.testruns[-1].first_commands[session_id] = commands.index(command) + 1 # except KeyError: # continue # except (RemoteDisconnected, requests.exceptions.ConnectionError, TimeoutException): # pass except AttributeError: pass finally: try: cls.loop.close() except AttributeError: pass logs = dict(zip(log_names, log_contents)) logs_paths = github_report.save_logs(logs) for test in test_suite_data.tests: if group_setup_failed: test.logs_paths = logs_paths github_report.save_test(test) if pytest_config_global['env'] == 'local': MultipleSharedDeviceTestCase = LocalSharedMultipleDeviceTestCase else: MultipleSharedDeviceTestCase = LambdaTestSharedMultipleDeviceTestCase class NoDeviceTestCase(AbstractTestCase): def setup_method(self, method, **kwargs): pass def teardown_method(self, method): github_report.save_test(test_suite_data.current_test)