qemu

FORK: QEMU emulator
git clone https://git.neptards.moe/neptards/qemu.git
Log | Files | Refs | Submodules | LICENSE

acpi-bits.py (16301B)


      1 #!/usr/bin/env python3
      2 # group: rw quick
      3 # Exercize 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 <ani@anisinha.ca>
     22 
     23 # pylint: disable=invalid-name
     24 # pylint: disable=consider-using-f-string
     25 
     26 """
     27 This is QEMU ACPI/SMBIOS avocado tests using biosbits.
     28 Biosbits is available originally at https://biosbits.org/.
     29 This test uses a fork of the upstream bits and has numerous fixes
     30 including an upgraded acpica. The fork is located here:
     31 https://gitlab.com/qemu-project/biosbits-bits .
     32 """
     33 
     34 import logging
     35 import os
     36 import platform
     37 import re
     38 import shutil
     39 import subprocess
     40 import tarfile
     41 import tempfile
     42 import time
     43 import zipfile
     44 from typing import (
     45     List,
     46     Optional,
     47     Sequence,
     48 )
     49 from qemu.machine import QEMUMachine
     50 from avocado import skipIf
     51 from avocado_qemu import QemuBaseTest
     52 
     53 deps = ["xorriso"] # dependent tools needed in the test setup/box.
     54 supported_platforms = ['x86_64'] # supported test platforms.
     55 
     56 
     57 def which(tool):
     58     """ looks up the full path for @tool, returns None if not found
     59         or if @tool does not have executable permissions.
     60     """
     61     paths=os.getenv('PATH')
     62     for p in paths.split(os.path.pathsep):
     63         p = os.path.join(p, tool)
     64         if os.path.exists(p) and os.access(p, os.X_OK):
     65             return p
     66     return None
     67 
     68 def missing_deps():
     69     """ returns True if any of the test dependent tools are absent.
     70     """
     71     for dep in deps:
     72         if which(dep) is None:
     73             return True
     74     return False
     75 
     76 def supported_platform():
     77     """ checks if the test is running on a supported platform.
     78     """
     79     return platform.machine() in supported_platforms
     80 
     81 class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods
     82     """
     83     A QEMU VM, with isa-debugcon enabled and bits iso passed
     84     using -cdrom to QEMU commandline.
     85 
     86     """
     87     def __init__(self,
     88                  binary: str,
     89                  args: Sequence[str] = (),
     90                  wrapper: Sequence[str] = (),
     91                  name: Optional[str] = None,
     92                  base_temp_dir: str = "/var/tmp",
     93                  debugcon_log: str = "debugcon-log.txt",
     94                  debugcon_addr: str = "0x403",
     95                  sock_dir: Optional[str] = None,
     96                  qmp_timer: Optional[float] = None):
     97         # pylint: disable=too-many-arguments
     98 
     99         if name is None:
    100             name = "qemu-bits-%d" % os.getpid()
    101         if sock_dir is None:
    102             sock_dir = base_temp_dir
    103         super().__init__(binary, args, wrapper=wrapper, name=name,
    104                          base_temp_dir=base_temp_dir,
    105                          sock_dir=sock_dir, qmp_timer=qmp_timer)
    106         self.debugcon_log = debugcon_log
    107         self.debugcon_addr = debugcon_addr
    108         self.base_temp_dir = base_temp_dir
    109 
    110     @property
    111     def _base_args(self) -> List[str]:
    112         args = super()._base_args
    113         args.extend([
    114             '-chardev',
    115             'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir,
    116                                                      self.debugcon_log),
    117             '-device',
    118             'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr,
    119         ])
    120         return args
    121 
    122     def base_args(self):
    123         """return the base argument to QEMU binary"""
    124         return self._base_args
    125 
    126 @skipIf(not supported_platform() or missing_deps() or os.getenv('GITLAB_CI'),
    127         'incorrect platform or dependencies (%s) not installed ' \
    128         'or running on GitLab' % ','.join(deps))
    129 class AcpiBitsTest(QemuBaseTest): #pylint: disable=too-many-instance-attributes
    130     """
    131     ACPI and SMBIOS tests using biosbits.
    132 
    133     :avocado: tags=arch:x86_64
    134     :avocado: tags=acpi
    135 
    136     """
    137     # in slower systems the test can take as long as 3 minutes to complete.
    138     timeout = 200
    139 
    140     def __init__(self, *args, **kwargs):
    141         super().__init__(*args, **kwargs)
    142         self._vm = None
    143         self._workDir = None
    144         self._baseDir = None
    145 
    146         # following are some standard configuration constants
    147         self._bitsInternalVer = 2020
    148         self._bitsCommitHash = 'b48b88ff' # commit hash must match
    149                                           # the artifact tag below
    150         self._bitsTag = "qemu-bits-10182022" # this is the latest bits
    151                                              # release as of today.
    152         self._bitsArtSHA1Hash = 'b04790ac9b99b5662d0416392c73b97580641fe5'
    153         self._bitsArtURL = ("https://gitlab.com/qemu-project/"
    154                             "biosbits-bits/-/jobs/artifacts/%s/"
    155                             "download?job=qemu-bits-build" %self._bitsTag)
    156         self._debugcon_addr = '0x403'
    157         self._debugcon_log = 'debugcon-log.txt'
    158         logging.basicConfig(level=logging.INFO)
    159         self.logger = logging.getLogger('acpi-bits')
    160 
    161     def _print_log(self, log):
    162         self.logger.info('\nlogs from biosbits follows:')
    163         self.logger.info('==========================================\n')
    164         self.logger.info(log)
    165         self.logger.info('==========================================\n')
    166 
    167     def copy_bits_config(self):
    168         """ copies the bios bits config file into bits.
    169         """
    170         config_file = 'bits-cfg.txt'
    171         bits_config_dir = os.path.join(self._baseDir, 'acpi-bits',
    172                                        'bits-config')
    173         target_config_dir = os.path.join(self._workDir,
    174                                          'bits-%d' %self._bitsInternalVer,
    175                                          'boot')
    176         self.assertTrue(os.path.exists(bits_config_dir))
    177         self.assertTrue(os.path.exists(target_config_dir))
    178         self.assertTrue(os.access(os.path.join(bits_config_dir,
    179                                                config_file), os.R_OK))
    180         shutil.copy2(os.path.join(bits_config_dir, config_file),
    181                      target_config_dir)
    182         self.logger.info('copied config file %s to %s',
    183                          config_file, target_config_dir)
    184 
    185     def copy_test_scripts(self):
    186         """copies the python test scripts into bits. """
    187 
    188         bits_test_dir = os.path.join(self._baseDir, 'acpi-bits',
    189                                      'bits-tests')
    190         target_test_dir = os.path.join(self._workDir,
    191                                        'bits-%d' %self._bitsInternalVer,
    192                                        'boot', 'python')
    193 
    194         self.assertTrue(os.path.exists(bits_test_dir))
    195         self.assertTrue(os.path.exists(target_test_dir))
    196 
    197         for filename in os.listdir(bits_test_dir):
    198             if os.path.isfile(os.path.join(bits_test_dir, filename)) and \
    199                filename.endswith('.py2'):
    200                 # all test scripts are named with extension .py2 so that
    201                 # avocado does not try to load them. These scripts are
    202                 # written for python 2.7 not python 3 and hence if avocado
    203                 # loaded them, it would complain about python 3 specific
    204                 # syntaxes.
    205                 newfilename = os.path.splitext(filename)[0] + '.py'
    206                 shutil.copy2(os.path.join(bits_test_dir, filename),
    207                              os.path.join(target_test_dir, newfilename))
    208                 self.logger.info('copied test file %s to %s',
    209                                  filename, target_test_dir)
    210 
    211                 # now remove the pyc test file if it exists, otherwise the
    212                 # changes in the python test script won't be executed.
    213                 testfile_pyc = os.path.splitext(filename)[0] + '.pyc'
    214                 if os.access(os.path.join(target_test_dir, testfile_pyc),
    215                              os.F_OK):
    216                     os.remove(os.path.join(target_test_dir, testfile_pyc))
    217                     self.logger.info('removed compiled file %s',
    218                                      os.path.join(target_test_dir,
    219                                      testfile_pyc))
    220 
    221     def fix_mkrescue(self, mkrescue):
    222         """ grub-mkrescue is a bash script with two variables, 'prefix' and
    223             'libdir'. They must be pointed to the right location so that the
    224             iso can be generated appropriately. We point the two variables to
    225             the directory where we have extracted our pre-built bits grub
    226             tarball.
    227         """
    228         grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi')
    229         grub_i386_mods = os.path.join(self._workDir, 'grub-inst')
    230 
    231         self.assertTrue(os.path.exists(grub_x86_64_mods))
    232         self.assertTrue(os.path.exists(grub_i386_mods))
    233 
    234         new_script = ""
    235         with open(mkrescue, 'r', encoding='utf-8') as filehandle:
    236             orig_script = filehandle.read()
    237             new_script = re.sub('(^prefix=)(.*)',
    238                                 r'\1"%s"' %grub_x86_64_mods,
    239                                 orig_script, flags=re.M)
    240             new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods,
    241                                 new_script, flags=re.M)
    242 
    243         with open(mkrescue, 'w', encoding='utf-8') as filehandle:
    244             filehandle.write(new_script)
    245 
    246     def generate_bits_iso(self):
    247         """ Uses grub-mkrescue to generate a fresh bits iso with the python
    248             test scripts
    249         """
    250         bits_dir = os.path.join(self._workDir,
    251                                 'bits-%d' %self._bitsInternalVer)
    252         iso_file = os.path.join(self._workDir,
    253                                 'bits-%d.iso' %self._bitsInternalVer)
    254         mkrescue_script = os.path.join(self._workDir,
    255                                        'grub-inst-x86_64-efi', 'bin',
    256                                        'grub-mkrescue')
    257 
    258         self.assertTrue(os.access(mkrescue_script,
    259                                   os.R_OK | os.W_OK | os.X_OK))
    260 
    261         self.fix_mkrescue(mkrescue_script)
    262 
    263         self.logger.info('using grub-mkrescue for generating biosbits iso ...')
    264 
    265         try:
    266             if os.getenv('V') or os.getenv('BITS_DEBUG'):
    267                 subprocess.check_call([mkrescue_script, '-o', iso_file,
    268                                        bits_dir], stderr=subprocess.STDOUT)
    269             else:
    270                 subprocess.check_call([mkrescue_script, '-o',
    271                                       iso_file, bits_dir],
    272                                       stderr=subprocess.DEVNULL,
    273                                       stdout=subprocess.DEVNULL)
    274         except Exception as e: # pylint: disable=broad-except
    275             self.skipTest("Error while generating the bits iso. "
    276                           "Pass V=1 in the environment to get more details. "
    277                           + str(e))
    278 
    279         self.assertTrue(os.access(iso_file, os.R_OK))
    280 
    281         self.logger.info('iso file %s successfully generated.', iso_file)
    282 
    283     def setUp(self): # pylint: disable=arguments-differ
    284         super().setUp('qemu-system-')
    285 
    286         self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR')
    287 
    288         # workdir could also be avocado's own workdir in self.workdir.
    289         # At present, I prefer to maintain my own temporary working
    290         # directory. It gives us more control over the generated bits
    291         # log files and also for debugging, we may chose not to remove
    292         # this working directory so that the logs and iso can be
    293         # inspected manually and archived if needed.
    294         self._workDir = tempfile.mkdtemp(prefix='acpi-bits-',
    295                                          suffix='.tmp')
    296         self.logger.info('working dir: %s', self._workDir)
    297 
    298         prebuiltDir = os.path.join(self._workDir, 'prebuilt')
    299         if not os.path.isdir(prebuiltDir):
    300             os.mkdir(prebuiltDir, mode=0o775)
    301 
    302         bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip'
    303                                      %(self._bitsInternalVer,
    304                                        self._bitsCommitHash))
    305         grub_tar_file = os.path.join(prebuiltDir,
    306                                      'bits-%d-%s-grub.tar.gz'
    307                                      %(self._bitsInternalVer,
    308                                        self._bitsCommitHash))
    309 
    310         bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL,
    311                                            asset_hash=self._bitsArtSHA1Hash)
    312         self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc)
    313 
    314         # extract the bits artifact in the temp working directory
    315         with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref:
    316             zref.extractall(prebuiltDir)
    317 
    318         # extract the bits software in the temp working directory
    319         with zipfile.ZipFile(bits_zip_file, 'r') as zref:
    320             zref.extractall(self._workDir)
    321 
    322         with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball:
    323             tarball.extractall(self._workDir)
    324 
    325         self.copy_test_scripts()
    326         self.copy_bits_config()
    327         self.generate_bits_iso()
    328 
    329     def parse_log(self):
    330         """parse the log generated by running bits tests and
    331            check for failures.
    332         """
    333         debugconf = os.path.join(self._workDir, self._debugcon_log)
    334         log = ""
    335         with open(debugconf, 'r', encoding='utf-8') as filehandle:
    336             log = filehandle.read()
    337 
    338         matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*',
    339                                 log)
    340         for match in matchiter:
    341             # verify that no test cases failed.
    342             try:
    343                 self.assertEqual(match.group(3).split()[0], '0',
    344                                  'Some bits tests seems to have failed. ' \
    345                                  'Please check the test logs for more info.')
    346             except AssertionError as e:
    347                 self._print_log(log)
    348                 raise e
    349             else:
    350                 if os.getenv('V') or os.getenv('BITS_DEBUG'):
    351                     self._print_log(log)
    352 
    353     def tearDown(self):
    354         """
    355            Lets do some cleanups.
    356         """
    357         if self._vm:
    358             self.assertFalse(not self._vm.is_running)
    359         if not os.getenv('BITS_DEBUG'):
    360             self.logger.info('removing the work directory %s', self._workDir)
    361             shutil.rmtree(self._workDir)
    362         else:
    363             self.logger.info('not removing the work directory %s ' \
    364                              'as BITS_DEBUG is ' \
    365                              'passed in the environment', self._workDir)
    366         super().tearDown()
    367 
    368     def test_acpi_smbios_bits(self):
    369         """The main test case implementaion."""
    370 
    371         iso_file = os.path.join(self._workDir,
    372                                 'bits-%d.iso' %self._bitsInternalVer)
    373 
    374         self.assertTrue(os.access(iso_file, os.R_OK))
    375 
    376         self._vm = QEMUBitsMachine(binary=self.qemu_bin,
    377                                    base_temp_dir=self._workDir,
    378                                    debugcon_log=self._debugcon_log,
    379                                    debugcon_addr=self._debugcon_addr)
    380 
    381         self._vm.add_args('-cdrom', '%s' %iso_file)
    382         # the vm needs to be run under icount so that TCG emulation is
    383         # consistent in terms of timing. smilatency tests have consistent
    384         # timing requirements.
    385         self._vm.add_args('-icount', 'auto')
    386 
    387         args = " ".join(str(arg) for arg in self._vm.base_args()) + \
    388             " " + " ".join(str(arg) for arg in self._vm.args)
    389 
    390         self.logger.info("launching QEMU vm with the following arguments: %s",
    391                          args)
    392 
    393         self._vm.launch()
    394         # biosbits has been configured to run all the specified test suites
    395         # in batch mode and then automatically initiate a vm shutdown.
    396         # Rely on avocado's unit test timeout.
    397         self._vm.wait(timeout=None)
    398         self.parse_log()