Commit 50faa7d2 authored by Adam Rice's avatar Adam Rice Committed by Commit Bot

Add C++ wrapper for TransformStream

Design doc
https://docs.google.com/document/d/17goe4jacAYjHHtprfVPSrqDvF_J58u2qirz0HQ58sQ4/edit

Implement the C++ classes TransformStream and
TransformStreamDefaultController. These provide thin wrappers for the
equivalent JavaScript classes.

Modify the JavaScript TransformStream implementation to pass the
|controller| argument to the algorithms. This is a departure from the
standard, but the difference is not observable to user code. It makes
the memory management considerably simpler as no C++ reference to the
TransformStreamDefaultController needs to be retained between calls.

Define the interface TransformStreamTransformer.

Also create unit tests for these new classes.

Bug: 845427
Change-Id: I067a8ff15daaa4912760fbdb9ca4697705f2e3f8
Reviewed-on: https://chromium-review.googlesource.com/1156324Reviewed-by: default avatarYuki Shiino <yukishiino@chromium.org>
Reviewed-by: default avatarKentaro Hara <haraken@chromium.org>
Reviewed-by: default avatarKinuko Yasuda <kinuko@chromium.org>
Reviewed-by: default avatarYutaka Hirano <yhirano@chromium.org>
Commit-Queue: Adam Rice <ricea@chromium.org>
Cr-Commit-Position: refs/heads/master@{#582512}
parent 690394a8
......@@ -5,6 +5,7 @@
#include "third_party/blink/renderer/bindings/core/v8/script_function.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/wtf/assertions.h"
namespace blink {
......@@ -26,6 +27,16 @@ v8::Local<v8::Function> ScriptFunction::BindToV8Function() {
.ToLocalChecked();
}
ScriptValue ScriptFunction::Call(ScriptValue) {
NOTREACHED();
return ScriptValue();
}
void ScriptFunction::CallRaw(const v8::FunctionCallbackInfo<v8::Value>& args) {
ScriptValue result = Call(ScriptValue(GetScriptState(), args[0]));
V8SetReturnValue(args, result.V8Value());
}
void ScriptFunction::CallCallback(
const v8::FunctionCallbackInfo<v8::Value>& args) {
DCHECK(args.Data()->IsExternal());
......@@ -33,9 +44,7 @@ void ScriptFunction::CallCallback(
"Blink_CallCallback");
ScriptFunction* script_function = static_cast<ScriptFunction*>(
v8::Local<v8::External>::Cast(args.Data())->Value());
ScriptValue result = script_function->Call(
ScriptValue(script_function->GetScriptState(), args[0]));
V8SetReturnValue(args, result.V8Value());
script_function->CallRaw(args);
}
} // namespace blink
......@@ -41,14 +41,13 @@ namespace blink {
// A common way of using ScriptFunction is as follows:
//
// class DerivedFunction : public ScriptFunction {
// // This returns a V8 function which the DerivedFunction is bound to.
// // The DerivedFunction is destructed when the V8 function is
// // garbage-collected.
// static v8::Local<v8::Function> createFunction(ScriptState* scriptState)
// {
// DerivedFunction* self = new DerivedFunction(scriptState);
// return self->bindToV8Function();
// }
// // This returns a V8 function which the DerivedFunction is bound to.
// // The DerivedFunction is destroyed when the V8 function is
// // garbage-collected.
// static v8::Local<v8::Function> CreateFunction(ScriptState* script_state) {
// DerivedFunction* self = new DerivedFunction(script_state);
// return self->BindToV8Function();
// }
// };
class CORE_EXPORT ScriptFunction
: public GarbageCollectedFinalized<ScriptFunction> {
......@@ -65,7 +64,14 @@ class CORE_EXPORT ScriptFunction
v8::Local<v8::Function> BindToV8Function();
private:
virtual ScriptValue Call(ScriptValue) = 0;
// Subclasses should implement one of Call() or CallRaw(). Most will implement
// Call().
virtual ScriptValue Call(ScriptValue);
// To support more than one argument, or for low-level access to the V8 API,
// implement CallRaw(). The default implementation delegates to Call().
virtual void CallRaw(const v8::FunctionCallbackInfo<v8::Value>&);
static void CallCallback(const v8::FunctionCallbackInfo<v8::Value>&);
Member<ScriptState> script_state_;
......
......@@ -2085,6 +2085,7 @@ jumbo_source_set("unit_tests") {
"scroll/scrollable_area_test.cc",
"scroll/scrollbar_theme_overlay_test.cc",
"streams/readable_stream_operations_test.cc",
"streams/transform_stream_test.cc",
"style/border_value_test.cc",
"style/computed_style_test.cc",
"style/filter_operations_test.cc",
......
......@@ -9,6 +9,11 @@ blink_core_sources("streams") {
"readable_stream_default_controller_wrapper.h",
"readable_stream_operations.cc",
"readable_stream_operations.h",
"transform_stream.cc",
"transform_stream.h",
"transform_stream_default_controller.cc",
"transform_stream_default_controller.h",
"transform_stream_transformer.h",
"underlying_source_base.cc",
"underlying_source_base.h",
]
......
......@@ -23,7 +23,12 @@
const _writable = v8.createPrivateSymbol('[[writable]]');
const _controlledTransformStream =
v8.createPrivateSymbol('[[controlledTransformStream]]');
// Unlike the version in the standard, the controller is passed to this.
const _flushAlgorithm = v8.createPrivateSymbol('[[flushAlgorithm]]');
// Unlike the version in the standard, the controller is passed in as the
// second argument.
const _transformAlgorithm = v8.createPrivateSymbol('[[transformAlgorithm]]');
// Javascript functions. It is important to use these copies, as the ones on
......@@ -45,7 +50,7 @@
const {
hasOwnPropertyNoThrow,
resolvePromise,
CreateAlgorithmFromUnderlyingMethodPassingController,
CreateAlgorithmFromUnderlyingMethod,
CallOrNoop1,
MakeSizeAlgorithmFromSizeFunction,
PromiseCall2,
......@@ -129,6 +134,8 @@
const TransformStream_prototype = TransformStream.prototype;
// The controller is passed to |transformAlgorithm| and |flushAlgorithm|,
// unlike in the standard.
function CreateTransformStream(
startAlgorithm, transformAlgorithm, flushAlgorithm, writableHighWaterMark,
writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm) {
......@@ -322,8 +329,8 @@
}
};
}
const flushAlgorithm = CreateAlgorithmFromUnderlyingMethodPassingController(
transformer, 'flush', 0, controller, 'transformer.flush');
const flushAlgorithm = CreateAlgorithmFromUnderlyingMethod(
transformer, 'flush', 1, 'transformer.flush');
SetUpTransformStreamDefaultController(
stream, controller, transformAlgorithm, flushAlgorithm);
}
......@@ -397,11 +404,11 @@
// assert(binding.isWritableStreamWritable(writable),
// `state is "writable"`);
return controller[_transformAlgorithm](chunk);
return controller[_transformAlgorithm](chunk, controller);
});
}
return controller[_transformAlgorithm](chunk);
return controller[_transformAlgorithm](chunk, controller);
}
function TransformStreamDefaultSinkAbortAlgorithm(stream, reason) {
......@@ -412,7 +419,7 @@
function TransformStreamDefaultSinkCloseAlgorithm(stream) {
const readable = stream[_readable];
const controller = stream[_transformStreamController];
const flushPromise = controller[_flushAlgorithm]();
const flushPromise = controller[_flushAlgorithm](controller);
TransformStreamDefaultControllerClearAlgorithms(controller);
return thenPromise(
......@@ -445,6 +452,22 @@
return stream[_backpressureChangePromise];
}
// A wrapper for CreateTransformStream() with only the arguments that
// blink::TransformStream needs. |transformAlgorithm| and |flushAlgorithm| are
// passed the controller, unlike in the standard.
function createTransformStreamSimple(transformAlgorithm, flushAlgorithm) {
return CreateTransformStream(() => Promise_resolve(),
transformAlgorithm, flushAlgorithm);
}
function getTransformStreamReadable(stream) {
return stream[_readable];
}
function getTransformStreamWritable(stream) {
return stream[_writable];
}
//
// Additions to the global object
//
......@@ -459,5 +482,10 @@
//
// Exports to Blink
//
binding.CreateTransformStream = CreateTransformStream;
Object.assign(binding, {
createTransformStreamSimple,
TransformStreamDefaultControllerEnqueue,
getTransformStreamReadable,
getTransformStreamWritable
});
});
// 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 "third_party/blink/renderer/bindings/core/v8/generated_code_helper.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_value.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_script_runner.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/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/v8_binding.h"
#include "third_party/blink/renderer/platform/heap/visitor.h"
namespace blink {
// Base class for FlushAlgorithm and TransformAlgorithm. Contains common
// construction code and members.
class TransformStream::Algorithm : public ScriptFunction {
public:
// This is templated just to avoid having two identical copies of the
// function.
template <typename T>
static v8::Local<v8::Function> Create(TransformStreamTransformer* transformer,
ScriptState* script_state,
ExceptionState& exception_state) {
auto* algorithm = new T(transformer, script_state, exception_state);
return algorithm->BindToV8Function();
}
void Trace(Visitor* visitor) override {
visitor->Trace(transformer_);
ScriptFunction::Trace(visitor);
}
protected:
Algorithm(TransformStreamTransformer* transformer,
ScriptState* script_state,
ExceptionState& exception_state)
: ScriptFunction(script_state),
transformer_(transformer),
context_(exception_state.Context()),
interface_name_(exception_state.InterfaceName()),
property_name_(exception_state.PropertyName()) {}
// AlgorithmScope holds the stack-allocated objects used by the CallRaw()
// methods for FlushAlgorithm and TransformAlgorithm.
class AlgorithmScope {
STACK_ALLOCATED();
public:
AlgorithmScope(Algorithm* owner,
const v8::FunctionCallbackInfo<v8::Value>& info,
v8::Local<v8::Value> controller)
: controller_(owner->GetScriptState(), controller),
exception_state_(owner->GetScriptState()->GetIsolate(),
owner->context_,
owner->interface_name_,
owner->property_name_),
reject_promise_scope_(info, exception_state_) {}
TransformStreamDefaultController* GetController() { return &controller_; }
ExceptionState* GetExceptionState() { return &exception_state_; }
private:
TransformStreamDefaultController controller_;
ExceptionState exception_state_;
ExceptionToRejectPromiseScope reject_promise_scope_;
};
Member<TransformStreamTransformer> transformer_;
const ExceptionState::ContextType context_;
const char* const interface_name_;
const char* const property_name_;
};
class TransformStream::FlushAlgorithm : public TransformStream::Algorithm {
protected:
using Algorithm::Algorithm;
private:
void CallRaw(const v8::FunctionCallbackInfo<v8::Value>& info) override {
DCHECK_EQ(info.Length(), 1);
AlgorithmScope algorithm_scope(this, info, info[0]);
ExceptionState& exception_state = *algorithm_scope.GetExceptionState();
transformer_->Flush(algorithm_scope.GetController(), exception_state);
if (exception_state.HadException())
return;
V8SetReturnValue(info,
ScriptPromise::CastUndefined(GetScriptState()).V8Value());
}
};
class TransformStream::TransformAlgorithm : public TransformStream::Algorithm {
protected:
using Algorithm::Algorithm;
private:
void CallRaw(const v8::FunctionCallbackInfo<v8::Value>& info) override {
DCHECK_EQ(info.Length(), 2);
AlgorithmScope algorithm_scope(this, info, info[1]);
ExceptionState& exception_state = *algorithm_scope.GetExceptionState();
transformer_->Transform(info[0], algorithm_scope.GetController(),
exception_state);
if (exception_state.HadException())
return;
V8SetReturnValue(info,
ScriptPromise::CastUndefined(GetScriptState()).V8Value());
}
};
TransformStream::TransformStream() = default;
TransformStream::~TransformStream() = default;
void TransformStream::Init(TransformStreamTransformer* transformer,
ScriptState* script_state,
ExceptionState& exception_state) {
auto transform_algorithm = Algorithm::Create<TransformAlgorithm>(
transformer, script_state, exception_state);
auto flush_algorithm = Algorithm::Create<FlushAlgorithm>(
transformer, script_state, exception_state);
v8::Local<v8::Value> args[] = {transform_algorithm, flush_algorithm};
v8::TryCatch block(script_state->GetIsolate());
v8::Local<v8::Value> stream;
if (!V8ScriptRunner::CallExtra(script_state, "createTransformStreamSimple",
args)
.ToLocal(&stream)) {
DCHECK(block.HasCaught());
exception_state.RethrowV8Exception(block.Exception());
return;
}
DCHECK(!block.HasCaught());
DCHECK(stream->IsObject());
stream_.Set(script_state->GetIsolate(), stream);
}
ScriptValue TransformStream::Readable(ScriptState* script_state,
ExceptionState& exception_state) const {
return Accessor("getTransformStreamReadable", script_state, exception_state);
}
ScriptValue TransformStream::Writable(ScriptState* script_state,
ExceptionState& exception_state) const {
return Accessor("getTransformStreamWritable", script_state, exception_state);
}
void TransformStream::Trace(Visitor* visitor) {
visitor->Trace(stream_);
}
ScriptValue TransformStream::Accessor(const char* accessor_function_name,
ScriptState* script_state,
ExceptionState& exception_state) const {
v8::Local<v8::Value> result;
v8::Local<v8::Value> args[] = {stream_.NewLocal(script_state->GetIsolate())};
DCHECK(args[0]->IsObject());
v8::TryCatch block(script_state->GetIsolate());
if (!V8ScriptRunner::CallExtra(script_state, accessor_function_name, args)
.ToLocal(&result)) {
DCHECK(block.HasCaught());
exception_state.RethrowV8Exception(block.Exception());
return ScriptValue();
}
DCHECK(!block.HasCaught());
return ScriptValue(script_state, result);
}
} // namespace blink
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_H_
#include "base/macros.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/bindings/trace_wrapper_v8_reference.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "v8/include/v8.h"
namespace blink {
class ExceptionState;
class ScriptState;
class ScriptValue;
class TransformStreamTransformer;
class Visitor;
// Creates and wraps a JavaScript TransformStream object with a transformation
// defined in C++. Provides access to the readable and writable streams.
//
// On-heap references to this class must always be via a TraceWrapperMember, and
// must always have an ancestor in the V8 heap, or |stream_| will be lost.
//
// To ensure that the JS TransformStream is always referenced, this class uses
// two-stage construction. After calling the constructor, store the reference
// in a TraceWrapperMember before calling Init(). Init() must always be called
// before using the instance.
class CORE_EXPORT TransformStream final
: public GarbageCollectedFinalized<TransformStream> {
public:
TransformStream();
~TransformStream();
// If HadException is true on return, the object is invalid and should be
// destroyed.
void Init(TransformStreamTransformer*, ScriptState*, ExceptionState&);
ScriptValue Readable(ScriptState*, ExceptionState&) const;
ScriptValue Writable(ScriptState*, ExceptionState&) const;
void Trace(Visitor*);
private:
// These are class-scoped to avoid name clashes in jumbo builds.
class Algorithm;
class FlushAlgorithm;
class TransformAlgorithm;
// Common implementation for Readable() and Writable() accessors.
ScriptValue Accessor(const char* accessor_function_name,
ScriptState*,
ExceptionState&) const;
TraceWrapperV8Reference<v8::Value> stream_;
DISALLOW_COPY_AND_ASSIGN(TransformStream);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_H_
// 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_default_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_script_runner.h"
namespace blink {
TransformStreamDefaultController::TransformStreamDefaultController(
ScriptState* script_state,
v8::Local<v8::Value> controller)
: script_state_(script_state), controller_(controller) {
DCHECK(controller->IsObject());
}
void TransformStreamDefaultController::Enqueue(
v8::Local<v8::Value> chunk,
ExceptionState& exception_state) {
DCHECK(controller_->IsObject());
v8::Local<v8::Value> args[] = {controller_, chunk};
v8::TryCatch block(script_state_->GetIsolate());
V8ScriptRunner::CallExtra(script_state_,
"TransformStreamDefaultControllerEnqueue", args);
if (block.HasCaught()) {
exception_state.RethrowV8Exception(block.Exception());
return;
}
}
} // namespace blink
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_DEFAULT_CONTROLLER_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_DEFAULT_CONTROLLER_H_
#include "base/macros.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/heap/member.h"
#include "v8/include/v8.h"
namespace blink {
// Thin wrapper for the JavaScript TransformStreamDefaultController object. The
// API mimics the JavaScript API
// https://streams.spec.whatwg.org/#ts-default-controller-class but unneeded
// parts have not been implemented.
class CORE_EXPORT TransformStreamDefaultController final {
STACK_ALLOCATED();
public:
TransformStreamDefaultController(ScriptState*,
v8::Local<v8::Value> controller);
void Enqueue(v8::Local<v8::Value> chunk, ExceptionState&);
private:
Member<ScriptState> script_state_;
v8::Local<v8::Value> controller_;
DISALLOW_COPY_AND_ASSIGN(TransformStreamDefaultController);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_DEFAULT_CONTROLLER_H_
// 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_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_iterator_result_value.h"
#include "third_party/blink/renderer/core/streams/readable_stream_operations.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/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/core/testing/garbage_collected_script_wrappable.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/trace_wrapper_member.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::Mock;
class TransformStreamTest : public ::testing::Test {
public:
void TearDown() override {
if (holder_)
ClearHolder();
}
void Init(TransformStreamTransformer* transformer,
ScriptState* script_state,
ExceptionState& exception_state) {
holder_ = new Holder(script_state);
holder_->Stream()->Init(transformer, script_state, exception_state);
}
TransformStream* Stream() const { return holder_->Stream(); }
// 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();
ScriptValue readable =
Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
ScriptValue writable =
Stream()->Writable(script_state, ASSERT_NO_EXCEPTION);
v8::Local<v8::Object> global = script_state->GetContext()->Global();
EXPECT_TRUE(global
->Set(scope.GetContext(),
V8String(scope.GetIsolate(), "readable"),
readable.V8Value())
.IsJust());
EXPECT_TRUE(global
->Set(scope.GetContext(),
V8String(scope.GetIsolate(), "writable"),
writable.V8Value())
.IsJust());
}
void ClearHolder() {
holder_->Destroy();
holder_ = nullptr;
}
private:
// In normal use, TransformStream will be referenced by a ScriptWrappable and
// so will be visible to the V8 garbage collector via wrapper tracing. For
// testing purposes we need a dummy ScriptWrappable object to do the same
// thing.
// TODO(ricea): Remove this once unified GC has replaced wrapper tracing.
class Holder : public GarbageCollectedScriptWrappable {
public:
explicit Holder(ScriptState* script_state)
: GarbageCollectedScriptWrappable("Holder"),
this_as_v8_value_(
ScriptValue(script_state, ToV8(this, script_state))),
stream_(new TransformStream()) {}
// Destroy() must be called to break the reference cycle.
void Destroy() {
this_as_v8_value_.Clear();
stream_ = nullptr;
}
TransformStream* Stream() const { return stream_.Get(); }
void Trace(Visitor* visitor) override {
visitor->Trace(stream_);
GarbageCollectedScriptWrappable::Trace(visitor);
}
private:
// Self-reference to keep this object referenced from V8.
ScriptValue this_as_v8_value_;
TraceWrapperMember<TransformStream> stream_;
};
Persistent<Holder> holder_;
};
class IdentityTransformer final : public TransformStreamTransformer {
public:
void Transform(v8::Local<v8::Value> chunk,
TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
controller->Enqueue(chunk, exception_state);
}
void Flush(TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {}
};
class MockTransformStreamTransformer : public TransformStreamTransformer {
public:
MOCK_METHOD3(Transform,
void(v8::Local<v8::Value> chunk,
TransformStreamDefaultController*,
ExceptionState&));
MOCK_METHOD2(Flush, void(TransformStreamDefaultController*, ExceptionState&));
};
// If this doesn't work then nothing else will.
TEST_F(TransformStreamTest, Construct) {
V8TestingScope scope;
Init(new IdentityTransformer(), scope.GetScriptState(), ASSERT_NO_EXCEPTION);
EXPECT_TRUE(Stream());
}
TEST_F(TransformStreamTest, Accessors) {
V8TestingScope scope;
Init(new IdentityTransformer(), scope.GetScriptState(), ASSERT_NO_EXCEPTION);
ScriptValue readable =
Stream()->Readable(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
ScriptValue writable =
Stream()->Writable(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
EXPECT_TRUE(readable.IsObject());
EXPECT_TRUE(writable.IsObject());
EXPECT_TRUE(ReadableStreamOperations::IsReadableStream(
scope.GetScriptState(), readable, ASSERT_NO_EXCEPTION)
.value_or(false));
// TODO(ricea): Check writable too once we have a wrapper for
// IsWritableStream().
}
TEST_F(TransformStreamTest, TransformIsCalled) {
V8TestingScope scope;
auto* mock = new ::testing::StrictMock<MockTransformStreamTransformer>();
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(_, _, _));
// 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 = new ::testing::StrictMock<MockTransformStreamTransformer>();
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(_, _));
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
Mock::VerifyAndClear(mock);
Mock::AllowLeak(mock);
}
class ExpectNotReached : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state) {
auto* self = new ExpectNotReached(script_state);
return self->BindToV8Function();
}
private:
explicit ExpectNotReached(ScriptState* script_state)
: ScriptFunction(script_state) {}
ScriptValue Call(ScriptValue) override {
ADD_FAILURE() << "ExpectNotReached was reached";
return ScriptValue();
}
};
// Fails the test if the iterator passed to the function does not have a value
// of exactly |expected|.
class ExpectChunkIsString : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state,
const String& expected,
bool* called) {
auto* self = new ExpectChunkIsString(script_state, expected, called);
return self->BindToV8Function();
}
private:
ExpectChunkIsString(ScriptState* script_state,
const String& expected,
bool* called)
: ScriptFunction(script_state), expected_(expected), called_(called) {}
ScriptValue Call(ScriptValue value) override {
*called_ = true;
if (!value.IsObject()) {
ADD_FAILURE() << "iterator must be an object";
return ScriptValue();
}
bool done = false;
auto* script_state = GetScriptState();
auto chunk = V8UnpackIteratorResult(
script_state,
value.V8Value()->ToObject(script_state->GetContext()).ToLocalChecked(),
&done);
EXPECT_FALSE(done);
EXPECT_FALSE(chunk.IsEmpty());
EXPECT_EQ(ToCoreStringWithUndefinedOrNullCheck(chunk.ToLocalChecked()),
expected_);
return ScriptValue();
}
String expected_;
bool* called_;
};
class ExpectTypeError : public ScriptFunction {
public:
static v8::Local<v8::Function> Create(ScriptState* script_state,
const String& message,
bool* called) {
auto* self = new ExpectTypeError(script_state, message, called);
return self->BindToV8Function();
}
private:
ExpectTypeError(ScriptState* script_state,
const String& message,
bool* called)
: ScriptFunction(script_state), message_(message), called_(called) {}
ScriptValue Call(ScriptValue value) override {
*called_ = true;
EXPECT_TRUE(IsTypeError(GetScriptState(), value, message_));
return ScriptValue();
}
static 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;
return Has(script_state, object, "name", "TypeError") &&
Has(script_state, object, "message", message);
}
static bool Has(ScriptState* script_state,
v8::Local<v8::Object> object,
const String& key,
const String& value) {
auto context = script_state->GetContext();
auto* isolate = script_state->GetIsolate();
v8::Local<v8::Value> actual;
return object->Get(context, V8AtomicString(isolate, key))
.ToLocal(&actual) &&
ToCoreStringWithUndefinedOrNullCheck(actual) == value;
}
String message_;
bool* called_;
};
TEST_F(TransformStreamTest, EnqueueFromTransform) {
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(new IdentityTransformer(), script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.write('a');\n");
ScriptValue readable = Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
ScriptValue reader = ReadableStreamOperations::GetReader(
script_state, readable, ASSERT_NO_EXCEPTION);
bool chunk_seen = false;
ReadableStreamOperations::DefaultReaderRead(script_state, reader)
.Then(ExpectChunkIsString::Create(script_state, "a", &chunk_seen),
ExpectNotReached::Create(script_state));
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(chunk_seen);
}
TEST_F(TransformStreamTest, EnqueueFromFlush) {
class EnqueueFromFlushTransformer : public TransformStreamTransformer {
public:
EnqueueFromFlushTransformer(v8::Local<v8::Object> global,
v8::Isolate* isolate)
: global_(global), isolate_(isolate) {}
void Transform(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState&) override {}
void Flush(TransformStreamDefaultController* controller,
ExceptionState& exception_state) override {
controller->Enqueue(ToV8("a", global_, isolate_), exception_state);
}
private:
v8::Local<v8::Object> global_;
v8::Isolate* isolate_;
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(new EnqueueFromFlushTransformer(scope.GetContext()->Global(),
scope.GetIsolate()),
script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
ScriptValue readable = Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
ScriptValue reader = ReadableStreamOperations::GetReader(
script_state, readable, ASSERT_NO_EXCEPTION);
bool chunkSeen = false;
ReadableStreamOperations::DefaultReaderRead(script_state, reader)
.Then(ExpectChunkIsString::Create(script_state, "a", &chunkSeen),
ExpectNotReached::Create(script_state));
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(chunkSeen);
}
// TODO(ricea): Re-enable this test when throwing from Transform() works
// properly. See https://github.com/whatwg/streams/issues/946.
TEST_F(TransformStreamTest, DISABLED_ThrowFromTransform) {
static constexpr char kMessage[] = "errorInTransform";
class ThrowFromTransformTransformer : public TransformStreamTransformer {
public:
void Transform(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState& exception_state) override {
exception_state.ThrowTypeError(kMessage);
}
void Flush(TransformStreamDefaultController*, ExceptionState&) override {}
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(new ThrowFromTransformTransformer(), script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.write('a');\n");
ScriptValue readable = Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
ScriptValue reader = ReadableStreamOperations::GetReader(
script_state, readable, ASSERT_NO_EXCEPTION);
bool readableTypeErrorThrown = false;
bool writableTypeErrorThrown = false;
ReadableStreamOperations::DefaultReaderRead(script_state, reader)
.Then(ExpectNotReached::Create(script_state),
ExpectTypeError::Create(script_state, kMessage,
&readableTypeErrorThrown));
ScriptPromise::Cast(script_state, promise)
.Then(ExpectNotReached::Create(script_state),
ExpectTypeError::Create(script_state, kMessage,
&writableTypeErrorThrown));
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(readableTypeErrorThrown);
EXPECT_TRUE(writableTypeErrorThrown);
}
TEST_F(TransformStreamTest, ThrowFromFlush) {
static constexpr char kMessage[] = "errorInFlush";
class ThrowFromFlushTransformer : public TransformStreamTransformer {
public:
void Transform(v8::Local<v8::Value>,
TransformStreamDefaultController*,
ExceptionState&) override {}
void Flush(TransformStreamDefaultController*,
ExceptionState& exception_state) override {
exception_state.ThrowTypeError(kMessage);
}
};
V8TestingScope scope;
auto* script_state = scope.GetScriptState();
Init(new ThrowFromFlushTransformer(), script_state, ASSERT_NO_EXCEPTION);
CopyReadableAndWritableToGlobal(scope);
ScriptValue promise =
EvalWithPrintingError(&scope,
"const writer = writable.getWriter();\n"
"writer.close();\n");
ScriptValue readable = Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
ScriptValue reader = ReadableStreamOperations::GetReader(
script_state, readable, ASSERT_NO_EXCEPTION);
bool readableTypeErrorThrown = false;
bool writableTypeErrorThrown = false;
ReadableStreamOperations::DefaultReaderRead(script_state, reader)
.Then(ExpectNotReached::Create(script_state),
ExpectTypeError::Create(script_state, kMessage,
&readableTypeErrorThrown));
ScriptPromise::Cast(script_state, promise)
.Then(ExpectNotReached::Create(script_state),
ExpectTypeError::Create(script_state, kMessage,
&writableTypeErrorThrown));
v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
EXPECT_TRUE(readableTypeErrorThrown);
EXPECT_TRUE(writableTypeErrorThrown);
}
// Verify that the JavaScript TransformStream object is kept alive by the C++
// TransformStream object.
TEST_F(TransformStreamTest, SurvivesGarbageCollectionWhenTraced) {
auto page_holder = DummyPageHolder::Create();
Persistent<ScriptState> script_state =
ToScriptStateForMainWorld(page_holder->GetDocument().GetFrame());
{
ScriptState::Scope scope(script_state);
Init(new IdentityTransformer(), script_state, ASSERT_NO_EXCEPTION);
}
Microtask::PerformCheckpoint(script_state->GetIsolate());
script_state->GetIsolate()->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
ScriptState::Scope scope(script_state);
ScriptValue readable = Stream()->Readable(script_state, ASSERT_NO_EXCEPTION);
EXPECT_TRUE(readable.IsObject());
EXPECT_TRUE(ReadableStreamOperations::IsReadableStream(script_state, readable,
ASSERT_NO_EXCEPTION)
.value_or(false));
}
// Verify that JS TransformStream is collected when it is not reachable from V8.
// TODO(ricea): Remove this test when unified garbage collection is introduced,
// as it will fail.
#if GTEST_HAS_DEATH_TEST
#define MAYBE_IsGarbageCollectedWhenNotTraced IsGarbageCollectedWhenNotTraced
#else
#define MAYBE_IsGarbageCollectedWhenNotTraced \
DISABLED_IsGarbageCollectedWhenNotTraced
#endif
TEST_F(TransformStreamTest, MAYBE_IsGarbageCollectedWhenNotTraced) {
auto page_holder = DummyPageHolder::Create();
Persistent<ScriptState> script_state =
ToScriptStateForMainWorld(page_holder->GetDocument().GetFrame());
{
ScriptState::Scope scope(script_state);
Init(new IdentityTransformer(), script_state, ASSERT_NO_EXCEPTION);
}
Persistent<TransformStream> stream = Stream();
ClearHolder();
Microtask::PerformCheckpoint(script_state->GetIsolate());
script_state->GetIsolate()->RequestGarbageCollectionForTesting(
v8::Isolate::kFullGarbageCollection);
ScriptState::Scope scope(script_state);
// This emits a warning that death tests are unsafe with threads, but it works
// anyway. The crash message depends on whether DCHECK is enabled or not, so
// the regex it is required to match is empty.
EXPECT_DEATH(stream->Readable(script_state, ASSERT_NO_EXCEPTION), "");
}
} // namespace
} // namespace blink
// 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.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_TRANSFORMER_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_TRANSFORMER_H_
#include "base/macros.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/heap/heap.h"
#include "v8/include/v8.h"
namespace blink {
class ExceptionState;
class TransformStreamDefaultController;
class Visitor;
// Interface to be implemented by C++ code that needs to create a
// TransformStream. Very similar to the JavaScript [Transformer
// API](https://streams.spec.whatwg.org/#transformer-api), but asynchronous
// transforms are not currently supported. Errors should be signalled by
// exceptions.
//
// An instance is stored in a JS object as a Persistent reference, so to avoid
// uncollectable cycles implementations must not directly or indirectly strongly
// reference any JS object.
class CORE_EXPORT TransformStreamTransformer
: public GarbageCollectedFinalized<TransformStreamTransformer> {
public:
TransformStreamTransformer() = default;
virtual ~TransformStreamTransformer() = default;
virtual void Transform(v8::Local<v8::Value> chunk,
TransformStreamDefaultController*,
ExceptionState&) = 0;
virtual void Flush(TransformStreamDefaultController*, ExceptionState&) = 0;
virtual void Trace(Visitor*) {}
private:
DISALLOW_COPY_AND_ASSIGN(TransformStreamTransformer);
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_STREAMS_TRANSFORM_STREAM_TRANSFORMER_H_
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment