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