autograder.testing.asserts
1import os 2import re 3import typing 4 5import edq.testing.asserts 6import edq.testing.unittest 7import edq.util.json 8 9import autograder.testing.constants 10 11THIS_DIR: str = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 12ROOT_DIR: str = os.path.join(THIS_DIR, '..', '..') 13AUTOGRADER_TESTDATA_REPO: str = os.path.join(ROOT_DIR, 'testdata', 'autograder-testdata') 14AUTOGRADER_SERVER_REPO: str = os.path.join(AUTOGRADER_TESTDATA_REPO, 'autograder-server') 15API_DESCRIPTION_PATH: str = os.path.join(AUTOGRADER_SERVER_REPO, 'resources', 'api.json') 16 17TOKEN_CLEARTEXT_PATTERN: str = r'\w{32}' 18TOKEN_ID_PATTERN: str = r'\w{8}-\w{4}-\w{4}-\w{4}-\w{12}' 19 20PRETTY_TIME_PATTERN: str = r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+[\+\-]\d+:\d+' 21PRETTY_TIME_REPLACEMENT: str = '<PRETTY TIME>' 22 23NORMALIZE_TIMESTAMP_KEYS: typing.Set[str] = { 24 'analysis-timestamp', 25 'first-timestamp', 26 'last-timestamp', 27} 28""" Keys for timestamp values to normalize. """ 29 30_cached_api_description: typing.Union[typing.Dict[str, typing.Any], None] = None # pylint: disable=invalid-name 31 32def get_expected_api_description() -> typing.Dict[str, typing.Any]: 33 """ Get the expected API description from the submodule (or the cache). """ 34 35 global _cached_api_description # pylint: disable=global-statement 36 37 if (_cached_api_description is not None): 38 return _cached_api_description 39 40 _cached_api_description = edq.util.json.load_path(API_DESCRIPTION_PATH, strict = True) 41 42 return _cached_api_description 43 44def content_equals_noramlize_json(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 45 """ 46 A CLI test assertion function for JSON output. 47 The output will be converted to a dict and then compared with edq.testing.unittest.BaseTest.assertJSONDictEqual(). 48 """ 49 50 # Convert both to dicts. 51 expected_dict = edq.util.json.loads(expected, strict = True) 52 actual_dict = edq.util.json.loads(actual, strict = True) 53 54 # Normalize the actual data (the expected should already be normalized (by the tester)). 55 actual_dict = normalize_dict(actual_dict) 56 actual_dict = _normalize_timestamps(actual_dict) 57 58 test.assertJSONDictEqual(expected_dict, actual_dict) 59 60def contains(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 61 """ 62 A CLI test assertion function that checks if the expected output is a contained in the actual output. 63 """ 64 65 test.assertIn(expected, actual) 66 67def content_equals_noramlize_regrade(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 68 """ A CLI test assertion function for regrade output. """ 69 70 # Convert both to dicts. 71 expected_dict = edq.util.json.loads(expected, strict = True) 72 actual_dict = edq.util.json.loads(actual, strict = True) 73 74 # Normalize the actual data (the expected should already be normalized (by the tester)). 75 actual_dict = normalize_dict(actual_dict) 76 actual_dict = _normalize_timestamps(actual_dict, keys = {'grading_start_time', 'regrade-cutoff'}) 77 78 test.assertJSONDictEqual(expected_dict, actual_dict) 79 80def content_equals_stack_trace(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 81 """ 82 A CLI test assertion function for checking if the output looks like a stack trace. 83 """ 84 85 # Actual output should be JSON. 86 actual_dict = edq.util.json.loads(actual, strict = True) 87 88 test.assertGreater(actual_dict['count'], 5) 89 test.assertEqual(actual_dict['count'], len(actual_dict['stacks'])) 90 91def normalize_dict(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 92 """ Noramlize a dict that typically comes from testing output. """ 93 94 data = _noramlize_version(data) 95 data = _noramlize_tokens(data) 96 97 return data 98 99def normalize_analysis(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 100 """ Noramlize a dict that comes from an analysis result. """ 101 102 _normalize_timestamps(data) 103 return data 104 105def _noramlize_version(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 106 """ Normalize server version information. """ 107 108 if ('server-version' not in data): 109 return data 110 111 data['server-version']['base-version'] = autograder.testing.constants.TEST_BASE_VERSION 112 data['server-version']['git-hash'] = autograder.testing.constants.TEST_GIT_HASH 113 data['server-version']['is-dirty'] = autograder.testing.constants.TEST_IS_DIRTY 114 115 return data 116 117def _noramlize_tokens(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 118 """ Normalize token identifiers. """ 119 120 if ('token-id' in data): 121 data['token-id'] = autograder.testing.constants.TEST_TOKEN_ID 122 123 if ('token-cleartext' in data): 124 data['token-cleartext'] = autograder.testing.constants.TEST_TOKEN_CLEARTEXT 125 126 if ('token-info' in data): 127 _normalize_token_info(data['token-info']) 128 129 if ('tokens' in data): 130 if (data['tokens'] is not None): 131 for token_info in data['tokens']: 132 _normalize_token_info(token_info) 133 134 return data 135 136def _normalize_token_info(token_info: typing.Union[typing.Dict[str, typing.Any], None]) -> None: 137 if (token_info is None): 138 return 139 140 token_info['access-time'] = autograder.testing.constants.TEST_TIMESTAMP 141 token_info['creation-time'] = autograder.testing.constants.TEST_TIMESTAMP 142 token_info['id'] = autograder.testing.constants.TEST_TOKEN_ID 143 144def equals_clean_imageinfo(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 145 """ 146 A CLI test assertion function for JSON image info. 147 """ 148 149 # Convert both to dicts. 150 expected_dict = edq.util.json.loads(expected, strict = True) 151 actual_dict = edq.util.json.loads(actual, strict = True) 152 153 for content in [expected_dict, actual_dict]: 154 content['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 155 content['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 156 157 test.assertJSONDictEqual(expected_dict, actual_dict) 158 159def equals_api_description(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 160 """ A CLI test assertion function for the API description (read from a submodule). """ 161 162 expected_dict = get_expected_api_description() 163 actual_dict = edq.util.json.loads(actual, strict = True) 164 165 test.assertJSONDictEqual(expected_dict, actual_dict) 166 167def equals_clean_tokens(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 168 """ A CLI test assertion function for text that contains tokens. """ 169 170 expected = re.sub(TOKEN_CLEARTEXT_PATTERN, autograder.testing.constants.TEST_TOKEN_CLEARTEXT, expected) 171 expected = re.sub(TOKEN_ID_PATTERN, autograder.testing.constants.TEST_TOKEN_ID, expected) 172 173 actual = re.sub(TOKEN_CLEARTEXT_PATTERN, autograder.testing.constants.TEST_TOKEN_CLEARTEXT, actual) 174 actual = re.sub(TOKEN_ID_PATTERN, autograder.testing.constants.TEST_TOKEN_ID, actual) 175 176 test.assertEqual(expected, actual) 177 178def equals_clean_pretty_time(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 179 """ A CLI test assertion function for text that contains pretty timestamps. """ 180 181 actual = re.sub(PRETTY_TIME_PATTERN, PRETTY_TIME_REPLACEMENT, actual) 182 183 edq.testing.asserts.content_equals_normalize(test, expected, actual) 184 185def _normalize_timestamps(data: typing.Any, keys: typing.Union[typing.Set[str], None] = None) -> typing.Any: 186 """ 187 Recursively normalize specified timestamps. 188 It is assumed that the specified data comes from JSON deserialization 189 (and therefore has a limited number of types). 190 """ 191 192 if (keys is None): 193 keys = NORMALIZE_TIMESTAMP_KEYS 194 195 if (isinstance(data, list)): 196 return [_normalize_timestamps(item, keys = keys) for item in data] 197 198 if (isinstance(data, dict)): 199 for (key, value) in data.items(): 200 if (key in keys): 201 data[key] = autograder.testing.constants.TEST_TIMESTAMP 202 else: 203 data[key] = _normalize_timestamps(value, keys = keys) 204 205 return data
Keys for timestamp values to normalize.
33def get_expected_api_description() -> typing.Dict[str, typing.Any]: 34 """ Get the expected API description from the submodule (or the cache). """ 35 36 global _cached_api_description # pylint: disable=global-statement 37 38 if (_cached_api_description is not None): 39 return _cached_api_description 40 41 _cached_api_description = edq.util.json.load_path(API_DESCRIPTION_PATH, strict = True) 42 43 return _cached_api_description
Get the expected API description from the submodule (or the cache).
45def content_equals_noramlize_json(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 46 """ 47 A CLI test assertion function for JSON output. 48 The output will be converted to a dict and then compared with edq.testing.unittest.BaseTest.assertJSONDictEqual(). 49 """ 50 51 # Convert both to dicts. 52 expected_dict = edq.util.json.loads(expected, strict = True) 53 actual_dict = edq.util.json.loads(actual, strict = True) 54 55 # Normalize the actual data (the expected should already be normalized (by the tester)). 56 actual_dict = normalize_dict(actual_dict) 57 actual_dict = _normalize_timestamps(actual_dict) 58 59 test.assertJSONDictEqual(expected_dict, actual_dict)
A CLI test assertion function for JSON output. The output will be converted to a dict and then compared with edq.testing.unittest.BaseTest.assertJSONDictEqual().
61def contains(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 62 """ 63 A CLI test assertion function that checks if the expected output is a contained in the actual output. 64 """ 65 66 test.assertIn(expected, actual)
A CLI test assertion function that checks if the expected output is a contained in the actual output.
68def content_equals_noramlize_regrade(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 69 """ A CLI test assertion function for regrade output. """ 70 71 # Convert both to dicts. 72 expected_dict = edq.util.json.loads(expected, strict = True) 73 actual_dict = edq.util.json.loads(actual, strict = True) 74 75 # Normalize the actual data (the expected should already be normalized (by the tester)). 76 actual_dict = normalize_dict(actual_dict) 77 actual_dict = _normalize_timestamps(actual_dict, keys = {'grading_start_time', 'regrade-cutoff'}) 78 79 test.assertJSONDictEqual(expected_dict, actual_dict)
A CLI test assertion function for regrade output.
81def content_equals_stack_trace(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 82 """ 83 A CLI test assertion function for checking if the output looks like a stack trace. 84 """ 85 86 # Actual output should be JSON. 87 actual_dict = edq.util.json.loads(actual, strict = True) 88 89 test.assertGreater(actual_dict['count'], 5) 90 test.assertEqual(actual_dict['count'], len(actual_dict['stacks']))
A CLI test assertion function for checking if the output looks like a stack trace.
92def normalize_dict(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 93 """ Noramlize a dict that typically comes from testing output. """ 94 95 data = _noramlize_version(data) 96 data = _noramlize_tokens(data) 97 98 return data
Noramlize a dict that typically comes from testing output.
100def normalize_analysis(data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]: 101 """ Noramlize a dict that comes from an analysis result. """ 102 103 _normalize_timestamps(data) 104 return data
Noramlize a dict that comes from an analysis result.
145def equals_clean_imageinfo(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 146 """ 147 A CLI test assertion function for JSON image info. 148 """ 149 150 # Convert both to dicts. 151 expected_dict = edq.util.json.loads(expected, strict = True) 152 actual_dict = edq.util.json.loads(actual, strict = True) 153 154 for content in [expected_dict, actual_dict]: 155 content['created-timestamp'] = autograder.testing.constants.TEST_TIMESTAMP 156 content['size-bytes'] = len(autograder.testing.constants.TEST_PAYLOAD_BYTES) 157 158 test.assertJSONDictEqual(expected_dict, actual_dict)
A CLI test assertion function for JSON image info.
160def equals_api_description(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 161 """ A CLI test assertion function for the API description (read from a submodule). """ 162 163 expected_dict = get_expected_api_description() 164 actual_dict = edq.util.json.loads(actual, strict = True) 165 166 test.assertJSONDictEqual(expected_dict, actual_dict)
A CLI test assertion function for the API description (read from a submodule).
168def equals_clean_tokens(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 169 """ A CLI test assertion function for text that contains tokens. """ 170 171 expected = re.sub(TOKEN_CLEARTEXT_PATTERN, autograder.testing.constants.TEST_TOKEN_CLEARTEXT, expected) 172 expected = re.sub(TOKEN_ID_PATTERN, autograder.testing.constants.TEST_TOKEN_ID, expected) 173 174 actual = re.sub(TOKEN_CLEARTEXT_PATTERN, autograder.testing.constants.TEST_TOKEN_CLEARTEXT, actual) 175 actual = re.sub(TOKEN_ID_PATTERN, autograder.testing.constants.TEST_TOKEN_ID, actual) 176 177 test.assertEqual(expected, actual)
A CLI test assertion function for text that contains tokens.
179def equals_clean_pretty_time(test: edq.testing.unittest.BaseTest, expected: str, actual: str) -> None: 180 """ A CLI test assertion function for text that contains pretty timestamps. """ 181 182 actual = re.sub(PRETTY_TIME_PATTERN, PRETTY_TIME_REPLACEMENT, actual) 183 184 edq.testing.asserts.content_equals_normalize(test, expected, actual)
A CLI test assertion function for text that contains pretty timestamps.