The new test library is split into two parts: The framework which resides in ext/, and the gem5 helping components in /tests/gem5. Change-Id: Ib4f3ae8d7eb96a7306335a3e739b7e8041aa99b9 Signed-off-by: Sean Wilson <spwilson2@wisc.edu> Reviewed-on: https://gem5-review.googlesource.com/4421 Reviewed-by: Giacomo Travaglini <giacomo.travaglini@arm.com> Maintainer: Jason Lowe-Power <jason@lowepower.com>
460 lines
14 KiB
Python
460 lines
14 KiB
Python
# Copyright (c) 2017 Mark D. Hill and David A. Wood
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are
|
|
# met: redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer;
|
|
# redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution;
|
|
# neither the name of the copyright holders nor the names of its
|
|
# contributors may be used to endorse or promote products derived from
|
|
# this software without specific prior written permission.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
#
|
|
# Authors: Sean Wilson
|
|
|
|
'''
|
|
Helper classes for writing tests with this test library.
|
|
'''
|
|
from collections import MutableSet, OrderedDict
|
|
|
|
import difflib
|
|
import errno
|
|
import os
|
|
import Queue
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import traceback
|
|
|
|
#TODO Tear out duplicate logic from the sandbox IOManager
|
|
def log_call(logger, command, *popenargs, **kwargs):
|
|
'''
|
|
Calls the given process and automatically logs the command and output.
|
|
|
|
If stdout or stderr are provided output will also be piped into those
|
|
streams as well.
|
|
|
|
:params stdout: Iterable of items to write to as we read from the
|
|
subprocess.
|
|
|
|
:params stderr: Iterable of items to write to as we read from the
|
|
subprocess.
|
|
'''
|
|
if isinstance(command, str):
|
|
cmdstr = command
|
|
else:
|
|
cmdstr = ' '.join(command)
|
|
|
|
logger_callback = logger.trace
|
|
logger.trace('Logging call to command: %s' % cmdstr)
|
|
|
|
stdout_redirect = kwargs.get('stdout', tuple())
|
|
stderr_redirect = kwargs.get('stderr', tuple())
|
|
|
|
if hasattr(stdout_redirect, 'write'):
|
|
stdout_redirect = (stdout_redirect,)
|
|
if hasattr(stderr_redirect, 'write'):
|
|
stderr_redirect = (stderr_redirect,)
|
|
|
|
kwargs['stdout'] = subprocess.PIPE
|
|
kwargs['stderr'] = subprocess.PIPE
|
|
p = subprocess.Popen(command, *popenargs, **kwargs)
|
|
|
|
def log_output(log_callback, pipe, redirects=tuple()):
|
|
# Read iteractively, don't allow input to fill the pipe.
|
|
for line in iter(pipe.readline, ''):
|
|
for r in redirects:
|
|
r.write(line)
|
|
log_callback(line.rstrip())
|
|
|
|
stdout_thread = threading.Thread(target=log_output,
|
|
args=(logger_callback, p.stdout, stdout_redirect))
|
|
stdout_thread.setDaemon(True)
|
|
stderr_thread = threading.Thread(target=log_output,
|
|
args=(logger_callback, p.stderr, stderr_redirect))
|
|
stderr_thread.setDaemon(True)
|
|
|
|
stdout_thread.start()
|
|
stderr_thread.start()
|
|
|
|
retval = p.wait()
|
|
stdout_thread.join()
|
|
stderr_thread.join()
|
|
# Return the return exit code of the process.
|
|
if retval != 0:
|
|
raise subprocess.CalledProcessError(retval, cmdstr)
|
|
|
|
# lru_cache stuff (Introduced in python 3.2+)
|
|
# Renamed and modified to cacheresult
|
|
class _HashedSeq(list):
|
|
'''
|
|
This class guarantees that hash() will be called no more than once per
|
|
element. This is important because the cacheresult() will hash the key
|
|
multiple times on a cache miss.
|
|
|
|
.. note:: From cpython 3.7
|
|
'''
|
|
|
|
__slots__ = 'hashvalue'
|
|
|
|
def __init__(self, tup, hash=hash):
|
|
self[:] = tup
|
|
self.hashvalue = hash(tup)
|
|
|
|
def __hash__(self):
|
|
return self.hashvalue
|
|
|
|
def _make_key(args, kwds, typed,
|
|
kwd_mark = (object(),),
|
|
fasttypes = {int, str, frozenset, type(None)},
|
|
tuple=tuple, type=type, len=len):
|
|
'''
|
|
Make a cache key from optionally typed positional and keyword arguments.
|
|
The key is constructed in a way that is flat as possible rather than as
|
|
a nested structure that would take more memory. If there is only a single
|
|
argument and its data type is known to cache its hash value, then that
|
|
argument is returned without a wrapper. This saves space and improves
|
|
lookup speed.
|
|
|
|
.. note:: From cpython 3.7
|
|
'''
|
|
key = args
|
|
if kwds:
|
|
key += kwd_mark
|
|
for item in kwds.items():
|
|
key += item
|
|
if typed:
|
|
key += tuple(type(v) for v in args)
|
|
if kwds:
|
|
key += tuple(type(v) for v in kwds.values())
|
|
elif len(key) == 1 and type(key[0]) in fasttypes:
|
|
return key[0]
|
|
return _HashedSeq(key)
|
|
|
|
|
|
def cacheresult(function, typed=False):
|
|
'''
|
|
:param typed: If typed is True, arguments of different types will be
|
|
cached separately. I.e. f(3.0) and f(3) will be treated as distinct
|
|
calls with distinct results.
|
|
|
|
.. note:: From cpython 3.7
|
|
'''
|
|
sentinel = object() # unique object used to signal cache misses
|
|
make_key = _make_key # build a key from the function arguments
|
|
cache = {}
|
|
def wrapper(*args, **kwds):
|
|
# Simple caching without ordering or size limit
|
|
key = _make_key(args, kwds, typed)
|
|
result = cache.get(key, sentinel)
|
|
if result is not sentinel:
|
|
return result
|
|
result = function(*args, **kwds)
|
|
cache[key] = result
|
|
return result
|
|
return wrapper
|
|
|
|
class OrderedSet(MutableSet):
|
|
'''
|
|
Maintain ordering of insertion in items to the set with quick iteration.
|
|
|
|
http://code.activestate.com/recipes/576694/
|
|
'''
|
|
|
|
def __init__(self, iterable=None):
|
|
self.end = end = []
|
|
end += [None, end, end] # sentinel node for doubly linked list
|
|
self.map = {} # key --> [key, prev, next]
|
|
if iterable is not None:
|
|
self |= iterable
|
|
|
|
def __len__(self):
|
|
return len(self.map)
|
|
|
|
def __contains__(self, key):
|
|
return key in self.map
|
|
|
|
def add(self, key):
|
|
if key not in self.map:
|
|
end = self.end
|
|
curr = end[1]
|
|
curr[2] = end[1] = self.map[key] = [key, curr, end]
|
|
|
|
def update(self, keys):
|
|
for key in keys:
|
|
self.add(key)
|
|
|
|
def discard(self, key):
|
|
if key in self.map:
|
|
key, prev, next = self.map.pop(key)
|
|
prev[2] = next
|
|
next[1] = prev
|
|
|
|
def __iter__(self):
|
|
end = self.end
|
|
curr = end[2]
|
|
while curr is not end:
|
|
yield curr[0]
|
|
curr = curr[2]
|
|
|
|
def __reversed__(self):
|
|
end = self.end
|
|
curr = end[1]
|
|
while curr is not end:
|
|
yield curr[0]
|
|
curr = curr[1]
|
|
|
|
def pop(self, last=True):
|
|
if not self:
|
|
raise KeyError('set is empty')
|
|
key = self.end[1][0] if last else self.end[2][0]
|
|
self.discard(key)
|
|
return key
|
|
|
|
def __repr__(self):
|
|
if not self:
|
|
return '%s()' % (self.__class__.__name__,)
|
|
return '%s(%r)' % (self.__class__.__name__, list(self))
|
|
|
|
def __eq__(self, other):
|
|
if isinstance(other, OrderedSet):
|
|
return len(self) == len(other) and list(self) == list(other)
|
|
return set(self) == set(other)
|
|
|
|
def absdirpath(path):
|
|
'''
|
|
Return the directory component of the absolute path of the given path.
|
|
'''
|
|
return os.path.dirname(os.path.abspath(path))
|
|
|
|
joinpath = os.path.join
|
|
|
|
def mkdir_p(path):
|
|
'''
|
|
Same thing as mkdir -p
|
|
|
|
https://stackoverflow.com/a/600612
|
|
'''
|
|
try:
|
|
os.makedirs(path)
|
|
except OSError as exc: # Python >2.5
|
|
if exc.errno == errno.EEXIST and os.path.isdir(path):
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
|
|
class FrozenSetException(Exception):
|
|
'''Signals one tried to set a value in a 'frozen' object.'''
|
|
pass
|
|
|
|
|
|
class AttrDict(object):
|
|
'''Object which exposes its own internal dictionary through attributes.'''
|
|
def __init__(self, dict_={}):
|
|
self.update(dict_)
|
|
|
|
def __getattr__(self, attr):
|
|
dict_ = self.__dict__
|
|
if attr in dict_:
|
|
return dict_[attr]
|
|
raise AttributeError('Could not find %s attribute' % attr)
|
|
|
|
def __setattr__(self, attr, val):
|
|
self.__dict__[attr] = val
|
|
|
|
def __iter__(self):
|
|
return iter(self.__dict__)
|
|
|
|
def __getitem__(self, item):
|
|
return self.__dict__[item]
|
|
|
|
def update(self, items):
|
|
self.__dict__.update(items)
|
|
|
|
|
|
class FrozenAttrDict(AttrDict):
|
|
'''An AttrDict whose attributes cannot be modified directly.'''
|
|
__initialized = False
|
|
def __init__(self, dict_={}):
|
|
super(FrozenAttrDict, self).__init__(dict_)
|
|
self.__initialized = True
|
|
|
|
def __setattr__(self, attr, val):
|
|
if self.__initialized:
|
|
raise FrozenSetException(
|
|
'Cannot modify an attribute in a FozenAttrDict')
|
|
else:
|
|
super(FrozenAttrDict, self).__setattr__(attr, val)
|
|
|
|
def update(self, items):
|
|
if self.__initialized:
|
|
raise FrozenSetException(
|
|
'Cannot modify an attribute in a FozenAttrDict')
|
|
else:
|
|
super(FrozenAttrDict, self).update(items)
|
|
|
|
|
|
class InstanceCollector(object):
|
|
'''
|
|
A class used to simplify collecting of Classes.
|
|
|
|
>> instance_list = collector.create()
|
|
>> # Create a bunch of classes which call collector.collect(self)
|
|
>> # instance_list contains all instances created since
|
|
>> # collector.create was called
|
|
>> collector.remove(instance_list)
|
|
'''
|
|
def __init__(self):
|
|
self.collectors = []
|
|
|
|
def create(self):
|
|
collection = []
|
|
self.collectors.append(collection)
|
|
return collection
|
|
|
|
def remove(self, collector):
|
|
self.collectors.remove(collector)
|
|
|
|
def collect(self, instance):
|
|
for col in self.collectors:
|
|
col.append(instance)
|
|
|
|
|
|
def append_dictlist(dict_, key, value):
|
|
'''
|
|
Append the `value` to a list associated with `key` in `dict_`.
|
|
If `key` doesn't exist, create a new list in the `dict_` with value in it.
|
|
'''
|
|
list_ = dict_.get(key, [])
|
|
list_.append(value)
|
|
dict_[key] = list_
|
|
|
|
|
|
class ExceptionThread(threading.Thread):
|
|
'''
|
|
Wrapper around a python :class:`Thread` which will raise an
|
|
exception on join if the child threw an unhandled exception.
|
|
'''
|
|
def __init__(self, *args, **kwargs):
|
|
threading.Thread.__init__(self, *args, **kwargs)
|
|
self._eq = Queue.Queue()
|
|
|
|
def run(self, *args, **kwargs):
|
|
try:
|
|
threading.Thread.run(self, *args, **kwargs)
|
|
self._eq.put(None)
|
|
except:
|
|
tb = traceback.format_exc()
|
|
self._eq.put(tb)
|
|
|
|
def join(self, *args, **kwargs):
|
|
threading.Thread.join(*args, **kwargs)
|
|
exception = self._eq.get()
|
|
if exception:
|
|
raise Exception(exception)
|
|
|
|
|
|
def _filter_file(fname, filters):
|
|
with open(fname, "r") as file_:
|
|
for line in file_:
|
|
for regex in filters:
|
|
if re.match(regex, line):
|
|
break
|
|
else:
|
|
yield line
|
|
|
|
|
|
def _copy_file_keep_perms(source, target):
|
|
'''Copy a file keeping the original permisions of the target.'''
|
|
st = os.stat(target)
|
|
shutil.copy2(source, target)
|
|
os.chown(target, st[stat.ST_UID], st[stat.ST_GID])
|
|
|
|
|
|
def _filter_file_inplace(fname, filters):
|
|
'''
|
|
Filter the given file writing filtered lines out to a temporary file, then
|
|
copy that tempfile back into the original file.
|
|
'''
|
|
reenter = False
|
|
(_, tfname) = tempfile.mkstemp(text=True)
|
|
with open(tfname, 'w') as tempfile_:
|
|
for line in _filter_file(fname, filters):
|
|
tempfile_.write(line)
|
|
|
|
# Now filtered output is into tempfile_
|
|
_copy_file_keep_perms(tfname, fname)
|
|
|
|
|
|
def diff_out_file(ref_file, out_file, logger, ignore_regexes=tuple()):
|
|
'''Diff two files returning the diff as a string.'''
|
|
|
|
if not os.path.exists(ref_file):
|
|
raise OSError("%s doesn't exist in reference directory"\
|
|
% ref_file)
|
|
if not os.path.exists(out_file):
|
|
raise OSError("%s doesn't exist in output directory" % out_file)
|
|
|
|
_filter_file_inplace(out_file, ignore_regexes)
|
|
_filter_file_inplace(ref_file, ignore_regexes)
|
|
|
|
#try :
|
|
(_, tfname) = tempfile.mkstemp(text=True)
|
|
with open(tfname, 'r+') as tempfile_:
|
|
try:
|
|
log_call(logger, ['diff', out_file, ref_file], stdout=tempfile_)
|
|
except OSError:
|
|
# Likely signals that diff does not exist on this system. fallback
|
|
# to difflib
|
|
with open(out_file, 'r') as outf, open(ref_file, 'r') as reff:
|
|
diff = difflib.unified_diff(iter(reff.readline, ''),
|
|
iter(outf.readline, ''),
|
|
fromfile=ref_file,
|
|
tofile=out_file)
|
|
return ''.join(diff)
|
|
except subprocess.CalledProcessError:
|
|
tempfile_.seek(0)
|
|
return ''.join(tempfile_.readlines())
|
|
else:
|
|
return None
|
|
|
|
class Timer():
|
|
def __init__(self):
|
|
self.restart()
|
|
|
|
def restart(self):
|
|
self._start = self.timestamp()
|
|
self._stop = None
|
|
|
|
def stop(self):
|
|
self._stop = self.timestamp()
|
|
return self._stop - self._start
|
|
|
|
def runtime(self):
|
|
return self._stop - self._start
|
|
|
|
def active_time(self):
|
|
return self.timestamp() - self._start
|
|
|
|
@staticmethod
|
|
def timestamp():
|
|
return time.time() |