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