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 123 def _print_log(self, log): 124 self.logger.info('\nlogs from biosbits follows:') 125 self.logger.info('==========================================\n') 126 self.logger.info(log) 127 self.logger.info('==========================================\n') 128 129 def copy_bits_config(self): 130 """ copies the bios bits config file into bits. 131 """ 132 bits_config_file = self.data_file('acpi-bits', 133 'bits-config', 134 'bits-cfg.txt') 135 target_config_dir = self.scratch_file('bits-%d' % 136 self.BITS_INTERNAL_VER, 137 'boot') 138 self.assertTrue(os.path.exists(bits_config_file)) 139 self.assertTrue(os.path.exists(target_config_dir)) 140 shutil.copy2(bits_config_file, target_config_dir) 141 self.logger.info('copied config file %s to %s', 142 bits_config_file, target_config_dir) 143 144 def copy_test_scripts(self): 145 """copies the python test scripts into bits. """ 146 147 bits_test_dir = self.data_file('acpi-bits', 'bits-tests') 148 target_test_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER, 149 'boot', 'python') 150 151 self.assertTrue(os.path.exists(bits_test_dir)) 152 self.assertTrue(os.path.exists(target_test_dir)) 153 154 for filename in os.listdir(bits_test_dir): 155 if os.path.isfile(os.path.join(bits_test_dir, filename)) and \ 156 filename.endswith('.py2'): 157 # All test scripts are named with extension .py2 so that 158 # they are not run by accident. 159 # 160 # These scripts are intended to run inside the test VM 161 # and are written for python 2.7 not python 3, hence 162 # would cause syntax errors if loaded ouside the VM. 163 newfilename = os.path.splitext(filename)[0] + '.py' 164 shutil.copy2(os.path.join(bits_test_dir, filename), 165 os.path.join(target_test_dir, newfilename)) 166 self.logger.info('copied test file %s to %s', 167 filename, target_test_dir) 168 169 # now remove the pyc test file if it exists, otherwise the 170 # changes in the python test script won't be executed. 171 testfile_pyc = os.path.splitext(filename)[0] + '.pyc' 172 if os.access(os.path.join(target_test_dir, testfile_pyc), 173 os.F_OK): 174 os.remove(os.path.join(target_test_dir, testfile_pyc)) 175 self.logger.info('removed compiled file %s', 176 os.path.join(target_test_dir, 177 testfile_pyc)) 178 179 def fix_mkrescue(self, mkrescue): 180 """ grub-mkrescue is a bash script with two variables, 'prefix' and 181 'libdir'. They must be pointed to the right location so that the 182 iso can be generated appropriately. We point the two variables to 183 the directory where we have extracted our pre-built bits grub 184 tarball. 185 """ 186 grub_x86_64_mods = self.scratch_file('grub-inst-x86_64-efi') 187 grub_i386_mods = self.scratch_file('grub-inst') 188 189 self.assertTrue(os.path.exists(grub_x86_64_mods)) 190 self.assertTrue(os.path.exists(grub_i386_mods)) 191 192 new_script = "" 193 with open(mkrescue, 'r', encoding='utf-8') as filehandle: 194 orig_script = filehandle.read() 195 new_script = re.sub('(^prefix=)(.*)', 196 r'\1"%s"' %grub_x86_64_mods, 197 orig_script, flags=re.M) 198 new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods, 199 new_script, flags=re.M) 200 201 with open(mkrescue, 'w', encoding='utf-8') as filehandle: 202 filehandle.write(new_script) 203 204 def generate_bits_iso(self): 205 """ Uses grub-mkrescue to generate a fresh bits iso with the python 206 test scripts 207 """ 208 bits_dir = self.scratch_file('bits-%d' % self.BITS_INTERNAL_VER) 209 iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER) 210 mkrescue_script = self.scratch_file('grub-inst-x86_64-efi', 211 'bin', 212 'grub-mkrescue') 213 214 self.assertTrue(os.access(mkrescue_script, 215 os.R_OK | os.W_OK | os.X_OK)) 216 217 self.fix_mkrescue(mkrescue_script) 218 219 self.logger.info('using grub-mkrescue for generating biosbits iso ...') 220 221 try: 222 if os.getenv('V') or os.getenv('BITS_DEBUG'): 223 proc = subprocess.run([mkrescue_script, '-o', iso_file, 224 bits_dir], 225 stdout=subprocess.PIPE, 226 stderr=subprocess.STDOUT, 227 check=True) 228 self.logger.info("grub-mkrescue output %s" % proc.stdout) 229 else: 230 subprocess.check_call([mkrescue_script, '-o', 231 iso_file, bits_dir], 232 stderr=subprocess.DEVNULL, 233 stdout=subprocess.DEVNULL) 234 except Exception as e: # pylint: disable=broad-except 235 self.skipTest("Error while generating the bits iso. " 236 "Pass V=1 in the environment to get more details. " 237 + str(e)) 238 239 self.assertTrue(os.access(iso_file, os.R_OK)) 240 241 self.logger.info('iso file %s successfully generated.', iso_file) 242 243 def setUp(self): # pylint: disable=arguments-differ 244 super().setUp() 245 self.logger = self.log 246 247 prebuiltDir = self.scratch_file('prebuilt') 248 if not os.path.isdir(prebuiltDir): 249 os.mkdir(prebuiltDir, mode=0o775) 250 251 bits_zip_file = self.scratch_file('prebuilt', 252 'bits-%d-%s.zip' 253 %(self.BITS_INTERNAL_VER, 254 self.BITS_COMMIT_HASH)) 255 grub_tar_file = self.scratch_file('prebuilt', 256 'bits-%d-%s-grub.tar.gz' 257 %(self.BITS_INTERNAL_VER, 258 self.BITS_COMMIT_HASH)) 259 260 # extract the bits artifact in the temp working directory 261 self.archive_extract(self.ASSET_BITS, sub_dir='prebuilt', format='zip') 262 263 # extract the bits software in the temp working directory 264 self.archive_extract(bits_zip_file) 265 self.archive_extract(grub_tar_file) 266 267 self.copy_test_scripts() 268 self.copy_bits_config() 269 self.generate_bits_iso() 270 271 def parse_log(self): 272 """parse the log generated by running bits tests and 273 check for failures. 274 """ 275 debugconf = self.scratch_file(self._debugcon_log) 276 log = "" 277 with open(debugconf, 'r', encoding='utf-8') as filehandle: 278 log = filehandle.read() 279 280 matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*', 281 log) 282 for match in matchiter: 283 # verify that no test cases failed. 284 try: 285 self.assertEqual(match.group(3).split()[0], '0', 286 'Some bits tests seems to have failed. ' \ 287 'Please check the test logs for more info.') 288 except AssertionError as e: 289 self._print_log(log) 290 raise e 291 else: 292 if os.getenv('V') or os.getenv('BITS_DEBUG'): 293 self._print_log(log) 294 295 def tearDown(self): 296 """ 297 Lets do some cleanups. 298 """ 299 if self._vm: 300 self.assertFalse(not self._vm.is_running) 301 super().tearDown() 302 303 def test_acpi_smbios_bits(self): 304 """The main test case implementation.""" 305 306 self.set_machine('pc') 307 iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER) 308 309 self.assertTrue(os.access(iso_file, os.R_OK)) 310 311 self._vm = QEMUBitsMachine(binary=self.qemu_bin, 312 base_temp_dir=self.workdir, 313 debugcon_log=self._debugcon_log, 314 debugcon_addr=self._debugcon_addr) 315 316 self._vm.add_args('-cdrom', '%s' %iso_file) 317 # the vm needs to be run under icount so that TCG emulation is 318 # consistent in terms of timing. smilatency tests have consistent 319 # timing requirements. 320 self._vm.add_args('-icount', 'auto') 321 # currently there is no support in bits for recognizing 64-bit SMBIOS 322 # entry points. QEMU defaults to 64-bit entry points since the 323 # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 324 # for newer machine models"). Therefore, enforce 32-bit entry point. 325 self._vm.add_args('-machine', 'smbios-entry-point-type=32') 326 327 # enable console logging 328 self._vm.set_console() 329 self._vm.launch() 330 331 332 # biosbits has been configured to run all the specified test suites 333 # in batch mode and then automatically initiate a vm shutdown. 334 self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT) 335 self._vm.wait(timeout=None) 336 self.logger.debug("Checking console output ...") 337 self.parse_log() 338 339if __name__ == '__main__': 340 QemuSystemTest.main() 341