usb: Add a manual test for USB transfer completion order
This manual test attempts to reproduce a case seen in issue 1153647
where parallel calls to transferIn(), recommended for performance, can
return data out of order.
This test is a starting point in the investigation as it only sends a
small amount of data while the original report involves much larger
transfers.
Bug: 1153647
Change-Id: I4f4b8f5ad502d08ae7c4fb9cc22889047029afc4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2587950
Commit-Queue: Reilly Grant <reillyg@chromium.org>
Reviewed-by: James Hollyer <jameshollyer@chromium.org>
Auto-Submit: Reilly Grant <reillyg@chromium.org>
Cr-Commit-Position: refs/heads/master@{#841773}
diff --git a/webusb/usbDevice_transferIn-manual.https.html b/webusb/usbDevice_transferIn-manual.https.html
new file mode 100644
index 0000000..bd3df7d
--- /dev/null
+++ b/webusb/usbDevice_transferIn-manual.https.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title></title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="resources/manual.js"></script>
+ </head>
+ <body>
+ <p>
+ This test requires a USB device implementing the USB CDC-ACM protocol
+ configured to loop back TX to RX. For example, this Arduino sketch could
+ be used:
+
+ <pre>
+void setup() {
+ Serial.begin(115200);
+ while (!Serial) {
+ ;
+ }
+}
+
+void loop() {
+ if (Serial.available()) {
+ Serial.write(Serial.read());
+ }
+}
+ </pre>
+ </p>
+ <script>
+ manual_usb_test(async (t, device) => {
+ await device.open();
+ t.add_cleanup(async () => {
+ if (device.opened) {
+ await device.close();
+ }
+ });
+
+ await device.selectConfiguration(1);
+
+ let controlInterface = undefined;
+ for (const iface of device.configuration.interfaces) {
+ const alternate = iface.alternates[0];
+ if (alternate.interfaceClass == 2 &&
+ alternate.interfaceSubclass == 2 &&
+ alternate.interfaceProtocol == 0) {
+ controlInterface = iface;
+ break;
+ }
+ }
+ assert_not_equals(controlInterface, undefined,
+ 'No control interface found.');
+
+ let dataInterface = undefined;
+ for (const iface of device.configuration.interfaces) {
+ const alternate = iface.alternates[0];
+ if (alternate.interfaceClass == 10 &&
+ alternate.interfaceSubclass == 0 &&
+ alternate.interfaceProtocol == 0) {
+ dataInterface = iface;
+ break;
+ }
+ }
+ assert_not_equals(dataInterface, undefined, 'No data interface found.');
+
+ await device.claimInterface(controlInterface.interfaceNumber);
+ await device.claimInterface(dataInterface.interfaceNumber);
+
+ let inEndpoint = undefined;
+ for (const endpoint of dataInterface.alternate.endpoints) {
+ if (endpoint.type == 'bulk' && endpoint.direction == 'in') {
+ inEndpoint = endpoint;
+ break;
+ }
+ }
+ assert_not_equals(inEndpoint, undefined, 'No IN endpoint found.');
+
+ let outEndpoint = undefined;
+ for (const endpoint of dataInterface.alternate.endpoints) {
+ if (endpoint.type == 'bulk' && endpoint.direction == 'out') {
+ outEndpoint = endpoint;
+ break;
+ }
+ }
+ assert_not_equals(outEndpoint, undefined, 'No OUT endpoint found.');
+
+ // Execute a SET_CONTROL_LINE_STATE command to let the device know the
+ // host is ready to transmit and receive data.
+ await device.controlTransferOut({
+ requestType: 'class',
+ recipient: 'interface',
+ request: 0x22,
+ value: 0x01,
+ index: controlInterface.interfaceNumber,
+ });
+
+ // Set up two IN transfers which should complete in order.
+ const transfer1 =
+ device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
+ const transfer2 =
+ device.transferIn(inEndpoint.endpointNumber, inEndpoint.packetSize);
+
+ // Write a single byte to the port which should be echoed to complete
+ // transfer1.
+ let result = await device.transferOut(
+ outEndpoint.endpointNumber, new Uint8Array(['a'.charCodeAt(0)]));
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 1);
+
+ result = await transfer1;
+ assert_equals(result.status, 'ok');
+ assert_not_equals(result.data, null);
+ assert_equals(result.data.byteLength, 1);
+ assert_equals(result.data.getUint8(0), 'a'.charCodeAt(0));
+
+ // Set up a third IN transfer which will be canceled when the device is
+ // closed at the end of the test.
+ const transfer3 = promise_rejects_dom(
+ t, 'NetworkError',
+ device.transferIn(inEndpoint.endpointNumber,
+ inEndpoint.packetSize));
+
+ // Write a single byte to the port which should be echoed to complete
+ // transfer2.
+ result = await device.transferOut(
+ outEndpoint.endpointNumber, new Uint8Array(['b'.charCodeAt(0)]));
+ assert_equals(result.status, 'ok');
+ assert_equals(result.bytesWritten, 1);
+
+ result = await transfer2;
+ assert_equals(result.status, 'ok');
+ assert_not_equals(result.data, null);
+ assert_equals(result.data.byteLength, 1);
+ assert_equals(result.data.getUint8(0), 'b'.charCodeAt(0));
+
+ await device.close();
+ await transfer3;
+ }, 'Multiple IN transfers on an endpoint complete in order');
+ </script>
+ </body>
+</html>