forked from aboutcode-org/commoncode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtestcase.py
More file actions
404 lines (337 loc) · 13.9 KB
/
testcase.py
File metadata and controls
404 lines (337 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/nexB/commoncode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
import filecmp
import json
import os
import shutil
import stat
import sys
from os import path
from collections import defaultdict
from itertools import chain
from unittest import TestCase as TestCaseClass
import saneyaml
from commoncode import fileutils
from commoncode import filetype
from commoncode.archive import extract_tar
from commoncode.archive import extract_tar_raw
from commoncode.archive import extract_tar_uni
from commoncode.archive import extract_zip
from commoncode.archive import extract_zip_raw
from commoncode.archive import tar_can_extract # NOQA
from commoncode.system import on_posix
from commoncode.system import on_windows
# a base test dir specific to a given test run
# to ensure that multiple tests run can be launched in parallel
test_run_temp_dir = None
# set to 1 to see the slow tests
timing_threshold = sys.maxsize
def to_os_native_path(path):
"""
Normalize a path to use the native OS path separator.
"""
OS_PATH_SEP = '\\' if on_windows else '/'
return (
path.replace('/', OS_PATH_SEP)
.replace(u'\\', OS_PATH_SEP)
.rstrip(OS_PATH_SEP)
)
def get_test_loc(
test_path,
test_data_dir,
debug=False,
must_exist=True,
):
"""
Given a `test_path` relative to the `test_data_dir` directory, return the
location to a test file or directory for this path. No copy is done.
Raise an IOError if `must_exist` is True and the `test_path` does not exists.
"""
if debug:
import inspect
caller = inspect.stack()[1][3]
print('\nget_test_loc,%(caller)s,"%(test_path)s","%(test_data_dir)s"' % locals())
assert test_path
assert test_data_dir
if not path.exists(test_data_dir):
raise IOError("[Errno 2] No such directory: test_data_dir not found:"
" '%(test_data_dir)s'" % locals())
tpath = to_os_native_path(test_path)
test_loc = path.abspath(path.join(test_data_dir, tpath))
if must_exist and not path.exists(test_loc):
raise IOError("[Errno 2] No such file or directory: "
"test_path not found: '%(test_loc)s'" % locals())
return test_loc
class FileDrivenTesting(object):
"""
Add support for handling test files and directories, including managing
temporary test resources and doing file-based assertions.
This can be used as a standalone object if needed.
"""
test_data_dir = None
def get_test_loc(self, test_path, copy=False, debug=False, must_exist=True):
"""
Given a `test_path` relative to the self.test_data_dir directory, return the
location to a test file or directory for this path. Copy to a temp
test location if `copy` is True.
Raise an IOError if `must_exist` is True and the `test_path` does not
exists.
"""
test_data_dir = self.test_data_dir
if debug:
import inspect
caller = inspect.stack()[1][3]
print('\nself.get_test_loc,%(caller)s,"%(test_path)s"' % locals())
test_loc = get_test_loc(
test_path,
test_data_dir,
debug=debug,
must_exist=must_exist,
)
if copy:
base_name = path.basename(test_loc)
if filetype.is_file(test_loc):
# target must be an existing dir
target_dir = self.get_temp_dir()
fileutils.copyfile(test_loc, target_dir)
test_loc = path.join(target_dir, base_name)
else:
# target must be a NON existing dir
target_dir = path.join(self.get_temp_dir(), base_name)
fileutils.copytree(test_loc, target_dir)
# cleanup of VCS that could be left over from checkouts
self.remove_vcs(target_dir)
test_loc = target_dir
return test_loc
def get_temp_file(self, extension=None, dir_name='td', file_name='tf'):
"""
Return a unique new temporary file location to a non-existing temporary
file that can safely be created without a risk of name collision.
"""
if extension is None:
extension = '.txt'
if extension and not extension.startswith('.'):
extension = '.' + extension
file_name = file_name + extension
temp_dir = self.get_temp_dir(dir_name)
location = path.join(temp_dir, file_name)
return location
def get_temp_dir(self, sub_dir_path=None):
"""
Create a unique new temporary directory location. Create directories
identified by sub_dir_path if provided in this temporary directory.
Return the location for this unique directory joined with the
`sub_dir_path` if any.
"""
# ensure that we have a new unique temp directory for each test run
global test_run_temp_dir
if not test_run_temp_dir:
import tempfile
test_tmp_root_dir = tempfile.gettempdir()
# now we add a space in the path for testing path with spaces
test_run_temp_dir = fileutils.get_temp_dir(
base_dir=test_tmp_root_dir, prefix='scancode-tk-tests -')
test_run_temp_subdir = fileutils.get_temp_dir(
base_dir=test_run_temp_dir, prefix='')
if sub_dir_path:
# create a sub directory hierarchy if requested
sub_dir_path = to_os_native_path(sub_dir_path)
test_run_temp_subdir = path.join(test_run_temp_subdir, sub_dir_path)
fileutils.create_dir(test_run_temp_subdir)
return test_run_temp_subdir
def remove_vcs(self, test_dir):
"""
Remove some version control directories and some temp editor files.
"""
vcses = ('CVS', '.svn', '.git', '.hg')
for root, dirs, files in os.walk(test_dir):
for vcs_dir in vcses:
if vcs_dir in dirs:
for vcsroot, vcsdirs, vcsfiles in os.walk(test_dir):
for vcsfile in vcsdirs + vcsfiles:
vfile = path.join(vcsroot, vcsfile)
fileutils.chmod(vfile, fileutils.RW, recurse=False)
shutil.rmtree(path.join(root, vcs_dir), False)
# editors temp file leftovers
tilde_files = [path.join(root, file_loc)
for file_loc in files if file_loc.endswith('~')]
for tf in tilde_files:
os.remove(tf)
def __extract(self, test_path, extract_func=None, verbatim=False):
"""
Given an archive file identified by test_path relative
to a test files directory, return a new temp directory where the
archive file has been extracted using extract_func.
If `verbatim` is True preserve the permissions.
"""
assert test_path and test_path != ''
test_path = to_os_native_path(test_path)
target_path = path.basename(test_path)
target_dir = self.get_temp_dir(target_path)
original_archive = self.get_test_loc(test_path)
extract_func(original_archive, target_dir, verbatim=verbatim)
return target_dir
def extract_test_zip(self, test_path, *args, **kwargs):
return self.__extract(test_path, extract_zip)
def extract_test_zip_raw(self, test_path, *args, **kwargs):
return self.__extract(test_path, extract_zip_raw)
def extract_test_tar(self, test_path, verbatim=False):
return self.__extract(test_path, extract_tar, verbatim)
def extract_test_tar_raw(self, test_path, *args, **kwargs):
return self.__extract(test_path, extract_tar_raw)
def extract_test_tar_unicode(self, test_path, *args, **kwargs):
return self.__extract(test_path, extract_tar_uni)
class FileBasedTesting(TestCaseClass, FileDrivenTesting):
pass
class dircmp(filecmp.dircmp):
"""
Compare the content of dir1 and dir2. In contrast with filecmp.dircmp,
this subclass also compares the content of files with the same path.
"""
def phase3(self):
"""
Find out differences between common files.
Ensure we are using content comparison, not os.stat-only.
"""
comp = filecmp.cmpfiles(self.left, self.right, self.common_files, shallow=False)
self.same_files, self.diff_files, self.funny_files = comp
def is_same(dir1, dir2):
"""
Compare two directory trees for structure and file content.
Return False if they differ, True is they are the same.
"""
compared = dircmp(dir1, dir2)
if (compared.left_only or compared.right_only or compared.diff_files
or compared.funny_files):
return False
for subdir in compared.common_dirs:
if not is_same(path.join(dir1, subdir),
path.join(dir2, subdir)):
return False
return True
def file_cmp(file1, file2, ignore_line_endings=False):
"""
Compare two files content.
Return False if they differ, True is they are the same.
"""
with open(file1, 'rb') as f1:
f1c = f1.read()
if ignore_line_endings:
f1c = b'\n'.join(f1c.splitlines(False))
with open(file2, 'rb') as f2:
f2c = f2.read()
if ignore_line_endings:
f2c = b'\n'.join(f2c.splitlines(False))
assert f2c == f1c
def make_non_readable(location):
"""
Make location non readable for tests purpose.
"""
if on_posix:
current_stat = stat.S_IMODE(os.lstat(location).st_mode)
os.chmod(location, current_stat & ~stat.S_IREAD)
else:
os.chmod(location, 0o555)
def make_non_writable(location):
"""
Make location non writable for tests purpose.
"""
if on_posix:
current_stat = stat.S_IMODE(os.lstat(location).st_mode)
os.chmod(location, current_stat & ~stat.S_IWRITE)
else:
make_non_readable(location)
def make_non_executable(location):
"""
Make location non executable for tests purpose.
"""
if on_posix:
current_stat = stat.S_IMODE(os.lstat(location).st_mode)
os.chmod(location, current_stat & ~stat.S_IEXEC)
def get_test_file_pairs(test_dir):
"""
Yield tuples of (data_file, test_file) from a test data `test_dir` directory.
Raise exception for orphaned/dangling files.
Each test consist of a pair of files:
- a test file.
- a data file with the same name as a test file and a '.yml' extension added.
Each test file path should be unique in the tree ignoring case.
"""
# collect files with .yml extension and files with other extensions
data_files = {}
test_files = {}
dangling_test_files = set()
dangling_data_files = set()
paths_ignoring_case = defaultdict(list)
for top, _, files in os.walk(test_dir):
for tfile in files:
if tfile.endswith('~'):
continue
file_path = path.abspath(path.join(top, tfile))
if tfile.endswith('.yml'):
data_file_path = file_path
test_file_path = file_path.replace('.yml', '')
else:
test_file_path = file_path
data_file_path = test_file_path + '.yml'
if not path.exists(test_file_path):
dangling_test_files.add(test_file_path)
if not path.exists(data_file_path):
dangling_data_files.add(data_file_path)
paths_ignoring_case[file_path.lower()].append(file_path)
data_files[test_file_path] = data_file_path
test_files[test_file_path] = test_file_path
# ensure that we haev no dangling files
if dangling_test_files or dangling_data_files:
msg = ['Dangling missing test files without a YAML data file:'] + sorted(dangling_test_files)
msg += ['Dangling missing YAML data files without a test file'] + sorted(dangling_data_files)
msg = '\n'.join(msg)
print(msg)
raise Exception(msg)
# ensure that each data file has a corresponding test file
diff = set(data_files.keys()).symmetric_difference(set(test_files.keys()))
if diff:
msg = [
'Orphaned copyright test file(s) found: '
'test file without its YAML test data file '
'or YAML test data file without its test file.'] + sorted(diff)
msg = '\n'.join(msg)
print(msg)
raise Exception(msg)
# ensure that test file paths are unique when you ignore case
# we use the file names as test method names (and we have Windows that's
# case insensitive
dupes = list(chain.from_iterable(
paths for paths in paths_ignoring_case.values() if len(paths) != 1))
if dupes:
msg = ['Non unique test/data file(s) found when ignoring case!'] + sorted(dupes)
msg = '\n'.join(msg)
print(msg)
raise Exception(msg)
for test_file in test_files:
yield test_file + '.yml', test_file
def check_against_expected_json_file(results, expected_file, regen=False):
"""
Check that the ``results`` data are the same as the data in the
``expected_file`` expected JSON data file.
If `regen` is True the expected_file will overwritten with the ``results``.
This is convenient for updating tests expectations. But use with caution.
"""
if regen:
with open(expected_file, 'w') as reg:
json.dump(results, reg, indent=2, separators=(',', ': '))
expected = results
else:
with open(expected_file) as exp:
expected = json.load(exp)
# NOTE we redump the JSON as a YAML string for easier display of
# the failures comparison/diff
if results != expected:
expected = saneyaml.dump(expected)
results = saneyaml.dump(results)
assert results == expected