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