tests,python: Upgrading testlib to function with Python2
[gem5.git] / tests / gem5 / fixture.py
1 # Copyright (c) 2019 ARM Limited
2 # All rights reserved
3 #
4 # The license below extends only to copyright in the software and shall
5 # not be construed as granting a license to any other intellectual
6 # property including but not limited to intellectual property relating
7 # to a hardware implementation of the functionality of the software
8 # licensed hereunder. You may use the software subject to the license
9 # terms below provided that you ensure that this notice is replicated
10 # unmodified and in its entirety in all distributions of the software,
11 # modified or unmodified, in source code or in binary form.
12 #
13 # Copyright (c) 2017 Mark D. Hill and David A. Wood
14 # All rights reserved.
15 #
16 # Redistribution and use in source and binary forms, with or without
17 # modification, are permitted provided that the following conditions are
18 # met: redistributions of source code must retain the above copyright
19 # notice, this list of conditions and the following disclaimer;
20 # redistributions in binary form must reproduce the above copyright
21 # notice, this list of conditions and the following disclaimer in the
22 # documentation and/or other materials provided with the distribution;
23 # neither the name of the copyright holders nor the names of its
24 # contributors may be used to endorse or promote products derived from
25 # this software without specific prior written permission.
26 #
27 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
28 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
29 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
30 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
31 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
32 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
33 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
34 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
35 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
36 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
39 import os
40 import tempfile
41 import shutil
42 import sys
43 import socket
44 import threading
45
46 from six.moves import urllib
47
48 from testlib.fixture import Fixture
49 from testlib.configuration import config, constants
50 from testlib.helper import log_call, cacheresult, joinpath, absdirpath
51 import testlib.log as log
52 from testlib.state import Result
53
54
55 class VariableFixture(Fixture):
56 def __init__(self, value=None, name=None):
57 super(VariableFixture, self).__init__(name=name)
58 self.value = value
59
60
61 class TempdirFixture(Fixture):
62 def __init__(self):
63 self.path = None
64 super(TempdirFixture, self).__init__(
65 name=constants.tempdir_fixture_name)
66
67 def setup(self, testitem):
68 self.path = tempfile.mkdtemp(prefix='gem5out')
69
70 def teardown(self, testitem):
71 if testitem.result == Result.Passed:
72 shutil.rmtree(self.path)
73
74 class UniqueFixture(Fixture):
75 '''
76 Base class for fixtures that generate a target in the
77 filesystem. If the same fixture is used by more than one
78 test/suite, rather than creating a copy of the fixture, it returns
79 the same object and makes sure that setup is only executed
80 once. Devired classses should override the _init and _setup
81 functions.
82
83 :param target: The absolute path of the target in the filesystem.
84
85 '''
86 fixtures = {}
87
88 def __new__(cls, target):
89 if target in cls.fixtures:
90 obj = cls.fixtures[target]
91 else:
92 obj = super(UniqueFixture, cls).__new__(cls)
93 obj.lock = threading.Lock()
94 obj.target = target
95 cls.fixtures[target] = obj
96 return obj
97
98 def __init__(self, *args, **kwargs):
99 with self.lock:
100 if hasattr(self, '_init_done'):
101 return
102 super(UniqueFixture, self).__init__(self, **kwargs)
103 self._init(*args, **kwargs)
104 self._init_done = True
105
106 def setup(self, testitem):
107 with self.lock:
108 if hasattr(self, '_setup_done'):
109 return
110 self._setup_done = True
111 self._setup(testitem)
112
113
114 class SConsFixture(UniqueFixture):
115 '''
116 Fixture will wait until all SCons targets are collected and tests are
117 about to be ran, then will invocate a single instance of SCons for all
118 targets.
119
120 :param directory: The directory which scons will -C (cd) into before
121 executing. If None is provided, will choose the config base_dir.
122 '''
123
124 def __new__(cls, target):
125 obj = super(SConsFixture, cls).__new__(cls, target)
126 return obj
127
128 def _setup(self, testitem):
129 if config.skip_build:
130 return
131
132 command = [
133 'scons', '-C', self.directory,
134 '-j', str(config.threads),
135 '--ignore-style'
136 ]
137
138 if not self.targets:
139 log.test_log.warn(
140 'No SCons targets specified, this will'
141 ' build the default all target.\n'
142 'This is likely unintended, and you'
143 ' may wish to kill testlib and reconfigure.')
144 else:
145 log.test_log.message(
146 'Building the following targets.'
147 ' This may take a while.')
148 log.test_log.message('%s' % (', '.join(self.targets)))
149 log.test_log.message(
150 "You may want to run with only a single ISA"
151 "(--isa=), use --skip-build, or use 'rerun'.")
152
153 command.extend(self.targets)
154 if self.options:
155 command.extend(self.options)
156 log_call(log.test_log, command, stderr=sys.stderr)
157
158 class Gem5Fixture(SConsFixture):
159 def __new__(cls, isa, variant, protocol=None):
160 target_dir = joinpath(config.build_dir, isa.upper())
161 if protocol:
162 target_dir += '_' + protocol
163 target = joinpath(target_dir, 'gem5.%s' % variant)
164 obj = super(Gem5Fixture, cls).__new__(cls, target)
165 return obj
166
167 def _init(self, isa, variant, protocol=None):
168 self.name = constants.gem5_binary_fixture_name
169
170 self.targets = [self.target]
171 self.path = self.target
172 self.directory = config.base_dir
173
174 self.options = []
175 if protocol:
176 self.options = [ '--default=' + isa.upper(),
177 'PROTOCOL=' + protocol ]
178 self.set_global()
179
180 class MakeFixture(Fixture):
181 def __init__(self, directory, *args, **kwargs):
182 name = 'make -C %s' % directory
183 super(MakeFixture, self).__init__(build_once=True, lazy_init=False,
184 name=name,
185 *args, **kwargs)
186 self.targets = []
187 self.directory = directory
188
189 def setup(self):
190 super(MakeFixture, self).setup()
191 targets = set(self.required_by)
192 command = ['make', '-C', self.directory]
193 command.extend([target.target for target in targets])
194 log_call(command)
195
196
197 class MakeTarget(Fixture):
198 def __init__(self, target, make_fixture=None, *args, **kwargs):
199 '''
200 :param make_fixture: The make invocation we will be attached to.
201 Since we don't have a single global instance of make in gem5 like we do
202 scons we need to know what invocation to attach to. If none given,
203 creates its own.
204 '''
205 super(MakeTarget, self).__init__(name=target, *args, **kwargs)
206 self.target = self.name
207
208 if make_fixture is None:
209 make_fixture = MakeFixture(
210 absdirpath(target),
211 lazy_init=True,
212 build_once=False)
213
214 self.make_fixture = make_fixture
215
216 # Add our self to the required targets of the main MakeFixture
217 self.require(self.make_fixture)
218
219 def setup(self, testitem):
220 super(MakeTarget, self).setup()
221 self.make_fixture.setup()
222 return self
223
224 class TestProgram(MakeTarget):
225 def __init__(self, program, isa, os, recompile=False):
226 make_dir = joinpath('test-progs', program)
227 make_fixture = MakeFixture(make_dir)
228 target = joinpath('bin', isa, os, program)
229 super(TestProgram, self).__init__(target, make_fixture)
230 self.path = joinpath(make_dir, target)
231 self.recompile = recompile
232
233 def setup(self, testitem):
234 # Check if the program exists if it does then only compile if
235 # recompile was given.
236 if self.recompile:
237 super(MakeTarget, self).setup()
238 elif not os.path.exists(self.path):
239 super(MakeTarget, self).setup()
240
241 class DownloadedProgram(UniqueFixture):
242 """ Like TestProgram, but checks the version in the gem5 binary repository
243 and downloads an updated version if it is needed.
244 """
245
246 def __new__(cls, url, path, filename):
247 target = joinpath(path, filename)
248 return super(DownloadedProgram, cls).__new__(cls, target)
249
250 def _init(self, url, path, filename, **kwargs):
251 """
252 url: string
253 The url of the archive
254 path: string
255 The absolute path of the directory containing the archive
256 filename: string
257 The name of the archive
258 """
259
260 self.url = url
261 self.path = path
262 self.filename = joinpath(path, filename)
263 self.name = "Downloaded:" + self.filename
264
265 def _download(self):
266 import errno
267 log.test_log.debug("Downloading " + self.url + " to " + self.path)
268 if not os.path.exists(self.path):
269 try:
270 os.makedirs(self.path)
271 except OSError as e:
272 if e.errno != errno.EEXIST:
273 raise
274 urllib.request.urlretrieve(self.url, self.filename)
275
276 def _getremotetime(self):
277 import datetime, time
278 import _strptime # Needed for python threading bug
279
280 u = urllib.request.urlopen(self.url, timeout=10)
281
282 return time.mktime(datetime.datetime.strptime( \
283 u.info()["Last-Modified"],
284 "%a, %d %b %Y %X GMT").timetuple())
285
286 def _setup(self, testitem):
287 # Check to see if there is a file downloaded
288 if not os.path.exists(self.filename):
289 self._download()
290 else:
291 try:
292 t = self._getremotetime()
293 except (urllib.error.URLError, socket.timeout):
294 # Problem checking the server, use the old files.
295 log.test_log.debug("Could not contact server. Binaries may be old.")
296 return
297 # If the server version is more recent, download it
298 if t > os.path.getmtime(self.filename):
299 self._download()
300
301 class DownloadedArchive(DownloadedProgram):
302 """ Like TestProgram, but checks the version in the gem5 binary repository
303 and downloads an updated version if it is needed.
304 """
305
306 def _extract(self):
307 import tarfile
308 with tarfile.open(self.filename) as tf:
309 tf.extractall(self.path)
310
311 def _setup(self, testitem):
312 # Check to see if there is a file downloaded
313 if not os.path.exists(self.filename):
314 self._download()
315 self._extract()
316 else:
317 try:
318 t = self._getremotetime()
319 except (urllib.error.URLError, socket.timeout):
320 # Problem checking the server, use the old files.
321 log.test_log.debug("Could not contact server. "
322 "Binaries may be old.")
323 return
324 # If the server version is more recent, download it
325 if t > os.path.getmtime(self.filename):
326 self._download()
327 self._extract()