From 02f60f11ae3395869a26b9f04dca0e0b123bf036 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dan=20=C4=8Cerm=C3=A1k?= Date: Sun, 11 Feb 2018 23:43:42 +0100 Subject: [PATCH] [testsuite] Reimplemented a part of system_tests.CopyFiles - main file handling functionality is performed by a base class => can be easier extended if required --- tests/system_tests.py | 281 +++++++++++++++++++++++++++++++++--------- 1 file changed, 226 insertions(+), 55 deletions(-) diff --git a/tests/system_tests.py b/tests/system_tests.py index 8e75b0de..ec93dcf4 100644 --- a/tests/system_tests.py +++ b/tests/system_tests.py @@ -156,83 +156,254 @@ def configure_suite(config_file): _parameters[key] = abs_path -def _setUp_factory(old_setUp, *files): +class FileDecoratorBase(object): """ - Factory function that returns a setUp function suitable to replace the - existing setUp of a unittest.TestCase. The returned setUp calls at first - old_setUp(self) and then creates a copy of all files in *files with the - name: fname.ext -> fname_copy.ext - - All file names in *files are at first expanded using self.expand_variables() - and the path to the copy is saved in self._file_copies + Base class for decorators that manipulate files for test cases. + + The decorator expects to be provided with at least one file path + on construction. When called, it replaces the setUp() and + tearDown() functions of the type it is called on with custom ones. + + The new setUp() function performs the following steps: + - call the old setUp() + - create a list _files in the decorated class + - iterate over all files, performing: + - expand the file's path via expand_variables (member function + of the decorated class) + - call self.setUp_file_action(expanded file name) + - append the result to _files in the decorated class + + The function self.setUp_file_action is provided by this class and + is intended to be overridden by child classes to provide some + functionality, like file copies. + + + The new tearDown() function performs the following steps: + - iterate over all files in _files (from the decorated class): + - call self.tearDown_file_action(filename) + - call the old tearDown() function + + The function self.tearDown_file_action can be overridden by child + classes. The default version provided by this class simply deletes + all files that are passed to it. + + + Example + ------- + + We'll inherit from FileDecoratorBase and override the member + functions setUp_file_action and tearDown_file_action: + + >>> class TestDecorator(FileDecoratorBase): + ... def setUp_file_action(self, f): + ... print("setUp_file_action with", f) + ... return f.capitalize() + ... + ... def tearDown_file_action(self, f): + ... print("tearDown_file_action with", f) + + Then, we use that decorator to wrap a class mocking + system_tests.Case: + + >>> @TestDecorator("one", "two", "three") + ... class MockCase(object): + ... def setUp(self): + ... print("calling MockCase.setUp()") + ... + ... def tearDown(self): + ... print("calling MockCase.tearDown()") + ... + ... def expand_variables(self, var): + ... return var + "_file" + + >>> M = MockCase() + + setUp has been replaced by a the new version, but the old one is + still called. The new setUp iterates over all parameters passed to + the constructor of the decorator, passes them to expand_variables + and then to setUp_file_action: + >>> M.setUp() + calling MockCase.setUp() + setUp_file_action with one_file + setUp_file_action with two_file + setUp_file_action with three_file + + The tearDown() function works accordingly: + >>> M.tearDown() + tearDown_file_action with One_file + tearDown_file_action with Two_file + tearDown_file_action with Three_file + calling MockCase.tearDown() + + Please note the capitalized "file" names (this is due to + setUp_file_action returning f.capitalized()) and that the old + tearDown is called after the new one runs. """ - def setUp(self): - old_setUp(self) - self._file_copies = [] - for f in files: - expanded_fname = self.expand_variables(f) - fname, ext = os.path.splitext(expanded_fname) - new_name = fname + '_copy' + ext - self._file_copies.append( - shutil.copyfile(expanded_fname, new_name) - ) - return setUp + def __init__(self, *files): + """ + Constructor of FileDecoratorBase. + + To prevent accidental wrong usage, it raises an exception if + it is not called as a decorator with parameters. + + Only the following syntax works for this decorator: + >>> @FileDecoratorBase("test") + ... class Test(unittest.TestCase): + ... pass + + Calling it without parameters or without parenthesis raises an + exception: + >>> @FileDecoratorBase() + ... class Test(unittest.TestCase): + ... pass + Traceback (most recent call last): + .. + ValueError: No files supplied. + + >>> @FileDecoratorBase + ... class Test(unittest.TestCase): + ... pass + Traceback (most recent call last): + .. + UserWarning: Decorator used wrongly, must be called with filenames in parenthesis + """ + if len(files) == 0: + raise ValueError("No files supplied.") + elif len(files) == 1: + if isinstance(files[0], type): + raise UserWarning( + "Decorator used wrongly, must be called with " + "filenames in parenthesis" + ) -def _tearDown_factory(old_tearDown): - """ - Factory function that returns a new tearDown method to replace an existing - tearDown method. It at first deletes all files in self._file_copies and then - calls old_tearDown(self). - This factory is intended to be used in conjunction with _setUp_factory - """ - def tearDown(self): - for f in self._file_copies: - os.remove(f) - old_tearDown(self) - return tearDown + self._files = files + + def new_setUp(self, old_setUp): + """ + Returns a new setUp() function that can be used as a class + member function (i.e. invoked via self.setUp()). + + It's functionality is described in this classes' docstring. + """ + + def setUp(other): + old_setUp(other) + other._files = [] + for f in self._files: + expanded_fname = other.expand_variables(f) + other._files.append( + self.setUp_file_action(expanded_fname) + ) + return setUp + + def setUp_file_action(self, expanded_file_name): + """ + This function is called on each file that is passed to the + constructor during the call of the decorated class' setUp(). + + Parameters: + - expanded_file_name: the file's path expanded via + expand_variables from system_tests.Case + + Returns: + This function should return a path that will be stored in the + decorated class' list _files. The custom tearDown() function + (that is returned by self.new_tearDown()) iterates over this + list and invokes self.tearDown_file_action on each element in + that list. + E.g. if a child class creates file copies, that should be + deleted after the test ran, then one would have to return the + path of the copy, so that tearDown() can delete the copies. + + The default implementation does nothing. + """ + pass + + def new_tearDown(self, old_tearDown): + """ + Returns a new tearDown() function that can be used as a class + member function. + + It's functionality is described in this classes' docstring. + """ + + def tearDown(other): + for f in other._files: + self.tearDown_file_action(f) + old_tearDown(other) + + return tearDown + + def tearDown_file_action(self, f): + """ + This function is called on each file in the decorated class' + list _files (that list is populated during setUp()). + + It can be used to perform cleanup operations after a test run. + + Parameters: + - f: An element of _files + + Returns: + The return value is ignored + + The default implementation removes f. + """ + os.remove(f) + def __call__(self, cls): + """ + Call operator for the usage as a decorator. It is + automatically used by Python when this class is used as a + decorator. + + Parameters: + - cls: The decorated type. Must be a type + + Returns: + - cls where the setUp and tearDown functions have been + replaced by the functions that are returned by + self.new_setUp() and self.new_tearDown() + """ + if not isinstance(cls, type): + raise ValueError("The decorator must be called on a type") + old_setUp = cls.setUp + cls.setUp = self.new_setUp(old_setUp) -def CopyFiles(*files): + old_tearDown = cls.tearDown + cls.tearDown = self.new_tearDown(old_tearDown) + + return cls + + +class CopyFiles(FileDecoratorBase): """ Decorator for subclasses of system_test.Case that automatically creates a - copy of the files specified as the parameters to the decorator. + copy of the files specified as the parameters passed to the decorator. Example: >>> @CopyFiles("{some_var}/file.txt", "{another_var}/other_file.png") - class Foo(Case): - pass + ... class Foo(Case): + ... pass The decorator will inject new setUp method that at first calls the already defined setUp(), then expands all supplied file names using Case.expand_variables and then creates copies by appending '_copy' before - the file extension. The paths to the copies are stored in self._file_copies. + the file extension. The paths to the copies are stored in self._files. The decorator also injects a new tearDown method that deletes all files in - self._file_copies and then calls the original tearDown method. + self._files and then calls the original tearDown method. This function will also complain if it is called without arguments or without paranthesis, which is valid decorator syntax but is obviously a bug - in this case. + in this case as it can result in tests not being run without a warning. """ - if len(files) == 0: - raise ValueError("No files to copy supplied.") - elif len(files) == 1: - if isinstance(files[0], type): - raise UserWarning( - "Decorator used wrongly, must be called with filenames in paranthesis" - ) - - def wrapper(cls): - old_setUp = cls.setUp - cls.setUp = _setUp_factory(old_setUp, *files) - - old_tearDown = cls.tearDown - cls.tearDown = _tearDown_factory(old_tearDown) - - return cls - return wrapper + def setUp_file_action(self, expanded_file_name): + fname, ext = os.path.splitext(expanded_file_name) + new_name = fname + '_copy' + ext + return shutil.copyfile(expanded_file_name, new_name) class Case(unittest.TestCase):