edq-utils 0.2.3__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 (88) hide show
  1. edq/__init__.py +5 -0
  2. edq/cli/__init__.py +0 -0
  3. edq/cli/__main__.py +17 -0
  4. edq/cli/config/__init__.py +3 -0
  5. edq/cli/config/__main__.py +15 -0
  6. edq/cli/config/list.py +69 -0
  7. edq/cli/http/__init__.py +3 -0
  8. edq/cli/http/__main__.py +15 -0
  9. edq/cli/http/exchange-server.py +71 -0
  10. edq/cli/http/send-exchange.py +45 -0
  11. edq/cli/http/verify-exchanges.py +38 -0
  12. edq/cli/testing/__init__.py +3 -0
  13. edq/cli/testing/__main__.py +15 -0
  14. edq/cli/testing/cli-test.py +49 -0
  15. edq/cli/version.py +28 -0
  16. edq/core/__init__.py +0 -0
  17. edq/core/argparser.py +137 -0
  18. edq/core/argparser_test.py +124 -0
  19. edq/core/config.py +268 -0
  20. edq/core/config_test.py +1038 -0
  21. edq/core/log.py +101 -0
  22. edq/core/version.py +6 -0
  23. edq/procedure/__init__.py +0 -0
  24. edq/procedure/verify_exchanges.py +85 -0
  25. edq/py.typed +0 -0
  26. edq/testing/__init__.py +3 -0
  27. edq/testing/asserts.py +65 -0
  28. edq/testing/cli.py +360 -0
  29. edq/testing/cli_test.py +15 -0
  30. edq/testing/httpserver.py +578 -0
  31. edq/testing/httpserver_test.py +424 -0
  32. edq/testing/run.py +142 -0
  33. edq/testing/testdata/cli/data/configs/empty/edq-config.json +1 -0
  34. edq/testing/testdata/cli/data/configs/simple-1/edq-config.json +4 -0
  35. edq/testing/testdata/cli/data/configs/simple-2/edq-config.json +3 -0
  36. edq/testing/testdata/cli/data/configs/value-number/edq-config.json +3 -0
  37. edq/testing/testdata/cli/tests/config/list/config_list_base.txt +16 -0
  38. edq/testing/testdata/cli/tests/config/list/config_list_config_value_number.txt +10 -0
  39. edq/testing/testdata/cli/tests/config/list/config_list_ignore_config.txt +14 -0
  40. edq/testing/testdata/cli/tests/config/list/config_list_no_config.txt +8 -0
  41. edq/testing/testdata/cli/tests/config/list/config_list_show_origin.txt +13 -0
  42. edq/testing/testdata/cli/tests/config/list/config_list_skip_header.txt +10 -0
  43. edq/testing/testdata/cli/tests/help_base.txt +9 -0
  44. edq/testing/testdata/cli/tests/platform_skip.txt +5 -0
  45. edq/testing/testdata/cli/tests/version_base.txt +6 -0
  46. edq/testing/testdata/http/exchanges/simple.httpex.json +5 -0
  47. edq/testing/testdata/http/exchanges/simple_anchor.httpex.json +5 -0
  48. edq/testing/testdata/http/exchanges/simple_file.httpex.json +10 -0
  49. edq/testing/testdata/http/exchanges/simple_file_binary.httpex.json +10 -0
  50. edq/testing/testdata/http/exchanges/simple_file_get_params.httpex.json +14 -0
  51. edq/testing/testdata/http/exchanges/simple_file_multiple.httpex.json +13 -0
  52. edq/testing/testdata/http/exchanges/simple_file_name.httpex.json +11 -0
  53. edq/testing/testdata/http/exchanges/simple_file_post_multiple.httpex.json +13 -0
  54. edq/testing/testdata/http/exchanges/simple_file_post_params.httpex.json +14 -0
  55. edq/testing/testdata/http/exchanges/simple_headers.httpex.json +8 -0
  56. edq/testing/testdata/http/exchanges/simple_jsonresponse_dict.httpex.json +7 -0
  57. edq/testing/testdata/http/exchanges/simple_jsonresponse_list.httpex.json +9 -0
  58. edq/testing/testdata/http/exchanges/simple_params.httpex.json +9 -0
  59. edq/testing/testdata/http/exchanges/simple_post.httpex.json +5 -0
  60. edq/testing/testdata/http/exchanges/simple_post_params.httpex.json +9 -0
  61. edq/testing/testdata/http/exchanges/simple_post_urlparams.httpex.json +5 -0
  62. edq/testing/testdata/http/exchanges/simple_urlparams.httpex.json +5 -0
  63. edq/testing/testdata/http/exchanges/specialcase_listparams_explicit.httpex.json +8 -0
  64. edq/testing/testdata/http/exchanges/specialcase_listparams_url.httpex.json +5 -0
  65. edq/testing/testdata/http/files/a.txt +1 -0
  66. edq/testing/testdata/http/files/tiny.png +0 -0
  67. edq/testing/unittest.py +88 -0
  68. edq/util/__init__.py +3 -0
  69. edq/util/cli.py +151 -0
  70. edq/util/dirent.py +346 -0
  71. edq/util/dirent_test.py +1004 -0
  72. edq/util/encoding.py +18 -0
  73. edq/util/hash.py +41 -0
  74. edq/util/hash_test.py +89 -0
  75. edq/util/json.py +180 -0
  76. edq/util/json_test.py +228 -0
  77. edq/util/net.py +1047 -0
  78. edq/util/parse.py +33 -0
  79. edq/util/pyimport.py +94 -0
  80. edq/util/pyimport_test.py +119 -0
  81. edq/util/reflection.py +32 -0
  82. edq/util/time.py +75 -0
  83. edq/util/time_test.py +107 -0
  84. edq_utils-0.2.3.dist-info/METADATA +164 -0
  85. edq_utils-0.2.3.dist-info/RECORD +88 -0
  86. edq_utils-0.2.3.dist-info/WHEEL +5 -0
  87. edq_utils-0.2.3.dist-info/licenses/LICENSE +21 -0
  88. edq_utils-0.2.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1004 @@
1
+ import os
2
+
3
+ import edq.testing.unittest
4
+ import edq.util.dirent
5
+
6
+ DIRENT_TYPE_DIR = 'dir'
7
+ DIRENT_TYPE_FILE = 'file'
8
+ DIRENT_TYPE_BROKEN_SYMLINK = 'broken_symlink'
9
+
10
+ def create_test_dir(temp_dir_prefix: str) -> str:
11
+ """
12
+ Create a temp dir and populate it with dirents for testing.
13
+
14
+ This test data directory is laid out as:
15
+ .
16
+ ├── a.txt
17
+ ├── dir_1
18
+ │   ├── b.txt
19
+ │   └── dir_2
20
+ │   └── c.txt
21
+ ├── dir_empty
22
+ ├── file_empty
23
+ ├── symlink_a.txt -> a.txt
24
+ ├── symlink_dir_1 -> dir_1
25
+ ├── symlink_dir_empty -> dir_empty
26
+ └── symlink_file_empty -> file_empty
27
+
28
+ Where non-empty files are filled with their filename (without the extension).
29
+ """
30
+
31
+ temp_dir = edq.util.dirent.get_temp_dir(prefix = temp_dir_prefix)
32
+
33
+ # Dirs
34
+ edq.util.dirent.mkdir(os.path.join(temp_dir, 'dir_1', 'dir_2'))
35
+ edq.util.dirent.mkdir(os.path.join(temp_dir, 'dir_empty'))
36
+
37
+ # Files
38
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'a.txt'), 'a')
39
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'file_empty'), '')
40
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'dir_1', 'b.txt'), 'b')
41
+ edq.util.dirent.write_file(os.path.join(temp_dir, 'dir_1', 'dir_2', 'c.txt'), 'c')
42
+
43
+ # Links
44
+ os.symlink('a.txt', os.path.join(temp_dir, 'symlink_a.txt'))
45
+ os.symlink('dir_1', os.path.join(temp_dir, 'symlink_dir_1'))
46
+ os.symlink('dir_empty', os.path.join(temp_dir, 'symlink_dir_empty'))
47
+ os.symlink('file_empty', os.path.join(temp_dir, 'symlink_file_empty'))
48
+
49
+ return temp_dir
50
+
51
+ class TestDirent(edq.testing.unittest.BaseTest):
52
+ """ Test basic operations on dirents. """
53
+
54
+ def test_setup(self):
55
+ """ Test that the base temp directory is properly setup. """
56
+
57
+ temp_dir = self._prep_temp_dir()
58
+
59
+ expected_paths = [
60
+ ('a.txt', DIRENT_TYPE_FILE),
61
+ ('dir_1', DIRENT_TYPE_DIR),
62
+ (os.path.join('dir_1', 'b.txt'), DIRENT_TYPE_FILE),
63
+ (os.path.join('dir_1', 'dir_2'), DIRENT_TYPE_DIR),
64
+ (os.path.join('dir_1', 'dir_2', 'c.txt'), DIRENT_TYPE_FILE),
65
+ ('dir_empty', DIRENT_TYPE_DIR),
66
+ ('file_empty', DIRENT_TYPE_FILE),
67
+ ('symlink_a.txt', DIRENT_TYPE_FILE, True),
68
+ ('symlink_dir_1', DIRENT_TYPE_DIR, True),
69
+ ('symlink_dir_empty', DIRENT_TYPE_DIR, True),
70
+ ('symlink_file_empty', DIRENT_TYPE_FILE, True),
71
+ ]
72
+
73
+ self._check_existing_paths(temp_dir, expected_paths)
74
+
75
+ def test_contains_path_base(self):
76
+ """ Test checking path containment. """
77
+
78
+ temp_dir = self._prep_temp_dir()
79
+
80
+ # [(parent, child, contains?), ...]
81
+ test_cases = [
82
+ # Containment
83
+ ('a', os.path.join('a', 'b', 'c'), True),
84
+ (os.path.join('a', 'b'), os.path.join('a', 'b', 'c'), True),
85
+ ('.', os.path.join('a', 'b', 'c'), True),
86
+ ('..', '.', True),
87
+
88
+ # Self No Containment
89
+ ('a', 'a', False),
90
+ (os.path.join('a', 'b', 'c'), os.path.join('a', 'b', 'c'), False),
91
+ ('.', '.', False),
92
+
93
+ # Trivial No Containment
94
+ ('a', 'b', False),
95
+ ('z', os.path.join('a', 'b', 'c'), False),
96
+ ('aa', os.path.join('a', 'b', 'c'), False),
97
+ ('a', os.path.join('aa', 'b', 'c'), False),
98
+
99
+ # Child Contains Parent
100
+ (os.path.join('a', 'b', 'c'), 'a', False),
101
+ (os.path.join('a', 'b', 'c'), os.path.join('a', 'b'), False),
102
+ ]
103
+
104
+ for (i, test_case) in enumerate(test_cases):
105
+ (parent, child, expected) = test_case
106
+
107
+ with self.subTest(msg = f"Case {i} ('{parent}' ⊂ '{child}'):"):
108
+ parent = os.path.join(temp_dir, parent)
109
+ child = os.path.join(temp_dir, child)
110
+
111
+ actual = edq.util.dirent.contains_path(parent, child)
112
+ self.assertEqual(expected, actual)
113
+
114
+ def test_read_write_file_bytes_base(self):
115
+ """ Test reading and writing a file as bytes. """
116
+
117
+ # [(path, write kwargs, read kwargs, write contents, expected contents, error substring), ...]
118
+ # All conent should be strings that will be encoded.
119
+ test_cases = [
120
+ # Base
121
+ (
122
+ "test.txt",
123
+ {},
124
+ {},
125
+ "test",
126
+ "test",
127
+ None,
128
+ ),
129
+
130
+ # Empty Write
131
+ (
132
+ "test.txt",
133
+ {},
134
+ {},
135
+ "",
136
+ "",
137
+ None,
138
+ ),
139
+
140
+ # None Write
141
+ (
142
+ "test.txt",
143
+ {},
144
+ {},
145
+ None,
146
+ "",
147
+ None,
148
+ ),
149
+
150
+ # Clobber
151
+ (
152
+ "a.txt",
153
+ {},
154
+ {},
155
+ "test",
156
+ "test",
157
+ None,
158
+ ),
159
+ (
160
+ "dir_1",
161
+ {},
162
+ {},
163
+ "test",
164
+ "test",
165
+ None,
166
+ ),
167
+ (
168
+ "symlink_a.txt",
169
+ {},
170
+ {},
171
+ "test",
172
+ "test",
173
+ None,
174
+ ),
175
+
176
+ # No Clobber
177
+ (
178
+ "a.txt",
179
+ {'no_clobber': True},
180
+ {},
181
+ "test",
182
+ "test",
183
+ 'already exists',
184
+ ),
185
+ (
186
+ "dir_1",
187
+ {'no_clobber': True},
188
+ {},
189
+ "test",
190
+ "test",
191
+ 'already exists',
192
+ ),
193
+ (
194
+ "symlink_a.txt",
195
+ {'no_clobber': True},
196
+ {},
197
+ "test",
198
+ "test",
199
+ 'already exists',
200
+ ),
201
+ ]
202
+
203
+ for (i, test_case) in enumerate(test_cases):
204
+ (path, write_options, read_options, write_contents, expected_contents, error_substring) = test_case
205
+
206
+ with self.subTest(msg = f"Case {i} ('{path}'):"):
207
+ temp_dir = self._prep_temp_dir()
208
+ path = os.path.join(temp_dir, path)
209
+
210
+ if (write_contents is not None):
211
+ write_contents = bytes(write_contents, edq.util.dirent.DEFAULT_ENCODING)
212
+
213
+ expected_contents = bytes(expected_contents, edq.util.dirent.DEFAULT_ENCODING)
214
+
215
+ try:
216
+ edq.util.dirent.write_file_bytes(path, write_contents, **write_options)
217
+ actual_contents = edq.util.dirent.read_file_bytes(path, **read_options)
218
+ except Exception as ex:
219
+ error_string = self.format_error_string(ex)
220
+ if (error_substring is None):
221
+ self.fail(f"Unexpected error: '{error_string}'.")
222
+
223
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
224
+
225
+ continue
226
+
227
+ if (error_substring is not None):
228
+ self.fail(f"Did not get expected error: '{error_substring}'.")
229
+
230
+ self.assertEqual(expected_contents, actual_contents)
231
+
232
+ def test_read_write_file_base(self):
233
+ """ Test reading and writing a file. """
234
+
235
+ # [(path, write kwargs, read kwargs, write contents, expected contents, error substring), ...]
236
+ test_cases = [
237
+ # Base
238
+ (
239
+ "test.txt",
240
+ {},
241
+ {},
242
+ "test",
243
+ "test",
244
+ None,
245
+ ),
246
+
247
+ # Defaults
248
+ (
249
+ "test.txt",
250
+ {},
251
+ {},
252
+ " test ",
253
+ "test",
254
+ None,
255
+ ),
256
+
257
+ # No Modifications
258
+ (
259
+ "test.txt",
260
+ {'strip': False, 'newline': False},
261
+ {'strip': False},
262
+ " test ",
263
+ " test ",
264
+ None,
265
+ ),
266
+
267
+ # No Strip
268
+ (
269
+ "test.txt",
270
+ {'strip': False, 'newline': True},
271
+ {'strip': False},
272
+ " test ",
273
+ " test \n",
274
+ None,
275
+ ),
276
+
277
+ # No Read Strip
278
+ (
279
+ "test.txt",
280
+ {},
281
+ {'strip': False},
282
+ "test",
283
+ "test\n",
284
+ None,
285
+ ),
286
+
287
+ # Empty Write
288
+ (
289
+ "test.txt",
290
+ {'newline': False},
291
+ {},
292
+ "",
293
+ "",
294
+ None,
295
+ ),
296
+
297
+ # None Write
298
+ (
299
+ "test.txt",
300
+ {'newline': False},
301
+ {},
302
+ None,
303
+ "",
304
+ None,
305
+ ),
306
+
307
+ # Clobber
308
+ (
309
+ "a.txt",
310
+ {},
311
+ {},
312
+ "test",
313
+ "test",
314
+ None,
315
+ ),
316
+ (
317
+ "dir_1",
318
+ {},
319
+ {},
320
+ "test",
321
+ "test",
322
+ None,
323
+ ),
324
+ (
325
+ "symlink_a.txt",
326
+ {},
327
+ {},
328
+ "test",
329
+ "test",
330
+ None,
331
+ ),
332
+
333
+ # No Clobber
334
+ (
335
+ "a.txt",
336
+ {'no_clobber': True},
337
+ {},
338
+ "test",
339
+ "test",
340
+ 'Destination of write already exists',
341
+ ),
342
+ (
343
+ "dir_1",
344
+ {'no_clobber': True},
345
+ {},
346
+ "test",
347
+ "test",
348
+ 'Destination of write already exists',
349
+ ),
350
+ (
351
+ "symlink_a.txt",
352
+ {'no_clobber': True},
353
+ {},
354
+ "test",
355
+ "test",
356
+ 'Destination of write already exists',
357
+ ),
358
+ ]
359
+
360
+ for (i, test_case) in enumerate(test_cases):
361
+ (path, write_options, read_options, write_contents, expected_contents, error_substring) = test_case
362
+
363
+ with self.subTest(msg = f"Case {i} ('{path}'):"):
364
+ temp_dir = self._prep_temp_dir()
365
+ path = os.path.join(temp_dir, path)
366
+
367
+ try:
368
+ edq.util.dirent.write_file(path, write_contents, **write_options)
369
+ actual_contents = edq.util.dirent.read_file(path, **read_options)
370
+ except Exception as ex:
371
+ error_string = self.format_error_string(ex)
372
+ if (error_substring is None):
373
+ self.fail(f"Unexpected error: '{error_string}'.")
374
+
375
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
376
+
377
+ continue
378
+
379
+ if (error_substring is not None):
380
+ self.fail(f"Did not get expected error: '{error_substring}'.")
381
+
382
+ self.assertEqual(expected_contents, actual_contents)
383
+
384
+ def test_copy_contents_base(self):
385
+ """ Test copying the contents of a dirent. """
386
+
387
+ # [(source, dest, no clobber?, error substring), ...]
388
+ test_cases = [
389
+ ('a.txt', 'dir_1', False, None),
390
+ ('a.txt', 'ZZZ', False, None),
391
+ ('dir_empty', 'dir_1', False, None),
392
+
393
+ ('dir_1', 'dir_1', False, 'Source and destination of contents copy cannot be the same'),
394
+ ('dir_empty', 'symlink_dir_empty', False, 'Source and destination of contents copy cannot be the same'),
395
+
396
+ ('a.txt', 'file_empty', False, 'Destination of contents copy exists and is not a dir'),
397
+ ]
398
+
399
+ for (i, test_case) in enumerate(test_cases):
400
+ (source, dest, no_clobber, error_substring) = test_case
401
+
402
+ with self.subTest(msg = f"Case {i} ('{source}' -> '{dest}'):"):
403
+ temp_dir = self._prep_temp_dir()
404
+
405
+ source = os.path.join(temp_dir, source)
406
+ dest = os.path.join(temp_dir, dest)
407
+
408
+ try:
409
+ edq.util.dirent.copy_contents(source, dest, no_clobber = no_clobber)
410
+ except Exception as ex:
411
+ error_string = self.format_error_string(ex)
412
+ if (error_substring is None):
413
+ self.fail(f"Unexpected error: '{error_string}'.")
414
+
415
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
416
+
417
+ continue
418
+
419
+ if (error_substring is not None):
420
+ self.fail(f"Did not get expected error: '{error_substring}'.")
421
+
422
+ def test_copy_base(self):
423
+ """ Test copying dirents. """
424
+
425
+ # [(source, dest, no_clobber?, error substring), ...]
426
+ test_cases = [
427
+ # File
428
+ ('a.txt', 'test.txt', False, None),
429
+ ('a.txt', 'test.txt', True, None),
430
+ ('a.txt', os.path.join('dir_1', 'test.txt'), False, None),
431
+ ('a.txt', os.path.join('dir_1', 'test.txt'), True, None),
432
+
433
+ # File - Clobber
434
+ ('a.txt', 'file_empty', False, None),
435
+ ('a.txt', os.path.join('dir_1', 'b.txt'), False, None),
436
+ ('a.txt', 'dir_1', False, None),
437
+ ('a.txt', os.path.join('dir_1', 'dir_2'), False, None),
438
+
439
+ # File - No Clobber
440
+ ('a.txt', 'file_empty', True, 'Destination of copy already exists'),
441
+ ('a.txt', os.path.join('dir_1', 'b.txt'), True, 'Destination of copy already exists'),
442
+ ('a.txt', 'dir_1', True, 'Destination of copy already exists'),
443
+ ('a.txt', os.path.join('dir_1', 'dir_2'), True, 'Destination of copy already exists'),
444
+
445
+ # Dir
446
+ ('dir_empty', 'test', False, None),
447
+ ('dir_empty', 'test', True, None),
448
+ ('dir_empty', os.path.join('dir_1', 'test'), False, None),
449
+ ('dir_empty', os.path.join('dir_1', 'test'), True, None),
450
+
451
+ # Dir - Clobber
452
+ ('dir_empty', 'file_empty', False, None),
453
+ ('dir_empty', os.path.join('dir_1', 'b.txt'), False, None),
454
+ ('dir_empty', 'dir_1', False, None),
455
+ ('dir_empty', os.path.join('dir_1', 'dir_2'), False, None),
456
+
457
+ # Dir - No Clobber
458
+ ('dir_empty', 'file_empty', True, 'Destination of copy already exists'),
459
+ ('dir_empty', os.path.join('dir_1', 'b.txt'), True, 'Destination of copy already exists'),
460
+ ('dir_empty', 'dir_1', True, 'Destination of copy already exists'),
461
+ ('dir_empty', os.path.join('dir_1', 'dir_2'), True, 'Destination of copy already exists'),
462
+
463
+ # Link
464
+ ('symlink_a.txt', 'test.txt', False, None),
465
+ ('symlink_dir_1', 'test', False, None),
466
+ ('symlink_dir_empty', 'test', False, None),
467
+ ('symlink_file_empty', 'test.txt', False, None),
468
+
469
+ # Link - Clobber
470
+ ('symlink_a.txt', 'file_empty', False, None),
471
+ ('symlink_a.txt', 'symlink_dir_1', False, None),
472
+
473
+ # Link - No Clobber
474
+ ('symlink_a.txt', 'file_empty', True, 'Destination of copy already exists'),
475
+ ('symlink_a.txt', 'symlink_dir_1', True, 'Destination of copy already exists'),
476
+
477
+ # Clobber Parent
478
+ (os.path.join('dir_1', 'b.txt'), 'dir_1', False, 'Destination of copy cannot contain the source.'),
479
+
480
+ # Same
481
+ ('a.txt', 'a.txt', False, None),
482
+ ('symlink_a.txt', 'a.txt', False, None),
483
+ ('a.txt', 'a.txt', True, None),
484
+ ('symlink_a.txt', 'a.txt', True, None),
485
+
486
+ # Missing Source
487
+ ('ZZZ', 'test.txt', False, 'Source of copy does not exist'),
488
+ ('ZZZ', 'test.txt', True, 'Source of copy does not exist'),
489
+ ]
490
+
491
+ for (i, test_case) in enumerate(test_cases):
492
+ (source, dest, no_clobber, error_substring) = test_case
493
+
494
+ with self.subTest(msg = f"Case {i} ('{source}' -> '{dest}'):"):
495
+ temp_dir = self._prep_temp_dir()
496
+
497
+ source = os.path.join(temp_dir, source)
498
+ dest = os.path.join(temp_dir, dest)
499
+
500
+ try:
501
+ edq.util.dirent.copy(source, dest, no_clobber = no_clobber)
502
+ except Exception as ex:
503
+ error_string = self.format_error_string(ex)
504
+ if (error_substring is None):
505
+ self.fail(f"Unexpected error: '{error_string}'.")
506
+
507
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
508
+
509
+ continue
510
+
511
+ if (error_substring is not None):
512
+ self.fail(f"Did not get expected error: '{error_substring}'.")
513
+
514
+ dirent_type, is_link = self._get_dirent_type(source)
515
+
516
+ checks = [
517
+ (source, dirent_type, is_link),
518
+ ]
519
+
520
+ if (not edq.util.dirent.same(source, dest)):
521
+ checks += [
522
+ (dest, dirent_type, is_link),
523
+ ]
524
+
525
+ self._check_existing_paths(temp_dir, checks)
526
+
527
+ def test_copy_special_matching_subdir_name(self):
528
+ """ Test copying a special case of copying a files into themselves with matching names. """
529
+
530
+ base_dir = edq.util.dirent.get_temp_dir()
531
+
532
+ target_dir = os.path.join(base_dir, 'already_exists')
533
+ target_file = os.path.join(target_dir, 'already_exists.txt')
534
+
535
+ edq.util.dirent.mkdir(target_dir)
536
+ edq.util.dirent.write_file(target_file, 'aaa')
537
+
538
+ try:
539
+ edq.util.dirent.copy(target_dir, target_file)
540
+ self.fail("Did not get expected error.")
541
+ except Exception as ex:
542
+ error_string = self.format_error_string(ex)
543
+ self.assertIn('Source of copy cannot contain the destination', error_string, 'Error is not as expected.')
544
+
545
+ try:
546
+ edq.util.dirent.copy(target_file, target_dir)
547
+ self.fail("Did not get expected error.")
548
+ except Exception as ex:
549
+ error_string = self.format_error_string(ex)
550
+ self.assertIn('Destination of copy cannot contain the source', error_string, 'Error is not as expected.')
551
+
552
+ def test_same_base(self):
553
+ """ Test checking for two paths pointing to the same dirent. """
554
+
555
+ temp_dir = self._prep_temp_dir()
556
+
557
+ # [(path, path, same?), ...]
558
+ test_cases = [
559
+ # Same
560
+ ('a.txt', 'a.txt', True),
561
+ ('dir_1', 'dir_1', True),
562
+ (os.path.join('dir_1', 'b.txt'), os.path.join('dir_1', 'b.txt'), True),
563
+ (os.path.join('dir_1', 'b.txt'), os.path.join('dir_1', '..', 'dir_1', 'b.txt'), True),
564
+
565
+ # Not Same
566
+ ('a.txt', 'dir_1', False),
567
+ ('a.txt', os.path.join('dir_1', 'b.txt'), False),
568
+ ('a.txt', 'file_empty', False),
569
+ ('a.txt', 'dir_empty', False),
570
+
571
+ # Not Exists
572
+ ('a.txt', 'ZZZ', False),
573
+ (os.path.join('dir_1', 'b.txt'), os.path.join('dir_1', 'ZZZ'), False),
574
+ (os.path.join('dir_1', 'b.txt'), os.path.join('ZZZ', 'b.txt'), False),
575
+
576
+ # Links
577
+ ('a.txt', 'symlink_a.txt', True),
578
+ ('a.txt', 'symlink_file_empty', False),
579
+ ('dir_1', 'symlink_dir_1', True),
580
+ ('dir_1', 'symlink_dir_empty', False),
581
+ ]
582
+
583
+ for (i, test_case) in enumerate(test_cases):
584
+ (a, b, expected) = test_case
585
+
586
+ with self.subTest(msg = f"Case {i} ('{a}' vs '{b}'):"):
587
+ a = os.path.join(temp_dir, a)
588
+ b = os.path.join(temp_dir, b)
589
+
590
+ actual = edq.util.dirent.same(a, b)
591
+ self.assertEqual(expected, actual)
592
+
593
+ def test_mkdir_base(self):
594
+ """ Test creating directories. """
595
+
596
+ temp_dir = self._prep_temp_dir()
597
+
598
+ # [(path, error substring), ...]
599
+ test_cases = [
600
+ # Base
601
+ ('new_dir_1', None),
602
+ (os.path.join('dir_1', 'new_dir_2'), None),
603
+
604
+ # Missing Parents
605
+ (os.path.join('ZZZ', 'new_dir_ZZZ'), None),
606
+ (os.path.join('ZZZ', 'YYY', 'XXX', 'new_dir_XXX'), None),
607
+
608
+ # Existing Dir
609
+ ('dir_1', None),
610
+ ('dir_empty', None),
611
+ ('symlink_dir_1', None),
612
+
613
+ # Existing Non-Dir
614
+ ('a.txt', 'Target of mkdir already exists'),
615
+ ('symlink_a.txt', 'Target of mkdir already exists'),
616
+
617
+ # Existing Non-Dir Parent
618
+ (os.path.join('dir_1', 'b.txt', 'BBB'), 'Target of mkdir contains parent'),
619
+ ]
620
+
621
+ for (i, test_case) in enumerate(test_cases):
622
+ (path, error_substring) = test_case
623
+
624
+ with self.subTest(msg = f"Case {i} ('{path}'):"):
625
+ path = os.path.join(temp_dir, path)
626
+
627
+ try:
628
+ edq.util.dirent.mkdir(path)
629
+ except Exception as ex:
630
+ error_string = self.format_error_string(ex)
631
+ if (error_substring is None):
632
+ self.fail(f"Unexpected error: '{error_string}'.")
633
+
634
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
635
+
636
+ continue
637
+
638
+ if (error_substring is not None):
639
+ self.fail(f"Did not get expected error: '{error_substring}'.")
640
+
641
+ self.assertTrue(edq.util.dirent.exists(path), 'Dir does not exist post mkdir.')
642
+
643
+ def test_get_temp_path_base(self):
644
+ """ Ensure that temp paths are not the same. """
645
+
646
+ a = edq.util.dirent.get_temp_path()
647
+ b = edq.util.dirent.get_temp_path()
648
+
649
+ self.assertNotEqual(a, b)
650
+
651
+ def test_get_temp_dir_base(self):
652
+ """ Ensure that the temp dir exists. """
653
+
654
+ path = edq.util.dirent.get_temp_dir()
655
+ self.assertTrue(edq.util.dirent.exists(path))
656
+
657
+ def test_exists_base(self):
658
+ """
659
+ Test checking for existence.
660
+
661
+ ./dir_empty and ./file_empty will be removed to check for broken links.
662
+ """
663
+
664
+ temp_dir = self._prep_temp_dir()
665
+
666
+ # Remove some dirents to break links.
667
+ edq.util.dirent.remove(os.path.join(temp_dir, 'dir_empty'))
668
+ edq.util.dirent.remove(os.path.join(temp_dir, 'file_empty'))
669
+
670
+ # [(path, exists?), ...]
671
+ test_cases = [
672
+ # File
673
+ ('a.txt', True),
674
+ (os.path.join('dir_1', 'b.txt'), True),
675
+
676
+ # Dir
677
+ ('dir_1', True),
678
+ (os.path.join('dir_1', 'dir_2'), True),
679
+
680
+ # Links
681
+ ('symlink_a.txt', True),
682
+ ('symlink_dir_1', True),
683
+ ('symlink_dir_empty', True), # Broken Link
684
+ ('symlink_file_empty', True), # Broken Link
685
+
686
+ # Not Exists
687
+ ('dir_empty', False),
688
+ ('file_empty', False),
689
+ (os.path.join('dir_1', 'ZZZ'), False),
690
+ ]
691
+
692
+ for (i, test_case) in enumerate(test_cases):
693
+ (path, expected) = test_case
694
+
695
+ with self.subTest(msg = f"Case {i} ('{path}'):"):
696
+ path = os.path.join(temp_dir, path)
697
+ actual = edq.util.dirent.exists(path)
698
+ self.assertEqual(expected, actual)
699
+
700
+ def test_move_base(self):
701
+ """
702
+ Test moving dirents.
703
+
704
+ This test will create some additional dirents:
705
+ ├── dir_1
706
+ │   └── dir_2
707
+ │   ├── a.txt
708
+ │   └── dir_empty
709
+ """
710
+
711
+ # [(source, dest, no_clobber?, error substring), ...]
712
+ # The dest can be a single string, or a tuple of (operation input, expected output).
713
+ test_cases = [
714
+ # File
715
+ ('a.txt', 'test.txt', False, None),
716
+
717
+ # Move into Dir - Explicit
718
+ ('a.txt', os.path.join('dir_1', 'a.txt'), False, None),
719
+
720
+ # Move into Dir - Implicit
721
+ ('a.txt', ('dir_1', os.path.join('dir_1', 'a.txt')), False, None),
722
+
723
+ # Move out of Dir
724
+ (os.path.join('dir_1', 'b.txt'), 'b.txt', False, None),
725
+
726
+ # Missing Parents
727
+ ('a.txt', os.path.join('dir_1', 'a', 'b', 'a.txt'), False, None),
728
+
729
+ # Same File
730
+ ('a.txt', 'a.txt', False, None),
731
+
732
+ # Clobber File with File
733
+ ('a.txt', os.path.join('dir_1', 'b.txt'), False, None),
734
+
735
+ # No Clobber File with File
736
+ ('a.txt', os.path.join('dir_1', 'b.txt'), True, 'Destination of move already exists'),
737
+
738
+ # Clobber File with File - Implicit
739
+ ('a.txt', (os.path.join('dir_1', 'dir_2'), os.path.join('dir_1', 'dir_2', 'a.txt')), False, None),
740
+
741
+ # No Clobber File with File - Implicit
742
+ ('a.txt', os.path.join('dir_1', 'dir_2'), True, 'Destination of move already exists'),
743
+
744
+ # Clobber Dir with Dir
745
+ ('dir_empty', 'dir_1', False, None),
746
+
747
+ # Clobber Dir with Dir - Implicit
748
+ ('dir_empty', (os.path.join('dir_1', 'dir_2'), os.path.join('dir_1', 'dir_2', 'dir_empty')), False, None),
749
+
750
+ # No Clobber Dir with Dir - Implicit
751
+ ('dir_empty', os.path.join('dir_1', 'dir_2'), True, 'Destination of move already exists'),
752
+ ]
753
+
754
+ for (i, test_case) in enumerate(test_cases):
755
+ (source, raw_dest, no_clobber, error_substring) = test_case
756
+
757
+ with self.subTest(msg = f"Case {i} ('{source}' -> '{raw_dest}'):"):
758
+ temp_dir = self._prep_temp_dir()
759
+
760
+ # Create the additional dirents for this test.
761
+ edq.util.dirent.copy(os.path.join(temp_dir, 'a.txt'), os.path.join(temp_dir, 'dir_1', 'dir_2', 'a.txt'))
762
+ edq.util.dirent.copy(os.path.join(temp_dir, 'dir_empty'), os.path.join(temp_dir, 'dir_1', 'dir_2', 'dir_empty'))
763
+
764
+ if (isinstance(raw_dest, tuple)):
765
+ (input_dest, expected_dest) = raw_dest
766
+ else:
767
+ input_dest = raw_dest
768
+ expected_dest = raw_dest
769
+
770
+ source = os.path.join(temp_dir, source)
771
+ input_dest = os.path.join(temp_dir, input_dest)
772
+ expected_dest = os.path.join(temp_dir, expected_dest)
773
+
774
+ try:
775
+ edq.util.dirent.move(source, input_dest, no_clobber = no_clobber)
776
+ except Exception as ex:
777
+ error_string = self.format_error_string(ex)
778
+ if (error_substring is None):
779
+ self.fail(f"Unexpected error: '{error_string}'.")
780
+
781
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
782
+
783
+ continue
784
+
785
+ if (error_substring is not None):
786
+ self.fail(f"Did not get expected error: '{error_substring}'.")
787
+
788
+ self._check_existing_paths(temp_dir, [expected_dest])
789
+
790
+ if (not edq.util.dirent.same(os.path.join(temp_dir, source), os.path.join(temp_dir, expected_dest))):
791
+ self._check_nonexisting_paths(temp_dir, [source])
792
+
793
+ def test_move_rename(self):
794
+ """ Test renaming dirents (via move()). """
795
+
796
+ temp_dir = self._prep_temp_dir()
797
+
798
+ # [(source, dest, expected error), ...]
799
+ rename_relpaths = [
800
+ # Symlink - File
801
+ ('symlink_a.txt', 'rename_symlink_a.txt', None),
802
+
803
+ # Symlink - Dir
804
+ ('symlink_dir_1', 'rename_symlink_dir_1', None),
805
+
806
+ # File in Directory
807
+ (os.path.join('dir_1', 'dir_2', 'c.txt'), os.path.join('dir_1', 'dir_2', 'rename_c.txt'), None),
808
+
809
+ # File
810
+ ('a.txt', 'rename_a.txt', None),
811
+
812
+ # Empty File
813
+ ('file_empty', 'rename_file_empty', None),
814
+
815
+ # Directory
816
+ ('dir_1', 'rename_dir_1', None),
817
+
818
+ # Empty Directory
819
+ ('dir_empty', 'rename_dir_empty', None),
820
+
821
+ # Non-Existent
822
+ ('ZZZ', 'rename_ZZZ', 'Source of move does not exist'),
823
+ ]
824
+
825
+ expected_paths = [
826
+ ('rename_a.txt', DIRENT_TYPE_FILE),
827
+ ('rename_dir_1', DIRENT_TYPE_DIR),
828
+ (os.path.join('rename_dir_1', 'b.txt'), DIRENT_TYPE_FILE),
829
+ (os.path.join('rename_dir_1', 'dir_2'), DIRENT_TYPE_DIR),
830
+ (os.path.join('rename_dir_1', 'dir_2', 'rename_c.txt'), DIRENT_TYPE_FILE),
831
+ ('rename_dir_empty', DIRENT_TYPE_DIR),
832
+ ('rename_file_empty', DIRENT_TYPE_FILE),
833
+ ('rename_symlink_a.txt', DIRENT_TYPE_BROKEN_SYMLINK, True),
834
+ ('rename_symlink_dir_1', DIRENT_TYPE_BROKEN_SYMLINK, True),
835
+ ('symlink_dir_empty', DIRENT_TYPE_BROKEN_SYMLINK, True),
836
+ ('symlink_file_empty', DIRENT_TYPE_BROKEN_SYMLINK, True),
837
+ ]
838
+
839
+ unexpected_paths = [
840
+ 'symlink_a.txt',
841
+ 'symlink_dir_1',
842
+ os.path.join('dir_1', 'dir_2', 'c.txt'),
843
+ 'a.txt',
844
+ 'file_empty',
845
+ 'dir_1',
846
+ 'dir_empty',
847
+ 'ZZZ',
848
+ 'rename_ZZZ',
849
+ ]
850
+
851
+ for (i, test_case) in enumerate(rename_relpaths):
852
+ (source, dest, error_substring) = test_case
853
+
854
+ source = os.path.join(temp_dir, source)
855
+ dest = os.path.join(temp_dir, dest)
856
+
857
+ try:
858
+ edq.util.dirent.move(source, dest)
859
+ except Exception as ex:
860
+ error_string = self.format_error_string(ex)
861
+ if (error_substring is None):
862
+ self.fail(f"Case {i}: Unexpected error: '{error_string}'.")
863
+
864
+ self.assertIn(error_substring, error_string, 'Error is not as expected.')
865
+
866
+ continue
867
+
868
+ if (error_substring is not None):
869
+ self.fail(f"Case {i}: Did not get expected error: '{error_substring}'.")
870
+
871
+ self._check_nonexisting_paths(temp_dir, unexpected_paths)
872
+ self._check_existing_paths(temp_dir, expected_paths)
873
+
874
+ def test_remove_base(self):
875
+ """ Test removing dirents. """
876
+
877
+ temp_dir = self._prep_temp_dir()
878
+
879
+ # Remove these paths in this order.
880
+ remove_relpaths = [
881
+ # Symlink - File
882
+ 'symlink_a.txt',
883
+
884
+ # Symlink - Dir
885
+ 'symlink_dir_1',
886
+
887
+ # File in Directory
888
+ os.path.join('dir_1', 'dir_2', 'c.txt'),
889
+
890
+ # File
891
+ 'a.txt',
892
+
893
+ # Empty File
894
+ 'file_empty'
895
+
896
+ # Directory
897
+ 'dir_1',
898
+
899
+ # Empty Directory
900
+ 'dir_empty',
901
+
902
+ # Non-Existent
903
+ 'ZZZ',
904
+ ]
905
+
906
+ expected_paths = [
907
+ (os.path.join('dir_1', 'dir_2'), DIRENT_TYPE_DIR),
908
+ ('file_empty', DIRENT_TYPE_FILE),
909
+ # Windows has some symlink issues, so we will not check for this file.
910
+ # ('symlink_dir_empty', DIRENT_TYPE_DIR, True),
911
+ ]
912
+
913
+ for relpath in remove_relpaths:
914
+ path = os.path.join(temp_dir, relpath)
915
+ edq.util.dirent.remove(path)
916
+
917
+ self._check_nonexisting_paths(temp_dir, remove_relpaths)
918
+ self._check_existing_paths(temp_dir, expected_paths)
919
+
920
+ def _prep_temp_dir(self):
921
+ return create_test_dir('edq_test_dirent_')
922
+
923
+ def _check_existing_paths(self, base_dir, raw_paths):
924
+ """
925
+ Ensure that specific paths exists, and fail the test if they do not.
926
+ All paths should be relative to the base dir.
927
+ Paths can be:
928
+ - A string.
929
+ - A two-item tuple (path, dirent type).
930
+ - A three-item tuple (path, dirent type, is link?).
931
+ Missing components are not defaulted, they are just not checked.
932
+ """
933
+
934
+ for raw_path in raw_paths:
935
+ relpath = ''
936
+ dirent_type = None
937
+ is_link = False
938
+
939
+ if (isinstance(raw_path, str)):
940
+ relpath = raw_path
941
+ elif (isinstance(raw_path, tuple)):
942
+ if (len(raw_path) not in [2, 3]):
943
+ raise ValueError(f"Expected exactly two or three items for path check, found {len(raw_path)} items: '{raw_path}'.")
944
+
945
+ relpath = raw_path[0]
946
+ dirent_type = raw_path[1]
947
+
948
+ if (len(raw_path) == 3):
949
+ is_link = raw_path[2]
950
+ else:
951
+ raise ValueError(f"Could not parse expected path ({type(raw_path)}): '{raw_path}'.")
952
+
953
+ path = os.path.join(base_dir, relpath)
954
+
955
+ # Check the path exists.
956
+ if (not edq.util.dirent.exists(path)):
957
+ self.fail(f"Expected path does not exist: '{relpath}'.")
958
+
959
+ # Check the link status.
960
+ if (is_link is not None):
961
+ if (is_link != os.path.islink(path)):
962
+ self.fail(f"Expected path does not have a matching link status. Expected {is_link}, but is {not is_link}: '{relpath}'.")
963
+
964
+ # Check the type of the dirent.
965
+ if (dirent_type is not None):
966
+ if (dirent_type == DIRENT_TYPE_DIR):
967
+ if (not os.path.isdir(path)):
968
+ self.fail(f"Expected path to be a directory, but it is not: '{relpath}'.")
969
+ elif (dirent_type == DIRENT_TYPE_FILE):
970
+ if (not os.path.isfile(path)):
971
+ self.fail(f"Expected path to be a file, but it is not: '{relpath}'.")
972
+ elif (dirent_type == DIRENT_TYPE_BROKEN_SYMLINK):
973
+ if ((not os.path.islink(path)) or os.path.isfile(path) or os.path.isdir(path)):
974
+ self.fail(f"Expected path to be a broken link, but it is not: '{relpath}'.")
975
+ else:
976
+ raise ValueError(f"Unknown dirent type '{dirent_type}' for path: '{relpath}'.")
977
+
978
+ def _check_nonexisting_paths(self, base_dir, raw_paths):
979
+ """
980
+ Ensure that specific paths do not exists, and fail the test if they do exist.
981
+ All paths should be relative to the base dir.
982
+ Unlike _check_existing_paths(), paths should only be strings.
983
+ """
984
+
985
+ for relpath in raw_paths:
986
+ path = os.path.join(base_dir, relpath)
987
+
988
+ if (edq.util.dirent.exists(path)):
989
+ self.fail(f"Path exists when it should not: '{relpath}'.")
990
+
991
+ def _get_dirent_type(self, path):
992
+ is_link = os.path.islink(path)
993
+ dirent_type = None
994
+
995
+ if (os.path.isdir(path)):
996
+ dirent_type = DIRENT_TYPE_DIR
997
+ elif (os.path.isfile(path)):
998
+ dirent_type = DIRENT_TYPE_FILE
999
+ elif (os.path.islink(path)):
1000
+ dirent_type = DIRENT_TYPE_BROKEN_SYMLINK
1001
+ else:
1002
+ raise ValueError(f"Unknown dirent type: '{path}'.")
1003
+
1004
+ return dirent_type, is_link