| 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 | """ |
| 27 | This is QEMU ACPI/SMBIOS functional tests using biosbits. |
| 28 | Biosbits is available originally at https://biosbits.org/. |
| 29 | This test uses a fork of the upstream bits and has numerous fixes |
| 30 | including an upgraded acpica. The fork is located here: |
| 31 | https://gitlab.com/qemu-project/biosbits-bits . |
| 32 | """ |
| 33 | |
| 34 | import os |
| 35 | import re |
| 36 | import shutil |
| 37 | import subprocess |
| 38 | |
| 39 | from typing import ( |
| 40 | List, |
| 41 | Optional, |
| 42 | Sequence, |
| 43 | ) |
| 44 | from qemu.machine import QEMUMachine |
| 45 | from qemu_test import (QemuSystemTest, Asset, skipIfMissingCommands, |
| 46 | skipIfNotMachine) |
| 47 | |
| 48 | |
| 49 | # default timeout of 120 secs is sometimes not enough for bits test. |
| 50 | BITS_TIMEOUT = 200 |
| 51 | |
| 52 | class 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 | *, |
| 61 | args: Sequence[str] = (), |
| 62 | wrapper: Sequence[str] = (), |
| 63 | name: Optional[str] = None, |
| 64 | base_temp_dir: str = "/var/tmp", |
| 65 | debugcon_log: str = "debugcon-log.txt", |
| 66 | debugcon_addr: str = "0x403", |
| 67 | qmp_timer: Optional[float] = None): |
| 68 | # pylint: disable=too-many-arguments |
| 69 | |
| 70 | if name is None: |
| 71 | name = "qemu-bits-%d" % os.getpid() |
| 72 | super().__init__(binary, args, wrapper=wrapper, name=name, |
| 73 | base_temp_dir=base_temp_dir, |
| 74 | qmp_timer=qmp_timer) |
| 75 | self.debugcon_log = debugcon_log |
| 76 | self.debugcon_addr = debugcon_addr |
| 77 | self.base_temp_dir = base_temp_dir |
| 78 | |
| 79 | @property |
| 80 | def _base_args(self) -> List[str]: |
| 81 | args = super()._base_args |
| 82 | args.extend([ |
| 83 | '-chardev', |
| 84 | 'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir, |
| 85 | self.debugcon_log), |
| 86 | '-device', |
| 87 | 'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr, |
| 88 | ]) |
| 89 | return args |
| 90 | |
| 91 | def base_args(self): |
| 92 | """return the base argument to QEMU binary""" |
| 93 | return self._base_args |
| 94 | |
| 95 | @skipIfMissingCommands("xorriso", "mformat") |
| 96 | @skipIfNotMachine("x86_64") |
| 97 | class AcpiBitsTest(QemuSystemTest): #pylint: disable=too-many-instance-attributes |
| 98 | """ |
| 99 | ACPI and SMBIOS tests using biosbits. |
| 100 | """ |
| 101 | # in slower systems the test can take as long as 3 minutes to complete. |
| 102 | timeout = BITS_TIMEOUT |
| 103 | |
| 104 | # following are some standard configuration constants |
| 105 | # gitlab CI does shallow clones of depth 20 |
| 106 | BITS_INTERNAL_VER = 2020 |
| 107 | # commit hash must match the artifact tag below |
| 108 | BITS_COMMIT_HASH = 'c7920d2b' |
| 109 | # this is the latest bits release as of today. |
| 110 | BITS_TAG = "qemu-bits-10262023" |
| 111 | |
| 112 | ASSET_BITS = Asset(("https://gitlab.com/qemu-project/" |
| 113 | "biosbits-bits/-/jobs/artifacts/%s/" |
| 114 | "download?job=qemu-bits-build" % BITS_TAG), |
| 115 | '1b8dd612c6831a6b491716a77acc486666aaa867051cdc34f7ce169c2e25f487') |
| 116 | |
| 117 | def __init__(self, *args, **kwargs): |
| 118 | super().__init__(*args, **kwargs) |
| 119 | self._vm = None |
| 120 | |
| 121 | self._debugcon_addr = '0x403' |
| 122 | self._debugcon_log = 'debugcon-log.txt' |
| 123 | |
| 124 | def _print_log(self, log): |
| 125 | self.log.info('\nlogs from biosbits follows:') |
| 126 | self.log.info('==========================================\n') |
| 127 | self.log.info(log) |
| 128 | self.log.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.log.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.log.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.log.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.log.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.log.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.log.info('iso file %s successfully generated.', iso_file) |
| 243 | |
| 244 | def setUp(self): # pylint: disable=arguments-differ |
| 245 | super().setUp() |
| 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 | if os.getenv('V') or os.getenv('BITS_DEBUG'): |
| 292 | self._print_log(log) |
| 293 | |
| 294 | def tearDown(self): |
| 295 | """ |
| 296 | Lets do some cleanups. |
| 297 | """ |
| 298 | if self._vm: |
| 299 | self.assertFalse(not self._vm.is_running) |
| 300 | super().tearDown() |
| 301 | |
| 302 | def test_acpi_smbios_bits(self): |
| 303 | """The main test case implementation.""" |
| 304 | |
| 305 | self.set_machine('pc') |
| 306 | iso_file = self.scratch_file('bits-%d.iso' % self.BITS_INTERNAL_VER) |
| 307 | |
| 308 | self.assertTrue(os.access(iso_file, os.R_OK)) |
| 309 | |
| 310 | self._vm = QEMUBitsMachine(binary=self.qemu_bin, |
| 311 | base_temp_dir=self.workdir, |
| 312 | debugcon_log=self._debugcon_log, |
| 313 | debugcon_addr=self._debugcon_addr) |
| 314 | |
| 315 | self._vm.add_args('-cdrom', '%s' %iso_file) |
| 316 | # the vm needs to be run under icount so that TCG emulation is |
| 317 | # consistent in terms of timing. smilatency tests have consistent |
| 318 | # timing requirements. |
| 319 | self._vm.add_args('-icount', 'auto') |
| 320 | # currently there is no support in bits for recognizing 64-bit SMBIOS |
| 321 | # entry points. QEMU defaults to 64-bit entry points since the |
| 322 | # upstream commit bf376f3020 ("hw/i386/pc: Default to use SMBIOS 3.0 |
| 323 | # for newer machine models"). Therefore, enforce 32-bit entry point. |
| 324 | self._vm.add_args('-machine', 'smbios-entry-point-type=32') |
| 325 | |
| 326 | # enable console logging |
| 327 | self._vm.set_console() |
| 328 | self._vm.launch() |
| 329 | |
| 330 | |
| 331 | # biosbits has been configured to run all the specified test suites |
| 332 | # in batch mode and then automatically initiate a vm shutdown. |
| 333 | self._vm.event_wait('SHUTDOWN', timeout=BITS_TIMEOUT) |
| 334 | self._vm.wait(timeout=None) |
| 335 | self.log.debug("Checking console output ...") |
| 336 | self.parse_log() |
| 337 | |
| 338 | if __name__ == '__main__': |
| 339 | QemuSystemTest.main() |