VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/utils/audio/vkatCommon.cpp@ 89962

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

Audio/ValKit: Initial implementation / support for NATed VMs by using reversed (server) connections. The ATS client now also makes use of the transport layer and now can also be configured more flexible on a per-transport layer basis. bugref:10008

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 38.6 KB
Line 
1/* $Id: vkatCommon.cpp 89962 2021-06-30 07:02:07Z vboxsync $ */
2/** @file
3 * Validation Kit Audio Test (VKAT) - Self test code.
4 */
5
6/*
7 * Copyright (C) 2021 Oracle Corporation
8 *
9 * This file is part of VirtualBox Open Source Edition (OSE), as
10 * available from http://www.virtualbox.org. This file is free software;
11 * you can redistribute it and/or modify it under the terms of the GNU
12 * General Public License (GPL) as published by the Free Software
13 * Foundation, in version 2 as it comes in the "COPYING" file of the
14 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
15 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
16 *
17 * The contents of this file may alternatively be used under the terms
18 * of the Common Development and Distribution License Version 1.0
19 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
20 * VirtualBox OSE distribution, in which case the provisions of the
21 * CDDL are applicable instead of those of the GPL.
22 *
23 * You may elect to license modified versions of this file under the
24 * terms and conditions of either the GPL or the CDDL or both.
25 */
26
27
28/*********************************************************************************************************************************
29* Header Files *
30*********************************************************************************************************************************/
31
32#include <iprt/ctype.h>
33#include <iprt/dir.h>
34#include <iprt/errcore.h>
35#include <iprt/getopt.h>
36#include <iprt/message.h>
37#include <iprt/rand.h>
38#include <iprt/test.h>
39
40#include "Audio/AudioHlp.h"
41#include "Audio/AudioTest.h"
42#include "Audio/AudioTestService.h"
43#include "Audio/AudioTestServiceClient.h"
44
45#include "vkatInternal.h"
46
47
48/*********************************************************************************************************************************
49* Defined Constants And Macros *
50*********************************************************************************************************************************/
51/**
52 * Structure for keeping a user context for the test service callbacks.
53 */
54typedef struct ATSCALLBACKCTX
55{
56 /** The test environment bound to this context. */
57 PAUDIOTESTENV pTstEnv;
58 /** Absolute path to the packed up test set archive.
59 * Keep it simple for now and only support one (open) archive at a time. */
60 char szTestSetArchive[RTPATH_MAX];
61 /** File handle to the (opened) test set archive for reading. */
62 RTFILE hTestSetArchive;
63} ATSCALLBACKCTX;
64typedef ATSCALLBACKCTX *PATSCALLBACKCTX;
65
66
67/*********************************************************************************************************************************
68* Internal Functions *
69*********************************************************************************************************************************/
70static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream, PDMAUDIODIR enmDir, PCPDMAUDIOPCMPROPS pProps, bool fWithMixer, uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint);
71static int audioTestStreamDestroy(PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream);
72static int audioTestDevicesEnumerateAndCheck(PAUDIOTESTENV pTstEnv, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev);
73
74
75/*********************************************************************************************************************************
76* Device enumeration + handling. *
77*********************************************************************************************************************************/
78
79/**
80 * Enumerates audio devices and optionally searches for a specific device.
81 *
82 * @returns VBox status code.
83 * @param pTstEnv Test env to use for enumeration.
84 * @param pszDev Device name to search for. Can be NULL if the default device shall be used.
85 * @param ppDev Where to return the pointer of the device enumeration of \a pTstEnv when a
86 * specific device was found.
87 */
88static int audioTestDevicesEnumerateAndCheck(PAUDIOTESTENV pTstEnv, const char *pszDev, PPDMAUDIOHOSTDEV *ppDev)
89{
90#ifdef DEBUG_andy
91 return VINF_SUCCESS;
92#endif
93
94 RTTestSubF(g_hTest, "Enumerating audio devices and checking for device '%s'", pszDev ? pszDev : "<Default>");
95
96 if (!pTstEnv->DrvStack.pIHostAudio->pfnGetDevices)
97 {
98 RTTestSkipped(g_hTest, "Backend does not support device enumeration, skipping");
99 return VINF_NOT_SUPPORTED;
100 }
101
102 Assert(pszDev == NULL || ppDev);
103
104 if (ppDev)
105 *ppDev = NULL;
106
107 int rc = pTstEnv->DrvStack.pIHostAudio->pfnGetDevices(pTstEnv->DrvStack.pIHostAudio, &pTstEnv->DevEnum);
108 if (RT_SUCCESS(rc))
109 {
110 PPDMAUDIOHOSTDEV pDev;
111 RTListForEach(&pTstEnv->DevEnum.LstDevices, pDev, PDMAUDIOHOSTDEV, ListEntry)
112 {
113 char szFlags[PDMAUDIOHOSTDEV_MAX_FLAGS_STRING_LEN];
114 if (pDev->pszId)
115 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s' (ID '%s'):\n", pDev->pszName, pDev->pszId);
116 else
117 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Device '%s':\n", pDev->pszName);
118 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Usage = %s\n", PDMAudioDirGetName(pDev->enmUsage));
119 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Flags = %s\n", PDMAudioHostDevFlagsToString(szFlags, pDev->fFlags));
120 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Input channels = %RU8\n", pDev->cMaxInputChannels);
121 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enum: Output channels = %RU8\n", pDev->cMaxOutputChannels);
122
123 if ( pszDev
124 && !RTStrCmp(pDev->pszName, pszDev))
125 {
126 *ppDev = pDev;
127 }
128 }
129 }
130 else
131 RTTestFailed(g_hTest, "Enumerating audio devices failed with %Rrc", rc);
132
133 RTTestSubDone(g_hTest);
134
135 if ( pszDev
136 && *ppDev == NULL)
137 {
138 RTTestFailed(g_hTest, "Audio device '%s' not found", pszDev);
139 return VERR_NOT_FOUND;
140 }
141
142 return VINF_SUCCESS;
143}
144
145static int audioTestStreamInit(PAUDIOTESTDRVSTACK pDrvStack, PAUDIOTESTSTREAM pStream,
146 PDMAUDIODIR enmDir, PCPDMAUDIOPCMPROPS pProps, bool fWithMixer,
147 uint32_t cMsBufferSize, uint32_t cMsPreBuffer, uint32_t cMsSchedulingHint)
148{
149 int rc;
150
151 if (enmDir == PDMAUDIODIR_IN)
152 rc = audioTestDriverStackStreamCreateInput(pDrvStack, pProps, cMsBufferSize,
153 cMsPreBuffer, cMsSchedulingHint, &pStream->pStream, &pStream->Cfg);
154 else if (enmDir == PDMAUDIODIR_OUT)
155 rc = audioTestDriverStackStreamCreateOutput(pDrvStack, pProps, cMsBufferSize,
156 cMsPreBuffer, cMsSchedulingHint, &pStream->pStream, &pStream->Cfg);
157 else
158 rc = VERR_NOT_SUPPORTED;
159
160 if (RT_SUCCESS(rc))
161 {
162 if (!pDrvStack->pIAudioConnector)
163 {
164 pStream->pBackend = &((PAUDIOTESTDRVSTACKSTREAM)pStream->pStream)->Backend;
165 }
166 else
167 pStream->pBackend = NULL;
168
169 /*
170 * Automatically enable the mixer if the PCM properties don't match.
171 */
172 if ( !fWithMixer
173 && !PDMAudioPropsAreEqual(pProps, &pStream->Cfg.Props))
174 {
175 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Enabling stream mixer\n");
176 fWithMixer = true;
177 }
178
179 rc = AudioTestMixStreamInit(&pStream->Mix, pDrvStack, pStream->pStream,
180 fWithMixer ? pProps : NULL, 100 /* ms */); /** @todo Configure mixer buffer? */
181 }
182
183 if (RT_FAILURE(rc))
184 RTTestFailed(g_hTest, "Initializing %s stream failed with %Rrc", enmDir == PDMAUDIODIR_IN ? "input" : "output", rc);
185
186 return rc;
187}
188
189/**
190 * Destroys an audio test stream.
191 *
192 * @returns VBox status code.
193 * @param pTstEnv Test environment the stream to destroy contains.
194 * @param pStream Audio stream to destroy.
195 */
196static int audioTestStreamDestroy(PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream)
197{
198 int rc = VINF_SUCCESS;
199 if (pStream && pStream->pStream)
200 {
201 /** @todo Anything else to do here, e.g. test if there are left over samples or some such? */
202
203 audioTestDriverStackStreamDestroy(&pTstEnv->DrvStack, pStream->pStream);
204 pStream->pStream = NULL;
205 pStream->pBackend = NULL;
206 }
207
208 AudioTestMixStreamTerm(&pStream->Mix);
209
210 return rc;
211}
212
213
214/*********************************************************************************************************************************
215* Test Primitives *
216*********************************************************************************************************************************/
217
218#if 0 /* Unused */
219/**
220 * Returns a random scheduling hint (in ms).
221 */
222DECLINLINE(uint32_t) audioTestEnvGetRandomSchedulingHint(void)
223{
224 static const unsigned s_aSchedulingHintsMs[] =
225 {
226 10,
227 25,
228 50,
229 100,
230 200,
231 250
232 };
233
234 return s_aSchedulingHintsMs[RTRandU32Ex(0, RT_ELEMENTS(s_aSchedulingHintsMs) - 1)];
235}
236#endif
237
238/**
239 * Plays a test tone on a specific audio test stream.
240 *
241 * @returns VBox status code.
242 * @param pTstEnv Test environment to use for running the test.
243 * @param pStream Stream to use for playing the tone.
244 * @param pParms Tone parameters to use.
245 *
246 * @note Blocking function.
247 */
248static int audioTestPlayTone(PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms)
249{
250 AUDIOTESTTONE TstTone;
251 AudioTestToneInit(&TstTone, &pStream->Cfg.Props, pParms->dbFreqHz);
252
253 const char *pcszPathOut = pTstEnv->Set.szPathAbs;
254
255 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Playing test tone (tone frequency is %RU16Hz, %RU32ms)\n", (uint16_t)pParms->dbFreqHz, pParms->msDuration);
256 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using %RU32ms stream scheduling hint\n", pStream->Cfg.Device.cMsSchedulingHint);
257 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Writing to '%s'\n", pcszPathOut);
258
259 /** @todo Use .WAV here? */
260 PAUDIOTESTOBJ pObj;
261 int rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-play.pcm", &pObj);
262 AssertRCReturn(rc, rc);
263
264 rc = AudioTestMixStreamEnable(&pStream->Mix);
265 if ( RT_SUCCESS(rc)
266 && AudioTestMixStreamIsOkay(&pStream->Mix))
267 {
268 uint8_t abBuf[_4K];
269
270 uint32_t cbToPlayTotal = PDMAudioPropsMilliToBytes(&pStream->Cfg.Props, pParms->msDuration);
271 AssertStmt(cbToPlayTotal, rc = VERR_INVALID_PARAMETER);
272
273 RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Playing %RU32 bytes total\n", cbToPlayTotal);
274
275 AudioTestSetObjAddMetadataStr(pObj, "stream_to_play_bytes=%RU32\n", cbToPlayTotal);
276 AudioTestSetObjAddMetadataStr(pObj, "stream_period_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPeriod);
277 AudioTestSetObjAddMetadataStr(pObj, "stream_buffer_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesBufferSize);
278 AudioTestSetObjAddMetadataStr(pObj, "stream_prebuf_size_frames=%RU32\n", pStream->Cfg.Backend.cFramesPreBuffering);
279 /* Note: This mostly is provided by backend (e.g. PulseAudio / ALSA / ++) and
280 * has nothing to do with the device emulation scheduling hint. */
281 AudioTestSetObjAddMetadataStr(pObj, "device_scheduling_hint_ms=%RU32\n", pStream->Cfg.Device.cMsSchedulingHint);
282
283 while (cbToPlayTotal)
284 {
285 uint32_t cbPlayed = 0;
286 uint32_t const cbCanWrite = AudioTestMixStreamGetWritable(&pStream->Mix);
287 if (cbCanWrite)
288 {
289 uint32_t const cbToGenerate = RT_MIN(RT_MIN(cbToPlayTotal, sizeof(abBuf)), cbCanWrite);
290 uint32_t cbToPlay;
291 rc = AudioTestToneGenerate(&TstTone, abBuf, cbToGenerate, &cbToPlay);
292 if (RT_SUCCESS(rc))
293 {
294 RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Playing %RU32 bytes ...\n", cbToPlay);
295
296 /* Write stuff to disk before trying to play it. Help analysis later. */
297 rc = AudioTestSetObjWrite(pObj, abBuf, cbToPlay);
298 if (RT_SUCCESS(rc))
299 rc = AudioTestMixStreamPlay(&pStream->Mix, abBuf, cbToPlay, &cbPlayed);
300 }
301
302 if (RT_FAILURE(rc))
303 break;
304 }
305 else if (AudioTestMixStreamIsOkay(&pStream->Mix))
306 RTThreadSleep(RT_MIN(RT_MAX(1, pStream->Cfg.Device.cMsSchedulingHint), 256));
307 else
308 AssertFailedBreakStmt(rc = VERR_AUDIO_STREAM_NOT_READY);
309
310 Assert(cbToPlayTotal >= cbPlayed);
311 cbToPlayTotal -= cbPlayed;
312 }
313
314 if (cbToPlayTotal != 0)
315 RTTestFailed(g_hTest, "Playback ended unexpectedly (%RU32 bytes left)\n", cbToPlayTotal);
316 }
317 else
318 rc = VERR_AUDIO_STREAM_NOT_READY;
319
320 int rc2 = AudioTestSetObjClose(pObj);
321 if (RT_SUCCESS(rc))
322 rc = rc2;
323
324 if (RT_FAILURE(rc))
325 RTTestFailed(g_hTest, "Playing tone failed with %Rrc\n", rc);
326
327 return rc;
328}
329
330/**
331 * Records a test tone from a specific audio test stream.
332 *
333 * @returns VBox status code.
334 * @param pTstEnv Test environment to use for running the test.
335 * @param pStream Stream to use for recording the tone.
336 * @param pParms Tone parameters to use.
337 *
338 * @note Blocking function.
339 */
340static int audioTestRecordTone(PAUDIOTESTENV pTstEnv, PAUDIOTESTSTREAM pStream, PAUDIOTESTTONEPARMS pParms)
341{
342 const char *pcszPathOut = pTstEnv->Set.szPathAbs;
343
344 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Recording test tone (for %RU32ms)\n", pParms->msDuration);
345 RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Writing to '%s'\n", pcszPathOut);
346
347 /** @todo Use .WAV here? */
348 PAUDIOTESTOBJ pObj;
349 int rc = AudioTestSetObjCreateAndRegister(&pTstEnv->Set, "guest-tone-rec.pcm", &pObj);
350 AssertRCReturn(rc, rc);
351
352 if (audioTestDriverStackStreamIsOkay(&pTstEnv->DrvStack, pStream->pStream))
353 {
354 const uint32_t cbPerSched = PDMAudioPropsMilliToBytes(&pParms->Props, pTstEnv->cMsSchedulingHint);
355 AssertStmt(cbPerSched, rc = VERR_INVALID_PARAMETER);
356 uint32_t cbToRead = PDMAudioPropsMilliToBytes(&pParms->Props, pParms->msDuration);
357 AssertStmt(cbToRead, rc = VERR_INVALID_PARAMETER);
358
359 if (RT_SUCCESS(rc))
360 {
361 AudioTestSetObjAddMetadataStr(pObj, "buffer_size_ms=%RU32\n", pTstEnv->cMsBufferSize);
362 AudioTestSetObjAddMetadataStr(pObj, "prebuf_size_ms=%RU32\n", pTstEnv->cMsPreBuffer);
363 AudioTestSetObjAddMetadataStr(pObj, "scheduling_hint_ms=%RU32\n", pTstEnv->cMsSchedulingHint);
364
365 uint8_t abBuf[_4K];
366
367 while (cbToRead)
368 {
369 const uint32_t cbChunk = RT_MIN(cbToRead, RT_MIN(cbPerSched, sizeof(abBuf)));
370
371 uint32_t cbRead = 0;
372 rc = audioTestDriverStackStreamCapture(&pTstEnv->DrvStack, pStream->pStream, (void *)abBuf, cbChunk, &cbRead);
373 if (RT_SUCCESS(rc))
374 rc = AudioTestSetObjWrite(pObj, abBuf, cbRead);
375
376 if (RT_FAILURE(rc))
377 break;
378
379 RTThreadSleep(pTstEnv->cMsSchedulingHint);
380
381 Assert(cbToRead >= cbRead);
382 cbToRead -= cbRead;
383 }
384 }
385 }
386 else
387 rc = VERR_AUDIO_STREAM_NOT_READY;
388
389 int rc2 = AudioTestSetObjClose(pObj);
390 if (RT_SUCCESS(rc))
391 rc = rc2;
392
393 if (RT_FAILURE(rc))
394 RTTestFailed(g_hTest, "Recording tone done failed with %Rrc\n", rc);
395
396 return rc;
397}
398
399
400/*********************************************************************************************************************************
401* ATS Callback Implementations *
402*********************************************************************************************************************************/
403
404/** @copydoc ATSCALLBACKS::pfnTestSetBegin
405 *
406 * @note Runs as part of the guest ATS.
407 */
408static DECLCALLBACK(int) audioTestGstAtsTestSetBeginCallback(void const *pvUser, const char *pszTag)
409{
410 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
411 PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
412
413 RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Beginning test set '%s' in '%s'\n", pszTag, pTstEnv->szPathTemp);
414
415 return AudioTestSetCreate(&pTstEnv->Set, pTstEnv->szPathTemp, pszTag);
416}
417
418/** @copydoc ATSCALLBACKS::pfnTestSetEnd
419 *
420 * @note Runs as part of the guest ATS.
421 */
422static DECLCALLBACK(int) audioTestGstAtsTestSetEndCallback(void const *pvUser, const char *pszTag)
423{
424 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
425 PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
426
427 RTTestPrintf(g_hTest, RTTESTLVL_DEBUG, "Ending test set '%s'\n", pszTag);
428
429 /* Pack up everything to be ready for transmission. */
430 return audioTestEnvPrologue(pTstEnv, true /* fPack */, pCtx->szTestSetArchive, sizeof(pCtx->szTestSetArchive));
431}
432
433/** @copydoc ATSCALLBACKS::pfnTonePlay
434 *
435 * @note Runs as part of the guest ATS.
436 */
437static DECLCALLBACK(int) audioTestGstAtsTonePlayCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)
438{
439 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
440 PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
441
442 AUDIOTESTTONE TstTone;
443 AudioTestToneInitRandom(&TstTone, &pToneParms->Props);
444
445 const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */
446
447 int rc = audioTestStreamInit(&pTstEnv->DrvStack, pTstStream, PDMAUDIODIR_OUT, &pTstEnv->Props, false /* fWithMixer */,
448 pTstEnv->cMsBufferSize, pTstEnv->cMsPreBuffer, pTstEnv->cMsSchedulingHint);
449 if (RT_SUCCESS(rc))
450 {
451 AUDIOTESTPARMS TstParms;
452 RT_ZERO(TstParms);
453 TstParms.enmType = AUDIOTESTTYPE_TESTTONE_PLAY;
454 TstParms.enmDir = PDMAUDIODIR_OUT;
455 TstParms.TestTone = *pToneParms;
456
457 PAUDIOTESTENTRY pTst;
458 rc = AudioTestSetTestBegin(&pTstEnv->Set, "Playing test tone", &TstParms, &pTst);
459 if (RT_SUCCESS(rc))
460 {
461 rc = audioTestPlayTone(pTstEnv, pTstStream, pToneParms);
462 if (RT_SUCCESS(rc))
463 {
464 AudioTestSetTestDone(pTst);
465 }
466 else
467 AudioTestSetTestFailed(pTst, rc, "Playing tone failed");
468 }
469
470 int rc2 = audioTestStreamDestroy(pTstEnv, pTstStream);
471 if (RT_SUCCESS(rc))
472 rc = rc2;
473 }
474 else
475 RTTestFailed(g_hTest, "Error creating output stream, rc=%Rrc\n", rc);
476
477 return rc;
478}
479
480/** @copydoc ATSCALLBACKS::pfnToneRecord */
481static DECLCALLBACK(int) audioTestGstAtsToneRecordCallback(void const *pvUser, PAUDIOTESTTONEPARMS pToneParms)
482{
483 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
484 PAUDIOTESTENV pTstEnv = pCtx->pTstEnv;
485
486 const PAUDIOTESTSTREAM pTstStream = &pTstEnv->aStreams[0]; /** @todo Make this dynamic. */
487
488 int rc = audioTestStreamInit(&pTstEnv->DrvStack, pTstStream, PDMAUDIODIR_IN, &pTstEnv->Props, false /* fWithMixer */,
489 pTstEnv->cMsBufferSize, pTstEnv->cMsPreBuffer, pTstEnv->cMsSchedulingHint);
490 if (RT_SUCCESS(rc))
491 {
492 AUDIOTESTPARMS TstParms;
493 RT_ZERO(TstParms);
494 TstParms.enmType = AUDIOTESTTYPE_TESTTONE_RECORD;
495 TstParms.enmDir = PDMAUDIODIR_IN;
496 TstParms.Props = pToneParms->Props;
497 TstParms.TestTone = *pToneParms;
498
499 PAUDIOTESTENTRY pTst;
500 rc = AudioTestSetTestBegin(&pTstEnv->Set, "Recording test tone from host", &TstParms, &pTst);
501 if (RT_SUCCESS(rc))
502 {
503 rc = audioTestRecordTone(pTstEnv, pTstStream, pToneParms);
504 if (RT_SUCCESS(rc))
505 {
506 AudioTestSetTestDone(pTst);
507 }
508 else
509 AudioTestSetTestFailed(pTst, rc, "Recording tone failed");
510 }
511
512 int rc2 = audioTestStreamDestroy(pTstEnv, pTstStream);
513 if (RT_SUCCESS(rc))
514 rc = rc2;
515 }
516 else
517 RTTestFailed(g_hTest, "Error creating input stream, rc=%Rrc\n", rc);
518
519 return rc;
520}
521
522/** @copydoc ATSCALLBACKS::pfnTestSetSendBegin */
523static DECLCALLBACK(int) audioTestGstAtsTestSetSendBeginCallback(void const *pvUser, const char *pszTag)
524{
525 RT_NOREF(pszTag);
526
527 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
528
529 if (!RTFileExists(pCtx->szTestSetArchive)) /* Has the archive successfully been created yet? */
530 return VERR_WRONG_ORDER;
531
532 int rc = RTFileOpen(&pCtx->hTestSetArchive, pCtx->szTestSetArchive, RTFILE_O_READ | RTFILE_O_OPEN | RTFILE_O_DENY_WRITE);
533 if (RT_SUCCESS(rc))
534 {
535 uint64_t uSize;
536 rc = RTFileQuerySize(pCtx->hTestSetArchive, &uSize);
537 if (RT_SUCCESS(rc))
538 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Sending test set '%s' (%zu bytes)\n", pCtx->szTestSetArchive, uSize);
539 }
540
541 return rc;
542}
543
544/** @copydoc ATSCALLBACKS::pfnTestSetSendRead */
545static DECLCALLBACK(int) audioTestGstAtsTestSetSendReadCallback(void const *pvUser,
546 const char *pszTag, void *pvBuf, size_t cbBuf, size_t *pcbRead)
547{
548 RT_NOREF(pszTag);
549
550 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
551
552 return RTFileRead(pCtx->hTestSetArchive, pvBuf, cbBuf, pcbRead);
553}
554
555/** @copydoc ATSCALLBACKS::pfnTestSetSendEnd */
556static DECLCALLBACK(int) audioTestGstAtsTestSetSendEndCallback(void const *pvUser, const char *pszTag)
557{
558 RT_NOREF(pszTag);
559
560 PATSCALLBACKCTX pCtx = (PATSCALLBACKCTX)pvUser;
561
562 int rc = RTFileClose(pCtx->hTestSetArchive);
563 if (RT_SUCCESS(rc))
564 {
565 pCtx->hTestSetArchive = NIL_RTFILE;
566 }
567
568 return rc;
569}
570
571
572/*********************************************************************************************************************************
573* Implementation of audio test environment handling *
574*********************************************************************************************************************************/
575
576/**
577 * Connects an ATS client via TCP/IP to a peer.
578 *
579 * @returns VBox status code.
580 * @param pTstEnv Test environment to use.
581 * @param pClient Client to connect.
582 * @param pszWhat Hint of what to connect to where.
583 * @param pszTcpBindAddr TCP/IP bind address. Optionl and can be NULL.
584 * Server mode will be disabled then.
585 * @param uTcpBindPort TCP/IP bind port. Optionl and can be 0.
586 * Server mode will be disabled then. *
587 * @param pszTcpConnectAddr TCP/IP connect address. Optionl and can be NULL.
588 * Client mode will be disabled then.
589 * @param uTcpConnectPort TCP/IP connect port. Optionl and can be 0.
590 * Client mode will be disabled then.
591 */
592int audioTestEnvConnectViaTcp(PAUDIOTESTENV pTstEnv, PATSCLIENT pClient, const char *pszWhat,
593 const char *pszTcpBindAddr, uint16_t uTcpBindPort,
594 const char *pszTcpConnectAddr, uint16_t uTcpConnectPort)
595{
596 RT_NOREF(pTstEnv);
597
598 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting %s ...\n", pszWhat);
599
600 RTGETOPTUNION Val;
601 RT_ZERO(Val);
602
603 int rc;
604
605 if ( !pszTcpBindAddr
606 || !uTcpBindPort)
607 {
608 Val.psz = "client";
609 }
610 else if ( !pszTcpConnectAddr
611 || !uTcpConnectPort)
612 {
613 Val.psz = "server";
614 }
615 else
616 Val.psz = "both";
617
618 rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_MODE, &Val);
619 AssertRCReturn(rc, rc);
620
621 if ( !RTStrCmp(Val.psz, "client")
622 || !RTStrCmp(Val.psz, "both"))
623 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connecting at %s:%RU32\n", pszTcpConnectAddr, uTcpConnectPort);
624
625 if ( !RTStrCmp(Val.psz, "server")
626 || !RTStrCmp(Val.psz, "both"))
627 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Listening at %s:%RU32\n", pszTcpBindAddr ? pszTcpBindAddr : "<None>", uTcpBindPort);
628
629 if (pszTcpBindAddr)
630 {
631 Val.psz = pszTcpBindAddr;
632 rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_ADDRESS, &Val);
633 AssertRCReturn(rc, rc);
634 }
635
636 if (uTcpBindPort)
637 {
638 Val.u16 = uTcpBindPort;
639 rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_BIND_PORT, &Val);
640 AssertRCReturn(rc, rc);
641 }
642
643 if (pszTcpConnectAddr)
644 {
645 Val.psz = pszTcpConnectAddr;
646 rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_ADDRESS, &Val);
647 AssertRCReturn(rc, rc);
648 }
649
650 if (uTcpConnectPort)
651 {
652 Val.u16 = uTcpConnectPort;
653 rc = AudioTestSvcClientHandleOption(pClient, ATSTCPOPT_CONNECT_PORT, &Val);
654 AssertRCReturn(rc, rc);
655 }
656
657 rc = AudioTestSvcClientConnect(pClient);
658 if (RT_FAILURE(rc))
659 {
660 RTTestFailed(g_hTest, "Connecting %s failed with %Rrc\n", pszWhat, rc);
661 return rc;
662 }
663
664 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Connected %s\n", pszWhat);
665 return rc;
666}
667
668/**
669 * Configures and starts an ATS TCP/IP server.
670 *
671 * @returns VBox status code.
672 * @param pSrv ATS server instance to configure and start.
673 * @param pCallbacks ATS callback table to use.
674 * @param pszDesc Hint of server type which is being started.
675 * @param pszTcpBindAddr TCP/IP bind address. Optionl and can be NULL.
676 * Server mode will be disabled then.
677 * @param uTcpBindPort TCP/IP bind port. Optionl and can be 0.
678 * Server mode will be disabled then. *
679 * @param pszTcpConnectAddr TCP/IP connect address. Optionl and can be NULL.
680 * Client mode will be disabled then.
681 * @param uTcpConnectPort TCP/IP connect port. Optionl and can be 0.
682 * Client mode will be disabled then.
683 */
684int audioTestEnvConfigureAndStartTcpServer(PATSSERVER pSrv, PCATSCALLBACKS pCallbacks, const char *pszDesc,
685 const char *pszTcpBindAddr, uint16_t uTcpBindPort,
686 const char *pszTcpConnectAddr, uint16_t uTcpConnectPort)
687{
688 RTGETOPTUNION Val;
689 RT_ZERO(Val);
690
691 if (pszTcpBindAddr)
692 {
693 Val.psz = pszTcpBindAddr;
694 AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_ADDRESS, &Val);
695 }
696
697 if (uTcpBindPort)
698 {
699 Val.u16 = uTcpBindPort;
700 AudioTestSvcHandleOption(pSrv, ATSTCPOPT_BIND_PORT, &Val);
701 }
702
703 if (pszTcpConnectAddr)
704 {
705 Val.psz = pszTcpConnectAddr;
706 AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_ADDRESS, &Val);
707 }
708
709 if (uTcpConnectPort)
710 {
711 Val.u16 = uTcpConnectPort;
712 AudioTestSvcHandleOption(pSrv, ATSTCPOPT_CONNECT_PORT, &Val);
713 }
714
715 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Starting server for %s at %s:%RU32 ...\n",
716 pszDesc, pszTcpBindAddr[0] ? pszTcpBindAddr : "0.0.0.0", uTcpBindPort);
717 if (pszTcpConnectAddr[0])
718 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Trying %s to connect as client to %s:%RU32 ...\n",
719 pszDesc, pszTcpConnectAddr[0] ? pszTcpConnectAddr : "0.0.0.0", uTcpConnectPort);
720
721 int rc = AudioTestSvcInit(pSrv, pCallbacks);
722 if (RT_SUCCESS(rc))
723 rc = AudioTestSvcStart(pSrv);
724
725 if (RT_FAILURE(rc))
726 RTTestFailed(g_hTest, "Starting server for %s failed with %Rrc\n", pszDesc, rc);
727
728 return rc;
729}
730
731/**
732 * Initializes an audio test environment.
733 *
734 * @param pTstEnv Audio test environment to initialize.
735 * @param pDrvReg Audio driver to use.
736 * @param fWithDrvAudio Whether to include DrvAudio in the stack or not.
737 */
738int audioTestEnvInit(PAUDIOTESTENV pTstEnv,
739 PCPDMDRVREG pDrvReg, bool fWithDrvAudio)
740{
741 int rc = VINF_SUCCESS;
742
743 /*
744 * Set sane defaults if not already set.
745 */
746 if (!RTStrNLen(pTstEnv->szTag, sizeof(pTstEnv->szTag)))
747 {
748 rc = AudioTestGenTag(pTstEnv->szTag, sizeof(pTstEnv->szTag));
749 AssertRCReturn(rc, rc);
750 }
751
752 if (!RTStrNLen(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp)))
753 {
754 rc = AudioTestPathGetTemp(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp));
755 AssertRCReturn(rc, rc);
756 }
757
758 if (!RTStrNLen(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut)))
759 {
760 rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), pTstEnv->szPathTemp, "vkat-temp");
761 AssertRCReturn(rc, rc);
762 }
763
764 /* Go with the platform's default backend if nothing else is set. */
765 if (!pDrvReg)
766 pDrvReg = AudioTestGetDefaultBackend();
767
768 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Initializing environment for mode '%s'\n", pTstEnv->enmMode == AUDIOTESTMODE_HOST ? "host" : "guest");
769 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using backend '%s'\n", pDrvReg->szName);
770 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Using tag '%s'\n", pTstEnv->szTag);
771 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Output directory is '%s'\n", pTstEnv->szPathOut);
772 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Temp directory is '%s'\n", pTstEnv->szPathTemp);
773
774 if (!pTstEnv->cMsBufferSize)
775 pTstEnv->cMsBufferSize = UINT32_MAX;
776 if (!pTstEnv->cMsPreBuffer)
777 pTstEnv->cMsPreBuffer = UINT32_MAX;
778 if (!pTstEnv->cMsSchedulingHint)
779 pTstEnv->cMsSchedulingHint = UINT32_MAX;
780
781 PDMAudioHostEnumInit(&pTstEnv->DevEnum);
782
783 bool fUseDriverStack = false; /* Whether to init + use the audio driver stack or not. */
784
785 /* In regular testing mode only the guest mode needs initializing the driver stack. */
786 if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST)
787 fUseDriverStack = true;
788
789 /* When running in self-test mode, the host mode also needs to initialize the stack in order to
790 * to run the Valdation Kit audio driver ATS (no "real" VBox involved). */
791 if (pTstEnv->enmMode == AUDIOTESTMODE_HOST && pTstEnv->fSelftest)
792 fUseDriverStack = true;
793
794 if (fUseDriverStack)
795 {
796 rc = audioTestDriverStackInitEx(&pTstEnv->DrvStack, pDrvReg,
797 true /* fEnabledIn */, true /* fEnabledOut */, fWithDrvAudio);
798 if (RT_FAILURE(rc))
799 return rc;
800
801 PPDMAUDIOHOSTDEV pDev;
802 rc = audioTestDevicesEnumerateAndCheck(pTstEnv, pTstEnv->szDev, &pDev);
803 if (RT_FAILURE(rc))
804 return rc;
805 }
806
807 char szPathTemp[RTPATH_MAX];
808 if ( !strlen(pTstEnv->szPathTemp)
809 || !strlen(pTstEnv->szPathOut))
810 rc = RTPathTemp(szPathTemp, sizeof(szPathTemp));
811
812 if ( RT_SUCCESS(rc)
813 && !strlen(pTstEnv->szPathTemp))
814 rc = RTPathJoin(pTstEnv->szPathTemp, sizeof(pTstEnv->szPathTemp), szPathTemp, "vkat-temp");
815
816 if (RT_SUCCESS(rc))
817 {
818 rc = RTDirCreate(pTstEnv->szPathTemp, RTFS_UNIX_IRWXU, 0 /* fFlags */);
819 if (rc == VERR_ALREADY_EXISTS)
820 rc = VINF_SUCCESS;
821 }
822
823 if ( RT_SUCCESS(rc)
824 && !strlen(pTstEnv->szPathOut))
825 rc = RTPathJoin(pTstEnv->szPathOut, sizeof(pTstEnv->szPathOut), szPathTemp, "vkat");
826
827 if (RT_SUCCESS(rc))
828 {
829 rc = RTDirCreate(pTstEnv->szPathOut, RTFS_UNIX_IRWXU, 0 /* fFlags */);
830 if (rc == VERR_ALREADY_EXISTS)
831 rc = VINF_SUCCESS;
832 }
833
834 if (RT_FAILURE(rc))
835 return rc;
836
837 if (pTstEnv->enmMode == AUDIOTESTMODE_GUEST)
838 {
839 ATSCALLBACKCTX Ctx;
840 Ctx.pTstEnv = pTstEnv;
841
842 ATSCALLBACKS Callbacks;
843 RT_ZERO(Callbacks);
844 Callbacks.pfnTestSetBegin = audioTestGstAtsTestSetBeginCallback;
845 Callbacks.pfnTestSetEnd = audioTestGstAtsTestSetEndCallback;
846 Callbacks.pfnTonePlay = audioTestGstAtsTonePlayCallback;
847 Callbacks.pfnToneRecord = audioTestGstAtsToneRecordCallback;
848 Callbacks.pfnTestSetSendBegin = audioTestGstAtsTestSetSendBeginCallback;
849 Callbacks.pfnTestSetSendRead = audioTestGstAtsTestSetSendReadCallback;
850 Callbacks.pfnTestSetSendEnd = audioTestGstAtsTestSetSendEndCallback;
851 Callbacks.pvUser = &Ctx;
852
853 if (!pTstEnv->u.Guest.TcpOpts.uTcpBindPort)
854 pTstEnv->u.Guest.TcpOpts.uTcpBindPort = ATS_TCP_DEF_BIND_PORT_GUEST;
855
856 if (!pTstEnv->u.Guest.TcpOpts.szTcpBindAddr[0])
857 RTStrCopy(pTstEnv->u.Guest.TcpOpts.szTcpBindAddr, sizeof(pTstEnv->u.Guest.TcpOpts.szTcpBindAddr), "0.0.0.0");
858
859 if (!pTstEnv->u.Guest.TcpOpts.uTcpConnectPort)
860 pTstEnv->u.Guest.TcpOpts.uTcpConnectPort = ATS_TCP_DEF_CONNECT_PORT_GUEST;
861
862 if (!pTstEnv->u.Guest.TcpOpts.szTcpConnectAddr[0])
863 RTStrCopy(pTstEnv->u.Guest.TcpOpts.szTcpConnectAddr, sizeof(pTstEnv->u.Guest.TcpOpts.szTcpConnectAddr), "10.0.2.2");
864
865 /*
866 * Start the ATS (Audio Test Service) on the guest side.
867 * That service then will perform playback and recording operations on the guest, triggered from the host.
868 *
869 * When running this in self-test mode, that service also can be run on the host if nothing else is specified.
870 * Note that we have to bind to "0.0.0.0" by default so that the host can connect to it.
871 */
872 rc = audioTestEnvConfigureAndStartTcpServer(&pTstEnv->u.Guest.Srv, &Callbacks, "Guest ATS",
873 pTstEnv->u.Guest.TcpOpts.szTcpBindAddr, pTstEnv->u.Guest.TcpOpts.uTcpBindPort,
874 pTstEnv->u.Guest.TcpOpts.szTcpConnectAddr, pTstEnv->u.Guest.TcpOpts.uTcpConnectPort);
875
876 }
877 else /* Host mode */
878 {
879
880 ATSCALLBACKCTX Ctx;
881 Ctx.pTstEnv = pTstEnv;
882
883 ATSCALLBACKS Callbacks;
884 RT_ZERO(Callbacks);
885 Callbacks.pvUser = &Ctx;
886
887 if (!pTstEnv->u.Host.TcpOpts.uTcpBindPort)
888 pTstEnv->u.Host.TcpOpts.uTcpBindPort = ATS_TCP_DEF_BIND_PORT_HOST;
889
890 if (!pTstEnv->u.Host.TcpOpts.szTcpBindAddr[0])
891 RTStrCopy(pTstEnv->u.Host.TcpOpts.szTcpBindAddr, sizeof(pTstEnv->u.Host.TcpOpts.szTcpBindAddr), "0.0.0.0");
892
893 if (!pTstEnv->u.Host.TcpOpts.uTcpConnectPort)
894 pTstEnv->u.Host.TcpOpts.uTcpConnectPort = ATS_TCP_DEF_CONNECT_PORT_HOST_PORT_FWD;
895
896 if (!pTstEnv->u.Host.TcpOpts.szTcpConnectAddr[0])
897 RTStrCopy(pTstEnv->u.Host.TcpOpts.szTcpConnectAddr, sizeof(pTstEnv->u.Host.TcpOpts.szTcpConnectAddr),
898 ATS_TCP_DEF_CONNECT_HOST_ADDR_STR); /** @todo Get VM IP? Needs port forwarding. */
899
900 /* We need to start a server on the host so that VMs configured with NAT networking
901 * can connect to it as well. */
902 rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClGuest);
903 if (RT_SUCCESS(rc))
904 rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClGuest,
905 "Host -> Guest ATS",
906 pTstEnv->u.Host.TcpOpts.szTcpBindAddr, pTstEnv->u.Host.TcpOpts.uTcpBindPort,
907 pTstEnv->u.Host.TcpOpts.szTcpConnectAddr, pTstEnv->u.Host.TcpOpts.uTcpConnectPort);
908 if (RT_SUCCESS(rc))
909 {
910 if (!pTstEnv->ValKitTcpOpts.uTcpConnectPort)
911 pTstEnv->ValKitTcpOpts.uTcpConnectPort = ATS_TCP_DEF_CONNECT_PORT_VALKIT;
912
913 if (!pTstEnv->ValKitTcpOpts.szTcpConnectAddr[0])
914 RTStrCopy(pTstEnv->ValKitTcpOpts.szTcpConnectAddr, sizeof(pTstEnv->ValKitTcpOpts.szTcpConnectAddr),
915 ATS_TCP_DEF_CONNECT_HOST_ADDR_STR);
916
917 rc = AudioTestSvcClientCreate(&pTstEnv->u.Host.AtsClValKit);
918 if (RT_SUCCESS(rc))
919 rc = audioTestEnvConnectViaTcp(pTstEnv, &pTstEnv->u.Host.AtsClValKit,
920 "Host -> Validation Kit Host Audio Driver ATS",
921 pTstEnv->ValKitTcpOpts.szTcpBindAddr, pTstEnv->ValKitTcpOpts.uTcpBindPort,
922 pTstEnv->ValKitTcpOpts.szTcpConnectAddr, pTstEnv->ValKitTcpOpts.uTcpConnectPort);
923 }
924 }
925
926 if ( RT_FAILURE(rc)
927 && fUseDriverStack)
928 audioTestDriverStackDelete(&pTstEnv->DrvStack);
929
930 return rc;
931}
932
933/**
934 * Destroys an audio test environment.
935 *
936 * @param pTstEnv Audio test environment to destroy.
937 */
938void audioTestEnvDestroy(PAUDIOTESTENV pTstEnv)
939{
940 if (!pTstEnv)
941 return;
942
943 PDMAudioHostEnumDelete(&pTstEnv->DevEnum);
944
945 for (unsigned i = 0; i < RT_ELEMENTS(pTstEnv->aStreams); i++)
946 {
947 int rc2 = audioTestStreamDestroy(pTstEnv, &pTstEnv->aStreams[i]);
948 if (RT_FAILURE(rc2))
949 RTTestFailed(g_hTest, "Stream destruction for stream #%u failed with %Rrc\n", i, rc2);
950 }
951
952 /* Try cleaning up a bit. */
953 RTDirRemove(pTstEnv->szPathTemp);
954 RTDirRemove(pTstEnv->szPathOut);
955
956 audioTestDriverStackDelete(&pTstEnv->DrvStack);
957}
958
959/**
960 * Closes, packs up and destroys a test environment.
961 *
962 * @returns VBox status code.
963 * @param pTstEnv Test environment to handle.
964 * @param fPack Whether to pack the test set up before destroying / wiping it.
965 * @param pszPackFile Where to store the packed test set file on success. Can be NULL if \a fPack is \c false.
966 * @param cbPackFile Size (in bytes) of \a pszPackFile. Can be 0 if \a fPack is \c false.
967 */
968int audioTestEnvPrologue(PAUDIOTESTENV pTstEnv, bool fPack, char *pszPackFile, size_t cbPackFile)
969{
970 /* Close the test set first. */
971 AudioTestSetClose(&pTstEnv->Set);
972
973 int rc = VINF_SUCCESS;
974
975 if (fPack)
976 {
977 /* Before destroying the test environment, pack up the test set so
978 * that it's ready for transmission. */
979 rc = AudioTestSetPack(&pTstEnv->Set, pTstEnv->szPathOut, pszPackFile, cbPackFile);
980 if (RT_SUCCESS(rc))
981 RTTestPrintf(g_hTest, RTTESTLVL_ALWAYS, "Test set packed up to '%s'\n", pszPackFile);
982 }
983
984 if (!g_fDrvAudioDebug) /* Don't wipe stuff when debugging. Can be useful for introspecting data. */
985 /* ignore rc */ AudioTestSetWipe(&pTstEnv->Set);
986
987 AudioTestSetDestroy(&pTstEnv->Set);
988
989 if (RT_FAILURE(rc))
990 RTTestFailed(g_hTest, "Test set prologue failed with %Rrc\n", rc);
991
992 return rc;
993}
994
995/**
996 * Initializes an audio test parameters set.
997 *
998 * @param pTstParms Test parameters set to initialize.
999 */
1000void audioTestParmsInit(PAUDIOTESTPARMS pTstParms)
1001{
1002 RT_ZERO(*pTstParms);
1003}
1004
1005/**
1006 * Destroys an audio test parameters set.
1007 *
1008 * @param pTstParms Test parameters set to destroy.
1009 */
1010void audioTestParmsDestroy(PAUDIOTESTPARMS pTstParms)
1011{
1012 if (!pTstParms)
1013 return;
1014
1015 return;
1016}
1017
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