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