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