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