diff --git a/test/appium/requirements.txt b/test/appium/requirements.txt index 1f1b41e074..03c848b3ef 100644 --- a/test/appium/requirements.txt +++ b/test/appium/requirements.txt @@ -15,7 +15,7 @@ namedlist==1.7 py==1.4.34 pytest==3.2.1 pytest-forked==0.2 -pytest-xdist==1.20.0 +pytest-xdist==1.22.2 requests==2.18.3 sauceclient==1.0.0 selenium==3.8.1 diff --git a/test/appium/support/base_test_report.py b/test/appium/support/base_test_report.py index 2d289c9089..0d58fb444c 100644 --- a/test/appium/support/base_test_report.py +++ b/test/appium/support/base_test_report.py @@ -2,7 +2,7 @@ import json import hmac import os from hashlib import md5 -from tests import SingleTestData +from support.test_data import SingleTestData class BaseTestReport: @@ -28,24 +28,34 @@ class BaseTestReport: def save_test(self, test): file_path = self.get_test_report_file_path(test.name) - json.dump(test.__dict__, open(file_path, 'w')) + test_dict = { + 'testrail_case_id': test.testrail_case_id, + 'name': test.name, + 'testruns': list() + } + for testrun in test.testruns: + test_dict['testruns'].append(testrun.__dict__) + json.dump(test_dict, open(file_path, 'w')) def get_all_tests(self): tests = list() file_list = [f for f in os.listdir(self.TEST_REPORT_DIR)] for file_name in file_list: file_path = os.path.join(self.TEST_REPORT_DIR, file_name) - test_dict = json.load(open(file_path)) - tests.append(SingleTestData(name=test_dict['name'], steps=test_dict['steps'], - jobs=test_dict['jobs'], error=test_dict['error'], - testrail_case_id=test_dict['testrail_case_id'])) + test_data = json.load(open(file_path)) + testruns = list() + for testrun_data in test_data['testruns']: + testruns.append(SingleTestData.TestRunData( + steps=testrun_data['steps'], jobs=testrun_data['jobs'], error=testrun_data['error'])) + tests.append(SingleTestData(name=test_data['name'], testruns=testruns, + testrail_case_id=test_data['testrail_case_id'])) return tests def get_failed_tests(self): tests = self.get_all_tests() failed = list() for test in tests: - if test.error is not None: + if not self.is_test_successful(test): failed.append(test) return failed @@ -53,11 +63,16 @@ class BaseTestReport: tests = self.get_all_tests() passed = list() for test in tests: - if test.error is None: + if self.is_test_successful(test): passed.append(test) return passed def get_sauce_job_url(self, job_id): token = hmac.new(bytes(self.sauce_username + ":" + self.sauce_access_key, 'latin-1'), bytes(job_id, 'latin-1'), md5).hexdigest() - return "https://saucelabs.com/jobs/%s?auth=%s" % (job_id, token) \ No newline at end of file + return "https://saucelabs.com/jobs/%s?auth=%s" % (job_id, token) + + @staticmethod + def is_test_successful(test): + # Test passed if last testrun has passed + return test.testruns[-1].error is None diff --git a/test/appium/support/github_report.py b/test/appium/support/github_report.py index 05ce8b2264..e685e62bb5 100644 --- a/test/appium/support/github_report.py +++ b/test/appium/support/github_report.py @@ -56,9 +56,10 @@ class GithubHtmlReport(BaseTestReport): html = "%d. %s" % (index+1, test.name) html += "" test_steps_html = list() - for step in test.steps: + last_testrun = test.testruns[-1] + for step in last_testrun.steps: test_steps_html.append("
%s
" % step) - if test.error: + if last_testrun.error: if test_steps_html: html += "

" html += "

" @@ -66,10 +67,10 @@ class GithubHtmlReport(BaseTestReport): html += "%s" % ''.join(test_steps_html[-2:]) html += "
" html += "

" - html += "%s" % test.error + html += "%s" % last_testrun.error html += "

" - if test.jobs: - html += self.build_device_sessions_html(test.jobs) + if last_testrun.jobs: + html += self.build_device_sessions_html(last_testrun.jobs) html += "" return html @@ -80,3 +81,4 @@ class GithubHtmlReport(BaseTestReport): html += "
  • Device %d
  • " % (self.get_sauce_job_url(job_id), i+1) html += "

    " return html + diff --git a/test/appium/support/test_data.py b/test/appium/support/test_data.py new file mode 100644 index 0000000000..b09ce88420 --- /dev/null +++ b/test/appium/support/test_data.py @@ -0,0 +1,30 @@ +class SingleTestData(object): + def __init__(self, name, testruns, testrail_case_id): + self.testrail_case_id = testrail_case_id + self.name = name + self.testruns = testruns + + class TestRunData(object): + def __init__(self, steps, jobs, error): + self.steps = steps + self.jobs = jobs + self.error = error + + def create_new_testrun(self): + self.testruns.append(SingleTestData.TestRunData(list(), list(), None)) + + +class TestSuiteData(object): + def __init__(self): + self.apk_name = None + self.current_test = None + self.tests = list() + + def set_current_test(self, test_name, testrail_case_id): + existing_test = next((test for test in self.tests if test.name == test_name), None) + if existing_test: + self.current_test = existing_test + else: + test = SingleTestData(test_name, list(), testrail_case_id) + self.tests.append(test) + self.current_test = test diff --git a/test/appium/support/test_rerun.py b/test/appium/support/test_rerun.py new file mode 100644 index 0000000000..47163a983a --- /dev/null +++ b/test/appium/support/test_rerun.py @@ -0,0 +1,13 @@ +RERUN_ERRORS = [ + 'Original error: Error: ESOCKETTIMEDOUT', + "The server didn't respond in time.", + 'An unknown server-side error occurred while processing the command.', + 'Could not proxy command to remote server. Original error: Error: socket hang up' +] + + +def should_rerun_test(test_error): + for rerun_error in RERUN_ERRORS: + if rerun_error in test_error: + return True + return False diff --git a/test/appium/support/testrail_report.py b/test/appium/support/testrail_report.py index 0ad9031236..9ba8b84512 100644 --- a/test/appium/support/testrail_report.py +++ b/test/appium/support/testrail_report.py @@ -62,11 +62,12 @@ class TestrailReport(BaseTestReport): test_steps = "# Steps: \n" devices = str() method = 'add_result_for_case/%s/%s' % (self.run_id, test.testrail_case_id) - for step in test.steps: + last_testrun = test.testruns[-1] + for step in last_testrun.steps: test_steps += step + "\n" - for i, device in enumerate(test.jobs): + for i, device in enumerate(last_testrun.jobs): devices += "# [Device %d](%s) \n" % (i + 1, self.get_sauce_job_url(device)) - data = {'status_id': self.outcomes['undefined_fail'] if test.error else self.outcomes['passed'], - 'comment': '%s' % ('# Error: \n %s \n' % test.error) + devices + test_steps if test.error + data = {'status_id': self.outcomes['undefined_fail'] if last_testrun.error else self.outcomes['passed'], + 'comment': '%s' % ('# Error: \n %s \n' % last_testrun.error) + devices + test_steps if last_testrun.error else devices + test_steps} self.post(method, data=data) diff --git a/test/appium/tests/__init__.py b/test/appium/tests/__init__.py index 4360dae462..63d72c9f24 100644 --- a/test/appium/tests/__init__.py +++ b/test/appium/tests/__init__.py @@ -2,6 +2,8 @@ import asyncio import logging from datetime import datetime +from support.test_data import TestSuiteData + @asyncio.coroutine def start_threads(quantity: int, func: type, returns: dict, *args): @@ -20,27 +22,11 @@ def get_current_time(): def info(text: str): if "Base" not in text: logging.info(text) - test_suite_data.current_test.steps.append(text) + test_suite_data.current_test.testruns[-1].steps.append(text) -class SingleTestData(object): - def __init__(self, name, steps=list(), jobs=list(), error=None, testrail_case_id=None): - self.testrail_case_id = testrail_case_id - self.name = name - self.steps = steps - self.jobs = jobs - self.error = error - - -class TestSuiteData(object): - def __init__(self): - self.apk_name = None - self.current_test = None - self.tests = list() - - def add_test(self, test): - self.tests.append(test) - self.current_test = test +def debug(text: str): + logging.debug(text) test_suite_data = TestSuiteData() diff --git a/test/appium/tests/base_test_case.py b/test/appium/tests/base_test_case.py index e6cfe428c7..15122292ab 100644 --- a/test/appium/tests/base_test_case.py +++ b/test/appium/tests/base_test_case.py @@ -121,7 +121,7 @@ class SingleDeviceTestCase(AbstractTestCase): capabilities[self.environment]['capabilities']) self.driver.implicitly_wait(self.implicitly_wait) BaseView(self.driver).accept_agreements() - test_suite_data.current_test.jobs.append(self.driver.session_id) + test_suite_data.current_test.testruns[-1].jobs.append(self.driver.session_id) break except WebDriverException: counter += 1 @@ -146,7 +146,7 @@ class LocalMultipleDeviceTestCase(AbstractTestCase): self.drivers[driver] = webdriver.Remote(self.executor_local, capabilities[driver]) self.drivers[driver].implicitly_wait(self.implicitly_wait) BaseView(self.drivers[driver]).accept_agreements() - test_suite_data.current_test.jobs.append(self.drivers[driver].session_id) + test_suite_data.current_test.testruns[-1].jobs.append(self.drivers[driver].session_id) def teardown_method(self, method): for driver in self.drivers: @@ -174,7 +174,7 @@ class SauceMultipleDeviceTestCase(AbstractTestCase): for driver in range(quantity): self.drivers[driver].implicitly_wait(self.implicitly_wait) BaseView(self.drivers[driver]).accept_agreements() - test_suite_data.current_test.jobs.append(self.drivers[driver].session_id) + test_suite_data.current_test.testruns[-1].jobs.append(self.drivers[driver].session_id) def teardown_method(self, method): for driver in self.drivers: diff --git a/test/appium/tests/conftest.py b/test/appium/tests/conftest.py index bb522484eb..26e4502ee9 100644 --- a/test/appium/tests/conftest.py +++ b/test/appium/tests/conftest.py @@ -1,5 +1,9 @@ -from tests import test_suite_data, SingleTestData +from _pytest.runner import runtestprotocol + +from support.test_rerun import should_rerun_test +from tests import test_suite_data, debug import requests +import re import pytest from datetime import datetime from os import environ @@ -42,6 +46,14 @@ def pytest_addoption(parser): action='store', default=False, help='boolean; For running extended test suite against nightly build') + parser.addoption('--rerun_count', + action='store', + default=0, + help='How many times tests should be re-run if failed') + + +def get_rerun_count(): + return int(pytest.config.getoption('rerun_count')) def is_master(config): @@ -95,13 +107,13 @@ def pytest_unconfigure(config): def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() - is_sauce_env = pytest.config.getoption('env') == 'sauce' - current_test = test_suite_data.current_test if report.when == 'call': + is_sauce_env = pytest.config.getoption('env') == 'sauce' + current_test = test_suite_data.current_test if report.failed: - current_test.error = report.longreprtext + current_test.testruns[-1].error = report.longreprtext if is_sauce_env: - update_sauce_jobs(current_test.name, current_test.jobs, report.passed) + update_sauce_jobs(current_test.name, current_test.testruns[-1].jobs, report.passed) github_report.save_test(current_test) @@ -116,4 +128,15 @@ def get_testrail_case_id(obj): def pytest_runtest_setup(item): - test_suite_data.add_test(SingleTestData(item.name, testrail_case_id=get_testrail_case_id(item))) + test_suite_data.set_current_test(item.name, testrail_case_id=get_testrail_case_id(item)) + test_suite_data.current_test.create_new_testrun() + + +def pytest_runtest_protocol(item, nextitem): + for i in range(get_rerun_count()): + reports = runtestprotocol(item, nextitem=nextitem) + for report in reports: + if report.failed and should_rerun_test(report.longreprtext): + break # rerun + else: + return True # no need to rerun