edq-utils 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of edq-utils might be problematic. Click here for more details.

Files changed (71) hide show
  1. edq/__init__.py +1 -1
  2. edq/cli/config/__init__.py +3 -0
  3. edq/cli/config/list.py +69 -0
  4. edq/cli/http/__init__.py +3 -0
  5. edq/cli/http/exchange-server.py +71 -0
  6. edq/cli/http/send-exchange.py +45 -0
  7. edq/cli/http/verify-exchanges.py +38 -0
  8. edq/cli/testing/__init__.py +3 -0
  9. edq/cli/testing/cli-test.py +12 -5
  10. edq/cli/version.py +2 -1
  11. edq/core/argparser.py +28 -3
  12. edq/core/config.py +268 -0
  13. edq/core/config_test.py +1038 -0
  14. edq/procedure/verify_exchanges.py +85 -0
  15. edq/testing/asserts.py +0 -1
  16. edq/testing/cli.py +107 -29
  17. edq/testing/cli_test.py +8 -1
  18. edq/testing/httpserver.py +553 -0
  19. edq/testing/httpserver_test.py +424 -0
  20. edq/testing/run.py +40 -10
  21. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  22. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  23. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  24. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  25. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  26. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  27. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  28. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  29. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  30. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  31. edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
  32. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  33. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  34. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  35. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  36. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  37. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  38. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  39. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  40. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  41. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  42. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  43. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  44. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  45. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  46. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  47. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  49. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  50. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  51. edq/testing/testdata/http/files/tiny.png +0 -0
  52. edq/testing/unittest.py +26 -6
  53. edq/util/dirent.py +2 -0
  54. edq/util/dirent_test.py +43 -32
  55. edq/util/json.py +21 -4
  56. edq/util/net.py +894 -0
  57. edq_utils-0.0.6.dist-info/METADATA +156 -0
  58. edq_utils-0.0.6.dist-info/RECORD +78 -0
  59. edq/util/testdata/dirent-operations/dir_1/b.txt +0 -1
  60. edq/util/testdata/dirent-operations/dir_1/dir_2/c.txt +0 -1
  61. edq/util/testdata/dirent-operations/symlink_a.txt +0 -1
  62. edq/util/testdata/dirent-operations/symlink_dir_1/b.txt +0 -1
  63. edq/util/testdata/dirent-operations/symlink_dir_1/dir_2/c.txt +0 -1
  64. edq/util/testdata/dirent-operations/symlink_file_empty +0 -0
  65. edq_utils-0.0.4.dist-info/METADATA +0 -63
  66. edq_utils-0.0.4.dist-info/RECORD +0 -41
  67. /edq/{util/testdata/dirent-operations/file_empty → procedure/__init__.py} +0 -0
  68. /edq/{util/testdata/dirent-operations → testing/testdata/http/files}/a.txt +0 -0
  69. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/WHEEL +0 -0
  70. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/licenses/LICENSE +0 -0
  71. {edq_utils-0.0.4.dist-info → edq_utils-0.0.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,85 @@
1
+ """
2
+ Verify that exchanges sent to a given server have the same response.
3
+ """
4
+
5
+ import glob
6
+ import logging
7
+ import os
8
+ import typing
9
+ import unittest
10
+
11
+ import edq.testing.unittest
12
+ import edq.util.net
13
+
14
+ class ExchangeVerification(edq.testing.unittest.BaseTest):
15
+ """ Verify that exchanges match their content. """
16
+
17
+ def run(paths: typing.List[str], server: str) -> int:
18
+ """ Run exchange verification. """
19
+
20
+ exchange_paths = _collect_exchange_paths(paths)
21
+
22
+ _attach_tests(exchange_paths, server)
23
+
24
+ runner = unittest.TextTestRunner(verbosity = 2)
25
+ tests = unittest.defaultTestLoader.loadTestsFromTestCase(ExchangeVerification)
26
+ results = runner.run(tests)
27
+
28
+ return len(results.errors) + len(results.failures)
29
+
30
+ def _attach_tests(
31
+ paths: typing.List[str],
32
+ server: str,
33
+ extension: str = edq.util.net.DEFAULT_HTTP_EXCHANGE_EXTENSION,
34
+ ) -> None:
35
+ """ Create tests for each path and attach them to the ExchangeVerification class. """
36
+
37
+ common_prefix = os.path.commonprefix(paths)
38
+
39
+ for path in paths:
40
+ name = path.replace(common_prefix, '').replace(extension, '')
41
+ test_name = f"test_verify_exchange__{name}"
42
+
43
+ setattr(ExchangeVerification, test_name, _get_test_method(path, server))
44
+
45
+ def _get_test_method(path: str, server: str,
46
+ match_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
47
+ ) -> typing.Callable:
48
+ """ Create a test method for the given path. """
49
+
50
+ if (match_options is None):
51
+ match_options = {}
52
+
53
+ def __method(self: edq.testing.unittest.BaseTest) -> None:
54
+ exchange = edq.util.net.HTTPExchange.from_path(path)
55
+ response, body = exchange.make_request(server, raise_for_status = False, **match_options)
56
+
57
+ match, hint = exchange.match_response(response, override_body = body, **match_options)
58
+ if (not match):
59
+ raise AssertionError(f"Exchange does not match: '{hint}'.")
60
+
61
+ return __method
62
+
63
+ def _collect_exchange_paths(
64
+ paths: typing.List[str],
65
+ extension: str = edq.util.net.DEFAULT_HTTP_EXCHANGE_EXTENSION,
66
+ ) -> typing.List[str]:
67
+ """ Collect exchange files by matching extensions and descending dirs. """
68
+
69
+ final_paths = []
70
+
71
+ for path in paths:
72
+ path = os.path.abspath(path)
73
+
74
+ if (os.path.isfile(path)):
75
+ if (path.endswith(extension)):
76
+ final_paths.append(path)
77
+ else:
78
+ logging.warning("Path does not look like an exchange file: '%s'.", path)
79
+ else:
80
+ dirent_paths = glob.glob(os.path.join(path, "**", f"*{extension}"), recursive = True)
81
+ for dirent_path in dirent_paths:
82
+ final_paths.append(dirent_path)
83
+
84
+ final_paths.sort()
85
+ return final_paths
edq/testing/asserts.py CHANGED
@@ -37,7 +37,6 @@ class StringComparisonAssertion(typing.Protocol):
37
37
  Perform an assertion between expected and actual data.
38
38
  """
39
39
 
40
-
41
40
  def content_equals_raw(test: edq.testing.unittest.BaseTest, expected: str, actual: str, **kwargs: typing.Any) -> None:
42
41
  """ Check for equality using a simple string comparison. """
43
42
 
edq/testing/cli.py CHANGED
@@ -2,13 +2,19 @@
2
2
  Infrastructure for testing CLI tools using a JSON file which describes a test case,
3
3
  which is essentially an invocation of a CLI tool and the expected output.
4
4
 
5
- The test case file must be a `.txt` file that live in TEST_CASES_DIR.
5
+ The test case file must be a `.txt` file that live in the test cases dir.
6
6
  The file contains two parts (separated by a line with just TEST_CASE_SEP):
7
7
  the first part which is a JSON object (see below for available keys),
8
8
  and a second part which is the expected text output (stdout).
9
9
  For the keys of the JSON section, see the defaulted arguments to CLITestInfo.
10
10
  The options JSON will be splatted into CLITestInfo's constructor.
11
11
 
12
+ If a test class implements a method with the signature `modify_cli_test_info(self, test_info: CLITestInfo) -> None`,
13
+ then this method will be called with the test info right after the test info is read from disk.
14
+
15
+ If a test class implements a class method with the signature `get_test_basename(cls, path: str) -> str`,
16
+ then this method will be called to create the base name for the test case at the given path.
17
+
12
18
  The expected output or any argument can reference the test's current temp or data dirs with `__TEMP_DIR__()` or `__DATA_DIR__()`, respectively.
13
19
  An optional slash-separated path can be used as an argument to reference a path within those base directories.
14
20
  For example, `__DATA_DIR__(foo/bar.txt)` references `bar.txt` inside the `foo` directory inside the data directory.
@@ -28,15 +34,11 @@ import edq.util.dirent
28
34
  import edq.util.json
29
35
  import edq.util.pyimport
30
36
 
31
- THIS_DIR: str = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
32
- BASE_TESTDATA_DIR: str = os.path.join(THIS_DIR, "testdata", "cli")
33
- TEST_CASES_DIR: str = os.path.join(BASE_TESTDATA_DIR, "tests")
34
- DATA_DIR: str = os.path.join(BASE_TESTDATA_DIR, "data")
35
-
36
37
  TEST_CASE_SEP: str = '---'
37
38
  DATA_DIR_ID: str = '__DATA_DIR__'
39
+ ABS_DATA_DIR_ID: str = '__ABS_DATA_DIR__'
38
40
  TEMP_DIR_ID: str = '__TEMP_DIR__'
39
- REL_DIR_ID: str = '__REL_DIR__'
41
+ BASE_DIR_ID: str = '__BASE_DIR__'
40
42
 
41
43
  DEFAULT_ASSERTION_FUNC_NAME: str = 'edq.testing.asserts.content_equals_normalize'
42
44
 
@@ -48,6 +50,7 @@ class CLITestInfo:
48
50
  def __init__(self,
49
51
  test_name: str,
50
52
  base_dir: str,
53
+ data_dir: str,
51
54
  temp_dir: str,
52
55
  cli: typing.Union[str, None] = None,
53
56
  arguments: typing.Union[typing.List[str], None] = None,
@@ -57,16 +60,45 @@ class CLITestInfo:
57
60
  stderr_assertion_func: typing.Union[str, None] = None,
58
61
  expected_stdout: str = '',
59
62
  expected_stderr: str = '',
63
+ split_stdout_stderr: bool = False,
60
64
  strip_error_output: bool = True,
65
+ extra_options: typing.Union[typing.Dict[str, typing.Any], None] = None,
61
66
  **kwargs: typing.Any) -> None:
67
+ self.skip_reasons: typing.List[str] = []
68
+ """
69
+ Reasons that this test will be skipped.
70
+ Any entries in this list indicate that the test should be skipped.
71
+ """
72
+
73
+ self.platform_skip_pattern: typing.Union[str, None] = platform_skip
74
+ """
75
+ A pattern to check if the test should be skipped on the current platform.
76
+ Will be used in `re.search()` against `sys.platform`.
77
+ """
78
+
79
+ if ((platform_skip is not None) and re.search(platform_skip, sys.platform)):
80
+ self.skip_reasons.append(f"not available on platform: '{sys.platform}'")
81
+
62
82
  self.test_name: str = test_name
63
83
  """ The name of this test. """
64
84
 
65
85
  self.base_dir: str = base_dir
66
- """ The base directory for this test (usually the dir the CLI test file lives. """
86
+ """
87
+ The base directory for this test (usually the dir the CLI test file lives.
88
+ This is the expansion for `__BASE_DIR__` paths.
89
+ """
90
+
91
+ self.data_dir: str = data_dir
92
+ """
93
+ A directory that additional testing data lives in.
94
+ This is the expansion for `__DATA_DIR__` paths.
95
+ """
67
96
 
68
97
  self.temp_dir: str = temp_dir
69
- """ A temp directory that this test has access to. """
98
+ """
99
+ A temp directory that this test has access to.
100
+ This is the expansion for `__TEMP_DIR__` paths.
101
+ """
70
102
 
71
103
  edq.util.dirent.mkdir(temp_dir)
72
104
 
@@ -76,9 +108,12 @@ class CLITestInfo:
76
108
  self.module_name: str = cli
77
109
  """ The name of the module to invoke. """
78
110
 
79
- self.module: typing.Any = edq.util.pyimport.import_name(self.module_name)
111
+ self.module: typing.Any = None
80
112
  """ The module to invoke. """
81
113
 
114
+ if (not self.should_skip()):
115
+ self.module = edq.util.pyimport.import_name(self.module_name)
116
+
82
117
  if (arguments is None):
83
118
  arguments = []
84
119
 
@@ -88,19 +123,16 @@ class CLITestInfo:
88
123
  self.error: bool = error
89
124
  """ Whether or not this test is expected to be an error (raise an exception). """
90
125
 
91
- self.platform_skip: typing.Union[str, None] = platform_skip
92
- """ If the current platform matches this regular expression, then the test will be skipped. """
93
-
94
126
  self.stdout_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None
95
127
  """ The assertion func to compare the expected and actual stdout of the CLI. """
96
128
 
97
- if (stdout_assertion_func is not None):
129
+ if ((stdout_assertion_func is not None) and (not self.should_skip())):
98
130
  self.stdout_assertion_func = edq.util.pyimport.fetch(stdout_assertion_func)
99
131
 
100
132
  self.stderr_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None
101
133
  """ The assertion func to compare the expected and actual stderr of the CLI. """
102
134
 
103
- if (stderr_assertion_func is not None):
135
+ if ((stderr_assertion_func is not None) and (not self.should_skip())):
104
136
  self.stderr_assertion_func = edq.util.pyimport.fetch(stderr_assertion_func)
105
137
 
106
138
  self.expected_stdout: str = expected_stdout
@@ -113,12 +145,33 @@ class CLITestInfo:
113
145
  self.expected_stdout = self.expected_stdout.strip()
114
146
  self.expected_stderr = self.expected_stderr.strip()
115
147
 
148
+ self.split_stdout_stderr: bool = split_stdout_stderr
149
+ """
150
+ Split stdout and stderr into different strings for testing.
151
+ By default, these two will be combined.
152
+ If both are non-empty, then they will be joined like: f"{stdout}\n{TEST_CASE_SEP}\n{stderr}".
153
+ Otherwise, only the non-empty one will be present with no separator.
154
+ Any stdout assertions will be applied to the combined text.
155
+ """
156
+
116
157
  # Make any path normalizations over the arguments and expected output.
117
158
  self.expected_stdout = self._expand_paths(self.expected_stdout)
118
159
  self.expected_stderr = self._expand_paths(self.expected_stderr)
119
160
  for (i, argument) in enumerate(self.arguments):
120
161
  self.arguments[i] = self._expand_paths(argument)
121
162
 
163
+ if (extra_options is None):
164
+ extra_options = {}
165
+
166
+ self.extra_options: typing.Union[typing.Dict[str, typing.Any], None] = extra_options
167
+ """
168
+ A place to store additional options.
169
+ Extra top-level options will cause tests to error.
170
+ """
171
+
172
+ if (len(kwargs) > 0):
173
+ raise ValueError(f"Found unknown CLI test options: '{kwargs}'.")
174
+
122
175
  def _expand_paths(self, text: str) -> str:
123
176
  """
124
177
  Expand path replacements in testing text.
@@ -126,9 +179,10 @@ class CLITestInfo:
126
179
  """
127
180
 
128
181
  replacements = [
129
- (DATA_DIR_ID, DATA_DIR),
182
+ (DATA_DIR_ID, self.data_dir),
130
183
  (TEMP_DIR_ID, self.temp_dir),
131
- (REL_DIR_ID, self.base_dir),
184
+ (BASE_DIR_ID, self.base_dir),
185
+ (ABS_DATA_DIR_ID, os.path.abspath(self.data_dir)),
132
186
  ]
133
187
 
134
188
  for (key, target_dir) in replacements:
@@ -136,8 +190,18 @@ class CLITestInfo:
136
190
 
137
191
  return text
138
192
 
193
+ def should_skip(self) -> bool:
194
+ """ Check if this test should be skipped. """
195
+
196
+ return (len(self.skip_reasons) > 0)
197
+
198
+ def skip_message(self) -> str:
199
+ """ Get a message displaying the reasons this test should be skipped. """
200
+
201
+ return f"This test has been skipped because of the following: {self.skip_reasons}."
202
+
139
203
  @staticmethod
140
- def load_path(path: str, test_name: str, base_temp_dir: str) -> 'CLITestInfo':
204
+ def load_path(path: str, test_name: str, base_temp_dir: str, data_dir: str) -> 'CLITestInfo':
141
205
  """ Load a CLI test file and extract the test info. """
142
206
 
143
207
  options, expected_stdout = read_test_file(path)
@@ -147,7 +211,7 @@ class CLITestInfo:
147
211
  base_dir = os.path.dirname(os.path.abspath(path))
148
212
  temp_dir = os.path.join(base_temp_dir, test_name)
149
213
 
150
- return CLITestInfo(test_name, base_dir, temp_dir, **options)
214
+ return CLITestInfo(test_name, base_dir, data_dir, temp_dir, **options)
151
215
 
152
216
  def read_test_file(path: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
153
217
  """ Read a test case file and split the output into JSON data and text. """
@@ -189,14 +253,18 @@ def replace_path_pattern(text: str, key: str, target_dir: str) -> str:
189
253
 
190
254
  return text
191
255
 
192
- def _get_test_method(test_name: str, path: str) -> typing.Callable:
256
+ def _get_test_method(test_name: str, path: str, data_dir: str) -> typing.Callable:
193
257
  """ Get a test method that represents the test case at the given path. """
194
258
 
195
259
  def __method(self: edq.testing.unittest.BaseTest) -> None:
196
- test_info = CLITestInfo.load_path(path, test_name, getattr(self, BASE_TEMP_DIR_ATTR))
260
+ test_info = CLITestInfo.load_path(path, test_name, getattr(self, BASE_TEMP_DIR_ATTR), data_dir)
261
+
262
+ # Allow the test class a chance to modify the test info before the test runs.
263
+ if (hasattr(self, 'modify_cli_test_info')):
264
+ self.modify_cli_test_info(test_info)
197
265
 
198
- if ((test_info.platform_skip is not None) and re.search(test_info.platform_skip, sys.platform)):
199
- self.skipTest(f"Test is not available on {sys.platform}.")
266
+ if (test_info.should_skip()):
267
+ self.skipTest(test_info.skip_message())
200
268
 
201
269
  old_args = sys.argv
202
270
  sys.argv = [test_info.module.__file__] + test_info.arguments
@@ -223,6 +291,12 @@ def _get_test_method(test_name: str, path: str) -> typing.Callable:
223
291
  finally:
224
292
  sys.argv = old_args
225
293
 
294
+ if (not test_info.split_stdout_stderr):
295
+ if ((len(stdout_text) > 0) and (len(stderr_text) > 0)):
296
+ stdout_text = f"{stdout_text}\n{TEST_CASE_SEP}\n{stderr_text}"
297
+ elif (len(stderr_text) > 0):
298
+ stdout_text = stderr_text
299
+
226
300
  if (test_info.stdout_assertion_func is not None):
227
301
  test_info.stdout_assertion_func(self, test_info.expected_stdout, stdout_text)
228
302
 
@@ -231,7 +305,7 @@ def _get_test_method(test_name: str, path: str) -> typing.Callable:
231
305
 
232
306
  return __method
233
307
 
234
- def add_test_paths(target_class: type, paths: typing.List[str]) -> None:
308
+ def add_test_paths(target_class: type, data_dir: str, paths: typing.List[str]) -> None:
235
309
  """ Add tests from the given test files. """
236
310
 
237
311
  # Attach a temp directory to the testing class so all tests can share a common base temp dir.
@@ -239,15 +313,19 @@ def add_test_paths(target_class: type, paths: typing.List[str]) -> None:
239
313
  setattr(target_class, BASE_TEMP_DIR_ATTR, edq.util.dirent.get_temp_path('edq_cli_test_'))
240
314
 
241
315
  for path in sorted(paths):
242
- test_name = 'test_cli__' + os.path.splitext(os.path.basename(path))[0]
316
+ basename = os.path.splitext(os.path.basename(path))[0]
317
+ if (hasattr(target_class, 'get_test_basename')):
318
+ basename = getattr(target_class, 'get_test_basename')(path)
319
+
320
+ test_name = 'test_cli__' + basename
243
321
 
244
322
  try:
245
- setattr(target_class, test_name, _get_test_method(test_name, path))
323
+ setattr(target_class, test_name, _get_test_method(test_name, path, data_dir))
246
324
  except Exception as ex:
247
325
  raise ValueError(f"Failed to parse test case '{path}'.") from ex
248
326
 
249
- def discover_test_cases(target_class: type) -> None:
327
+ def discover_test_cases(target_class: type, test_cases_dir: str, data_dir: str) -> None:
250
328
  """ Look in the text cases directory for any test cases and add them as test methods to the test class. """
251
329
 
252
- paths = list(sorted(glob.glob(os.path.join(TEST_CASES_DIR, "**", "*.txt"), recursive = True)))
253
- add_test_paths(target_class, paths)
330
+ paths = list(sorted(glob.glob(os.path.join(test_cases_dir, "**", "*.txt"), recursive = True)))
331
+ add_test_paths(target_class, data_dir, paths)
edq/testing/cli_test.py CHANGED
@@ -1,8 +1,15 @@
1
+ import os
2
+
1
3
  import edq.testing.cli
2
4
  import edq.testing.unittest
3
5
 
6
+ THIS_DIR: str = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
7
+ BASE_TESTDATA_DIR: str = os.path.join(THIS_DIR, "testdata", "cli")
8
+ TEST_CASES_DIR: str = os.path.join(BASE_TESTDATA_DIR, "tests")
9
+ DATA_DIR: str = os.path.join(BASE_TESTDATA_DIR, "data")
10
+
4
11
  class CLITest(edq.testing.unittest.BaseTest):
5
12
  """ Test CLI invocations. """
6
13
 
7
14
  # Populate CLITest with all the test methods.
8
- edq.testing.cli.discover_test_cases(CLITest)
15
+ edq.testing.cli.discover_test_cases(CLITest, TEST_CASES_DIR, DATA_DIR)