diff options
Diffstat (limited to 'cli/js/tests/streams_transform_test.ts')
-rw-r--r-- | cli/js/tests/streams_transform_test.ts | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/cli/js/tests/streams_transform_test.ts b/cli/js/tests/streams_transform_test.ts new file mode 100644 index 000000000..f3ec148ae --- /dev/null +++ b/cli/js/tests/streams_transform_test.ts @@ -0,0 +1,562 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { + unitTest, + assert, + assertEquals, + assertNotEquals, + assertThrows, +} from "./test_util.ts"; + +function delay(seconds: number): Promise<void> { + return new Promise<void>((resolve) => { + setTimeout(() => { + resolve(); + }, seconds); + }); +} + +function readableStreamToArray<R>( + readable: { getReader(): ReadableStreamDefaultReader<R> }, + reader?: ReadableStreamDefaultReader<R> +): Promise<R[]> { + if (reader === undefined) { + reader = readable.getReader(); + } + + const chunks: R[] = []; + + return pump(); + + function pump(): Promise<R[]> { + return reader!.read().then((result) => { + if (result.done) { + return chunks; + } + + chunks.push(result.value); + return pump(); + }); + } +} + +unitTest(function transformStreamConstructedWithTransformFunction() { + new TransformStream({ transform(): void {} }); +}); + +unitTest(function transformStreamConstructedNoTransform() { + new TransformStream(); + new TransformStream({}); +}); + +unitTest(function transformStreamIntstancesHaveProperProperties() { + const ts = new TransformStream({ transform(): void {} }); + const proto = Object.getPrototypeOf(ts); + + const writableStream = Object.getOwnPropertyDescriptor(proto, "writable"); + assert(writableStream !== undefined, "it has a writable property"); + assert(!writableStream.enumerable, "writable should be non-enumerable"); + assertEquals( + typeof writableStream.get, + "function", + "writable should have a getter" + ); + assertEquals( + writableStream.set, + undefined, + "writable should not have a setter" + ); + assert(writableStream.configurable, "writable should be configurable"); + assert( + ts.writable instanceof WritableStream, + "writable is an instance of WritableStream" + ); + assert( + WritableStream.prototype.getWriter.call(ts.writable), + "writable should pass WritableStream brand check" + ); + + const readableStream = Object.getOwnPropertyDescriptor(proto, "readable"); + assert(readableStream !== undefined, "it has a readable property"); + assert(!readableStream.enumerable, "readable should be non-enumerable"); + assertEquals( + typeof readableStream.get, + "function", + "readable should have a getter" + ); + assertEquals( + readableStream.set, + undefined, + "readable should not have a setter" + ); + assert(readableStream.configurable, "readable should be configurable"); + assert( + ts.readable instanceof ReadableStream, + "readable is an instance of ReadableStream" + ); + assertNotEquals( + ReadableStream.prototype.getReader.call(ts.readable), + undefined, + "readable should pass ReadableStream brand check" + ); +}); + +unitTest(function transformStreamWritableStartsAsWritable() { + const ts = new TransformStream({ transform(): void {} }); + + const writer = ts.writable.getWriter(); + assertEquals(writer.desiredSize, 1, "writer.desiredSize should be 1"); +}); + +unitTest(async function transformStreamReadableCanReadOutOfWritable() { + const ts = new TransformStream(); + + const writer = ts.writable.getWriter(); + writer.write("a"); + assertEquals( + writer.desiredSize, + 0, + "writer.desiredSize should be 0 after write()" + ); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "a", + "result from reading the readable is the same as was written to writable" + ); + assert(!result.done, "stream should not be done"); + + await delay(0); + assert(writer.desiredSize === 1, "desiredSize should be 1 again"); +}); + +unitTest(async function transformStreamCanReadWhatIsWritten() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): void { + c.enqueue(chunk.toUpperCase()); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "A", + "result from reading the readable is the transformation of what was written to writable" + ); + assert(!result.done, "stream should not be done"); +}); + +unitTest(async function transformStreamCanReadBothChunks() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): void { + c.enqueue(chunk.toUpperCase()); + c.enqueue(chunk.toUpperCase()); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const reader = ts.readable.getReader(); + + const result1 = await reader.read(); + assertEquals( + result1.value, + "A", + "the first chunk read is the transformation of the single chunk written" + ); + assert(!result1.done, "stream should not be done"); + + const result2 = await reader.read(); + assertEquals( + result2.value, + "A", + "the second chunk read is also the transformation of the single chunk written" + ); + assert(!result2.done, "stream should not be done"); +}); + +unitTest(async function transformStreamCanReadWhatIsWritten() { + let c: TransformStreamDefaultController; + const ts = new TransformStream({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(chunk: string): Promise<void> { + return delay(0).then(() => c.enqueue(chunk.toUpperCase())); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result = await ts.readable.getReader().read(); + assertEquals( + result.value, + "A", + "result from reading the readable is the transformation of what was written to writable" + ); + assert(!result.done, "stream should not be done"); +}); + +unitTest(async function transformStreamAsyncReadMultipleChunks() { + let doSecondEnqueue: () => void; + let returnFromTransform: () => void; + const ts = new TransformStream({ + transform( + chunk: string, + controller: TransformStreamDefaultController + ): Promise<void> { + delay(0).then(() => controller.enqueue(chunk.toUpperCase())); + doSecondEnqueue = (): void => controller.enqueue(chunk.toUpperCase()); + return new Promise((resolve) => { + returnFromTransform = resolve; + }); + }, + }); + + const reader = ts.readable.getReader(); + + const writer = ts.writable.getWriter(); + writer.write("a"); + + const result1 = await reader.read(); + assertEquals( + result1.value, + "A", + "the first chunk read is the transformation of the single chunk written" + ); + assert(!result1.done, "stream should not be done"); + doSecondEnqueue!(); + + const result2 = await reader.read(); + assertEquals( + result2.value, + "A", + "the second chunk read is also the transformation of the single chunk written" + ); + assert(!result2.done, "stream should not be done"); + returnFromTransform!(); +}); + +unitTest(function transformStreamClosingWriteClosesRead() { + const ts = new TransformStream({ transform(): void {} }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]).then( + undefined + ); +}); + +unitTest(async function transformStreamCloseWaitAwaitsTransforms() { + let transformResolve: () => void; + const transformPromise = new Promise<void>((resolve) => { + transformResolve = resolve; + }); + const ts = new TransformStream( + { + transform(): Promise<void> { + return transformPromise; + }, + }, + undefined, + { highWaterMark: 1 } + ); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + let rsClosed = false; + ts.readable.getReader().closed.then(() => { + rsClosed = true; + }); + + await delay(0); + assertEquals(rsClosed, false, "readable is not closed after a tick"); + transformResolve!(); + + await writer.closed; + // TODO: Is this expectation correct? + assertEquals(rsClosed, true, "readable is closed at that point"); +}); + +unitTest(async function transformStreamCloseWriteAfterSyncEnqueues() { + let c: TransformStreamDefaultController<string>; + const ts = new TransformStream<string, string>({ + start(controller: TransformStreamDefaultController): void { + c = controller; + }, + transform(): Promise<void> { + c.enqueue("x"); + c.enqueue("y"); + return delay(0); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["x", "y"], + "both enqueued chunks can be read from the readable" + ); +}); + +unitTest(async function transformStreamWritableCloseAsyncAfterAsyncEnqueues() { + let c: TransformStreamDefaultController<string>; + const ts = new TransformStream<string, string>({ + start(controller: TransformStreamDefaultController<string>): void { + c = controller; + }, + transform(): Promise<void> { + return delay(0) + .then(() => c.enqueue("x")) + .then(() => c.enqueue("y")) + .then(() => delay(0)); + }, + }); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["x", "y"], + "both enqueued chunks can be read from the readable" + ); +}); + +unitTest(async function transformStreamTransformerMethodsCalledAsMethods() { + let c: TransformStreamDefaultController<string>; + const transformer = { + suffix: "-suffix", + + start(controller: TransformStreamDefaultController<string>): void { + c = controller; + c.enqueue("start" + this.suffix); + }, + + transform(chunk: string): void { + c.enqueue(chunk + this.suffix); + }, + + flush(): void { + c.enqueue("flushed" + this.suffix); + }, + }; + const ts = new TransformStream(transformer); + + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + const readableChunks = readableStreamToArray(ts.readable); + + await writer.closed; + const chunks = await readableChunks; + assertEquals( + chunks, + ["start-suffix", "a-suffix", "flushed-suffix"], + "all enqueued chunks have suffixes" + ); +}); + +unitTest(async function transformStreamMethodsShouldNotBeAppliedOrCalled() { + function functionWithOverloads(): void {} + functionWithOverloads.apply = (): void => { + throw new Error("apply() should not be called"); + }; + functionWithOverloads.call = (): void => { + throw new Error("call() should not be called"); + }; + const ts = new TransformStream({ + start: functionWithOverloads, + transform: functionWithOverloads, + flush: functionWithOverloads, + }); + const writer = ts.writable.getWriter(); + writer.write("a"); + writer.close(); + + await readableStreamToArray(ts.readable); +}); + +unitTest(async function transformStreamCallTransformSync() { + let transformCalled = false; + const ts = new TransformStream( + { + transform(): void { + transformCalled = true; + }, + }, + undefined, + { highWaterMark: Infinity } + ); + // transform() is only called synchronously when there is no backpressure and + // all microtasks have run. + await delay(0); + const writePromise = ts.writable.getWriter().write(undefined); + assert(transformCalled, "transform() should have been called"); + await writePromise; +}); + +unitTest(function transformStreamCloseWriteCloesesReadWithNoChunks() { + const ts = new TransformStream({}, undefined, { highWaterMark: 0 }); + + const writer = ts.writable.getWriter(); + writer.close(); + + return Promise.all([writer.closed, ts.readable.getReader().closed]).then( + undefined + ); +}); + +unitTest(function transformStreamEnqueueThrowsAfterTerminate() { + new TransformStream({ + start(controller: TransformStreamDefaultController): void { + controller.terminate(); + assertThrows(() => { + controller.enqueue(undefined); + }, TypeError); + }, + }); +}); + +unitTest(function transformStreamEnqueueThrowsAfterReadableCancel() { + let controller: TransformStreamDefaultController; + const ts = new TransformStream({ + start(c: TransformStreamDefaultController): void { + controller = c; + }, + }); + const cancelPromise = ts.readable.cancel(); + assertThrows( + () => controller.enqueue(undefined), + TypeError, + undefined, + "enqueue should throw" + ); + return cancelPromise; +}); + +unitTest(function transformStreamSecondTerminateNoOp() { + new TransformStream({ + start(controller: TransformStreamDefaultController): void { + controller.terminate(); + controller.terminate(); + }, + }); +}); + +unitTest(async function transformStreamTerminateAfterReadableCancelIsNoop() { + let controller: TransformStreamDefaultController; + const ts = new TransformStream({ + start(c: TransformStreamDefaultController): void { + controller = c; + }, + }); + const cancelReason = { name: "cancelReason" }; + const cancelPromise = ts.readable.cancel(cancelReason); + controller!.terminate(); + await cancelPromise; + try { + await ts.writable.getWriter().closed; + } catch (e) { + assert(e === cancelReason); + return; + } + throw new Error("closed should have rejected"); +}); + +unitTest(async function transformStreamStartCalledOnce() { + let calls = 0; + new TransformStream({ + start(): void { + ++calls; + }, + }); + await delay(0); + assertEquals(calls, 1, "start() should have been called exactly once"); +}); + +unitTest(function transformStreamReadableTypeThrows() { + assertThrows( + // eslint-disable-next-line + () => new TransformStream({ readableType: "bytes" as any }), + RangeError, + undefined, + "constructor should throw" + ); +}); + +unitTest(function transformStreamWirtableTypeThrows() { + assertThrows( + // eslint-disable-next-line + () => new TransformStream({ writableType: "bytes" as any }), + RangeError, + undefined, + "constructor should throw" + ); +}); + +unitTest(function transformStreamSubclassable() { + class Subclass extends TransformStream { + extraFunction(): boolean { + return true; + } + } + assert( + Object.getPrototypeOf(Subclass.prototype) === TransformStream.prototype, + "Subclass.prototype's prototype should be TransformStream.prototype" + ); + assert( + Object.getPrototypeOf(Subclass) === TransformStream, + "Subclass's prototype should be TransformStream" + ); + const sub = new Subclass(); + assert( + sub instanceof TransformStream, + "Subclass object should be an instance of TransformStream" + ); + assert( + sub instanceof Subclass, + "Subclass object should be an instance of Subclass" + ); + const readableGetter = Object.getOwnPropertyDescriptor( + TransformStream.prototype, + "readable" + )!.get; + assert( + readableGetter!.call(sub) === sub.readable, + "Subclass object should pass brand check" + ); + assert( + sub.extraFunction(), + "extraFunction() should be present on Subclass object" + ); +}); |