| #!/usr/bin/env python3 | |
| # | |
| # Exercise QEMU generated ACPI/SMBIOS tables using biosbits, | |
| # https://biosbits.org/ | |
| # | |
| # This program is free software; you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation; either version 2 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| # | |
| # | |
| # Author: | |
| # Ani Sinha <anisinha@redhat.com> | |
| # pylint: disable=invalid-name | |
| # pylint: disable=consider-using-f-string | |
| """ | |
| This is QEMU ACPI/SMBIOS functional tests using biosbits. | |
| Biosbits is available originally at https://biosbits.org/. | |
| This test uses a fork of the upstream bits and has numerous fixes | |
| including an upgraded acpica. The fork is located here: | |
| https://gitlab.com/qemu-project/biosbits-bits . | |
| """ | |
| import os | |
| import re | |
| import shutil | |
| import subprocess | |
| from typing import ( | |
| List, | |
| Optional, | |
| Sequence, | |
| ) | |
| from qemu.machine import QEMUMachine | |
| from qemu_test import (QemuSystemTest, Asset, skipIfMissingCommands, | |
| skipIfNotMachine) | |
| # default timeout of 120 secs is sometimes not enough for bits test. | |
| BITS_TIMEOUT = 200 | |
| class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods | |
| """ | |
| A QEMU VM, with isa-debugcon enabled and bits iso passed | |
| using -cdrom to QEMU commandline. | |
| """ | |
| def __init__(self, | |
| binary: str, | |
| args: Sequence[str] = (), | |
| wrapper: Sequence[str] = (), | |
| name: Optional[str] = None, | |
| base_temp_dir: str = "/var/tmp", | |
| debugcon_log: str = "debugcon-log.txt", | |
| debugcon_addr: str = "0x403", | |
| qmp_timer: Optional[float] = None): | |
| # pylint: disable=too-many-arguments | |
| if name is None: | |
| name = "qemu-bits-%d" % os.getpid() | |
| super().__init__(binary, args, wrapper=wrapper, name=name, | |
| base_temp_dir=base_temp_dir, | |
| qmp_timer=qmp_timer) | |
| self.debugcon_log = debugcon_log | |
| self.debugcon_addr = debugcon_addr | |
| self.base_temp_dir = base_temp_dir | |
| @property | |
| def _base_args(self) -> List[str]: | |
| args = super()._base_args | |
| args.extend([ | |
| '-chardev', | |
| 'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir, | |
| self.debugcon_log), | |
| '-device', | |
| 'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr, | |
| ]) | |
| return args | |
| def base_args(self): | |
| """return the base argument to QEMU binary""" | |
| return self._base_args | |
| @skipIfMissingCommands("xorriso", "mformat") | |
| @skipIfNotMachine("x86_64") | |
| class AcpiBitsTest(QemuSystemTest): #pylint: disable=too-many-instance-attributes | |
| """ | |
| ACPI and SMBIOS tests using biosbits. | |
| """ | |
| # in slower systems the test can take as long as 3 minutes to complete. | |
| timeout = BITS_TIMEOUT | |
| # following are some standard configuration constants | |
| # gitlab CI does shallow clones of depth 20 | |
| BITS_INTERNAL_VER = 2020 | |
| # commit hash must match the artifact tag below | |
| BITS_COMMIT_HASH = 'c7920d2b' | |
| # this is the latest bits release as of today. | |
| BITS_TAG = "qemu-bits-10262023" | |
| ASSET_BITS = Asset(("https://gitlab.com/qemu-project/" | |
| "biosbits-bits/-/jobs/artifacts/%s/" | |
| "download?job=qemu-bits-build" % BITS_TAG), | |
| '1b8dd612c6831a6b491716a77acc486666aaa867051cdc34f7ce169c2e25f487') | |
| def __init__(self, *args, **kwargs): | |
| super().__init__(*args, **kwargs) | |
| self._vm = None | |
| self._debugcon_addr = '0x403' | |
| self._debugcon_log = 'debugcon-log.txt' | |
| self.logger = self.log | |
| def _print_log(self, log): | |
| self.logger.info('\nlogs from biosbits follows:') | |
| self.logger.info('==========================================\n') | |
| self.logger.info(log) | |
| self.logger.info('==========================================\n') | |
| def copy_bits_config(self): | |
| """ copies the bios bits config file into bits. | |
| """ | |
| bits_config_file = self.data_file('acpi-bits', | |
| 'bits-config', | |
| 'bits-cfg.txt') | |
| target_config_dir = self.scratch_file('bits-%d' % | |
| self.BITS_INTERNAL_VER, | |
| 'boot') | |
| self.assertTrue(os.path.exists(bits_config_file)) | |
| self.assertTrue(os.path.exists(target_config_dir)) | |
| shutil.copy2(bits_config_file, target_config_dir) | |
| self.logger.info('copied config file %s to %s', | |
| bits_config_file, target_config_dir) | |
| def copy_test_scripts(self): | |
| """copies the python test scripts into bits. """ | |
| bits_test_dir = self.data_file('acpi-bits', 'bits-tests') | |
| target_test_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER, | |
| 'boot', 'python') | |
| self.assertTrue(os.path.exists(bits_test_dir)) | |
| self.assertTrue(os.path.exists(target_test_dir)) | |
| for filename in os.listdir(bits_test_dir): | |
| if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ | |
| filename.endswith('.py2'): | |
| # All test scripts are named with extension .py2 so that | |
| # they are not run by accident. | |
| # | |
| # These scripts are intended to run inside the test VM | |
| # and are written for python 2.7 not python 3, hence | |
| # would cause syntax errors if loaded ouside the VM. | |
| newfilename = os.path.splitext(filename)[0] + '.py' | |
| shutil.copy2(os.path.join(bits_test_dir, filename), | |
| os.path.join(target_test_dir, newfilename)) | |
| self.logger.info('copied test file %s to %s', | |
| filename, target_test_dir) | |
| # now remove the pyc test file if it exists, otherwise the | |
| # changes in the python test script won't be executed. | |
| testfile_pyc = os.path.splitext(filename)[0] + '.pyc' | |
| if os.access(os.path.join(target_test_dir, testfile_pyc), | |
| os.F_OK): | |
| os.remove(os.path.join(target_test_dir, testfile_pyc)) | |
| self.logger.info('removed compiled file %s', | |
| os.path.join(target_test_dir, | |
| testfile_pyc)) | |
| def fix_mkrescue(self, mkrescue): | |
| """ grub-mkrescue is a bash script with two variables, 'prefix' and | |
| 'libdir'. They must be pointed to the right location so that the | |
| iso can be generated appropriately. We point the two variables to | |
| the directory where we have extracted our pre-built bits grub | |
| tarball. | |
| """ | |
| grub_x86_64_mods = self.scratch_file('grub-inst-x86_64-efi') | |
| grub_i386_mods = self.scratch_file('grub-inst') | |
| self.assertTrue(os.path.exists(grub_x86_64_mods)) | |
| self.assertTrue(os.path.exists(grub_i386_mods)) | |
| new_script = "" | |
| with open(mkrescue, 'r', encoding='utf-8') as filehandle: | |
| orig_script = filehandle.read() | |
| new_script = re.sub('(^prefix=)(.*)', | |
| r'\1"%s"' %grub_x86_64_mods, | |
| orig_script, flags=re.M) | |
| new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, | |
| new_script, flags=re.M) | |
| with open(mkrescue, 'w', encoding='utf-8') as filehandle: | |
| filehandle.write(new_script) | |
| def generate_bits_iso(self): | |
| """ Uses grub-mkrescue to generate a fresh bits iso with the python | |
| test scripts | |
| """ | |
| bits_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER) | |
| iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER) | |
| mkrescue_script = self.scratch_file('grub-inst-x86_64-efi', | |
| 'bin', | |
| 'grub-mkrescue') | |
| self.assertTrue(os.access(mkrescue_script, | |
| os.R_OK | os.W_OK | os.X_OK)) | |
| self.fix_mkrescue(mkrescue_script) | |
| self.logger.info('using grub-mkrescue for generating biosbits iso ...') | |
| try: | |
| if os.getenv('V') or os.getenv('BITS_DEBUG'): | |
| proc = subprocess.run([mkrescue_script, '-o', iso_file, | |
| bits_dir], | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.STDOUT, | |
| check=True) | |
| self.logger.info("grub-mkrescue output %s" % proc.stdout) | |
| else: | |
| subprocess.check_call([mkrescue_script, '-o', | |
| iso_file, bits_dir], | |
| stderr=subprocess.DEVNULL, | |
| stdout=subprocess.DEVNULL) | |
| except Exception as e: # pylint: disable=broad-except | |
| self.skipTest("Error while generating the bits iso. " | |
| "Pass V=1 in the environment to get more details. " | |
| + str(e)) | |
| self.assertTrue(os.access(iso_file, os.R_OK)) | |
| self.logger.info('iso file %s successfully generated.', iso_file) | |
| def setUp(self): # pylint: disable=arguments-differ | |
| super().setUp() | |
| self.logger = self.log | |
| prebuiltDir = self.scratch_file('prebuilt') | |
| if not os.path.isdir(prebuiltDir): | |
| os.mkdir(prebuiltDir, mode=0o775) | |
| bits_zip_file = self.scratch_file('prebuilt', | |
| 'bits-%d-%s.zip' | |
| %(self.BITS_INTERNAL_VER, | |
| self.BITS_COMMIT_HASH)) | |
| grub_tar_file = self.scratch_file('prebuilt', | |
| 'bits-%d-%s-grub.tar.gz' | |
| %(self.BITS_INTERNAL_VER, | |
| self.BITS_COMMIT_HASH)) | |
| # extract the bits artifact in the temp working directory | |
| self.archive_extract(self.ASSET_BITS, sub_dir='prebuilt', format='zip') | |
| # extract the bits software in the temp working directory | |
| self.archive_extract(bits_zip_file) | |
| self.archive_extract(grub_tar_file) | |
| self.copy_test_scripts() | |
| self.copy_bits_config() | |
| self.generate_bits_iso() | |
| def parse_log(self): | |
| """parse the log generated by running bits tests and | |
| check for failures. | |
| """ | |
| debugconf = self.scratch_file(self._debugcon_log) | |
| log = "" | |
| with open(debugconf, 'r', encoding='utf-8') as filehandle: | |
| log = filehandle.read() | |
| matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', | |
| log) | |
| for match in matchiter: | |
| # verify that no test cases failed. | |
| try: | |
| self.assertEqual(match.group(3).split()[0], '0', | |
| 'Some bits tests seems to have failed. ' \ | |
| 'Please check the test logs for more info.') | |
| except AssertionError as e: | |
| self._print_log(log) | |
| raise e | |
| else: | |
| if os.getenv('V') or os.getenv('BITS_DEBUG'): | |
| self._print_log(log) | |
| def tearDown(self): | |
| """ | |
| Lets do some cleanups. | |
| """ | |
| if self._vm: | |
| self.assertFalse(not self._vm.is_running) | |
| super().tearDown() | |
| def test_acpi_smbios_bits(self): | |
| """The main test case implementation.""" | |
| self.set_machine('pc') | |
| iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER) | |
| self.assertTrue(os.access(iso_file, os.R_OK)) | |
| self._vm = QEMUBitsMachine(binary=self.qemu_bin, | |
| base_temp_dir=self.workdir, | |
| debugcon_log=self._debugcon_log, | |
| debugcon_addr=self._debugcon_addr) | |
| self._vm.add_args('-cdrom', '%s' %iso_file) | |
| # the vm needs to be run under icount so that TCG emulation is | |
| # consistent in terms of timing. smilatency tests have consistent | |
| # timing requirements. | |
| self._vm.add_args('-icount', 'auto') | |
| # currently there is no support in bits for recognizing 64-bit SMBIOS | |
| # entry points. QEMU defaults to 64-bit entry points since the | |
| # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 | |
| # for newer machine models"). Therefore, enforce 32-bit entry point. | |
| self._vm.add_args('-machine', 'smbios-entry-point-type=32') | |
| # enable console logging | |
| self._vm.set_console() | |
| self._vm.launch() | |
| # biosbits has been configured to run all the specified test suites | |
| # in batch mode and then automatically initiate a vm shutdown. | |
| self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT) | |
| self._vm.wait(timeout=None) | |
| self.logger.debug("Checking console output ...") | |
| self.parse_log() | |
| if __name__ == '__main__': | |
| QemuSystemTest.main() |