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>