xref: /qemu/tests/functional/test_acpi_bits.py (revision b103cc6e74ac92f070a0e004bd84334e845c20b5)
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
123    def _print_log(self, log):
124        self.logger.info('\nlogs from biosbits follows:')
125        self.logger.info('==========================================\n')
126        self.logger.info(log)
127        self.logger.info('==========================================\n')
128
129    def copy_bits_config(self):
130        """ copies the bios bits config file into bits.
131        """
132        bits_config_file = self.data_file('acpi-bits',
133                                          'bits-config',
134                                          'bits-cfg.txt')
135        target_config_dir = self.scratch_file('bits-%d' %
136                                              self.BITS_INTERNAL_VER,
137                                              'boot')
138        self.assertTrue(os.path.exists(bits_config_file))
139        self.assertTrue(os.path.exists(target_config_dir))
140        shutil.copy2(bits_config_file, target_config_dir)
141        self.logger.info('copied config file %s to %s',
142                         bits_config_file, target_config_dir)
143
144    def copy_test_scripts(self):
145        """copies the python test scripts into bits. """
146
147        bits_test_dir = self.data_file('acpi-bits', 'bits-tests')
148        target_test_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER,
149                                            'boot', 'python')
150
151        self.assertTrue(os.path.exists(bits_test_dir))
152        self.assertTrue(os.path.exists(target_test_dir))
153
154        for filename in os.listdir(bits_test_dir):
155            if os.path.isfile(os.path.join(bits_test_dir, filename)) and \
156               filename.endswith('.py2'):
157                # All test scripts are named with extension .py2 so that
158                # they are not run by accident.
159                #
160                # These scripts are intended to run inside the test VM
161                # and are written for python 2.7 not python 3, hence
162                # would cause syntax errors if loaded ouside the VM.
163                newfilename = os.path.splitext(filename)[0] + '.py'
164                shutil.copy2(os.path.join(bits_test_dir, filename),
165                             os.path.join(target_test_dir, newfilename))
166                self.logger.info('copied test file %s to %s',
167                                 filename, target_test_dir)
168
169                # now remove the pyc test file if it exists, otherwise the
170                # changes in the python test script won't be executed.
171                testfile_pyc = os.path.splitext(filename)[0] + '.pyc'
172                if os.access(os.path.join(target_test_dir, testfile_pyc),
173                             os.F_OK):
174                    os.remove(os.path.join(target_test_dir, testfile_pyc))
175                    self.logger.info('removed compiled file %s',
176                                     os.path.join(target_test_dir,
177                                     testfile_pyc))
178
179    def fix_mkrescue(self, mkrescue):
180        """ grub-mkrescue is a bash script with two variables, 'prefix' and
181            'libdir'. They must be pointed to the right location so that the
182            iso can be generated appropriately. We point the two variables to
183            the directory where we have extracted our pre-built bits grub
184            tarball.
185        """
186        grub_x86_64_mods = self.scratch_file('grub-inst-x86_64-efi')
187        grub_i386_mods = self.scratch_file('grub-inst')
188
189        self.assertTrue(os.path.exists(grub_x86_64_mods))
190        self.assertTrue(os.path.exists(grub_i386_mods))
191
192        new_script = ""
193        with open(mkrescue, 'r', encoding='utf-8') as filehandle:
194            orig_script = filehandle.read()
195            new_script = re.sub('(^prefix=)(.*)',
196                                r'\1"%s"' %grub_x86_64_mods,
197                                orig_script, flags=re.M)
198            new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods,
199                                new_script, flags=re.M)
200
201        with open(mkrescue, 'w', encoding='utf-8') as filehandle:
202            filehandle.write(new_script)
203
204    def generate_bits_iso(self):
205        """ Uses grub-mkrescue to generate a fresh bits iso with the python
206            test scripts
207        """
208        bits_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER)
209        iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER)
210        mkrescue_script = self.scratch_file('grub-inst-x86_64-efi',
211                                            'bin',
212                                            'grub-mkrescue')
213
214        self.assertTrue(os.access(mkrescue_script,
215                                  os.R_OK | os.W_OK | os.X_OK))
216
217        self.fix_mkrescue(mkrescue_script)
218
219        self.logger.info('using grub-mkrescue for generating biosbits iso ...')
220
221        try:
222            if os.getenv('V') or os.getenv('BITS_DEBUG'):
223                proc = subprocess.run([mkrescue_script, '-o', iso_file,
224                                       bits_dir],
225                                      stdout=subprocess.PIPE,
226                                      stderr=subprocess.STDOUT,
227                                      check=True)
228                self.logger.info("grub-mkrescue output %s" % proc.stdout)
229            else:
230                subprocess.check_call([mkrescue_script, '-o',
231                                      iso_file, bits_dir],
232                                      stderr=subprocess.DEVNULL,
233                                      stdout=subprocess.DEVNULL)
234        except Exception as e: # pylint: disable=broad-except
235            self.skipTest("Error while generating the bits iso. "
236                          "Pass V=1 in the environment to get more details. "
237                          + str(e))
238
239        self.assertTrue(os.access(iso_file, os.R_OK))
240
241        self.logger.info('iso file %s successfully generated.', iso_file)
242
243    def setUp(self): # pylint: disable=arguments-differ
244        super().setUp()
245        self.logger = self.log
246
247        prebuiltDir = self.scratch_file('prebuilt')
248        if not os.path.isdir(prebuiltDir):
249            os.mkdir(prebuiltDir, mode=0o775)
250
251        bits_zip_file = self.scratch_file('prebuilt',
252                                          'bits-%d-%s.zip'
253                                          %(self.BITS_INTERNAL_VER,
254                                            self.BITS_COMMIT_HASH))
255        grub_tar_file = self.scratch_file('prebuilt',
256                                          'bits-%d-%s-grub.tar.gz'
257                                          %(self.BITS_INTERNAL_VER,
258                                            self.BITS_COMMIT_HASH))
259
260        # extract the bits artifact in the temp working directory
261        self.archive_extract(self.ASSET_BITS, sub_dir='prebuilt', format='zip')
262
263        # extract the bits software in the temp working directory
264        self.archive_extract(bits_zip_file)
265        self.archive_extract(grub_tar_file)
266
267        self.copy_test_scripts()
268        self.copy_bits_config()
269        self.generate_bits_iso()
270
271    def parse_log(self):
272        """parse the log generated by running bits tests and
273           check for failures.
274        """
275        debugconf = self.scratch_file(self._debugcon_log)
276        log = ""
277        with open(debugconf, 'r', encoding='utf-8') as filehandle:
278            log = filehandle.read()
279
280        matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*',
281                                log)
282        for match in matchiter:
283            # verify that no test cases failed.
284            try:
285                self.assertEqual(match.group(3).split()[0], '0',
286                                 'Some bits tests seems to have failed. ' \
287                                 'Please check the test logs for more info.')
288            except AssertionError as e:
289                self._print_log(log)
290                raise e
291            else:
292                if os.getenv('V') or os.getenv('BITS_DEBUG'):
293                    self._print_log(log)
294
295    def tearDown(self):
296        """
297           Lets do some cleanups.
298        """
299        if self._vm:
300            self.assertFalse(not self._vm.is_running)
301        super().tearDown()
302
303    def test_acpi_smbios_bits(self):
304        """The main test case implementation."""
305
306        self.set_machine('pc')
307        iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER)
308
309        self.assertTrue(os.access(iso_file, os.R_OK))
310
311        self._vm = QEMUBitsMachine(binary=self.qemu_bin,
312                                   base_temp_dir=self.workdir,
313                                   debugcon_log=self._debugcon_log,
314                                   debugcon_addr=self._debugcon_addr)
315
316        self._vm.add_args('-cdrom', '%s' %iso_file)
317        # the vm needs to be run under icount so that TCG emulation is
318        # consistent in terms of timing. smilatency tests have consistent
319        # timing requirements.
320        self._vm.add_args('-icount', 'auto')
321        # currently there is no support in bits for recognizing 64-bit SMBIOS
322        # entry points. QEMU defaults to 64-bit entry points since the
323        # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0
324        # for newer machine models"). Therefore, enforce 32-bit entry point.
325        self._vm.add_args('-machine', 'smbios-entry-point-type=32')
326
327        # enable console logging
328        self._vm.set_console()
329        self._vm.launch()
330
331
332        # biosbits has been configured to run all the specified test suites
333        # in batch mode and then automatically initiate a vm shutdown.
334        self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT)
335        self._vm.wait(timeout=None)
336        self.logger.debug("Checking console output ...")
337        self.parse_log()
338
339if __name__ == '__main__':
340    QemuSystemTest.main()
341