edq.testing.unittest
1import datetime 2import difflib 3import os 4import typing 5import unittest 6 7import edq.util.dirent 8import edq.util.json 9import edq.util.reflection 10import edq.util.serial 11import edq.util.time 12 13FORMAT_STR: str = "\n--- Expected ---\n%s\n--- Actual ---\n%s\n---\n" 14 15DirentSpec = typing.Tuple[str, typing.Union[str, typing.Dict[str, edq.util.serial.PODType], typing.List['DirentSpec']]] # pylint: disable=invalid-name 16""" 17A simple spec for a directory entry. 18See create_directory_structure(). 19""" 20 21class BaseTest(unittest.TestCase): 22 """ 23 A base class for unit tests. 24 """ 25 26 maxDiff = None 27 """ Don't limit the size of diffs. """ 28 29 testing_timezone: typing.Union[datetime.timezone, None] = edq.util.time.UTC 30 """ The timezone to use. """ 31 32 use_diff_output: bool = True 33 """ Use diff-like comparisons. """ 34 35 @classmethod 36 def setUpClass(cls) -> None: 37 super().setUpClass() 38 39 edq.util.time.set_testing_local_timezone(cls.testing_timezone) 40 41 @classmethod 42 def tearDownClass(cls) -> None: 43 super().tearDownClass() 44 45 edq.util.time.set_testing_local_timezone(None) 46 47 def assertStringEqual(self, expected: str, actual: str, message: typing.Union[str, None] = None, strip: bool = False) -> None: # pylint: disable=invalid-name 48 """ Check that two strings are equal. """ 49 50 expected = str(expected) 51 actual = str(actual) 52 53 if (strip): 54 expected = expected.strip() 55 actual = actual.strip() 56 57 if (actual != expected): 58 if (message is None): 59 message = self._format_comparison_message(expected, actual) 60 61 self.fail(message) 62 63 def assertJSONEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 64 """ 65 Like unittest.TestCase.assertEqual(), 66 but uses expected default assertion message containing the full JSON representation of the arguments. 67 """ 68 69 expected_json = edq.util.json.dumps(expected, indent = 4) 70 actual_json = edq.util.json.dumps(actual, indent = 4) 71 72 if (message is None): 73 message = self._format_comparison_message(expected_json, actual_json) 74 75 super().assertEqual(expected, actual, msg = message) 76 77 def assertJSONDictEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 78 """ 79 Like unittest.TestCase.assertDictEqual(), 80 but will try to convert each comparison argument to a dict if it is not already, 81 and uses a default assertion message containing the full JSON representation of the arguments. 82 """ 83 84 if (not isinstance(expected, dict)): 85 if (isinstance(expected, edq.util.serial.DictConverter)): 86 expected = expected.to_dict(edq.util.serial.SerializationContext()) 87 else: 88 expected = vars(expected) 89 90 if (not isinstance(actual, dict)): 91 if (isinstance(actual, edq.util.serial.DictConverter)): 92 actual = actual.to_dict(edq.util.serial.SerializationContext()) 93 else: 94 actual = vars(actual) 95 96 expected_json = edq.util.json.dumps(expected, indent = 4) 97 actual_json = edq.util.json.dumps(actual, indent = 4) 98 99 if (message is None): 100 message = self._format_comparison_message(expected_json, actual_json) 101 102 super().assertDictEqual(expected, actual, msg = message) 103 104 def assertJSONListEqual(self, # pylint: disable=invalid-name 105 expected: typing.List[typing.Any], 106 actual: typing.List[typing.Any], 107 message: typing.Union[str, None] = None, 108 ) -> None: 109 """ 110 Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments. 111 """ 112 113 expected_json = edq.util.json.dumps(expected, indent = 4) 114 actual_json = edq.util.json.dumps(actual, indent = 4) 115 116 if (message is None): 117 message = self._format_comparison_message(expected_json, actual_json) 118 119 super().assertListEqual(expected, actual, msg = message) 120 121 def assertFileHashEqual(self, expected: str, actual: str) -> None: # pylint: disable=invalid-name 122 """ 123 Assert that the hash of two files matches. 124 Will fail if either path does not exist. 125 """ 126 127 if (not edq.util.dirent.exists(expected)): 128 self.fail(f"File does not exist: '{expected}'.") 129 130 if (not edq.util.dirent.exists(actual)): 131 self.fail(f"File does not exist: '{actual}'.") 132 133 expected_hash = edq.util.dirent.hash_file(expected) 134 actual_hash = edq.util.dirent.hash_file(actual) 135 136 self.assertEqual(expected_hash, actual_hash, msg = f"Hash mismatch: '{expected}' ({expected_hash}) vs '{actual}' ({actual_hash}).") 137 138 def format_error_string(self, ex: typing.Union[BaseException, None]) -> str: 139 """ 140 Format an error string from an exception so it can be checked for testing. 141 The type of the error will be included, 142 and any nested errors will be joined together. 143 """ 144 145 parts = [] 146 147 while (ex is not None): 148 type_name = edq.util.reflection.get_qualified_name(ex) 149 message = str(ex) 150 151 parts.append(f"{type_name}: {message}") 152 153 ex = ex.__cause__ 154 155 return "; ".join(parts) 156 157 def _format_comparison_message(self, expected: str, actual: str) -> str: 158 """ Create a message string comparing the two given strings. """ 159 160 message = FORMAT_STR % (expected, actual) 161 162 if (not self.use_diff_output): 163 return message 164 165 expected_lines = expected.splitlines(keepends = True) 166 actual_lines = actual.splitlines(keepends = True) 167 168 lines = list(difflib.unified_diff(expected_lines, actual_lines, fromfile = 'expected', tofile = 'actual')) 169 message += "\n--- Diff ---\n" + ''.join(lines) + "\n------------" 170 171 return message 172 173def create_directory_structure(spec: typing.List[DirentSpec], base_dir: str) -> None: 174 """ 175 Create a directory strucutre from a simple definition. 176 The definition represents a list of direents. 177 Each dirent is a tuple with the name of a dirent and the contents of the dirent. 178 The contents can be: 179 1) a list if dirents (indicating that this dirent is a directory), 180 2) a dict (indicating that this dirent is a JSON file), 181 or 3) a string (indicating that this dirent is a text file). 182 """ 183 184 for (name, contents) in spec: 185 path = os.path.join(base_dir, name) 186 187 if (isinstance(contents, str)): 188 edq.util.dirent.write_file(path, contents) 189 elif (isinstance(contents, dict)): 190 edq.util.json.dump_path(contents, path, indent = 4) 191 elif (isinstance(contents, list)): 192 edq.util.dirent.mkdir(path) 193 create_directory_structure(contents, path) 194 else: 195 raise ValueError(f"Unknown dirent content type ('{type(contents)}') at path: '{path}'.")
A simple spec for a directory entry. See create_directory_structure().
22class BaseTest(unittest.TestCase): 23 """ 24 A base class for unit tests. 25 """ 26 27 maxDiff = None 28 """ Don't limit the size of diffs. """ 29 30 testing_timezone: typing.Union[datetime.timezone, None] = edq.util.time.UTC 31 """ The timezone to use. """ 32 33 use_diff_output: bool = True 34 """ Use diff-like comparisons. """ 35 36 @classmethod 37 def setUpClass(cls) -> None: 38 super().setUpClass() 39 40 edq.util.time.set_testing_local_timezone(cls.testing_timezone) 41 42 @classmethod 43 def tearDownClass(cls) -> None: 44 super().tearDownClass() 45 46 edq.util.time.set_testing_local_timezone(None) 47 48 def assertStringEqual(self, expected: str, actual: str, message: typing.Union[str, None] = None, strip: bool = False) -> None: # pylint: disable=invalid-name 49 """ Check that two strings are equal. """ 50 51 expected = str(expected) 52 actual = str(actual) 53 54 if (strip): 55 expected = expected.strip() 56 actual = actual.strip() 57 58 if (actual != expected): 59 if (message is None): 60 message = self._format_comparison_message(expected, actual) 61 62 self.fail(message) 63 64 def assertJSONEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 65 """ 66 Like unittest.TestCase.assertEqual(), 67 but uses expected default assertion message containing the full JSON representation of the arguments. 68 """ 69 70 expected_json = edq.util.json.dumps(expected, indent = 4) 71 actual_json = edq.util.json.dumps(actual, indent = 4) 72 73 if (message is None): 74 message = self._format_comparison_message(expected_json, actual_json) 75 76 super().assertEqual(expected, actual, msg = message) 77 78 def assertJSONDictEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 79 """ 80 Like unittest.TestCase.assertDictEqual(), 81 but will try to convert each comparison argument to a dict if it is not already, 82 and uses a default assertion message containing the full JSON representation of the arguments. 83 """ 84 85 if (not isinstance(expected, dict)): 86 if (isinstance(expected, edq.util.serial.DictConverter)): 87 expected = expected.to_dict(edq.util.serial.SerializationContext()) 88 else: 89 expected = vars(expected) 90 91 if (not isinstance(actual, dict)): 92 if (isinstance(actual, edq.util.serial.DictConverter)): 93 actual = actual.to_dict(edq.util.serial.SerializationContext()) 94 else: 95 actual = vars(actual) 96 97 expected_json = edq.util.json.dumps(expected, indent = 4) 98 actual_json = edq.util.json.dumps(actual, indent = 4) 99 100 if (message is None): 101 message = self._format_comparison_message(expected_json, actual_json) 102 103 super().assertDictEqual(expected, actual, msg = message) 104 105 def assertJSONListEqual(self, # pylint: disable=invalid-name 106 expected: typing.List[typing.Any], 107 actual: typing.List[typing.Any], 108 message: typing.Union[str, None] = None, 109 ) -> None: 110 """ 111 Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments. 112 """ 113 114 expected_json = edq.util.json.dumps(expected, indent = 4) 115 actual_json = edq.util.json.dumps(actual, indent = 4) 116 117 if (message is None): 118 message = self._format_comparison_message(expected_json, actual_json) 119 120 super().assertListEqual(expected, actual, msg = message) 121 122 def assertFileHashEqual(self, expected: str, actual: str) -> None: # pylint: disable=invalid-name 123 """ 124 Assert that the hash of two files matches. 125 Will fail if either path does not exist. 126 """ 127 128 if (not edq.util.dirent.exists(expected)): 129 self.fail(f"File does not exist: '{expected}'.") 130 131 if (not edq.util.dirent.exists(actual)): 132 self.fail(f"File does not exist: '{actual}'.") 133 134 expected_hash = edq.util.dirent.hash_file(expected) 135 actual_hash = edq.util.dirent.hash_file(actual) 136 137 self.assertEqual(expected_hash, actual_hash, msg = f"Hash mismatch: '{expected}' ({expected_hash}) vs '{actual}' ({actual_hash}).") 138 139 def format_error_string(self, ex: typing.Union[BaseException, None]) -> str: 140 """ 141 Format an error string from an exception so it can be checked for testing. 142 The type of the error will be included, 143 and any nested errors will be joined together. 144 """ 145 146 parts = [] 147 148 while (ex is not None): 149 type_name = edq.util.reflection.get_qualified_name(ex) 150 message = str(ex) 151 152 parts.append(f"{type_name}: {message}") 153 154 ex = ex.__cause__ 155 156 return "; ".join(parts) 157 158 def _format_comparison_message(self, expected: str, actual: str) -> str: 159 """ Create a message string comparing the two given strings. """ 160 161 message = FORMAT_STR % (expected, actual) 162 163 if (not self.use_diff_output): 164 return message 165 166 expected_lines = expected.splitlines(keepends = True) 167 actual_lines = actual.splitlines(keepends = True) 168 169 lines = list(difflib.unified_diff(expected_lines, actual_lines, fromfile = 'expected', tofile = 'actual')) 170 message += "\n--- Diff ---\n" + ''.join(lines) + "\n------------" 171 172 return message
A base class for unit tests.
36 @classmethod 37 def setUpClass(cls) -> None: 38 super().setUpClass() 39 40 edq.util.time.set_testing_local_timezone(cls.testing_timezone)
Hook method for setting up class fixture before running tests in the class.
42 @classmethod 43 def tearDownClass(cls) -> None: 44 super().tearDownClass() 45 46 edq.util.time.set_testing_local_timezone(None)
Hook method for deconstructing the class fixture after running all tests in the class.
48 def assertStringEqual(self, expected: str, actual: str, message: typing.Union[str, None] = None, strip: bool = False) -> None: # pylint: disable=invalid-name 49 """ Check that two strings are equal. """ 50 51 expected = str(expected) 52 actual = str(actual) 53 54 if (strip): 55 expected = expected.strip() 56 actual = actual.strip() 57 58 if (actual != expected): 59 if (message is None): 60 message = self._format_comparison_message(expected, actual) 61 62 self.fail(message)
Check that two strings are equal.
64 def assertJSONEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 65 """ 66 Like unittest.TestCase.assertEqual(), 67 but uses expected default assertion message containing the full JSON representation of the arguments. 68 """ 69 70 expected_json = edq.util.json.dumps(expected, indent = 4) 71 actual_json = edq.util.json.dumps(actual, indent = 4) 72 73 if (message is None): 74 message = self._format_comparison_message(expected_json, actual_json) 75 76 super().assertEqual(expected, actual, msg = message)
Like unittest.TestCase.assertEqual(), but uses expected default assertion message containing the full JSON representation of the arguments.
78 def assertJSONDictEqual(self, expected: typing.Any, actual: typing.Any, message: typing.Union[str, None] = None) -> None: # pylint: disable=invalid-name 79 """ 80 Like unittest.TestCase.assertDictEqual(), 81 but will try to convert each comparison argument to a dict if it is not already, 82 and uses a default assertion message containing the full JSON representation of the arguments. 83 """ 84 85 if (not isinstance(expected, dict)): 86 if (isinstance(expected, edq.util.serial.DictConverter)): 87 expected = expected.to_dict(edq.util.serial.SerializationContext()) 88 else: 89 expected = vars(expected) 90 91 if (not isinstance(actual, dict)): 92 if (isinstance(actual, edq.util.serial.DictConverter)): 93 actual = actual.to_dict(edq.util.serial.SerializationContext()) 94 else: 95 actual = vars(actual) 96 97 expected_json = edq.util.json.dumps(expected, indent = 4) 98 actual_json = edq.util.json.dumps(actual, indent = 4) 99 100 if (message is None): 101 message = self._format_comparison_message(expected_json, actual_json) 102 103 super().assertDictEqual(expected, actual, msg = message)
Like unittest.TestCase.assertDictEqual(), but will try to convert each comparison argument to a dict if it is not already, and uses a default assertion message containing the full JSON representation of the arguments.
105 def assertJSONListEqual(self, # pylint: disable=invalid-name 106 expected: typing.List[typing.Any], 107 actual: typing.List[typing.Any], 108 message: typing.Union[str, None] = None, 109 ) -> None: 110 """ 111 Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments. 112 """ 113 114 expected_json = edq.util.json.dumps(expected, indent = 4) 115 actual_json = edq.util.json.dumps(actual, indent = 4) 116 117 if (message is None): 118 message = self._format_comparison_message(expected_json, actual_json) 119 120 super().assertListEqual(expected, actual, msg = message)
Call assertDictEqual(), but supply a default message containing the full JSON representation of the arguments.
122 def assertFileHashEqual(self, expected: str, actual: str) -> None: # pylint: disable=invalid-name 123 """ 124 Assert that the hash of two files matches. 125 Will fail if either path does not exist. 126 """ 127 128 if (not edq.util.dirent.exists(expected)): 129 self.fail(f"File does not exist: '{expected}'.") 130 131 if (not edq.util.dirent.exists(actual)): 132 self.fail(f"File does not exist: '{actual}'.") 133 134 expected_hash = edq.util.dirent.hash_file(expected) 135 actual_hash = edq.util.dirent.hash_file(actual) 136 137 self.assertEqual(expected_hash, actual_hash, msg = f"Hash mismatch: '{expected}' ({expected_hash}) vs '{actual}' ({actual_hash}).")
Assert that the hash of two files matches. Will fail if either path does not exist.
139 def format_error_string(self, ex: typing.Union[BaseException, None]) -> str: 140 """ 141 Format an error string from an exception so it can be checked for testing. 142 The type of the error will be included, 143 and any nested errors will be joined together. 144 """ 145 146 parts = [] 147 148 while (ex is not None): 149 type_name = edq.util.reflection.get_qualified_name(ex) 150 message = str(ex) 151 152 parts.append(f"{type_name}: {message}") 153 154 ex = ex.__cause__ 155 156 return "; ".join(parts)
Format an error string from an exception so it can be checked for testing. The type of the error will be included, and any nested errors will be joined together.
174def create_directory_structure(spec: typing.List[DirentSpec], base_dir: str) -> None: 175 """ 176 Create a directory strucutre from a simple definition. 177 The definition represents a list of direents. 178 Each dirent is a tuple with the name of a dirent and the contents of the dirent. 179 The contents can be: 180 1) a list if dirents (indicating that this dirent is a directory), 181 2) a dict (indicating that this dirent is a JSON file), 182 or 3) a string (indicating that this dirent is a text file). 183 """ 184 185 for (name, contents) in spec: 186 path = os.path.join(base_dir, name) 187 188 if (isinstance(contents, str)): 189 edq.util.dirent.write_file(path, contents) 190 elif (isinstance(contents, dict)): 191 edq.util.json.dump_path(contents, path, indent = 4) 192 elif (isinstance(contents, list)): 193 edq.util.dirent.mkdir(path) 194 create_directory_structure(contents, path) 195 else: 196 raise ValueError(f"Unknown dirent content type ('{type(contents)}') at path: '{path}'.")
Create a directory strucutre from a simple definition. The definition represents a list of direents. Each dirent is a tuple with the name of a dirent and the contents of the dirent. The contents can be: 1) a list if dirents (indicating that this dirent is a directory), 2) a dict (indicating that this dirent is a JSON file), or 3) a string (indicating that this dirent is a text file).