diff options
Diffstat (limited to 'testing/web-platform/tests/webtransport')
29 files changed, 1455 insertions, 0 deletions
diff --git a/testing/web-platform/tests/webtransport/META.yml b/testing/web-platform/tests/webtransport/META.yml new file mode 100644 index 0000000000..3499931112 --- /dev/null +++ b/testing/web-platform/tests/webtransport/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/webtransport/ +suggested_reviewers: + - bashi + - nidhijaju + - yutakahirano diff --git a/testing/web-platform/tests/webtransport/README.md b/testing/web-platform/tests/webtransport/README.md new file mode 100644 index 0000000000..543d876e01 --- /dev/null +++ b/testing/web-platform/tests/webtransport/README.md @@ -0,0 +1,14 @@ +# Notes about WebTransport WPTs + +### Running the WebTransport WPTs using the test server +Although these tests do not currently run on the Chromium CI, they can still be +run using the WebTransport WPT server manually. + +Please refer to the following document for detailed instructions: +[Running WPT WebTransport test server](https://docs.google.com/document/d/1OBoZTcC9vDoLTgv_5WUznRFrmwXP0Gprj7V9oOzH9cU/edit?usp=sharing) + + +### Server Handlers +The python server handlers are stored under `handlers/` and are written using the +PEP8 style. +For details, please see the [style guide](https://www.python.org/dev/peps/pep-0008/). diff --git a/testing/web-platform/tests/webtransport/close.https.any.js b/testing/web-platform/tests/webtransport/close.https.any.js new file mode 100644 index 0000000000..4501e94fbf --- /dev/null +++ b/testing/web-platform/tests/webtransport/close.https.any.js @@ -0,0 +1,127 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js +// META: script=/common/utils.js + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + wt.close(); + + const close_info = await wt.closed; + + assert_not_own_property(close_info, 'closeCode'); + assert_not_own_property(close_info, 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 0, 'code'); + assert_equals(info.close_info.reason, '', 'reason'); +}, 'close'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + wt.close({closeCode: 99, reason: 'reason X'}); + + const close_info = await wt.closed; + + assert_equals(close_info.closeCode, 99, 'code'); + assert_equals(close_info.reason, 'reason X', 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 99, 'code'); + assert_equals(info.close_info.reason, 'reason X', 'reason'); +}, 'close with code and reason'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + const reason = 'あいうえお'.repeat(1000); + + wt.close({closeCode: 11, reason}); + + const close_info = await wt.closed; + + assert_equals(close_info.closeCode, 11, 'code'); + // This should be truncated to 1023 bytes! + const reason_truncated = 'あいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあいうえおあ'; + assert_equals(close_info.reason, reason_truncated, 'reason'); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'session-close-info'); + const info = data['session-close-info'] + + const expected_reason = + new TextDecoder().decode( + new TextEncoder().encode(reason).slice(0, 1024)).replaceAll('\ufffd', ''); + assert_false(info.abruptly, 'abruptly'); + assert_equals(info.close_info.code, 11, 'code'); + assert_equals(info.close_info.reason, expected_reason, 'reason'); +}, 'close with code and long reason'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-close.py')); + + const close_info = await wt.closed; + assert_equals(close_info.closeCode, 0, 'code'); + assert_equals(close_info.reason, '', 'reason'); +}, 'server initiated closure without code and reason'); + +promise_test(async t => { + const code = 32; + const reason = 'abc'; + const wt = new WebTransport( + webtransport_url(`server-close.py?code=${code}&reason=${reason}`)); + add_completion_callback(() => wt.close()); + + const close_info = await wt.closed; + assert_equals(close_info.closeCode, code, 'code'); + assert_equals(close_info.reason, reason, 'reason'); +}, 'server initiated closure with code and reason'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('server-connection-close.py')); + add_completion_callback(() => wt.close()); + + const streams_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi } = await streams_reader.read(); + const writer = bidi.writable.getWriter(); + const reader = bidi.readable.getReader(); + try { + writer.write(new Uint8Array([65])); + } catch (e) { + } + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await wt.closed.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'wt.closed'); + await promise_rejects_exactly(t, e, writer.closed, 'writer.closed'); + await promise_rejects_exactly(t, e, reader.closed, 'reader.closed'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'server initiated connection closure'); diff --git a/testing/web-platform/tests/webtransport/close.https.any.js.ini b/testing/web-platform/tests/webtransport/close.https.any.js.ini new file mode 100644 index 0000000000..6f118ff5f2 --- /dev/null +++ b/testing/web-platform/tests/webtransport/close.https.any.js.ini @@ -0,0 +1,19 @@ +[close.https.any.html] + disabled: + if product != "chrome": true + +[close.https.any.window.html] + disabled: + if product != "chrome": true + +[close.https.any.worker.html] + disabled: + if product != "chrome": true + +[close.https.any.sharedworker.html] + disabled: + if product != "chrome": true + +[close.https.any.serviceworker.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/connect.https.any.js b/testing/web-platform/tests/webtransport/connect.https.any.js new file mode 100644 index 0000000000..93df5fe3f4 --- /dev/null +++ b/testing/web-platform/tests/webtransport/connect.https.any.js @@ -0,0 +1,88 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + await wt.ready; +}, 'WebTransport session is established with status code 200'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=204')); + await wt.ready; +}, 'WebTransport session is established with status code 204'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=301')); + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. Ditto below. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment fails with status code 301'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=401')); + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment with status code 401'); + +promise_test(async t => { + const wt = new WebTransport(webtransport_url('custom-response.py?:status=404')); + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise shoud be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport session establishment fails with status code 404'); + +promise_test(async t => { + // Create WebTransport session. + const wt = new WebTransport(webtransport_url('echo-request-headers.py')); + await wt.ready; + + // Read incoming unidirectional stream for echoed request headers. + const streams = await wt.incomingUnidirectionalStreams; + + const stream_reader = streams.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const request_headers = await read_stream_as_json(recv_stream); + + // Check the standard request headers. + check_and_remove_standard_headers(request_headers); +}, 'Echo back request headers'); + +promise_test(async t => { + // Create WebTransport session, and attach "Set-Cookie: foo=bar" to the response of + // the handshake. + const encodedSetCookie = encodeURIComponent('foo=bar'); + let wt = new WebTransport(webtransport_url('custom-response.py?set-cookie=' + encodedSetCookie)); + await wt.ready; + + wt = new WebTransport(webtransport_url('echo-request-headers.py')); + await wt.ready; + + // Read incoming unidirectional stream for echoed request headers. + const streams = await wt.incomingUnidirectionalStreams; + + const stream_reader = streams.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const request_headers = await read_stream_as_json(recv_stream); + + // Check cookie header is not echoed back. + check_and_remove_standard_headers(request_headers); + assert_equals(request_headers['cookie'], undefined); +}, 'Cookie header is not echoed back'); diff --git a/testing/web-platform/tests/webtransport/connect.https.any.js.ini b/testing/web-platform/tests/webtransport/connect.https.any.js.ini new file mode 100644 index 0000000000..d53cd702cc --- /dev/null +++ b/testing/web-platform/tests/webtransport/connect.https.any.js.ini @@ -0,0 +1,19 @@ +[connect.https.any.html] + disabled: + if product != "chrome": true + +[connect.https.any.window.html] + disabled: + if product != "chrome": true + +[connect.https.any.worker.html] + disabled: + if product != "chrome": true + +[connect.https.any.sharedworker.html] + disabled: + if product != "chrome": true + +[connect.https.any.serviceworker.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/constructor.https.any.js b/testing/web-platform/tests/webtransport/constructor.https.any.js new file mode 100644 index 0000000000..acb91d84aa --- /dev/null +++ b/testing/web-platform/tests/webtransport/constructor.https.any.js @@ -0,0 +1,55 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +const BAD_URLS = [ + null, + '', + 'no-scheme', + 'http://example.com/' /* scheme is wrong */, + 'quic-transport://example.com/' /* scheme is wrong */, + 'https:///' /* no host specified */, + 'https://example.com/#failing' /* has fragment */, + `https://${HOST}:999999/` /* invalid port */, +]; + +for (const url of BAD_URLS) { + test(() => { + assert_throws_dom('SyntaxError', () => new WebTransport(url), + 'constructor should throw'); + }, `WebTransport constructor should reject URL '${url}'`); +} + +const OPTIONS = [ + { allowPooling: true }, + { requireUnreliable: true }, + { allowPooling: true, requireUnreliable: true }, + { congestionControl: "default" }, + { congestionControl: "throughput" }, + { congestionControl: "low-latency" }, + { allowPooling: true, requireUnreliable: true, congestionControl: "low-latency" }, + // XXX Need to test serverCertificateHashes +]; + +for (const options of OPTIONS) { + promise_test(async t => { + const wt = new WebTransport(`https://${HOST}:0/`, options ); + await wt.ready; + wt.close(); + }, "WebTransport constructor should allow options " + JSON.stringify(options)); +} + +promise_test(async t => { + const wt = new WebTransport(`https://${HOST}:0/`); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + // We get a possible error, and then make sure wt.ready is rejected with it. + const e = await wt.ready.catch(e => e); + + await promise_rejects_exactly(t, e, wt.ready, 'ready should be rejected'); + await promise_rejects_exactly(t, e, wt.closed, 'closed should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'Connection to port 0 should fail'); diff --git a/testing/web-platform/tests/webtransport/csp-fail.https.window.js b/testing/web-platform/tests/webtransport/csp-fail.https.window.js new file mode 100644 index 0000000000..f2e49fad99 --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-fail.https.window.js @@ -0,0 +1,27 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +function set_csp(destination) { + let meta = document.createElement('meta'); + meta.httpEquiv = 'Content-Security-Policy'; + meta.content = `connect-src ${destination}`; + return meta; +} + +promise_test(async t => { + let meta = set_csp("'none'"); + document.head.appendChild(meta); + + let wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. + const e = await wt.ready.catch(e => e); + await promise_rejects_exactly(t, e, wt.ready, 'ready promise should be rejected'); + await promise_rejects_exactly(t, e, wt.closed, 'closed promise should be rejected'); + assert_equals(e.name, 'WebTransportError', 'WebTransportError'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'session', 'source'); + assert_equals(e.streamErrorCode, null, 'streamErrorCode'); +}, 'WebTransport connection should fail when CSP connect-src is set to none and reject the promises'); diff --git a/testing/web-platform/tests/webtransport/csp-fail.https.window.js.ini b/testing/web-platform/tests/webtransport/csp-fail.https.window.js.ini new file mode 100644 index 0000000000..adbd3c1359 --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-fail.https.window.js.ini @@ -0,0 +1,3 @@ +[csp-fail.https.window.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/csp-pass.https.window.js b/testing/web-platform/tests/webtransport/csp-pass.https.window.js new file mode 100644 index 0000000000..3c315d078f --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-pass.https.window.js @@ -0,0 +1,18 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +function set_csp(destination) { + let meta = document.createElement('meta'); + meta.httpEquiv = 'Content-Security-Policy'; + meta.content = `connect-src ${destination}`; + return meta; +} + +promise_test(async t => { + let meta = set_csp(`${BASE}`); + document.head.appendChild(meta); + + let wt = new WebTransport(webtransport_url('custom-response.py?:status=200')); + await wt.ready; +}, 'WebTransport connection should succeed when CSP connect-src destination is set to the page'); diff --git a/testing/web-platform/tests/webtransport/csp-pass.https.window.js.ini b/testing/web-platform/tests/webtransport/csp-pass.https.window.js.ini new file mode 100644 index 0000000000..28d68410bb --- /dev/null +++ b/testing/web-platform/tests/webtransport/csp-pass.https.window.js.ini @@ -0,0 +1,3 @@ +[csp-pass.https.window.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js new file mode 100644 index 0000000000..fae613e213 --- /dev/null +++ b/testing/web-platform/tests/webtransport/datagram-cancel-crash.https.window.js @@ -0,0 +1,12 @@ +// This test reproduces Chromium issue https://crbug.com/1292387. If it doesn't +// crash then the test passed. + +test(() => { + const iframeTag = document.createElement('iframe'); + document.body.appendChild(iframeTag); + const wt = new iframeTag.contentWindow.WebTransport('https://example.com/'); + iframeTag.remove(); + const datagrams = wt.datagrams; + const reader = datagrams.readable; + reader.cancel(); +}, 'call cancel() on stream in destroyed realm'); diff --git a/testing/web-platform/tests/webtransport/datagrams.https.any.js b/testing/web-platform/tests/webtransport/datagrams.https.any.js new file mode 100644 index 0000000000..b96b36fc36 --- /dev/null +++ b/testing/web-platform/tests/webtransport/datagrams.https.any.js @@ -0,0 +1,271 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +// Write datagrams until the producer receives the AbortSignal. +async function write_datagrams(writer, signal) { + const encoder = new TextEncoder(); + let counter = 0; + const sentTokens = []; + const aborted = new Promise((resolve) => { + signal.addEventListener('abort', resolve); + }); + while (true) { + await Promise.race([writer.ready, aborted]); + if (signal.aborted) { + break; + } + var token = counter.toString(); + sentTokens.push(token); + writer.write(encoder.encode(token)); + counter++; + } + return sentTokens; +} + +// Read datagrams until the consumer has received enough i.e. N datagrams. Call +// abort() after reading. +async function read_datagrams(reader, controller, N) { + const decoder = new TextDecoder(); + const receivedTokens = []; + while (receivedTokens.length < N) { + const { value: token, done } = await reader.read(); + assert_false(done); + receivedTokens.push(decoder.decode(token)); + } + controller.abort(); + return receivedTokens; +} + +// Write numbers until the producer receives the AbortSignal. +async function write_numbers(writer, signal) { + let counter = 0; + const sentNumbers = []; + const aborted = + new Promise((resolve) => signal.addEventListener('abort', resolve)); + // Counter should be less than 256 because reader stores numbers in Uint8Array. + while (counter < 256) { + await Promise.race([writer.ready, aborted]) + if (signal.aborted) { + break; + } + sentNumbers.push(counter); + chunk = new Uint8Array(1); + chunk[0] = counter; + writer.write(chunk); + counter++; + } + return sentNumbers; +} + +// Write large datagrams of size 10 until the producer receives the AbortSignal. +async function write_large_datagrams(writer, signal) { + const aborted = new Promise((resolve) => { + signal.addEventListener('abort', resolve); + }); + while (true) { + await Promise.race([writer.ready, aborted]); + if (signal.aborted) { + break; + } + writer.write(new Uint8Array(10)); + } +} + +// Read datagrams with BYOB reader until the consumer has received enough i.e. N +// datagrams. Call abort() after reading. +async function read_numbers_byob(reader, controller, N) { + let buffer = new ArrayBuffer(N); + buffer = await readInto(reader, buffer); + controller.abort(); + return Array.from(new Uint8Array(buffer)); +} + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + const N = 5; + const [sentTokens, receivedTokens] = await Promise.all([ + write_datagrams(writer, signal), + read_datagrams(reader, controller, N) + ]); + + // Check receivedTokens is a subset of sentTokens. + const subset = receivedTokens.every(token => sentTokens.includes(token)); + assert_true(subset); +}, 'Datagrams are echoed successfully'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader({ mode: 'byob' }); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + // Numbers are less than 256, consider N to be a small number. + const N = 5; + const [sentNumbers, receiveNumbers] = await Promise.all([ + write_numbers(writer, signal), + read_numbers_byob(reader, controller, N) + ]); + + // No duplicated numbers received. + assert_equals((new Set(receiveNumbers)).size, N); + + // Check receiveNumbers is a subset of sentNumbers. + const subset = receiveNumbers.every(token => sentNumbers.includes(token)); + assert_true(subset); +}, 'Successfully reading datagrams with BYOB reader.'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader({ mode: 'byob' }); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write datagrams of size 10, but only 1 byte buffer is provided for BYOB + // reader. To avoid splitting a datagram, stream will be errored. + const buffer = new ArrayBuffer(1); + const [error, _] = await Promise.all([ + reader.read(new Uint8Array(buffer)).catch(e => { + controller.abort(); + return e; + }), + write_large_datagrams(writer, signal) + ]); + assert_equals(error.name, 'RangeError'); +}, 'Reading datagrams with insufficient buffer should be rejected.'); + +promise_test(async t => { + // Make a WebTransport connection, but session is not necessarily established. + const wt = new WebTransport(webtransport_url('echo.py')); + + const writer = wt.datagrams.writable.getWriter(); + const reader = wt.datagrams.readable.getReader(); + + const controller = new AbortController(); + const signal = controller.signal; + + // Write and read datagrams. + const N = 1; + const [sentTokens, receivedTokens] = await Promise.all([ + write_datagrams(writer, signal), + read_datagrams(reader, controller, N) + ]); + + // Check receivedTokens is a subset of sentTokens. + const subset = receivedTokens.every(token => sentTokens.includes(token)); + assert_true(subset); + + // Make sure WebTransport session is established. + await wt.ready; +}, 'Sending and receiving datagrams is ready to use before session is established'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const N = 5; + wt.datagrams.outgoingHighWaterMark = N; + + const writer = wt.datagrams.writable.getWriter(); + const encoder = new TextEncoder(); + + // Write N-1 datagrams. + let counter; + for (counter = 0; counter < N-1; counter++) { + var datagram = counter.toString(); + let resolved = false; + writer.write(encoder.encode(datagram)); + + // Check writer.ready resolves immediately. + writer.ready.then(() => resolved = true); + // TODO(nidhijaju): The number of `await Promise.resolve()` calls is + // implementation dependent, so we should not have this as the final + // solution. + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + assert_true(resolved); + } + + // Write one more datagram. + resolved = false; + const last_datagram = counter.toString(); + writer.write(encoder.encode(last_datagram)); + + // Check writer.ready does not resolve immediately. + writer.ready.then(() => resolved = true); + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + assert_false(resolved); + + // Make sure writer.ready is resolved eventually. + await writer.ready; +}, 'Datagram\'s outgoingHighWaterMark correctly regulates written datagrams'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + const N = 5; + wt.datagrams.incomingHighWaterMark = N; + + const writer = wt.datagrams.writable.getWriter(); + const encoder = new TextEncoder(); + + // Write 10*N datagrams. + let counter; + for (counter = 0; counter < 10*N; counter++) { + var datagram = counter.toString(); + writer.write(encoder.encode(datagram)); + await writer.ready; + } + + // Wait for incoming datagrams to arrive. + wait(500); + + const reader = wt.datagrams.readable.getReader(); + + // Read all of the immediately available datagrams. + let receivedDatagrams = 0; + while (true) { + let resolved = false; + reader.read().then(() => resolved = true); + // TODO(nidhijaju): Find a better solution instead of just having numerous + // `await Promise.resolve()` calls. + for (let i = 0; i < 10; i++) { + await Promise.resolve(); + } + if (!resolved) { + break; + } + receivedDatagrams++; + } + + // Check that the receivedDatagrams is less than or equal to the + // incomingHighWaterMark. + assert_less_than_equal(receivedDatagrams, N); +}, 'Datagrams read is less than or equal to the incomingHighWaterMark'); diff --git a/testing/web-platform/tests/webtransport/datagrams.https.any.js.ini b/testing/web-platform/tests/webtransport/datagrams.https.any.js.ini new file mode 100644 index 0000000000..49ad1548df --- /dev/null +++ b/testing/web-platform/tests/webtransport/datagrams.https.any.js.ini @@ -0,0 +1,19 @@ +[datagrams.https.any.html] + disabled: + if product != "chrome": true + +[datagrams.https.any.window.html] + disabled: + if product != "chrome": true + +[datagrams.https.any.worker.html] + disabled: + if product != "chrome": true + +[datagrams.https.any.sharedworker.html] + disabled: + if product != "chrome": true + +[datagrams.https.any.serviceworker.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py new file mode 100644 index 0000000000..1d92b705e6 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/abort-stream-from-server.py @@ -0,0 +1,27 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + code = qs[b'code'] + if code is None: + raise Exception('code is missing, path = {}'.format(path)) + session.dict_for_handlers['code'] = int(code) + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + code: int = session.dict_for_handlers['code'] + if session.stream_is_unidirectional(stream_id): + session.stop_stream(stream_id, code) + else: + session.stop_stream(stream_id, code) + session.reset_stream(stream_id, code)
\ No newline at end of file diff --git a/testing/web-platform/tests/webtransport/handlers/client-close.py b/testing/web-platform/tests/webtransport/handlers/client-close.py new file mode 100644 index 0000000000..01fb14b134 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/client-close.py @@ -0,0 +1,59 @@ +from typing import Optional, Tuple +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + token = qs[b'token'] + if token is None: + raise Exception('token is missing, path = {}'.format(path)) + session.dict_for_handlers['token'] = token + session.create_bidirectional_stream() + + +def stream_reset(session, stream_id: int, error_code: int) -> None: + token = session.dict_for_handlers['token'] + data = session.stash.take(key=token) or {} + + data['stream-close-info'] = { + 'source': 'reset', + 'code': error_code + } + session.stash.put(key=token, value=data) + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + if stream_ended: + token = session.dict_for_handlers['token'] + stashed_data = session.stash.take(key=token) or {} + stashed_data['stream-close-info'] = { + 'source': 'FIN', + } + session.stash.put(key=token, value=stashed_data) + + +def session_closed( + session, close_info: Optional[Tuple[int, bytes]], abruptly: bool) -> None: + token = session.dict_for_handlers['token'] + data = session.stash.take(key=token) or {} + + decoded_close_info: Optional[Dict[str, Any]] = None + if close_info: + decoded_close_info = { + 'code': close_info[0], + 'reason': close_info[1].decode() + } + + data['session-close-info'] = { + 'abruptly': abruptly, + 'close_info': decoded_close_info + } + session.stash.put(key=token, value=data) diff --git a/testing/web-platform/tests/webtransport/handlers/custom-response.py b/testing/web-platform/tests/webtransport/handlers/custom-response.py new file mode 100644 index 0000000000..05e4c4ba36 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/custom-response.py @@ -0,0 +1,14 @@ +from urllib.parse import urlsplit, parse_qsl + + +def connect_received(request_headers, response_headers): + for data in request_headers: + if data[0] == b':path': + path = data[1].decode('utf-8') + + qs = dict(parse_qsl(urlsplit(path).query)) + for key, value in qs.items(): + response_headers.append((key.encode('utf-8'), value.encode('utf-8'))) + + break + return diff --git a/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py new file mode 100644 index 0000000000..122d6f0601 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/echo-request-headers.py @@ -0,0 +1,11 @@ +import json + + +def session_established(session): + headers = {} + for name, value in session.request_headers: + headers[name.decode('utf-8')] = value.decode('utf-8') + + stream_id = session.create_unidirectional_stream() + data = json.dumps(headers).encode('utf-8') + session.send_stream_data(stream_id, data, end_stream=True) diff --git a/testing/web-platform/tests/webtransport/handlers/echo.py b/testing/web-platform/tests/webtransport/handlers/echo.py new file mode 100644 index 0000000000..4347d4aae3 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/echo.py @@ -0,0 +1,33 @@ +streams_dict = {} + + +def session_established(session): + # When a WebTransport session is established, a bidirectional stream is + # created by the server, which is used to echo back stream data from the + # client. + session.create_bidirectional_stream() + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + # If a stream is unidirectional, create a new unidirectional stream and echo + # back the data on that stream. + if session.stream_is_unidirectional(stream_id): + if (session.session_id, stream_id) not in streams_dict.keys(): + new_stream_id = session.create_unidirectional_stream() + streams_dict[(session.session_id, stream_id)] = new_stream_id + session.send_stream_data(streams_dict[(session.session_id, stream_id)], + data, + end_stream=stream_ended) + if (stream_ended): + del streams_dict[(session.session_id, stream_id)] + return + # Otherwise (e.g. if the stream is bidirectional), echo back the data on the + # same stream. + session.send_stream_data(stream_id, data, end_stream=stream_ended) + + +def datagram_received(session, data: bytes): + session.send_datagram(data) diff --git a/testing/web-platform/tests/webtransport/handlers/query.py b/testing/web-platform/tests/webtransport/handlers/query.py new file mode 100644 index 0000000000..75d458255a --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/query.py @@ -0,0 +1,19 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl +import json + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + token = qs[b'token'] + if token is None: + raise Exception('token is missing, path = {}'.format(path)) + + stream_id = session.create_unidirectional_stream() + data = json.dumps(session.stash.take(key=token) or {}).encode('utf-8') + session.send_stream_data(stream_id, data, end_stream=True) diff --git a/testing/web-platform/tests/webtransport/handlers/server-close.py b/testing/web-platform/tests/webtransport/handlers/server-close.py new file mode 100644 index 0000000000..e9d08f483c --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/server-close.py @@ -0,0 +1,16 @@ +from typing import Optional +from urllib.parse import urlsplit, parse_qsl + + +def session_established(session): + path: Optional[bytes] = None + for key, value in session.request_headers: + if key == b':path': + path = value + assert path is not None + qs = dict(parse_qsl(urlsplit(path).query)) + code = qs[b'code'] if b'code' in qs else None + reason = qs[b'reason'] if b'reason' in qs else b'' + close_info = None if code is None else (int(code), reason) + + session.close(close_info) diff --git a/testing/web-platform/tests/webtransport/handlers/server-connection-close.py b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py new file mode 100644 index 0000000000..9721c67170 --- /dev/null +++ b/testing/web-platform/tests/webtransport/handlers/server-connection-close.py @@ -0,0 +1,9 @@ +def session_established(session): + session.create_bidirectional_stream() + + +def stream_data_received(session, + stream_id: int, + data: bytes, + stream_ended: bool): + session._http._quic.close()
\ No newline at end of file diff --git a/testing/web-platform/tests/webtransport/idlharness.https.any.js b/testing/web-platform/tests/webtransport/idlharness.https.any.js new file mode 100644 index 0000000000..248bc41abe --- /dev/null +++ b/testing/web-platform/tests/webtransport/idlharness.https.any.js @@ -0,0 +1,24 @@ +// META: global=window,worker +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +idl_test( + ['webtransport'], + ['webidl', 'streams'], + idl_array => { + idl_array.add_objects({ + WebTransport: ['webTransport'], + // TODO: The stream APIs below require a working connection to create. + // BidirectionalStream + // SendStream + // ReceiveStream + }); + self.webTransport = new WebTransport("https://example.com/"); + // `ready` and `closed` promises will be rejected due to connection error. + // Catches them to avoid unhandled rejections. + self.webTransport.ready.catch(() => {}); + self.webTransport.closed.catch(() => {}); + } +); diff --git a/testing/web-platform/tests/webtransport/in-removed-iframe.https.html b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html new file mode 100644 index 0000000000..76204ac6a1 --- /dev/null +++ b/testing/web-platform/tests/webtransport/in-removed-iframe.https.html @@ -0,0 +1,25 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> +'use strict'; +setup({ single_test: true }); + +function iframeOnLoad() { + const target = document.querySelector('#target'); + const wt = target.contentWindow.wt; + target.remove(); + const streams = wt.incomingBidirectionalStreams; + assert_equals(typeof streams, 'object', 'streams should be an object'); + done(); +} +</script> + +<iframe id=target onload="iframeOnLoad()" srcdoc=" +<!doctype html> +<script src=/common/get-host-info.sub.js></script> +<script src=resources/webtransport-test-helpers.sub.js></script> +<script> +window.wt = new WebTransport(webtransport_url('echo.py')); +</script> +"></iframe> diff --git a/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js new file mode 100644 index 0000000000..733153e120 --- /dev/null +++ b/testing/web-platform/tests/webtransport/resources/webtransport-test-helpers.sub.js @@ -0,0 +1,95 @@ +// The file including this must also include /common/get-host-info.sub.js to +// pick up the necessary constants. + +const HOST = get_host_info().ORIGINAL_HOST; +const PORT = '{{ports[webtransport-h3][0]}}'; +const BASE = `https://${HOST}:${PORT}`; + +// Wait for the given number of milliseconds (ms). +function wait(ms) { return new Promise(res => step_timeout(res, ms)); } + +// Create URL for WebTransport session. +function webtransport_url(handler) { + return `${BASE}/webtransport/handlers/${handler}`; +} + +// Converts WebTransport stream error code to HTTP/3 error code. +// https://ietf-wg-webtrans.github.io/draft-ietf-webtrans-http3/draft-ietf-webtrans-http3.html#section-4.3 +function webtransport_code_to_http_code(n) { + const first = 0x52e4a40fa8db; + return first + n + Math.floor(n / 0x1e); +} + +// Read all chunks from |readable_stream|, decode chunks to a utf-8 string, then +// return the string. +async function read_stream_as_string(readable_stream) { + const decoder = new TextDecoderStream(); + const decode_stream = readable_stream.pipeThrough(decoder); + const reader = decode_stream.getReader(); + + let chunks = ''; + while (true) { + const {value: chunk, done} = await reader.read(); + if (done) { + break; + } + chunks += chunk; + } + reader.releaseLock(); + + return chunks; +} + +// Decode all chunks in a given ReadableStream, and parse the data using JSON. +async function read_stream_as_json(readable_stream) { + const text = await read_stream_as_string(readable_stream); + return JSON.parse(text); +} + +// Check the standard request headers and delete them, leaving any "unique" +// headers to check in the test. +function check_and_remove_standard_headers(headers) { + assert_equals(headers[':scheme'], 'https'); + delete headers[':scheme']; + assert_equals(headers[':method'], 'CONNECT'); + delete headers[':method']; + assert_equals(headers[':authority'], `${HOST}:${PORT}`); + delete headers[':authority']; + assert_equals(headers[':path'], '/webtransport/handlers/echo-request-headers.py'); + delete headers[':path']; + assert_equals(headers[':protocol'], 'webtransport'); + delete headers[':protocol']; + assert_equals(headers['origin'], `${get_host_info().ORIGIN}`); + delete headers['origin']; +} + +async function query(token) { + const wt = new WebTransport(webtransport_url(`query.py?token=${token}`)); + try { + await wt.ready; + const streams = await wt.incomingUnidirectionalStreams; + const streams_reader = streams.getReader(); + const { value: readable } = await streams_reader.read(); + streams_reader.releaseLock(); + + return await read_stream_as_json(readable); + } finally { + wt.close(); + } +} + +async function readInto(reader, buffer) { + let offset = 0; + + while (offset < buffer.byteLength) { + const {value: view, done} = await reader.read( + new Uint8Array(buffer, offset, buffer.byteLength - offset)); + buffer = view.buffer; + if (done) { + break; + } + offset += view.byteLength; + } + + return buffer; +} diff --git a/testing/web-platform/tests/webtransport/streams-close.https.any.js b/testing/web-platform/tests/webtransport/streams-close.https.any.js new file mode 100644 index 0000000000..4871ee8e7b --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-close.https.any.js @@ -0,0 +1,252 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=/common/utils.js +// META: script=resources/webtransport-test-helpers.sub.js + +// Note: There is no aioquic event for STOP_SENDING yet, so the server does +// not support checking this yet. Hence, tests checking from the STOP_SENDING +// signal cannot be tested yet. + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi_stream = await wt.createBidirectionalStream(); + + const writable = bidi_stream.writable; + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / bidi-1'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const streams_reader = wt.incomingBidirectionalStreams.getReader(); + const {value: bidi} = await streams_reader.read(); + + const writable = bidi.writable; + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / bidi-2'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + writable.close(); + + await wait(10); + const data = await query(id); + + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'FIN', 'source'); +}, 'Close outgoing stream / uni'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi_stream = await wt.createBidirectionalStream(); + + const writable = bidi_stream.writable; + + const WT_CODE = 139; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writable.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort client-created bidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + const writer = bidi_stream.writable.getWriter(); + + const WT_CODE = 52; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writer.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort server-initiated bidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + + const WT_CODE = 95; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + await writable.abort( + new WebTransportError({streamErrorCode: WT_CODE})); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Abort unidirectional stream with WebTransportError'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + const writer = writable.getWriter(); + + const WT_CODE = 134; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + + // We use a large chunk so that sending the FIN signal takes time. + const chunk = new Uint8Array(64 * 1024); + const e = new WebTransportError({streamErrorCode: WT_CODE}); + // Write a chunk, close the stream, and then abort the stream immediately to + // abort the closing operation. + // TODO: Check that the abort promise is correctly rejected/resolved based on + // the spec discussion at https://github.com/whatwg/streams/issues/1203. + await writer.write(chunk); + const close_promise = writer.close(); + const abort_promise = writer.abort(e); + + await promise_rejects_exactly(t, e, close_promise, 'close_promise'); + await promise_rejects_exactly(t, e, writer.closed, '.closed'); + await promise_rejects_exactly(t, e, abort_promise, 'abort_promise'); + writer.releaseLock(); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, HTTP_CODE, 'code'); +}, 'Close and abort unidirectional stream'); + +promise_test(async t => { + const id = token(); + const wt = new WebTransport(webtransport_url(`client-close.py?token=${id}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + await writable.abort(); + + await wait(10); + const data = await query(id); + + // Check that stream is aborted with RESET_STREAM with the code and reason + assert_own_property(data, 'stream-close-info'); + const info = data['stream-close-info']; + + assert_equals(info.source, 'reset', 'reset_stream'); + assert_equals(info.code, webtransport_code_to_http_code(0), 'code'); +}, 'Abort unidirectional stream with default error code'); + +promise_test(async t => { + const WT_CODE = 240; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + const wt = new WebTransport( + webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const writable = await wt.createUnidirectionalStream(); + const writer = writable.getWriter(); + + // Write something, to make the stream visible to the server side. + await writer.write(new Uint8Array([64])); + + // Sadly we cannot use promise_rejects_dom as the error constructor is + // WebTransportError rather than DOMException. Ditto below. + // We get a possible error, and then make sure wt.closed is rejected with it. + const e = await writer.closed.catch(e => e); + await promise_rejects_exactly( + t, e, writer.closed, 'closed promise should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'stream', 'source'); + assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode'); +}, 'STOP_SENDING coming from server'); + +promise_test(async t => { + const WT_CODE = 127; + const HTTP_CODE = webtransport_code_to_http_code(WT_CODE); + const wt = new WebTransport( + webtransport_url(`abort-stream-from-server.py?code=${HTTP_CODE}`)); + add_completion_callback(() => wt.close()); + await wt.ready; + + const bidi = await wt.createBidirectionalStream(); + const writer = bidi.writable.getWriter(); + + // Write something, to make the stream visible to the server side. + await writer.write(new Uint8Array([64])); + + const reader = bidi.readable.getReader(); + const e = await reader.closed.catch(e => e); + await promise_rejects_exactly( + t, e, reader.closed, 'closed promise should be rejected'); + assert_true(e instanceof WebTransportError); + assert_equals(e.source, 'stream', 'source'); + assert_equals(e.streamErrorCode, WT_CODE, 'streamErrorCode'); +}, 'RESET_STREAM coming from server'); diff --git a/testing/web-platform/tests/webtransport/streams-close.https.any.js.ini b/testing/web-platform/tests/webtransport/streams-close.https.any.js.ini new file mode 100644 index 0000000000..0d60f7e54d --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-close.https.any.js.ini @@ -0,0 +1,19 @@ +[streams-close.https.any.html] + disabled: + if product != "chrome": true + +[streams-close.https.any.window.html] + disabled: + if product != "chrome": true + +[streams-close.https.any.worker.html] + disabled: + if product != "chrome": true + +[streams-close.https.any.sharedworker.html] + disabled: + if product != "chrome": true + +[streams-close.https.any.serviceworker.html] + disabled: + if product != "chrome": true diff --git a/testing/web-platform/tests/webtransport/streams-echo.https.any.js b/testing/web-platform/tests/webtransport/streams-echo.https.any.js new file mode 100644 index 0000000000..32781419eb --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-echo.https.any.js @@ -0,0 +1,153 @@ +// META: global=window,worker +// META: script=/common/get-host-info.sub.js +// META: script=resources/webtransport-test-helpers.sub.js + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a bidirectional stream. + const bidi_stream = await wt.createBidirectionalStream(); + + // Write a message to the writable end, and close it. + const writer = bidi_stream.writable.getWriter(); + const encoder = new TextEncoder(); + await writer.write(encoder.encode('Hello World')); + await writer.close(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(bidi_stream.readable); + + // Check that the message from the readable end matches the writable end. + assert_equals(reply, 'Hello World'); +}, 'WebTransport server should be able to create and handle a bidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // The echo handler creates a bidirectional stream when a WebTransport session + // is established. Accept the bidirectional stream. + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const { value: bidi_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Write a message to the writable end, and close it. + const encoder = new TextEncoderStream(); + encoder.readable.pipeTo(bidi_stream.writable); + const writer = encoder.writable.getWriter(); + await writer.write('Hello World'); + await writer.close(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(bidi_stream.readable); + + // Check that the message from the readable end matches the writable end. + assert_equals(reply, 'Hello World'); +}, 'WebTransport server should be able to accept and handle a bidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a unidirectional stream. + const writable = await wt.createUnidirectionalStream(); + + // Write a message to the writable end, and close it. + const encoder = new TextEncoderStream(); + encoder.readable.pipeTo(writable); + const writer = encoder.writable.getWriter(); + await writer.write('Hello World'); + await writer.close(); + + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const { value: recv_stream } = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end. + const reply = await read_stream_as_string(recv_stream); + + // Make sure the message on the writable and readable ends of the streams + // match. + assert_equals(reply, 'Hello World'); +}, 'WebTransport server should be able to create, accept, and handle a unidirectional stream'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // The echo handler creates a bidirectional stream when a WebTransport session + // is established. Accept the bidirectional stream. + const stream_reader = wt.incomingBidirectionalStreams.getReader(); + const {value: bidi_stream} = await stream_reader.read(); + stream_reader.releaseLock(); + + // Write data to the writable end, and close it. + const buffer_size = 256; + const data = new Uint8Array(buffer_size); + for (let i = 0; i < data.byteLength; ++i) { + data[i] = i; + } + const writer = bidi_stream.writable.getWriter(); + writer.write(data); + await writer.close(); + + // Read the data on the readable end and check if it matches the writable end. + const reader = bidi_stream.readable.getReader({mode: 'byob'}); + assert_true(reader instanceof ReadableStreamBYOBReader); + const half_buffer_size = buffer_size / 2; + for (let i = 0; i < 2; i++) { + let buffer = new ArrayBuffer(half_buffer_size); + buffer = await readInto(reader, buffer); + assert_array_equals( + new Uint8Array(buffer), + data.subarray(half_buffer_size * i, half_buffer_size * (i + 1))) + } + reader.releaseLock(); +}, 'Can read data from a bidirectional stream with BYOB reader'); + +promise_test(async t => { + // Establish a WebTransport session. + const wt = new WebTransport(webtransport_url('echo.py')); + await wt.ready; + + // Create a unidirectional stream. + const writable = await wt.createUnidirectionalStream(); + + // Write data to the writable end, and close it. + const buffer_size = 256; + const data = new Uint8Array(buffer_size); + for (let i = 0; i < data.byteLength; ++i) { + data[i] = i; + } + const writer = writable.getWriter(); + writer.write(data); + await writer.close(); + + // The echo handler creates a new unidirectional stream to echo back data from + // the server to client. Accept the unidirectional stream. + const readable = wt.incomingUnidirectionalStreams; + const stream_reader = readable.getReader(); + const {value: recv_stream} = await stream_reader.read(); + stream_reader.releaseLock(); + + // Read the data on the readable end and check if it matches the writable end. + const reader = recv_stream.getReader({mode: 'byob'}); + assert_true(reader instanceof ReadableStreamBYOBReader); + const half_buffer_size = buffer_size / 2; + let buffer = new ArrayBuffer(half_buffer_size); + for (let i = 0; i < 2; i++) { + buffer = await readInto(reader, buffer); + assert_array_equals( + new Uint8Array(buffer), + data.subarray(half_buffer_size * i, half_buffer_size * (i + 1))) + } + reader.releaseLock(); +}, 'Can read data from a unidirectional stream with BYOB reader'); diff --git a/testing/web-platform/tests/webtransport/streams-echo.https.any.js.ini b/testing/web-platform/tests/webtransport/streams-echo.https.any.js.ini new file mode 100644 index 0000000000..eb11ca786f --- /dev/null +++ b/testing/web-platform/tests/webtransport/streams-echo.https.any.js.ini @@ -0,0 +1,19 @@ +[streams-echo.https.any.html] + disabled: + if product != "chrome": true + +[streams-echo.https.any.window.html] + disabled: + if product != "chrome": true + +[streams-echo.https.any.worker.html] + disabled: + if product != "chrome": true + +[streams-echo.https.any.sharedworker.html] + disabled: + if product != "chrome": true + +[streams-echo.https.any.serviceworker.html] + disabled: + if product != "chrome": true |