VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/tests/audio/tdAudioTest.py@ 90772

Last change on this file since 90772 was 90772, checked in by vboxsync, 3 years ago

Audio/ValKit: tdAudioTest.py: Always decode process output as UTF-8. bugref:10008

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.8 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: tdAudioTest.py 90772 2021-08-21 06:17:37Z vboxsync $
3
4"""
5AudioTest test driver which invokes the VKAT (Validation Kit Audio Test)
6binary to perform the actual audio tests.
7
8The generated test set archive on the guest will be downloaded by TXS
9to the host for later audio comparison / verification.
10"""
11
12__copyright__ = \
13"""
14Copyright (C) 2021 Oracle Corporation
15
16This file is part of VirtualBox Open Source Edition (OSE), as
17available from http://www.virtualbox.org. This file is free software;
18you can redistribute it and/or modify it under the terms of the GNU
19General Public License (GPL) as published by the Free Software
20Foundation, in version 2 as it comes in the "COPYING" file of the
21VirtualBox OSE distribution. VirtualBox OSE is distributed in the
22hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
23
24The contents of this file may alternatively be used under the terms
25of the Common Development and Distribution License Version 1.0
26(CDDL) only, as it comes in the "COPYING.CDDL" file of the
27VirtualBox OSE distribution, in which case the provisions of the
28CDDL are applicable instead of those of the GPL.
29
30You may elect to license modified versions of this file under the
31terms and conditions of either the GPL or the CDDL or both.
32"""
33__version__ = "$Revision: 90772 $"
34
35# Standard Python imports.
36import os
37import sys
38import signal
39import subprocess
40import uuid
41
42# Only the main script needs to modify the path.
43try: __file__
44except: __file__ = sys.argv[0];
45g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))));
46sys.path.append(g_ksValidationKitDir);
47
48# Validation Kit imports.
49from testdriver import reporter
50from testdriver import base
51from testdriver import vbox
52from testdriver import vboxtestvms
53from common import utils;
54
55# pylint: disable=unnecessary-semicolon
56
57class tdAudioTest(vbox.TestDriver):
58 """
59 Runs various audio tests.
60 """
61 def __init__(self):
62 vbox.TestDriver.__init__(self);
63 self.oTestVmSet = self.oTestVmManager.getSmokeVmSet('nat');
64 self.asGstVkatPaths = [
65 # Debugging stuff (SCP'd over to the guest).
66 '/tmp/vkat',
67 '/tmp/VBoxAudioTest',
68 'C:\\Temp\\vkat',
69 'C:\\Temp\\VBoxAudioTest',
70 # Validation Kit .ISO.
71 '${CDROM}/vboxvalidationkit/${OS/ARCH}/vkat${EXESUFF}',
72 '${CDROM}/${OS/ARCH}/vkat${EXESUFF}',
73 # Test VMs.
74 '/opt/apps/vkat',
75 '/opt/apps/VBoxAudioTest',
76 '/apps/vkat',
77 '/apps/VBoxAudioTest',
78 'C:\\Apps\\vkat${EXESUFF}',
79 'C:\\Apps\\VBoxAudioTest${EXESUFF}',
80 ## @odo VBoxAudioTest on Guest Additions?
81 ];
82 self.asTestsDef = [
83 'guest_tone_playback', 'guest_tone_recording'
84 ];
85 self.asTests = self.asTestsDef;
86
87 # Enable audio debug mode.
88 #
89 # This is needed in order to load and use the Validation Kit audio driver,
90 # which in turn is being used in conjunction with the guest side to record
91 # output (guest is playing back) and injecting input (guest is recording).
92 self.asOptExtraData = [
93 'VBoxInternal2/Audio/Debug/Enabled:true',
94 ];
95
96 # Name of the running VM to use for running the test driver. Optional, and None if not being used.
97 self.sRunningVmName = None;
98
99 def showUsage(self):
100 """
101 Shows the audio test driver-specific command line options.
102 """
103 fRc = vbox.TestDriver.showUsage(self);
104 reporter.log('');
105 reporter.log('tdAudioTest Options:');
106 reporter.log(' --runningvmname <vmname>');
107 reporter.log(' --audio-tests <s1[:s2[:]]>');
108 reporter.log(' Default: %s (all)' % (':'.join(self.asTestsDef)));
109 return fRc;
110
111 def parseOption(self, asArgs, iArg):
112 """
113 Parses the audio test driver-specific command line options.
114 """
115 if asArgs[iArg] == '--runningvmname':
116 iArg += 1;
117 if iArg >= len(asArgs):
118 raise base.InvalidOption('The "--runningvmname" needs VM name');
119
120 self.sRunningVmName = asArgs[iArg];
121 elif asArgs[iArg] == '--audio-tests':
122 iArg += 1;
123 if asArgs[iArg] == 'all': # Nice for debugging scripts.
124 self.asTests = self.asTestsDef;
125 else:
126 self.asTests = asArgs[iArg].split(':');
127 for s in self.asTests:
128 if s not in self.asTestsDef:
129 raise base.InvalidOption('The "--audio-tests" value "%s" is not valid; valid values are: %s'
130 % (s, ' '.join(self.asTestsDef)));
131 else:
132 return vbox.TestDriver.parseOption(self, asArgs, iArg);
133 return iArg + 1;
134
135 def actionVerify(self):
136 """
137 Verifies the test driver before running.
138 """
139 if self.sVBoxValidationKitIso is None or not os.path.isfile(self.sVBoxValidationKitIso):
140 reporter.error('Cannot find the VBoxValidationKit.iso! (%s)'
141 'Please unzip a Validation Kit build in the current directory or in some parent one.'
142 % (self.sVBoxValidationKitIso,) );
143 return False;
144 return vbox.TestDriver.actionVerify(self);
145
146 def actionConfig(self):
147 """
148 Configures the test driver before running.
149 """
150 if not self.importVBoxApi(): # So we can use the constant below.
151 return False;
152
153 # Make sure that the Validation Kit .ISO is mounted
154 # to find the VKAT (Validation Kit Audio Test) binary on it.
155 assert self.sVBoxValidationKitIso is not None;
156 return self.oTestVmSet.actionConfig(self, sDvdImage = self.sVBoxValidationKitIso);
157
158 def actionExecute(self):
159 """
160 Executes the test driver.
161 """
162 if self.sRunningVmName is None:
163 return self.oTestVmSet.actionExecute(self, self.testOneVmConfig);
164 return self.actionExecuteOnRunnigVM();
165
166 def actionExecuteOnRunnigVM(self):
167 """
168 Executes the tests in an already configured + running VM.
169 """
170 if not self.importVBoxApi():
171 return False;
172
173 fRc = True;
174
175 oVirtualBox = self.oVBoxMgr.getVirtualBox();
176 try:
177 oVM = oVirtualBox.findMachine(self.sRunningVmName);
178 if oVM.state != self.oVBoxMgr.constants.MachineState_Running:
179 reporter.error("Machine '%s' is not in Running state" % (self.sRunningVmName));
180 fRc = False;
181 except:
182 reporter.errorXcpt("Machine '%s' not found" % (self.sRunningVmName));
183 fRc = False;
184
185 if fRc:
186 oSession = self.openSession(oVM);
187 if oSession:
188 # Tweak this to your likings.
189 oTestVm = vboxtestvms.TestVm('runningvm', sKind = 'WindowsXP'); #sKind = 'WindowsXP' # sKind = 'Ubuntu_64'
190 (fRc, oTxsSession) = self.txsDoConnectViaTcp(oSession, 30 * 1000);
191 if fRc:
192 self.doTest(oTestVm, oSession, oTxsSession);
193 else:
194 reporter.error("Unable to open session for machine '%s'" % (self.sRunningVmName));
195 fRc = False;
196
197 del oVM;
198 del oVirtualBox;
199 return fRc;
200
201 def getGstVkatLogFilePath(self, oTestVm):
202 """
203 Returns the log file path of VKAT running on the guest (daemonized).
204 """
205 return oTestVm.pathJoin(self.getGuestTempDir(oTestVm), 'vkat-guest.log');
206
207 def locateGstVkat(self, oSession, oTxsSession):
208 """
209 Returns guest side path to VKAT.
210 """
211 for sVkatPath in self.asGstVkatPaths:
212 reporter.log2('Checking for VKAT at: %s ...' % (sVkatPath));
213 if self.txsIsFile(oSession, oTxsSession, sVkatPath):
214 return (True, sVkatPath);
215 reporter.error('Unable to find guest VKAT in any of these places:\n%s' % ('\n'.join(self.asGstVkatPaths),));
216 return (False, "");
217
218 def executeHstLoop(self, sWhat, asArgs, fAsAdmin = False):
219
220 fRc = False;
221
222 if fAsAdmin \
223 and utils.getHostOs() != 'win':
224 oProcess = utils.sudoProcessPopen(asArgs,
225 stdout = subprocess.PIPE, stderr = subprocess.PIPE, shell = False,
226 close_fds = False);
227 else:
228 oProcess = utils.processPopenSafe(asArgs,
229 stdout = subprocess.PIPE, stderr = subprocess.PIPE);
230 if oProcess:
231 for line in iter(oProcess.stdout.readline, b''):
232 reporter.log('[' + sWhat + '] ' + line.decode('utf-8'));
233 iExitCode = oProcess.poll();
234 if iExitCode:
235 if iExitCode == 0:
236 reporter.error('Executing \"%s\" was successful' % (sWhat));
237 fRc = True;
238 else:
239 reporter.error('Executing \"%s\" on host returned exit code error %d' % (sWhat, iExitCode));
240
241 if not fRc:
242 reporter.error('Executing \"%s\" on host failed' % (sWhat,));
243
244 return fRc;
245
246 def executeHst(self, sWhat, asArgs, fAsync = False, fAsAdmin = False):
247 """
248 Runs a binary (image) with optional admin (root) rights on the host and
249 waits until it terminates.
250
251 Windows currently is not supported yet running stuff as Administrator.
252
253 Returns success status (exit code is 0).
254 """
255 reporter.log('Executing \"%s\" on host (as admin = %s, async = %s)' % (sWhat, fAsAdmin, fAsync));
256
257 reporter.testStart(sWhat);
258
259 fRc = self.executeHstLoop(sWhat, asArgs);
260 if fRc:
261 reporter.error('Executing \"%s\" on host done' % (sWhat,));
262 else:
263 reporter.error('Executing \"%s\" on host failed' % (sWhat,));
264
265 reporter.testDone();
266
267 return fRc;
268
269 def killHstProcessByName(self, sProcName):
270 """
271 Kills processes by their name.
272 """
273 reporter.log('Trying to kill processes named "%s"' % (sProcName,));
274 if sys.platform == 'win32':
275 sArgProcName = '\"%s.exe\"' % sProcName;
276 asArgs = [ 'taskkill', '/IM', sArgProcName, '/F' ];
277 self.executeHst('Killing process', asArgs);
278 else: # Note: killall is not available on older Debians (requires psmisc).
279 # Using the BSD syntax here; MacOS also should understand this.
280 procPs = subprocess.Popen(['ps', 'ax'], stdout=subprocess.PIPE);
281 out, err = procPs.communicate();
282 if err:
283 reporter.log2('PS stderr:');
284 for sLine in err.decode('utf-8').splitlines():
285 reporter.log2(sLine);
286 if out:
287 reporter.log2('PS stdout:');
288 for sLine in out.decode('utf-8').splitlines():
289 reporter.log2(sLine);
290 if sProcName in sLine:
291 pid = int(sLine.split(None, 1)[0]);
292 reporter.log('Killing PID %d' % (pid,));
293 os.kill(pid, signal.SIGKILL); # pylint: disable=no-member
294
295 def killHstVkat(self):
296 """
297 Kills VKAT (VBoxAudioTest) on the host side.
298 """
299 reporter.log('Killing stale/old VKAT processes ...');
300 self.killHstProcessByName("vkat");
301 self.killHstProcessByName("VBoxAudioTest");
302
303 def getWinFirewallArgsDisable(self, sOsType):
304 """
305 Returns the command line arguments for Windows OSes
306 to disable the built-in firewall (if any).
307
308 If not supported, returns an empty array.
309 """
310 if sOsType == 'vista': # pylint: disable=no-else-return
311 # Vista and up.
312 return (['netsh.exe', 'advfirewall', 'set', 'allprofiles', 'state', 'off']);
313 elif sOsType == 'xp': # Older stuff (XP / 2003).
314 return(['netsh.exe', 'firewall', 'set', 'opmode', 'mode=DISABLE']);
315 # Not supported / available.
316 return [];
317
318 def disableGstFirewall(self, oTestVm, oTxsSession):
319 """
320 Disables the firewall on a guest (if any).
321
322 Needs elevated / admin / root privileges.
323
324 Returns success status, not logged.
325 """
326 fRc = False;
327
328 asArgs = [];
329 sOsType = '';
330 if oTestVm.isWindows():
331 if oTestVm.sKind in ['WindowsNT4', 'WindowsNT3x']:
332 sOsType = 'nt3x'; # Not supported, but define it anyway.
333 elif oTestVm.sKind in ('Windows2000', 'WindowsXP', 'Windows2003'):
334 sOsType = 'xp';
335 else:
336 sOsType = 'vista';
337 asArgs = self.getWinFirewallArgsDisable(sOsType);
338 else:
339 sOsType = 'unsupported';
340
341 reporter.log('Disabling firewall on guest (type: %s) ...' % (sOsType,));
342
343 if asArgs:
344 fRc = self.txsRunTest(oTxsSession, 'Disabling guest firewall', 3 * 60 * 1000, \
345 oTestVm.pathJoin(self.getGuestSystemDir(oTestVm), asArgs[0]), asArgs);
346 if not fRc:
347 reporter.error('Disabling firewall on guest returned exit code error %d' % (self.getLastRcFromTxs(oTxsSession)));
348 else:
349 reporter.log('Firewall not available on guest, skipping');
350 fRc = True; # Not available, just skip.
351
352 return fRc;
353
354 def setGstAudioBackend(self, oTestVm, asArgs):
355 """
356 Guesses guest OS, uses an alternative (non-default) audio backends if necessary
357 and appends it to the given arguments sequence.
358
359 Returns the altered arguments sequence.
360 """
361 asArgsRet = asArgs;
362
363 # Note: Also works with the 64-bit variants (if any).
364 sOsType = oTestVm.getNonCanonicalGuestOsType();
365 if "Windows2000" in sOsType \
366 or "WindowsXP" in sOsType \
367 or "Windows2003" in sOsType \
368 or "Windows7" in sOsType:
369 ## @todo Some more here?
370 asArgsRet.extend( [ '--backend', 'directsound' ]);
371
372 ## @todo Tweak old(er) Linux'es as well to use OSS instead of PulseAudio?
373 return asArgsRet;
374
375 def disableHstFirewall(self):
376 """
377 Disables the firewall on the host (if any).
378
379 Needs elevated / admin / root privileges.
380
381 Returns success status, not logged.
382 """
383 fRc = False;
384
385 asArgs = [];
386 sOsType = sys.platform;
387
388 if sOsType == 'win32':
389 reporter.log('Disabling firewall on host (type: %s) ...' % (sOsType));
390
391 ## @todo For now we ASSUME that we don't run (and don't support even) on old(er)
392 # Windows hosts than Vista.
393 asArgs = self.getWinFirewallArgsDisable('vista');
394 if asArgs:
395 fRc = self.executeHst('Disabling host firewall', asArgs, fAsAdmin = True);
396 else:
397 reporter.log('Firewall not available on host, skipping');
398 fRc = True; # Not available, just skip.
399
400 return fRc;
401
402 def getLastRcFromTxs(self, oTxsSession):
403 """
404 Extracts the last exit code reported by TXS from a run before.
405 Assumes that nothing else has been run on the same TXS session in the meantime.
406 """
407 iRc = 0;
408 (_, sOpcode, abPayload) = oTxsSession.getLastReply();
409 if sOpcode.startswith('PROC NOK '): # Extract process rc
410 iRc = abPayload[0]; # ASSUMES 8-bit rc for now.
411 return iRc;
412
413 def startVkatOnGuest(self, oTestVm, oSession, oTxsSession):
414 """
415 Starts VKAT on the guest (running in background).
416 """
417 sPathTemp = self.getGuestTempDir(oTestVm);
418 sPathAudioOut = oTestVm.pathJoin(sPathTemp, 'vkat-guest-out');
419 sPathAudioTemp = oTestVm.pathJoin(sPathTemp, 'vkat-guest-temp');
420
421 reporter.log('Guest audio test temp path is \"%s\"' % (sPathAudioOut));
422 reporter.log('Guest audio test output path is \"%s\"' % (sPathAudioTemp));
423
424 fRc, sVkatExe = self.locateGstVkat(oSession, oTxsSession);
425 if fRc:
426 reporter.log('Using VKAT on guest at \"%s\"' % (sVkatExe));
427
428 asArgs = [ sVkatExe, 'test', '-vv', '--mode', 'guest', \
429 '--tempdir', sPathAudioTemp, '--outdir', sPathAudioOut ];
430
431 # Guess the guest backend and apply the new arguments (if any).
432 asArgs = self.setGstAudioBackend(oTestVm, asArgs);
433
434 #
435 # Add own environment stuff.
436 #
437 asEnv = [];
438
439 # Write the log file to some deterministic place so TxS can retrieve it later.
440 sVkatLogFile = 'VKAT_RELEASE_LOG_DEST=file=' + self.getGstVkatLogFilePath(oTestVm);
441 asEnv.extend([ sVkatLogFile ]);
442
443 #
444 # Execute asynchronously on the guest.
445 #
446 fRc = oTxsSession.asyncExec(sVkatExe, asArgs, asEnv, cMsTimeout = 15 * 60 * 1000);
447 if fRc:
448 self.addTask(oTxsSession);
449
450 if not fRc:
451 reporter.error('VKAT on guest returned exit code error %d' % (self.getLastRcFromTxs(oTxsSession)));
452 else:
453 reporter.error('VKAT on guest not found');
454
455 return fRc;
456
457 def runTests(self, oTestVm, oSession, oTxsSession, sDesc, asTests):
458 """
459 Runs one or more tests using VKAT on the host, which in turn will
460 communicate with VKAT running on the guest and the Validation Kit
461 audio driver ATS (Audio Testing Service).
462 """
463 _ = oSession, oTxsSession;
464 sTag = uuid.uuid4();
465
466 sPathTemp = self.sScratchPath;
467 sPathAudioOut = oTestVm.pathJoin(sPathTemp, 'vkat-host-out-%s' % (sTag));
468 sPathAudioTemp = oTestVm.pathJoin(sPathTemp, 'vkat-host-temp-%s' % (sTag));
469
470 reporter.log('Host audio test temp path is \"%s\"' % (sPathAudioOut));
471 reporter.log('Host audio test output path is \"%s\"' % (sPathAudioTemp));
472 reporter.log('Host audio test tag is \"%s\"' % (sTag));
473
474 reporter.testStart(sDesc);
475
476 sVkatExe = self.getBinTool('vkat');
477
478 reporter.log('Using VKAT on host at: \"%s\"' % (sVkatExe));
479
480 # Build the base command line, exclude all tests by default.
481 asArgs = [ sVkatExe, 'test', '-vv', '--mode', 'host', '--tempdir', sPathAudioTemp, '--outdir', sPathAudioOut, '-a' ];
482
483 # ... and extend it with wanted tests.
484 asArgs.extend(asTests);
485
486 #
487 # Let VKAT on the host run synchronously.
488 #
489 fRc = self.executeHst("VKAT Host", asArgs);
490
491 reporter.testDone();
492
493 return fRc;
494
495 def doTest(self, oTestVm, oSession, oTxsSession):
496 """
497 Executes the specified audio tests.
498 """
499
500 # Disable any OS-specific firewalls preventing VKAT / ATS to run.
501 fRc = self.disableHstFirewall();
502 fRc = self.disableGstFirewall(oTestVm, oTxsSession) and fRc;
503
504 if not fRc:
505 return False;
506
507 # First try to kill any old VKAT / VBoxAudioTest processes lurking around on the host.
508 # Might happen because of former (aborted) runs.
509 self.killHstVkat();
510
511 reporter.log("Active tests: %s" % (self.asTests,));
512
513 fRc = self.startVkatOnGuest(oTestVm, oSession, oTxsSession);
514 if fRc:
515 #
516 # Execute the tests using VKAT on the guest side (in guest mode).
517 #
518 if "guest_tone_playback" in self.asTests:
519 fRc = self.runTests(oTestVm, oSession, oTxsSession, 'Guest audio playback', asTests = [ '-i0' ]);
520 if "guest_tone_recording" in self.asTests:
521 fRc = fRc and self.runTests(oTestVm, oSession, oTxsSession, 'Guest audio recording', asTests = [ '-i1' ]);
522
523 #
524 # Retrieve log files for diagnosis.
525 #
526 self.txsDownloadFiles(oSession, oTxsSession,
527 [ ( self.getGstVkatLogFilePath(oTestVm),
528 'vkat-guest-%s.log' % (oTestVm.sVmName,),),
529 ],
530 fIgnoreErrors = True);
531
532 return fRc;
533
534 def testOneVmConfig(self, oVM, oTestVm):
535 """
536 Runs tests using one specific VM config.
537 """
538
539 self.logVmInfo(oVM);
540
541 if oTestVm.isWindows() \
542 and oTestVm.sKind in ('WindowsNT4', 'Windows2000'): # Too old for DirectSound and WASAPI backends.
543 reporter.log('Audio testing skipped, not implemented/available for that OS yet.');
544 return True;
545
546 fRc = False;
547
548 # Reconfigure the VM.
549 oSession = self.openSession(oVM);
550 if oSession is not None:
551 # Set extra data.
552 for sExtraData in self.asOptExtraData:
553 sKey, sValue = sExtraData.split(':');
554 reporter.log('Set extradata: %s => %s' % (sKey, sValue));
555 fRc = oSession.setExtraData(sKey, sValue) and fRc;
556
557 # Save the settings.
558 fRc = fRc and oSession.saveSettings();
559 fRc = oSession.close() and fRc;
560
561 reporter.testStart('Waiting for TXS');
562 oSession, oTxsSession = self.startVmAndConnectToTxsViaTcp(oTestVm.sVmName,
563 fCdWait = True,
564 cMsTimeout = 3 * 60 * 1000,
565 sFileCdWait = '${OS/ARCH}/vkat${EXESUFF}');
566 reporter.testDone();
567
568 if oSession is not None:
569 self.addTask(oTxsSession);
570
571 fRc = self.doTest(oTestVm, oSession, oTxsSession);
572
573 # Cleanup.
574 self.removeTask(oTxsSession);
575 self.terminateVmBySession(oSession);
576
577 return fRc;
578
579 def onExit(self, iRc):
580 """
581 Exit handler for this test driver.
582 """
583 return vbox.TestDriver.onExit(self, iRc);
584
585if __name__ == '__main__':
586 sys.exit(tdAudioTest().main(sys.argv))
Note: See TracBrowser for help on using the repository browser.

© 2024 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette