]>
Commit | Line | Data |
---|---|---|
77a8e24c AS |
1 | #!/usr/bin/env python3 |
2 | # group: rw quick | |
96420a30 | 3 | # Exercise QEMU generated ACPI/SMBIOS tables using biosbits, |
77a8e24c AS |
4 | # https://biosbits.org/ |
5 | # | |
6 | # This program is free software; you can redistribute it and/or modify | |
7 | # it under the terms of the GNU General Public License as published by | |
8 | # the Free Software Foundation; either version 2 of the License, or | |
9 | # (at your option) any later version. | |
10 | # | |
11 | # This program is distributed in the hope that it will be useful, | |
12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
14 | # GNU General Public License for more details. | |
15 | # | |
16 | # You should have received a copy of the GNU General Public License | |
17 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
18 | # | |
19 | # | |
20 | # Author: | |
94cd94f1 | 21 | # Ani Sinha <anisinha@redhat.com> |
77a8e24c AS |
22 | |
23 | # pylint: disable=invalid-name | |
24 | # pylint: disable=consider-using-f-string | |
25 | ||
26 | """ | |
27 | This is QEMU ACPI/SMBIOS avocado tests using biosbits. | |
28 | Biosbits is available originally at https://biosbits.org/. | |
29 | This test uses a fork of the upstream bits and has numerous fixes | |
30 | including an upgraded acpica. The fork is located here: | |
31 | https://gitlab.com/qemu-project/biosbits-bits . | |
32 | """ | |
33 | ||
34 | import logging | |
35 | import os | |
36 | import platform | |
37 | import re | |
38 | import shutil | |
39 | import subprocess | |
40 | import tarfile | |
41 | import tempfile | |
42 | import time | |
43 | import zipfile | |
44 | from typing import ( | |
45 | List, | |
46 | Optional, | |
47 | Sequence, | |
48 | ) | |
49 | from qemu.machine import QEMUMachine | |
50 | from avocado import skipIf | |
94cd94f1 | 51 | from avocado.utils import datadrainer as drainer |
77a8e24c AS |
52 | from avocado_qemu import QemuBaseTest |
53 | ||
ffa175f2 | 54 | deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box. |
77a8e24c AS |
55 | supported_platforms = ['x86_64'] # supported test platforms. |
56 | ||
7ef4c41e AS |
57 | # default timeout of 120 secs is sometimes not enough for bits test. |
58 | BITS_TIMEOUT = 200 | |
77a8e24c AS |
59 | |
60 | def which(tool): | |
61 | """ looks up the full path for @tool, returns None if not found | |
62 | or if @tool does not have executable permissions. | |
63 | """ | |
64 | paths=os.getenv('PATH') | |
65 | for p in paths.split(os.path.pathsep): | |
66 | p = os.path.join(p, tool) | |
67 | if os.path.exists(p) and os.access(p, os.X_OK): | |
68 | return p | |
69 | return None | |
70 | ||
71 | def missing_deps(): | |
72 | """ returns True if any of the test dependent tools are absent. | |
73 | """ | |
74 | for dep in deps: | |
75 | if which(dep) is None: | |
76 | return True | |
77 | return False | |
78 | ||
79 | def supported_platform(): | |
80 | """ checks if the test is running on a supported platform. | |
81 | """ | |
82 | return platform.machine() in supported_platforms | |
83 | ||
84 | class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods | |
85 | """ | |
86 | A QEMU VM, with isa-debugcon enabled and bits iso passed | |
87 | using -cdrom to QEMU commandline. | |
88 | ||
89 | """ | |
90 | def __init__(self, | |
91 | binary: str, | |
92 | args: Sequence[str] = (), | |
93 | wrapper: Sequence[str] = (), | |
94 | name: Optional[str] = None, | |
95 | base_temp_dir: str = "/var/tmp", | |
96 | debugcon_log: str = "debugcon-log.txt", | |
97 | debugcon_addr: str = "0x403", | |
77a8e24c AS |
98 | qmp_timer: Optional[float] = None): |
99 | # pylint: disable=too-many-arguments | |
100 | ||
101 | if name is None: | |
102 | name = "qemu-bits-%d" % os.getpid() | |
77a8e24c AS |
103 | super().__init__(binary, args, wrapper=wrapper, name=name, |
104 | base_temp_dir=base_temp_dir, | |
46d4747a | 105 | qmp_timer=qmp_timer) |
77a8e24c AS |
106 | self.debugcon_log = debugcon_log |
107 | self.debugcon_addr = debugcon_addr | |
108 | self.base_temp_dir = base_temp_dir | |
109 | ||
110 | @property | |
111 | def _base_args(self) -> List[str]: | |
112 | args = super()._base_args | |
113 | args.extend([ | |
114 | '-chardev', | |
115 | 'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir, | |
116 | self.debugcon_log), | |
117 | '-device', | |
118 | 'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr, | |
119 | ]) | |
120 | return args | |
121 | ||
122 | def base_args(self): | |
123 | """return the base argument to QEMU binary""" | |
124 | return self._base_args | |
125 | ||
1afae3b8 AS |
126 | @skipIf(not supported_platform() or missing_deps(), |
127 | 'unsupported platform or dependencies (%s) not installed' \ | |
128 | % ','.join(deps)) | |
77a8e24c AS |
129 | class AcpiBitsTest(QemuBaseTest): #pylint: disable=too-many-instance-attributes |
130 | """ | |
131 | ACPI and SMBIOS tests using biosbits. | |
132 | ||
133 | :avocado: tags=arch:x86_64 | |
134 | :avocado: tags=acpi | |
135 | ||
136 | """ | |
1b7a07c4 | 137 | # in slower systems the test can take as long as 3 minutes to complete. |
7ef4c41e | 138 | timeout = BITS_TIMEOUT |
1b7a07c4 | 139 | |
77a8e24c AS |
140 | def __init__(self, *args, **kwargs): |
141 | super().__init__(*args, **kwargs) | |
142 | self._vm = None | |
143 | self._workDir = None | |
144 | self._baseDir = None | |
145 | ||
146 | # following are some standard configuration constants | |
94cd94f1 AS |
147 | self._bitsInternalVer = 2020 # gitlab CI does shallow clones of depth 20 |
148 | self._bitsCommitHash = 'c7920d2b' # commit hash must match | |
77a8e24c | 149 | # the artifact tag below |
94cd94f1 | 150 | self._bitsTag = "qemu-bits-10262023" # this is the latest bits |
77a8e24c | 151 | # release as of today. |
94cd94f1 | 152 | self._bitsArtSHA1Hash = 'b22cdfcfc7453875297d06d626f5474ee36a343f' |
77a8e24c AS |
153 | self._bitsArtURL = ("https://gitlab.com/qemu-project/" |
154 | "biosbits-bits/-/jobs/artifacts/%s/" | |
155 | "download?job=qemu-bits-build" %self._bitsTag) | |
156 | self._debugcon_addr = '0x403' | |
157 | self._debugcon_log = 'debugcon-log.txt' | |
158 | logging.basicConfig(level=logging.INFO) | |
159 | self.logger = logging.getLogger('acpi-bits') | |
160 | ||
161 | def _print_log(self, log): | |
162 | self.logger.info('\nlogs from biosbits follows:') | |
163 | self.logger.info('==========================================\n') | |
164 | self.logger.info(log) | |
165 | self.logger.info('==========================================\n') | |
166 | ||
167 | def copy_bits_config(self): | |
168 | """ copies the bios bits config file into bits. | |
169 | """ | |
170 | config_file = 'bits-cfg.txt' | |
171 | bits_config_dir = os.path.join(self._baseDir, 'acpi-bits', | |
172 | 'bits-config') | |
173 | target_config_dir = os.path.join(self._workDir, | |
174 | 'bits-%d' %self._bitsInternalVer, | |
175 | 'boot') | |
176 | self.assertTrue(os.path.exists(bits_config_dir)) | |
177 | self.assertTrue(os.path.exists(target_config_dir)) | |
178 | self.assertTrue(os.access(os.path.join(bits_config_dir, | |
179 | config_file), os.R_OK)) | |
180 | shutil.copy2(os.path.join(bits_config_dir, config_file), | |
181 | target_config_dir) | |
182 | self.logger.info('copied config file %s to %s', | |
183 | config_file, target_config_dir) | |
184 | ||
185 | def copy_test_scripts(self): | |
186 | """copies the python test scripts into bits. """ | |
187 | ||
188 | bits_test_dir = os.path.join(self._baseDir, 'acpi-bits', | |
189 | 'bits-tests') | |
190 | target_test_dir = os.path.join(self._workDir, | |
191 | 'bits-%d' %self._bitsInternalVer, | |
192 | 'boot', 'python') | |
193 | ||
194 | self.assertTrue(os.path.exists(bits_test_dir)) | |
195 | self.assertTrue(os.path.exists(target_test_dir)) | |
196 | ||
197 | for filename in os.listdir(bits_test_dir): | |
198 | if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ | |
199 | filename.endswith('.py2'): | |
200 | # all test scripts are named with extension .py2 so that | |
201 | # avocado does not try to load them. These scripts are | |
202 | # written for python 2.7 not python 3 and hence if avocado | |
203 | # loaded them, it would complain about python 3 specific | |
204 | # syntaxes. | |
205 | newfilename = os.path.splitext(filename)[0] + '.py' | |
206 | shutil.copy2(os.path.join(bits_test_dir, filename), | |
207 | os.path.join(target_test_dir, newfilename)) | |
208 | self.logger.info('copied test file %s to %s', | |
209 | filename, target_test_dir) | |
210 | ||
211 | # now remove the pyc test file if it exists, otherwise the | |
212 | # changes in the python test script won't be executed. | |
213 | testfile_pyc = os.path.splitext(filename)[0] + '.pyc' | |
214 | if os.access(os.path.join(target_test_dir, testfile_pyc), | |
215 | os.F_OK): | |
216 | os.remove(os.path.join(target_test_dir, testfile_pyc)) | |
217 | self.logger.info('removed compiled file %s', | |
218 | os.path.join(target_test_dir, | |
219 | testfile_pyc)) | |
220 | ||
221 | def fix_mkrescue(self, mkrescue): | |
222 | """ grub-mkrescue is a bash script with two variables, 'prefix' and | |
223 | 'libdir'. They must be pointed to the right location so that the | |
224 | iso can be generated appropriately. We point the two variables to | |
225 | the directory where we have extracted our pre-built bits grub | |
226 | tarball. | |
227 | """ | |
228 | grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi') | |
229 | grub_i386_mods = os.path.join(self._workDir, 'grub-inst') | |
230 | ||
231 | self.assertTrue(os.path.exists(grub_x86_64_mods)) | |
232 | self.assertTrue(os.path.exists(grub_i386_mods)) | |
233 | ||
234 | new_script = "" | |
235 | with open(mkrescue, 'r', encoding='utf-8') as filehandle: | |
236 | orig_script = filehandle.read() | |
237 | new_script = re.sub('(^prefix=)(.*)', | |
238 | r'\1"%s"' %grub_x86_64_mods, | |
239 | orig_script, flags=re.M) | |
240 | new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, | |
241 | new_script, flags=re.M) | |
242 | ||
243 | with open(mkrescue, 'w', encoding='utf-8') as filehandle: | |
244 | filehandle.write(new_script) | |
245 | ||
246 | def generate_bits_iso(self): | |
247 | """ Uses grub-mkrescue to generate a fresh bits iso with the python | |
248 | test scripts | |
249 | """ | |
250 | bits_dir = os.path.join(self._workDir, | |
251 | 'bits-%d' %self._bitsInternalVer) | |
252 | iso_file = os.path.join(self._workDir, | |
253 | 'bits-%d.iso' %self._bitsInternalVer) | |
254 | mkrescue_script = os.path.join(self._workDir, | |
255 | 'grub-inst-x86_64-efi', 'bin', | |
256 | 'grub-mkrescue') | |
257 | ||
258 | self.assertTrue(os.access(mkrescue_script, | |
259 | os.R_OK | os.W_OK | os.X_OK)) | |
260 | ||
261 | self.fix_mkrescue(mkrescue_script) | |
262 | ||
263 | self.logger.info('using grub-mkrescue for generating biosbits iso ...') | |
264 | ||
265 | try: | |
04e5bd44 | 266 | if os.getenv('V') or os.getenv('BITS_DEBUG'): |
77a8e24c AS |
267 | subprocess.check_call([mkrescue_script, '-o', iso_file, |
268 | bits_dir], stderr=subprocess.STDOUT) | |
269 | else: | |
270 | subprocess.check_call([mkrescue_script, '-o', | |
271 | iso_file, bits_dir], | |
272 | stderr=subprocess.DEVNULL, | |
273 | stdout=subprocess.DEVNULL) | |
274 | except Exception as e: # pylint: disable=broad-except | |
275 | self.skipTest("Error while generating the bits iso. " | |
276 | "Pass V=1 in the environment to get more details. " | |
277 | + str(e)) | |
278 | ||
279 | self.assertTrue(os.access(iso_file, os.R_OK)) | |
280 | ||
281 | self.logger.info('iso file %s successfully generated.', iso_file) | |
282 | ||
283 | def setUp(self): # pylint: disable=arguments-differ | |
284 | super().setUp('qemu-system-') | |
285 | ||
286 | self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR') | |
287 | ||
288 | # workdir could also be avocado's own workdir in self.workdir. | |
289 | # At present, I prefer to maintain my own temporary working | |
290 | # directory. It gives us more control over the generated bits | |
291 | # log files and also for debugging, we may chose not to remove | |
292 | # this working directory so that the logs and iso can be | |
293 | # inspected manually and archived if needed. | |
294 | self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', | |
295 | suffix='.tmp') | |
296 | self.logger.info('working dir: %s', self._workDir) | |
297 | ||
298 | prebuiltDir = os.path.join(self._workDir, 'prebuilt') | |
299 | if not os.path.isdir(prebuiltDir): | |
300 | os.mkdir(prebuiltDir, mode=0o775) | |
301 | ||
302 | bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' | |
303 | %(self._bitsInternalVer, | |
304 | self._bitsCommitHash)) | |
305 | grub_tar_file = os.path.join(prebuiltDir, | |
306 | 'bits-%d-%s-grub.tar.gz' | |
307 | %(self._bitsInternalVer, | |
308 | self._bitsCommitHash)) | |
309 | ||
310 | bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL, | |
311 | asset_hash=self._bitsArtSHA1Hash) | |
312 | self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) | |
313 | ||
314 | # extract the bits artifact in the temp working directory | |
315 | with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: | |
316 | zref.extractall(prebuiltDir) | |
317 | ||
318 | # extract the bits software in the temp working directory | |
319 | with zipfile.ZipFile(bits_zip_file, 'r') as zref: | |
320 | zref.extractall(self._workDir) | |
321 | ||
322 | with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: | |
323 | tarball.extractall(self._workDir) | |
324 | ||
325 | self.copy_test_scripts() | |
326 | self.copy_bits_config() | |
327 | self.generate_bits_iso() | |
328 | ||
329 | def parse_log(self): | |
330 | """parse the log generated by running bits tests and | |
331 | check for failures. | |
332 | """ | |
333 | debugconf = os.path.join(self._workDir, self._debugcon_log) | |
334 | log = "" | |
335 | with open(debugconf, 'r', encoding='utf-8') as filehandle: | |
336 | log = filehandle.read() | |
337 | ||
338 | matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', | |
339 | log) | |
340 | for match in matchiter: | |
341 | # verify that no test cases failed. | |
342 | try: | |
343 | self.assertEqual(match.group(3).split()[0], '0', | |
344 | 'Some bits tests seems to have failed. ' \ | |
345 | 'Please check the test logs for more info.') | |
346 | except AssertionError as e: | |
347 | self._print_log(log) | |
348 | raise e | |
349 | else: | |
04e5bd44 | 350 | if os.getenv('V') or os.getenv('BITS_DEBUG'): |
77a8e24c AS |
351 | self._print_log(log) |
352 | ||
353 | def tearDown(self): | |
354 | """ | |
355 | Lets do some cleanups. | |
356 | """ | |
357 | if self._vm: | |
358 | self.assertFalse(not self._vm.is_running) | |
1afae3b8 | 359 | if not os.getenv('BITS_DEBUG') and self._workDir: |
04e5bd44 AS |
360 | self.logger.info('removing the work directory %s', self._workDir) |
361 | shutil.rmtree(self._workDir) | |
362 | else: | |
363 | self.logger.info('not removing the work directory %s ' \ | |
364 | 'as BITS_DEBUG is ' \ | |
365 | 'passed in the environment', self._workDir) | |
77a8e24c AS |
366 | super().tearDown() |
367 | ||
368 | def test_acpi_smbios_bits(self): | |
96420a30 | 369 | """The main test case implementation.""" |
77a8e24c AS |
370 | |
371 | iso_file = os.path.join(self._workDir, | |
372 | 'bits-%d.iso' %self._bitsInternalVer) | |
373 | ||
374 | self.assertTrue(os.access(iso_file, os.R_OK)) | |
375 | ||
376 | self._vm = QEMUBitsMachine(binary=self.qemu_bin, | |
377 | base_temp_dir=self._workDir, | |
378 | debugcon_log=self._debugcon_log, | |
379 | debugcon_addr=self._debugcon_addr) | |
380 | ||
381 | self._vm.add_args('-cdrom', '%s' %iso_file) | |
382 | # the vm needs to be run under icount so that TCG emulation is | |
383 | # consistent in terms of timing. smilatency tests have consistent | |
384 | # timing requirements. | |
385 | self._vm.add_args('-icount', 'auto') | |
a874ddc9 AS |
386 | # currently there is no support in bits for recognizing 64-bit SMBIOS |
387 | # entry points. QEMU defaults to 64-bit entry points since the | |
388 | # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 | |
389 | # for newer machine models"). Therefore, enforce 32-bit entry point. | |
390 | self._vm.add_args('-machine', 'smbios-entry-point-type=32') | |
77a8e24c | 391 | |
94cd94f1 AS |
392 | # enable console logging |
393 | self._vm.set_console() | |
394 | self._vm.launch() | |
77a8e24c | 395 | |
94cd94f1 AS |
396 | self.logger.debug("Console output from bits VM follows ...") |
397 | c_drainer = drainer.LineLogger(self._vm.console_socket.fileno(), | |
398 | logger=self.logger.getChild("console"), | |
399 | stop_check=(lambda : | |
400 | not self._vm.is_running())) | |
401 | c_drainer.start() | |
77a8e24c | 402 | |
77a8e24c AS |
403 | # biosbits has been configured to run all the specified test suites |
404 | # in batch mode and then automatically initiate a vm shutdown. | |
7ef4c41e AS |
405 | # Set timeout to BITS_TIMEOUT for SHUTDOWN event from bits VM at par |
406 | # with the avocado test timeout. | |
407 | self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT) | |
c4d4c40c | 408 | self._vm.wait(timeout=None) |
77a8e24c | 409 | self.parse_log() |