| #!/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 logging |
| import os |
| import platform |
| import re |
| import shutil |
| import subprocess |
| import tarfile |
| import tempfile |
| import time |
| import zipfile |
| |
| from pathlib import Path |
| from typing import ( |
| List, |
| Optional, |
| Sequence, |
| ) |
| from qemu.machine import QEMUMachine |
| from unittest import skipIf |
| from qemu_test import QemuBaseTest, Asset |
| |
| deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box. |
| supported_platforms = ['x86_64'] # supported test platforms. |
| |
| # default timeout of 120 secs is sometimes not enough for bits test. |
| BITS_TIMEOUT = 200 |
| |
| def which(tool): |
| """ looks up the full path for @tool, returns None if not found |
| or if @tool does not have executable permissions. |
| """ |
| paths=os.getenv('PATH') |
| for p in paths.split(os.path.pathsep): |
| p = os.path.join(p, tool) |
| if os.path.exists(p) and os.access(p, os.X_OK): |
| return p |
| return None |
| |
| def missing_deps(): |
| """ returns True if any of the test dependent tools are absent. |
| """ |
| for dep in deps: |
| if which(dep) is None: |
| return True |
| return False |
| |
| def supported_platform(): |
| """ checks if the test is running on a supported platform. |
| """ |
| return platform.machine() in supported_platforms |
| |
| 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 |
| |
| @skipIf(not supported_platform() or missing_deps(), |
| 'unsupported platform or dependencies (%s) not installed' \ |
| % ','.join(deps)) |
| class AcpiBitsTest(QemuBaseTest): #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._workDir = None |
| self._baseDir = 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. |
| """ |
| config_file = 'bits-cfg.txt' |
| bits_config_dir = os.path.join(self._baseDir, 'acpi-bits', |
| 'bits-config') |
| target_config_dir = os.path.join(self._workDir, |
| 'bits-%d' %self.BITS_INTERNAL_VER, |
| 'boot') |
| self.assertTrue(os.path.exists(bits_config_dir)) |
| self.assertTrue(os.path.exists(target_config_dir)) |
| self.assertTrue(os.access(os.path.join(bits_config_dir, |
| config_file), os.R_OK)) |
| shutil.copy2(os.path.join(bits_config_dir, config_file), |
| target_config_dir) |
| self.logger.info('copied config file %s to %s', |
| config_file, target_config_dir) |
| |
| def copy_test_scripts(self): |
| """copies the python test scripts into bits. """ |
| |
| bits_test_dir = os.path.join(self._baseDir, 'acpi-bits', |
| 'bits-tests') |
| target_test_dir = os.path.join(self._workDir, |
| '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 |
| # avocado does not try to load them. These scripts are |
| # written for python 2.7 not python 3 and hence if avocado |
| # loaded them, it would complain about python 3 specific |
| # syntaxes. |
| 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 = os.path.join(self._workDir, 'grub-inst-x86_64-efi') |
| grub_i386_mods = os.path.join(self._workDir, '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 = os.path.join(self._workDir, |
| 'bits-%d' %self.BITS_INTERNAL_VER) |
| iso_file = os.path.join(self._workDir, |
| 'bits-%d.iso' %self.BITS_INTERNAL_VER) |
| mkrescue_script = os.path.join(self._workDir, |
| '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('qemu-system-') |
| self.logger = self.log |
| |
| self._baseDir = Path(__file__).parent |
| |
| # workdir could also be avocado's own workdir in self.workdir. |
| # At present, I prefer to maintain my own temporary working |
| # directory. It gives us more control over the generated bits |
| # log files and also for debugging, we may chose not to remove |
| # this working directory so that the logs and iso can be |
| # inspected manually and archived if needed. |
| self._workDir = tempfile.mkdtemp(prefix='acpi-bits-', |
| suffix='.tmp') |
| self.logger.info('working dir: %s', self._workDir) |
| |
| prebuiltDir = os.path.join(self._workDir, 'prebuilt') |
| if not os.path.isdir(prebuiltDir): |
| os.mkdir(prebuiltDir, mode=0o775) |
| |
| bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip' |
| %(self.BITS_INTERNAL_VER, |
| self.BITS_COMMIT_HASH)) |
| grub_tar_file = os.path.join(prebuiltDir, |
| 'bits-%d-%s-grub.tar.gz' |
| %(self.BITS_INTERNAL_VER, |
| self.BITS_COMMIT_HASH)) |
| |
| bitsLocalArtLoc = self.ASSET_BITS.fetch() |
| self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc) |
| |
| # extract the bits artifact in the temp working directory |
| with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref: |
| zref.extractall(prebuiltDir) |
| |
| # extract the bits software in the temp working directory |
| with zipfile.ZipFile(bits_zip_file, 'r') as zref: |
| zref.extractall(self._workDir) |
| |
| with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball: |
| tarball.extractall(self._workDir) |
| |
| 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 = os.path.join(self._workDir, 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) |
| if not os.getenv('BITS_DEBUG') and self._workDir: |
| self.logger.info('removing the work directory %s', self._workDir) |
| shutil.rmtree(self._workDir) |
| else: |
| self.logger.info('not removing the work directory %s ' \ |
| 'as BITS_DEBUG is ' \ |
| 'passed in the environment', self._workDir) |
| super().tearDown() |
| |
| def test_acpi_smbios_bits(self): |
| """The main test case implementation.""" |
| |
| iso_file = os.path.join(self._workDir, |
| '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. |
| # Set timeout to BITS_TIMEOUT for SHUTDOWN event from bits VM at par |
| # with the avocado test timeout. |
| 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__': |
| QemuBaseTest.main() |