autograder.testing.server
1import os 2import typing 3 4import edq.net.exchangeserver 5import edq.testing.httpserver 6import edq.util.serial 7import lms.model.base 8 9import autograder.api.common 10import autograder.error 11import autograder.model.config 12 13THIS_DIR: str = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 14ROOT_DIR: str = os.path.join(THIS_DIR, '..', '..') 15EXCHANGES_DIR: str = os.path.join(ROOT_DIR, 'testdata', 'autograder-testdata') 16 17# TEST - Remove? 18BASE_ARGUMENTS: typing.Dict[str, typing.Any] = { 19 # Will be set with the correct port when the test is run. 20 'server': None, 21} 22 23# Exchange tests are unnecessary. 24delattr(edq.testing.httpserver.HTTPServerTest, 'test_exchanges_base') 25 26@typing.runtime_checkable 27class CleanFunction(typing.Protocol): 28 """ A function for cleaning a test result (expected or actual) before assertion. """ 29 30 def __call__(self, raw: typing.Any) -> typing.Any: 31 """ 32 Clean and return data for assertion. 33 """ 34 35@typing.runtime_checkable 36class AssertionFunction(typing.Protocol): 37 """ A function for asserting during a test. """ 38 39 def __call__(self, expected: typing.Any, actual: typing.Any) -> typing.Any: 40 """ 41 Assert the relationship between expected and actual. 42 """ 43 44class ServerTest(edq.testing.httpserver.HTTPServerTest): 45 """ 46 A special test suite that is common across all tests that require a test autograder server. 47 """ 48 49 # Use the same server for all test classes. 50 tear_down_server = False 51 52 @classmethod 53 def child_class_setup(cls) -> None: 54 # Make the API request source information consistent. 55 autograder.api.common.set_testing_source_info() 56 57 # Don't exit on error. 58 autograder.error._exit_on_error_for_testing = False 59 60 @classmethod 61 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 62 context = edq.util.serial.SerializationContext(json_options = { 63 'strict': True, 64 }) 65 66 edq.testing.httpserver.HTTPServerTest.setup_server(server) 67 server.load_exchanges_dir(EXCHANGES_DIR, context = context) 68 69 def base_api_test(self, 70 api_function: typing.Callable, 71 test_cases: typing.List[typing.Tuple[autograder.model.config.Config, typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 72 actual_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 73 expected_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 74 assertion_func: typing.Union[AssertionFunction, None] = None, 75 ) -> None: 76 """ 77 A common test for the base API functionality. 78 Test cases are passed in as: `[(configs (and overrides), kwargs, expected, error substring), ...]`. 79 """ 80 81 for (i, test_case) in enumerate(test_cases): 82 (config, kwargs, expected, error_substring) = test_case 83 84 with self.subTest(msg = f"Case {i}:"): 85 config.server = self.get_server_url() 86 87 try: 88 actual = api_function(config, **kwargs) 89 except Exception as ex: 90 error_string = self.format_error_string(ex) 91 if (error_substring is None): 92 self.fail(f"Unexpected error: '{error_string}'.") 93 94 self.assertIn(error_substring, error_string, 'Error is not as expected.') 95 96 continue 97 98 if (error_substring is not None): 99 self.fail(f"Did not get expected error: '{error_substring}'.") 100 101 if (actual_clean_func is not None): 102 actual = actual_clean_func(actual) 103 104 if (expected_clean_func is not None): 105 expected = expected_clean_func(expected) 106 107 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 108 if (isinstance(expected, tuple)): 109 if (not isinstance(actual, tuple)): 110 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 111 112 if (len(expected) != len(actual)): 113 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 114 else: 115 # Wrap the results in a tuple. 116 expected = (expected, ) 117 actual = (actual, ) 118 119 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 120 expected_value = expected[i] 121 actual_value = actual[i] 122 123 if (assertion_func is not None): 124 assertion_func(expected_value, actual_value) 125 elif (isinstance(expected_value, lms.model.base.BaseType)): 126 self.assertJSONEqual(expected_value, actual_value) 127 elif (isinstance(expected_value, dict)): 128 self.assertJSONDictEqual(expected_value, actual_value) 129 elif (isinstance(expected_value, list)): 130 self.assertJSONListEqual(expected_value, actual_value) 131 else: 132 self.assertEqual(expected_value, actual_value)
THIS_DIR: str =
'/home/runner/work/autograder-py/autograder-py/autograder/testing'
ROOT_DIR: str =
'/home/runner/work/autograder-py/autograder-py/autograder/testing/../..'
EXCHANGES_DIR: str =
'/home/runner/work/autograder-py/autograder-py/autograder/testing/../../testdata/autograder-testdata'
BASE_ARGUMENTS: Dict[str, Any] =
{'server': None}
@typing.runtime_checkable
class
CleanFunction27@typing.runtime_checkable 28class CleanFunction(typing.Protocol): 29 """ A function for cleaning a test result (expected or actual) before assertion. """ 30 31 def __call__(self, raw: typing.Any) -> typing.Any: 32 """ 33 Clean and return data for assertion. 34 """
A function for cleaning a test result (expected or actual) before assertion.
CleanFunction(*args, **kwargs)
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)
@typing.runtime_checkable
class
AssertionFunction36@typing.runtime_checkable 37class AssertionFunction(typing.Protocol): 38 """ A function for asserting during a test. """ 39 40 def __call__(self, expected: typing.Any, actual: typing.Any) -> typing.Any: 41 """ 42 Assert the relationship between expected and actual. 43 """
A function for asserting during a test.
AssertionFunction(*args, **kwargs)
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)
class
ServerTest(edq.testing.httpserver.HTTPServerTest):
45class ServerTest(edq.testing.httpserver.HTTPServerTest): 46 """ 47 A special test suite that is common across all tests that require a test autograder server. 48 """ 49 50 # Use the same server for all test classes. 51 tear_down_server = False 52 53 @classmethod 54 def child_class_setup(cls) -> None: 55 # Make the API request source information consistent. 56 autograder.api.common.set_testing_source_info() 57 58 # Don't exit on error. 59 autograder.error._exit_on_error_for_testing = False 60 61 @classmethod 62 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 63 context = edq.util.serial.SerializationContext(json_options = { 64 'strict': True, 65 }) 66 67 edq.testing.httpserver.HTTPServerTest.setup_server(server) 68 server.load_exchanges_dir(EXCHANGES_DIR, context = context) 69 70 def base_api_test(self, 71 api_function: typing.Callable, 72 test_cases: typing.List[typing.Tuple[autograder.model.config.Config, typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 73 actual_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 74 expected_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 75 assertion_func: typing.Union[AssertionFunction, None] = None, 76 ) -> None: 77 """ 78 A common test for the base API functionality. 79 Test cases are passed in as: `[(configs (and overrides), kwargs, expected, error substring), ...]`. 80 """ 81 82 for (i, test_case) in enumerate(test_cases): 83 (config, kwargs, expected, error_substring) = test_case 84 85 with self.subTest(msg = f"Case {i}:"): 86 config.server = self.get_server_url() 87 88 try: 89 actual = api_function(config, **kwargs) 90 except Exception as ex: 91 error_string = self.format_error_string(ex) 92 if (error_substring is None): 93 self.fail(f"Unexpected error: '{error_string}'.") 94 95 self.assertIn(error_substring, error_string, 'Error is not as expected.') 96 97 continue 98 99 if (error_substring is not None): 100 self.fail(f"Did not get expected error: '{error_substring}'.") 101 102 if (actual_clean_func is not None): 103 actual = actual_clean_func(actual) 104 105 if (expected_clean_func is not None): 106 expected = expected_clean_func(expected) 107 108 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 109 if (isinstance(expected, tuple)): 110 if (not isinstance(actual, tuple)): 111 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 112 113 if (len(expected) != len(actual)): 114 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 115 else: 116 # Wrap the results in a tuple. 117 expected = (expected, ) 118 actual = (actual, ) 119 120 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 121 expected_value = expected[i] 122 actual_value = actual[i] 123 124 if (assertion_func is not None): 125 assertion_func(expected_value, actual_value) 126 elif (isinstance(expected_value, lms.model.base.BaseType)): 127 self.assertJSONEqual(expected_value, actual_value) 128 elif (isinstance(expected_value, dict)): 129 self.assertJSONDictEqual(expected_value, actual_value) 130 elif (isinstance(expected_value, list)): 131 self.assertJSONListEqual(expected_value, actual_value) 132 else: 133 self.assertEqual(expected_value, actual_value)
A special test suite that is common across all tests that require a test autograder server.
tear_down_server =
False
Tear down the relevant test server in tearDownClass(). If set to false then the server will never get torn down, but can be shared between child test classes.
@classmethod
def
child_class_setup(cls) -> None:
53 @classmethod 54 def child_class_setup(cls) -> None: 55 # Make the API request source information consistent. 56 autograder.api.common.set_testing_source_info() 57 58 # Don't exit on error. 59 autograder.error._exit_on_error_for_testing = False
This function is the recommended time for child classes to set any configuration.
@classmethod
def
setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None:
61 @classmethod 62 def setup_server(cls, server: edq.net.exchangeserver.HTTPExchangeServer) -> None: 63 context = edq.util.serial.SerializationContext(json_options = { 64 'strict': True, 65 }) 66 67 edq.testing.httpserver.HTTPServerTest.setup_server(server) 68 server.load_exchanges_dir(EXCHANGES_DIR, context = context)
An opportunity for child classes to configure the test server before starting it.
def
base_api_test( self, api_function: Callable, test_cases: List[Tuple[autograder.model.config.Config, Dict[str, Any], Any, Optional[str]]], actual_clean_func: Union[CleanFunction, Callable, NoneType] = None, expected_clean_func: Union[CleanFunction, Callable, NoneType] = None, assertion_func: Optional[AssertionFunction] = None) -> None:
70 def base_api_test(self, 71 api_function: typing.Callable, 72 test_cases: typing.List[typing.Tuple[autograder.model.config.Config, typing.Dict[str, typing.Any], typing.Any, typing.Union[str, None]]], 73 actual_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 74 expected_clean_func: typing.Union[CleanFunction, typing.Callable, None] = None, 75 assertion_func: typing.Union[AssertionFunction, None] = None, 76 ) -> None: 77 """ 78 A common test for the base API functionality. 79 Test cases are passed in as: `[(configs (and overrides), kwargs, expected, error substring), ...]`. 80 """ 81 82 for (i, test_case) in enumerate(test_cases): 83 (config, kwargs, expected, error_substring) = test_case 84 85 with self.subTest(msg = f"Case {i}:"): 86 config.server = self.get_server_url() 87 88 try: 89 actual = api_function(config, **kwargs) 90 except Exception as ex: 91 error_string = self.format_error_string(ex) 92 if (error_substring is None): 93 self.fail(f"Unexpected error: '{error_string}'.") 94 95 self.assertIn(error_substring, error_string, 'Error is not as expected.') 96 97 continue 98 99 if (error_substring is not None): 100 self.fail(f"Did not get expected error: '{error_substring}'.") 101 102 if (actual_clean_func is not None): 103 actual = actual_clean_func(actual) 104 105 if (expected_clean_func is not None): 106 expected = expected_clean_func(expected) 107 108 # If we expect a tuple, compare the tuple contents instead of the tuple itself. 109 if (isinstance(expected, tuple)): 110 if (not isinstance(actual, tuple)): 111 raise ValueError(f"Expected results to be a tuple, found '{type(actual)}'.") 112 113 if (len(expected) != len(actual)): 114 raise ValueError(f"Result size mismatch. Expected: {len(expected)}, Actual: {len(actual)}.") 115 else: 116 # Wrap the results in a tuple. 117 expected = (expected, ) 118 actual = (actual, ) 119 120 for i in range(len(expected)): # pylint: disable=consider-using-enumerate 121 expected_value = expected[i] 122 actual_value = actual[i] 123 124 if (assertion_func is not None): 125 assertion_func(expected_value, actual_value) 126 elif (isinstance(expected_value, lms.model.base.BaseType)): 127 self.assertJSONEqual(expected_value, actual_value) 128 elif (isinstance(expected_value, dict)): 129 self.assertJSONDictEqual(expected_value, actual_value) 130 elif (isinstance(expected_value, list)): 131 self.assertJSONListEqual(expected_value, actual_value) 132 else: 133 self.assertEqual(expected_value, actual_value)
A common test for the base API functionality.
Test cases are passed in as: [(configs (and overrides), kwargs, expected, error substring), ...].