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