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