summaryrefslogtreecommitdiffstats
path: root/src/VBox/ValidationKit/utils/audio/vkatCmdSelfTest.cpp
blob: 3259398dac8bc9b1067ed35a353a759129d6b90e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
/* $Id: vkatCmdSelfTest.cpp $ */
/** @file
 * Validation Kit Audio Test (VKAT) - Self test.
 *
 * Self-test which does a complete audio testing framework run without the need
 * of a VM or other infrastructure, i.e. all required parts are running locally
 * on the same machine.
 *
 * This self-test does the following:
 * - 1. Creates a separate thread for the guest side VKAT and connects to the ATS instance on
 *      the host side at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST).
 * - 2. Uses the Validation Kit audio backend, which in turn creates an ATS instance
 *      listening at port 6062 (ATS_TCP_DEF_BIND_PORT_VALKIT).
 * - 3. Uses the host test environment which creates an ATS instance
 *      listening at port 6052 (ATS_TCP_DEF_BIND_PORT_HOST).
 * - 4. Executes a complete test run locally (e.g. without any guest (VM) involved).
 */

/*
 * 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                                                                                                                 *
*********************************************************************************************************************************/

#include <iprt/ctype.h>
#include <iprt/errcore.h>
#include <iprt/getopt.h>
#include <iprt/message.h>
#include <iprt/rand.h>
#include <iprt/test.h>

#include "Audio/AudioHlp.h"
#include "Audio/AudioTest.h"
#include "Audio/AudioTestService.h"
#include "Audio/AudioTestServiceClient.h"

#include "vkatInternal.h"


/*********************************************************************************************************************************
*   Internal structures                                                                                                          *
*********************************************************************************************************************************/

/**
 * Structure for keeping a VKAT self test context.
 */
typedef struct SELFTESTCTX
{
    /** Common tag for guest and host side. */
    char             szTag[AUDIOTEST_TAG_MAX];
    /** The driver stack in use. */
    AUDIOTESTDRVSTACK DrvStack;
    /** Audio driver to use.
     *  Defaults to the platform's default driver. */
    PCPDMDRVREG      pDrvReg;
    struct
    {
        AUDIOTESTENV TstEnv;
        /** Where to bind the address of the guest ATS instance to.
         *  Defaults to localhost (127.0.0.1) if empty. */
        char         szAtsAddr[64];
        /** Port of the guest ATS instance.
         *  Defaults to ATS_ALT_PORT if not set. */
        uint32_t     uAtsPort;
    } Guest;
    struct
    {
        AUDIOTESTENV TstEnv;
        /** Address of the guest ATS instance.
         *  Defaults to localhost (127.0.0.1) if not set. */
        char         szGuestAtsAddr[64];
        /** Port of the guest ATS instance.
         *  Defaults to ATS_DEFAULT_PORT if not set. */
        uint32_t     uGuestAtsPort;
        /** Address of the Validation Kit audio driver ATS instance.
         *  Defaults to localhost (127.0.0.1) if not set. */
        char         szValKitAtsAddr[64];
        /** Port of the Validation Kit audio driver ATS instance.
         *  Defaults to ATS_ALT_PORT if not set. */
        uint32_t     uValKitAtsPort;
    } Host;
} SELFTESTCTX;
/** Pointer to a VKAT self test context. */
typedef SELFTESTCTX *PSELFTESTCTX;


/*********************************************************************************************************************************
*   Global Variables                                                                                                             *
*********************************************************************************************************************************/

/** The global self-text context. */
static SELFTESTCTX g_Ctx;


/*********************************************************************************************************************************
*   Driver stack self-test implementation                                                                                        *
*********************************************************************************************************************************/

/**
 * Performs a (quick) audio driver stack self test.
 *
 * Local only, no guest/host communication involved.
 *
 * @returns VBox status code.
 */
int AudioTestDriverStackPerformSelftest(void)
{
    PCPDMDRVREG pDrvReg = AudioTestGetDefaultBackend();

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack started\n");

    AUDIOTESTDRVSTACK DrvStack;
    int rc = audioTestDriverStackProbe(&DrvStack, pDrvReg,
                                       true /* fEnabledIn */, true /* fEnabledOut */, false /* fWithDrvAudio */);
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

    AUDIOTESTIOOPTS IoOpts;
    audioTestIoOptsInitDefaults(&IoOpts);

    PPDMAUDIOSTREAM   pStream;
    PDMAUDIOSTREAMCFG CfgAcq;
    rc = audioTestDriverStackStreamCreateOutput(&DrvStack, &IoOpts.Props,
                                                IoOpts.cMsBufferSize, IoOpts.cMsPreBuffer, IoOpts.cMsSchedulingHint,
                                                &pStream, &CfgAcq);
    AssertRCReturn(rc, rc);

    rc = audioTestDriverStackStreamEnable(&DrvStack, pStream);
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

    RTTEST_CHECK_RET(g_hTest, audioTestDriverStackStreamIsOkay(&DrvStack, pStream), VERR_AUDIO_STREAM_NOT_READY);

    uint8_t abBuf[_4K];
    memset(abBuf, 0x42, sizeof(abBuf));

    uint32_t cbWritten;
    rc = audioTestDriverStackStreamPlay(&DrvStack, pStream, abBuf, sizeof(abBuf), &cbWritten);
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);
    RTTEST_CHECK_RET(g_hTest, cbWritten == sizeof(abBuf), VERR_AUDIO_STREAM_NOT_READY);

    audioTestDriverStackStreamDrain(&DrvStack, pStream, true /* fSync */);
    audioTestDriverStackStreamDestroy(&DrvStack, pStream);

    audioTestDriverStackDelete(&DrvStack);

    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Testing driver stack ended with %Rrc\n", rc);
    return rc;
}


/*********************************************************************************************************************************
*   Self-test implementation                                                                                                     *
*********************************************************************************************************************************/

/**
 * Thread callback for mocking the guest (VM) side of things.
 *
 * @returns VBox status code.
 * @param   hThread             Thread handle.
 * @param   pvUser              Pointer to user-supplied data.
 */
static DECLCALLBACK(int) audioTestSelftestGuestAtsThread(RTTHREAD hThread, void *pvUser)
{
    RT_NOREF(hThread);
    PSELFTESTCTX pCtx = (PSELFTESTCTX)pvUser;

    PAUDIOTESTENV pTstEnvGst = &pCtx->Guest.TstEnv;

    audioTestEnvInit(pTstEnvGst);

    /* Flag the environment for self test mode. */
    pTstEnvGst->fSelftest = true;

    /* Tweak the address the guest ATS is trying to connect to the host if anything else is specified.
     * Note: The host also runs on the same host (this self-test is completely self-contained and does not need a VM). */
    if (!pTstEnvGst->TcpOpts.szConnectAddr[0])
        RTStrCopy(pTstEnvGst->TcpOpts.szConnectAddr, sizeof(pTstEnvGst->TcpOpts.szConnectAddr), "127.0.0.1");

    /* Generate tag for guest side. */
    int rc = RTStrCopy(pTstEnvGst->szTag, sizeof(pTstEnvGst->szTag), pCtx->szTag);
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

    rc = AudioTestPathCreateTemp(pTstEnvGst->szPathTemp, sizeof(pTstEnvGst->szPathTemp), "selftest-guest");
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

    rc = AudioTestPathCreateTemp(pTstEnvGst->szPathOut, sizeof(pTstEnvGst->szPathOut), "selftest-out");
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

    pTstEnvGst->enmMode = AUDIOTESTMODE_GUEST;

    rc = audioTestEnvCreate(pTstEnvGst, &pCtx->DrvStack);
    if (RT_SUCCESS(rc))
    {
        RTThreadUserSignal(hThread);

        rc = audioTestWorker(pTstEnvGst);
        RTTEST_CHECK_RC_OK_RET(g_hTest, rc, rc);

        audioTestEnvDestroy(pTstEnvGst);
    }

    return rc;
}

/**
 * Main function for performing the self test.
 *
 * @returns RTEXITCODE
 * @param   pCtx                Self test context to use.
 */
RTEXITCODE audioTestDoSelftest(PSELFTESTCTX pCtx)
{
    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,  "Running self test ...\n");

    /* Generate a common tag for guest and host side. */
    int rc = AudioTestGenTag(pCtx->szTag, sizeof(pCtx->szTag));
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);

    PAUDIOTESTENV pTstEnvHst = &pCtx->Host.TstEnv;

    audioTestEnvInit(pTstEnvHst);

    /* Flag the environment for self test mode. */
    pTstEnvHst->fSelftest = true;

    /* One test iteration with a 5s maximum test tone is enough for a (quick) self test. */
    pTstEnvHst->cIterations          = 1;
    pTstEnvHst->ToneParms.msDuration = RTRandU32Ex(500, RT_MS_5SEC);

    /* Generate tag for host side. */
    rc = RTStrCopy(pTstEnvHst->szTag, sizeof(pTstEnvHst->szTag), pCtx->szTag);
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);

    rc = AudioTestPathCreateTemp(pTstEnvHst->szPathTemp, sizeof(pTstEnvHst->szPathTemp), "selftest-tmp");
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);

    rc = AudioTestPathCreateTemp(pTstEnvHst->szPathOut, sizeof(pTstEnvHst->szPathOut), "selftest-out");
    RTTEST_CHECK_RC_OK_RET(g_hTest, rc, RTEXITCODE_FAILURE);

    /*
     * Step 1.
     */
    RTTHREAD hThreadGstAts = NIL_RTTHREAD;

    bool const fStartGuestAts = RTStrNLen(pCtx->Host.szGuestAtsAddr, sizeof(pCtx->Host.szGuestAtsAddr)) == 0;
    if (fStartGuestAts)
    {
        /* Step 1b. */
        rc = RTThreadCreate(&hThreadGstAts, audioTestSelftestGuestAtsThread, pCtx, 0, RTTHREADTYPE_IO, RTTHREADFLAGS_WAITABLE,
                            "VKATGstAts");
        if (RT_SUCCESS(rc))
            rc = RTThreadUserWait(hThreadGstAts, RT_MS_30SEC);
    }

    RTThreadSleep(2000); /* Fudge: Wait until guest ATS is up. 2 seconds should be enough (tm). */

    if (RT_SUCCESS(rc))
    {
        /*
         * Steps 2 + 3.
         */
        pTstEnvHst->enmMode = AUDIOTESTMODE_HOST;

        rc = audioTestEnvCreate(pTstEnvHst, &pCtx->DrvStack);
        if (RT_SUCCESS(rc))
        {
            /*
             * Step 4.
             */
            rc = audioTestWorker(pTstEnvHst);
            RTTEST_CHECK_RC_OK(g_hTest, rc);

            audioTestEnvDestroy(pTstEnvHst);
        }
    }

    /*
     * Shutting down.
     */
    RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS,  "Shutting down self test\n");

    /* If we started the guest ATS ourselves, wait for it to terminate properly. */
    if (fStartGuestAts)
    {
        int rcThread;
        int rc2 = RTThreadWait(hThreadGstAts, RT_MS_30SEC, &rcThread);
        if (RT_SUCCESS(rc2))
            rc2 = rcThread;
        if (RT_FAILURE(rc2))
            RTTestFailed(g_hTest, "Shutting down guest ATS failed with %Rrc\n", rc2);
        if (RT_SUCCESS(rc))
            rc = rc2;
    }

    if (RT_FAILURE(rc))
        RTTestFailed(g_hTest, "Self test failed with %Rrc\n", rc);

    return RT_SUCCESS(rc) ?  RTEXITCODE_SUCCESS : RTEXITCODE_FAILURE;
}


/*********************************************************************************************************************************
*   Command: selftest                                                                                                            *
*********************************************************************************************************************************/

/**
 * Command line parameters for self-test mode.
 */
static const RTGETOPTDEF s_aCmdSelftestOptions[] =
{
    { "--exclude-all",      'a',                                RTGETOPT_REQ_NOTHING },
    { "--backend",          'b',                                RTGETOPT_REQ_STRING  },
    { "--with-drv-audio",   'd',                                RTGETOPT_REQ_NOTHING },
    { "--with-mixer",       'm',                                RTGETOPT_REQ_NOTHING },
    { "--exclude",          'e',                                RTGETOPT_REQ_UINT32  },
    { "--include",          'i',                                RTGETOPT_REQ_UINT32  }
};

/** the 'selftest' command option help. */
static DECLCALLBACK(const char *) audioTestCmdSelftestHelp(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 'm': return "Use the internal mixing engine explicitly";
        default:  return NULL;
    }
}

/**
 * The 'selftest' command handler.
 *
 * @returns Program exit code.
 * @param   pGetState   RTGetOpt state.
 */
DECLCALLBACK(RTEXITCODE) audioTestCmdSelftestHandler(PRTGETOPTSTATE pGetState)
{
    RT_ZERO(g_Ctx);

    audioTestEnvInit(&g_Ctx.Guest.TstEnv);
    audioTestEnvInit(&g_Ctx.Host.TstEnv);

    AUDIOTESTIOOPTS IoOpts;
    audioTestIoOptsInitDefaults(&IoOpts);

    /* Argument processing loop: */
    int           rc;
    RTGETOPTUNION ValueUnion;
    while ((rc = RTGetOpt(pGetState, &ValueUnion)) != 0)
    {
        switch (rc)
        {
            case 'a':
                for (unsigned i = 0; i < g_cTests; i++)
                    g_aTests[i].fExcluded = true;
                break;

            case 'b':
                g_Ctx.pDrvReg = AudioTestFindBackendOpt(ValueUnion.psz);
                if (g_Ctx.pDrvReg == NULL)
                    return RTEXITCODE_SYNTAX;
                break;

            case 'd':
                IoOpts.fWithDrvAudio = true;
                break;

            case 'e':
                if (ValueUnion.u32 >= g_cTests)
                    return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --exclude", ValueUnion.u32);
                g_aTests[ValueUnion.u32].fExcluded = true;
                break;

            case 'i':
                if (ValueUnion.u32 >= g_cTests)
                    return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Invalid test number %u passed to --include", ValueUnion.u32);
                g_aTests[ValueUnion.u32].fExcluded = false;
                break;

            case 'm':
                IoOpts.fWithMixer = true;
                break;

            AUDIO_TEST_COMMON_OPTION_CASES(ValueUnion, &g_CmdSelfTest);

            default:
                return RTGetOptPrintError(rc, &ValueUnion);
        }
    }

    /* For simplicity both test environments, guest and host, will have the same I/O options.
     ** @todo Make this indepedent by a prefix, "--[guest|host]-<option>" -> e.g. "--guest-with-drv-audio". */
    memcpy(&g_Ctx.Guest.TstEnv.IoOpts, &IoOpts, sizeof(AUDIOTESTIOOPTS));
    memcpy(&g_Ctx.Host.TstEnv.IoOpts,  &IoOpts, sizeof(AUDIOTESTIOOPTS));

    rc = AudioTestDriverStackPerformSelftest();
    if (RT_FAILURE(rc))
        return RTMsgErrorExit(RTEXITCODE_FAILURE, "Testing driver stack failed: %Rrc\n", rc);

    /* Go with the Validation Kit audio backend if nothing else is specified. */
    if (g_Ctx.pDrvReg == NULL)
        g_Ctx.pDrvReg = AudioTestFindBackendOpt("valkit");

    /*
     * In self-test mode the guest and the host side have to share the same driver stack,
     * as we don't have any device emulation between the two sides.
     *
     * This is necessary to actually get the played/recorded audio to from/to the guest
     * and host respectively.
     *
     * Choosing any other backend than the Validation Kit above *will* break this self-test!
     */
    rc = audioTestDriverStackInitEx(&g_Ctx.DrvStack, g_Ctx.pDrvReg,
                                    true /* fEnabledIn */, true /* fEnabledOut */, g_Ctx.Host.TstEnv.IoOpts.fWithDrvAudio);
    if (RT_FAILURE(rc))
        return RTMsgErrorExit(RTEXITCODE_SYNTAX, "Unable to init driver stack: %Rrc\n", rc);

    /*
     * Start testing.
     */
    RTTestBanner(g_hTest);

    int rc2 = audioTestDoSelftest(&g_Ctx);
    if (RT_FAILURE(rc2))
        RTTestFailed(g_hTest, "Self test failed with rc=%Rrc", rc2);

    audioTestDriverStackDelete(&g_Ctx.DrvStack);

    /*
     * Print summary and exit.
     */
    return RTTestSummaryAndDestroy(g_hTest);
}

/**
 * Command table entry for 'selftest'.
 */
const VKATCMD g_CmdSelfTest =
{
    "selftest",
    audioTestCmdSelftestHandler,
    "Performs self-tests.",
    s_aCmdSelftestOptions,
    RT_ELEMENTS(s_aCmdSelftestOptions),
    audioTestCmdSelftestHelp,
    true /* fNeedsTransport */
};