master
py 339 lines 13.5 KB
Raw
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()