diff options
Diffstat (limited to '')
-rw-r--r-- | src/VBox/ValidationKit/utils/audio/vkat.cpp | 1649 |
1 files changed, 1649 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/utils/audio/vkat.cpp b/src/VBox/ValidationKit/utils/audio/vkat.cpp new file mode 100644 index 00000000..0ff17e4c --- /dev/null +++ b/src/VBox/ValidationKit/utils/audio/vkat.cpp @@ -0,0 +1,1649 @@ +/* $Id: vkat.cpp $ */ +/** @file + * Validation Kit Audio Test (VKAT) utility for testing and validating the audio stack. + */ + +/* + * Copyright (C) 2021-2023 Oracle and/or its affiliates. + * + * This file is part of VirtualBox base platform packages, as + * available from https://www.virtualbox.org. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation, in version 3 of the + * License. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see <https://www.gnu.org/licenses>. + * + * The contents of this file may alternatively be used under the terms + * of the Common Development and Distribution License Version 1.0 + * (CDDL), a copy of it is provided in the "COPYING.CDDL" file included + * in the VirtualBox distribution, in which case the provisions of the + * CDDL are applicable instead of those of the GPL. + * + * You may elect to license modified versions of this file under the + * terms and conditions of either the GPL or the CDDL or both. + * + * SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 + */ + + +/********************************************************************************************************************************* +* Header Files * +*********************************************************************************************************************************/ +#define LOG_GROUP LOG_GROUP_AUDIO_TEST + +#include <iprt/buildconfig.h> +#include <iprt/ctype.h> +#include <iprt/dir.h> +#include <iprt/errcore.h> +#include <iprt/file.h> +#include <iprt/initterm.h> +#include <iprt/getopt.h> +#include <iprt/message.h> +#include <iprt/path.h> +#include <iprt/process.h> +#include <iprt/rand.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <package-generated.h> +#include "product-generated.h" + +#include <VBox/version.h> +#include <VBox/log.h> + +#ifdef RT_OS_WINDOWS +# include <iprt/win/windows.h> /* for CoInitializeEx and SetConsoleCtrlHandler */ +#else +# include <signal.h> +#endif + +#include "vkatInternal.h" + + +/********************************************************************************************************************************* +* Internal Functions * +*********************************************************************************************************************************/ +static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts); + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** + * Backends description table. + * + * @note The first backend in the array is the default one for the platform. + */ +AUDIOTESTBACKENDDESC const g_aBackends[] = +{ +#ifdef VBOX_WITH_AUDIO_PULSE + { &g_DrvHostPulseAudio, "pulseaudio" }, + { &g_DrvHostPulseAudio, "pulse" }, + { &g_DrvHostPulseAudio, "pa" }, +#endif +/* + * Note: ALSA has to come second so that PulseAudio above always is the default on Linux-y OSes + * -- most distros are using an ALSA plugin for PulseAudio nowadays. + * However, some of these configurations do not seem to work by default (can't create audio streams). + * + * If PulseAudio is not available, the (optional) probing ("--probe-backends") will choose the "pure" ALSA stack instead then. + */ +#if defined(VBOX_WITH_AUDIO_ALSA) && defined(RT_OS_LINUX) + { &g_DrvHostALSAAudio, "alsa" }, +#endif +#ifdef VBOX_WITH_AUDIO_OSS + { &g_DrvHostOSSAudio, "oss" }, +#endif +#if defined(RT_OS_DARWIN) + { &g_DrvHostCoreAudio, "coreaudio" }, + { &g_DrvHostCoreAudio, "core" }, + { &g_DrvHostCoreAudio, "ca" }, +#endif +#if defined(RT_OS_WINDOWS) + { &g_DrvHostAudioWas, "wasapi" }, + { &g_DrvHostAudioWas, "was" }, + { &g_DrvHostDSound, "directsound" }, + { &g_DrvHostDSound, "dsound" }, + { &g_DrvHostDSound, "ds" }, +#endif +#ifdef VBOX_WITH_AUDIO_DEBUG + { &g_DrvHostDebugAudio, "debug" }, +#endif + { &g_DrvHostValidationKitAudio, "valkit" } +}; +AssertCompile(sizeof(g_aBackends) > 0 /* port me */); +/** Number of backends defined. */ +unsigned g_cBackends = RT_ELEMENTS(g_aBackends); + +/** + * Long option values for the 'test' command. + */ +enum +{ + VKAT_TEST_OPT_COUNT = 900, + VKAT_TEST_OPT_DEV, + VKAT_TEST_OPT_GUEST_ATS_ADDR, + VKAT_TEST_OPT_GUEST_ATS_PORT, + VKAT_TEST_OPT_HOST_ATS_ADDR, + VKAT_TEST_OPT_HOST_ATS_PORT, + VKAT_TEST_OPT_MODE, + VKAT_TEST_OPT_NO_AUDIO_OK, + VKAT_TEST_OPT_NO_VERIFY, + VKAT_TEST_OPT_OUTDIR, + VKAT_TEST_OPT_PAUSE, + VKAT_TEST_OPT_PCM_HZ, + VKAT_TEST_OPT_PCM_BIT, + VKAT_TEST_OPT_PCM_CHAN, + VKAT_TEST_OPT_PCM_SIGNED, + VKAT_TEST_OPT_PROBE_BACKENDS, + VKAT_TEST_OPT_TAG, + VKAT_TEST_OPT_TEMPDIR, + VKAT_TEST_OPT_VOL, + VKAT_TEST_OPT_TCP_BIND_ADDRESS, + VKAT_TEST_OPT_TCP_BIND_PORT, + VKAT_TEST_OPT_TCP_CONNECT_ADDRESS, + VKAT_TEST_OPT_TCP_CONNECT_PORT, + VKAT_TEST_OPT_TONE_DURATION_MS, + VKAT_TEST_OPT_TONE_VOL_PERCENT +}; + +/** + * Long option values for the 'verify' command. + */ +enum +{ + VKAT_VERIFY_OPT_MAX_DIFF_COUNT = 900, + VKAT_VERIFY_OPT_MAX_DIFF_PERCENT, + VKAT_VERIFY_OPT_MAX_SIZE_PERCENT, + VKAT_VERIFY_OPT_NORMALIZE +}; + +/** + * Common command line parameters. + */ +static const RTGETOPTDEF g_aCmdCommonOptions[] = +{ + { "--quiet", 'q', RTGETOPT_REQ_NOTHING }, + { "--verbose", 'v', RTGETOPT_REQ_NOTHING }, + { "--daemonize", AUDIO_TEST_OPT_CMN_DAEMONIZE, RTGETOPT_REQ_NOTHING }, + { "--daemonized", AUDIO_TEST_OPT_CMN_DAEMONIZED, RTGETOPT_REQ_NOTHING }, + { "--debug-audio", AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_ENABLE, RTGETOPT_REQ_NOTHING }, + { "--debug-audio-path", AUDIO_TEST_OPT_CMN_DEBUG_AUDIO_PATH, RTGETOPT_REQ_STRING }, +}; + +/** + * Command line parameters for test mode. + */ +static const RTGETOPTDEF g_aCmdTestOptions[] = +{ + { "--backend", 'b', RTGETOPT_REQ_STRING }, + { "--drvaudio", 'd', RTGETOPT_REQ_NOTHING }, + { "--exclude", 'e', RTGETOPT_REQ_UINT32 }, + { "--exclude-all", 'a', RTGETOPT_REQ_NOTHING }, + { "--guest-ats-addr", VKAT_TEST_OPT_GUEST_ATS_ADDR, RTGETOPT_REQ_STRING }, + { "--guest-ats-port", VKAT_TEST_OPT_GUEST_ATS_PORT, RTGETOPT_REQ_UINT32 }, + { "--host-ats-address", VKAT_TEST_OPT_HOST_ATS_ADDR, RTGETOPT_REQ_STRING }, + { "--host-ats-port", VKAT_TEST_OPT_HOST_ATS_PORT, RTGETOPT_REQ_UINT32 }, + { "--include", 'i', RTGETOPT_REQ_UINT32 }, + { "--outdir", VKAT_TEST_OPT_OUTDIR, RTGETOPT_REQ_STRING }, + { "--count", VKAT_TEST_OPT_COUNT, RTGETOPT_REQ_UINT32 }, + { "--device", VKAT_TEST_OPT_DEV, RTGETOPT_REQ_STRING }, + { "--pause", VKAT_TEST_OPT_PAUSE, RTGETOPT_REQ_UINT32 }, + { "--pcm-bit", VKAT_TEST_OPT_PCM_BIT, RTGETOPT_REQ_UINT8 }, + { "--pcm-chan", VKAT_TEST_OPT_PCM_CHAN, RTGETOPT_REQ_UINT8 }, + { "--pcm-hz", VKAT_TEST_OPT_PCM_HZ, RTGETOPT_REQ_UINT16 }, + { "--pcm-signed", VKAT_TEST_OPT_PCM_SIGNED, RTGETOPT_REQ_BOOL }, + { "--probe-backends", VKAT_TEST_OPT_PROBE_BACKENDS, RTGETOPT_REQ_NOTHING }, + { "--mode", VKAT_TEST_OPT_MODE, RTGETOPT_REQ_STRING }, + { "--no-audio-ok", VKAT_TEST_OPT_NO_AUDIO_OK, RTGETOPT_REQ_NOTHING }, + { "--no-verify", VKAT_TEST_OPT_NO_VERIFY, RTGETOPT_REQ_NOTHING }, + { "--tag", VKAT_TEST_OPT_TAG, RTGETOPT_REQ_STRING }, + { "--tempdir", VKAT_TEST_OPT_TEMPDIR, RTGETOPT_REQ_STRING }, + { "--vol", VKAT_TEST_OPT_VOL, RTGETOPT_REQ_UINT8 }, + { "--tcp-bind-addr", VKAT_TEST_OPT_TCP_BIND_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-bind-port", VKAT_TEST_OPT_TCP_BIND_PORT, RTGETOPT_REQ_UINT16 }, + { "--tcp-connect-addr", VKAT_TEST_OPT_TCP_CONNECT_ADDRESS, RTGETOPT_REQ_STRING }, + { "--tcp-connect-port", VKAT_TEST_OPT_TCP_CONNECT_PORT, RTGETOPT_REQ_UINT16 }, + { "--tone-duration", VKAT_TEST_OPT_TONE_DURATION_MS, RTGETOPT_REQ_UINT32 }, + { "--tone-vol", VKAT_TEST_OPT_TONE_VOL_PERCENT, RTGETOPT_REQ_UINT8 } +}; + +/** + * Command line parameters for verification mode. + */ +static const RTGETOPTDEF g_aCmdVerifyOptions[] = +{ + { "--max-diff-count", VKAT_VERIFY_OPT_MAX_DIFF_COUNT, RTGETOPT_REQ_UINT32 }, + { "--max-diff-percent", VKAT_VERIFY_OPT_MAX_DIFF_PERCENT, RTGETOPT_REQ_UINT8 }, + { "--max-size-percent", VKAT_VERIFY_OPT_MAX_SIZE_PERCENT, RTGETOPT_REQ_UINT8 }, + { "--normalize", VKAT_VERIFY_OPT_NORMALIZE, RTGETOPT_REQ_BOOL } +}; + +/** Terminate ASAP if set. Set on Ctrl-C. */ +bool volatile g_fTerminate = false; +/** The release logger. */ +PRTLOGGER g_pRelLogger = NULL; +/** The test handle. */ +RTTEST g_hTest; +/** The current verbosity level. */ +unsigned g_uVerbosity = 0; +/** DrvAudio: Enable debug (or not). */ +bool g_fDrvAudioDebug = false; +/** DrvAudio: The debug output path. */ +const char *g_pszDrvAudioDebug = NULL; + + +/** + * Get default backend. + */ +PCPDMDRVREG AudioTestGetDefaultBackend(void) +{ + return g_aBackends[0].pDrvReg; +} + + +/** + * Helper for handling --backend options. + * + * @returns Pointer to the specified backend, NULL if not found (error + * displayed). + * @param pszBackend The backend option value. + */ +PCPDMDRVREG AudioTestFindBackendOpt(const char *pszBackend) +{ + for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++) + if ( strcmp(pszBackend, g_aBackends[i].pszName) == 0 + || strcmp(pszBackend, g_aBackends[i].pDrvReg->szName) == 0) + return g_aBackends[i].pDrvReg; + RTMsgError("Unknown backend: '%s'\n\n", pszBackend); + RTPrintf("Supported backend values are: "); + for (uintptr_t i = 0; i < RT_ELEMENTS(g_aBackends); i++) + { + if (i > 0) + RTPrintf(", "); + RTPrintf(g_aBackends[i].pszName); + } + RTPrintf("\n"); + return NULL; +} + + +/********************************************************************************************************************************* +* Test callbacks * +*********************************************************************************************************************************/ + +/** + * @copydoc FNAUDIOTESTSETUP + */ +static DECLCALLBACK(int) audioTestPlayToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx) +{ + RT_NOREF(pTstDesc, ppvCtx); + + int rc = VINF_SUCCESS; + + if (strlen(pTstEnv->szDev)) + { + rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_OUT, pTstEnv->szDev); + if (RT_FAILURE(rc)) + return rc; + } + + pTstParmsAcq->enmType = AUDIOTESTTYPE_TESTTONE_PLAY; + pTstParmsAcq->enmDir = PDMAUDIODIR_OUT; + + pTstParmsAcq->TestTone = pTstEnv->ToneParms; + + pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */ + + return rc; +} + +/** + * @copydoc FNAUDIOTESTEXEC + */ +static DECLCALLBACK(int) audioTestPlayToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms) +{ + RT_NOREF(pvCtx); + + int rc = VINF_SUCCESS; + + PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone; + + uint32_t const idxTest = pToneParms->Hdr.idxTest; + + RTTIMESPEC NowTimeSpec; + RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec)); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing test tone (%RU16Hz, %RU32ms)\n", + idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration); + + /* + * 1. Arm the (host) ValKit ATS with the recording parameters. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Telling ValKit audio driver on host to record new tone ...\n", idxTest); + + rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClValKit, pToneParms); + if (RT_SUCCESS(rc)) + { + /* Give the Validaiton Kit audio driver on the host a bit of time to register / arming the new test. */ + RTThreadSleep(5000); /* Fudge factor. */ + + /* + * 2. Tell VKAT on guest to start playback. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to play tone ...\n", idxTest); + + rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClGuest, pToneParms); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc); + } + else + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc); + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Playing tone done\n", idxTest); + + /* Give the audio stack a random amount of time for draining data before the next iteration. */ + if (pTstEnv->cIterations > 1) + RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Playing test tone failed with %Rrc\n", idxTest, rc); + + return rc; +} + +/** + * @copydoc FNAUDIOTESTDESTROY + */ +static DECLCALLBACK(int) audioTestPlayToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx) +{ + RT_NOREF(pTstEnv, pvCtx); + + return VINF_SUCCESS; +} + +/** + * @copydoc FNAUDIOTESTSETUP + */ +static DECLCALLBACK(int) audioTestRecordToneSetup(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc, PAUDIOTESTPARMS pTstParmsAcq, void **ppvCtx) +{ + RT_NOREF(pTstDesc, ppvCtx); + + int rc = VINF_SUCCESS; + + if (strlen(pTstEnv->szDev)) + { + rc = audioTestDriverStackSetDevice(pTstEnv->pDrvStack, PDMAUDIODIR_IN, pTstEnv->szDev); + if (RT_FAILURE(rc)) + return rc; + } + + pTstParmsAcq->enmType = AUDIOTESTTYPE_TESTTONE_RECORD; + pTstParmsAcq->enmDir = PDMAUDIODIR_IN; + + pTstParmsAcq->TestTone = pTstEnv->ToneParms; + + pTstParmsAcq->TestTone.Hdr.idxTest = pTstEnv->idxTest; /* Assign unique test ID. */ + + return rc; +} + +/** + * @copydoc FNAUDIOTESTEXEC + */ +static DECLCALLBACK(int) audioTestRecordToneExec(PAUDIOTESTENV pTstEnv, void *pvCtx, PAUDIOTESTPARMS pTstParms) +{ + RT_NOREF(pvCtx); + + int rc = VINF_SUCCESS; + + PAUDIOTESTTONEPARMS const pToneParms = &pTstParms->TestTone; + + uint32_t const idxTest = pToneParms->Hdr.idxTest; + + RTTIMESPEC NowTimeSpec; + RTTimeExplode(&pToneParms->Hdr.tsCreated, RTTimeNow(&NowTimeSpec)); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Recording test tone (%RU16Hz, %RU32ms)\n", + idxTest, (uint16_t)pToneParms->dbFreqHz, pToneParms->msDuration); + + /* + * 1. Arm the (host) ValKit ATS with the playback parameters. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Test #%RU32: Telling ValKit audio driver on host to inject recording data ...\n", idxTest); + + rc = AudioTestSvcClientTonePlay(&pTstEnv->u.Host.AtsClValKit, &pTstParms->TestTone); + if (RT_SUCCESS(rc)) + { + /* + * 2. Tell the guest ATS to start recording. + */ + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32: Telling VKAT on guest to record audio ...\n", idxTest); + + rc = AudioTestSvcClientToneRecord(&pTstEnv->u.Host.AtsClGuest, &pTstParms->TestTone); + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientToneRecord() failed with %Rrc\n", idxTest, rc); + } + else + RTTestFailed(g_hTest, "Test #%RU32: AudioTestSvcClientTonePlay() failed with %Rrc\n", idxTest, rc); + + if (RT_SUCCESS(rc)) + { + /* Wait a bit to let the left over audio bits being processed. */ + if (pTstEnv->cIterations > 1) + RTThreadSleep(RTRandU32Ex(2000, 5000)); /** @todo Implement some dedicated ATS command for this? */ + } + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test #%RU32: Recording test tone failed with %Rrc\n", idxTest, rc); + + return rc; +} + +/** + * @copydoc FNAUDIOTESTDESTROY + */ +static DECLCALLBACK(int) audioTestRecordToneDestroy(PAUDIOTESTENV pTstEnv, void *pvCtx) +{ + RT_NOREF(pTstEnv, pvCtx); + + return VINF_SUCCESS; +} + + +/********************************************************************************************************************************* +* Test execution * +*********************************************************************************************************************************/ + +/** Test definition table. */ +AUDIOTESTDESC g_aTests[] = +{ + /* pszTest fExcluded pfnSetup */ + { "PlayTone", false, audioTestPlayToneSetup, audioTestPlayToneExec, audioTestPlayToneDestroy }, + { "RecordTone", false, audioTestRecordToneSetup, audioTestRecordToneExec, audioTestRecordToneDestroy } +}; +/** Number of tests defined. */ +unsigned g_cTests = RT_ELEMENTS(g_aTests); + +/** + * Runs one specific audio test. + * + * @returns VBox status code. + * @param pTstEnv Test environment to use for running the test. + * @param pTstDesc Test to run. + */ +static int audioTestOne(PAUDIOTESTENV pTstEnv, PAUDIOTESTDESC pTstDesc) +{ + int rc = VINF_SUCCESS; + + AUDIOTESTPARMS TstParms; + audioTestParmsInit(&TstParms); + + RTTestSub(g_hTest, pTstDesc->pszName); + + if (pTstDesc->fExcluded) + { + RTTestSkipped(g_hTest, "Test #%RU32 is excluded from list, skipping", pTstEnv->idxTest); + return VINF_SUCCESS; + } + + pTstEnv->cIterations = pTstEnv->cIterations == 0 ? RTRandU32Ex(1, 10) : pTstEnv->cIterations; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test #%RU32 (%RU32 iterations total)\n", pTstEnv->idxTest, pTstEnv->cIterations); + + void *pvCtx = NULL; /* Test-specific opaque context. Optional and can be NULL. */ + + AssertPtr(pTstDesc->pfnExec); + for (uint32_t i = 0; i < pTstEnv->cIterations; i++) + { + int rc2; + + if (pTstDesc->pfnSetup) + { + rc2 = pTstDesc->pfnSetup(pTstEnv, pTstDesc, &TstParms, &pvCtx); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 setup failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + else + rc2 = VINF_SUCCESS; + + if (RT_SUCCESS(rc2)) + { + AssertPtrBreakStmt(pTstDesc->pfnExec, VERR_INVALID_POINTER); + rc2 = pTstDesc->pfnExec(pTstEnv, pvCtx, &TstParms); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 execution failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + + if (pTstDesc->pfnDestroy) + { + rc2 = pTstDesc->pfnDestroy(pTstEnv, pvCtx); + if (RT_FAILURE(rc2)) + RTTestFailed(g_hTest, "Test #%RU32 destruction failed with %Rrc\n", pTstEnv->idxTest, rc2); + } + + if (RT_SUCCESS(rc)) + rc = rc2; + + /* Keep going. */ + pTstEnv->idxTest++; + } + + RTTestSubDone(g_hTest); + + audioTestParmsDestroy(&TstParms); + + return rc; +} + +/** + * Runs all specified tests in a row. + * + * @returns VBox status code. + * @param pTstEnv Test environment to use for running all tests. + */ +int audioTestWorker(PAUDIOTESTENV pTstEnv) +{ + int rc = VINF_SUCCESS; + + if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS running\n"); + + while (!g_fTerminate) + RTThreadSleep(100); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Shutting down guest ATS ...\n"); + + int rc2 = AudioTestSvcStop(pTstEnv->pSrv); + if (RT_SUCCESS(rc)) + rc = rc2; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Guest ATS shutdown complete\n"); + } + else if (pTstEnv->enmMode == AUDIOTESTMODE_HOST) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag); + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling ValKit audio driver on host to begin a new test set ...\n"); + rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Telling VKAT on guest to begin a new test set ...\n"); + rc = AudioTestSvcClientTestSetBegin(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag); + if (RT_FAILURE(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Beginning test set on guest failed with %Rrc\n", rc); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Beginning test set on host (Validation Kit audio driver) failed with %Rrc\n", rc); + + if (RT_SUCCESS(rc)) + { + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + { + int rc2 = audioTestOne(pTstEnv, &g_aTests[i]); + if (RT_SUCCESS(rc)) + rc = rc2; + + if (g_fTerminate) + break; + } + + if (RT_SUCCESS(rc)) + { + /** @todo Fudge! */ + RTMSINTERVAL const msWait = RTRandU32Ex(RT_MS_1SEC, RT_MS_5SEC); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Waiting %RU32ms to let guest and the audio stack process remaining data ...\n", msWait); + RTThreadSleep(msWait); + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest ...\n"); + int rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClGuest, pTstEnv->szTag); + if (RT_FAILURE(rc2)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on guest failed with %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Ending test set on host (Validation Kit audio driver) ...\n"); + rc2 = AudioTestSvcClientTestSetEnd(&pTstEnv->u.Host.AtsClValKit, pTstEnv->szTag); + if (RT_FAILURE(rc2)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Ending test set on host (Validation Kit audio driver) failed with %Rrc\n", rc2); + if (RT_SUCCESS(rc)) + rc = rc2; + } + + if ( !g_fTerminate + && RT_SUCCESS(rc)) + { + /* + * Download guest + Validation Kit audio driver test sets to our output directory. + */ + char szFileName[RTPATH_MAX]; + if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-guest.tar.gz", pTstEnv->szTag)) + { + rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetGuest, sizeof(pTstEnv->u.Host.szPathTestSetGuest), + pTstEnv->szPathOut, szFileName); + if (RT_SUCCESS(rc)) + { + if (RTStrPrintf2(szFileName, sizeof(szFileName), "%s-host.tar.gz", pTstEnv->szTag)) + { + rc = RTPathJoin(pTstEnv->u.Host.szPathTestSetValKit, sizeof(pTstEnv->u.Host.szPathTestSetValKit), + pTstEnv->szPathOut, szFileName); + } + else + rc = VERR_BUFFER_OVERFLOW; + } + else + rc = VERR_BUFFER_OVERFLOW; + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading guest test set to '%s'\n", + pTstEnv->u.Host.szPathTestSetGuest); + rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClGuest, + pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetGuest); + } + + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Downloading host test set to '%s'\n", + pTstEnv->u.Host.szPathTestSetValKit); + rc = AudioTestSvcClientTestSetDownload(&pTstEnv->u.Host.AtsClValKit, + pTstEnv->szTag, pTstEnv->u.Host.szPathTestSetValKit); + } + } + else + rc = VERR_BUFFER_OVERFLOW; + + if ( RT_SUCCESS(rc) + && !pTstEnv->fSkipVerify) + { + rc = audioVerifyOne(pTstEnv->u.Host.szPathTestSetGuest, pTstEnv->u.Host.szPathTestSetValKit, NULL /* pOpts */); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification skipped\n"); + + if (!pTstEnv->fSkipVerify) + { + RTFileDelete(pTstEnv->u.Host.szPathTestSetGuest); + RTFileDelete(pTstEnv->u.Host.szPathTestSetValKit); + } + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Leaving test set files behind\n"); + } + } + } + else + rc = VERR_NOT_IMPLEMENTED; + + /* Clean up. */ + RTDirRemove(pTstEnv->szPathTemp); + RTDirRemove(pTstEnv->szPathOut); + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Test worker failed with %Rrc", rc); + + return rc; +} + +/** Option help for the 'test' command. */ +static DECLCALLBACK(const char *) audioTestCmdTestHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case 'a': return "Exclude all tests from the list (useful to enable single tests later with --include)"; + case 'b': return "The audio backend to use"; + case 'd': return "Go via DrvAudio instead of directly interfacing with the backend"; + case 'e': return "Exclude the given test id from the list"; + case 'i': return "Include the given test id in the list"; + case VKAT_TEST_OPT_COUNT: return "Number of test iterations to perform for selected tests\n" + " Default: random number"; + case VKAT_TEST_OPT_DEV: return "Name of the input/output device to use\n" + " Default: default device"; + case VKAT_TEST_OPT_TONE_DURATION_MS: return "Test tone duration to play / record (ms)\n" + " Default: random duration"; + case VKAT_TEST_OPT_TONE_VOL_PERCENT: return "Test tone volume (percent)\n" + " Default: 100"; + case VKAT_TEST_OPT_GUEST_ATS_ADDR: return "Address of guest ATS to connect to\n" + " Default: " ATS_TCP_DEF_CONNECT_GUEST_STR; + case VKAT_TEST_OPT_GUEST_ATS_PORT: return "Port of guest ATS to connect to (needs NAT port forwarding)\n" + " Default: 6042"; /* ATS_TCP_DEF_CONNECT_PORT_GUEST */ + case VKAT_TEST_OPT_HOST_ATS_ADDR: return "Address of host ATS to connect to\n" + " Default: " ATS_TCP_DEF_CONNECT_HOST_ADDR_STR; + case VKAT_TEST_OPT_HOST_ATS_PORT: return "Port of host ATS to connect to\n" + " Default: 6052"; /* ATS_TCP_DEF_BIND_PORT_VALKIT */ + case VKAT_TEST_OPT_MODE: return "Test mode to use when running the tests\n" + " Available modes:\n" + " guest: Run as a guest-side ATS\n" + " host: Run as a host-side ATS"; + case VKAT_TEST_OPT_NO_AUDIO_OK: return "Enables running without any found audio hardware (e.g. servers)"; + case VKAT_TEST_OPT_NO_VERIFY: return "Skips the verification step"; + case VKAT_TEST_OPT_OUTDIR: return "Output directory to use"; + case VKAT_TEST_OPT_PAUSE: return "Not yet implemented"; + case VKAT_TEST_OPT_PCM_HZ: return "PCM Hertz (Hz) rate to use\n" + " Default: 44100"; + case VKAT_TEST_OPT_PCM_BIT: return "PCM sample bits (i.e. 16) to use\n" + " Default: 16"; + case VKAT_TEST_OPT_PCM_CHAN: return "PCM channels to use\n" + " Default: 2"; + case VKAT_TEST_OPT_PCM_SIGNED: return "PCM samples to use (signed = true, unsigned = false)\n" + " Default: true"; + case VKAT_TEST_OPT_PROBE_BACKENDS: return "Probes all (available) backends until a working one is found"; + case VKAT_TEST_OPT_TAG: return "Test set tag to use"; + case VKAT_TEST_OPT_TEMPDIR: return "Temporary directory to use"; + case VKAT_TEST_OPT_VOL: return "Audio volume (percent) to use"; + case VKAT_TEST_OPT_TCP_BIND_ADDRESS: return "TCP address listening to (server mode)"; + case VKAT_TEST_OPT_TCP_BIND_PORT: return "TCP port listening to (server mode)"; + case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS: return "TCP address to connect to (client mode)"; + case VKAT_TEST_OPT_TCP_CONNECT_PORT: return "TCP port to connect to (client mode)"; + default: + break; + } + return NULL; +} + +/** + * Main (entry) function for the testing functionality of VKAT. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioTestMain(PRTGETOPTSTATE pGetState) +{ + AUDIOTESTENV TstEnv; + audioTestEnvInit(&TstEnv); + + int rc; + + PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend(); + uint8_t cPcmSampleBit = 0; + uint8_t cPcmChannels = 0; + uint32_t uPcmHz = 0; + bool fPcmSigned = true; + bool fProbeBackends = false; + bool fNoAudioOk = false; + + const char *pszGuestTcpAddr = NULL; + uint16_t uGuestTcpPort = ATS_TCP_DEF_BIND_PORT_GUEST; + const char *pszValKitTcpAddr = NULL; + uint16_t uValKitTcpPort = ATS_TCP_DEF_BIND_PORT_VALKIT; + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion))) + { + switch (ch) + { + case 'a': + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + g_aTests[i].fExcluded = true; + break; + + case 'b': + pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz); + if (pDrvReg == NULL) + return RTEXITCODE_SYNTAX; + break; + + case 'd': + TstEnv.IoOpts.fWithDrvAudio = true; + break; + + case 'e': + if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = true; + break; + + case VKAT_TEST_OPT_GUEST_ATS_ADDR: + pszGuestTcpAddr = ValueUnion.psz; + break; + + case VKAT_TEST_OPT_GUEST_ATS_PORT: + uGuestTcpPort = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_HOST_ATS_ADDR: + pszValKitTcpAddr = ValueUnion.psz; + break; + + case VKAT_TEST_OPT_HOST_ATS_PORT: + uValKitTcpPort = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_MODE: + if (TstEnv.enmMode != AUDIOTESTMODE_UNKNOWN) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Test mode (guest / host) already specified"); + TstEnv.enmMode = RTStrICmp(ValueUnion.psz, "guest") == 0 ? AUDIOTESTMODE_GUEST : AUDIOTESTMODE_HOST; + break; + + case VKAT_TEST_OPT_NO_AUDIO_OK: + fNoAudioOk = true; + break; + + case VKAT_TEST_OPT_NO_VERIFY: + TstEnv.fSkipVerify = true; + break; + + case 'i': + if (ValueUnion.u32 >= RT_ELEMENTS(g_aTests)) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32); + g_aTests[ValueUnion.u32].fExcluded = false; + break; + + case VKAT_TEST_OPT_COUNT: + TstEnv.cIterations = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_DEV: + rc = RTStrCopy(TstEnv.szDev, sizeof(TstEnv.szDev), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to copy out device: %Rrc", rc); + break; + + case VKAT_TEST_OPT_TONE_DURATION_MS: + TstEnv.ToneParms.msDuration = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_TONE_VOL_PERCENT: + TstEnv.ToneParms.uVolumePercent = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PAUSE: + return RTMsgErrorExitFailure("Not yet implemented!"); + + case VKAT_TEST_OPT_OUTDIR: + rc = RTStrCopy(TstEnv.szPathOut, sizeof(TstEnv.szPathOut), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExitFailure("Failed to copy out directory: %Rrc", rc); + break; + + case VKAT_TEST_OPT_PCM_BIT: + cPcmSampleBit = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PCM_CHAN: + cPcmChannels = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_PCM_HZ: + uPcmHz = ValueUnion.u32; + break; + + case VKAT_TEST_OPT_PCM_SIGNED: + fPcmSigned = ValueUnion.f; + break; + + case VKAT_TEST_OPT_PROBE_BACKENDS: + fProbeBackends = true; + break; + + case VKAT_TEST_OPT_TAG: + rc = RTStrCopy(TstEnv.szTag, sizeof(TstEnv.szTag), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Tag invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TEMPDIR: + rc = RTStrCopy(TstEnv.szPathTemp, sizeof(TstEnv.szPathTemp), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Temp dir invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_VOL: + TstEnv.IoOpts.uVolumePercent = ValueUnion.u8; + break; + + case VKAT_TEST_OPT_TCP_BIND_ADDRESS: + rc = RTStrCopy(TstEnv.TcpOpts.szBindAddr, sizeof(TstEnv.TcpOpts.szBindAddr), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Bind address invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TCP_BIND_PORT: + TstEnv.TcpOpts.uBindPort = ValueUnion.u16; + break; + + case VKAT_TEST_OPT_TCP_CONNECT_ADDRESS: + rc = RTStrCopy(TstEnv.TcpOpts.szConnectAddr, sizeof(TstEnv.TcpOpts.szConnectAddr), ValueUnion.psz); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Connect address invalid, rc=%Rrc", rc); + break; + + case VKAT_TEST_OPT_TCP_CONNECT_PORT: + TstEnv.TcpOpts.uConnectPort = ValueUnion.u16; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdTest); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + if (TstEnv.enmMode == AUDIOTESTMODE_UNKNOWN) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "No test mode (--mode) specified!\n"); + + /* Validate TCP options. */ + if ( TstEnv.TcpOpts.szBindAddr[0] + && TstEnv.TcpOpts.szConnectAddr[0]) + return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Only one TCP connection mode (connect as client *or* bind as server) can be specified) at a time!\n"); + + /* Set new (override standard) I/O PCM properties if set by the user. */ + if ( cPcmSampleBit + || cPcmChannels + || uPcmHz) + { + PDMAudioPropsInit(&TstEnv.IoOpts.Props, + cPcmSampleBit ? cPcmSampleBit / 2 : 2 /* 16-bit */, fPcmSigned /* fSigned */, + cPcmChannels ? cPcmChannels : 2 /* Stereo */, uPcmHz ? uPcmHz : 44100); + } + + /* Do this first before everything else below. */ + rc = AudioTestDriverStackPerformSelftest(); + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Testing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc); + } + + AUDIOTESTDRVSTACK DrvStack; + if (fProbeBackends) + rc = audioTestDriverStackProbe(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */ + else + rc = audioTestDriverStackInitEx(&DrvStack, pDrvReg, + true /* fEnabledIn */, true /* fEnabledOut */, TstEnv.IoOpts.fWithDrvAudio); /** @todo Make in/out configurable, too. */ + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Unable to init driver stack: %Rrc\n", rc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "Warning: Initializing driver stack not possible (%Rrc), but --no-audio-ok was specified. Running on a server without audio hardware?\n", rc); + } + + PPDMAUDIOHOSTDEV pDev; + rc = audioTestDevicesEnumerateAndCheck(&DrvStack, TstEnv.szDev, &pDev); + if (RT_FAILURE(rc)) + { + if (!fNoAudioOk) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "Enumerating device(s) failed: %Rrc\n", rc); + } + + /* For now all tests have the same test environment and driver stack. */ + rc = audioTestEnvCreate(&TstEnv, &DrvStack); + if (RT_SUCCESS(rc)) + rc = audioTestWorker(&TstEnv); + + audioTestEnvDestroy(&TstEnv); + audioTestDriverStackDelete(&DrvStack); + + if (RT_FAILURE(rc)) /* Let us know that something went wrong in case we forgot to mention it. */ + RTTestFailed(g_hTest, "Testing failed with %Rrc\n", rc); + + /* + * Print summary and exit. + */ + return RTTestSummaryAndDestroy(g_hTest); +} + + +const VKATCMD g_CmdTest = +{ + "test", + audioTestMain, + "Runs audio tests and creates an audio test set.", + g_aCmdTestOptions, + RT_ELEMENTS(g_aCmdTestOptions), + audioTestCmdTestHelp, + true /* fNeedsTransport */ +}; + + +/********************************************************************************************************************************* +* Command: verify * +*********************************************************************************************************************************/ + +static int audioVerifyOpenTestSet(const char *pszPathSet, PAUDIOTESTSET pSet) +{ + int rc; + + char szPathExtracted[RTPATH_MAX]; + + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Opening test set '%s'\n", pszPathSet); + + const bool fPacked = AudioTestSetIsPacked(pszPathSet); + + if (fPacked) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set is an archive and needs to be unpacked\n"); + + if (!RTFileExists(pszPathSet)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set '%s' does not exist\n", pszPathSet); + rc = VERR_FILE_NOT_FOUND; + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + { + char szPathTemp[RTPATH_MAX]; + rc = RTPathTemp(szPathTemp, sizeof(szPathTemp)); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using temporary directory '%s'\n", szPathTemp); + + rc = RTPathJoin(szPathExtracted, sizeof(szPathExtracted), szPathTemp, "vkat-testset-XXXX"); + if (RT_SUCCESS(rc)) + { + rc = RTDirCreateTemp(szPathExtracted, 0755); + if (RT_SUCCESS(rc)) + { + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Unpacking archive to '%s'\n", szPathExtracted); + rc = AudioTestSetUnpack(pszPathSet, szPathExtracted); + if (RT_SUCCESS(rc)) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Archive successfully unpacked\n"); + } + } + } + } + } + else + rc = VINF_SUCCESS; + + if (RT_SUCCESS(rc)) + rc = AudioTestSetOpen(pSet, fPacked ? szPathExtracted : pszPathSet); + + if (RT_FAILURE(rc)) + RTTestFailed(g_hTest, "Unable to open / unpack test set archive: %Rrc", rc); + + return rc; +} + +/** + * Verifies one test set pair. + * + * @returns VBox status code. + * @param pszPathSetA Absolute path to test set A. + * @param pszPathSetB Absolute path to test set B. + * @param pOpts Verification options to use. Optional. + * When NULL, the (very strict) defaults will be used. + */ +static int audioVerifyOne(const char *pszPathSetA, const char *pszPathSetB, PAUDIOTESTVERIFYOPTS pOpts) +{ + RTTestSubF(g_hTest, "Verifying"); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verifying test set '%s' with test set '%s'\n", pszPathSetA, pszPathSetB); + + AUDIOTESTSET SetA, SetB; + int rc = audioVerifyOpenTestSet(pszPathSetA, &SetA); + if (RT_SUCCESS(rc)) + { + rc = audioVerifyOpenTestSet(pszPathSetB, &SetB); + if (RT_SUCCESS(rc)) + { + AUDIOTESTERRORDESC errDesc; + if (pOpts) + rc = AudioTestSetVerifyEx(&SetA, &SetB, pOpts, &errDesc); + else + rc = AudioTestSetVerify(&SetA, &SetB, &errDesc); + if (RT_SUCCESS(rc)) + { + uint32_t const cErr = AudioTestErrorDescCount(&errDesc); + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%RU32 errors occurred while verifying\n", cErr); + + /** @todo Use some AudioTestErrorXXX API for enumeration here later. */ + PAUDIOTESTERRORENTRY pErrEntry; + RTListForEach(&errDesc.List, pErrEntry, AUDIOTESTERRORENTRY, Node) + { + if (RT_FAILURE(pErrEntry->rc)) + RTTestFailed(g_hTest, "%s\n", pErrEntry->szDesc); + else + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "%s\n", pErrEntry->szDesc); + } + + if (cErr == 0) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Verification successful\n"); + + AudioTestErrorDescDestroy(&errDesc); + } + else + RTTestFailed(g_hTest, "Verification failed with %Rrc", rc); + +#ifdef DEBUG + if (g_fDrvAudioDebug) + RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, + "\n" + "Use the following command line to re-run verification in the debugger:\n" + "gdb --args ./VBoxAudioTest -vvvv --debug-audio verify \"%s\" \"%s\"\n", + SetA.szPathAbs, SetB.szPathAbs); +#endif + if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */ + AudioTestSetWipe(&SetB); + AudioTestSetClose(&SetB); + } + + if (!g_fDrvAudioDebug) /* Ditto. */ + AudioTestSetWipe(&SetA); + AudioTestSetClose(&SetA); + } + + RTTestSubDone(g_hTest); + + return rc; +} + +/** Option help for the 'verify' command. */ +static DECLCALLBACK(const char *) audioTestCmdVerifyHelp(PCRTGETOPTDEF pOpt) +{ + switch (pOpt->iShort) + { + case VKAT_VERIFY_OPT_MAX_DIFF_COUNT: return "Specifies the maximum number of differences\n" + " Default: 0 (strict)"; + case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT: return "Specifies the maximum difference (percent)\n" + " Default: 0 (strict)"; + case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT: return "Specifies the maximum size difference (percent)\n" + " Default: 1 (strict)"; + case VKAT_VERIFY_OPT_NORMALIZE: return "Enables / disables audio data normalization\n" + " Default: false"; + default: + break; + } + return NULL; +} + +/** + * Main (entry) function for the verification functionality of VKAT. + * + * @returns Program exit code. + * @param pGetState RTGetOpt state. + */ +static DECLCALLBACK(RTEXITCODE) audioVerifyMain(PRTGETOPTSTATE pGetState) +{ + /* + * Parse options and process arguments. + */ + const char *apszSets[2] = { NULL, NULL }; + unsigned iTestSet = 0; + + AUDIOTESTVERIFYOPTS Opts; + AudioTestSetVerifyOptsInit(&Opts); + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(pGetState, &ValueUnion))) + { + switch (ch) + { + case VKAT_VERIFY_OPT_MAX_DIFF_COUNT: + Opts.cMaxDiff = ValueUnion.u32; + break; + + case VKAT_VERIFY_OPT_MAX_DIFF_PERCENT: + Opts.uMaxDiffPercent = ValueUnion.u8; + break; + + case VKAT_VERIFY_OPT_MAX_SIZE_PERCENT: + Opts.uMaxSizePercent = ValueUnion.u8; + break; + + case VKAT_VERIFY_OPT_NORMALIZE: + Opts.fNormalize = ValueUnion.f; + break; + + case VINF_GETOPT_NOT_OPTION: + if (iTestSet == 0) + RTTestBanner(g_hTest); + if (iTestSet >= RT_ELEMENTS(apszSets)) + return RTMsgErrorExitFailure("Only two test sets can be verified at one time"); + apszSets[iTestSet++] = ValueUnion.psz; + break; + + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdVerify); + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + if (!iTestSet) + return RTMsgErrorExitFailure("At least one test set must be specified"); + + int rc = VINF_SUCCESS; + + /* + * If only test set A is given, default to the current directory + * for test set B. + */ + char szDirCur[RTPATH_MAX]; + if (iTestSet == 1) + { + rc = RTPathGetCurrent(szDirCur, sizeof(szDirCur)); + if (RT_SUCCESS(rc)) + apszSets[1] = szDirCur; + else + RTTestFailed(g_hTest, "Failed to retrieve current directory: %Rrc", rc); + } + + if (RT_SUCCESS(rc)) + audioVerifyOne(apszSets[0], apszSets[1], &Opts); + + /* + * Print summary and exit. + */ + return RTTestSummaryAndDestroy(g_hTest); +} + + +const VKATCMD g_CmdVerify = +{ + "verify", + audioVerifyMain, + "Verifies a formerly created audio test set.", + g_aCmdVerifyOptions, + RT_ELEMENTS(g_aCmdVerifyOptions), + audioTestCmdVerifyHelp, + false /* fNeedsTransport */ +}; + + +/********************************************************************************************************************************* +* Main * +*********************************************************************************************************************************/ + +/** + * Ctrl-C signal handler. + * + * This just sets g_fTerminate and hope it will be noticed soon. + * + * On non-Windows it restores the SIGINT action to default, so that a second + * Ctrl-C will have the normal effect (just in case the code doesn't respond to + * g_fTerminate). + */ +#ifdef RT_OS_WINDOWS +static BOOL CALLBACK audioTestConsoleCtrlHandler(DWORD dwCtrlType) RT_NOEXCEPT +{ + if (dwCtrlType != CTRL_C_EVENT && dwCtrlType != CTRL_BREAK_EVENT) + return false; + RTPrintf(dwCtrlType == CTRL_C_EVENT ? "Ctrl-C!\n" : "Ctrl-Break!\n"); + + ASMAtomicWriteBool(&g_fTerminate, true); + + return true; +} +#else +static void audioTestSignalHandler(int iSig) RT_NOEXCEPT +{ + Assert(iSig == SIGINT); RT_NOREF(iSig); + RTPrintf("Ctrl-C!\n"); + + ASMAtomicWriteBool(&g_fTerminate, true); + + signal(SIGINT, SIG_DFL); +} +#endif + +/** + * Commands. + */ +static const VKATCMD * const g_apCommands[] = +{ + &g_CmdTest, + &g_CmdVerify, + &g_CmdBackends, + &g_CmdEnum, + &g_CmdPlay, + &g_CmdRec, + &g_CmdSelfTest +}; + +/** + * Shows tool usage text. + */ +RTEXITCODE audioTestUsage(PRTSTREAM pStrm, PCVKATCMD pOnlyCmd) +{ + RTStrmPrintf(pStrm, "usage: %s [global options] <command> [command-options]\n", RTProcShortName()); + RTStrmPrintf(pStrm, + "\n" + "Global Options:\n" + " --debug-audio\n" + " Enables (DrvAudio) debugging\n" + " --debug-audio-path=<path>\n" + " Tells DrvAudio where to put its debug output (wav-files)\n" + " -q, --quiet\n" + " Sets verbosity to zero\n" + " -v, --verbose\n" + " Increase verbosity\n" + " -V, --version\n" + " Displays version\n" + " -h, -?, --help\n" + " Displays help\n" + ); + + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + { + PCVKATCMD const pCmd = g_apCommands[iCmd]; + if (!pOnlyCmd || pCmd == pOnlyCmd) + { + RTStrmPrintf(pStrm, + "\n" + "Command '%s':\n" + " %s\n" + "Options for '%s':\n", + pCmd->pszCommand, pCmd->pszDesc, pCmd->pszCommand); + PCRTGETOPTDEF const paOptions = pCmd->paOptions; + for (unsigned i = 0; i < pCmd->cOptions; i++) + { + if (RT_C_IS_PRINT(paOptions[i].iShort)) + RTStrmPrintf(pStrm, " -%c, %s\n", paOptions[i].iShort, paOptions[i].pszLong); + else + RTStrmPrintf(pStrm, " %s\n", paOptions[i].pszLong); + + const char *pszHelp = NULL; + if (pCmd->pfnOptionHelp) + pszHelp = pCmd->pfnOptionHelp(&paOptions[i]); + if (pszHelp) + RTStrmPrintf(pStrm, " %s\n", pszHelp); + } + + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + g_apTransports[iTx]->pfnUsage(pStrm); + } + } + + return RTEXITCODE_SUCCESS; +} + +/** + * Lists the commands and their descriptions. + */ +static RTEXITCODE audioTestListCommands(PRTSTREAM pStrm) +{ + RTStrmPrintf(pStrm, "Commands:\n"); + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + RTStrmPrintf(pStrm, "%8s - %s\n", g_apCommands[iCmd]->pszCommand, g_apCommands[iCmd]->pszDesc); + return RTEXITCODE_SUCCESS; +} + +/** + * Shows tool version. + */ +RTEXITCODE audioTestVersion(void) +{ + RTPrintf("%s\n", RTBldCfgRevisionStr()); + return RTEXITCODE_SUCCESS; +} + +/** + * Shows the logo. + * + * @param pStream Output stream to show logo on. + */ +void audioTestShowLogo(PRTSTREAM pStream) +{ + RTStrmPrintf(pStream, VBOX_PRODUCT " VKAT (Validation Kit Audio Test) Version " VBOX_VERSION_STRING " - r%s\n" + "Copyright (C) " VBOX_C_YEAR " " VBOX_VENDOR "\n\n", RTBldCfgRevisionStr()); +} + +int main(int argc, char **argv) +{ + /* + * Init IPRT. + */ + int rc = RTR3InitExe(argc, &argv, 0); + if (RT_FAILURE(rc)) + return RTMsgInitFailure(rc); + + /* + * Handle special command line options which need parsing before + * everything else. + */ + /** @todo r=bird: this isn't at all syntactically sane, because you don't know + * how to parse past the command (can almost be done safely thought, since + * you've got the option definitions for every command at hand). So, if someone + * wants to play a file named "-v.wav", you'll incorrectly take that as two 'v' + * options. The parsing has to stop when you get to the command, i.e. first + * VINF_GETOPT_NOT_OPTION or anything that isn't a common option. Daemonizing + * when for instance encountering an invalid command, is not correct. + * + * Btw. you MUST however process the 'q' option in parallel to 'v' here, they + * are oposites. For instance '-vqvvv' is supposed to give you level 3 logging, + * not quiet! So, either you process both 'v' and 'q' here, or you pospone them + * (better option). + */ + /** @todo r=bird: Is the daemonizing needed? The testcase doesn't seem to use + * it... If you don't need it, drop it as it make the parsing complex + * and illogical. The --daemonized / --damonize options should be + * required to before the command, then okay. */ + bool fDaemonize = false; + bool fDaemonized = false; + + RTGETOPTSTATE GetState; + rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions, + RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */); + AssertRCReturn(rc, RTEXITCODE_INIT); + + int ch; + RTGETOPTUNION ValueUnion; + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (ch) + { + case AUDIO_TEST_OPT_CMN_DAEMONIZE: + fDaemonize = true; + break; + + case AUDIO_TEST_OPT_CMN_DAEMONIZED: + fDaemonized = true; + break; + + /* Has to be defined here and not in AUDIO_TEST_COMMON_OPTION_CASES, to get the logger + * configured before the specific command handlers down below come into play. */ + case 'v': + g_uVerbosity++; + break; + + default: + break; + } + } + + /** @todo add something to suppress this stuff. */ + audioTestShowLogo(g_pStdOut); + + if (fDaemonize) + { + if (!fDaemonized) + { + rc = RTProcDaemonize(argv, "--daemonized"); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTProcDaemonize() failed with %Rrc\n", rc); + + RTMsgInfo("Starting in background (daemonizing) ..."); + return RTEXITCODE_SUCCESS; + } + /* else continue running in background. */ + } + + /* + * Init test and globals. + * Note: Needs to be done *after* daemonizing, otherwise the child will fail! + */ + rc = RTTestCreate("AudioTest", &g_hTest); + if (RT_FAILURE(rc)) + return RTMsgErrorExit(RTEXITCODE_FAILURE, "RTTestCreate() failed with %Rrc\n", rc); + +#ifdef RT_OS_WINDOWS + HRESULT hrc = CoInitializeEx(NULL /*pReserved*/, COINIT_MULTITHREADED | COINIT_SPEED_OVER_MEMORY | COINIT_DISABLE_OLE1DDE); + if (FAILED(hrc)) + RTMsgWarning("CoInitializeEx failed: %#x", hrc); +#endif + + /* + * Configure release logging to go to stdout. + */ + RTUINT fFlags = RTLOGFLAGS_PREFIX_THREAD | RTLOGFLAGS_PREFIX_TIME_PROG; +#if defined(RT_OS_WINDOWS) || defined(RT_OS_OS2) + fFlags |= RTLOGFLAGS_USECRLF; +#endif + static const char * const s_apszLogGroups[] = VBOX_LOGGROUP_NAMES; + rc = RTLogCreate(&g_pRelLogger, fFlags, "all.e.l", "VKAT_RELEASE_LOG", + RT_ELEMENTS(s_apszLogGroups), s_apszLogGroups, RTLOGDEST_STDOUT, NULL /*"vkat-release.log"*/); + if (RT_SUCCESS(rc)) + { + RTLogRelSetDefaultInstance(g_pRelLogger); + if (g_uVerbosity) + { + RTMsgInfo("Setting verbosity logging to level %u\n", g_uVerbosity); + switch (g_uVerbosity) /* Not very elegant, but has to do it for now. */ + { + case 1: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l+drv_host_audio.e.l+" + "audio_mixer.e.l+audio_test.e.l"); + break; + + case 2: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2+drv_host_audio.e.l.l2+" + "audio_mixer.e.l.l2+audio_test.e.l.l2"); + break; + + case 3: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2.l3+drv_host_audio.e.l.l2.l3+" + "audio_mixer.e.l.l2.l3+audio_test.e.l.l2.l3"); + break; + + case 4: + RT_FALL_THROUGH(); + default: + rc = RTLogGroupSettings(g_pRelLogger, + "drv_audio.e.l.l2.l3.l4.f+drv_host_audio.e.l.l2.l3.l4.f+" + "audio_mixer.e.l.l2.l3.l4.f+audio_test.e.l.l2.l3.l4.f"); + break; + } + if (RT_FAILURE(rc)) + RTMsgError("Setting debug logging failed, rc=%Rrc\n", rc); + } + } + else + RTMsgWarning("Failed to create release logger: %Rrc", rc); + + /* + * Install a Ctrl-C signal handler. + */ +#ifdef RT_OS_WINDOWS + SetConsoleCtrlHandler(audioTestConsoleCtrlHandler, TRUE); +#else + struct sigaction sa; + RT_ZERO(sa); + sa.sa_handler = audioTestSignalHandler; + sigaction(SIGINT, &sa, NULL); +#endif + + /* + * Process common options. + */ + RT_ZERO(GetState); + rc = RTGetOptInit(&GetState, argc, argv, g_aCmdCommonOptions, + RT_ELEMENTS(g_aCmdCommonOptions), 1 /*idxFirst*/, 0 /*fFlags - must not sort! */); + AssertRCReturn(rc, RTEXITCODE_INIT); + + while ((ch = RTGetOpt(&GetState, &ValueUnion)) != 0) + { + switch (ch) + { + AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, NULL); + + case VINF_GETOPT_NOT_OPTION: + { + for (uintptr_t iCmd = 0; iCmd < RT_ELEMENTS(g_apCommands); iCmd++) + { + PCVKATCMD const pCmd = g_apCommands[iCmd]; + if (strcmp(ValueUnion.psz, pCmd->pszCommand) == 0) + { + /* Count the combined option definitions: */ + size_t cCombinedOptions = pCmd->cOptions + RT_ELEMENTS(g_aCmdCommonOptions); + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + cCombinedOptions += g_apTransports[iTx]->cOpts; + + /* Combine the option definitions: */ + PRTGETOPTDEF paCombinedOptions = (PRTGETOPTDEF)RTMemAlloc(cCombinedOptions * sizeof(RTGETOPTDEF)); + if (paCombinedOptions) + { + uint32_t idxOpts = 0; + memcpy(paCombinedOptions, g_aCmdCommonOptions, sizeof(g_aCmdCommonOptions)); + idxOpts += RT_ELEMENTS(g_aCmdCommonOptions); + + memcpy(&paCombinedOptions[idxOpts], pCmd->paOptions, pCmd->cOptions * sizeof(RTGETOPTDEF)); + idxOpts += (uint32_t)pCmd->cOptions; + + if (pCmd->fNeedsTransport) + for (uintptr_t iTx = 0; iTx < g_cTransports; iTx++) + { + memcpy(&paCombinedOptions[idxOpts], + g_apTransports[iTx]->paOpts, g_apTransports[iTx]->cOpts * sizeof(RTGETOPTDEF)); + idxOpts += (uint32_t)g_apTransports[iTx]->cOpts; + } + + /* Re-initialize the option getter state and pass it to the command handler. */ + rc = RTGetOptInit(&GetState, argc, argv, paCombinedOptions, cCombinedOptions, + GetState.iNext /*idxFirst*/, RTGETOPTINIT_FLAGS_OPTS_FIRST); + if (RT_SUCCESS(rc)) + { + RTEXITCODE rcExit = pCmd->pfnHandler(&GetState); + RTMemFree(paCombinedOptions); + return rcExit; + } + RTMemFree(paCombinedOptions); + return RTMsgErrorExitFailure("RTGetOptInit failed for '%s': %Rrc", ValueUnion.psz, rc); + } + return RTMsgErrorExitFailure("Out of memory!"); + } + } + RTMsgError("Unknown command '%s'!\n", ValueUnion.psz); + audioTestListCommands(g_pStdErr); + return RTEXITCODE_SYNTAX; + } + + default: + return RTGetOptPrintError(ch, &ValueUnion); + } + } + + RTMsgError("No command specified!\n"); + audioTestListCommands(g_pStdErr); + return RTEXITCODE_SYNTAX; +} |