xref: /openbmc/qemu/tests/functional/test_acpi_bits.py (revision eb22a064455aeebc105cc89bf77f48aa18b52938)
1 #!/usr/bin/env python3
2 #
3 # Exercise QEMU generated ACPI/SMBIOS tables using biosbits,
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:
21 #  Ani Sinha <anisinha@redhat.com>
22 
23 # pylint: disable=invalid-name
24 # pylint: disable=consider-using-f-string
25 
26 """
27 This is QEMU ACPI/SMBIOS functional 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 
45 from pathlib import Path
46 from typing import (
47     List,
48     Optional,
49     Sequence,
50 )
51 from qemu.machine import QEMUMachine
52 from unittest import skipIf
53 from qemu_test import QemuSystemTest, Asset
54 
55 deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box.
56 supported_platforms = ['x86_64'] # supported test platforms.
57 
58 # default timeout of 120 secs is sometimes not enough for bits test.
59 BITS_TIMEOUT = 200
60 
61 def which(tool):
62     """ looks up the full path for @tool, returns None if not found
63         or if @tool does not have executable permissions.
64     """
65     paths=os.getenv('PATH')
66     for p in paths.split(os.path.pathsep):
67         p = os.path.join(p, tool)
68         if os.path.exists(p) and os.access(p, os.X_OK):
69             return p
70     return None
71 
72 def missing_deps():
73     """ returns True if any of the test dependent tools are absent.
74     """
75     for dep in deps:
76         if which(dep) is None:
77             return True
78     return False
79 
80 def supported_platform():
81     """ checks if the test is running on a supported platform.
82     """
83     return platform.machine() in supported_platforms
84 
85 class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods
86     """
87     A QEMU VM, with isa-debugcon enabled and bits iso passed
88     using -cdrom to QEMU commandline.
89 
90     """
91     def __init__(self,
92                  binary: str,
93                  args: Sequence[str] = (),
94                  wrapper: Sequence[str] = (),
95                  name: Optional[str] = None,
96                  base_temp_dir: str = "/var/tmp",
97                  debugcon_log: str = "debugcon-log.txt",
98                  debugcon_addr: str = "0x403",
99                  qmp_timer: Optional[float] = None):
100         # pylint: disable=too-many-arguments
101 
102         if name is None:
103             name = "qemu-bits-%d" % os.getpid()
104         super().__init__(binary, args, wrapper=wrapper, name=name,
105                          base_temp_dir=base_temp_dir,
106                          qmp_timer=qmp_timer)
107         self.debugcon_log = debugcon_log
108         self.debugcon_addr = debugcon_addr
109         self.base_temp_dir = base_temp_dir
110 
111     @property
112     def _base_args(self) -> List[str]:
113         args = super()._base_args
114         args.extend([
115             '-chardev',
116             'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir,
117                                                      self.debugcon_log),
118             '-device',
119             'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr,
120         ])
121         return args
122 
123     def base_args(self):
124         """return the base argument to QEMU binary"""
125         return self._base_args
126 
127 @skipIf(not supported_platform() or missing_deps(),
128         'unsupported platform or dependencies (%s) not installed' \
129         % ','.join(deps))
130 class AcpiBitsTest(QemuSystemTest): #pylint: disable=too-many-instance-attributes
131     """
132     ACPI and SMBIOS tests using biosbits.
133     """
134     # in slower systems the test can take as long as 3 minutes to complete.
135     timeout = BITS_TIMEOUT
136 
137     # following are some standard configuration constants
138     # gitlab CI does shallow clones of depth 20
139     BITS_INTERNAL_VER = 2020
140     # commit hash must match the artifact tag below
141     BITS_COMMIT_HASH = 'c7920d2b'
142     # this is the latest bits release as of today.
143     BITS_TAG = "qemu-bits-10262023"
144 
145     ASSET_BITS = Asset(("https://gitlab.com/qemu-project/"
146                         "biosbits-bits/-/jobs/artifacts/%s/"
147                         "download?job=qemu-bits-build" % BITS_TAG),
148                        '1b8dd612c6831a6b491716a77acc486666aaa867051cdc34f7ce169c2e25f487')
149 
150     def __init__(self, *args, **kwargs):
151         super().__init__(*args, **kwargs)
152         self._vm = None
153         self._baseDir = None
154 
155         self._debugcon_addr = '0x403'
156         self._debugcon_log = 'debugcon-log.txt'
157         self.logger = self.log
158 
159     def _print_log(self, log):
160         self.logger.info('\nlogs from biosbits follows:')
161         self.logger.info('==========================================\n')
162         self.logger.info(log)
163         self.logger.info('==========================================\n')
164 
165     def copy_bits_config(self):
166         """ copies the bios bits config file into bits.
167         """
168         config_file = 'bits-cfg.txt'
169         bits_config_dir = os.path.join(self._baseDir, 'acpi-bits',
170                                        'bits-config')
171         target_config_dir = os.path.join(self.workdir,
172                                          'bits-%d' %self.BITS_INTERNAL_VER,
173                                          'boot')
174         self.assertTrue(os.path.exists(bits_config_dir))
175         self.assertTrue(os.path.exists(target_config_dir))
176         self.assertTrue(os.access(os.path.join(bits_config_dir,
177                                                config_file), os.R_OK))
178         shutil.copy2(os.path.join(bits_config_dir, config_file),
179                      target_config_dir)
180         self.logger.info('copied config file %s to %s',
181                          config_file, target_config_dir)
182 
183     def copy_test_scripts(self):
184         """copies the python test scripts into bits. """
185 
186         bits_test_dir = os.path.join(self._baseDir, 'acpi-bits',
187                                      'bits-tests')
188         target_test_dir = os.path.join(self.workdir,
189                                        'bits-%d' %self.BITS_INTERNAL_VER,
190                                        'boot', 'python')
191 
192         self.assertTrue(os.path.exists(bits_test_dir))
193         self.assertTrue(os.path.exists(target_test_dir))
194 
195         for filename in os.listdir(bits_test_dir):
196             if os.path.isfile(os.path.join(bits_test_dir, filename)) and \
197                filename.endswith('.py2'):
198                 # All test scripts are named with extension .py2 so that
199                 # they are not run by accident.
200                 #
201                 # These scripts are intended to run inside the test VM
202                 # and are written for python 2.7 not python 3, hence
203                 # would cause syntax errors if loaded ouside the VM.
204                 newfilename = os.path.splitext(filename)[0] + '.py'
205                 shutil.copy2(os.path.join(bits_test_dir, filename),
206                              os.path.join(target_test_dir, newfilename))
207                 self.logger.info('copied test file %s to %s',
208                                  filename, target_test_dir)
209 
210                 # now remove the pyc test file if it exists, otherwise the
211                 # changes in the python test script won't be executed.
212                 testfile_pyc = os.path.splitext(filename)[0] + '.pyc'
213                 if os.access(os.path.join(target_test_dir, testfile_pyc),
214                              os.F_OK):
215                     os.remove(os.path.join(target_test_dir, testfile_pyc))
216                     self.logger.info('removed compiled file %s',
217                                      os.path.join(target_test_dir,
218                                      testfile_pyc))
219 
220     def fix_mkrescue(self, mkrescue):
221         """ grub-mkrescue is a bash script with two variables, 'prefix' and
222             'libdir'. They must be pointed to the right location so that the
223             iso can be generated appropriately. We point the two variables to
224             the directory where we have extracted our pre-built bits grub
225             tarball.
226         """
227         grub_x86_64_mods = os.path.join(self.workdir, 'grub-inst-x86_64-efi')
228         grub_i386_mods = os.path.join(self.workdir, 'grub-inst')
229 
230         self.assertTrue(os.path.exists(grub_x86_64_mods))
231         self.assertTrue(os.path.exists(grub_i386_mods))
232 
233         new_script = ""
234         with open(mkrescue, 'r', encoding='utf-8') as filehandle:
235             orig_script = filehandle.read()
236             new_script = re.sub('(^prefix=)(.*)',
237                                 r'\1"%s"' %grub_x86_64_mods,
238                                 orig_script, flags=re.M)
239             new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods,
240                                 new_script, flags=re.M)
241 
242         with open(mkrescue, 'w', encoding='utf-8') as filehandle:
243             filehandle.write(new_script)
244 
245     def generate_bits_iso(self):
246         """ Uses grub-mkrescue to generate a fresh bits iso with the python
247             test scripts
248         """
249         bits_dir = os.path.join(self.workdir,
250                                 'bits-%d' %self.BITS_INTERNAL_VER)
251         iso_file = os.path.join(self.workdir,
252                                 'bits-%d.iso' %self.BITS_INTERNAL_VER)
253         mkrescue_script = os.path.join(self.workdir,
254                                        'grub-inst-x86_64-efi', 'bin',
255                                        'grub-mkrescue')
256 
257         self.assertTrue(os.access(mkrescue_script,
258                                   os.R_OK | os.W_OK | os.X_OK))
259 
260         self.fix_mkrescue(mkrescue_script)
261 
262         self.logger.info('using grub-mkrescue for generating biosbits iso ...')
263 
264         try:
265             if os.getenv('V') or os.getenv('BITS_DEBUG'):
266                 proc = subprocess.run([mkrescue_script, '-o', iso_file,
267                                        bits_dir],
268                                       stdout=subprocess.PIPE,
269                                       stderr=subprocess.STDOUT,
270                                       check=True)
271                 self.logger.info("grub-mkrescue output %s" % proc.stdout)
272             else:
273                 subprocess.check_call([mkrescue_script, '-o',
274                                       iso_file, bits_dir],
275                                       stderr=subprocess.DEVNULL,
276                                       stdout=subprocess.DEVNULL)
277         except Exception as e: # pylint: disable=broad-except
278             self.skipTest("Error while generating the bits iso. "
279                           "Pass V=1 in the environment to get more details. "
280                           + str(e))
281 
282         self.assertTrue(os.access(iso_file, os.R_OK))
283 
284         self.logger.info('iso file %s successfully generated.', iso_file)
285 
286     def setUp(self): # pylint: disable=arguments-differ
287         super().setUp()
288         self.logger = self.log
289 
290         self._baseDir = Path(__file__).parent
291 
292         prebuiltDir = os.path.join(self.workdir, 'prebuilt')
293         if not os.path.isdir(prebuiltDir):
294             os.mkdir(prebuiltDir, mode=0o775)
295 
296         bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip'
297                                      %(self.BITS_INTERNAL_VER,
298                                        self.BITS_COMMIT_HASH))
299         grub_tar_file = os.path.join(prebuiltDir,
300                                      'bits-%d-%s-grub.tar.gz'
301                                      %(self.BITS_INTERNAL_VER,
302                                        self.BITS_COMMIT_HASH))
303 
304         bitsLocalArtLoc = self.ASSET_BITS.fetch()
305         self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc)
306 
307         # extract the bits artifact in the temp working directory
308         with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref:
309             zref.extractall(prebuiltDir)
310 
311         # extract the bits software in the temp working directory
312         with zipfile.ZipFile(bits_zip_file, 'r') as zref:
313             zref.extractall(self.workdir)
314 
315         with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball:
316             tarball.extractall(self.workdir)
317 
318         self.copy_test_scripts()
319         self.copy_bits_config()
320         self.generate_bits_iso()
321 
322     def parse_log(self):
323         """parse the log generated by running bits tests and
324            check for failures.
325         """
326         debugconf = os.path.join(self.workdir, self._debugcon_log)
327         log = ""
328         with open(debugconf, 'r', encoding='utf-8') as filehandle:
329             log = filehandle.read()
330 
331         matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*',
332                                 log)
333         for match in matchiter:
334             # verify that no test cases failed.
335             try:
336                 self.assertEqual(match.group(3).split()[0], '0',
337                                  'Some bits tests seems to have failed. ' \
338                                  'Please check the test logs for more info.')
339             except AssertionError as e:
340                 self._print_log(log)
341                 raise e
342             else:
343                 if os.getenv('V') or os.getenv('BITS_DEBUG'):
344                     self._print_log(log)
345 
346     def tearDown(self):
347         """
348            Lets do some cleanups.
349         """
350         if self._vm:
351             self.assertFalse(not self._vm.is_running)
352         super().tearDown()
353 
354     def test_acpi_smbios_bits(self):
355         """The main test case implementation."""
356 
357         self.set_machine('pc')
358         iso_file = os.path.join(self.workdir,
359                                 'bits-%d.iso' %self.BITS_INTERNAL_VER)
360 
361         self.assertTrue(os.access(iso_file, os.R_OK))
362 
363         self._vm = QEMUBitsMachine(binary=self.qemu_bin,
364                                    base_temp_dir=self.workdir,
365                                    debugcon_log=self._debugcon_log,
366                                    debugcon_addr=self._debugcon_addr)
367 
368         self._vm.add_args('-cdrom', '%s' %iso_file)
369         # the vm needs to be run under icount so that TCG emulation is
370         # consistent in terms of timing. smilatency tests have consistent
371         # timing requirements.
372         self._vm.add_args('-icount', 'auto')
373         # currently there is no support in bits for recognizing 64-bit SMBIOS
374         # entry points. QEMU defaults to 64-bit entry points since the
375         # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0
376         # for newer machine models"). Therefore, enforce 32-bit entry point.
377         self._vm.add_args('-machine', 'smbios-entry-point-type=32')
378 
379         # enable console logging
380         self._vm.set_console()
381         self._vm.launch()
382 
383 
384         # biosbits has been configured to run all the specified test suites
385         # in batch mode and then automatically initiate a vm shutdown.
386         self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT)
387         self._vm.wait(timeout=None)
388         self.logger.debug("Checking console output ...")
389         self.parse_log()
390 
391 if __name__ == '__main__':
392     QemuSystemTest.main()
393