xref: /qemu/tests/functional/test_acpi_bits.py (revision 5e03548bf222b0f8038dc61dbf0c93bd062025aa)
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                  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")
96 class 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 
339 if __name__ == '__main__':
340     QemuSystemTest.main()
341