edq-utils 0.0.2__py3-none-any.whl → 0.0.4__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.

edq/testing/cli.py ADDED
@@ -0,0 +1,253 @@
1
+ """
2
+ Infrastructure for testing CLI tools using a JSON file which describes a test case,
3
+ which is essentially an invocation of a CLI tool and the expected output.
4
+
5
+ The test case file must be a `.txt` file that live in TEST_CASES_DIR.
6
+ The file contains two parts (separated by a line with just TEST_CASE_SEP):
7
+ the first part which is a JSON object (see below for available keys),
8
+ and a second part which is the expected text output (stdout).
9
+ For the keys of the JSON section, see the defaulted arguments to CLITestInfo.
10
+ The options JSON will be splatted into CLITestInfo's constructor.
11
+
12
+ The expected output or any argument can reference the test's current temp or data dirs with `__TEMP_DIR__()` or `__DATA_DIR__()`, respectively.
13
+ An optional slash-separated path can be used as an argument to reference a path within those base directories.
14
+ For example, `__DATA_DIR__(foo/bar.txt)` references `bar.txt` inside the `foo` directory inside the data directory.
15
+ """
16
+
17
+ import contextlib
18
+ import glob
19
+ import io
20
+ import os
21
+ import re
22
+ import sys
23
+ import typing
24
+
25
+ import edq.testing.asserts
26
+ import edq.testing.unittest
27
+ import edq.util.dirent
28
+ import edq.util.json
29
+ import edq.util.pyimport
30
+
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
+ TEST_CASE_SEP: str = '---'
37
+ DATA_DIR_ID: str = '__DATA_DIR__'
38
+ TEMP_DIR_ID: str = '__TEMP_DIR__'
39
+ REL_DIR_ID: str = '__REL_DIR__'
40
+
41
+ DEFAULT_ASSERTION_FUNC_NAME: str = 'edq.testing.asserts.content_equals_normalize'
42
+
43
+ BASE_TEMP_DIR_ATTR: str = '_edq_cli_base_test_dir'
44
+
45
+ class CLITestInfo:
46
+ """ The required information to run a CLI test. """
47
+
48
+ def __init__(self,
49
+ test_name: str,
50
+ base_dir: str,
51
+ temp_dir: str,
52
+ cli: typing.Union[str, None] = None,
53
+ arguments: typing.Union[typing.List[str], None] = None,
54
+ error: bool = False,
55
+ platform_skip: typing.Union[str, None] = None,
56
+ stdout_assertion_func: typing.Union[str, None] = DEFAULT_ASSERTION_FUNC_NAME,
57
+ stderr_assertion_func: typing.Union[str, None] = None,
58
+ expected_stdout: str = '',
59
+ expected_stderr: str = '',
60
+ strip_error_output: bool = True,
61
+ **kwargs: typing.Any) -> None:
62
+ self.test_name: str = test_name
63
+ """ The name of this test. """
64
+
65
+ self.base_dir: str = base_dir
66
+ """ The base directory for this test (usually the dir the CLI test file lives. """
67
+
68
+ self.temp_dir: str = temp_dir
69
+ """ A temp directory that this test has access to. """
70
+
71
+ edq.util.dirent.mkdir(temp_dir)
72
+
73
+ if (cli is None):
74
+ raise ValueError("Missing CLI module.")
75
+
76
+ self.module_name: str = cli
77
+ """ The name of the module to invoke. """
78
+
79
+ self.module: typing.Any = edq.util.pyimport.import_name(self.module_name)
80
+ """ The module to invoke. """
81
+
82
+ if (arguments is None):
83
+ arguments = []
84
+
85
+ self.arguments: typing.List[str] = arguments
86
+ """ The CLI arguments. """
87
+
88
+ self.error: bool = error
89
+ """ Whether or not this test is expected to be an error (raise an exception). """
90
+
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
+ self.stdout_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None
95
+ """ The assertion func to compare the expected and actual stdout of the CLI. """
96
+
97
+ if (stdout_assertion_func is not None):
98
+ self.stdout_assertion_func = edq.util.pyimport.fetch(stdout_assertion_func)
99
+
100
+ self.stderr_assertion_func: typing.Union[edq.testing.asserts.StringComparisonAssertion, None] = None
101
+ """ The assertion func to compare the expected and actual stderr of the CLI. """
102
+
103
+ if (stderr_assertion_func is not None):
104
+ self.stderr_assertion_func = edq.util.pyimport.fetch(stderr_assertion_func)
105
+
106
+ self.expected_stdout: str = expected_stdout
107
+ """ The expected stdout. """
108
+
109
+ self.expected_stderr: str = expected_stderr
110
+ """ The expected stderr. """
111
+
112
+ if (error and strip_error_output):
113
+ self.expected_stdout = self.expected_stdout.strip()
114
+ self.expected_stderr = self.expected_stderr.strip()
115
+
116
+ # Make any path normalizations over the arguments and expected output.
117
+ self.expected_stdout = self._expand_paths(self.expected_stdout)
118
+ self.expected_stderr = self._expand_paths(self.expected_stderr)
119
+ for (i, argument) in enumerate(self.arguments):
120
+ self.arguments[i] = self._expand_paths(argument)
121
+
122
+ def _expand_paths(self, text: str) -> str:
123
+ """
124
+ Expand path replacements in testing text.
125
+ This allows for consistent paths (even absolute paths) in the test text.
126
+ """
127
+
128
+ replacements = [
129
+ (DATA_DIR_ID, DATA_DIR),
130
+ (TEMP_DIR_ID, self.temp_dir),
131
+ (REL_DIR_ID, self.base_dir),
132
+ ]
133
+
134
+ for (key, target_dir) in replacements:
135
+ text = replace_path_pattern(text, key, target_dir)
136
+
137
+ return text
138
+
139
+ @staticmethod
140
+ def load_path(path: str, test_name: str, base_temp_dir: str) -> 'CLITestInfo':
141
+ """ Load a CLI test file and extract the test info. """
142
+
143
+ options, expected_stdout = read_test_file(path)
144
+
145
+ options['expected_stdout'] = expected_stdout
146
+
147
+ base_dir = os.path.dirname(os.path.abspath(path))
148
+ temp_dir = os.path.join(base_temp_dir, test_name)
149
+
150
+ return CLITestInfo(test_name, base_dir, temp_dir, **options)
151
+
152
+ def read_test_file(path: str) -> typing.Tuple[typing.Dict[str, typing.Any], str]:
153
+ """ Read a test case file and split the output into JSON data and text. """
154
+
155
+ json_lines: typing.List[str] = []
156
+ output_lines: typing.List[str] = []
157
+
158
+ text = edq.util.dirent.read_file(path, strip = False)
159
+
160
+ accumulator = json_lines
161
+ for line in text.split("\n"):
162
+ if (line.strip() == TEST_CASE_SEP):
163
+ accumulator = output_lines
164
+ continue
165
+
166
+ accumulator.append(line)
167
+
168
+ options = edq.util.json.loads(''.join(json_lines))
169
+ output = "\n".join(output_lines)
170
+
171
+ return options, output
172
+
173
+ def replace_path_pattern(text: str, key: str, target_dir: str) -> str:
174
+ """ Make any test replacement inside the given string. """
175
+
176
+ match = re.search(rf'{key}\(([^)]*)\)', text)
177
+ if (match is not None):
178
+ filename = match.group(1)
179
+
180
+ # Normalize any path separators.
181
+ filename = os.path.join(*filename.split('/'))
182
+
183
+ if (filename == ''):
184
+ path = target_dir
185
+ else:
186
+ path = os.path.join(target_dir, filename)
187
+
188
+ text = text.replace(match.group(0), path)
189
+
190
+ return text
191
+
192
+ def _get_test_method(test_name: str, path: str) -> typing.Callable:
193
+ """ Get a test method that represents the test case at the given path. """
194
+
195
+ def __method(self: edq.testing.unittest.BaseTest) -> None:
196
+ test_info = CLITestInfo.load_path(path, test_name, getattr(self, BASE_TEMP_DIR_ATTR))
197
+
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}.")
200
+
201
+ old_args = sys.argv
202
+ sys.argv = [test_info.module.__file__] + test_info.arguments
203
+
204
+ try:
205
+ with contextlib.redirect_stdout(io.StringIO()) as stdout_output:
206
+ with contextlib.redirect_stderr(io.StringIO()) as stderr_output:
207
+ test_info.module.main()
208
+
209
+ stdout_text = stdout_output.getvalue()
210
+ stderr_text = stderr_output.getvalue()
211
+
212
+ if (test_info.error):
213
+ self.fail(f"No error was not raised when one was expected ('{str(test_info.expected_stdout)}').")
214
+ except BaseException as ex:
215
+ if (not test_info.error):
216
+ raise ex
217
+
218
+ stdout_text = self.format_error_string(ex)
219
+
220
+ stderr_text = ''
221
+ if (isinstance(ex, SystemExit) and (ex.__context__ is not None)):
222
+ stderr_text = self.format_error_string(ex.__context__)
223
+ finally:
224
+ sys.argv = old_args
225
+
226
+ if (test_info.stdout_assertion_func is not None):
227
+ test_info.stdout_assertion_func(self, test_info.expected_stdout, stdout_text)
228
+
229
+ if (test_info.stderr_assertion_func is not None):
230
+ test_info.stderr_assertion_func(self, test_info.expected_stderr, stderr_text)
231
+
232
+ return __method
233
+
234
+ def add_test_paths(target_class: type, paths: typing.List[str]) -> None:
235
+ """ Add tests from the given test files. """
236
+
237
+ # Attach a temp directory to the testing class so all tests can share a common base temp dir.
238
+ if (not hasattr(target_class, BASE_TEMP_DIR_ATTR)):
239
+ setattr(target_class, BASE_TEMP_DIR_ATTR, edq.util.dirent.get_temp_path('edq_cli_test_'))
240
+
241
+ for path in sorted(paths):
242
+ test_name = 'test_cli__' + os.path.splitext(os.path.basename(path))[0]
243
+
244
+ try:
245
+ setattr(target_class, test_name, _get_test_method(test_name, path))
246
+ except Exception as ex:
247
+ raise ValueError(f"Failed to parse test case '{path}'.") from ex
248
+
249
+ def discover_test_cases(target_class: type) -> None:
250
+ """ Look in the text cases directory for any test cases and add them as test methods to the test class. """
251
+
252
+ paths = list(sorted(glob.glob(os.path.join(TEST_CASES_DIR, "**", "*.txt"), recursive = True)))
253
+ add_test_paths(target_class, paths)
@@ -0,0 +1,8 @@
1
+ import edq.testing.cli
2
+ import edq.testing.unittest
3
+
4
+ class CLITest(edq.testing.unittest.BaseTest):
5
+ """ Test CLI invocations. """
6
+
7
+ # Populate CLITest with all the test methods.
8
+ edq.testing.cli.discover_test_cases(CLITest)
edq/testing/run.py CHANGED
@@ -11,10 +11,6 @@ import sys
11
11
  import typing
12
12
  import unittest
13
13
 
14
- THIS_DIR: str = os.path.join(os.path.dirname(os.path.realpath(__file__)))
15
- BASE_PACKAGE_DIR: str = os.path.join(THIS_DIR, '..')
16
- PROJECT_ROOT_DIR: str = os.path.join(BASE_PACKAGE_DIR, '..')
17
-
18
14
  DEFAULT_TEST_FILENAME_PATTERN: str = '*_test.py'
19
15
 
20
16
  def _collect_tests(suite: typing.Union[unittest.TestCase, unittest.suite.TestSuite]) -> typing.List[unittest.TestCase]:
@@ -41,13 +37,22 @@ def run(args: argparse.Namespace) -> int:
41
37
  Will raise if tests fail to load (e.g. syntax errors) and a suggested exit code otherwise.
42
38
  """
43
39
 
44
- # Start in the project's root and add it in the path.
45
- os.chdir(PROJECT_ROOT_DIR)
46
- sys.path.append(PROJECT_ROOT_DIR)
40
+ if (args.work_dir is not None):
41
+ os.chdir(args.work_dir)
42
+
43
+ if (args.path_additions is not None):
44
+ for path in args.path_additions:
45
+ sys.path.append(path)
46
+
47
+ if (args.test_dirs is None):
48
+ args.test_dirs = ['.']
47
49
 
48
50
  runner = unittest.TextTestRunner(verbosity = 3)
49
- discovered_suite = unittest.TestLoader().discover(BASE_PACKAGE_DIR, pattern = args.filename_pattern)
50
- test_cases = _collect_tests(discovered_suite)
51
+ test_cases = []
52
+
53
+ for test_dir in args.test_dirs:
54
+ discovered_suite = unittest.TestLoader().discover(test_dir, pattern = args.filename_pattern)
55
+ test_cases += _collect_tests(discovered_suite)
51
56
 
52
57
  tests = unittest.suite.TestSuite()
53
58
 
@@ -64,27 +69,43 @@ def run(args: argparse.Namespace) -> int:
64
69
  faults = len(result.errors) + len(result.failures)
65
70
 
66
71
  if (not result.wasSuccessful()):
67
- # This value will be used as an exit status, so don't larger than a byte.
72
+ # This value will be used as an exit status, so it should not be larger than a byte.
68
73
  # (Some higher values are used specially, so just keep it at a round number.)
69
74
  return max(1, min(faults, 100))
70
75
 
71
76
  return 0
72
77
 
73
78
  def main() -> int:
79
+ """ Parse the CLI arguments and run tests. """
80
+
74
81
  args = _get_parser().parse_args()
75
82
  return run(args)
76
83
 
77
84
  def _get_parser() -> argparse.ArgumentParser:
85
+ """ Build a parser for CLI arguments. """
86
+
78
87
  parser = argparse.ArgumentParser(description = 'Run unit tests discovered in this project.')
79
88
 
80
- parser.add_argument('pattern',
81
- action = 'store', type = str, default = None, nargs = '?',
82
- help = 'If supplied, only tests with names matching this pattern will be run. This pattern is used directly in re.search().')
89
+ parser.add_argument('--work-dir', dest = 'work_dir',
90
+ action = 'store', type = str, default = os.getcwd(),
91
+ help = 'Set the working directory when running tests, defaults to the current working directory (%(default)s).')
92
+
93
+ parser.add_argument('--tests-dir', dest = 'test_dirs',
94
+ action = 'append',
95
+ help = 'Discover tests from these directories. Defaults to the current directory.')
96
+
97
+ parser.add_argument('--add-path', dest = 'path_additions',
98
+ action = 'append',
99
+ help = 'If supplied, add this path the sys.path before running tests.')
83
100
 
84
101
  parser.add_argument('--filename-pattern', dest = 'filename_pattern',
85
102
  action = 'store', type = str, default = DEFAULT_TEST_FILENAME_PATTERN,
86
103
  help = 'The pattern to use to find test files (default: %(default)s).')
87
104
 
105
+ parser.add_argument('pattern',
106
+ action = 'store', type = str, default = None, nargs = '?',
107
+ help = 'If supplied, only tests with names matching this pattern will be run. This pattern is used directly in re.search().')
108
+
88
109
  return parser
89
110
 
90
111
  if __name__ == '__main__':
@@ -0,0 +1,6 @@
1
+ {
2
+ "cli": "edq.cli.version",
3
+ "arguments": [],
4
+ }
5
+ ---
6
+ <VERSION>
edq/testing/unittest.py CHANGED
@@ -1,4 +1,3 @@
1
- import json
2
1
  import typing
3
2
  import unittest
4
3
 
@@ -15,13 +14,21 @@ class BaseTest(unittest.TestCase):
15
14
  maxDiff = None
16
15
  """ Don't limit the size of diffs. """
17
16
 
18
- def assertJSONDictEqual(self, a: dict, b: dict) -> None:
17
+ def assertJSONDictEqual(self, a: typing.Dict[str, typing.Any], b: typing.Dict[str, typing.Any]) -> None: # pylint: disable=invalid-name
18
+ """
19
+ Call assertDictEqual(), but supply a message containing the full JSON representation of the arguments.
20
+ """
21
+
19
22
  a_json = edq.util.json.dumps(a, indent = 4)
20
23
  b_json = edq.util.json.dumps(b, indent = 4)
21
24
 
22
25
  super().assertDictEqual(a, b, FORMAT_STR % (a_json, b_json))
23
26
 
24
- def assertJSONListEqual(self, a: list, b: list) -> None:
27
+ def assertJSONListEqual(self, a: typing.List[typing.Any], b: typing.List[typing.Any]) -> None: # pylint: disable=invalid-name
28
+ """
29
+ Call assertListEqual(), but supply a message containing the full JSON representation of the arguments.
30
+ """
31
+
25
32
  a_json = edq.util.json.dumps(a, indent = 4)
26
33
  b_json = edq.util.json.dumps(b, indent = 4)
27
34
 
edq/util/__init__.py CHANGED
@@ -0,0 +1,3 @@
1
+ """
2
+ Low-level utilities.
3
+ """
edq/util/dirent.py CHANGED
@@ -13,6 +13,7 @@ import atexit
13
13
  import os
14
14
  import shutil
15
15
  import tempfile
16
+ import typing
16
17
  import uuid
17
18
 
18
19
  DEFAULT_ENCODING: str = 'utf-8'
@@ -114,7 +115,7 @@ def remove(path: str) -> None:
114
115
  else:
115
116
  raise ValueError(f"Unknown type of dirent: '{path}'.")
116
117
 
117
- def same(a: str, b: str):
118
+ def same(a: str, b: str) -> bool:
118
119
  """
119
120
  Check if two paths represent the same dirent.
120
121
  If either (or both) paths do not exist, false will be returned.
@@ -247,10 +248,10 @@ def read_file(raw_path: str, strip: bool = True, encoding: str = DEFAULT_ENCODIN
247
248
  return contents
248
249
 
249
250
  def write_file(
250
- raw_path: str, contents: str,
251
+ raw_path: str, contents: typing.Union[str, None],
251
252
  strip: bool = True, newline: bool = True,
252
253
  encoding: str = DEFAULT_ENCODING,
253
- no_clobber = False) -> None:
254
+ no_clobber: bool = False) -> None:
254
255
  """
255
256
  Write the contents of a file.
256
257
  If clobbering, any existing dirent will be removed before write.
@@ -276,6 +277,39 @@ def write_file(
276
277
  with open(path, 'w', encoding = encoding) as file:
277
278
  file.write(contents)
278
279
 
280
+ def read_file_bytes(raw_path: str) -> bytes:
281
+ """ Read the contents of a file as bytes. """
282
+
283
+ path = os.path.abspath(raw_path)
284
+
285
+ if (not exists(path)):
286
+ raise ValueError(f"Source of read bytes does not exist: '{raw_path}'.")
287
+
288
+ with open(path, 'rb') as file:
289
+ return file.read()
290
+
291
+ def write_file_bytes(
292
+ raw_path: str, contents: typing.Union[bytes, None],
293
+ no_clobber: bool = False) -> None:
294
+ """
295
+ Write the contents of a file as bytes.
296
+ If clobbering, any existing dirent will be removed before write.
297
+ """
298
+
299
+ path = os.path.abspath(raw_path)
300
+
301
+ if (exists(path)):
302
+ if (no_clobber):
303
+ raise ValueError(f"Destination of write bytes already exists: '{raw_path}'.")
304
+
305
+ remove(path)
306
+
307
+ if (contents is None):
308
+ contents = b''
309
+
310
+ with open(path, 'wb') as file:
311
+ file.write(contents)
312
+
279
313
  def contains_path(parent: str, child: str) -> bool:
280
314
  """
281
315
  Check if the parent path contains the child path.
edq/util/dirent_test.py CHANGED
@@ -92,6 +92,124 @@ class TestDirent(edq.testing.unittest.BaseTest):
92
92
  actual = edq.util.dirent.contains_path(parent, child)
93
93
  self.assertEqual(expected, actual)
94
94
 
95
+ def test_read_write_file_bytes_base(self):
96
+ """ Test reading and writing a file as bytes. """
97
+
98
+ # [(path, write kwargs, read kwargs, write contents, expected contents, error substring), ...]
99
+ # All conent should be strings that will be encoded.
100
+ test_cases = [
101
+ # Base
102
+ (
103
+ "test.txt",
104
+ {},
105
+ {},
106
+ "test",
107
+ "test",
108
+ None,
109
+ ),
110
+
111
+ # Empty Write
112
+ (
113
+ "test.txt",
114
+ {},
115
+ {},
116
+ "",
117
+ "",
118
+ None,
119
+ ),
120
+
121
+ # None Write
122
+ (
123
+ "test.txt",
124
+ {},
125
+ {},
126
+ None,
127
+ "",
128
+ None,
129
+ ),
130
+
131
+ # Clobber
132
+ (
133
+ "a.txt",
134
+ {},
135
+ {},
136
+ "test",
137
+ "test",
138
+ None,
139
+ ),
140
+ (
141
+ "dir_1",
142
+ {},
143
+ {},
144
+ "test",
145
+ "test",
146
+ None,
147
+ ),
148
+ (
149
+ "symlink_a.txt",
150
+ {},
151
+ {},
152
+ "test",
153
+ "test",
154
+ None,
155
+ ),
156
+
157
+ # No Clobber
158
+ (
159
+ "a.txt",
160
+ {'no_clobber': True},
161
+ {},
162
+ "test",
163
+ "test",
164
+ 'already exists',
165
+ ),
166
+ (
167
+ "dir_1",
168
+ {'no_clobber': True},
169
+ {},
170
+ "test",
171
+ "test",
172
+ 'already exists',
173
+ ),
174
+ (
175
+ "symlink_a.txt",
176
+ {'no_clobber': True},
177
+ {},
178
+ "test",
179
+ "test",
180
+ 'already exists',
181
+ ),
182
+ ]
183
+
184
+ for (i, test_case) in enumerate(test_cases):
185
+ (path, write_options, read_options, write_contents, expected_contents, error_substring) = test_case
186
+
187
+ with self.subTest(msg = f"Case {i} ('{path}'):"):
188
+ temp_dir = self._prep_temp_dir()
189
+ path = os.path.join(temp_dir, path)
190
+
191
+ if (write_contents is not None):
192
+ write_contents = bytes(write_contents, edq.util.dirent.DEFAULT_ENCODING)
193
+
194
+ expected_contents = bytes(expected_contents, edq.util.dirent.DEFAULT_ENCODING)
195
+
196
+ try:
197
+ edq.util.dirent.write_file_bytes(path, write_contents, **write_options)
198
+ actual_contents = edq.util.dirent.read_file_bytes(path, **read_options)
199
+ except Exception as ex:
200
+ error_string = self.format_error_string(ex)
201
+ if (error_substring is None):
202
+ self.fail(f"Unexpected error: '{error_string}'.")
203
+
204
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
205
+
206
+ continue
207
+
208
+ if (error_substring is not None):
209
+ self.fail(f"Did not get expected error: '{error_substring}'.")
210
+
211
+ self.assertEqual(expected_contents, actual_contents)
212
+
95
213
  def test_read_write_file_base(self):
96
214
  """ Test reading and writing a file. """
97
215
 
@@ -157,7 +275,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
157
275
  None,
158
276
  ),
159
277
 
160
- # None
278
+ # None Write
161
279
  (
162
280
  "test.txt",
163
281
  {'newline': False},
@@ -225,7 +343,6 @@ class TestDirent(edq.testing.unittest.BaseTest):
225
343
 
226
344
  with self.subTest(msg = f"Case {i} ('{path}'):"):
227
345
  temp_dir = self._prep_temp_dir()
228
-
229
346
  path = os.path.join(temp_dir, path)
230
347
 
231
348
  try:
@@ -243,7 +360,7 @@ class TestDirent(edq.testing.unittest.BaseTest):
243
360
  if (error_substring is not None):
244
361
  self.fail(f"Did not get expected error: '{error_substring}'.")
245
362
 
246
- self.assertEqual(expected_contents, actual_contents)
363
+ self.assertEqual(expected_contents, actual_contents)
247
364
 
248
365
  def test_copy_contents_base(self):
249
366
  """