misc: merge branch 'release-staging-v19.0.0.0' into develop
[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 import urllib
46 import urllib2
47
48 from testlib.fixture import Fixture
49 from testlib.config import config, constants
50 from testlib.helper import log_call, cacheresult, joinpath, absdirpath
51 import testlib.log as log
52
53
54 class VariableFixture(Fixture):
55 def __init__(self, value=None, name=None):
56 super(VariableFixture, self).__init__(name=name)
57 self.value = value
58
59
60 class TempdirFixture(Fixture):
61 def __init__(self):
62 self.path = None
63 super(TempdirFixture, self).__init__(
64 name=constants.tempdir_fixture_name)
65
66 def setup(self, testitem):
67 self.path = tempfile.mkdtemp(prefix='gem5out')
68
69 def teardown(self, testitem):
70 if self.path is not None:
71 shutil.rmtree(self.path)
72
73 def skip_cleanup(self):
74 # Set path to none so it's not deleted
75 self.path = None
76
77 class UniqueFixture(Fixture):
78 '''
79 Base class for fixtures that generate a target in the
80 filesystem. If the same fixture is used by more than one
81 test/suite, rather than creating a copy of the fixture, it returns
82 the same object and makes sure that setup is only executed
83 once. Devired classses should override the _init and _setup
84 functions.
85
86 :param target: The absolute path of the target in the filesystem.
87
88 '''
89 fixtures = {}
90
91 def __new__(cls, target):
92 if target in cls.fixtures:
93 obj = cls.fixtures[target]
94 else:
95 obj = super(UniqueFixture, cls).__new__(cls)
96 obj.lock = threading.Lock()
97 obj.target = target
98 cls.fixtures[target] = obj
99 return obj
100
101 def __init__(self, *args, **kwargs):
102 with self.lock:
103 if hasattr(self, '_init_done'):
104 return
105 super(UniqueFixture, self).__init__(self, **kwargs)
106 self._init(*args, **kwargs)
107 self._init_done = True
108
109 def setup(self, testitem):
110 with self.lock:
111 if hasattr(self, '_setup_done'):
112 return
113 self._setup_done = True
114 self._setup(testitem)
115
116
117 class SConsFixture(UniqueFixture):
118 '''
119 Fixture will wait until all SCons targets are collected and tests are
120 about to be ran, then will invocate a single instance of SCons for all
121 targets.
122
123 :param directory: The directory which scons will -C (cd) into before
124 executing. If None is provided, will choose the config base_dir.
125 '''
126
127 def __new__(cls, target):
128 obj = super(SConsFixture, cls).__new__(cls, target)
129 return obj
130
131 def _setup(self, testitem):
132 if config.skip_build:
133 return
134
135 command = [
136 'scons', '-C', self.directory,
137 '-j', str(config.threads),
138 '--ignore-style'
139 ]
140
141 if not self.targets:
142 log.test_log.warn(
143 'No SCons targets specified, this will'
144 ' build the default all target.\n'
145 'This is likely unintended, and you'
146 ' may wish to kill testlib and reconfigure.')
147 else:
148 log.test_log.message(
149 'Building the following targets.'
150 ' This may take a while.')
151 log.test_log.message('%s' % (', '.join(self.targets)))
152 log.test_log.message(
153 "You may want to run with only a single ISA"
154 "(--isa=), use --skip-build, or use 'rerun'.")
155
156 command.extend(self.targets)
157 if self.options:
158 command.extend(self.options)
159 log_call(log.test_log, command, stderr=sys.stderr)
160
161 class Gem5Fixture(SConsFixture):
162 def __new__(cls, isa, variant, protocol=None):
163 target_dir = joinpath(config.build_dir, isa.upper())
164 if protocol:
165 target_dir += '_' + protocol
166 target = joinpath(target_dir, 'gem5.%s' % variant)
167 obj = super(Gem5Fixture, cls).__new__(cls, target)
168 return obj
169
170 def _init(self, isa, variant, protocol=None):
171 self.name = constants.gem5_binary_fixture_name
172
173 self.targets = [self.target]
174 self.path = self.target
175 self.directory = config.base_dir
176
177 self.options = []
178 if protocol:
179 self.options = [ '--default=' + isa.upper(),
180 'PROTOCOL=' + protocol ]
181 self.set_global()
182
183 class MakeFixture(Fixture):
184 def __init__(self, directory, *args, **kwargs):
185 name = 'make -C %s' % directory
186 super(MakeFixture, self).__init__(build_once=True, lazy_init=False,
187 name=name,
188 *args, **kwargs)
189 self.targets = []
190 self.directory = directory
191
192 def setup(self):
193 super(MakeFixture, self).setup()
194 targets = set(self.required_by)
195 command = ['make', '-C', self.directory]
196 command.extend([target.target for target in targets])
197 log_call(command)
198
199
200 class MakeTarget(Fixture):
201 def __init__(self, target, make_fixture=None, *args, **kwargs):
202 '''
203 :param make_fixture: The make invocation we will be attached to.
204 Since we don't have a single global instance of make in gem5 like we do
205 scons we need to know what invocation to attach to. If none given,
206 creates its own.
207 '''
208 super(MakeTarget, self).__init__(name=target, *args, **kwargs)
209 self.target = self.name
210
211 if make_fixture is None:
212 make_fixture = MakeFixture(
213 absdirpath(target),
214 lazy_init=True,
215 build_once=False)
216
217 self.make_fixture = make_fixture
218
219 # Add our self to the required targets of the main MakeFixture
220 self.require(self.make_fixture)
221
222 def setup(self, testitem):
223 super(MakeTarget, self).setup()
224 self.make_fixture.setup()
225 return self
226
227 class TestProgram(MakeTarget):
228 def __init__(self, program, isa, os, recompile=False):
229 make_dir = joinpath('test-progs', program)
230 make_fixture = MakeFixture(make_dir)
231 target = joinpath('bin', isa, os, program)
232 super(TestProgram, self).__init__(target, make_fixture)
233 self.path = joinpath(make_dir, target)
234 self.recompile = recompile
235
236 def setup(self, testitem):
237 # Check if the program exists if it does then only compile if
238 # recompile was given.
239 if self.recompile:
240 super(MakeTarget, self).setup()
241 elif not os.path.exists(self.path):
242 super(MakeTarget, self).setup()
243
244 class DownloadedProgram(UniqueFixture):
245 """ Like TestProgram, but checks the version in the gem5 binary repository
246 and downloads an updated version if it is needed.
247 """
248
249 def __new__(cls, url, path, filename):
250 target = joinpath(path, filename)
251 return super(DownloadedProgram, cls).__new__(cls, target)
252
253 def _init(self, url, path, filename, **kwargs):
254 """
255 url: string
256 The url of the archive
257 path: string
258 The absolute path of the directory containing the archive
259 filename: string
260 The name of the archive
261 """
262
263 self.url = url
264 self.path = path
265 self.filename = joinpath(path, filename)
266 self.name = "Downloaded:" + self.filename
267
268 def _download(self):
269 import errno
270 log.test_log.debug("Downloading " + self.url + " to " + self.path)
271 if not os.path.exists(self.path):
272 try:
273 os.makedirs(self.path)
274 except OSError as e:
275 if e.errno != errno.EEXIST:
276 raise
277 urllib.urlretrieve(self.url, self.filename)
278
279 def _getremotetime(self):
280 import datetime, time
281 import _strptime # Needed for python threading bug
282
283 u = urllib2.urlopen(self.url, timeout=10)
284 return time.mktime(datetime.datetime.strptime( \
285 u.info().getheaders("Last-Modified")[0],
286 "%a, %d %b %Y %X GMT").timetuple())
287
288 def _setup(self, testitem):
289 # Check to see if there is a file downloaded
290 if not os.path.exists(self.filename):
291 self._download()
292 else:
293 try:
294 t = self._getremotetime()
295 except (urllib2.URLError, socket.timeout):
296 # Problem checking the server, use the old files.
297 log.test_log.debug("Could not contact server. Binaries may be old.")
298 return
299 # If the server version is more recent, download it
300 if t > os.path.getmtime(self.filename):
301 self._download()
302
303 class DownloadedArchive(DownloadedProgram):
304 """ Like TestProgram, but checks the version in the gem5 binary repository
305 and downloads an updated version if it is needed.
306 """
307
308 def _extract(self):
309 import tarfile
310 with tarfile.open(self.filename) as tf:
311 tf.extractall(self.path)
312
313 def _setup(self, testitem):
314 # Check to see if there is a file downloaded
315 if not os.path.exists(self.filename):
316 self._download()
317 self._extract()
318 else:
319 try:
320 t = self._getremotetime()
321 except (urllib2.URLError, socket.timeout):
322 # Problem checking the server, use the old files.
323 log.test_log.debug("Could not contact server. "
324 "Binaries may be old.")
325 return
326 # If the server version is more recent, download it
327 if t > os.path.getmtime(self.filename):
328 self._download()
329 self._extract()