From 7ebc8c2aedef30a994234a66736ac84ab2936e09 Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Thu, 9 Nov 2023 00:06:50 +0100 Subject: [PATCH] test,stream: enable compression WPTs PR-URL: https://github.com/nodejs/node/pull/50631 Reviewed-By: James M Snell Reviewed-By: Matteo Collina --- test/common/wpt.js | 19 +- test/fixtures/wpt/README.md | 1 + test/fixtures/wpt/compression/META.yml | 3 + .../compression-bad-chunks.tentative.any.js | 74 ++++ ...ression-constructor-error.tentative.any.js | 15 + ...ion-including-empty-chunk.tentative.any.js | 63 ++++ .../compression-large-flush-output.any.js | 41 +++ ...mpression-multiple-chunks.tentative.any.js | 67 ++++ ...compression-output-length.tentative.any.js | 64 ++++ .../compression-stream.tentative.any.js | 91 +++++ ...ompression-with-detach.tentative.window.js | 55 +++ .../decompression-bad-chunks.tentative.any.js | 85 +++++ ...ecompression-buffersource.tentative.any.js | 192 +++++++++++ ...ression-constructor-error.tentative.any.js | 15 + ...compression-correct-input.tentative.any.js | 39 +++ ...compression-corrupt-input.tentative.any.js | 318 ++++++++++++++++++ ...decompression-empty-input.tentative.any.js | 43 +++ ...decompression-split-chunk.tentative.any.js | 53 +++ ...ression-uint8array-output.tentative.any.js | 30 ++ ...ompression-with-detach.tentative.window.js | 41 +++ .../idlharness-shadowrealm.window.js | 2 + .../wpt/compression/idlharness.https.any.js | 17 + .../resources/concatenate-stream.js | 25 ++ .../wpt/compression/third_party/pako/LICENSE | 21 ++ .../wpt/compression/third_party/pako/README | 2 + .../third_party/pako/pako_inflate.min.js | 1 + test/fixtures/wpt/media/foo.vtt | 4 + ...-384k-44100Hz-1ch-320x240-30fps-10kfr.webm | Bin 0 -> 76501 bytes test/fixtures/wpt/versions.json | 4 + test/wpt/README.md | 2 +- test/wpt/status/compression.json | 58 ++++ test/wpt/status/performance-timeline.json | 3 + test/wpt/test-compression.js | 7 + 33 files changed, 1448 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/wpt/compression/META.yml create mode 100644 test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-large-flush-output.any.js create mode 100644 test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-output-length.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-stream.tentative.any.js create mode 100644 test/fixtures/wpt/compression/compression-with-detach.tentative.window.js create mode 100644 test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js create mode 100644 test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js create mode 100644 test/fixtures/wpt/compression/idlharness-shadowrealm.window.js create mode 100644 test/fixtures/wpt/compression/idlharness.https.any.js create mode 100644 test/fixtures/wpt/compression/resources/concatenate-stream.js create mode 100644 test/fixtures/wpt/compression/third_party/pako/LICENSE create mode 100644 test/fixtures/wpt/compression/third_party/pako/README create mode 100644 test/fixtures/wpt/compression/third_party/pako/pako_inflate.min.js create mode 100644 test/fixtures/wpt/media/foo.vtt create mode 100644 test/fixtures/wpt/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm create mode 100644 test/wpt/status/compression.json create mode 100644 test/wpt/test-compression.js diff --git a/test/common/wpt.js b/test/common/wpt.js index 7d0b030614b003..a65a0740d155d8 100644 --- a/test/common/wpt.js +++ b/test/common/wpt.js @@ -210,6 +210,7 @@ class ResourceLoader { const data = await fsPromises.readFile(file); return { ok: true, + arrayBuffer() { return data.buffer; }, json() { return JSON.parse(data.toString()); }, text() { return data.toString(); }, }; @@ -382,7 +383,7 @@ const kIntlRequirement = { // TODO(joyeecheung): we may need to deal with --with-intl=system-icu }; -class IntlRequirement { +class BuildRequirement { constructor() { this.currentIntl = kIntlRequirement.none; if (process.config.variables.v8_enable_i18n_support === 0) { @@ -395,6 +396,9 @@ class IntlRequirement { } else { this.currentIntl = kIntlRequirement.full; } + // Not using common.hasCrypto because of the global leak checks + this.hasCrypto = Boolean(process.versions.openssl) && + !process.env.NODE_SKIP_CRYPTO; } /** @@ -409,11 +413,14 @@ class IntlRequirement { if (requires.has('small-icu') && current < kIntlRequirement.small) { return 'small-icu'; } + if (requires.has('crypto') && !this.hasCrypto) { + return 'crypto'; + } return false; } } -const intlRequirements = new IntlRequirement(); +const buildRequirements = new BuildRequirement(); class StatusLoader { /** @@ -440,7 +447,7 @@ class StatusLoader { const list = this.grep(filepath); result = result.concat(list); } else { - if (!(/\.\w+\.js$/.test(filepath)) || filepath.endsWith('.helper.js')) { + if (!(/\.\w+\.js$/.test(filepath))) { continue; } result.push(filepath); @@ -945,9 +952,9 @@ class WPTRunner { continue; } - const lackingIntl = intlRequirements.isLacking(spec.requires); - if (lackingIntl) { - this.skip(spec, [ `requires ${lackingIntl}` ]); + const lackingSupport = buildRequirements.isLacking(spec.requires); + if (lackingSupport) { + this.skip(spec, [ `requires ${lackingSupport}` ]); continue; } diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 6404571a498200..1c7121feb30c9e 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -11,6 +11,7 @@ See [test/wpt](../../wpt/README.md) for information on how these tests are run. Last update: - common: https://github.com/web-platform-tests/wpt/tree/dbd648158d/common +- compression: https://github.com/web-platform-tests/wpt/tree/c82521cfa5/compression - console: https://github.com/web-platform-tests/wpt/tree/767ae35464/console - dom/abort: https://github.com/web-platform-tests/wpt/tree/d1f1ecbd52/dom/abort - dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events diff --git a/test/fixtures/wpt/compression/META.yml b/test/fixtures/wpt/compression/META.yml new file mode 100644 index 00000000000000..0afbe29a53e807 --- /dev/null +++ b/test/fixtures/wpt/compression/META.yml @@ -0,0 +1,3 @@ +spec: https://wicg.github.io/compression/ +suggested_reviewers: + - ricea diff --git a/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js b/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js new file mode 100644 index 00000000000000..2d0b5684733930 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-bad-chunks.tentative.any.js @@ -0,0 +1,74 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const badChunks = [ + { + name: 'undefined', + value: undefined + }, + { + name: 'null', + value: null + }, + { + name: 'numeric', + value: 3.14 + }, + { + name: 'object, not BufferSource', + value: {} + }, + { + name: 'array', + value: [65] + }, + { + name: 'SharedArrayBuffer', + // Use a getter to postpone construction so that all tests don't fail where + // SharedArrayBuffer is not yet implemented. + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer; + } + }, + { + name: 'shared Uint8Array', + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new Uint8Array(new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer) + } + }, +]; + +for (const chunk of badChunks) { + promise_test(async t => { + const cs = new CompressionStream('gzip'); + const reader = cs.readable.getReader(); + const writer = cs.writable.getWriter(); + const writePromise = writer.write(chunk.value); + const readPromise = reader.read(); + await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); + await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); + }, `chunk of type ${chunk.name} should error the stream for gzip`); + + promise_test(async t => { + const cs = new CompressionStream('deflate'); + const reader = cs.readable.getReader(); + const writer = cs.writable.getWriter(); + const writePromise = writer.write(chunk.value); + const readPromise = reader.read(); + await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); + await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); + }, `chunk of type ${chunk.name} should error the stream for deflate`); + + promise_test(async t => { + const cs = new CompressionStream('deflate-raw'); + const reader = cs.readable.getReader(); + const writer = cs.writable.getWriter(); + const writePromise = writer.write(chunk.value); + const readPromise = reader.read(); + await promise_rejects_js(t, TypeError, writePromise, 'write should reject'); + await promise_rejects_js(t, TypeError, readPromise, 'read should reject'); + }, `chunk of type ${chunk.name} should error the stream for deflate-raw`); +} diff --git a/test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js b/test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js new file mode 100644 index 00000000000000..b39ab93bd02aba --- /dev/null +++ b/test/fixtures/wpt/compression/compression-constructor-error.tentative.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +test(t => { + assert_throws_js(TypeError, () => new CompressionStream('a'), 'constructor should throw'); +}, '"a" should cause the constructor to throw'); + +test(t => { + assert_throws_js(TypeError, () => new CompressionStream(), 'constructor should throw'); +}, 'no input should cause the constructor to throw'); + +test(t => { + assert_throws_js(Error, () => new CompressionStream({ toString() { throw Error(); } }), 'constructor should throw'); +}, 'non-string input should cause the constructor to throw'); diff --git a/test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js b/test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js new file mode 100644 index 00000000000000..a7fd1ceb24f086 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-including-empty-chunk.tentative.any.js @@ -0,0 +1,63 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: timeout=long + +'use strict'; + +// This test asserts that compressing '' doesn't affect the compressed data. +// Example: compressing ['Hello', '', 'Hello'] results in 'HelloHello' + +async function compressChunkList(chunkList, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + for (const chunk of chunkList) { + const chunkByte = new TextEncoder().encode(chunk); + writer.write(chunkByte); + } + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +const chunkLists = [ + ['', 'Hello', 'Hello'], + ['Hello', '', 'Hello'], + ['Hello', 'Hello', ''] +]; +const expectedValue = new TextEncoder().encode('HelloHello'); + +for (const chunkList of chunkLists) { + promise_test(async t => { + const compressedData = await compressChunkList(chunkList, 'deflate'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); + }, `the result of compressing [${chunkList}] with deflate should be 'HelloHello'`); + + promise_test(async t => { + const compressedData = await compressChunkList(chunkList, 'gzip'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); + }, `the result of compressing [${chunkList}] with gzip should be 'HelloHello'`); + + promise_test(async t => { + const compressedData = await compressChunkList(chunkList, 'deflate-raw'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); + }, `the result of compressing [${chunkList}] with deflate-raw should be 'HelloHello'`); +} diff --git a/test/fixtures/wpt/compression/compression-large-flush-output.any.js b/test/fixtures/wpt/compression/compression-large-flush-output.any.js new file mode 100644 index 00000000000000..6afcb4d52875b9 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-large-flush-output.any.js @@ -0,0 +1,41 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: script=resources/concatenate-stream.js +// META: timeout=long + +'use strict'; + +// This test verifies that a large flush output will not truncate the +// final results. + +async function compressData(chunk, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(chunk); + writer.close(); + return await concatenateStream(cs.readable); +} + +// JSON-encoded array of 10 thousands numbers ("[0,1,2,...]"). This produces 48_891 bytes of data. +const fullData = new TextEncoder().encode(JSON.stringify(Array.from({ length: 10_000 }, (_, i) => i))); +const data = fullData.subarray(0, 35_579); +const expectedValue = data; + +promise_test(async t => { + const compressedData = await compressData(data, 'deflate'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); +}, `deflate compression with large flush output`); + +promise_test(async t => { + const compressedData = await compressData(data, 'gzip'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); +}, `gzip compression with large flush output`); + +promise_test(async t => { + const compressedData = await compressData(data, 'deflate-raw'); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); +}, `deflate-raw compression with large flush output`); + diff --git a/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js b/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js new file mode 100644 index 00000000000000..28a90e5ca53902 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-multiple-chunks.tentative.any.js @@ -0,0 +1,67 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: timeout=long + +'use strict'; + +// This test asserts that compressing multiple chunks should work. + +// Example: ('Hello', 3) => TextEncoder().encode('HelloHelloHello') +function makeExpectedChunk(input, numberOfChunks) { + const expectedChunk = input.repeat(numberOfChunks); + return new TextEncoder().encode(expectedChunk); +} + +// Example: ('Hello', 3, 'deflate') => compress ['Hello', 'Hello', Hello'] +async function compressMultipleChunks(input, numberOfChunks, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + const chunk = new TextEncoder().encode(input); + for (let i = 0; i < numberOfChunks; ++i) { + writer.write(chunk); + } + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +const hello = 'Hello'; + +for (let numberOfChunks = 2; numberOfChunks <= 16; ++numberOfChunks) { + promise_test(async t => { + const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'deflate'); + const expectedValue = makeExpectedChunk(hello, numberOfChunks); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); + }, `compressing ${numberOfChunks} chunks with deflate should work`); + + promise_test(async t => { + const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'gzip'); + const expectedValue = makeExpectedChunk(hello, numberOfChunks); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflate(compressedData), 'value should match'); + }, `compressing ${numberOfChunks} chunks with gzip should work`); + + promise_test(async t => { + const compressedData = await compressMultipleChunks(hello, numberOfChunks, 'deflate-raw'); + const expectedValue = makeExpectedChunk(hello, numberOfChunks); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(expectedValue, pako.inflateRaw(compressedData), 'value should match'); + }, `compressing ${numberOfChunks} chunks with deflate-raw should work`); +} diff --git a/test/fixtures/wpt/compression/compression-output-length.tentative.any.js b/test/fixtures/wpt/compression/compression-output-length.tentative.any.js new file mode 100644 index 00000000000000..7aa13734500d26 --- /dev/null +++ b/test/fixtures/wpt/compression/compression-output-length.tentative.any.js @@ -0,0 +1,64 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +// This test asserts that compressed data length is shorter than the original +// data length. If the input is extremely small, the compressed data may be +// larger than the original data. + +const LARGE_FILE = '/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm'; + +async function compressArrayBuffer(input, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(input); + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +promise_test(async () => { + const response = await fetch(LARGE_FILE); + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const originalLength = bufferView.length; + const compressedData = await compressArrayBuffer(bufferView, 'deflate'); + const compressedLength = compressedData.length; + assert_less_than(compressedLength, originalLength, 'output should be smaller'); +}, 'the length of deflated data should be shorter than that of the original data'); + +promise_test(async () => { + const response = await fetch(LARGE_FILE); + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const originalLength = bufferView.length; + const compressedData = await compressArrayBuffer(bufferView, 'gzip'); + const compressedLength = compressedData.length; + assert_less_than(compressedLength, originalLength, 'output should be smaller'); +}, 'the length of gzipped data should be shorter than that of the original data'); + +promise_test(async () => { + const response = await fetch(LARGE_FILE); + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const originalLength = bufferView.length; + const compressedData = await compressArrayBuffer(bufferView, 'deflate-raw'); + const compressedLength = compressedData.length; + assert_less_than(compressedLength, originalLength, 'output should be smaller'); +}, 'the length of deflated (with -raw) data should be shorter than that of the original data'); diff --git a/test/fixtures/wpt/compression/compression-stream.tentative.any.js b/test/fixtures/wpt/compression/compression-stream.tentative.any.js new file mode 100644 index 00000000000000..a7ea0cb908402f --- /dev/null +++ b/test/fixtures/wpt/compression/compression-stream.tentative.any.js @@ -0,0 +1,91 @@ +// META: global=window,worker,shadowrealm +// META: script=third_party/pako/pako_inflate.min.js +// META: timeout=long + +'use strict'; + +const SMALL_FILE = "/media/foo.vtt"; +const LARGE_FILE = "/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm"; + +async function compressArrayBuffer(input, format) { + const cs = new CompressionStream(format); + const writer = cs.writable.getWriter(); + writer.write(input); + const closePromise = writer.close(); + const out = []; + const reader = cs.readable.getReader(); + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) + break; + out.push(value); + totalSize += value.byteLength; + } + await closePromise; + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +test(() => { + assert_throws_js(TypeError, () => { + const transformer = new CompressionStream("nonvalid"); + }, "non supported format should throw"); +}, "CompressionStream constructor should throw on invalid format"); + +promise_test(async () => { + const buffer = new ArrayBuffer(0); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "deflate"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "deflated empty data should be reinflated back to its origin"); + +promise_test(async () => { + const response = await fetch(SMALL_FILE) + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "deflate"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "deflated small amount data should be reinflated back to its origin"); + +promise_test(async () => { + const response = await fetch(LARGE_FILE) + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "deflate"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "deflated large amount data should be reinflated back to its origin"); + +promise_test(async () => { + const buffer = new ArrayBuffer(0); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "gzip"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "gzipped empty data should be reinflated back to its origin"); + +promise_test(async () => { + const response = await fetch(SMALL_FILE) + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "gzip"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "gzipped small amount data should be reinflated back to its origin"); + +promise_test(async () => { + const response = await fetch(LARGE_FILE) + const buffer = await response.arrayBuffer(); + const bufferView = new Uint8Array(buffer); + const compressedData = await compressArrayBuffer(bufferView, "gzip"); + // decompress with pako, and check that we got the same result as our original string + assert_array_equals(bufferView, pako.inflate(compressedData)); +}, "gzipped large amount data should be reinflated back to its origin"); diff --git a/test/fixtures/wpt/compression/compression-with-detach.tentative.window.js b/test/fixtures/wpt/compression/compression-with-detach.tentative.window.js new file mode 100644 index 00000000000000..465feaa47d4e9a --- /dev/null +++ b/test/fixtures/wpt/compression/compression-with-detach.tentative.window.js @@ -0,0 +1,55 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/concatenate-stream.js + +'use strict'; + +const kInputLength = 500000; + +function createLargeRandomInput() { + const buffer = new ArrayBuffer(kInputLength); + // The getRandomValues API will only let us get 65536 bytes at a time, so call + // it multiple times. + const kChunkSize = 65536; + for (let offset = 0; offset < kInputLength; offset += kChunkSize) { + const length = + offset + kChunkSize > kInputLength ? kInputLength - offset : kChunkSize; + const view = new Uint8Array(buffer, offset, length); + crypto.getRandomValues(view); + } + return new Uint8Array(buffer); +} + +function decompress(view) { + const ds = new DecompressionStream('deflate'); + const writer = ds.writable.getWriter(); + writer.write(view); + writer.close(); + return concatenateStream(ds.readable); +} + +promise_test(async () => { + const input = createLargeRandomInput(); + const inputCopy = input.slice(0, input.byteLength); + const cs = new CompressionStream('deflate'); + const writer = cs.writable.getWriter(); + writer.write(input); + writer.close(); + // Object.prototype.then will be looked up synchronously when the promise + // returned by read() is resolved. + Object.defineProperty(Object.prototype, 'then', { + get() { + // Cause input to become detached and unreferenced. + try { + postMessage(undefined, 'nowhere', [input.buffer]); + } catch (e) { + // It's already detached. + } + } + }); + const output = await concatenateStream(cs.readable); + // Perform the comparison as strings since this is reasonably fast even when + // JITted JavaScript is running under an emulator. + assert_equals( + inputCopy.toString(), (await decompress(output)).toString(), + 'decompressing the output should return the input'); +}, 'data should be correctly compressed even if input is detached partway'); diff --git a/test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js b/test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js new file mode 100644 index 00000000000000..f450b0c4cb2553 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-bad-chunks.tentative.any.js @@ -0,0 +1,85 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const badChunks = [ + { + name: 'undefined', + value: undefined + }, + { + name: 'null', + value: null + }, + { + name: 'numeric', + value: 3.14 + }, + { + name: 'object, not BufferSource', + value: {} + }, + { + name: 'array', + value: [65] + }, + { + name: 'SharedArrayBuffer', + // Use a getter to postpone construction so that all tests don't fail where + // SharedArrayBuffer is not yet implemented. + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer; + } + }, + { + name: 'shared Uint8Array', + get value() { + // See https://github.com/whatwg/html/issues/5380 for why not `new SharedArrayBuffer()` + return new Uint8Array(new WebAssembly.Memory({ shared:true, initial:1, maximum:1 }).buffer) + } + }, + { + name: 'invalid deflate bytes', + value: new Uint8Array([0, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]) + }, + { + name: 'invalid gzip bytes', + value: new Uint8Array([0, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]) + }, +]; + +// Test Case Design +// We need to wait until after we close the writable stream to check if the decoded stream is valid. +// We can end up in a state where all reads/writes are valid, but upon closing the writable stream an error is detected. +// (Example: A zlib encoded chunk w/o the checksum). + +async function decompress(chunk, format, t) +{ + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + + writer.write(chunk.value).then(() => {}, () => {}); + reader.read().then(() => {}, () => {}); + + await promise_rejects_js(t, TypeError, writer.close(), 'writer.close() should reject'); + await promise_rejects_js(t, TypeError, writer.closed, 'write.closed should reject'); + + await promise_rejects_js(t, TypeError, reader.read(), 'reader.read() should reject'); + await promise_rejects_js(t, TypeError, reader.closed, 'read.closed should reject'); +} + +for (const chunk of badChunks) { + promise_test(async t => { + await decompress(chunk, 'gzip', t); + }, `chunk of type ${chunk.name} should error the stream for gzip`); + + promise_test(async t => { + await decompress(chunk, 'deflate', t); + }, `chunk of type ${chunk.name} should error the stream for deflate`); + + promise_test(async t => { + await decompress(chunk, 'deflate-raw', t); + }, `chunk of type ${chunk.name} should error the stream for deflate-raw`); +} diff --git a/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js b/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js new file mode 100644 index 00000000000000..e81fc566779800 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-buffersource.tentative.any.js @@ -0,0 +1,192 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const compressedBytesWithDeflate = [120, 156, 75, 52, 48, 52, 50, 54, 49, 53, 3, 0, 8, 136, 1, 199]; +const compressedBytesWithGzip = [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 52, 48, 52, 2, 0, 216, 252, 63, 136, 4, 0, 0, 0]; +const compressedBytesWithDeflateRaw = [ + 0x00, 0x06, 0x00, 0xf9, 0xff, 0x41, 0x42, 0x43, + 0x44, 0x45, 0x46, 0x01, 0x00, 0x00, 0xff, 0xff, +]; +// These chunk values below were chosen to make the length of the compressed +// output be a multiple of 8 bytes. +const deflateExpectedChunkValue = new TextEncoder().encode('a0123456'); +const gzipExpectedChunkValue = new TextEncoder().encode('a012'); +const deflateRawExpectedChunkValue = new TextEncoder().encode('ABCDEF'); + +const bufferSourceChunksForDeflate = [ + { + name: 'ArrayBuffer', + value: new Uint8Array(compressedBytesWithDeflate).buffer + }, + { + name: 'Int8Array', + value: new Int8Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Uint8Array', + value: new Uint8Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Uint8ClampedArray', + value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Int16Array', + value: new Int16Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Uint16Array', + value: new Uint16Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Int32Array', + value: new Int32Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Uint32Array', + value: new Uint32Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Float32Array', + value: new Float32Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'Float64Array', + value: new Float64Array(new Uint8Array(compressedBytesWithDeflate).buffer) + }, + { + name: 'DataView', + value: new DataView(new Uint8Array(compressedBytesWithDeflate).buffer) + }, +]; + +const bufferSourceChunksForGzip = [ + { + name: 'ArrayBuffer', + value: new Uint8Array(compressedBytesWithGzip).buffer + }, + { + name: 'Int8Array', + value: new Int8Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Uint8Array', + value: new Uint8Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Uint8ClambedArray', + value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Int16Array', + value: new Int16Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Uint16Array', + value: new Uint16Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Int32Array', + value: new Int32Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Uint32Array', + value: new Uint32Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Float32Array', + value: new Float32Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'Float64Array', + value: new Float64Array(new Uint8Array(compressedBytesWithGzip).buffer) + }, + { + name: 'DataView', + value: new DataView(new Uint8Array(compressedBytesWithGzip).buffer) + }, +]; + +const bufferSourceChunksForDeflateRaw = [ + { + name: 'ArrayBuffer', + value: new Uint8Array(compressedBytesWithDeflateRaw).buffer + }, + { + name: 'Int8Array', + value: new Int8Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Uint8Array', + value: new Uint8Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Uint8ClampedArray', + value: new Uint8ClampedArray(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Int16Array', + value: new Int16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Uint16Array', + value: new Uint16Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Int32Array', + value: new Int32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Uint32Array', + value: new Uint32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Float32Array', + value: new Float32Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'Float64Array', + value: new Float64Array(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, + { + name: 'DataView', + value: new DataView(new Uint8Array(compressedBytesWithDeflateRaw).buffer) + }, +]; + +for (const chunk of bufferSourceChunksForDeflate) { + promise_test(async t => { + const ds = new DecompressionStream('deflate'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(chunk.value); + writer.close(); + const { value } = await reader.read(); + assert_array_equals(Array.from(value), deflateExpectedChunkValue, 'value should match'); + }, `chunk of type ${chunk.name} should work for deflate`); +} + +for (const chunk of bufferSourceChunksForGzip) { + promise_test(async t => { + const ds = new DecompressionStream('gzip'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(chunk.value); + writer.close(); + const { value } = await reader.read(); + assert_array_equals(Array.from(value), gzipExpectedChunkValue, 'value should match'); + }, `chunk of type ${chunk.name} should work for gzip`); +} + +for (const chunk of bufferSourceChunksForDeflateRaw) { + promise_test(async t => { + const ds = new DecompressionStream('deflate-raw'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(chunk.value); + writer.close(); + const { value } = await reader.read(); + assert_array_equals(Array.from(value), deflateRawExpectedChunkValue, 'value should match'); + }, `chunk of type ${chunk.name} should work for deflate-raw`); +} diff --git a/test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js b/test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js new file mode 100644 index 00000000000000..0270ba7353128c --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-constructor-error.tentative.any.js @@ -0,0 +1,15 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +test(t => { + assert_throws_js(TypeError, () => new DecompressionStream('a'), 'constructor should throw'); +}, '"a" should cause the constructor to throw'); + +test(t => { + assert_throws_js(TypeError, () => new DecompressionStream(), 'constructor should throw'); +}, 'no input should cause the constructor to throw'); + +test(t => { + assert_throws_js(Error, () => new DecompressionStream({ toString() { throw Error(); } }), 'constructor should throw'); +}, 'non-string input should cause the constructor to throw'); diff --git a/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js new file mode 100644 index 00000000000000..90519445e3667b --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-correct-input.tentative.any.js @@ -0,0 +1,39 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); +const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); +const deflateRawChunkValue = new Uint8Array([ + 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, + 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, +]); +const trueChunkValue = new TextEncoder().encode('expected output'); + +promise_test(async t => { + const ds = new DecompressionStream('deflate'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(deflateChunkValue); + const { done, value } = await reader.read(); + assert_array_equals(Array.from(value), trueChunkValue, "value should match"); +}, 'decompressing deflated input should work'); + + +promise_test(async t => { + const ds = new DecompressionStream('gzip'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(gzipChunkValue); + const { done, value } = await reader.read(); + assert_array_equals(Array.from(value), trueChunkValue, "value should match"); +}, 'decompressing gzip input should work'); + +promise_test(async t => { + const ds = new DecompressionStream('deflate-raw'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(deflateRawChunkValue); + const { done, value } = await reader.read(); + assert_array_equals(Array.from(value), trueChunkValue, "value should match"); +}, 'decompressing deflated (with -raw) input should work'); diff --git a/test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js new file mode 100644 index 00000000000000..fc18197dfbd3db --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-corrupt-input.tentative.any.js @@ -0,0 +1,318 @@ +// META global=window,worker,shadowrealm + +// This test checks that DecompressionStream behaves according to the standard +// when the input is corrupted. To avoid a combinatorial explosion in the +// number of tests, we only mutate one field at a time, and we only test +// "interesting" values. + +'use strict'; + +// The many different cases are summarised in this data structure. +const expectations = [ + { + format: 'deflate', + + // Decompresses to 'expected output'. + baseInput: [120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, + 40, 45, 1, 0, 48, 173, 6, 36], + + // See RFC1950 for the definition of the various fields used by deflate: + // https://tools.ietf.org/html/rfc1950. + fields: [ + { + // The function of this field. This matches the name used in the RFC. + name: 'CMF', + + // The offset of the field in bytes from the start of the input. + offset: 0, + + // The length of the field in bytes. + length: 1, + + cases: [ + { + // The value to set the field to. If the field contains multiple + // bytes, all the bytes will be set to this value. + value: 0, + + // The expected result. 'success' means the input is decoded + // successfully. 'error' means that the stream will be errored. + result: 'error' + } + ] + }, + { + name: 'FLG', + offset: 1, + length: 1, + + // FLG contains a 4-bit checksum (FCHECK) which is calculated in such a + // way that there are 4 valid values for this field. + cases: [ + { + value: 218, + result: 'success' + }, + { + value: 1, + result: 'success' + }, + { + value: 94, + result: 'success' + }, + { + // The remaining 252 values cause an error. + value: 157, + result: 'error' + } + ] + }, + { + name: 'DATA', + // In general, changing any bit of the data will trigger a checksum + // error. Only the last byte does anything else. + offset: 18, + length: 1, + cases: [ + { + value: 4, + result: 'success' + }, + { + value: 5, + result: 'error' + } + ] + }, + { + name: 'ADLER', + offset: -4, + length: 4, + cases: [ + { + value: 255, + result: 'error' + } + ] + } + ] + }, + { + format: 'gzip', + + // Decompresses to 'expected output'. + baseInput: [31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, + 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, + 0, 0], + + // See RFC1952 for the definition of the various fields used by gzip: + // https://tools.ietf.org/html/rfc1952. + fields: [ + { + name: 'ID', + offset: 0, + length: 2, + cases: [ + { + value: 255, + result: 'error' + } + ] + }, + { + name: 'CM', + offset: 2, + length: 1, + cases: [ + { + value: 0, + result: 'error' + } + ] + }, + { + name: 'FLG', + offset: 3, + length: 1, + cases: [ + { + value: 1, // FTEXT + result: 'success' + }, + { + value: 2, // FHCRC + result: 'error' + } + ] + }, + { + name: 'MTIME', + offset: 4, + length: 4, + cases: [ + { + // Any value is valid for this field. + value: 255, + result: 'success' + } + ] + }, + { + name: 'XFL', + offset: 8, + length: 1, + cases: [ + { + // Any value is accepted. + value: 255, + result: 'success' + } + ] + }, + { + name: 'OS', + offset: 9, + length: 1, + cases: [ + { + // Any value is accepted. + value: 128, + result: 'success' + } + ] + }, + { + name: 'DATA', + + // The last byte of the data is the most interesting. + offset: 26, + length: 1, + cases: [ + { + value: 3, + result: 'error' + }, + { + value: 4, + result: 'success' + } + ] + }, + { + name: 'CRC', + offset: -8, + length: 4, + cases: [ + { + // Any change will error the stream. + value: 0, + result: 'error' + } + ] + }, + { + name: 'ISIZE', + offset: -4, + length: 4, + cases: [ + { + // A mismatch will error the stream. + value: 1, + result: 'error' + } + ] + } + ] + } +]; + +async function tryDecompress(input, format) { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + writer.write(input).catch(() => {}); + writer.close().catch(() => {}); + let out = []; + while (true) { + try { + const { value, done } = await reader.read(); + if (done) { + break; + } + out = out.concat(Array.from(value)); + } catch (e) { + if (e instanceof TypeError) { + return { result: 'error' }; + } else { + return { result: e.name }; + } + } + } + const expectedOutput = 'expected output'; + if (out.length !== expectedOutput.length) { + return { result: 'corrupt' }; + } + for (let i = 0; i < out.length; ++i) { + if (out[i] !== expectedOutput.charCodeAt(i)) { + return { result: 'corrupt' }; + } + } + return { result: 'success' }; +} + +function corruptInput(input, offset, length, value) { + const output = new Uint8Array(input); + if (offset < 0) { + offset += input.length; + } + for (let i = offset; i < offset + length; ++i) { + output[i] = value; + } + return output; +} + +for (const { format, baseInput, fields } of expectations) { + promise_test(async () => { + const { result } = await tryDecompress(new Uint8Array(baseInput), format); + assert_equals(result, 'success', 'decompression should succeed'); + }, `the unchanged input for '${format}' should decompress successfully`); + + promise_test(async () => { + const truncatedInput = new Uint8Array(baseInput.slice(0, -1)); + const { result } = await tryDecompress(truncatedInput, format); + assert_equals(result, 'error', 'decompression should fail'); + }, `truncating the input for '${format}' should give an error`); + + promise_test(async () => { + const extendedInput = new Uint8Array(baseInput.concat([0])); + const { result } = await tryDecompress(extendedInput, format); + assert_equals(result, 'error', 'decompression should fail'); + }, `trailing junk for '${format}' should give an error`); + + for (const { name, offset, length, cases } of fields) { + for (const { value, result } of cases) { + promise_test(async () => { + const corruptedInput = corruptInput(baseInput, offset, length, value); + const { result: actual } = + await tryDecompress(corruptedInput, format); + assert_equals(actual, result, 'result should match'); + }, `format '${format}' field ${name} should be ${result} for ${value}`); + } + } +} + +promise_test(async () => { + // Data generated in Python: + // ```py + // h = b"thequickbrownfoxjumped\x00" + // words = h.split() + // zdict = b''.join(words) + // co = zlib.compressobj(zdict=zdict) + // cd = co.compress(h) + co.flush() + // ``` + const { result } = await tryDecompress(new Uint8Array([ + 0x78, 0xbb, 0x74, 0xee, 0x09, 0x59, 0x2b, 0xc1, 0x2e, 0x0c, 0x00, 0x74, 0xee, 0x09, 0x59 + ]), 'deflate'); + assert_equals(result, 'error', 'Data compressed with a dictionary should throw TypeError'); +}, 'the deflate input compressed with dictionary should give an error') diff --git a/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js b/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js new file mode 100644 index 00000000000000..201db8ec0b0d7c --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-empty-input.tentative.any.js @@ -0,0 +1,43 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const gzipEmptyValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]); +const deflateEmptyValue = new Uint8Array([120, 156, 3, 0, 0, 0, 0, 1]); +const deflateRawEmptyValue = new Uint8Array([1, 0, 0, 255, 255]); + +promise_test(async t => { + const ds = new DecompressionStream('gzip'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(gzipEmptyValue); + writer.close(); + const { value, done } = await reader.read(); + assert_true(done, "read() should set done"); + assert_equals(value, undefined, "value should be undefined"); + await writePromise; +}, 'decompressing gzip empty input should work'); + +promise_test(async t => { + const ds = new DecompressionStream('deflate'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(deflateEmptyValue); + writer.close(); + const { value, done } = await reader.read(); + assert_true(done, "read() should set done"); + assert_equals(value, undefined, "value should be undefined"); + await writePromise; +}, 'decompressing deflate empty input should work'); + +promise_test(async t => { + const ds = new DecompressionStream('deflate-raw'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(deflateRawEmptyValue); + writer.close(); + const { value, done } = await reader.read(); + assert_true(done, "read() should set done"); + assert_equals(value, undefined, "value should be undefined"); + await writePromise; +}, 'decompressing deflate-raw empty input should work'); diff --git a/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js b/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js new file mode 100644 index 00000000000000..eb12c2a2360cd9 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-split-chunk.tentative.any.js @@ -0,0 +1,53 @@ +// META: global=window,worker,shadowrealm + +'use strict'; + +const compressedBytesWithDeflate = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); +const compressedBytesWithGzip = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); +const compressedBytesWithDeflateRaw = new Uint8Array([ + 0x4b, 0xad, 0x28, 0x48, 0x4d, 0x2e, 0x49, 0x4d, 0x51, 0xc8, + 0x2f, 0x2d, 0x29, 0x28, 0x2d, 0x01, 0x00, +]); +const expectedChunkValue = new TextEncoder().encode('expected output'); + +async function decompressArrayBuffer(input, format, chunkSize) { + const ds = new DecompressionStream(format); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + for (let beginning = 0; beginning < input.length; beginning += chunkSize) { + writer.write(input.slice(beginning, beginning + chunkSize)); + } + writer.close(); + const out = []; + let totalSize = 0; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + out.push(value); + totalSize += value.byteLength; + } + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const array of out) { + concatenated.set(array, offset); + offset += array.byteLength; + } + return concatenated; +} + +for (let chunkSize = 1; chunkSize < 16; ++chunkSize) { + promise_test(async t => { + const decompressedData = await decompressArrayBuffer(compressedBytesWithDeflate, 'deflate', chunkSize); + assert_array_equals(decompressedData, expectedChunkValue, "value should match"); + }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in deflate`); + + promise_test(async t => { + const decompressedData = await decompressArrayBuffer(compressedBytesWithGzip, 'gzip', chunkSize); + assert_array_equals(decompressedData, expectedChunkValue, "value should match"); + }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in gzip`); + + promise_test(async t => { + const decompressedData = await decompressArrayBuffer(compressedBytesWithDeflateRaw, 'deflate-raw', chunkSize); + assert_array_equals(decompressedData, expectedChunkValue, "value should match"); + }, `decompressing splitted chunk into pieces of size ${chunkSize} should work in deflate-raw`); +} diff --git a/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js b/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js new file mode 100644 index 00000000000000..0c45a0aaa727f1 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-uint8array-output.tentative.any.js @@ -0,0 +1,30 @@ +// META: global=window,worker,shadowrealm +// META: timeout=long +// +// This test isn't actually slow usually, but sometimes it takes >10 seconds on +// Firefox with service worker for no obvious reason. + +'use strict'; + +const deflateChunkValue = new Uint8Array([120, 156, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 48, 173, 6, 36]); +const gzipChunkValue = new Uint8Array([31, 139, 8, 0, 0, 0, 0, 0, 0, 3, 75, 173, 40, 72, 77, 46, 73, 77, 81, 200, 47, 45, 41, 40, 45, 1, 0, 176, 1, 57, 179, 15, 0, 0, 0]); + +promise_test(async t => { + const ds = new DecompressionStream('deflate'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(deflateChunkValue); + const { value } = await reader.read(); + assert_equals(value.constructor, Uint8Array, "type should match"); + await writePromise; +}, 'decompressing deflated output should give Uint8Array chunks'); + +promise_test(async t => { + const ds = new DecompressionStream('gzip'); + const reader = ds.readable.getReader(); + const writer = ds.writable.getWriter(); + const writePromise = writer.write(gzipChunkValue); + const { value } = await reader.read(); + assert_equals(value.constructor, Uint8Array, "type should match"); + await writePromise; +}, 'decompressing gzip output should give Uint8Array chunks'); diff --git a/test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js b/test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js new file mode 100644 index 00000000000000..1ff9c269837022 --- /dev/null +++ b/test/fixtures/wpt/compression/decompression-with-detach.tentative.window.js @@ -0,0 +1,41 @@ +// META: global=window,worker,shadowrealm +// META: script=resources/concatenate-stream.js + +'use strict'; + +const kInputLength = 1000000; + +async function createLargeCompressedInput() { + const cs = new CompressionStream('deflate'); + // The input has to be large enough that it won't fit in a single chunk when + // decompressed. + const writer = cs.writable.getWriter(); + writer.write(new Uint8Array(kInputLength)); + writer.close(); + return concatenateStream(cs.readable); +} + +promise_test(async () => { + const input = await createLargeCompressedInput(); + const ds = new DecompressionStream('deflate'); + const writer = ds.writable.getWriter(); + writer.write(input); + writer.close(); + // Object.prototype.then will be looked up synchronously when the promise + // returned by read() is resolved. + Object.defineProperty(Object.prototype, 'then', { + get() { + // Cause input to become detached and unreferenced. + try { + postMessage(undefined, 'nowhere', [input.buffer]); + } catch (e) { + // It's already detached. + } + } + }); + const output = await concatenateStream(ds.readable); + // If output successfully decompressed and gave the right length, we can be + // reasonably confident that no data corruption happened. + assert_equals( + output.byteLength, kInputLength, 'output should be the right length'); +}, 'data should be correctly decompressed even if input is detached partway'); diff --git a/test/fixtures/wpt/compression/idlharness-shadowrealm.window.js b/test/fixtures/wpt/compression/idlharness-shadowrealm.window.js new file mode 100644 index 00000000000000..2fdc807ee07e32 --- /dev/null +++ b/test/fixtures/wpt/compression/idlharness-shadowrealm.window.js @@ -0,0 +1,2 @@ +// META: script=/resources/idlharness-shadowrealm.js +idl_test_shadowrealm(["compression"], ["streams"]); diff --git a/test/fixtures/wpt/compression/idlharness.https.any.js b/test/fixtures/wpt/compression/idlharness.https.any.js new file mode 100644 index 00000000000000..8d96cf523c4953 --- /dev/null +++ b/test/fixtures/wpt/compression/idlharness.https.any.js @@ -0,0 +1,17 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +'use strict'; + +// https://wicg.github.io/compression/ + +idl_test( + ['compression'], + ['streams'], + idl_array => { + idl_array.add_objects({ + CompressionStream: ['new CompressionStream("deflate")'], + DecompressionStream: ['new DecompressionStream("deflate")'], + }); + } +); diff --git a/test/fixtures/wpt/compression/resources/concatenate-stream.js b/test/fixtures/wpt/compression/resources/concatenate-stream.js new file mode 100644 index 00000000000000..a35bb1416e7548 --- /dev/null +++ b/test/fixtures/wpt/compression/resources/concatenate-stream.js @@ -0,0 +1,25 @@ +'use strict'; + +// Read all the chunks from a stream that returns BufferSource objects and +// concatenate them into a single Uint8Array. +async function concatenateStream(readableStream) { + const reader = readableStream.getReader(); + let totalSize = 0; + const buffers = []; + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + buffers.push(value); + totalSize += value.byteLength; + } + reader.releaseLock(); + const concatenated = new Uint8Array(totalSize); + let offset = 0; + for (const buffer of buffers) { + concatenated.set(buffer, offset); + offset += buffer.byteLength; + } + return concatenated; +} diff --git a/test/fixtures/wpt/compression/third_party/pako/LICENSE b/test/fixtures/wpt/compression/third_party/pako/LICENSE new file mode 100644 index 00000000000000..a934ef8db47845 --- /dev/null +++ b/test/fixtures/wpt/compression/third_party/pako/LICENSE @@ -0,0 +1,21 @@ +(The MIT License) + +Copyright (C) 2014-2017 by Vitaly Puzrin and Andrei Tuputcyn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/test/fixtures/wpt/compression/third_party/pako/README b/test/fixtures/wpt/compression/third_party/pako/README new file mode 100644 index 00000000000000..96028388ebb9d5 --- /dev/null +++ b/test/fixtures/wpt/compression/third_party/pako/README @@ -0,0 +1,2 @@ +original repository: +https://github.com/nodeca/pako diff --git a/test/fixtures/wpt/compression/third_party/pako/pako_inflate.min.js b/test/fixtures/wpt/compression/third_party/pako/pako_inflate.min.js new file mode 100644 index 00000000000000..a191a78a8956cd --- /dev/null +++ b/test/fixtures/wpt/compression/third_party/pako/pako_inflate.min.js @@ -0,0 +1 @@ +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).pako=e()}}(function(){return function r(o,s,f){function l(t,e){if(!s[t]){if(!o[t]){var i="function"==typeof require&&require;if(!e&&i)return i(t,!0);if(d)return d(t,!0);var n=new Error("Cannot find module '"+t+"'");throw n.code="MODULE_NOT_FOUND",n}var a=s[t]={exports:{}};o[t][0].call(a.exports,function(e){return l(o[t][1][e]||e)},a,a.exports,r,o,s,f)}return s[t].exports}for(var d="function"==typeof require&&require,e=0;e>>6:(i<65536?t[r++]=224|i>>>12:(t[r++]=240|i>>>18,t[r++]=128|i>>>12&63),t[r++]=128|i>>>6&63),t[r++]=128|63&i);return t},i.buf2binstring=function(e){return d(e,e.length)},i.binstring2buf=function(e){for(var t=new f.Buf8(e.length),i=0,n=t.length;i>10&1023,s[n++]=56320|1023&a)}return d(s,n)},i.utf8border=function(e,t){var i;for((t=t||e.length)>e.length&&(t=e.length),i=t-1;0<=i&&128==(192&e[i]);)i--;return i<0?t:0===i?t:i+l[e[i]]>t?i:t}},{"./common":1}],3:[function(e,t,i){"use strict";t.exports=function(e,t,i,n){for(var a=65535&e|0,r=e>>>16&65535|0,o=0;0!==i;){for(i-=o=2e3>>1:e>>>1;t[i]=e}return t}();t.exports=function(e,t,i,n){var a=s,r=n+i;e^=-1;for(var o=n;o>>8^a[255&(e^t[o])];return-1^e}},{}],6:[function(e,t,i){"use strict";t.exports=function(){this.text=0,this.time=0,this.xflags=0,this.os=0,this.extra=null,this.extra_len=0,this.name="",this.comment="",this.hcrc=0,this.done=!1}},{}],7:[function(e,t,i){"use strict";t.exports=function(e,t){var i,n,a,r,o,s,f,l,d,c,u,h,b,m,w,k,_,g,v,p,x,y,S,E,Z;i=e.state,n=e.next_in,E=e.input,a=n+(e.avail_in-5),r=e.next_out,Z=e.output,o=r-(t-e.avail_out),s=r+(e.avail_out-257),f=i.dmax,l=i.wsize,d=i.whave,c=i.wnext,u=i.window,h=i.hold,b=i.bits,m=i.lencode,w=i.distcode,k=(1<>>=v=g>>>24,b-=v,0===(v=g>>>16&255))Z[r++]=65535&g;else{if(!(16&v)){if(0==(64&v)){g=m[(65535&g)+(h&(1<>>=v,b-=v),b<15&&(h+=E[n++]<>>=v=g>>>24,b-=v,!(16&(v=g>>>16&255))){if(0==(64&v)){g=w[(65535&g)+(h&(1<>>=v,b-=v,(v=r-o)>3,h&=(1<<(b-=p<<3))-1,e.next_in=n,e.next_out=r,e.avail_in=n>>24&255)+(e>>>8&65280)+((65280&e)<<8)+((255&e)<<24)}function r(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new z.Buf16(320),this.work=new z.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function o(e){var t;return e&&e.state?(t=e.state,e.total_in=e.total_out=t.total=0,e.msg="",t.wrap&&(e.adler=1&t.wrap),t.mode=F,t.last=0,t.havedict=0,t.dmax=32768,t.head=null,t.hold=0,t.bits=0,t.lencode=t.lendyn=new z.Buf32(n),t.distcode=t.distdyn=new z.Buf32(a),t.sane=1,t.back=-1,T):U}function s(e){var t;return e&&e.state?((t=e.state).wsize=0,t.whave=0,t.wnext=0,o(e)):U}function f(e,t){var i,n;return e&&e.state?(n=e.state,t<0?(i=0,t=-t):(i=1+(t>>4),t<48&&(t&=15)),t&&(t<8||15=r.wsize?(z.arraySet(r.window,t,i-r.wsize,r.wsize,0),r.wnext=0,r.whave=r.wsize):(n<(a=r.wsize-r.wnext)&&(a=n),z.arraySet(r.window,t,i-n,a,r.wnext),(n-=a)?(z.arraySet(r.window,t,i-n,n,0),r.wnext=n,r.whave=r.wsize):(r.wnext+=a,r.wnext===r.wsize&&(r.wnext=0),r.whave>>8&255,i.check=N(i.check,B,2,0),d=l=0,i.mode=2;break}if(i.flags=0,i.head&&(i.head.done=!1),!(1&i.wrap)||(((255&l)<<8)+(l>>8))%31){e.msg="incorrect header check",i.mode=30;break}if(8!=(15&l)){e.msg="unknown compression method",i.mode=30;break}if(d-=4,x=8+(15&(l>>>=4)),0===i.wbits)i.wbits=x;else if(x>i.wbits){e.msg="invalid window size",i.mode=30;break}i.dmax=1<>8&1),512&i.flags&&(B[0]=255&l,B[1]=l>>>8&255,i.check=N(i.check,B,2,0)),d=l=0,i.mode=3;case 3:for(;d<32;){if(0===s)break e;s--,l+=n[r++]<>>8&255,B[2]=l>>>16&255,B[3]=l>>>24&255,i.check=N(i.check,B,4,0)),d=l=0,i.mode=4;case 4:for(;d<16;){if(0===s)break e;s--,l+=n[r++]<>8),512&i.flags&&(B[0]=255&l,B[1]=l>>>8&255,i.check=N(i.check,B,2,0)),d=l=0,i.mode=5;case 5:if(1024&i.flags){for(;d<16;){if(0===s)break e;s--,l+=n[r++]<>>8&255,i.check=N(i.check,B,2,0)),d=l=0}else i.head&&(i.head.extra=null);i.mode=6;case 6:if(1024&i.flags&&(s<(h=i.length)&&(h=s),h&&(i.head&&(x=i.head.extra_len-i.length,i.head.extra||(i.head.extra=new Array(i.head.extra_len)),z.arraySet(i.head.extra,n,r,h,x)),512&i.flags&&(i.check=N(i.check,n,h,r)),s-=h,r+=h,i.length-=h),i.length))break e;i.length=0,i.mode=7;case 7:if(2048&i.flags){if(0===s)break e;for(h=0;x=n[r+h++],i.head&&x&&i.length<65536&&(i.head.name+=String.fromCharCode(x)),x&&h>9&1,i.head.done=!0),e.adler=i.check=0,i.mode=12;break;case 10:for(;d<32;){if(0===s)break e;s--,l+=n[r++]<>>=7&d,d-=7&d,i.mode=27;break}for(;d<3;){if(0===s)break e;s--,l+=n[r++]<>>=1)){case 0:i.mode=14;break;case 1:if(H(i),i.mode=20,6!==t)break;l>>>=2,d-=2;break e;case 2:i.mode=17;break;case 3:e.msg="invalid block type",i.mode=30}l>>>=2,d-=2;break;case 14:for(l>>>=7&d,d-=7&d;d<32;){if(0===s)break e;s--,l+=n[r++]<>>16^65535)){e.msg="invalid stored block lengths",i.mode=30;break}if(i.length=65535&l,d=l=0,i.mode=15,6===t)break e;case 15:i.mode=16;case 16:if(h=i.length){if(s>>=5,d-=5,i.ndist=1+(31&l),l>>>=5,d-=5,i.ncode=4+(15&l),l>>>=4,d-=4,286>>=3,d-=3}for(;i.have<19;)i.lens[A[i.have++]]=0;if(i.lencode=i.lendyn,i.lenbits=7,S={bits:i.lenbits},y=C(0,i.lens,0,19,i.lencode,0,i.work,S),i.lenbits=S.bits,y){e.msg="invalid code lengths set",i.mode=30;break}i.have=0,i.mode=19;case 19:for(;i.have>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=w,d-=w,i.lens[i.have++]=_;else{if(16===_){for(E=w+2;d>>=w,d-=w,0===i.have){e.msg="invalid bit length repeat",i.mode=30;break}x=i.lens[i.have-1],h=3+(3&l),l>>>=2,d-=2}else if(17===_){for(E=w+3;d>>=w)),l>>>=3,d-=3}else{for(E=w+7;d>>=w)),l>>>=7,d-=7}if(i.have+h>i.nlen+i.ndist){e.msg="invalid bit length repeat",i.mode=30;break}for(;h--;)i.lens[i.have++]=x}}if(30===i.mode)break;if(0===i.lens[256]){e.msg="invalid code -- missing end-of-block",i.mode=30;break}if(i.lenbits=9,S={bits:i.lenbits},y=C(I,i.lens,0,i.nlen,i.lencode,0,i.work,S),i.lenbits=S.bits,y){e.msg="invalid literal/lengths set",i.mode=30;break}if(i.distbits=6,i.distcode=i.distdyn,S={bits:i.distbits},y=C(D,i.lens,i.nlen,i.ndist,i.distcode,0,i.work,S),i.distbits=S.bits,y){e.msg="invalid distances set",i.mode=30;break}if(i.mode=20,6===t)break e;case 20:i.mode=21;case 21:if(6<=s&&258<=f){e.next_out=o,e.avail_out=f,e.next_in=r,e.avail_in=s,i.hold=l,i.bits=d,O(e,u),o=e.next_out,a=e.output,f=e.avail_out,r=e.next_in,n=e.input,s=e.avail_in,l=i.hold,d=i.bits,12===i.mode&&(i.back=-1);break}for(i.back=0;k=(Z=i.lencode[l&(1<>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>g)])>>>16&255,_=65535&Z,!(g+(w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=g,d-=g,i.back+=g}if(l>>>=w,d-=w,i.back+=w,i.length=_,0===k){i.mode=26;break}if(32&k){i.back=-1,i.mode=12;break}if(64&k){e.msg="invalid literal/length code",i.mode=30;break}i.extra=15&k,i.mode=22;case 22:if(i.extra){for(E=i.extra;d>>=i.extra,d-=i.extra,i.back+=i.extra}i.was=i.length,i.mode=23;case 23:for(;k=(Z=i.distcode[l&(1<>>16&255,_=65535&Z,!((w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>g)])>>>16&255,_=65535&Z,!(g+(w=Z>>>24)<=d);){if(0===s)break e;s--,l+=n[r++]<>>=g,d-=g,i.back+=g}if(l>>>=w,d-=w,i.back+=w,64&k){e.msg="invalid distance code",i.mode=30;break}i.offset=_,i.extra=15&k,i.mode=24;case 24:if(i.extra){for(E=i.extra;d>>=i.extra,d-=i.extra,i.back+=i.extra}if(i.offset>i.dmax){e.msg="invalid distance too far back",i.mode=30;break}i.mode=25;case 25:if(0===f)break e;if(h=u-f,i.offset>h){if((h=i.offset-h)>i.whave&&i.sane){e.msg="invalid distance too far back",i.mode=30;break}h>i.wnext?(h-=i.wnext,b=i.wsize-h):b=i.wnext-h,h>i.length&&(h=i.length),m=i.window}else m=a,b=o-i.offset,h=i.length;for(fh?(m=O[C+o[g]],w=A[z+o[g]]):(m=96,w=0),f=1<<_-S,v=l=1<>S)+(l-=f)]=b<<24|m<<16|w|0,0!==l;);for(f=1<<_-1;B&f;)f>>=1;if(0!==f?(B&=f-1,B+=f):B=0,g++,0==--R[_]){if(_===p)break;_=t[i+o[g]]}if(x<_&&(B&c)!==d){for(0===S&&(S=x),u+=v,E=1<<(y=_-S);y+S 00:00:05.000 +Foo diff --git a/test/fixtures/wpt/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm b/test/fixtures/wpt/media/test-av-384k-44100Hz-1ch-320x240-30fps-10kfr.webm new file mode 100644 index 0000000000000000000000000000000000000000..8b705dbc8980708aa847f7c1868300979faa12f8 GIT binary patch literal 76501 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�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

KtQ30wQD4}}8P_y`E&y}w3U0aS` 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 4b28c06e6c40df..64de3fbbbeff27 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -3,6 +3,10 @@ "commit": "dbd648158d337580885e70a54f929daf215211a0", "path": "common" }, + "compression": { + "commit": "c82521cfa587505746a853a24d22589633825b10", + "path": "compression" + }, "console": { "commit": "767ae354642bee1e4d90b28df4480475b9260e14", "path": "console" diff --git a/test/wpt/README.md b/test/wpt/README.md index a378e3244a1f0b..0f73062e289a47 100644 --- a/test/wpt/README.md +++ b/test/wpt/README.md @@ -153,7 +153,7 @@ expected failures. { "something.scope.js": { // the file name // Optional: If the requirement is not met, this test will be skipped - "requires": ["small-icu"], // supports: "small-icu", "full-icu" + "requires": ["small-icu"], // supports: "small-icu", "full-icu", "crypto" // Optional: the test will be skipped with the reason printed "skip": "explain why we cannot run a test that's supposed to pass", diff --git a/test/wpt/status/compression.json b/test/wpt/status/compression.json new file mode 100644 index 00000000000000..8c8535c15815e4 --- /dev/null +++ b/test/wpt/status/compression.json @@ -0,0 +1,58 @@ +{ + "compression-bad-chunks.tentative.any.js": { + "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" + }, + "compression-constructor-error.tentative.any.js": { + "fail": { + "expected": [ + "non-string input should cause the constructor to throw" + ] + } + }, + "decompression-bad-chunks.tentative.any.js": { + "skip": "Execution \"hangs\", ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" + }, + "decompression-buffersource.tentative.any.js": { + "skip": "ArrayBuffer and TypedArray is not accepted and throws, instead of rejects during writer.write" + }, + "decompression-constructor-error.tentative.any.js": { + "fail": { + "expected": [ + "non-string input should cause the constructor to throw" + ] + } + }, + "compression-with-detach.tentative.window.js": { + "requires": ["crypto"] + }, + "decompression-corrupt-input.tentative.any.js": { + "fail": { + "expected": [ + "truncating the input for 'deflate' should give an error", + "trailing junk for 'deflate' should give an error", + "format 'deflate' field CMF should be error for 0", + "format 'deflate' field FLG should be error for 157", + "format 'deflate' field DATA should be error for 5", + "format 'deflate' field ADLER should be error for 255", + "truncating the input for 'gzip' should give an error", + "trailing junk for 'gzip' should give an error", + "format 'gzip' field ID should be error for 255", + "format 'gzip' field CM should be error for 0", + "format 'gzip' field FLG should be error for 2", + "format 'gzip' field DATA should be error for 3", + "format 'gzip' field CRC should be error for 0", + "format 'gzip' field ISIZE should be error for 1", + "the deflate input compressed with dictionary should give an error" + ] + } + }, + "idlharness-shadowrealm.window.js": { + "skip": "ShadowRealm support is not enabled" + }, + "idlharness.https.any.js": { + "skip": "wpt/resources is not as simple to bring up to date" + }, + "third_party/pako/pako_inflate.min.js": { + "skip": "This is not a test file." + } +} diff --git a/test/wpt/status/performance-timeline.json b/test/wpt/status/performance-timeline.json index 9a297e641437df..36eeb36782c9aa 100644 --- a/test/wpt/status/performance-timeline.json +++ b/test/wpt/status/performance-timeline.json @@ -7,6 +7,9 @@ ] } }, + "navigation-id.helper.js": { + "skip": "This is not a test file." + }, "webtiming-resolution.any.js": { "skip": "flaky" } diff --git a/test/wpt/test-compression.js b/test/wpt/test-compression.js new file mode 100644 index 00000000000000..6991adff5645b4 --- /dev/null +++ b/test/wpt/test-compression.js @@ -0,0 +1,7 @@ +'use strict'; + +const { WPTRunner } = require('../common/wpt'); + +const runner = new WPTRunner('compression'); + +runner.runJsTests();