diff --git a/testproject/functional_tests/base.py b/testproject/functional_tests/base.py new file mode 100644 --- /dev/null +++ b/testproject/functional_tests/base.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Branko Majic +# +# This file is part of Django Conntrackt. +# +# Django Conntrackt is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# Django Conntrackt is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# Django Conntrackt. If not, see . +# + + +# Standard library imports. +import os +import time + +# Django imports. +from django.contrib.staticfiles.testing import StaticLiveServerTestCase + +# Third-party library imports. +from selenium.common.exceptions import WebDriverException +from selenium import webdriver + + +# Maximum amount of time to wait before assuming a test has +# failed. Used as default for functions that wait. +MAX_WAIT = 10 + +# Delay between subsequent runs of function when using the wait +# decorator. +EXECUTION_DELAY = 0.5 + +# Base directory where the tests are located at. +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) + +# Base directory where the test binaries are located at. +TEST_BINARIES_DIR = os.path.join(BASE_DIR, "..", "..", "test_binaries") + +# Location of Firefox binary. +FIREFOX_BINARY = os.path.join(TEST_BINARIES_DIR, "firefox", "firefox") + +# Location of geckodriver binary. +GECKODRIVER_BINARY = os.path.join(TEST_BINARIES_DIR, "geckodriver") + + +def wait(fn, execution_delay=EXECUTION_DELAY, max_wait=MAX_WAIT): + """ + Decorator that will produce a wrapper function that will run the + specified function repeatedly as long as all of the following + conditions have been met: + + - Function throws either AssertionError or + selenium.common.exceptions.WebDriverException. + - Combined runtime of all function calls does not exceed the + maximum wait time. + + As soon as the original function returns without any thrown + exceptions, the wrapper function will return its result. + + If the function throws any other exception beyond the ones listed + above, this exception will get propagated. + + Arguments: + + fn + Function that should be run. + + max_wait + Maximum time in seconds to wait for function to execute + successfully. + + execution_delay + Delay in seconds between subsequent function runs. Used to + prevent high CPU usage. + + Returns: + + Wrapper function around passed-in function. The wrapper + function, provided it executes without failure, returns the + result of original function. + """ + + def modified_fn(*args, **kwargs): + start_time = time.time() + + while True: + try: + return fn(*args, **kwargs) + except (AssertionError, WebDriverException) as e: + if time.time() - start_time > max_wait: + raise e + time.sleep(execution_delay) + + return modified_fn + + +class FunctionalTest(StaticLiveServerTestCase): + """ + Helper test class that provides some convenience when writing + functional tests. + + During test set-up, the class will: + + - Set-up a browser instance, storing it in property self.browser. + + During the test tear-down, the class will: + + - Destroy the browser instance. + + In addition to this, the class provides a convenience method + wait_for which can be used to wait for a function to execute + within a time slot and assert something, as described for the wait + decorator. + """ + + def setUp(self): + self.browser = webdriver.Firefox(firefox_binary=FIREFOX_BINARY, executable_path=GECKODRIVER_BINARY) + + def tearDown(self): + self.browser.quit() + super(FunctionalTest, self).tearDown() + + @wait + def wait_for(self, fn): + return fn()