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