1 # Copyright (c) 2017 Mark D. Hill and David A. Wood
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
6 # met: redistributions of source code must retain the above copyright
7 # notice, this list of conditions and the following disclaimer;
8 # redistributions in binary form must reproduce the above copyright
9 # notice, this list of conditions and the following disclaimer in the
10 # documentation and/or other materials provided with the distribution;
11 # neither the name of the copyright holders nor the names of its
12 # contributors may be used to endorse or promote products derived from
13 # this software without specific prior written permission.
15 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 # Authors: Sean Wilson
30 Helper classes for writing tests with this test library.
32 from collections
import MutableSet
45 #TODO Tear out duplicate logic from the sandbox IOManager
46 def log_call(logger
, command
, *popenargs
, **kwargs
):
48 Calls the given process and automatically logs the command and output.
50 If stdout or stderr are provided output will also be piped into those
53 :params stdout: Iterable of items to write to as we read from the
56 :params stderr: Iterable of items to write to as we read from the
59 if isinstance(command
, str):
62 cmdstr
= ' '.join(command
)
64 logger_callback
= logger
.trace
65 logger
.trace('Logging call to command: %s' % cmdstr
)
67 stdout_redirect
= kwargs
.get('stdout', tuple())
68 stderr_redirect
= kwargs
.get('stderr', tuple())
70 if hasattr(stdout_redirect
, 'write'):
71 stdout_redirect
= (stdout_redirect
,)
72 if hasattr(stderr_redirect
, 'write'):
73 stderr_redirect
= (stderr_redirect
,)
75 kwargs
['stdout'] = subprocess
.PIPE
76 kwargs
['stderr'] = subprocess
.PIPE
77 p
= subprocess
.Popen(command
, *popenargs
, **kwargs
)
79 def log_output(log_callback
, pipe
, redirects
=tuple()):
80 # Read iteractively, don't allow input to fill the pipe.
81 for line
in iter(pipe
.readline
, b
''):
82 line
= line
.decode("utf-8")
85 log_callback(line
.rstrip())
87 stdout_thread
= threading
.Thread(target
=log_output
,
88 args
=(logger_callback
, p
.stdout
, stdout_redirect
))
89 stdout_thread
.setDaemon(True)
90 stderr_thread
= threading
.Thread(target
=log_output
,
91 args
=(logger_callback
, p
.stderr
, stderr_redirect
))
92 stderr_thread
.setDaemon(True)
100 # Return the return exit code of the process.
102 raise subprocess
.CalledProcessError(retval
, cmdstr
)
104 # lru_cache stuff (Introduced in python 3.2+)
105 # Renamed and modified to cacheresult
106 class _HashedSeq(list):
108 This class guarantees that hash() will be called no more than once per
109 element. This is important because the cacheresult() will hash the key
110 multiple times on a cache miss.
112 .. note:: From cpython 3.7
115 __slots__
= 'hashvalue'
117 def __init__(self
, tup
, hash=hash):
119 self
.hashvalue
= hash(tup
)
122 return self
.hashvalue
124 def _make_key(args
, kwds
, typed
,
125 kwd_mark
= (object(),),
126 fasttypes
= {int, str, frozenset, type(None)},
127 tuple=tuple, type=type, len=len):
129 Make a cache key from optionally typed positional and keyword arguments.
130 The key is constructed in a way that is flat as possible rather than as
131 a nested structure that would take more memory. If there is only a single
132 argument and its data type is known to cache its hash value, then that
133 argument is returned without a wrapper. This saves space and improves
136 .. note:: From cpython 3.7
141 for item
in kwds
.items():
144 key
+= tuple(type(v
) for v
in args
)
146 key
+= tuple(type(v
) for v
in kwds
.values())
147 elif len(key
) == 1 and type(key
[0]) in fasttypes
:
149 return _HashedSeq(key
)
152 def cacheresult(function
, typed
=False):
154 :param typed: If typed is True, arguments of different types will be
155 cached separately. I.e. f(3.0) and f(3) will be treated as distinct
156 calls with distinct results.
158 .. note:: From cpython 3.7
160 sentinel
= object() # unique object used to signal cache misses
162 def wrapper(*args
, **kwds
):
163 # Simple caching without ordering or size limit
164 key
= _make_key(args
, kwds
, typed
)
165 result
= cache
.get(key
, sentinel
)
166 if result
is not sentinel
:
168 result
= function(*args
, **kwds
)
173 class OrderedSet(MutableSet
):
175 Maintain ordering of insertion in items to the set with quick iteration.
177 http://code.activestate.com/recipes/576694/
180 def __init__(self
, iterable
=None):
182 end
+= [None, end
, end
] # sentinel node for doubly linked list
183 self
.map = {} # key --> [key, prev, next]
184 if iterable
is not None:
190 def __contains__(self
, key
):
191 return key
in self
.map
194 if key
not in self
.map:
197 curr
[2] = end
[1] = self
.map[key
] = [key
, curr
, end
]
199 def update(self
, keys
):
203 def discard(self
, key
):
205 key
, prev
, next
= self
.map.pop(key
)
212 while curr
is not end
:
216 def __reversed__(self
):
219 while curr
is not end
:
223 def pop(self
, last
=True):
225 raise KeyError('set is empty')
226 key
= self
.end
[1][0] if last
else self
.end
[2][0]
232 return '%s()' % (self
.__class
__.__name
__,)
233 return '%s(%r)' % (self
.__class
__.__name
__, list(self
))
235 def __eq__(self
, other
):
236 if isinstance(other
, OrderedSet
):
237 return len(self
) == len(other
) and list(self
) == list(other
)
238 return set(self
) == set(other
)
240 def absdirpath(path
):
242 Return the directory component of the absolute path of the given path.
244 return os
.path
.dirname(os
.path
.abspath(path
))
246 joinpath
= os
.path
.join
250 Same thing as mkdir -p
252 https://stackoverflow.com/a/600612
256 except OSError as exc
: # Python >2.5
257 if exc
.errno
== errno
.EEXIST
and os
.path
.isdir(path
):
263 class FrozenSetException(Exception):
264 '''Signals one tried to set a value in a 'frozen' object.'''
268 class AttrDict(object):
269 '''Object which exposes its own internal dictionary through attributes.'''
270 def __init__(self
, dict_
={}):
273 def __getattr__(self
, attr
):
274 dict_
= self
.__dict
__
277 raise AttributeError('Could not find %s attribute' % attr
)
279 def __setattr__(self
, attr
, val
):
280 self
.__dict
__[attr
] = val
283 return iter(self
.__dict
__)
285 def __getitem__(self
, item
):
286 return self
.__dict
__[item
]
288 def update(self
, items
):
289 self
.__dict
__.update(items
)
292 class FrozenAttrDict(AttrDict
):
293 '''An AttrDict whose attributes cannot be modified directly.'''
294 __initialized
= False
295 def __init__(self
, dict_
={}):
296 super(FrozenAttrDict
, self
).__init
__(dict_
)
297 self
.__initialized
= True
299 def __setattr__(self
, attr
, val
):
300 if self
.__initialized
:
301 raise FrozenSetException(
302 'Cannot modify an attribute in a FozenAttrDict')
304 super(FrozenAttrDict
, self
).__setattr
__(attr
, val
)
306 def update(self
, items
):
307 if self
.__initialized
:
308 raise FrozenSetException(
309 'Cannot modify an attribute in a FozenAttrDict')
311 super(FrozenAttrDict
, self
).update(items
)
314 class InstanceCollector(object):
316 A class used to simplify collecting of Classes.
318 >> instance_list = collector.create()
319 >> # Create a bunch of classes which call collector.collect(self)
320 >> # instance_list contains all instances created since
321 >> # collector.create was called
322 >> collector.remove(instance_list)
329 self
.collectors
.append(collection
)
332 def remove(self
, collector
):
333 self
.collectors
.remove(collector
)
335 def collect(self
, instance
):
336 for col
in self
.collectors
:
340 def append_dictlist(dict_
, key
, value
):
342 Append the `value` to a list associated with `key` in `dict_`.
343 If `key` doesn't exist, create a new list in the `dict_` with value in it.
345 list_
= dict_
.get(key
, [])
349 def _filter_file(fname
, filters
):
350 with
open(fname
, "r") as file_
:
352 for regex
in filters
:
353 if re
.match(regex
, line
):
359 def _copy_file_keep_perms(source
, target
):
360 '''Copy a file keeping the original permisions of the target.'''
362 shutil
.copy2(source
, target
)
363 os
.chown(target
, st
[stat
.ST_UID
], st
[stat
.ST_GID
])
366 def _filter_file_inplace(fname
, dir, filters
):
368 Filter the given file writing filtered lines out to a temporary file, then
369 copy that tempfile back into the original file.
371 (_
, tfname
) = tempfile
.mkstemp(dir=dir, text
=True)
372 with
open(tfname
, 'w') as tempfile_
:
373 for line
in _filter_file(fname
, filters
):
374 tempfile_
.write(line
)
376 # Now filtered output is into tempfile_
377 _copy_file_keep_perms(tfname
, fname
)
380 def diff_out_file(ref_file
, out_file
, logger
, ignore_regexes
=tuple()):
381 '''Diff two files returning the diff as a string.'''
383 if not os
.path
.exists(ref_file
):
384 raise OSError("%s doesn't exist in reference directory"\
386 if not os
.path
.exists(out_file
):
387 raise OSError("%s doesn't exist in output directory" % out_file
)
389 _filter_file_inplace(out_file
, os
.path
.dirname(out_file
), ignore_regexes
)
390 _filter_file_inplace(ref_file
, os
.path
.dirname(out_file
), ignore_regexes
)
393 (_
, tfname
) = tempfile
.mkstemp(dir=os
.path
.dirname(out_file
), text
=True)
394 with
open(tfname
, 'r+') as tempfile_
:
396 log_call(logger
, ['diff', out_file
, ref_file
], stdout
=tempfile_
)
398 # Likely signals that diff does not exist on this system. fallback
400 with
open(out_file
, 'r') as outf
, open(ref_file
, 'r') as reff
:
401 diff
= difflib
.unified_diff(iter(reff
.readline
, ''),
402 iter(outf
.readline
, ''),
406 except subprocess
.CalledProcessError
:
408 return ''.join(tempfile_
.readlines())
417 self
._start
= self
.timestamp()
421 self
._stop
= self
.timestamp()
422 return self
._stop
- self
._start
425 return self
._stop
- self
._start
427 def active_time(self
):
428 return self
.timestamp() - self
._start