edq.testing.cli
Infrastructure for testing CLI tools using a JSON file which describes a test case, which is essentially an invocation of a CLI tool and the expected output.
The test case file must be a .txt
file that live in the test cases dir.
The file contains two parts (separated by a line with just TEST_CASE_SEP):
the first part which is a JSON object (see below for available keys),
and a second part which is the expected text output (stdout).
For the keys of the JSON section, see the defaulted arguments to CLITestInfo.
The options JSON will be splatted into CLITestInfo's constructor.
The expected output or any argument can reference the test's current temp or data dirs with __TEMP_DIR__()
or __DATA_DIR__()
, respectively.
An optional slash-separated path can be used as an argument to reference a path within those base directories.
For example, __DATA_DIR__(foo/bar.txt)
references bar.txt
inside the foo
directory inside the data directory.
1""" 2Infrastructure for testing CLI tools using a JSON file which describes a test case, 3which is essentially an invocation of a CLI tool and the expected output. 4 5The test case file must be a `.txt` file that live in the test cases dir. 6The file contains two parts (separated by a line with just TEST_CASE_SEP): 7the first part which is a JSON object (see below for available keys), 8and a second part which is the expected text output (stdout). 9For the keys of the JSON section, see the defaulted arguments to CLITestInfo. 10The options JSON will be splatted into CLITestInfo's constructor. 11 12The expected output or any argument can reference the test's current temp or data dirs with `__TEMP_DIR__()` or `__DATA_DIR__()`, respectively. 13An optional slash-separated path can be used as an argument to reference a path within those base directories. 14For example, `__DATA_DIR__(foo/bar.txt)` references `bar.txt` inside the `foo` directory inside the data directory. 15""" 16 17import contextlib 18import glob 19import io 20import os 21import re 22import sys 23import typing 24 25import edq.testing.asserts 26import edq.testing.unittest 27import edq.util.dirent 28import edq.util.json 29import edq.util.pyimport 30 31TEST_CASE_SEP: str = '---' 32DATA_DIR_ID: str = '__DATA_DIR__' 33TEMP_DIR_ID: str = '__TEMP_DIR__' 34BASE_DIR_ID: str = '__BASE_DIR__' 35 36DEFAULT_ASSERTION_FUNC_NAME: str = 'edq.testing.asserts.content_equals_normalize' 37 38BASE_TEMP_DIR_ATTR: str = '_edq_cli_base_test_dir' 39 40class CLITestInfo: 41 """ The required information to run a CLI test. """ 42 43 def __init__(self, 44 test_name: str, 45 base_dir: str, 46 data_dir: str, 47 temp_dir: str, 48 cli: typing.Union[str, None] = None, 49 arguments: typing.Union[typing.List[str], None] = None, 50 error: bool = False, 51 platform_skip: typing.Union[str, None] = None, 52 stdout_assertion_func: typing.Union[str, None] = DEFAULT_ASSERTION_FUNC_NAME, 53 stderr_assertion_func: typing.Union[str, None] = None, 54 expected_stdout: str = '', 55 expected_stderr: str = '', 56 split_stdout_stderr: bool = False, 57 strip_error_output: bool = True, 58 extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None, 59 **kwargs: typing.Any) -> None: 60 self.skip_reasons: typing.List[str] = [] 61 """ 62 Reasons that this test will be skipped. 63 Any entries in this list indicate that the test should be skipped. 64 """ 65 66 self.platform_skip_pattern: typing.Union[str, None] = platform_skip 67 """ 68 A pattern to check if the test should be skipped on the current platform. 69 Will be used in `re.search()` against `sys.platform`. 70 """ 71 72 if ((platform_skip is not None) and re.search(platform_skip, sys.platform)): 73 self.skip_reasons.append(f"not available on platform: '{sys.platform}'") 74 75 self.test_name: str = test_name 76 """ The name of this test. """ 77 78 self.base_dir: str = base_dir 79 """ 80 The base directory for this test (usually the dir the CLI test file lives. 81 This is the expansion for `__BASE_DIR__` paths. 82 """ 83 84 self.data_dir: str = data_dir 85 """ 86 A directory that additional testing data lives in. 87 This is the expansion for `__DATA_DIR__` paths. 88 """ 89 90 self.temp_dir: str = temp_dir 91 """ 92 A temp directory that this test has access to. 93 This is the expansion for `__TEMP_DIR__` paths. 94 """ 95 96 edq.util.dirent.mkdir(temp_dir) 97 98 if (cli is None): 99 raise ValueError("Missing CLI module.") 100 101 self.module_name: str = cli 102 """ The name of the module to invoke. """ 103 104 self.module: typing.Any = None 105 """ The module to invoke. """ 106 107 if (not self.should_skip()): 108 self.module = edq.util.pyimport.import_name(self.module_name) 109 110 if (arguments is None): 111 arguments = [] 112 113 self.arguments: typing.List[str] = arguments 114 """ The CLI arguments. """ 115 116 self.error: bool = error 117 """ Whether or not this test is expected to be an error (raise an exception). """ 118 119 self.stdout_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 120 """ The assertion func to compare the expected and actual stdout of the CLI. """ 121 122 if ((stdout_assertion_func is not None) and (not self.should_skip())): 123 self.stdout_assertion_func = edq.util.pyimport.fetch(stdout_assertion_func) 124 125 self.stderr_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 126 """ The assertion func to compare the expected and actual stderr of the CLI. """ 127 128 if ((stderr_assertion_func is not None) and (not self.should_skip())): 129 self.stderr_assertion_func = edq.util.pyimport.fetch(stderr_assertion_func) 130 131 self.expected_stdout: str = expected_stdout 132 """ The expected stdout. """ 133 134 self.expected_stderr: str = expected_stderr 135 """ The expected stderr. """ 136 137 if (error and strip_error_output): 138 self.expected_stdout = self.expected_stdout.strip() 139 self.expected_stderr = self.expected_stderr.strip() 140 141 self.split_stdout_stderr: bool = split_stdout_stderr 142 """ 143 Split stdout and stderr into different strings for testing. 144 By default, these two will be combined. 145 If both are non-empty, then they will be joined like: f"{stdout}\n{TEST_CASE_SEP}\n{stderr}". 146 Otherwise, only the non-empty one will be present with no separator. 147 Any stdout assertions will be applied to the combined text. 148 """ 149 150 # Make any path normalizations over the arguments and expected output. 151 self.expected_stdout = self._expand_paths(self.expected_stdout) 152 self.expected_stderr = self._expand_paths(self.expected_stderr) 153 for (i, argument) in enumerate(self.arguments): 154 self.arguments[i] = self._expand_paths(argument) 155 156 if (extra_options is None): 157 extra_options = {} 158 159 self.extra_options: typing.Union[typing.Dict[str, typing.Any], None] = extra_options 160 """ 161 A place to store additional options. 162 Extra top-level options will cause tests to error. 163 """ 164 165 if (len(kwargs) > 0): 166 raise ValueError(f"Found unknown CLI test options: '{kwargs}'.") 167 168 def _expand_paths(self, text: str) -> str: 169 """ 170 Expand path replacements in testing text. 171 This allows for consistent paths (even absolute paths) in the test text. 172 """ 173 174 replacements = [ 175 (DATA_DIR_ID, self.data_dir), 176 (TEMP_DIR_ID, self.temp_dir), 177 (BASE_DIR_ID, self.base_dir), 178 ] 179 180 for (key, target_dir) in replacements: 181 text = replace_path_pattern(text, key, target_dir) 182 183 return text 184 185 def should_skip(self) -> bool: 186 """ Check if this test should be skipped. """ 187 188 return (len(self.skip_reasons) > 0) 189 190 def skip_message(self) -> str: 191 """ Get a message displaying the reasons this test should be skipped. """ 192 193 return f"This test has been skipped because of the following: {self.skip_reasons}." 194 195 @staticmethod 196 def load_path(path: str, test_name: str, base_temp_dir: str, data_dir: str) -> 'CLITestInfo': 197 """ Load a CLI test file and extract the test info. """ 198 199 options, expected_stdout = read_test_file(path) 200 201 options['expected_stdout'] = expected_stdout 202 203 base_dir = os.path.dirname(os.path.abspath(path)) 204 temp_dir = os.path.join(base_temp_dir, test_name) 205 206 return CLITestInfo(test_name, base_dir, data_dir, temp_dir, **options) 207 208def read_test_file(path: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]: 209 """ Read a test case file and split the output into JSON data and text. """ 210 211 json_lines: typing.List[str] = [] 212 output_lines: typing.List[str] = [] 213 214 text = edq.util.dirent.read_file(path, strip = False) 215 216 accumulator = json_lines 217 for line in text.split("\n"): 218 if (line.strip() == TEST_CASE_SEP): 219 accumulator = output_lines 220 continue 221 222 accumulator.append(line) 223 224 options = edq.util.json.loads(''.join(json_lines)) 225 output = "\n".join(output_lines) 226 227 return options, output 228 229def replace_path_pattern(text: str, key: str, target_dir: str) -> str: 230 """ Make any test replacement inside the given string. """ 231 232 match = re.search(rf'{key}\(([^)]*)\)', text) 233 if (match is not None): 234 filename = match.group(1) 235 236 # Normalize any path separators. 237 filename = os.path.join(*filename.split('/')) 238 239 if (filename == ''): 240 path = target_dir 241 else: 242 path = os.path.join(target_dir, filename) 243 244 text = text.replace(match.group(0), path) 245 246 return text 247 248def _get_test_method(test_name: str, path: str, data_dir: str) -> typing.Callable: 249 """ Get a test method that represents the test case at the given path. """ 250 251 def __method(self: edq.testing.unittest.BaseTest) -> None: 252 test_info = CLITestInfo.load_path(path, test_name, getattr(self, BASE_TEMP_DIR_ATTR), data_dir) 253 254 if (test_info.should_skip()): 255 self.skipTest(test_info.skip_message()) 256 257 old_args = sys.argv 258 sys.argv = [test_info.module.__file__] + test_info.arguments 259 260 try: 261 with contextlib.redirect_stdout(io.StringIO()) as stdout_output: 262 with contextlib.redirect_stderr(io.StringIO()) as stderr_output: 263 test_info.module.main() 264 265 stdout_text = stdout_output.getvalue() 266 stderr_text = stderr_output.getvalue() 267 268 if (test_info.error): 269 self.fail(f"No error was not raised when one was expected ('{str(test_info.expected_stdout)}').") 270 except BaseException as ex: 271 if (not test_info.error): 272 raise ex 273 274 stdout_text = self.format_error_string(ex) 275 276 stderr_text = '' 277 if (isinstance(ex, SystemExit) and (ex.__context__ is not None)): 278 stderr_text = self.format_error_string(ex.__context__) 279 finally: 280 sys.argv = old_args 281 282 if (not test_info.split_stdout_stderr): 283 if ((len(stdout_text) > 0) and (len(stderr_text) > 0)): 284 stdout_text = f"{stdout_text}\n{TEST_CASE_SEP}\n{stderr_text}" 285 elif (len(stderr_text) > 0): 286 stdout_text = stderr_text 287 288 if (test_info.stdout_assertion_func is not None): 289 test_info.stdout_assertion_func(self, test_info.expected_stdout, stdout_text) 290 291 if (test_info.stderr_assertion_func is not None): 292 test_info.stderr_assertion_func(self, test_info.expected_stderr, stderr_text) 293 294 return __method 295 296def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str]) -> None: 297 """ Add tests from the given test files. """ 298 299 # Attach a temp directory to the testing class so all tests can share a common base temp dir. 300 if (not hasattr(target_class, BASE_TEMP_DIR_ATTR)): 301 setattr(target_class, BASE_TEMP_DIR_ATTR, edq.util.dirent.get_temp_path('edq_cli_test_')) 302 303 for path in sorted(paths): 304 test_name = 'test_cli__' + os.path.splitext(os.path.basename(path))[0] 305 306 try: 307 setattr(target_class, test_name, _get_test_method(test_name, path, data_dir)) 308 except Exception as ex: 309 raise ValueError(f"Failed to parse test case '{path}'.") from ex 310 311def discover_test_cases(target_class: type, test_cases_dir: str, data_dir: str) -> None: 312 """ Look in the text cases directory for any test cases and add them as test methods to the test class. """ 313 314 paths = list(sorted(glob.glob(os.path.join(test_cases_dir, "**", "*.txt"), recursive = True))) 315 add_test_paths(target_class, data_dir, paths)
41class CLITestInfo: 42 """ The required information to run a CLI test. """ 43 44 def __init__(self, 45 test_name: str, 46 base_dir: str, 47 data_dir: str, 48 temp_dir: str, 49 cli: typing.Union[str, None] = None, 50 arguments: typing.Union[typing.List[str], None] = None, 51 error: bool = False, 52 platform_skip: typing.Union[str, None] = None, 53 stdout_assertion_func: typing.Union[str, None] = DEFAULT_ASSERTION_FUNC_NAME, 54 stderr_assertion_func: typing.Union[str, None] = None, 55 expected_stdout: str = '', 56 expected_stderr: str = '', 57 split_stdout_stderr: bool = False, 58 strip_error_output: bool = True, 59 extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None, 60 **kwargs: typing.Any) -> None: 61 self.skip_reasons: typing.List[str] = [] 62 """ 63 Reasons that this test will be skipped. 64 Any entries in this list indicate that the test should be skipped. 65 """ 66 67 self.platform_skip_pattern: typing.Union[str, None] = platform_skip 68 """ 69 A pattern to check if the test should be skipped on the current platform. 70 Will be used in `re.search()` against `sys.platform`. 71 """ 72 73 if ((platform_skip is not None) and re.search(platform_skip, sys.platform)): 74 self.skip_reasons.append(f"not available on platform: '{sys.platform}'") 75 76 self.test_name: str = test_name 77 """ The name of this test. """ 78 79 self.base_dir: str = base_dir 80 """ 81 The base directory for this test (usually the dir the CLI test file lives. 82 This is the expansion for `__BASE_DIR__` paths. 83 """ 84 85 self.data_dir: str = data_dir 86 """ 87 A directory that additional testing data lives in. 88 This is the expansion for `__DATA_DIR__` paths. 89 """ 90 91 self.temp_dir: str = temp_dir 92 """ 93 A temp directory that this test has access to. 94 This is the expansion for `__TEMP_DIR__` paths. 95 """ 96 97 edq.util.dirent.mkdir(temp_dir) 98 99 if (cli is None): 100 raise ValueError("Missing CLI module.") 101 102 self.module_name: str = cli 103 """ The name of the module to invoke. """ 104 105 self.module: typing.Any = None 106 """ The module to invoke. """ 107 108 if (not self.should_skip()): 109 self.module = edq.util.pyimport.import_name(self.module_name) 110 111 if (arguments is None): 112 arguments = [] 113 114 self.arguments: typing.List[str] = arguments 115 """ The CLI arguments. """ 116 117 self.error: bool = error 118 """ Whether or not this test is expected to be an error (raise an exception). """ 119 120 self.stdout_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 121 """ The assertion func to compare the expected and actual stdout of the CLI. """ 122 123 if ((stdout_assertion_func is not None) and (not self.should_skip())): 124 self.stdout_assertion_func = edq.util.pyimport.fetch(stdout_assertion_func) 125 126 self.stderr_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 127 """ The assertion func to compare the expected and actual stderr of the CLI. """ 128 129 if ((stderr_assertion_func is not None) and (not self.should_skip())): 130 self.stderr_assertion_func = edq.util.pyimport.fetch(stderr_assertion_func) 131 132 self.expected_stdout: str = expected_stdout 133 """ The expected stdout. """ 134 135 self.expected_stderr: str = expected_stderr 136 """ The expected stderr. """ 137 138 if (error and strip_error_output): 139 self.expected_stdout = self.expected_stdout.strip() 140 self.expected_stderr = self.expected_stderr.strip() 141 142 self.split_stdout_stderr: bool = split_stdout_stderr 143 """ 144 Split stdout and stderr into different strings for testing. 145 By default, these two will be combined. 146 If both are non-empty, then they will be joined like: f"{stdout}\n{TEST_CASE_SEP}\n{stderr}". 147 Otherwise, only the non-empty one will be present with no separator. 148 Any stdout assertions will be applied to the combined text. 149 """ 150 151 # Make any path normalizations over the arguments and expected output. 152 self.expected_stdout = self._expand_paths(self.expected_stdout) 153 self.expected_stderr = self._expand_paths(self.expected_stderr) 154 for (i, argument) in enumerate(self.arguments): 155 self.arguments[i] = self._expand_paths(argument) 156 157 if (extra_options is None): 158 extra_options = {} 159 160 self.extra_options: typing.Union[typing.Dict[str, typing.Any], None] = extra_options 161 """ 162 A place to store additional options. 163 Extra top-level options will cause tests to error. 164 """ 165 166 if (len(kwargs) > 0): 167 raise ValueError(f"Found unknown CLI test options: '{kwargs}'.") 168 169 def _expand_paths(self, text: str) -> str: 170 """ 171 Expand path replacements in testing text. 172 This allows for consistent paths (even absolute paths) in the test text. 173 """ 174 175 replacements = [ 176 (DATA_DIR_ID, self.data_dir), 177 (TEMP_DIR_ID, self.temp_dir), 178 (BASE_DIR_ID, self.base_dir), 179 ] 180 181 for (key, target_dir) in replacements: 182 text = replace_path_pattern(text, key, target_dir) 183 184 return text 185 186 def should_skip(self) -> bool: 187 """ Check if this test should be skipped. """ 188 189 return (len(self.skip_reasons) > 0) 190 191 def skip_message(self) -> str: 192 """ Get a message displaying the reasons this test should be skipped. """ 193 194 return f"This test has been skipped because of the following: {self.skip_reasons}." 195 196 @staticmethod 197 def load_path(path: str, test_name: str, base_temp_dir: str, data_dir: str) -> 'CLITestInfo': 198 """ Load a CLI test file and extract the test info. """ 199 200 options, expected_stdout = read_test_file(path) 201 202 options['expected_stdout'] = expected_stdout 203 204 base_dir = os.path.dirname(os.path.abspath(path)) 205 temp_dir = os.path.join(base_temp_dir, test_name) 206 207 return CLITestInfo(test_name, base_dir, data_dir, temp_dir, **options)
The required information to run a CLI test.
44 def __init__(self, 45 test_name: str, 46 base_dir: str, 47 data_dir: str, 48 temp_dir: str, 49 cli: typing.Union[str, None] = None, 50 arguments: typing.Union[typing.List[str], None] = None, 51 error: bool = False, 52 platform_skip: typing.Union[str, None] = None, 53 stdout_assertion_func: typing.Union[str, None] = DEFAULT_ASSERTION_FUNC_NAME, 54 stderr_assertion_func: typing.Union[str, None] = None, 55 expected_stdout: str = '', 56 expected_stderr: str = '', 57 split_stdout_stderr: bool = False, 58 strip_error_output: bool = True, 59 extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None, 60 **kwargs: typing.Any) -> None: 61 self.skip_reasons: typing.List[str] = [] 62 """ 63 Reasons that this test will be skipped. 64 Any entries in this list indicate that the test should be skipped. 65 """ 66 67 self.platform_skip_pattern: typing.Union[str, None] = platform_skip 68 """ 69 A pattern to check if the test should be skipped on the current platform. 70 Will be used in `re.search()` against `sys.platform`. 71 """ 72 73 if ((platform_skip is not None) and re.search(platform_skip, sys.platform)): 74 self.skip_reasons.append(f"not available on platform: '{sys.platform}'") 75 76 self.test_name: str = test_name 77 """ The name of this test. """ 78 79 self.base_dir: str = base_dir 80 """ 81 The base directory for this test (usually the dir the CLI test file lives. 82 This is the expansion for `__BASE_DIR__` paths. 83 """ 84 85 self.data_dir: str = data_dir 86 """ 87 A directory that additional testing data lives in. 88 This is the expansion for `__DATA_DIR__` paths. 89 """ 90 91 self.temp_dir: str = temp_dir 92 """ 93 A temp directory that this test has access to. 94 This is the expansion for `__TEMP_DIR__` paths. 95 """ 96 97 edq.util.dirent.mkdir(temp_dir) 98 99 if (cli is None): 100 raise ValueError("Missing CLI module.") 101 102 self.module_name: str = cli 103 """ The name of the module to invoke. """ 104 105 self.module: typing.Any = None 106 """ The module to invoke. """ 107 108 if (not self.should_skip()): 109 self.module = edq.util.pyimport.import_name(self.module_name) 110 111 if (arguments is None): 112 arguments = [] 113 114 self.arguments: typing.List[str] = arguments 115 """ The CLI arguments. """ 116 117 self.error: bool = error 118 """ Whether or not this test is expected to be an error (raise an exception). """ 119 120 self.stdout_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 121 """ The assertion func to compare the expected and actual stdout of the CLI. """ 122 123 if ((stdout_assertion_func is not None) and (not self.should_skip())): 124 self.stdout_assertion_func = edq.util.pyimport.fetch(stdout_assertion_func) 125 126 self.stderr_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None 127 """ The assertion func to compare the expected and actual stderr of the CLI. """ 128 129 if ((stderr_assertion_func is not None) and (not self.should_skip())): 130 self.stderr_assertion_func = edq.util.pyimport.fetch(stderr_assertion_func) 131 132 self.expected_stdout: str = expected_stdout 133 """ The expected stdout. """ 134 135 self.expected_stderr: str = expected_stderr 136 """ The expected stderr. """ 137 138 if (error and strip_error_output): 139 self.expected_stdout = self.expected_stdout.strip() 140 self.expected_stderr = self.expected_stderr.strip() 141 142 self.split_stdout_stderr: bool = split_stdout_stderr 143 """ 144 Split stdout and stderr into different strings for testing. 145 By default, these two will be combined. 146 If both are non-empty, then they will be joined like: f"{stdout}\n{TEST_CASE_SEP}\n{stderr}". 147 Otherwise, only the non-empty one will be present with no separator. 148 Any stdout assertions will be applied to the combined text. 149 """ 150 151 # Make any path normalizations over the arguments and expected output. 152 self.expected_stdout = self._expand_paths(self.expected_stdout) 153 self.expected_stderr = self._expand_paths(self.expected_stderr) 154 for (i, argument) in enumerate(self.arguments): 155 self.arguments[i] = self._expand_paths(argument) 156 157 if (extra_options is None): 158 extra_options = {} 159 160 self.extra_options: typing.Union[typing.Dict[str, typing.Any], None] = extra_options 161 """ 162 A place to store additional options. 163 Extra top-level options will cause tests to error. 164 """ 165 166 if (len(kwargs) > 0): 167 raise ValueError(f"Found unknown CLI test options: '{kwargs}'.")
Reasons that this test will be skipped. Any entries in this list indicate that the test should be skipped.
A pattern to check if the test should be skipped on the current platform.
Will be used in re.search()
against sys.platform
.
The base directory for this test (usually the dir the CLI test file lives.
This is the expansion for __BASE_DIR__
paths.
A directory that additional testing data lives in.
This is the expansion for __DATA_DIR__
paths.
A temp directory that this test has access to.
This is the expansion for __TEMP_DIR__
paths.
The assertion func to compare the expected and actual stdout of the CLI.
The assertion func to compare the expected and actual stderr of the CLI.
Split stdout and stderr into different strings for testing. By default, these two will be combined. If both are non-empty, then they will be joined like: f"{stdout} {TEST_CASE_SEP} {stderr}". Otherwise, only the non-empty one will be present with no separator. Any stdout assertions will be applied to the combined text.
A place to store additional options. Extra top-level options will cause tests to error.
186 def should_skip(self) -> bool: 187 """ Check if this test should be skipped. """ 188 189 return (len(self.skip_reasons) > 0)
Check if this test should be skipped.
191 def skip_message(self) -> str: 192 """ Get a message displaying the reasons this test should be skipped. """ 193 194 return f"This test has been skipped because of the following: {self.skip_reasons}."
Get a message displaying the reasons this test should be skipped.
196 @staticmethod 197 def load_path(path: str, test_name: str, base_temp_dir: str, data_dir: str) -> 'CLITestInfo': 198 """ Load a CLI test file and extract the test info. """ 199 200 options, expected_stdout = read_test_file(path) 201 202 options['expected_stdout'] = expected_stdout 203 204 base_dir = os.path.dirname(os.path.abspath(path)) 205 temp_dir = os.path.join(base_temp_dir, test_name) 206 207 return CLITestInfo(test_name, base_dir, data_dir, temp_dir, **options)
Load a CLI test file and extract the test info.
209def read_test_file(path: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]: 210 """ Read a test case file and split the output into JSON data and text. """ 211 212 json_lines: typing.List[str] = [] 213 output_lines: typing.List[str] = [] 214 215 text = edq.util.dirent.read_file(path, strip = False) 216 217 accumulator = json_lines 218 for line in text.split("\n"): 219 if (line.strip() == TEST_CASE_SEP): 220 accumulator = output_lines 221 continue 222 223 accumulator.append(line) 224 225 options = edq.util.json.loads(''.join(json_lines)) 226 output = "\n".join(output_lines) 227 228 return options, output
Read a test case file and split the output into JSON data and text.
230def replace_path_pattern(text: str, key: str, target_dir: str) -> str: 231 """ Make any test replacement inside the given string. """ 232 233 match = re.search(rf'{key}\(([^)]*)\)', text) 234 if (match is not None): 235 filename = match.group(1) 236 237 # Normalize any path separators. 238 filename = os.path.join(*filename.split('/')) 239 240 if (filename == ''): 241 path = target_dir 242 else: 243 path = os.path.join(target_dir, filename) 244 245 text = text.replace(match.group(0), path) 246 247 return text
Make any test replacement inside the given string.
297def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str]) -> None: 298 """ Add tests from the given test files. """ 299 300 # Attach a temp directory to the testing class so all tests can share a common base temp dir. 301 if (not hasattr(target_class, BASE_TEMP_DIR_ATTR)): 302 setattr(target_class, BASE_TEMP_DIR_ATTR, edq.util.dirent.get_temp_path('edq_cli_test_')) 303 304 for path in sorted(paths): 305 test_name = 'test_cli__' + os.path.splitext(os.path.basename(path))[0] 306 307 try: 308 setattr(target_class, test_name, _get_test_method(test_name, path, data_dir)) 309 except Exception as ex: 310 raise ValueError(f"Failed to parse test case '{path}'.") from ex
Add tests from the given test files.
312def discover_test_cases(target_class: type, test_cases_dir: str, data_dir: str) -> None: 313 """ Look in the text cases directory for any test cases and add them as test methods to the test class. """ 314 315 paths = list(sorted(glob.glob(os.path.join(test_cases_dir, "**", "*.txt"), recursive = True))) 316 add_test_paths(target_class, data_dir, paths)
Look in the text cases directory for any test cases and add them as test methods to the test class.