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