blob: 13debfef60a94aecbfb52d82ce2fd441f20defd2 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/streams/transform_stream.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/script_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_extras_test_utils.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_iterator_result_value.h"
#include "third_party/blink/renderer/core/streams/readable_stream.h"
#include "third_party/blink/renderer/core/streams/transform_stream_default_controller.h"
#include "third_party/blink/renderer/core/streams/transform_stream_transformer.h"
#include "third_party/blink/renderer/core/streams/writable_stream.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/microtask.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/to_v8.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
using ::testing::_;
using ::testing::ByMove;
using ::testing::Mock;
using ::testing::Return;
class TransformStreamTest : public ::testing::Test {
public:
TransformStreamTest() {}
TransformStream* Stream() const { return stream_; }
void Init(TransformStreamTransformer* transformer,
ScriptState* script_state,
ExceptionState& exception_state) {
stream_ =
TransformStream::Create(script_state, transformer, exception_state);
}
// This takes the |readable| and |writable| properties of the TransformStream
// and copies them onto the global object so they can be accessed by Eval().
void CopyReadableAndWritableToGlobal(const V8TestingScope& scope) {
auto* script_state = scope.GetScriptState();
ReadableStream* readable = Stream()->Readable();
WritableStream* writable = Stream()->Writable();
v8::Local<v8::Object> global = script_state->GetContext()->Global();
EXPECT_TRUE(global
->Set(scope.GetContext(),
V8String(scope.GetIsolate(), "readable"),
ToV8(readable, script_state))
.IsJust());
EXPECT_TRUE(global
->Set(scope.GetContext(),
V8String(scope.GetIsolate(), "writable"),
ToV8(writable, script_state))
.IsJust());
}
private:
Persistent<TransformStream> stream_;
};
// A convenient base class to make tests shorter. Subclasses need not implement
// both Transform() and Flush(), and can override the void versions to avoid the
// need to create a promise to return. Not appropriate for use in production.
class TestTransformer : public TransformStreamTransformer {
public:
explicit TestTransformer(ScriptState* script_state)
: script_state_(script_state) {}
virtual void TransformVoid(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState&) {}
ScriptPromise Transform(v8::Local<v8::Value> chunk,
TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
TransformVoid(chunk, controller, exception_state);
return ScriptPromise::CastUndefined(script_state_);
}
virtual void FlushVoid(TransformStreamDefaultController*, ExceptionState&) {}
ScriptPromise Flush(TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
FlushVoid(controller, exception_state);
return ScriptPromise::CastUndefined(script_state_);
}
ScriptState* GetScriptState() override { return script_state_; }
void Trace(Visitor* visitor) override {
visitor->Trace(script_state_);
TransformStreamTransformer::Trace(visitor);
}
private:
const Member<ScriptState> script_state_;
};
class IdentityTransformer final : public TestTransformer {
public:
explicit IdentityTransformer(ScriptState* script_state)
: TestTransformer(script_state) {}
void TransformVoid(v8::Local<v8::Value> chunk,
TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
controller->enqueue(GetScriptState(),
ScriptValue(GetScriptState()->GetIsolate(), chunk),
exception_state);
}
};
class MockTransformStreamTransformer : public TransformStreamTransformer {
public:
explicit MockTransformStreamTransformer(ScriptState* script_state)
: script_state_(script_state) {}
MOCK_METHOD3(Transform,
ScriptPromise(v8::Local<v8::Value> chunk,
TransformStreamDefaultController*,
ExceptionState&));
MOCK_METHOD2(Flush,
ScriptPromise(TransformStreamDefaultController*,
ExceptionState&));
ScriptState* GetScriptState() override { return script_state_; }
void Trace(Visitor* visitor) override {
visitor->Trace(script_state_);
TransformStreamTransformer::Trace(visitor);
}
private:
const Member<ScriptState> script_state_;
};
// If this doesn't work then nothing else will.
TEST_F(TransformStreamTest, Construct) {
V8TestingScope scope;
Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
scope.GetScriptState(), ASSERT_NO_EXCEPTION);
EXPECT_TRUE(Stream());
}
TEST_F(TransformStreamTest, Accessors) {
V8TestingScope scope;
Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
scope.GetScriptState(), ASSERT_NO_EXCEPTION);
ReadableStream* readable = Stream()->Readable();
WritableStream* writable = Stream()->Writable();
EXPECT_TRUE(readable);
EXPECT_TRUE(writable);
}
TEST_F(TransformStreamTest, TransformIsCalled) {
V8TestingScope scope;
auto* mock = MakeGarbageCollected<MockTransformStreamTransformer>(
scope.GetScriptState());
Init(mock, scope.GetScriptState(), ASSERT_NO_EXCEPTION);
// Need to run microtasks so the startAlgorithm promise resolves.
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
CopyReadableAndWritableToGlobal(scope);
EXPECT_CALL(*mock, Transform(_, _, _))
.WillOnce(
Return(ByMove(ScriptPromise::CastUndefined(scope.GetScriptState()))));
// The initial read is needed to relieve backpressure.
EvalWithPrintingError(&scope,
"readable.getReader().read();\n"
"const writer = writable.getWriter();\n"
"writer.write('a');\n");
Mock::VerifyAndClear(mock);
Mock::AllowLeak(mock);
}
TEST_F(TransformStreamTest, FlushIsCalled) {
V8TestingScope scope;
auto* mock = MakeGarbageCollected<MockTransformStreamTransformer>(
scope.GetScriptState());
Init(mock, scope.GetScriptState(), ASSERT_NO_EXCEPTION);
// Need to run microtasks so the startAlgorithm promise resolves.
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
CopyReadableAndWritableToGlobal(scope);
EXPECT_CALL(*mock, Flush(_, _))
.WillOnce(
Return(ByMove(ScriptPromise::CastUndefined(scope.GetScriptState()))));
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
Mock::VerifyAndClear(mock);
Mock::AllowLeak(mock);
}
bool IsIteratorForStringMatching(ScriptState* script_state,
ScriptValue value,
const String& expected) {
if (!value.IsObject()) {
return false;
}
bool done = false;
auto chunk = V8UnpackIteratorResult(
script_state,
value.V8Value()->ToObject(script_state->GetContext()).ToLocalChecked(),
&done);
if (done || chunk.IsEmpty())
return false;
return ToCoreStringWithUndefinedOrNullCheck(chunk.ToLocalChecked()) ==
expected;
}
bool IsTypeError(ScriptState* script_state,
ScriptValue value,
const String& message) {
v8::Local<v8::Object> object;
if (!value.V8Value()->ToObject(script_state->GetContext()).ToLocal(&object)) {
return false;
}
if (!object->IsNativeError())
return false;
const auto& Has = [script_state, object](const String& key,
const String& value) -> bool {
v8::Local<v8::Value> actual;
return object
->Get(script_state->GetContext(),
V8AtomicString(script_state->GetIsolate(), key))
.ToLocal(&actual) &&
ToCoreStringWithUndefinedOrNullCheck(actual) == value;
};
return Has("name", "TypeError") && Has("message", message);
}
TEST_F(TransformStreamTest, EnqueueFromTransform) {
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.write('a');\n");
ReadableStream* readable = Stream()->Readable();
auto* reader = readable->getReader(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester tester(script_state,
reader->read(script_state, ASSERT_NO_EXCEPTION));
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
EXPECT_TRUE(IsIteratorForStringMatching(script_state, tester.Value(), "a"));
}
TEST_F(TransformStreamTest, EnqueueFromFlush) {
class EnqueueFromFlushTransformer final : public TestTransformer {
public:
explicit EnqueueFromFlushTransformer(ScriptState* script_state)
: TestTransformer(script_state) {}
void FlushVoid(TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
controller->enqueue(GetScriptState(),
ScriptValue::From(GetScriptState(), "a"),
exception_state);
}
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(MakeGarbageCollected<EnqueueFromFlushTransformer>(script_state),
script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
ReadableStream* readable = Stream()->Readable();
auto* reader = readable->getReader(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester tester(script_state,
reader->read(script_state, ASSERT_NO_EXCEPTION));
tester.WaitUntilSettled();
EXPECT_TRUE(tester.IsFulfilled());
EXPECT_TRUE(IsIteratorForStringMatching(script_state, tester.Value(), "a"));
}
TEST_F(TransformStreamTest, ThrowFromTransform) {
static constexpr char kMessage[] = "errorInTransform";
class ThrowFromTransformTransformer final : public TestTransformer {
public:
explicit ThrowFromTransformTransformer(ScriptState* script_state)
: TestTransformer(script_state) {}
void TransformVoid(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState& exception_state) override {
exception_state.ThrowTypeError(kMessage);
}
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(MakeGarbageCollected<ThrowFromTransformTransformer>(
scope.GetScriptState()),
script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.write('a');\n");
ReadableStream* readable = Stream()->Readable();
auto* reader = readable->getReader(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester read_tester(
script_state, reader->read(script_state, ASSERT_NO_EXCEPTION));
read_tester.WaitUntilSettled();
EXPECT_TRUE(read_tester.IsRejected());
EXPECT_TRUE(IsTypeError(script_state, read_tester.Value(), kMessage));
ScriptPromiseTester write_tester(script_state,
ScriptPromise::Cast(script_state, promise));
write_tester.WaitUntilSettled();
EXPECT_TRUE(write_tester.IsRejected());
EXPECT_TRUE(IsTypeError(script_state, write_tester.Value(), kMessage));
}
TEST_F(TransformStreamTest, ThrowFromFlush) {
static constexpr char kMessage[] = "errorInFlush";
class ThrowFromFlushTransformer final : public TestTransformer {
public:
explicit ThrowFromFlushTransformer(ScriptState* script_state)
: TestTransformer(script_state) {}
void FlushVoid(TransformStreamDefaultController*,
ExceptionState& exception_state) override {
exception_state.ThrowTypeError(kMessage);
}
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(MakeGarbageCollected<ThrowFromFlushTransformer>(scope.GetScriptState()),
script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
ReadableStream* readable = Stream()->Readable();
auto* reader = readable->getReader(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester read_tester(
script_state, reader->read(script_state, ASSERT_NO_EXCEPTION));
read_tester.WaitUntilSettled();
EXPECT_TRUE(read_tester.IsRejected());
EXPECT_TRUE(IsTypeError(script_state, read_tester.Value(), kMessage));
ScriptPromiseTester write_tester(script_state,
ScriptPromise::Cast(script_state, promise));
write_tester.WaitUntilSettled();
EXPECT_TRUE(write_tester.IsRejected());
EXPECT_TRUE(IsTypeError(script_state, write_tester.Value(), kMessage));
}
TEST_F(TransformStreamTest, CreateFromReadableWritablePair) {
V8TestingScope scope;
ReadableStream* readable =
ReadableStream::Create(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
WritableStream* writable =
WritableStream::Create(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
TransformStream transform(readable, writable);
EXPECT_EQ(readable, transform.Readable());
EXPECT_EQ(writable, transform.Writable());
}
TEST_F(TransformStreamTest, WaitInTransform) {
class WaitInTransformTransformer final : public TestTransformer {
public:
explicit WaitInTransformTransformer(ScriptState* script_state)
: TestTransformer(script_state),
transform_promise_resolver_(
MakeGarbageCollected<ScriptPromiseResolver>(script_state)) {}
ScriptPromise Transform(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState&) override {
return transform_promise_resolver_->Promise();
}
void FlushVoid(TransformStreamDefaultController*,
ExceptionState&) override {
flush_called_ = true;
}
void ResolvePromise() { transform_promise_resolver_->Resolve(); }
bool FlushCalled() const { return flush_called_; }
void Trace(Visitor* visitor) override {
visitor->Trace(transform_promise_resolver_);
TestTransformer::Trace(visitor);
}
private:
const Member<ScriptPromiseResolver> transform_promise_resolver_;
bool flush_called_ = false;
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
auto* transformer =
MakeGarbageCollected<WaitInTransformTransformer>(script_state);
Init(transformer, script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"const promise = writer.write('a');\n"
"writer.close();\n"
"promise;\n");
// Need to read to relieve backpressure.
Stream()
->Readable()
->getReader(script_state, ASSERT_NO_EXCEPTION)
->read(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester write_tester(script_state,
ScriptPromise::Cast(script_state, promise));
// Give Transform() the opportunity to be called.
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(write_tester.IsFulfilled());
EXPECT_FALSE(transformer->FlushCalled());
transformer->ResolvePromise();
write_tester.WaitUntilSettled();
EXPECT_TRUE(write_tester.IsFulfilled());
EXPECT_TRUE(transformer->FlushCalled());
}
TEST_F(TransformStreamTest, WaitInFlush) {
class WaitInFlushTransformer final : public TestTransformer {
public:
explicit WaitInFlushTransformer(ScriptState* script_state)
: TestTransformer(script_state),
flush_promise_resolver_(
MakeGarbageCollected<ScriptPromiseResolver>(script_state)) {}
ScriptPromise Flush(TransformStreamDefaultController*,
ExceptionState&) override {
return flush_promise_resolver_->Promise();
}
void ResolvePromise() { flush_promise_resolver_->Resolve(); }
void Trace(Visitor* visitor) override {
visitor->Trace(flush_promise_resolver_);
TestTransformer::Trace(visitor);
}
private:
const Member<ScriptPromiseResolver> flush_promise_resolver_;
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
auto* transformer =
MakeGarbageCollected<WaitInFlushTransformer>(script_state);
Init(transformer, script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
// Need to read to relieve backpressure.
Stream()
->Readable()
->getReader(script_state, ASSERT_NO_EXCEPTION)
->read(script_state, ASSERT_NO_EXCEPTION);
ScriptPromiseTester close_tester(script_state,
ScriptPromise::Cast(script_state, promise));
// Give Flush() the opportunity to be called.
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_FALSE(close_tester.IsFulfilled());
transformer->ResolvePromise();
close_tester.WaitUntilSettled();
EXPECT_TRUE(close_tester.IsFulfilled());
}
} // namespace
} // namespace blink