#include // Note: A lot of these tests make specific assumptions about the timing of // various operations. If the tests fail intermittently, some parameters may // need to be adjusted. const uint8_t pipe_id = 0x82; #ifdef USE_TEST_DEVICE_A static void check_error_for_cancelled_transfer(const libusbp::error & error) { if (!error) { // The transfer actually completed successfully. return; } if (error.has_code(LIBUSBP_ERROR_CANCELLED)) { // This is expected. } else { // Some other error happened that we didn't expect. throw error; } } static void clean_up_async_in_pipe(libusbp::async_in_pipe & pipe) { pipe.cancel_transfers(); test_timeout timeout(500); while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error)) { check_error_for_cancelled_transfer(transfer_error); } timeout.check(); sleep_quick(); } } static void clean_up_async_in_pipe_and_expect_a_success(libusbp::async_in_pipe & pipe) { pipe.cancel_transfers(); test_timeout timeout(500); uint32_t success_count = 0; while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; uint8_t buffer[64] = {0}; size_t transferred; while(pipe.handle_finished_transfer(buffer, &transferred, &transfer_error)) { check_error_for_cancelled_transfer(transfer_error); if (!transfer_error) { REQUIRE(buffer[4] == 0xAB); REQUIRE(transferred == 5); success_count++; } } timeout.check(); sleep_quick(); } REQUIRE(success_count > 0); } #endif TEST_CASE("async_in_pipe traits") { libusbp::async_in_pipe pipe, pipe2; SECTION("is not copy-constructible") { REQUIRE(std::is_copy_constructible::value == false); // Should not compile: // libusbp::async_in_pipe pipe3(pipe); } SECTION("is not copy-assignable") { REQUIRE(std::is_copy_assignable::value == false); // Should not compile: // pipe2 = pipe; } } #ifdef USE_TEST_DEVICE_A TEST_CASE("async_in_pipe basic properties") { libusbp::device device = find_test_device_a(); libusbp::generic_interface gi(device, 0, true); libusbp::generic_handle handle(gi); libusbp::async_in_pipe pipe = handle.open_async_in_pipe(pipe_id); SECTION("is present") { REQUIRE(pipe); } SECTION("is movable") { libusbp::async_in_pipe pipe2 = std::move(pipe); REQUIRE(pipe2); REQUIRE_FALSE(pipe); } SECTION("is move-assignable") { libusbp::async_in_pipe pipe2; pipe2 = std::move(pipe); REQUIRE(pipe2); REQUIRE_FALSE(pipe); } } TEST_CASE("null async_in_pipe") { libusbp::async_in_pipe pipe; std::string expected_message = "Pipe argument is null."; SECTION("is not present") { CHECK_FALSE(pipe); } SECTION("cannot allocate transfers") { try { pipe.allocate_transfers(4, 5); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } } SECTION("cannot start endless transfers") { try { pipe.start_endless_transfers(); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } } SECTION("cannot handle events") { try { pipe.handle_events(); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } } SECTION("cannot say if it has pending transfers") { // Test it this way to make sure the C++ wrapper throws an exception // instead of ignoring it. try { pipe.has_pending_transfers(); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } // Also test it this way so we can check the "result" output parameter // in the C API. bool result = true; libusbp::error error(libusbp_async_in_pipe_has_pending_transfers(NULL, &result)); REQUIRE(error.message() == expected_message); REQUIRE_FALSE(result); } SECTION("cannot handle a finished transfer") { uint8_t buffer[] = "hi there"; size_t transferred = 10; libusbp::error error = get_some_error(); try { pipe.handle_finished_transfer(buffer, &transferred, &error); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } CHECK(buffer[0] == 'h'); CHECK(transferred == 0); CHECK_FALSE(error); } SECTION("cannot cancel all transfers") { try { pipe.cancel_transfers(); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == expected_message); } } } TEST_CASE("async_in_pipe parameter validation and state checks") { libusbp::device device = find_test_device_a(); libusbp::generic_interface gi(device, 0, true); libusbp::generic_handle handle(gi); libusbp::async_in_pipe pipe = handle.open_async_in_pipe(0x82); SECTION("allocate_transfers") { SECTION("cannot be called twice on the same pipe") { pipe.allocate_transfers(1, 64); try { pipe.allocate_transfers(1, 64); } catch(const libusbp::error & error) { REQUIRE(error.message() == "Transfers were already allocated for this pipe."); } } SECTION("does not allow transfer_count to be 0") { // Set the size to 0 also so we can test that the count is checked // before the size (it's nice if the little details stay consistent // over time and across platforms). try { pipe.allocate_transfers(0, 0); } catch(const libusbp::error & error) { REQUIRE(error.message() == "Transfer count cannot be zero."); } } SECTION("does not allow transfer_size to be 0") { try { pipe.allocate_transfers(64, 0); } catch(const libusbp::error & error) { REQUIRE(error.message() == "Transfer size cannot be zero."); } } SECTION("rejects transfer sizes too large for the underlying APIs") { #ifdef _WIN32 // On Windows, URB buffer sizes are represented by ULONGs. if (SIZE_MAX <= ULONG_MAX) { return; } size_t too_large_size = (size_t)ULONG_MAX + 1; #endif #ifdef __linux__ // On Linux, URB buffer sizes are represented by ints. if (SIZE_MAX <= INT_MAX) { return; } size_t too_large_size = (size_t)INT_MAX + 1; #endif #ifdef __APPLE__ size_t too_large_size = (size_t)UINT32_MAX + 1; #endif try { pipe.allocate_transfers(1, too_large_size); REQUIRE(0); } catch(const libusbp::error & error) { REQUIRE(error.message() == "Failed to allocate transfers for asynchronous IN pipe. " "Transfer size is too large."); } } } SECTION("start_endless_transfers") { SECTION("complains if transfers were not allocated") { try { pipe.start_endless_transfers(); } catch(const libusbp::error & error) { REQUIRE(error.message() == "Pipe transfers have not been allocated yet."); } } } SECTION("has_pending_transfers") { SECTION("works even if transfers were not allocated") { REQUIRE_FALSE(pipe.has_pending_transfers()); } SECTION("complains if the output pointer is NULL") { libusbp::error error(libusbp_async_in_pipe_has_pending_transfers( pipe.pointer_get(), NULL)); REQUIRE(error.message() == "Boolean output pointer is null."); } } } TEST_CASE("async_in_pipe for an interrupt endpoint") { // If it works for an IN endpoint, it should work for a bulk endpoint too // because the underlying APIs that libusbp uses allow us to treat those // types of endpoints the same. libusbp::device device = find_test_device_a(); libusbp::generic_interface gi(device, 0, true); libusbp::generic_handle handle(gi); libusbp::async_in_pipe pipe = handle.open_async_in_pipe(0x82); const size_t transfer_size = 5; test_timeout timeout(500); SECTION("can do continuous transfers and then cancel them") { pipe.allocate_transfers(5, transfer_size); pipe.start_endless_transfers(); size_t finish_count = 0; while(finish_count < 12) { // Don't use REQUIRE or CATCH here because then the number of // assertions printed at the end of the tests will be unpredictable. if (!pipe.has_pending_transfers()) { throw "No pending transfers."; } uint8_t buffer[transfer_size] = {0}; size_t transferred; libusbp::error transfer_error; if (pipe.handle_finished_transfer(buffer, &transferred, &transfer_error)) { if (transfer_error) { throw transfer_error; } REQUIRE(buffer[4] == 0xAB); finish_count++; } pipe.handle_events(); timeout.check(); sleep_quick(); } REQUIRE(pipe.has_pending_transfers()); clean_up_async_in_pipe(pipe); } SECTION("cancelling a transfer before it is completed") { // Event order tested here: submit, cancel, "reap" // Make a lot of transfers and cancel then immediately to make sure at // least one was cancelled before it was completed. #ifdef __linux__ // On a normal Linux machine it takes about 1 ms per transfer to cancel // transfers. (Maybe that is because we are using an endpoint with a 1 // ms polling interval.) On a VirtualBox machine running on a Windows // guest, it takes about much longer: 20 ms per transfer. To work // around this slowness, we only allocate 20 transfers at a time in // Linux. // Maybe later we should we provide a workaround that lets // people close the generic_handle and all its pipes at the same time, // thus saving their users from this painful wait, without causing // memory leaks. If the generic handle fd gets closed and replaced with // -1, then it would be safe to just free all the resources for the URBs // without actually cancelling them. // We would like to make the transfer count larger (500) to make this // test more resilient to lag caused by the operating system, but that // slows down the development process too much. const size_t transfer_count = 20; #else const size_t transfer_count = 500; #endif pipe.allocate_transfers(transfer_count, transfer_size); pipe.start_endless_transfers(); //printf("Cancelling %d transfers\n", (int)transfer_count); test_timeout cancel_timer(10000); pipe.cancel_transfers(); //printf("Done cancelling, took %d ms\n", cancel_timer.get_milliseconds()); size_t cancel_count = 0; test_timeout timeout(10000); while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error)) { check_error_for_cancelled_transfer(transfer_error); if (transfer_error.has_code(LIBUSBP_ERROR_CANCELLED)) { cancel_count++; } } timeout.check(); sleep_quick(); } // Make sure at least one error from the cancelled transfers had the // right error code. //printf("Cancel count: %d\n", (int)cancel_count); REQUIRE(cancel_count > 5); } SECTION("cancelling a transfer after it is completed but before the libusbp knows") { // Event order tested here: submit, complete, cancel, "reap" pipe.allocate_transfers(1, transfer_size); pipe.start_endless_transfers(); sleep_ms(30); clean_up_async_in_pipe_and_expect_a_success(pipe); } SECTION("cancelling a transfer after it is completed and the event is handled") { // Event order tested here: submit, complete, "reap", cancel pipe.allocate_transfers(1, transfer_size); pipe.start_endless_transfers(); sleep_ms(30); pipe.handle_events(); clean_up_async_in_pipe_and_expect_a_success(pipe); } SECTION("cancelling a partially completed transfer") { // Note: This test seems to always fail on Windows Vista and Windows 7 // because WinUSB reports that 0 bytes have been transferred instead of // 10. It might be that older versions of WinUSB or of the USB stack // didn't support returning data from a cancelled, partially completed // transfer. // Assumption: there will be two packets queued up by Test Device A in // its ping-pong buffers. So when we tell it to pause the ADC for 100 // ms, a three-packet transfer will quickly receive those two packets // and then keep waiting for more. // Previous versions of macOS returned one packet instead of two, // but this is no longer true on Darwin Kernel 22.1.0 (Oct 2022). // Pause the ADC for 100 ms. handle.control_transfer(0x40, 0xA0, 100, 0); pipe.allocate_transfers(1, transfer_size * 3); pipe.start_endless_transfers(); sleep_ms(20); pipe.cancel_transfers(); test_timeout timeout(500); while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; uint8_t buffer[transfer_size * 3]; size_t transferred; while(pipe.handle_finished_transfer(buffer, &transferred, &transfer_error)) { CHECK(transfer_error); check_error_for_cancelled_transfer(transfer_error); #if defined(VBOX_LINUX_ON_WINDOWS) CHECK(transferred == 0); #else CHECK(transferred == transfer_size * 2); CHECK(buffer[4] == 0xAB); CHECK(buffer[9] == 0xAB); #endif } timeout.check(); sleep_quick(); } // Unpause the ADC. handle.control_transfer(0x40, 0xA0, 0, 0); } SECTION("cancelling transfers twice is okay") { pipe.allocate_transfers(1, transfer_size); pipe.start_endless_transfers(); pipe.cancel_transfers(); clean_up_async_in_pipe(pipe); } SECTION("might respect the time out from set_timeout") { #ifdef __linux__ // On Linux, asynchronous requests cannot have a timeout, so this test // will not pass. If we want to implement a timeout feature, we would // need to make some kind of timer like libusb does, and cancel the // requests when the time is up. It's probably fine for the user to // just implement a timeout themselves using a separate time library. return; #endif #ifdef __APPLE__ // On Mac OS X, interrupt endpoints cannot have a timeout, and our // library simply doesn't use a timeout (we use ReadPipeAsync instead of // ReadPipeAsyncTO). return; #endif // Pause the ADC for 200 ms. handle.control_transfer(0x40, 0xA0, 200, 0); handle.set_timeout(pipe_id, 10); pipe.allocate_transfers(5, transfer_size); pipe.start_endless_transfers(); uint32_t timeout_count = 0; uint32_t success_count = 0; test_timeout timeout(500); while(timeout_count < 8) { pipe.handle_events(); libusbp::error transfer_error; while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error)) { // We expect there to be 0 to 2 successes, and the rest of the // transfers will be timeouts. if (transfer_error == false) { // Successful transfer. success_count++; if (timeout_count != 0) { throw "Got a success after a timeout."; } if (success_count > 2) { throw "Got too many successes."; } } else if (transfer_error.has_code(LIBUSBP_ERROR_TIMEOUT)) { // Timeout. const char * expected = "Asynchronous IN transfer failed. " "The operation timed out. " #ifdef _WIN32 "Windows error code 0x79." #endif ; REQUIRE(transfer_error.message() == expected); timeout_count++; } else { // Unexpected error. throw transfer_error; } } timeout.check(); sleep_quick(); } // Clean up the pipe, while accepting that there might be more transfers // finishing with a cancellation error. pipe.cancel_transfers(); test_timeout timeout_cleanup(500); while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error)) { if (transfer_error && !transfer_error.has_code(LIBUSBP_ERROR_CANCELLED) && !transfer_error.has_code(LIBUSBP_ERROR_TIMEOUT)) { throw transfer_error; } } timeout_cleanup.check(); sleep_quick(); } // Unpause the ADC. handle.control_transfer(0x40, 0xA0, 0, 0); } SECTION("overflow") { #ifdef VBOX_LINUX_ON_WINDOWS // This test fails and then puts the USB device into a weird state // if run on Linux inside VirtualBox on a Windows host. std::cerr << "Skipping asynchronous IN pipe overflow test.\n"; return; #endif pipe.allocate_transfers(1, transfer_size + 1); pipe.start_endless_transfers(); sleep_ms(10); pipe.handle_events(); uint8_t buffer[transfer_size] = {0}; size_t transferred; libusbp::error transfer_error; bool finished = pipe.handle_finished_transfer(buffer, &transferred, &transfer_error); REQUIRE(finished); std::string expected_message; size_t expected_transferred; #ifdef _WIN32 // This request is an error in WinUSB since we are using RAW_IO. The // error is detected before any data is transferred. expected_transferred = 0; expected_message = "Asynchronous IN transfer failed. " "Incorrect function. Windows error code 0x1."; #elif defined(__linux__) // This request results in an error in Linux after some data is transferred. // On some older Linux systems, expected_transferred was transfer_size + 1. // With Linux 5.15 on a Raspberry Pi 4, expected_transferred is transfer_size. expected_transferred = transfer_size; expected_message = "Asynchronous IN transfer failed. " "The transfer overflowed. Error code 75."; #elif defined(__APPLE__) // On Mac OS X, this results in an error after some data is transferred. expected_transferred = transfer_size; expected_message = "Asynchronous IN transfer failed. " "The transfer overflowed. Error code 0xe00002e8."; #else REQUIRE(0); #endif CHECK(transferred == expected_transferred); CHECK(transfer_error.message() == expected_message); // We can't just call clean_up_async_in_pipe because we might be running // on a slower computer and the next transfer has actually already been // queued. pipe.cancel_transfers(); test_timeout timeout(500); while(pipe.has_pending_transfers()) { pipe.handle_events(); libusbp::error transfer_error; while(pipe.handle_finished_transfer(NULL, NULL, &transfer_error)) { if (!transfer_error) { #if defined(__linux__) // Overflowing is always an error, none of the transfers // should have succeeded. throw "Expected to get a transfer error."; #endif } else if (transfer_error.message() == expected_message) { // This is fine; the error indicates an overflow. } #ifdef __APPLE__ else if (transfer_error.has_code(LIBUSBP_ERROR_STALL)) { // Mac OS X considers the pipe to be "stalled" after an // overflow happens and we cannot actually submit any more // transfers. } #endif else if (transfer_error.has_code(LIBUSBP_ERROR_CANCELLED)) { // This is fine; the transfer was cancelled. } else { // Unexpected error. throw transfer_error; } } timeout.check(); sleep_quick(); } clean_up_async_in_pipe(pipe); } #ifdef __linux__ SECTION("does not prevent the creation of another generic_interface object") { // It seems that the "usbfs" driver gets attached to the device // while we are doing asynchronous transfers. This tests that our // code is OK with that and we can still open up other handles // to the device, at least in Linux. pipe.allocate_transfers(1, transfer_size); pipe.start_endless_transfers(); libusbp::generic_interface gi2(device, 0, true); libusbp::generic_interface gi3(device, 0, true); libusbp::generic_handle handle2(gi3); handle2.control_transfer(0x40, 0x90, 1, 0); clean_up_async_in_pipe(pipe); } #endif } #endif