diff options
Diffstat (limited to '')
-rw-r--r-- | src/VBox/ValidationKit/utils/usb/UsbTest.cpp | 667 |
1 files changed, 667 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/utils/usb/UsbTest.cpp b/src/VBox/ValidationKit/utils/usb/UsbTest.cpp new file mode 100644 index 00000000..809e3fe1 --- /dev/null +++ b/src/VBox/ValidationKit/utils/usb/UsbTest.cpp @@ -0,0 +1,667 @@ +/* $Id: UsbTest.cpp $ */ +/** @file + * UsbTest - User frontend for the Linux usbtest USB test and benchmarking module. + * Integrates with our test framework for nice outputs. + */ + +/* + * Copyright (C) 2014-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/dir.h> +#include <iprt/err.h> +#include <iprt/file.h> +#include <iprt/getopt.h> +#include <iprt/path.h> +#include <iprt/param.h> +#include <iprt/process.h> +#include <iprt/stream.h> +#include <iprt/string.h> +#include <iprt/test.h> + +#include <iprt/linux/sysfs.h> + +#include <unistd.h> +#include <errno.h> +#include <limits.h> + +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include <sys/ioctl.h> +#include <linux/usbdevice_fs.h> + + +/********************************************************************************************************************************* +* Defined Constants And Macros * +*********************************************************************************************************************************/ + + +/********************************************************************************************************************************* +* Structures and Typedefs * +*********************************************************************************************************************************/ + +/** + * USB test request data. + * There is no public header with this information so we define it ourself here. + */ +typedef struct USBTESTPARMS +{ + /** Specifies the test to run. */ + uint32_t idxTest; + /** How many iterations the test should be executed. */ + uint32_t cIterations; + /** Size of the data packets. */ + uint32_t cbData; + /** Size of */ + uint32_t cbVariation; + /** Length of the S/G list for the test. */ + uint32_t cSgLength; + /** Returned time data after completing the test. */ + struct timeval TimeTest; +} USBTESTPARAMS; +/** Pointer to a test parameter structure. */ +typedef USBTESTPARAMS *PUSBTESTPARAMS; + +/** + * USB device descriptor. Used to search for the test device based + * on the vendor and product id. + */ +#pragma pack(1) +typedef struct USBDEVDESC +{ + uint8_t bLength; + uint8_t bDescriptorType; + uint16_t bcdUSB; + uint8_t bDeviceClass; + uint8_t bDeviceSubClass; + uint8_t bDeviceProtocol; + uint8_t bMaxPacketSize0; + uint16_t idVendor; + uint16_t idProduct; + uint16_t bcdDevice; + uint8_t iManufacturer; + uint8_t iProduct; + uint8_t iSerialNumber; + uint8_t bNumConfigurations; +} USBDEVDESC; +#pragma pack() + +#define USBTEST_REQUEST _IOWR('U', 100, USBTESTPARMS) + +/** + * Callback to set up the test parameters for a specific test. + * + * @returns IPRT status code. + * @retval VINF_SUCCESS if setting the parameters up succeeded. Any other error code + * otherwise indicating the kind of error. + * @param idxTest The test index. + * @param pszTest Test name. + * @param pParams The USB test parameters to set up. + */ +typedef DECLCALLBACKTYPE(int, FNUSBTESTPARAMSSETUP,(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams)); +/** Pointer to a USB test parameters setup callback. */ +typedef FNUSBTESTPARAMSSETUP *PFNUSBTESTPARAMSSETUP; + +/** + * USB test descriptor. + */ +typedef struct USBTESTDESC +{ + /** (Sort of) Descriptive test name. */ + const char *pszName; + /** Flag whether the test is excluded. */ + bool fExcluded; + /** The parameter setup callback. */ + PFNUSBTESTPARAMSSETUP pfnParamsSetup; +} USBTESTDESC; +/** Pointer a USB test descriptor. */ +typedef USBTESTDESC *PUSBTESTDESC; + +/** + * USB speed values. + */ +typedef enum USBTESTSPEED +{ + USBTESTSPEED_ANY = 0, + USBTESTSPEED_UNKNOWN, + USBTESTSPEED_LOW, + USBTESTSPEED_FULL, + USBTESTSPEED_HIGH, + USBTESTSPEED_SUPER +} USBTESTSPEED; + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ + +/** Some forward method declarations. */ +static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); +static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams); + +/** Command line parameters */ +static const RTGETOPTDEF g_aCmdOptions[] = +{ + {"--device", 'd', RTGETOPT_REQ_STRING }, + {"--help", 'h', RTGETOPT_REQ_NOTHING}, + {"--exclude", 'e', RTGETOPT_REQ_UINT32}, + {"--exclude-all", 'a', RTGETOPT_REQ_NOTHING}, + {"--include", 'i', RTGETOPT_REQ_UINT32}, + {"--expected-speed", 's', RTGETOPT_REQ_STRING } +}; + +static USBTESTDESC g_aTests[] = +{ + /* pszTest fExcluded pfnParamsSetup */ + {"NOP", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk write", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk read", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, + {"Non-queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk write", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk read", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk write variabe size", false, usbTestParamsSetupReadWrite}, + {"Queued Bulk read variabe size", false, usbTestParamsSetupReadWrite}, + {"Chapter 9 Control Test", false, usbTestParamsSetupReadWrite}, + {"Queued control messaging", false, usbTestParamsSetupReadWrite}, + {"Unlink reads", false, usbTestParamsSetupReadWrite}, + {"Unlink writes", false, usbTestParamsSetupReadWrite}, + {"Set/Clear halts", false, usbTestParamsSetupReadWrite}, + {"Control writes", false, usbTestParamsSetupControlWrites}, + {"Isochronous write", false, usbTestParamsSetupReadWrite}, + {"Isochronous read", false, usbTestParamsSetupReadWrite}, + {"Bulk write unaligned (DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk read unaligned (DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk write unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, + {"Bulk read unaligned (no DMA)", false, usbTestParamsSetupReadWrite}, + {"Control writes unaligned", false, usbTestParamsSetupControlWrites}, + {"Isochronous write unaligned", false, usbTestParamsSetupReadWrite}, + {"Isochronous read unaligned", false, usbTestParamsSetupReadWrite}, + {"Unlink queued Bulk", false, usbTestParamsSetupReadWrite} +}; + +/** The test handle. */ +static RTTEST g_hTest; +/** The expected device speed. */ +static USBTESTSPEED g_enmSpeed = USBTESTSPEED_ANY; + +/** + * Setup callback for basic read/write (bulk, isochronous) tests. + * + * @copydoc FNUSBTESTPARAMSSETUP + */ +static DECLCALLBACK(int) usbTestParamsSetupReadWrite(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) +{ + NOREF(idxTest); + NOREF(pszTest); + + pParams->cIterations = 1000; + pParams->cbData = 512; + pParams->cbVariation = 512; + pParams->cSgLength = 32; + + return VINF_SUCCESS; +} + +/** + * Setup callback for the control writes test. + * + * @copydoc FNUSBTESTPARAMSSETUP + */ +static DECLCALLBACK(int) usbTestParamsSetupControlWrites(unsigned idxTest, const char *pszTest, PUSBTESTPARAMS pParams) +{ + NOREF(idxTest); + NOREF(pszTest); + + pParams->cIterations = 1000; + pParams->cbData = 512; + /* + * Must be smaller than cbData or the parameter check in the usbtest module fails, + * no idea yet why it must be this. + */ + pParams->cbVariation = 256; + pParams->cSgLength = 32; + + return VINF_SUCCESS; +} + +/** + * Shows tool usage text. + */ +static void usbTestUsage(PRTSTREAM pStrm) +{ + char szExec[RTPATH_MAX]; + RTStrmPrintf(pStrm, "usage: %s [options]\n", + RTPathFilename(RTProcGetExecutablePath(szExec, sizeof(szExec)))); + RTStrmPrintf(pStrm, "\n"); + RTStrmPrintf(pStrm, "options: \n"); + + + for (unsigned i = 0; i < RT_ELEMENTS(g_aCmdOptions); i++) + { + const char *pszHelp; + switch (g_aCmdOptions[i].iShort) + { + case 'h': + pszHelp = "Displays this help and exit"; + break; + case 'd': + pszHelp = "Use the specified test device"; + break; + case 'e': + pszHelp = "Exclude the given test id from the list"; + break; + case 'a': + pszHelp = "Exclude all tests from the list (useful to enable single tests later with --include)"; + break; + case 'i': + pszHelp = "Include the given test id in the list"; + break; + case 's': + pszHelp = "The device speed to expect"; + break; + default: + pszHelp = "Option undocumented"; + break; + } + char szOpt[256]; + RTStrPrintf(szOpt, sizeof(szOpt), "%s, -%c", g_aCmdOptions[i].pszLong, g_aCmdOptions[i].iShort); + RTStrmPrintf(pStrm, " %-30s%s\n", szOpt, pszHelp); + } +} + +/** + * Searches for a USB test device and returns the bus and device ID and the device speed. + */ +static int usbTestDeviceQueryBusAndDevId(uint16_t *pu16BusId, uint16_t *pu16DevId, USBTESTSPEED *penmSpeed) +{ + bool fFound = false; + +#define USBTEST_USB_DEV_SYSFS "/sys/bus/usb/devices/" + + RTDIR hDirUsb = NULL; + int rc = RTDirOpen(&hDirUsb, USBTEST_USB_DEV_SYSFS); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbBus; + rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); + if ( RT_SUCCESS(rc) + && RTStrNCmp(DirUsbBus.szName, "usb", 3) + && RTLinuxSysFsExists(USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName)) + { + int64_t idVendor = 0; + int64_t idProduct = 0; + int64_t iBusId = 0; + int64_t iDevId = 0; + char aszSpeed[20]; + + rc = RTLinuxSysFsReadIntFile(16, &idVendor, USBTEST_USB_DEV_SYSFS "%s/idVendor", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &idProduct, USBTEST_USB_DEV_SYSFS "%s/idProduct", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &iBusId, USBTEST_USB_DEV_SYSFS "%s/busnum", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadIntFile(16, &iDevId, USBTEST_USB_DEV_SYSFS "%s/devnum", DirUsbBus.szName); + if (RT_SUCCESS(rc)) + rc = RTLinuxSysFsReadStrFile(&aszSpeed[0], sizeof(aszSpeed), NULL, USBTEST_USB_DEV_SYSFS "%s/speed", DirUsbBus.szName); + + if ( RT_SUCCESS(rc) + && idVendor == 0x0525 + && idProduct == 0xa4a0) + { + if (penmSpeed) + { + /* Parse the speed. */ + if (!RTStrCmp(&aszSpeed[0], "1.5")) + *penmSpeed = USBTESTSPEED_LOW; + else if (!RTStrCmp(&aszSpeed[0], "12")) + *penmSpeed = USBTESTSPEED_FULL; + else if (!RTStrCmp(&aszSpeed[0], "480")) + *penmSpeed = USBTESTSPEED_HIGH; + else if ( !RTStrCmp(&aszSpeed[0], "5000") + || !RTStrCmp(&aszSpeed[0], "10000")) + *penmSpeed = USBTESTSPEED_SUPER; + else + *penmSpeed = USBTESTSPEED_UNKNOWN; + } + + if (pu16BusId) + *pu16BusId = (uint16_t)iBusId; + if (pu16DevId) + *pu16DevId = (uint16_t)iDevId; + fFound = true; + break; + } + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + } while ( RT_SUCCESS(rc) + && !fFound); + + if (rc == VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + RTDirClose(hDirUsb); + } + + if (RT_SUCCESS(rc) && !fFound) + rc = VERR_NOT_FOUND; + + return rc; +} + +/** + * Search for a USB test device and return the device path. + * + * @returns Path to the USB test device or NULL if none was found. + */ +static char *usbTestFindDevice(void) +{ + /* + * Very crude and quick way to search for the correct test device. + * Assumption is that the path looks like /dev/bus/usb/%3d/%3d. + */ + char *pszDevPath = NULL; + + RTDIR hDirUsb = NULL; + int rc = RTDirOpen(&hDirUsb, "/dev/bus/usb"); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbBus; + rc = RTDirRead(hDirUsb, &DirUsbBus, NULL); + if (RT_SUCCESS(rc)) + { + char aszPath[RTPATH_MAX + 1]; + RTStrPrintf(&aszPath[0], RT_ELEMENTS(aszPath), "/dev/bus/usb/%s", DirUsbBus.szName); + + RTDIR hDirUsbBus = NULL; + rc = RTDirOpen(&hDirUsbBus, &aszPath[0]); + if (RT_SUCCESS(rc)) + { + do + { + RTDIRENTRY DirUsbDev; + rc = RTDirRead(hDirUsbBus, &DirUsbDev, NULL); + if (RT_SUCCESS(rc)) + { + char aszPathDev[RTPATH_MAX + 1]; + RTStrPrintf(&aszPathDev[0], RT_ELEMENTS(aszPathDev), "/dev/bus/usb/%s/%s", + DirUsbBus.szName, DirUsbDev.szName); + + RTFILE hFileDev; + rc = RTFileOpen(&hFileDev, aszPathDev, RTFILE_O_OPEN | RTFILE_O_READ | RTFILE_O_DENY_NONE); + if (RT_SUCCESS(rc)) + { + USBDEVDESC DevDesc; + + rc = RTFileRead(hFileDev, &DevDesc, sizeof(DevDesc), NULL); + RTFileClose(hFileDev); + + if ( RT_SUCCESS(rc) + && DevDesc.idVendor == 0x0525 + && DevDesc.idProduct == 0xa4a0) + pszDevPath = RTStrDup(aszPathDev); + } + + rc = VINF_SUCCESS; + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + + } while ( RT_SUCCESS(rc) + && !pszDevPath); + + rc = VINF_SUCCESS; + RTDirClose(hDirUsbBus); + } + } + else if (rc != VERR_NO_MORE_FILES) + rc = VINF_SUCCESS; + } while ( RT_SUCCESS(rc) + && !pszDevPath); + + RTDirClose(hDirUsb); + } + + return pszDevPath; +} + +static int usbTestIoctl(int iDevFd, int iInterface, PUSBTESTPARAMS pParams) +{ + struct usbdevfs_ioctl IoCtlData; + + IoCtlData.ifno = iInterface; + IoCtlData.ioctl_code = (int)USBTEST_REQUEST; + IoCtlData.data = pParams; + return ioctl(iDevFd, USBDEVFS_IOCTL, &IoCtlData); +} + +/** + * Test execution worker. + * + * @param pszDevice The device to use for testing. + */ +static void usbTestExec(const char *pszDevice) +{ + int iDevFd; + + RTTestSub(g_hTest, "Opening device"); + iDevFd = open(pszDevice, O_RDWR); + if (iDevFd != -1) + { + USBTESTPARAMS Params; + + RTTestPassed(g_hTest, "Opening device successful\n"); + + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + { + RTTestSub(g_hTest, g_aTests[i].pszName); + + if (g_aTests[i].fExcluded) + { + RTTestSkipped(g_hTest, "Excluded from list"); + continue; + } + + int rc = g_aTests[i].pfnParamsSetup(i, g_aTests[i].pszName, &Params); + if (RT_SUCCESS(rc)) + { + Params.idxTest = i; + + /* Assume the test interface has the number 0 for now. */ + int rcPosix = usbTestIoctl(iDevFd, 0, &Params); + if (rcPosix < 0 && errno == EOPNOTSUPP) + { + RTTestSkipped(g_hTest, "Not supported"); + continue; + } + + if (rcPosix < 0) + { + /* + * The error status code of the unlink testcase is + * offset by 2000 for the sync and 1000 for the sync code path + * (see drivers/usb/misc/usbtest.c in the Linux kernel sources). + * + * Adjust to the actual status code so converting doesn't assert. + */ + int iTmpErrno = errno; + if (iTmpErrno >= 2000) + iTmpErrno -= 2000; + else if (iTmpErrno >= 1000) + iTmpErrno -= 1000; + RTTestFailed(g_hTest, "Test failed with %Rrc\n", RTErrConvertFromErrno(iTmpErrno)); + } + else + { + uint64_t u64Ns = Params.TimeTest.tv_sec * RT_NS_1SEC + Params.TimeTest.tv_usec * RT_NS_1US; + RTTestValue(g_hTest, "Runtime", u64Ns, RTTESTUNIT_NS); + } + } + else + RTTestFailed(g_hTest, "Setting up test parameters failed with %Rrc\n", rc); + RTTestSubDone(g_hTest); + } + + close(iDevFd); + } + else + RTTestFailed(g_hTest, "Opening device failed with %Rrc\n", RTErrConvertFromErrno(errno)); + +} + +int main(int argc, char *argv[]) +{ + /* + * Init IPRT and globals. + */ + int rc = RTTestInitAndCreate("UsbTest", &g_hTest); + if (rc) + return rc; + + /* + * Default values. + */ + const char *pszDevice = NULL; + + RTGETOPTUNION ValueUnion; + RTGETOPTSTATE GetState; + RTGetOptInit(&GetState, argc, argv, g_aCmdOptions, RT_ELEMENTS(g_aCmdOptions), 1, 0 /* fFlags */); + while ((rc = RTGetOpt(&GetState, &ValueUnion))) + { + switch (rc) + { + case 'h': + usbTestUsage(g_pStdOut); + return RTEXITCODE_SUCCESS; + case 'd': + pszDevice = ValueUnion.psz; + break; + case 's': + if (!RTStrICmp(ValueUnion.psz, "Low")) + g_enmSpeed = USBTESTSPEED_LOW; + else if (!RTStrICmp(ValueUnion.psz, "Full")) + g_enmSpeed = USBTESTSPEED_FULL; + else if (!RTStrICmp(ValueUnion.psz, "High")) + g_enmSpeed = USBTESTSPEED_HIGH; + else if (!RTStrICmp(ValueUnion.psz, "Super")) + g_enmSpeed = USBTESTSPEED_SUPER; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid speed passed to --expected-speed\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + case 'e': + if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) + g_aTests[ValueUnion.u32].fExcluded = true; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --exclude\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + case 'a': + for (unsigned i = 0; i < RT_ELEMENTS(g_aTests); i++) + g_aTests[i].fExcluded = true; + break; + case 'i': + if (ValueUnion.u32 < RT_ELEMENTS(g_aTests)) + g_aTests[ValueUnion.u32].fExcluded = false; + else + { + RTTestPrintf(g_hTest, RTTESTLVL_FAILURE, "Invalid test number passed to --include\n"); + RTTestErrorInc(g_hTest); + return RTGetOptPrintError(VERR_INVALID_PARAMETER, &ValueUnion); + } + break; + default: + return RTGetOptPrintError(rc, &ValueUnion); + } + } + + /* + * Start testing. + */ + RTTestBanner(g_hTest); + + /* Find the first test device if none was given. */ + if (!pszDevice) + { + RTTestSub(g_hTest, "Detecting device"); + pszDevice = usbTestFindDevice(); + if (!pszDevice) + RTTestFailed(g_hTest, "Failed to find suitable device\n"); + + RTTestSubDone(g_hTest); + } + + if (pszDevice) + { + /* First check that the requested speed matches. */ + if (g_enmSpeed != USBTESTSPEED_ANY) + { + RTTestSub(g_hTest, "Checking correct device speed"); + + USBTESTSPEED enmSpeed = USBTESTSPEED_UNKNOWN; + rc = usbTestDeviceQueryBusAndDevId(NULL, NULL, &enmSpeed); + if (RT_SUCCESS(rc)) + { + if (enmSpeed == g_enmSpeed) + RTTestPassed(g_hTest, "Reported device speed matches requested speed\n"); + else + RTTestFailed(g_hTest, "Reported device speed doesn'match requested speed (%u vs %u)\n", + enmSpeed, g_enmSpeed); + } + else + RTTestFailed(g_hTest, "Failed to query device speed with rc=%Rrc\n", rc); + + RTTestSubDone(g_hTest); + } + usbTestExec(pszDevice); + } + + RTEXITCODE rcExit = RTTestSummaryAndDestroy(g_hTest); + return rcExit; +} + |