]> Git Repo - qemu.git/blob - tests/avocado/acpi-bits.py
works with less than base ISA qemu-system-riscv32 -M virt -bios none -kernel output...
[qemu.git] / tests / avocado / acpi-bits.py
1 #!/usr/bin/env python3
2 # group: rw quick
3 # Exercize 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 <[email protected]>
22
23 # pylint: disable=invalid-name
24 # pylint: disable=consider-using-f-string
25
26 """
27 This is QEMU ACPI/SMBIOS avocado 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 logging
35 import os
36 import platform
37 import re
38 import shutil
39 import subprocess
40 import tarfile
41 import tempfile
42 import time
43 import zipfile
44 from typing import (
45     List,
46     Optional,
47     Sequence,
48 )
49 from qemu.machine import QEMUMachine
50 from avocado import skipIf
51 from avocado_qemu import QemuBaseTest
52
53 deps = ["xorriso", "mformat"] # dependent tools needed in the test setup/box.
54 supported_platforms = ['x86_64'] # supported test platforms.
55
56
57 def which(tool):
58     """ looks up the full path for @tool, returns None if not found
59         or if @tool does not have executable permissions.
60     """
61     paths=os.getenv('PATH')
62     for p in paths.split(os.path.pathsep):
63         p = os.path.join(p, tool)
64         if os.path.exists(p) and os.access(p, os.X_OK):
65             return p
66     return None
67
68 def missing_deps():
69     """ returns True if any of the test dependent tools are absent.
70     """
71     for dep in deps:
72         if which(dep) is None:
73             return True
74     return False
75
76 def supported_platform():
77     """ checks if the test is running on a supported platform.
78     """
79     return platform.machine() in supported_platforms
80
81 class QEMUBitsMachine(QEMUMachine): # pylint: disable=too-few-public-methods
82     """
83     A QEMU VM, with isa-debugcon enabled and bits iso passed
84     using -cdrom to QEMU commandline.
85
86     """
87     def __init__(self,
88                  binary: str,
89                  args: Sequence[str] = (),
90                  wrapper: Sequence[str] = (),
91                  name: Optional[str] = None,
92                  base_temp_dir: str = "/var/tmp",
93                  debugcon_log: str = "debugcon-log.txt",
94                  debugcon_addr: str = "0x403",
95                  sock_dir: Optional[str] = None,
96                  qmp_timer: Optional[float] = None):
97         # pylint: disable=too-many-arguments
98
99         if name is None:
100             name = "qemu-bits-%d" % os.getpid()
101         if sock_dir is None:
102             sock_dir = base_temp_dir
103         super().__init__(binary, args, wrapper=wrapper, name=name,
104                          base_temp_dir=base_temp_dir,
105                          sock_dir=sock_dir, qmp_timer=qmp_timer)
106         self.debugcon_log = debugcon_log
107         self.debugcon_addr = debugcon_addr
108         self.base_temp_dir = base_temp_dir
109
110     @property
111     def _base_args(self) -> List[str]:
112         args = super()._base_args
113         args.extend([
114             '-chardev',
115             'file,path=%s,id=debugcon' %os.path.join(self.base_temp_dir,
116                                                      self.debugcon_log),
117             '-device',
118             'isa-debugcon,iobase=%s,chardev=debugcon' %self.debugcon_addr,
119         ])
120         return args
121
122     def base_args(self):
123         """return the base argument to QEMU binary"""
124         return self._base_args
125
126 @skipIf(not supported_platform() or missing_deps() or os.getenv('GITLAB_CI'),
127         'incorrect platform or dependencies (%s) not installed ' \
128         'or running on GitLab' % ','.join(deps))
129 class AcpiBitsTest(QemuBaseTest): #pylint: disable=too-many-instance-attributes
130     """
131     ACPI and SMBIOS tests using biosbits.
132
133     :avocado: tags=arch:x86_64
134     :avocado: tags=acpi
135
136     """
137     # in slower systems the test can take as long as 3 minutes to complete.
138     timeout = 200
139
140     def __init__(self, *args, **kwargs):
141         super().__init__(*args, **kwargs)
142         self._vm = None
143         self._workDir = None
144         self._baseDir = None
145
146         # following are some standard configuration constants
147         self._bitsInternalVer = 2020
148         self._bitsCommitHash = 'b48b88ff' # commit hash must match
149                                           # the artifact tag below
150         self._bitsTag = "qemu-bits-10182022" # this is the latest bits
151                                              # release as of today.
152         self._bitsArtSHA1Hash = 'b04790ac9b99b5662d0416392c73b97580641fe5'
153         self._bitsArtURL = ("https://gitlab.com/qemu-project/"
154                             "biosbits-bits/-/jobs/artifacts/%s/"
155                             "download?job=qemu-bits-build" %self._bitsTag)
156         self._debugcon_addr = '0x403'
157         self._debugcon_log = 'debugcon-log.txt'
158         logging.basicConfig(level=logging.INFO)
159         self.logger = logging.getLogger('acpi-bits')
160
161     def _print_log(self, log):
162         self.logger.info('\nlogs from biosbits follows:')
163         self.logger.info('==========================================\n')
164         self.logger.info(log)
165         self.logger.info('==========================================\n')
166
167     def copy_bits_config(self):
168         """ copies the bios bits config file into bits.
169         """
170         config_file = 'bits-cfg.txt'
171         bits_config_dir = os.path.join(self._baseDir, 'acpi-bits',
172                                        'bits-config')
173         target_config_dir = os.path.join(self._workDir,
174                                          'bits-%d' %self._bitsInternalVer,
175                                          'boot')
176         self.assertTrue(os.path.exists(bits_config_dir))
177         self.assertTrue(os.path.exists(target_config_dir))
178         self.assertTrue(os.access(os.path.join(bits_config_dir,
179                                                config_file), os.R_OK))
180         shutil.copy2(os.path.join(bits_config_dir, config_file),
181                      target_config_dir)
182         self.logger.info('copied config file %s to %s',
183                          config_file, target_config_dir)
184
185     def copy_test_scripts(self):
186         """copies the python test scripts into bits. """
187
188         bits_test_dir = os.path.join(self._baseDir, 'acpi-bits',
189                                      'bits-tests')
190         target_test_dir = os.path.join(self._workDir,
191                                        'bits-%d' %self._bitsInternalVer,
192                                        'boot', 'python')
193
194         self.assertTrue(os.path.exists(bits_test_dir))
195         self.assertTrue(os.path.exists(target_test_dir))
196
197         for filename in os.listdir(bits_test_dir):
198             if os.path.isfile(os.path.join(bits_test_dir, filename)) and \
199                filename.endswith('.py2'):
200                 # all test scripts are named with extension .py2 so that
201                 # avocado does not try to load them. These scripts are
202                 # written for python 2.7 not python 3 and hence if avocado
203                 # loaded them, it would complain about python 3 specific
204                 # syntaxes.
205                 newfilename = os.path.splitext(filename)[0] + '.py'
206                 shutil.copy2(os.path.join(bits_test_dir, filename),
207                              os.path.join(target_test_dir, newfilename))
208                 self.logger.info('copied test file %s to %s',
209                                  filename, target_test_dir)
210
211                 # now remove the pyc test file if it exists, otherwise the
212                 # changes in the python test script won't be executed.
213                 testfile_pyc = os.path.splitext(filename)[0] + '.pyc'
214                 if os.access(os.path.join(target_test_dir, testfile_pyc),
215                              os.F_OK):
216                     os.remove(os.path.join(target_test_dir, testfile_pyc))
217                     self.logger.info('removed compiled file %s',
218                                      os.path.join(target_test_dir,
219                                      testfile_pyc))
220
221     def fix_mkrescue(self, mkrescue):
222         """ grub-mkrescue is a bash script with two variables, 'prefix' and
223             'libdir'. They must be pointed to the right location so that the
224             iso can be generated appropriately. We point the two variables to
225             the directory where we have extracted our pre-built bits grub
226             tarball.
227         """
228         grub_x86_64_mods = os.path.join(self._workDir, 'grub-inst-x86_64-efi')
229         grub_i386_mods = os.path.join(self._workDir, 'grub-inst')
230
231         self.assertTrue(os.path.exists(grub_x86_64_mods))
232         self.assertTrue(os.path.exists(grub_i386_mods))
233
234         new_script = ""
235         with open(mkrescue, 'r', encoding='utf-8') as filehandle:
236             orig_script = filehandle.read()
237             new_script = re.sub('(^prefix=)(.*)',
238                                 r'\1"%s"' %grub_x86_64_mods,
239                                 orig_script, flags=re.M)
240             new_script = re.sub('(^libdir=)(.*)', r'\1"%s/lib"' %grub_i386_mods,
241                                 new_script, flags=re.M)
242
243         with open(mkrescue, 'w', encoding='utf-8') as filehandle:
244             filehandle.write(new_script)
245
246     def generate_bits_iso(self):
247         """ Uses grub-mkrescue to generate a fresh bits iso with the python
248             test scripts
249         """
250         bits_dir = os.path.join(self._workDir,
251                                 'bits-%d' %self._bitsInternalVer)
252         iso_file = os.path.join(self._workDir,
253                                 'bits-%d.iso' %self._bitsInternalVer)
254         mkrescue_script = os.path.join(self._workDir,
255                                        'grub-inst-x86_64-efi', 'bin',
256                                        'grub-mkrescue')
257
258         self.assertTrue(os.access(mkrescue_script,
259                                   os.R_OK | os.W_OK | os.X_OK))
260
261         self.fix_mkrescue(mkrescue_script)
262
263         self.logger.info('using grub-mkrescue for generating biosbits iso ...')
264
265         try:
266             if os.getenv('V') or os.getenv('BITS_DEBUG'):
267                 subprocess.check_call([mkrescue_script, '-o', iso_file,
268                                        bits_dir], stderr=subprocess.STDOUT)
269             else:
270                 subprocess.check_call([mkrescue_script, '-o',
271                                       iso_file, bits_dir],
272                                       stderr=subprocess.DEVNULL,
273                                       stdout=subprocess.DEVNULL)
274         except Exception as e: # pylint: disable=broad-except
275             self.skipTest("Error while generating the bits iso. "
276                           "Pass V=1 in the environment to get more details. "
277                           + str(e))
278
279         self.assertTrue(os.access(iso_file, os.R_OK))
280
281         self.logger.info('iso file %s successfully generated.', iso_file)
282
283     def setUp(self): # pylint: disable=arguments-differ
284         super().setUp('qemu-system-')
285
286         self._baseDir = os.getenv('AVOCADO_TEST_BASEDIR')
287
288         # workdir could also be avocado's own workdir in self.workdir.
289         # At present, I prefer to maintain my own temporary working
290         # directory. It gives us more control over the generated bits
291         # log files and also for debugging, we may chose not to remove
292         # this working directory so that the logs and iso can be
293         # inspected manually and archived if needed.
294         self._workDir = tempfile.mkdtemp(prefix='acpi-bits-',
295                                          suffix='.tmp')
296         self.logger.info('working dir: %s', self._workDir)
297
298         prebuiltDir = os.path.join(self._workDir, 'prebuilt')
299         if not os.path.isdir(prebuiltDir):
300             os.mkdir(prebuiltDir, mode=0o775)
301
302         bits_zip_file = os.path.join(prebuiltDir, 'bits-%d-%s.zip'
303                                      %(self._bitsInternalVer,
304                                        self._bitsCommitHash))
305         grub_tar_file = os.path.join(prebuiltDir,
306                                      'bits-%d-%s-grub.tar.gz'
307                                      %(self._bitsInternalVer,
308                                        self._bitsCommitHash))
309
310         bitsLocalArtLoc = self.fetch_asset(self._bitsArtURL,
311                                            asset_hash=self._bitsArtSHA1Hash)
312         self.logger.info("downloaded bits artifacts to %s", bitsLocalArtLoc)
313
314         # extract the bits artifact in the temp working directory
315         with zipfile.ZipFile(bitsLocalArtLoc, 'r') as zref:
316             zref.extractall(prebuiltDir)
317
318         # extract the bits software in the temp working directory
319         with zipfile.ZipFile(bits_zip_file, 'r') as zref:
320             zref.extractall(self._workDir)
321
322         with tarfile.open(grub_tar_file, 'r', encoding='utf-8') as tarball:
323             tarball.extractall(self._workDir)
324
325         self.copy_test_scripts()
326         self.copy_bits_config()
327         self.generate_bits_iso()
328
329     def parse_log(self):
330         """parse the log generated by running bits tests and
331            check for failures.
332         """
333         debugconf = os.path.join(self._workDir, self._debugcon_log)
334         log = ""
335         with open(debugconf, 'r', encoding='utf-8') as filehandle:
336             log = filehandle.read()
337
338         matchiter = re.finditer(r'(.*Summary: )(\d+ passed), (\d+ failed).*',
339                                 log)
340         for match in matchiter:
341             # verify that no test cases failed.
342             try:
343                 self.assertEqual(match.group(3).split()[0], '0',
344                                  'Some bits tests seems to have failed. ' \
345                                  'Please check the test logs for more info.')
346             except AssertionError as e:
347                 self._print_log(log)
348                 raise e
349             else:
350                 if os.getenv('V') or os.getenv('BITS_DEBUG'):
351                     self._print_log(log)
352
353     def tearDown(self):
354         """
355            Lets do some cleanups.
356         """
357         if self._vm:
358             self.assertFalse(not self._vm.is_running)
359         if not os.getenv('BITS_DEBUG'):
360             self.logger.info('removing the work directory %s', self._workDir)
361             shutil.rmtree(self._workDir)
362         else:
363             self.logger.info('not removing the work directory %s ' \
364                              'as BITS_DEBUG is ' \
365                              'passed in the environment', self._workDir)
366         super().tearDown()
367
368     def test_acpi_smbios_bits(self):
369         """The main test case implementaion."""
370
371         iso_file = os.path.join(self._workDir,
372                                 'bits-%d.iso' %self._bitsInternalVer)
373
374         self.assertTrue(os.access(iso_file, os.R_OK))
375
376         self._vm = QEMUBitsMachine(binary=self.qemu_bin,
377                                    base_temp_dir=self._workDir,
378                                    debugcon_log=self._debugcon_log,
379                                    debugcon_addr=self._debugcon_addr)
380
381         self._vm.add_args('-cdrom', '%s' %iso_file)
382         # the vm needs to be run under icount so that TCG emulation is
383         # consistent in terms of timing. smilatency tests have consistent
384         # timing requirements.
385         self._vm.add_args('-icount', 'auto')
386
387         args = " ".join(str(arg) for arg in self._vm.base_args()) + \
388             " " + " ".join(str(arg) for arg in self._vm.args)
389
390         self.logger.info("launching QEMU vm with the following arguments: %s",
391                          args)
392
393         self._vm.launch()
394         # biosbits has been configured to run all the specified test suites
395         # in batch mode and then automatically initiate a vm shutdown.
396         # Rely on avocado's unit test timeout.
397         self._vm.wait(timeout=None)
398         self.parse_log()
This page took 0.047001 seconds and 4 git commands to generate.