1
0
Fork 0
mirror of https://github.com/LadybirdBrowser/ladybird.git synced 2025-06-11 02:13:56 +09:00

LibWeb: Import ReadableStreamTee tests

This commit is contained in:
Timothy Flynn 2025-04-15 12:20:22 -04:00 committed by Tim Flynn
parent df1e51b80b
commit b324b876f2
Notes: github-actions[bot] 2025-04-16 08:06:36 +00:00
3 changed files with 529 additions and 0 deletions

View file

@ -0,0 +1,32 @@
Harness status: OK
Found 26 tests
25 Pass
1 Fail
Pass ReadableStream teeing: rs.tee() returns an array of two ReadableStreams
Pass ReadableStream teeing: should be able to read one branch to the end without affecting the other
Pass ReadableStream teeing: values should be equal across each branch
Fail ReadableStream teeing: errors in the source should propagate to both branches
Pass ReadableStream teeing: canceling branch1 should not impact branch2
Pass ReadableStream teeing: canceling branch2 should not impact branch1
Pass Running templatedRSTeeCancel with ReadableStream teeing
Pass ReadableStream teeing: canceling both branches should aggregate the cancel reasons into an array
Pass ReadableStream teeing: canceling both branches in reverse order should aggregate the cancel reasons into an array
Pass ReadableStream teeing: failing to cancel the original stream should cause cancel() to reject on branches
Pass ReadableStream teeing: erroring a teed stream should properly handle canceled branches
Pass ReadableStream teeing: erroring a teed stream should error both branches
Pass ReadableStream teeing: closing the original should immediately close the branches
Pass ReadableStream teeing: erroring the original should immediately error the branches
Pass ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream
Pass ReadableStream teeing: canceling branch1 should finish when original stream errors
Pass ReadableStream teeing: canceling both branches in sequence with delay
Pass ReadableStream teeing: failing to cancel when canceling both branches in sequence with delay
Pass ReadableStreamTee should not use a modified ReadableStream constructor from the global object
Pass ReadableStreamTee should not pull more chunks than can fit in the branch queue
Pass ReadableStreamTee should only pull enough to fill the emptiest queue
Pass ReadableStreamTee should not pull when original is already errored
Pass ReadableStreamTee stops pulling when original stream errors while branch 1 is reading
Pass ReadableStreamTee stops pulling when original stream errors while branch 2 is reading
Pass ReadableStreamTee stops pulling when original stream errors while both branches are reading
Pass ReadableStream teeing: enqueue() and close() while both branches are pulling

View file

@ -0,0 +1,18 @@
<!doctype html>
<meta charset=utf-8>
<script>
self.GLOBAL = {
isWindow: function() { return true; },
isWorker: function() { return false; },
isShadowRealm: function() { return false; },
};
</script>
<script src="../../resources/testharness.js"></script>
<script src="../../resources/testharnessreport.js"></script>
<script src="../resources/rs-utils.js"></script>
<script src="../resources/test-utils.js"></script>
<script src="../resources/recording-streams.js"></script>
<script src="../resources/rs-test-templates.js"></script>
<div id=log></div>
<script src="../../streams/readable-streams/tee.any.js"></script>

View file

@ -0,0 +1,479 @@
// META: global=window,worker,shadowrealm
// META: script=../resources/rs-utils.js
// META: script=../resources/test-utils.js
// META: script=../resources/recording-streams.js
// META: script=../resources/rs-test-templates.js
'use strict';
test(() => {
const rs = new ReadableStream();
const result = rs.tee();
assert_true(Array.isArray(result), 'return value should be an array');
assert_equals(result.length, 2, 'array should have length 2');
assert_equals(result[0].constructor, ReadableStream, '0th element should be a ReadableStream');
assert_equals(result[1].constructor, ReadableStream, '1st element should be a ReadableStream');
}, 'ReadableStream teeing: rs.tee() returns an array of two ReadableStreams');
promise_test(t => {
const rs = new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
c.close();
}
});
const branch = rs.tee();
const branch1 = branch[0];
const branch2 = branch[1];
const reader1 = branch1.getReader();
const reader2 = branch2.getReader();
reader2.closed.then(t.unreached_func('branch2 should not be closed'));
return Promise.all([
reader1.closed,
reader1.read().then(r => {
assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch1 should be correct');
}),
reader1.read().then(r => {
assert_object_equals(r, { value: 'b', done: false }, 'second chunk from branch1 should be correct');
}),
reader1.read().then(r => {
assert_object_equals(r, { value: undefined, done: true }, 'third read() from branch1 should be done');
}),
reader2.read().then(r => {
assert_object_equals(r, { value: 'a', done: false }, 'first chunk from branch2 should be correct');
})
]);
}, 'ReadableStream teeing: should be able to read one branch to the end without affecting the other');
promise_test(() => {
const theObject = { the: 'test object' };
const rs = new ReadableStream({
start(c) {
c.enqueue(theObject);
}
});
const branch = rs.tee();
const branch1 = branch[0];
const branch2 = branch[1];
const reader1 = branch1.getReader();
const reader2 = branch2.getReader();
return Promise.all([reader1.read(), reader2.read()]).then(values => {
assert_object_equals(values[0], values[1], 'the values should be equal');
});
}, 'ReadableStream teeing: values should be equal across each branch');
promise_test(t => {
const theError = { name: 'boo!' };
const rs = new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
},
pull() {
throw theError;
}
});
const branches = rs.tee();
const reader1 = branches[0].getReader();
const reader2 = branches[1].getReader();
reader1.label = 'reader1';
reader2.label = 'reader2';
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed),
reader1.read().then(r => {
assert_object_equals(r, { value: 'a', done: false }, 'should be able to read the first chunk in branch1');
}),
reader1.read().then(r => {
assert_object_equals(r, { value: 'b', done: false }, 'should be able to read the second chunk in branch1');
return promise_rejects_exactly(t, theError, reader2.read());
})
.then(() => promise_rejects_exactly(t, theError, reader1.read()))
]);
}, 'ReadableStream teeing: errors in the source should propagate to both branches');
promise_test(() => {
const rs = new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
c.close();
}
});
const branches = rs.tee();
const branch1 = branches[0];
const branch2 = branches[1];
branch1.cancel();
return Promise.all([
readableStreamToArray(branch1).then(chunks => {
assert_array_equals(chunks, [], 'branch1 should have no chunks');
}),
readableStreamToArray(branch2).then(chunks => {
assert_array_equals(chunks, ['a', 'b'], 'branch2 should have two chunks');
})
]);
}, 'ReadableStream teeing: canceling branch1 should not impact branch2');
promise_test(() => {
const rs = new ReadableStream({
start(c) {
c.enqueue('a');
c.enqueue('b');
c.close();
}
});
const branches = rs.tee();
const branch1 = branches[0];
const branch2 = branches[1];
branch2.cancel();
return Promise.all([
readableStreamToArray(branch1).then(chunks => {
assert_array_equals(chunks, ['a', 'b'], 'branch1 should have two chunks');
}),
readableStreamToArray(branch2).then(chunks => {
assert_array_equals(chunks, [], 'branch2 should have no chunks');
})
]);
}, 'ReadableStream teeing: canceling branch2 should not impact branch1');
templatedRSTeeCancel('ReadableStream teeing', (extras) => {
return new ReadableStream({ ...extras });
});
promise_test(t => {
let controller;
const stream = new ReadableStream({ start(c) { controller = c; } });
const [branch1, branch2] = stream.tee();
const error = new Error();
error.name = 'distinctive';
// Ensure neither branch is waiting in ReadableStreamDefaultReaderRead().
controller.enqueue();
controller.enqueue();
return delay(0).then(() => {
// This error will have to be detected via [[closedPromise]].
controller.error(error);
const reader1 = branch1.getReader();
const reader2 = branch2.getReader();
return Promise.all([
promise_rejects_exactly(t, error, reader1.closed, 'reader1.closed should reject'),
promise_rejects_exactly(t, error, reader2.closed, 'reader2.closed should reject')
]);
});
}, 'ReadableStream teeing: erroring a teed stream should error both branches');
promise_test(() => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const branches = rs.tee();
const reader1 = branches[0].getReader();
const reader2 = branches[1].getReader();
const promise = Promise.all([reader1.closed, reader2.closed]);
controller.close();
return promise;
}, 'ReadableStream teeing: closing the original should immediately close the branches');
promise_test(t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const branches = rs.tee();
const reader1 = branches[0].getReader();
const reader2 = branches[1].getReader();
const theError = { name: 'boo!' };
const promise = Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
controller.error(theError);
return promise;
}, 'ReadableStream teeing: erroring the original should immediately error the branches');
promise_test(async t => {
let controller;
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const cancelPromise = reader2.cancel();
controller.enqueue('a');
const read1 = await reader1.read();
assert_object_equals(read1, { value: 'a', done: false }, 'first read() from branch1 should fulfill with the chunk');
controller.close();
const read2 = await reader1.read();
assert_object_equals(read2, { value: undefined, done: true }, 'second read() from branch1 should be done');
await Promise.all([
reader1.closed,
cancelPromise
]);
}, 'ReadableStream teeing: canceling branch1 should finish when branch2 reads until end of stream');
promise_test(async t => {
let controller;
const theError = { name: 'boo!' };
const rs = new ReadableStream({
start(c) {
controller = c;
}
});
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const cancelPromise = reader2.cancel();
controller.error(theError);
await Promise.all([
promise_rejects_exactly(t, theError, reader1.read()),
cancelPromise
]);
}, 'ReadableStream teeing: canceling branch1 should finish when original stream errors');
promise_test(async () => {
const rs = new ReadableStream({});
const [branch1, branch2] = rs.tee();
const cancel1 = branch1.cancel();
await flushAsyncEvents();
const cancel2 = branch2.cancel();
await Promise.all([cancel1, cancel2]);
}, 'ReadableStream teeing: canceling both branches in sequence with delay');
promise_test(async t => {
const theError = { name: 'boo!' };
const rs = new ReadableStream({
cancel() {
throw theError;
}
});
const [branch1, branch2] = rs.tee();
const cancel1 = branch1.cancel();
await flushAsyncEvents();
const cancel2 = branch2.cancel();
await Promise.all([
promise_rejects_exactly(t, theError, cancel1),
promise_rejects_exactly(t, theError, cancel2)
]);
}, 'ReadableStream teeing: failing to cancel when canceling both branches in sequence with delay');
test(t => {
// Copy original global.
const oldReadableStream = ReadableStream;
const getReader = ReadableStream.prototype.getReader;
const origRS = new ReadableStream();
// Replace the global ReadableStream constructor with one that doesn't work.
ReadableStream = function() {
throw new Error('global ReadableStream constructor called');
};
t.add_cleanup(() => {
ReadableStream = oldReadableStream;
});
// This will probably fail if the global ReadableStream constructor was used.
const [rs1, rs2] = origRS.tee();
// These will definitely fail if the global ReadableStream constructor was used.
assert_not_equals(getReader.call(rs1), undefined, 'getReader should work on rs1');
assert_not_equals(getReader.call(rs2), undefined, 'getReader should work on rs2');
}, 'ReadableStreamTee should not use a modified ReadableStream constructor from the global object');
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
// Create two branches, each with a HWM of 1. This should result in one
// chunk being pulled, not two.
rs.tee();
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should only be called once');
});
}, 'ReadableStreamTee should not pull more chunks than can fit in the branch queue');
promise_test(t => {
const rs = recordingReadableStream({
pull(controller) {
controller.enqueue('a');
}
}, { highWaterMark: 0 });
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return Promise.all([reader1.read(), reader2.read()])
.then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, 'ReadableStreamTee should only pull enough to fill the emptiest queue');
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
rs.controller.error(theError);
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, [], 'pull should not be called');
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
});
}, 'ReadableStreamTee should not pull when original is already errored');
for (const branch of [1, 2]) {
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should be called once');
rs.controller.enqueue('a');
const reader = (branch === 1) ? reader1 : reader2;
return reader.read();
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
rs.controller.error(theError);
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, `ReadableStreamTee stops pulling when original stream errors while branch ${branch} is reading`);
}
promise_test(t => {
const rs = recordingReadableStream({}, { highWaterMark: 0 });
const theError = { name: 'boo!' };
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
return flushAsyncEvents().then(() => {
assert_array_equals(rs.events, ['pull'], 'pull should be called once');
rs.controller.enqueue('a');
return Promise.all([reader1.read(), reader2.read()]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
rs.controller.error(theError);
return Promise.all([
promise_rejects_exactly(t, theError, reader1.closed),
promise_rejects_exactly(t, theError, reader2.closed)
]);
}).then(() => flushAsyncEvents()).then(() => {
assert_array_equals(rs.events, ['pull', 'pull'], 'pull should be called twice');
});
}, 'ReadableStreamTee stops pulling when original stream errors while both branches are reading');
promise_test(async () => {
const rs = recordingReadableStream();
const [reader1, reader2] = rs.tee().map(branch => branch.getReader());
const branch1Reads = [reader1.read(), reader1.read()];
const branch2Reads = [reader2.read(), reader2.read()];
await flushAsyncEvents();
rs.controller.enqueue('a');
rs.controller.close();
assert_object_equals(await branch1Reads[0], { value: 'a', done: false }, 'first chunk from branch1 should be correct');
assert_object_equals(await branch2Reads[0], { value: 'a', done: false }, 'first chunk from branch2 should be correct');
assert_object_equals(await branch1Reads[1], { value: undefined, done: true }, 'second read() from branch1 should be done');
assert_object_equals(await branch2Reads[1], { value: undefined, done: true }, 'second read() from branch2 should be done');
}, 'ReadableStream teeing: enqueue() and close() while both branches are pulling');