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