lms.backend.testing
1import glob 2import os 3import typing 4 5import edq.core.log 6import edq.net.exchangeserver 7import edq.testing.cli 8import edq.testing.unittest 9import edq.testing.httpserver 10import edq.util.pyimport 11 12import lms.model.backend 13import lms.model.base 14import lms.backend.instance 15 16THIS_DIR: str = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 17TESTDATA_DIR: str = os.path.join(THIS_DIR, 'testdata') 18 19BACKEND_TESTS_DIR: str = os.path.join(TESTDATA_DIR, 'backendtests') 20 21CLI_TESTDATA_DIR: str = os.path.join(TESTDATA_DIR, 'cli') 22CLI_TESTS_DIR: str = os.path.join(CLI_TESTDATA_DIR, 'tests') 23CLI_DATA_DIR: str = os.path.join(CLI_TESTDATA_DIR, 'data') 24CLI_GLOBAL_CONFG_PATH: str = os.path.join(CLI_DATA_DIR, 'testing-edq-lms.json') 25 26TEST_FUNC_NAME_PREFIX: str = 'test_' 27TEST_FILENAME_GLOB_PATTERN: str = '*_backendtest.py' 28 29class BackendTest(edq.testing.httpserver.HTTPServerTest): 30 """ 31 A special test suite that is common across all LMS backends. 32 33 This is an HTTP test that will start a test server with exchanges specific to the target backend. 34 35 A common directory (BACKEND_TESTS_DIR) will be searched for any file that starts with TEST_FILENAME_GLOB_PATTERN. 36 Then, that file will be checked for any function that starts with TEST_FUNC_NAME_PREFIX and matches BackendTestFunction. 37 """ 38 39 backend_type: typing.Union[str, None] = None 40 """ 41 The backend type for this test. 42 Must be set by the child class. 43 """ 44 45 exchanges_dir: typing.Union[str, None] = None 46 """ 47 The directory to load HTTP exchanges from. 48 Must be set by the child class. 49 """ 50 51 params_to_skip: typing.List[str] = [] 52 """ Parameters to skip while looking up exchanges. """ 53 54 headers_to_skip: typing.List[str] = [] 55 """ Headers to skip while looking up exchanges. """ 56 57 backend: typing.Union[lms.model.backend.APIBackend, None] = None 58 """ 59 The backend for this test. 60 Will be created during setup_server(). 61 """ 62 63 backend_args: typing.Dict[str, typing.Any] = { 64 'testing': True, 65 } 66 """ Any additional arguments to send to get_backend(). """ 67 68 skip_base_request_test: bool = False 69 """ Skip any base request tests. """ 70 71 allowed_backend: typing.Union[str, None] = None 72 """ If set, skip any backend tests that do not match this filter. """ 73 74 def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 75 super().__init__(*args, **kwargs) 76 77 self._user_email: typing.Union[str, None] = None 78 """ 79 The email of the current user for this backend. 80 Setting the user allows child classes to fetch specific information (like authentication information). 81 """ 82 83 @classmethod 84 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 85 if (cls.server_key == ''): 86 raise ValueError("BackendTest subclass did not set server key properly.") 87 88 if (cls.backend_type is None): 89 raise ValueError("BackendTest subclass did not set backend type properly.") 90 91 if (cls.exchanges_dir is None): 92 raise ValueError("BackendTest subclass did not set exchanges dir properly.") 93 94 edq.testing.httpserver.HTTPServerTest.setup_server(server) 95 server.load_exchanges_dir(cls.exchanges_dir) 96 97 # Update match options. 98 for (key, values) in [('params_to_skip', cls.params_to_skip), ('headers_to_skip', cls.headers_to_skip)]: 99 if (key not in server.match_options): 100 server.match_options[key] = [] 101 102 server.match_options[key] += values 103 104 @classmethod 105 def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 106 cls.backend = lms.backend.instance.get_backend(cls.get_server_url(), backend_type = cls.backend_type, **cls.backend_args) 107 108 @classmethod 109 def get_base_args(cls) -> typing.Dict[str, typing.Any]: 110 """ Get a copy of the base arguments for a request (function). """ 111 112 return {} 113 114 def setUp(self) -> None: 115 edq.core.log.init('ERROR') 116 117 self.clear_user() 118 119 def set_user(self, email: str) -> None: 120 """ 121 Set the current user for this test. 122 This can be especially useful for child classes that need to set information based on the user 123 (like authentication headers). 124 """ 125 126 self._user_email = email 127 128 def clear_user(self) -> None: 129 """ 130 Clear the current user for this test. 131 This is automatically called before each test method. 132 """ 133 134 self._user_email = None 135 136 def base_request_test(self, 137 request_function: typing.Callable, 138 test_cases: typing.List[typing.Tuple[typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 139 stop_on_notimplemented: bool = True, 140 actual_clean_func: typing.Union[typing.Callable, None] = None, 141 expected_clean_func: typing.Union[typing.Callable, None] = None, 142 assertion_func: typing.Union[typing.Callable, None] = None, 143 ) -> None: 144 """ 145 A common test for the base request functionality. 146 Test cases are passed in as: `[(kwargs (and overrides), expected, error substring), ...]`. 147 """ 148 149 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 150 self.skipTest(f"Backend {self.backend_type} has been filtered.") 151 152 skip_reason = None 153 154 for (i, test_case) in enumerate(test_cases): 155 (extra_kwargs, expected, error_substring) = test_case 156 157 with self.subTest(msg = f"Case {i}:"): 158 kwargs = self.get_base_args() 159 kwargs.update(extra_kwargs) 160 161 try: 162 actual = request_function(**kwargs) 163 except NotImplementedError as ex: 164 # We must handle this directly since we are in a subtest. 165 if (stop_on_notimplemented): 166 skip_reason = str(ex) 167 break 168 169 self.skipTest(f"Backend component not implemented: {str(ex)}.") 170 except Exception as ex: 171 error_string = self.format_error_string(ex) 172 if (error_substring is None): 173 self.fail(f"Unexpected error: '{error_string}'.") 174 175 self.assertIn(error_substring, error_string, 'Error is not as expected.') 176 177 continue 178 179 if (error_substring is not None): 180 self.fail(f"Did not get expected error: '{error_substring}'.") 181 182 if (actual_clean_func is not None): 183 actual = actual_clean_func(actual) 184 185 if (expected_clean_func is not None): 186 expected = expected_clean_func(expected) 187 188 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 189 if (isinstance(expected, tuple)): 190 if (not isinstance(actual, tuple)): 191 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 192 193 if (len(expected) != len(actual)): 194 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 195 else: 196 # Wrap the results in a tuple. 197 expected = (expected, ) 198 actual = (actual, ) 199 200 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 201 expected_value = expected[i] 202 actual_value = actual[i] 203 204 if (assertion_func is not None): 205 assertion_func(expected_value, actual_value) 206 elif (isinstance(expected_value, lms.model.base.BaseType)): 207 self.assertJSONEqual(expected_value, actual_value) 208 elif (isinstance(expected_value, dict)): 209 self.assertJSONDictEqual(expected_value, actual_value) 210 elif (isinstance(expected_value, list)): 211 self.assertJSONListEqual(expected_value, actual_value) 212 else: 213 self.assertEqual(expected_value, actual_value) 214 215 if (skip_reason is not None): 216 self.skipTest(f"Backend component not implemented: {skip_reason}.") 217 218 def modify_cli_test_info(self, test_info: edq.testing.cli.CLITestInfo) -> None: 219 """ Adjust the CLI test info to include core info (like server information). """ 220 221 test_info.arguments += [ 222 '--config-global', CLI_GLOBAL_CONFG_PATH, 223 '--server', self.get_server_url(), 224 '--server-type', str(self.backend_type), 225 '--config', 'testing=true', 226 ] 227 228 # Mark this CLI test for skipping based on the backend filter. 229 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 230 test_info.skip_reasons.append(f"Backend {self.backend_type} has been filtered.") 231 232 @classmethod 233 def get_test_basename(cls, path: str) -> str: 234 """ Get the test's name based off of its filename and location. """ 235 236 path = os.path.abspath(path) 237 238 name = os.path.splitext(os.path.basename(path))[0] 239 240 ancestors = os.path.dirname(path).replace(CLI_TESTS_DIR, '') 241 prefix = ancestors.replace(os.sep, '_') 242 243 if (prefix.startswith('_')): 244 prefix = prefix.replace('_', '', 1) 245 246 if (len(prefix) > 0): 247 name = f"{prefix}_{name}" 248 249 return name 250 251@typing.runtime_checkable 252class BackendTestFunction(typing.Protocol): 253 """ 254 A test function for backend tests. 255 A copy of this function will be attached to a test class created for each backend. 256 Therefore, `self` will be an instance of BackendTest. 257 """ 258 259 def __call__(self, test: BackendTest) -> None: 260 """ 261 A unit test for a BackendTest. 262 """ 263 264def _wrap_test_function(test_function: BackendTestFunction) -> typing.Callable: 265 """ Wrap the backend test function in some common code for backend tests. """ 266 267 def __method(self: BackendTest) -> None: 268 try: 269 test_function(self) 270 except NotImplementedError as ex: 271 # Skip tests for backend component that do not have implementations. 272 self.skipTest(f"Backend component not implemented: {str(ex)}.") 273 274 return __method 275 276def add_test_path(target_class: type, path: str) -> None: 277 """ Add tests from the given test files. """ 278 279 test_module = edq.util.pyimport.import_path(path) 280 281 for attr_name in sorted(dir(test_module)): 282 if (not attr_name.startswith(TEST_FUNC_NAME_PREFIX)): 283 continue 284 285 test_function = getattr(test_module, attr_name) 286 setattr(target_class, attr_name, _wrap_test_function(test_function)) 287 288def discover_test_cases(target_class: type) -> None: 289 """ Look in the text cases directory for any test cases and add them as test methods to the test class. """ 290 291 paths = list(sorted(glob.glob(os.path.join(BACKEND_TESTS_DIR, "**", TEST_FILENAME_GLOB_PATTERN), recursive = True))) 292 for path in sorted(paths): 293 add_test_path(target_class, path) 294 295def attach_test_cases(target_class: type) -> None: 296 """ Attach all the standard test cases to the given class. """ 297 298 # Attach backend tests. 299 discover_test_cases(target_class) 300 301 # Attach CLI tests. 302 edq.testing.cli.discover_test_cases(target_class, CLI_TESTS_DIR, CLI_DATA_DIR, test_method_wrapper = _wrap_cli_test_method) 303 304def _wrap_cli_test_method(test_method: typing.Callable, test_info_path: str) -> typing.Callable: 305 """ Wrap the CLI tests to ignore NotImplemented errors. """ 306 307 def __method(self: edq.testing.unittest.BaseTest) -> None: 308 try: 309 test_method(self, reraise_exception_types = (NotImplementedError,)) 310 except NotImplementedError as ex: 311 # Skip tests for backend component that do not have implementations. 312 self.skipTest(f"Backend component not implemented: {str(ex)}.") 313 314 return __method
30class BackendTest(edq.testing.httpserver.HTTPServerTest): 31 """ 32 A special test suite that is common across all LMS backends. 33 34 This is an HTTP test that will start a test server with exchanges specific to the target backend. 35 36 A common directory (BACKEND_TESTS_DIR) will be searched for any file that starts with TEST_FILENAME_GLOB_PATTERN. 37 Then, that file will be checked for any function that starts with TEST_FUNC_NAME_PREFIX and matches BackendTestFunction. 38 """ 39 40 backend_type: typing.Union[str, None] = None 41 """ 42 The backend type for this test. 43 Must be set by the child class. 44 """ 45 46 exchanges_dir: typing.Union[str, None] = None 47 """ 48 The directory to load HTTP exchanges from. 49 Must be set by the child class. 50 """ 51 52 params_to_skip: typing.List[str] = [] 53 """ Parameters to skip while looking up exchanges. """ 54 55 headers_to_skip: typing.List[str] = [] 56 """ Headers to skip while looking up exchanges. """ 57 58 backend: typing.Union[lms.model.backend.APIBackend, None] = None 59 """ 60 The backend for this test. 61 Will be created during setup_server(). 62 """ 63 64 backend_args: typing.Dict[str, typing.Any] = { 65 'testing': True, 66 } 67 """ Any additional arguments to send to get_backend(). """ 68 69 skip_base_request_test: bool = False 70 """ Skip any base request tests. """ 71 72 allowed_backend: typing.Union[str, None] = None 73 """ If set, skip any backend tests that do not match this filter. """ 74 75 def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 76 super().__init__(*args, **kwargs) 77 78 self._user_email: typing.Union[str, None] = None 79 """ 80 The email of the current user for this backend. 81 Setting the user allows child classes to fetch specific information (like authentication information). 82 """ 83 84 @classmethod 85 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 86 if (cls.server_key == ''): 87 raise ValueError("BackendTest subclass did not set server key properly.") 88 89 if (cls.backend_type is None): 90 raise ValueError("BackendTest subclass did not set backend type properly.") 91 92 if (cls.exchanges_dir is None): 93 raise ValueError("BackendTest subclass did not set exchanges dir properly.") 94 95 edq.testing.httpserver.HTTPServerTest.setup_server(server) 96 server.load_exchanges_dir(cls.exchanges_dir) 97 98 # Update match options. 99 for (key, values) in [('params_to_skip', cls.params_to_skip), ('headers_to_skip', cls.headers_to_skip)]: 100 if (key not in server.match_options): 101 server.match_options[key] = [] 102 103 server.match_options[key] += values 104 105 @classmethod 106 def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 107 cls.backend = lms.backend.instance.get_backend(cls.get_server_url(), backend_type = cls.backend_type, **cls.backend_args) 108 109 @classmethod 110 def get_base_args(cls) -> typing.Dict[str, typing.Any]: 111 """ Get a copy of the base arguments for a request (function). """ 112 113 return {} 114 115 def setUp(self) -> None: 116 edq.core.log.init('ERROR') 117 118 self.clear_user() 119 120 def set_user(self, email: str) -> None: 121 """ 122 Set the current user for this test. 123 This can be especially useful for child classes that need to set information based on the user 124 (like authentication headers). 125 """ 126 127 self._user_email = email 128 129 def clear_user(self) -> None: 130 """ 131 Clear the current user for this test. 132 This is automatically called before each test method. 133 """ 134 135 self._user_email = None 136 137 def base_request_test(self, 138 request_function: typing.Callable, 139 test_cases: typing.List[typing.Tuple[typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 140 stop_on_notimplemented: bool = True, 141 actual_clean_func: typing.Union[typing.Callable, None] = None, 142 expected_clean_func: typing.Union[typing.Callable, None] = None, 143 assertion_func: typing.Union[typing.Callable, None] = None, 144 ) -> None: 145 """ 146 A common test for the base request functionality. 147 Test cases are passed in as: `[(kwargs (and overrides), expected, error substring), ...]`. 148 """ 149 150 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 151 self.skipTest(f"Backend {self.backend_type} has been filtered.") 152 153 skip_reason = None 154 155 for (i, test_case) in enumerate(test_cases): 156 (extra_kwargs, expected, error_substring) = test_case 157 158 with self.subTest(msg = f"Case {i}:"): 159 kwargs = self.get_base_args() 160 kwargs.update(extra_kwargs) 161 162 try: 163 actual = request_function(**kwargs) 164 except NotImplementedError as ex: 165 # We must handle this directly since we are in a subtest. 166 if (stop_on_notimplemented): 167 skip_reason = str(ex) 168 break 169 170 self.skipTest(f"Backend component not implemented: {str(ex)}.") 171 except Exception as ex: 172 error_string = self.format_error_string(ex) 173 if (error_substring is None): 174 self.fail(f"Unexpected error: '{error_string}'.") 175 176 self.assertIn(error_substring, error_string, 'Error is not as expected.') 177 178 continue 179 180 if (error_substring is not None): 181 self.fail(f"Did not get expected error: '{error_substring}'.") 182 183 if (actual_clean_func is not None): 184 actual = actual_clean_func(actual) 185 186 if (expected_clean_func is not None): 187 expected = expected_clean_func(expected) 188 189 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 190 if (isinstance(expected, tuple)): 191 if (not isinstance(actual, tuple)): 192 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 193 194 if (len(expected) != len(actual)): 195 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 196 else: 197 # Wrap the results in a tuple. 198 expected = (expected, ) 199 actual = (actual, ) 200 201 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 202 expected_value = expected[i] 203 actual_value = actual[i] 204 205 if (assertion_func is not None): 206 assertion_func(expected_value, actual_value) 207 elif (isinstance(expected_value, lms.model.base.BaseType)): 208 self.assertJSONEqual(expected_value, actual_value) 209 elif (isinstance(expected_value, dict)): 210 self.assertJSONDictEqual(expected_value, actual_value) 211 elif (isinstance(expected_value, list)): 212 self.assertJSONListEqual(expected_value, actual_value) 213 else: 214 self.assertEqual(expected_value, actual_value) 215 216 if (skip_reason is not None): 217 self.skipTest(f"Backend component not implemented: {skip_reason}.") 218 219 def modify_cli_test_info(self, test_info: edq.testing.cli.CLITestInfo) -> None: 220 """ Adjust the CLI test info to include core info (like server information). """ 221 222 test_info.arguments += [ 223 '--config-global', CLI_GLOBAL_CONFG_PATH, 224 '--server', self.get_server_url(), 225 '--server-type', str(self.backend_type), 226 '--config', 'testing=true', 227 ] 228 229 # Mark this CLI test for skipping based on the backend filter. 230 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 231 test_info.skip_reasons.append(f"Backend {self.backend_type} has been filtered.") 232 233 @classmethod 234 def get_test_basename(cls, path: str) -> str: 235 """ Get the test's name based off of its filename and location. """ 236 237 path = os.path.abspath(path) 238 239 name = os.path.splitext(os.path.basename(path))[0] 240 241 ancestors = os.path.dirname(path).replace(CLI_TESTS_DIR, '') 242 prefix = ancestors.replace(os.sep, '_') 243 244 if (prefix.startswith('_')): 245 prefix = prefix.replace('_', '', 1) 246 247 if (len(prefix) > 0): 248 name = f"{prefix}_{name}" 249 250 return name
A special test suite that is common across all LMS backends.
This is an HTTP test that will start a test server with exchanges specific to the target backend.
A common directory (BACKEND_TESTS_DIR) will be searched for any file that starts with TEST_FILENAME_GLOB_PATTERN. Then, that file will be checked for any function that starts with TEST_FUNC_NAME_PREFIX and matches BackendTestFunction.
75 def __init__(self, *args: typing.Any, **kwargs: typing.Any) -> None: 76 super().__init__(*args, **kwargs) 77 78 self._user_email: typing.Union[str, None] = None 79 """ 80 The email of the current user for this backend. 81 Setting the user allows child classes to fetch specific information (like authentication information). 82 """
Create an instance of the class that will use the named test method when executed. Raises a ValueError if the instance does not have a method with the specified name.
The directory to load HTTP exchanges from. Must be set by the child class.
84 @classmethod 85 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 86 if (cls.server_key == ''): 87 raise ValueError("BackendTest subclass did not set server key properly.") 88 89 if (cls.backend_type is None): 90 raise ValueError("BackendTest subclass did not set backend type properly.") 91 92 if (cls.exchanges_dir is None): 93 raise ValueError("BackendTest subclass did not set exchanges dir properly.") 94 95 edq.testing.httpserver.HTTPServerTest.setup_server(server) 96 server.load_exchanges_dir(cls.exchanges_dir) 97 98 # Update match options. 99 for (key, values) in [('params_to_skip', cls.params_to_skip), ('headers_to_skip', cls.headers_to_skip)]: 100 if (key not in server.match_options): 101 server.match_options[key] = [] 102 103 server.match_options[key] += values
An opportunity for child classes to configure the test server before starting it.
105 @classmethod 106 def post_start_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 107 cls.backend = lms.backend.instance.get_backend(cls.get_server_url(), backend_type = cls.backend_type, **cls.backend_args)
An opportunity for child classes to work with the server after it has been started, but before any tests.
109 @classmethod 110 def get_base_args(cls) -> typing.Dict[str, typing.Any]: 111 """ Get a copy of the base arguments for a request (function). """ 112 113 return {}
Get a copy of the base arguments for a request (function).
120 def set_user(self, email: str) -> None: 121 """ 122 Set the current user for this test. 123 This can be especially useful for child classes that need to set information based on the user 124 (like authentication headers). 125 """ 126 127 self._user_email = email
Set the current user for this test. This can be especially useful for child classes that need to set information based on the user (like authentication headers).
129 def clear_user(self) -> None: 130 """ 131 Clear the current user for this test. 132 This is automatically called before each test method. 133 """ 134 135 self._user_email = None
Clear the current user for this test. This is automatically called before each test method.
137 def base_request_test(self, 138 request_function: typing.Callable, 139 test_cases: typing.List[typing.Tuple[typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 140 stop_on_notimplemented: bool = True, 141 actual_clean_func: typing.Union[typing.Callable, None] = None, 142 expected_clean_func: typing.Union[typing.Callable, None] = None, 143 assertion_func: typing.Union[typing.Callable, None] = None, 144 ) -> None: 145 """ 146 A common test for the base request functionality. 147 Test cases are passed in as: `[(kwargs (and overrides), expected, error substring), ...]`. 148 """ 149 150 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 151 self.skipTest(f"Backend {self.backend_type} has been filtered.") 152 153 skip_reason = None 154 155 for (i, test_case) in enumerate(test_cases): 156 (extra_kwargs, expected, error_substring) = test_case 157 158 with self.subTest(msg = f"Case {i}:"): 159 kwargs = self.get_base_args() 160 kwargs.update(extra_kwargs) 161 162 try: 163 actual = request_function(**kwargs) 164 except NotImplementedError as ex: 165 # We must handle this directly since we are in a subtest. 166 if (stop_on_notimplemented): 167 skip_reason = str(ex) 168 break 169 170 self.skipTest(f"Backend component not implemented: {str(ex)}.") 171 except Exception as ex: 172 error_string = self.format_error_string(ex) 173 if (error_substring is None): 174 self.fail(f"Unexpected error: '{error_string}'.") 175 176 self.assertIn(error_substring, error_string, 'Error is not as expected.') 177 178 continue 179 180 if (error_substring is not None): 181 self.fail(f"Did not get expected error: '{error_substring}'.") 182 183 if (actual_clean_func is not None): 184 actual = actual_clean_func(actual) 185 186 if (expected_clean_func is not None): 187 expected = expected_clean_func(expected) 188 189 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 190 if (isinstance(expected, tuple)): 191 if (not isinstance(actual, tuple)): 192 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 193 194 if (len(expected) != len(actual)): 195 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 196 else: 197 # Wrap the results in a tuple. 198 expected = (expected, ) 199 actual = (actual, ) 200 201 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 202 expected_value = expected[i] 203 actual_value = actual[i] 204 205 if (assertion_func is not None): 206 assertion_func(expected_value, actual_value) 207 elif (isinstance(expected_value, lms.model.base.BaseType)): 208 self.assertJSONEqual(expected_value, actual_value) 209 elif (isinstance(expected_value, dict)): 210 self.assertJSONDictEqual(expected_value, actual_value) 211 elif (isinstance(expected_value, list)): 212 self.assertJSONListEqual(expected_value, actual_value) 213 else: 214 self.assertEqual(expected_value, actual_value) 215 216 if (skip_reason is not None): 217 self.skipTest(f"Backend component not implemented: {skip_reason}.")
A common test for the base request functionality.
Test cases are passed in as: [(kwargs (and overrides), expected, error substring), ...].
219 def modify_cli_test_info(self, test_info: edq.testing.cli.CLITestInfo) -> None: 220 """ Adjust the CLI test info to include core info (like server information). """ 221 222 test_info.arguments += [ 223 '--config-global', CLI_GLOBAL_CONFG_PATH, 224 '--server', self.get_server_url(), 225 '--server-type', str(self.backend_type), 226 '--config', 'testing=true', 227 ] 228 229 # Mark this CLI test for skipping based on the backend filter. 230 if ((self.allowed_backend is not None) and (self.allowed_backend != self.backend_type)): 231 test_info.skip_reasons.append(f"Backend {self.backend_type} has been filtered.")
Adjust the CLI test info to include core info (like server information).
233 @classmethod 234 def get_test_basename(cls, path: str) -> str: 235 """ Get the test's name based off of its filename and location. """ 236 237 path = os.path.abspath(path) 238 239 name = os.path.splitext(os.path.basename(path))[0] 240 241 ancestors = os.path.dirname(path).replace(CLI_TESTS_DIR, '') 242 prefix = ancestors.replace(os.sep, '_') 243 244 if (prefix.startswith('_')): 245 prefix = prefix.replace('_', '', 1) 246 247 if (len(prefix) > 0): 248 name = f"{prefix}_{name}" 249 250 return name
Get the test's name based off of its filename and location.
252@typing.runtime_checkable 253class BackendTestFunction(typing.Protocol): 254 """ 255 A test function for backend tests. 256 A copy of this function will be attached to a test class created for each backend. 257 Therefore, `self` will be an instance of BackendTest. 258 """ 259 260 def __call__(self, test: BackendTest) -> None: 261 """ 262 A unit test for a BackendTest. 263 """
A test function for backend tests.
A copy of this function will be attached to a test class created for each backend.
Therefore, self will be an instance of BackendTest.
1953def _no_init_or_replace_init(self, *args, **kwargs): 1954 cls = type(self) 1955 1956 if cls._is_protocol: 1957 raise TypeError('Protocols cannot be instantiated') 1958 1959 # Already using a custom `__init__`. No need to calculate correct 1960 # `__init__` to call. This can lead to RecursionError. See bpo-45121. 1961 if cls.__init__ is not _no_init_or_replace_init: 1962 return 1963 1964 # Initially, `__init__` of a protocol subclass is set to `_no_init_or_replace_init`. 1965 # The first instantiation of the subclass will call `_no_init_or_replace_init` which 1966 # searches for a proper new `__init__` in the MRO. The new `__init__` 1967 # replaces the subclass' old `__init__` (ie `_no_init_or_replace_init`). Subsequent 1968 # instantiation of the protocol subclass will thus use the new 1969 # `__init__` and no longer call `_no_init_or_replace_init`. 1970 for base in cls.__mro__: 1971 init = base.__dict__.get('__init__', _no_init_or_replace_init) 1972 if init is not _no_init_or_replace_init: 1973 cls.__init__ = init 1974 break 1975 else: 1976 # should not happen 1977 cls.__init__ = object.__init__ 1978 1979 cls.__init__(self, *args, **kwargs)
277def add_test_path(target_class: type, path: str) -> None: 278 """ Add tests from the given test files. """ 279 280 test_module = edq.util.pyimport.import_path(path) 281 282 for attr_name in sorted(dir(test_module)): 283 if (not attr_name.startswith(TEST_FUNC_NAME_PREFIX)): 284 continue 285 286 test_function = getattr(test_module, attr_name) 287 setattr(target_class, attr_name, _wrap_test_function(test_function))
Add tests from the given test files.
289def discover_test_cases(target_class: type) -> None: 290 """ Look in the text cases directory for any test cases and add them as test methods to the test class. """ 291 292 paths = list(sorted(glob.glob(os.path.join(BACKEND_TESTS_DIR, "**", TEST_FILENAME_GLOB_PATTERN), recursive = True))) 293 for path in sorted(paths): 294 add_test_path(target_class, path)
Look in the text cases directory for any test cases and add them as test methods to the test class.
296def attach_test_cases(target_class: type) -> None: 297 """ Attach all the standard test cases to the given class. """ 298 299 # Attach backend tests. 300 discover_test_cases(target_class) 301 302 # Attach CLI tests. 303 edq.testing.cli.discover_test_cases(target_class, CLI_TESTS_DIR, CLI_DATA_DIR, test_method_wrapper = _wrap_cli_test_method)
Attach all the standard test cases to the given class.