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