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