new file mode 100644
@@ -0,0 +1,334 @@
+#!/usr/bin/env python3
+# group: rw quick
+# Exercize 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 <ani@anisinha.ca>
+
+# pylint: disable=invalid-name
+# pylint: disable=consider-using-f-string
+
+"""
+This is QEMU ACPI/SMBIOS avocado 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 re
+import shutil
+import subprocess
+import tarfile
+import tempfile
+import time
+import zipfile
+from typing import (
+ List,
+ Optional,
+ Sequence,
+)
+from avocado import skipIf
+from avocado_qemu import QemuBaseTest
+from qemu.machine import QEMUMachine
+
+class QEMUBitsMachine(QEMUMachine):
+ """
+ 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",
+ sock_dir: Optional[str] = None,
+ qmp_timer: Optional[float] = None):
+ # pylint: disable=too-many-arguments
+
+ if name is None:
+ name = "qemu-bits-%d" % os.getpid()
+ if sock_dir is None:
+ sock_dir = base_temp_dir
+ super().__init__(binary, args, wrapper=wrapper, name=name,
+ base_temp_dir=base_temp_dir,
+ sock_dir=sock_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(os.getenv('GITLAB_CI'), 'Running on GitLab')
+class AcpiBitsTest(QemuBaseTest):
+ """
+ ACPI and SMBIOS tests using biosbits.
+
+ :avocado: tags=arch:x86_64
+ :avocado: tags=acpi
+
+ """
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self._vm = None
+ self._workDir = None
+ self._baseDir = None
+
+ # following are some standard configuration constants
+ self._bitsInternalVer = 2020
+ self._bitsCommitHash = 'b972c69d' # commit hash must match
+ # the artifact tag below
+ self._bitsTag = "qemu-bits-09272022" # this is the latest bits
+ # release as of today.
+ self._bitsArtSHA1Hash = '6915ad4781de0d80d1a099438a4cb4bd9e12df70'
+ self._bitsArtURL = ("https://gitlab.com/qemu-project/"
+ "biosbits-bits/-/jobs/artifacts/%s/"
+ "download?job=qemu-bits-build" %self._bitsTag)
+ self._debugcon_addr = '0x403'
+ self._debugcon_log = 'debugcon-log.txt'
+ logging.basicConfig(level=logging.INFO)
+
+ 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._bitsInternalVer,
+ '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)
+ logging.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._bitsInternalVer,
+ '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('.py'):
+ shutil.copy2(os.path.join(bits_test_dir, filename),
+ target_test_dir)
+ logging.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))
+ logging.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._bitsInternalVer)
+ iso_file = os.path.join(self._workDir,
+ 'bits-%d.iso' %self._bitsInternalVer)
+ 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)
+
+ logging.info('calling grub-mkrescue to generate the biosbits iso ...')
+
+ try:
+ if os.getenv('V'):
+ subprocess.check_call([mkrescue_script, '-o',
+ iso_file, bits_dir],
+ stdout=subprocess.DEVNULL)
+ 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))
+
+ logging.info('iso file %s successfully generated.', iso_file)
+
+ def setUp(self): # pylint: disable=arguments-differ
+ super().setUp('qemu-system-')
+ self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR')
+ # workdir could also be avocado's own workdir in self.workdir.
+ # At present, I prefer to maintain my own working directory in /tmp
+ # since I do not want to mess up with directories maintained by avocado.
+ self._workDir = tempfile.mkdtemp(prefix='acpi-bits-',
+ suffix='.tmp')
+ logging.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._bitsInternalVer,
+ self._bitsCommitHash))
+ grub_tar_file = os.path.join(prebuiltDir,
+ 'bits-%d-%s-grub.tar.gz'
+ %(self._bitsInternalVer,
+ self._bitsCommitHash))
+
+ bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL,
+ asset_hash=self._bitsArtSHA1Hash)
+ logging.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()
+
+ if os.getenv('V'):
+ print('\nlogs from biosbits follows:')
+ print('==========================================\n')
+ print(log)
+ print('==========================================\n')
+
+ def tearDown(self):
+ """
+ Lets do some cleanups.
+ """
+ if self._vm:
+ self.assertFalse(not self._vm.is_running)
+ logging.info('removing the work directory %s', self._workDir)
+ shutil.rmtree(self._workDir)
+ super().tearDown()
+
+ def test_acpi_smbios_bits(self):
+ """The main test case implementaion."""
+
+ iso_file = os.path.join(self._workDir,
+ 'bits-%d.iso' %self._bitsInternalVer)
+
+ 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)
+
+ args = " ".join(str(arg) for arg in self._vm.base_args()) + \
+ " " + " ".join(str(arg) for arg in self._vm.args)
+
+ logging.info("launching QEMU vm with the following arguments: %s",
+ args)
+
+ self._vm.launch()
+ # biosbits has been configured to run all the specified test suites
+ # in batch mode and then automatically initiate a vm shutdown.
+ # sleep for maximum of one minute
+ max_sleep_time = time.monotonic() + 60
+ while self._vm.is_running() and time.monotonic() < max_sleep_time:
+ time.sleep(1)
+
+ self.assertFalse(time.monotonic() > max_sleep_time,
+ 'The VM seems to have failed to shutdown in time')
+
+ self.parse_log()
This introduces QEMU acpi/smbios biosbits avocado test which is run from within the python virtual environment. When the bios bits tests are run, bios bits binaries are downloaded from an external repo/location. Cc: Daniel P. Berrangé <berrange@redhat.com> Cc: Paolo Bonzini <pbonzini@redhat.com> Cc: Maydell Peter <peter.maydell@linaro.org> Cc: John Snow <jsnow@redhat.com> Cc: Thomas Huth <thuth@redhat.com> Signed-off-by: Ani Sinha <ani@anisinha.ca> --- tests/avocado/acpi-bits.py | 334 +++++++++++++++++++++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 tests/avocado/acpi-bits.py