From 3c260affd02d99a13cc14d352f00021f2b58d077 Mon Sep 17 00:00:00 2001 From: Michael Mintz Date: Wed, 3 May 2023 14:01:44 -0400 Subject: [PATCH] Refactoring --- seleniumbase/core/log_helper.py | 42 +++++- seleniumbase/fixtures/base_case.py | 172 +++++++++++++++++------ seleniumbase/fixtures/constants.py | 7 + seleniumbase/fixtures/page_actions.py | 2 - seleniumbase/fixtures/unittest_helper.py | 41 ++++++ seleniumbase/plugins/base_plugin.py | 3 +- seleniumbase/plugins/pytest_plugin.py | 5 +- 7 files changed, 214 insertions(+), 58 deletions(-) create mode 100644 seleniumbase/fixtures/unittest_helper.py diff --git a/seleniumbase/core/log_helper.py b/seleniumbase/core/log_helper.py index 083e0e9a..ed810a7a 100644 --- a/seleniumbase/core/log_helper.py +++ b/seleniumbase/core/log_helper.py @@ -10,6 +10,7 @@ from seleniumbase.fixtures import constants python3_11_or_newer = False if sys.version_info >= (3, 11): python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH def log_screenshot(test_logpath, driver, screenshot=None, get=False): @@ -79,7 +80,11 @@ def get_master_time(): def get_browser_version(driver): - if python3_11_or_newer and hasattr(sb_config, "_browser_version"): + if ( + python3_11_or_newer + and py311_patch2 + and hasattr(sb_config, "_browser_version") + ): return sb_config._browser_version driver_capabilities = driver.capabilities if "version" in driver_capabilities: @@ -187,6 +192,25 @@ def log_test_failure_data(test, test_logpath, driver, browser, url=None): traceback_list = traceback.format_list( traceback.extract_tb(traceback_address)[1:] ) + updated_list = [] + counter = 0 + for traceback_item in traceback_list: + if "self._callTestMethod(testMethod)" in traceback_item: + counter = 1 + updated_list.append(traceback_item) # In case not cleared + continue + elif ( + ", in _callTestMethod" in traceback_item.strip() + and "method()" in traceback_item.strip() + and counter == 1 + ): + counter = 0 + updated_list = [] + continue + else: + counter = 0 + updated_list.append(traceback_item) + traceback_list = updated_list traceback_message = "".join(traceback_list).strip() except Exception: exc_message = "(Unknown Exception)" @@ -201,13 +225,17 @@ def log_test_failure_data(test, test_logpath, driver, browser, url=None): if sb_config.behave_step.error_message: traceback_message = sb_config.behave_step.error_message else: - traceback_message = "".join( - traceback.format_exception( - sys.exc_info()[0], - sys.exc_info()[1], - sys.exc_info()[2], - ) + format_exception = traceback.format_exception( + sys.exc_info()[0], sys.exc_info()[1], sys.exc_info()[2] ) + if format_exception: + updated_list = [] + for line in format_exception: + if "sb_manager.py" in line and "yield sb" in line: + continue + updated_list.append(line) + format_exception = updated_list + traceback_message = "".join(format_exception) if ( not traceback_message or len(str(traceback_message)) < 30 diff --git a/seleniumbase/fixtures/base_case.py b/seleniumbase/fixtures/base_case.py index a1121cb6..509e7fa4 100644 --- a/seleniumbase/fixtures/base_case.py +++ b/seleniumbase/fixtures/base_case.py @@ -77,6 +77,7 @@ from seleniumbase.fixtures import js_utils from seleniumbase.fixtures import page_actions from seleniumbase.fixtures import page_utils from seleniumbase.fixtures import shared_utils +from seleniumbase.fixtures import unittest_helper from seleniumbase.fixtures import xpath_to_css __all__ = ["BaseCase"] @@ -91,6 +92,7 @@ if sys.platform in ["win32", "win64", "x64"]: python3_11_or_newer = False if sys.version_info >= (3, 11): python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH selenium4_or_newer = False if sys.version_info >= (3, 7): selenium4_or_newer = True @@ -307,7 +309,6 @@ class BaseCase(unittest.TestCase): pass # Odd issue where the open did happen. Continue. else: raise - unittest.has_exception = False if ( self.undetectable or ( @@ -4120,7 +4121,6 @@ class BaseCase(unittest.TestCase): and settings.SKIP_JS_WAITS ): time.sleep(0.05) - unittest.has_exception = False return True def wait_for_angularjs(self, timeout=None, **kwargs): @@ -6362,14 +6362,12 @@ class BaseCase(unittest.TestCase): ) if type(page) is int: if text not in pdf_text: - unittest.has_exception = True raise Exception( "PDF [%s] is missing expected text [%s] on " "page [%s]!" % (pdf, text, page) ) else: if text not in pdf_text: - unittest.has_exception = True raise Exception( "PDF [%s] is missing expected text [%s]!" % (pdf, text) ) @@ -6495,7 +6493,19 @@ class BaseCase(unittest.TestCase): from PIL import Image, ImageDraw except Exception: shared_utils.pip_install("Pillow") - from PIL import Image, ImageDraw + try: + from PIL import Image, ImageDraw + except Exception as e: + if ( + sys.version_info >= (3, 12) + and "symbol not found" in e.msg + ): + raise Exception( + "PIL / Pillow is not supported on Python %s" + % ".".join(str(s) for s in sys.version_info) + ) + else: + raise text_rows = overlay_text.split("\n") len_text_rows = len(text_rows) max_width = 0 @@ -6744,44 +6754,32 @@ class BaseCase(unittest.TestCase): def assert_true(self, expr, msg=None): """Asserts that the expression is True. Will raise an exception if the statement if False.""" - unittest.has_exception = True self.assertTrue(expr, msg=msg) - unittest.has_exception = False def assert_false(self, expr, msg=None): """Asserts that the expression is False. Will raise an exception if the statement if True.""" - unittest.has_exception = True self.assertFalse(expr, msg=msg) - unittest.has_exception = False def assert_equal(self, first, second, msg=None): """Asserts that the two values are equal. Will raise an exception if the values are not equal.""" - unittest.has_exception = True self.assertEqual(first, second, msg=msg) - unittest.has_exception = False def assert_not_equal(self, first, second, msg=None): """Asserts that the two values are not equal. Will raise an exception if the values are equal.""" - unittest.has_exception = True self.assertNotEqual(first, second, msg=msg) - unittest.has_exception = False def assert_in(self, first, second, msg=None): """Asserts that the first string is in the second string. Will raise an exception if the first string is not in the second.""" - unittest.has_exception = True self.assertIn(first, second, msg=msg) - unittest.has_exception = False def assert_not_in(self, first, second, msg=None): """Asserts that the first string is not in the second string. Will raise an exception if the first string is in the second string.""" - unittest.has_exception = True self.assertNotIn(first, second, msg=msg) - unittest.has_exception = False def assert_raises(self, *args, **kwargs): """Asserts that the following block of code raises an exception. @@ -6900,11 +6898,9 @@ class BaseCase(unittest.TestCase): self.wait_for_ready_state_complete() time.sleep(2) actual = self.get_page_title().strip() - unittest.has_exception = True self.assertEqual( expected, actual, error % (expected, actual) ) - unittest.has_exception = False if self.demo_mode and not self.recorder_mode: a_t = "ASSERT TITLE" if self._language != "English": @@ -6949,11 +6945,9 @@ class BaseCase(unittest.TestCase): self.wait_for_ready_state_complete() time.sleep(2) actual = self.get_page_title().strip() - unittest.has_exception = True self.assertIn( expected, actual, error % (expected, actual) ) - unittest.has_exception = False if self.demo_mode and not self.recorder_mode: a_t = "ASSERT TITLE CONTAINS" if self._language != "English": @@ -6988,9 +6982,7 @@ class BaseCase(unittest.TestCase): self.wait_for_ready_state_complete() time.sleep(2) actual = self.get_current_url().strip() - unittest.has_exception = True self.assertEqual(expected, actual, error % (expected, actual)) - unittest.has_exception = False if self.demo_mode and not self.recorder_mode: a_u = "ASSERT URL" if self._language != "English": @@ -7028,9 +7020,7 @@ class BaseCase(unittest.TestCase): self.wait_for_ready_state_complete() time.sleep(2) actual = self.get_current_url().strip() - unittest.has_exception = True self.assertIn(expected, actual, error % (expected, actual)) - unittest.has_exception = False if self.demo_mode and not self.recorder_mode: a_u = "ASSERT URL CONTAINS" if self._language != "English": @@ -7127,7 +7117,6 @@ class BaseCase(unittest.TestCase): er_str = str(errors) er_str = er_str.replace("[{", "[\n{").replace("}, {", "},\n{") current_url = self.get_current_url() - unittest.has_exception = True raise Exception( "JavaScript errors found on %s => %s" % (current_url, er_str) ) @@ -7683,9 +7672,7 @@ class BaseCase(unittest.TestCase): def fail(self, msg=None): """Fail immediately, with the given message.""" - unittest.has_exception = True super().fail(msg) - raise self.failureException(msg) def skip(self, reason=""): """Mark the test as Skipped.""" @@ -9463,7 +9450,10 @@ class BaseCase(unittest.TestCase): raise VisualException(minified_exception) def _process_visual_baseline_logs(self): - if not (python3_11_or_newer or "--pdb" in sys.argv): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): return self.__process_visual_baseline_logs() @@ -9837,7 +9827,6 @@ class BaseCase(unittest.TestCase): def __check_scope(self): if hasattr(self, "browser"): # self.browser stores the type of browser - unittest.has_exception = False return # All good: setUp() already initialized variables in "self" else: message = ( @@ -9853,7 +9842,6 @@ class BaseCase(unittest.TestCase): "\n variables, which are initialized during the setUp() method" "\n that runs automatically before all tests called by pytest." ) - unittest.has_exception = True raise OutOfScopeException(message) ############ @@ -10109,7 +10097,6 @@ class BaseCase(unittest.TestCase): if print_only: print(exception_output) else: - unittest.has_exception = True raise Exception(exception_output.replace("\\n", "\n")) ############ @@ -13408,11 +13395,9 @@ class BaseCase(unittest.TestCase): """This method runs before every test begins. Be careful if a subclass of BaseCase overrides setUp(). If so, add the following line to the subclass setUp() method: - super().setUp() - """ + super().setUp() """ if not hasattr(self, "_using_sb_fixture") and self.__called_setup: - # This test already called setUp() - return + return # This test already called setUp() self.__called_setup = True self.__called_teardown = False self.masterqa_mode = masterqa_mode @@ -13902,8 +13887,6 @@ class BaseCase(unittest.TestCase): # Some actions such as hover-clicking are different on mobile. self.mobile_emulator = False - unittest.has_exception = False - # Configure the test time limit (if used). self.set_time_limit(self.time_limit) @@ -14080,7 +14063,10 @@ class BaseCase(unittest.TestCase): self.testcase_manager.update_testcase_data(data_payload) def _add_pytest_html_extra(self): - if not (python3_11_or_newer or "--pdb" in sys.argv): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): return self.__add_pytest_html_extra() @@ -14188,8 +14174,6 @@ class BaseCase(unittest.TestCase): has_exception = sys.exc_info()[1] is not None if self.__will_be_skipped and hasattr(self, "_using_sb_fixture"): has_exception = False - if python3_11_or_newer and unittest.has_exception: - has_exception = True return has_exception def __get_test_id(self): @@ -14731,7 +14715,7 @@ class BaseCase(unittest.TestCase): if ( self.__has_exception() or self.save_screenshot_after_test - or python3_11_or_newer + or (python3_11_or_newer and py311_patch2) or "--pdb" in sys.argv ): self.__set_last_page_screenshot() @@ -14742,7 +14726,10 @@ class BaseCase(unittest.TestCase): self.__add_pytest_html_extra() def _log_fail_data(self): - if not (python3_11_or_newer or "--pdb" in sys.argv): + if not ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ): return test_id = self.__get_test_id() test_logpath = os.path.join(self.log_path, test_id) @@ -14801,12 +14788,102 @@ class BaseCase(unittest.TestCase): else: return None + def _addSkip(self, result, test_case, reason): + """This method should NOT be called directly from tests. + (It will be called AUTOMATICALLY as needed.)""" + addSkip = getattr(result, 'addSkip', None) + if addSkip is not None: + addSkip(test_case, reason) + else: + import warnings + warnings.warn( + "TestResult has no addSkip method! Skips not reported!", + RuntimeWarning, 2 + ) + result.addSuccess(test_case) + + def _callTestMethod(self, method): + """This method should NOT be called directly from tests. + (It will be called AUTOMATICALLY as needed.)""" + method() + + def run(self, result=None): + """Overwrite the unittest run() method for Python 3.11 or newer. + This method should NOT be called directly from tests. + (It will be called AUTOMATICALLY as needed.)""" + if not python3_11_or_newer: + return super().run(result=result) + if result is None: + result = self.defaultTestResult() + startTestRun = getattr(result, 'startTestRun', None) + stopTestRun = getattr(result, 'stopTestRun', None) + if startTestRun is not None: + startTestRun() + else: + stopTestRun = None + result.startTest(self) + try: + testMethod = getattr(self, self._testMethodName) + if ( + getattr(self.__class__, "__unittest_skip__", False) + or getattr(testMethod, "__unittest_skip__", False) + ): + skip_why = ( + getattr(self.__class__, '__unittest_skip_why__', '') + or getattr(testMethod, '__unittest_skip_why__', '') + ) + self._addSkip(result, self, skip_why) + return result + expecting_failure = ( + getattr(self, "__unittest_expecting_failure__", False) + or getattr(testMethod, "__unittest_expecting_failure__", False) + ) + outcome = unittest_helper._Outcome(result) + try: + self._outcome = outcome + with outcome.testPartExecutor(self): + self._callSetUp() + if outcome.success: + outcome.expecting_failure = expecting_failure + with outcome.testPartExecutor(self, isTest=True): + self._callTestMethod(testMethod) + outcome.expecting_failure = False + with outcome.testPartExecutor(self): + self._callTearDown() + self.doCleanups() + for test, reason in outcome.skipped: + self._addSkip(result, test, reason) + for test, exc_info in outcome.errors: + if exc_info is not None: + if issubclass(exc_info[0], self.failureException): + result.addFailure(test, exc_info) + else: + result.addError(test, exc_info) + if outcome.success: + if expecting_failure: + if outcome.expectedFailure: + self._addExpectedFailure( + result, outcome.expectedFailure + ) + else: + self._addUnexpectedSuccess(result) + else: + result.addSuccess(self) + return result + finally: + outcome.errors.clear() + outcome.expectedFailure = None + self._outcome = None + finally: + result.stopTest(self) + if stopTestRun is not None: + stopTestRun() + def tearDown(self): """This method runs after every test completes. Be careful if a subclass of BaseCase overrides setUp(). If so, add the following line to the subclass's tearDown() method: - super().tearDown() - """ + super().tearDown() """ if not hasattr(self, "_using_sb_fixture") and self.__called_teardown: # This test already called tearDown() return @@ -14900,7 +14977,10 @@ class BaseCase(unittest.TestCase): self.__add_pytest_html_extra() sb_config._has_logs = True elif ( - (python3_11_or_newer or "--pdb" in sys.argv) + ( + (python3_11_or_newer and py311_patch2) + or "--pdb" in sys.argv + ) and not has_exception ): # Handle a bug where exceptions aren't seen diff --git a/seleniumbase/fixtures/constants.py b/seleniumbase/fixtures/constants.py index 0dc5cf0a..1e852be1 100644 --- a/seleniumbase/fixtures/constants.py +++ b/seleniumbase/fixtures/constants.py @@ -41,6 +41,13 @@ class ValidEnvs: ] +class PatchPy311: + # Now that unittest is "patched/fixed" in Python 3.11 and up, + # this second patch might not be needed to fix error-handling. + # Enabling this might slow things slightly to fix some things. + PATCH = False + + class PageLoadStrategy: # Usage Example => "--pls=none" NORMAL = "normal" diff --git a/seleniumbase/fixtures/page_actions.py b/seleniumbase/fixtures/page_actions.py index ed6f72f0..a12de6df 100644 --- a/seleniumbase/fixtures/page_actions.py +++ b/seleniumbase/fixtures/page_actions.py @@ -23,7 +23,6 @@ By.PARTIAL_LINK_TEXT # "partial link text" import codecs import os import time -import unittest from selenium.common.exceptions import ElementNotInteractableException from selenium.common.exceptions import ElementNotVisibleException from selenium.common.exceptions import NoAlertPresentException @@ -225,7 +224,6 @@ def hover_element(driver, element): def timeout_exception(exception, message): - unittest.has_exception = True exc, msg = shared_utils.format_exc(exception, message) raise exc(msg) diff --git a/seleniumbase/fixtures/unittest_helper.py b/seleniumbase/fixtures/unittest_helper.py new file mode 100644 index 00000000..e672a56d --- /dev/null +++ b/seleniumbase/fixtures/unittest_helper.py @@ -0,0 +1,41 @@ +import sys +from contextlib import contextmanager +from unittest.case import _ShouldStop, SkipTest + + +class _Outcome(object): + def __init__(self, result=None): + self.expecting_failure = False + self.result = result + self.result_supports_subtests = hasattr(result, "addSubTest") + self.success = True + self.skipped = [] + self.expectedFailure = None + self.errors = [] + + @contextmanager + def testPartExecutor(self, test_case, isTest=False): + old_success = self.success + self.success = True + try: + yield + except KeyboardInterrupt: + raise + except SkipTest as e: + self.success = False + self.skipped.append((test_case, str(e))) + except _ShouldStop: + pass + except Exception: + exc_info = sys.exc_info() + if self.expecting_failure: + self.expectedFailure = exc_info + else: + self.success = False + self.errors.append((test_case, exc_info)) + exc_info = None + else: + if self.result_supports_subtests and self.success: + self.errors.append((test_case, None)) + finally: + self.success = self.success and old_success diff --git a/seleniumbase/plugins/base_plugin.py b/seleniumbase/plugins/base_plugin.py index fe64836f..137842ad 100644 --- a/seleniumbase/plugins/base_plugin.py +++ b/seleniumbase/plugins/base_plugin.py @@ -13,6 +13,7 @@ from seleniumbase.fixtures import constants python3_11_or_newer = False if sys.version_info >= (3, 11): python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH = True class Base(Plugin): @@ -276,7 +277,7 @@ class Base(Plugin): test, self.test_count, self.duration ) ) - if python3_11_or_newer: + if python3_11_or_newer and py311_patch2: # Handle a bug on Python 3.11 where exceptions aren't seen sb_config._browser_version = None try: diff --git a/seleniumbase/plugins/pytest_plugin.py b/seleniumbase/plugins/pytest_plugin.py index 2de7baac..b5f356bc 100644 --- a/seleniumbase/plugins/pytest_plugin.py +++ b/seleniumbase/plugins/pytest_plugin.py @@ -15,6 +15,7 @@ if sys.platform in ["win32", "win64", "x64"]: python3_11_or_newer = False if sys.version_info >= (3, 11): python3_11_or_newer = True +py311_patch2 = constants.PatchPy311.PATCH sys_argv = sys.argv pytest_plugins = ["pytester"] # Adds the "testdir" fixture @@ -1876,7 +1877,7 @@ def pytest_runtest_teardown(item): if ( ( sb_config._has_exception - or python3_11_or_newer + or (python3_11_or_newer and py311_patch2) or "--pdb" in sys_argv ) and sb_config.list_fp @@ -2209,7 +2210,7 @@ def pytest_runtest_makereport(item, call): sb_config._extra_dash_entries.append(test_id) elif ( sb_config._sbase_detected - and (python3_11_or_newer or "--pdb" in sys_argv) + and ((python3_11_or_newer and py311_patch2) or "--pdb" in sys_argv) and (report.outcome == "failed" or "AssertionError" in str(call)) and not sb_config._has_exception ):