blob: faff6a3ce00b9994cc84d6fae3ba521a8a43835a [file] [log] [blame]
// Copyright 2015 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 "core/streams/ReadableStreamOperations.h"
#include "bindings/core/v8/ExceptionState.h"
#include "bindings/core/v8/ScriptFunction.h"
#include "bindings/core/v8/ScriptState.h"
#include "bindings/core/v8/ScriptValue.h"
#include "bindings/core/v8/V8Binding.h"
#include "bindings/core/v8/V8BindingForTesting.h"
#include "bindings/core/v8/V8BindingMacros.h"
#include "bindings/core/v8/V8IteratorResultValue.h"
#include "bindings/core/v8/V8ThrowException.h"
#include "core/dom/Document.h"
#include "core/streams/ReadableStreamController.h"
#include "core/streams/UnderlyingSourceBase.h"
#include "platform/heap/Handle.h"
#include "testing/gtest/include/gtest/gtest.h"
#include <v8.h>
namespace blink {
namespace {
class NotReached : public ScriptFunction {
public:
static v8::Local<v8::Function> createFunction(ScriptState* scriptState)
{
NotReached* self = new NotReached(scriptState);
return self->bindToV8Function();
}
private:
explicit NotReached(ScriptState* scriptState)
: ScriptFunction(scriptState)
{
}
ScriptValue call(ScriptValue) override;
};
ScriptValue NotReached::call(ScriptValue)
{
EXPECT_TRUE(false) << "'Unreachable' code was reached";
return ScriptValue();
}
class Iteration final : public GarbageCollectedFinalized<Iteration> {
public:
Iteration()
: m_isSet(false)
, m_isDone(false)
, m_isValid(true) {}
void set(ScriptValue v)
{
ASSERT(!v.isEmpty());
m_isSet = true;
v8::TryCatch block(v.getScriptState()->isolate());
v8::Local<v8::Value> value;
v8::Local<v8::Value> item = v.v8Value();
if (!item->IsObject() || !v8Call(v8UnpackIteratorResult(v.getScriptState(), item.As<v8::Object>(), &m_isDone), value)) {
m_isValid = false;
return;
}
m_value = toCoreString(value->ToString());
}
bool isSet() const { return m_isSet; }
bool isDone() const { return m_isDone; }
bool isValid() const { return m_isValid; }
const String& value() const { return m_value; }
DEFINE_INLINE_TRACE() {}
private:
bool m_isSet;
bool m_isDone;
bool m_isValid;
String m_value;
};
class Function : public ScriptFunction {
public:
static v8::Local<v8::Function> createFunction(ScriptState* scriptState, Iteration* iteration)
{
Function* self = new Function(scriptState, iteration);
return self->bindToV8Function();
}
DEFINE_INLINE_VIRTUAL_TRACE()
{
visitor->trace(m_iteration);
ScriptFunction::trace(visitor);
}
private:
Function(ScriptState* scriptState, Iteration* iteration)
: ScriptFunction(scriptState)
, m_iteration(iteration)
{
}
ScriptValue call(ScriptValue value) override
{
m_iteration->set(value);
return value;
}
Member<Iteration> m_iteration;
};
class TestUnderlyingSource final : public UnderlyingSourceBase {
public:
explicit TestUnderlyingSource(ScriptState* scriptState)
: UnderlyingSourceBase(scriptState)
{
}
// Just expose the controller methods for easy testing
void enqueue(ScriptValue value) { controller()->enqueue(value); }
void close() { controller()->close(); }
void error(ScriptValue value) { controller()->error(value); }
double desiredSize() { return controller()->desiredSize(); }
};
class TryCatchScope {
public:
explicit TryCatchScope(v8::Isolate* isolate)
: m_isolate(isolate)
, m_trycatch(isolate)
{
}
~TryCatchScope()
{
v8::MicrotasksScope::PerformCheckpoint(m_isolate);
EXPECT_FALSE(m_trycatch.HasCaught());
}
private:
v8::Isolate* m_isolate;
v8::TryCatch m_trycatch;
};
ScriptValue eval(V8TestingScope* scope, const char* s)
{
v8::Local<v8::String> source;
v8::Local<v8::Script> script;
v8::MicrotasksScope microtasks(scope->isolate(), v8::MicrotasksScope::kDoNotRunMicrotasks);
if (!v8Call(v8::String::NewFromUtf8(scope->isolate(), s, v8::NewStringType::kNormal), source)) {
ADD_FAILURE();
return ScriptValue();
}
if (!v8Call(v8::Script::Compile(scope->context(), source), script)) {
ADD_FAILURE() << "Compilation fails";
return ScriptValue();
}
return ScriptValue(scope->getScriptState(), script->Run(scope->context()));
}
ScriptValue evalWithPrintingError(V8TestingScope* scope, const char* s)
{
v8::TryCatch block(scope->isolate());
ScriptValue r = eval(scope, s);
if (block.HasCaught()) {
ADD_FAILURE() << toCoreString(block.Exception()->ToString(scope->isolate())).utf8().data();
block.ReThrow();
}
return r;
}
TEST(ReadableStreamOperationsTest, IsReadableStream)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
EXPECT_FALSE(ReadableStreamOperations::isReadableStream(scope.getScriptState(), ScriptValue(scope.getScriptState(), v8::Undefined(scope.isolate()))));
EXPECT_FALSE(ReadableStreamOperations::isReadableStream(scope.getScriptState(), ScriptValue::createNull(scope.getScriptState())));
EXPECT_FALSE(ReadableStreamOperations::isReadableStream(scope.getScriptState(), ScriptValue(scope.getScriptState(), v8::Object::New(scope.isolate()))));
ScriptValue stream = evalWithPrintingError(&scope, "new ReadableStream()");
EXPECT_FALSE(stream.isEmpty());
EXPECT_TRUE(ReadableStreamOperations::isReadableStream(scope.getScriptState(), stream));
}
TEST(ReadableStreamOperationsTest, IsReadableStreamDefaultReaderInvalid)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
EXPECT_FALSE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), ScriptValue(scope.getScriptState(), v8::Undefined(scope.isolate()))));
EXPECT_FALSE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), ScriptValue::createNull(scope.getScriptState())));
EXPECT_FALSE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), ScriptValue(scope.getScriptState(), v8::Object::New(scope.isolate()))));
ScriptValue stream = evalWithPrintingError(&scope, "new ReadableStream()");
EXPECT_FALSE(stream.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), stream));
}
TEST(ReadableStreamOperationsTest, GetReader)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue stream = evalWithPrintingError(&scope, "new ReadableStream()");
EXPECT_FALSE(stream.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isLocked(scope.getScriptState(), stream));
ScriptValue reader;
{
TrackExceptionState es;
reader = ReadableStreamOperations::getReader(scope.getScriptState(), stream, es);
ASSERT_FALSE(es.hadException());
}
EXPECT_TRUE(ReadableStreamOperations::isLocked(scope.getScriptState(), stream));
ASSERT_FALSE(reader.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isReadableStream(scope.getScriptState(), reader));
EXPECT_TRUE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), reader));
// Already locked!
{
TrackExceptionState es;
reader = ReadableStreamOperations::getReader(scope.getScriptState(), stream, es);
ASSERT_TRUE(es.hadException());
}
ASSERT_TRUE(reader.isEmpty());
}
TEST(ReadableStreamOperationsTest, IsDisturbed)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue stream = evalWithPrintingError(&scope, "stream = new ReadableStream()");
EXPECT_FALSE(stream.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isDisturbed(scope.getScriptState(), stream));
ASSERT_FALSE(evalWithPrintingError(&scope, "stream.cancel()").isEmpty());
EXPECT_TRUE(ReadableStreamOperations::isDisturbed(scope.getScriptState(), stream));
}
TEST(ReadableStreamOperationsTest, Read)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue reader = evalWithPrintingError(&scope,
"var controller;"
"function start(c) { controller = c; }"
"new ReadableStream({start}).getReader()");
EXPECT_FALSE(reader.isEmpty());
ASSERT_TRUE(ReadableStreamOperations::isReadableStreamDefaultReader(scope.getScriptState(), reader));
Iteration* it1 = new Iteration();
Iteration* it2 = new Iteration();
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader).then(
Function::createFunction(scope.getScriptState(), it1),
NotReached::createFunction(scope.getScriptState()));
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader).then(
Function::createFunction(scope.getScriptState(), it2),
NotReached::createFunction(scope.getScriptState()));
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_FALSE(it1->isSet());
EXPECT_FALSE(it2->isSet());
ASSERT_FALSE(evalWithPrintingError(&scope, "controller.enqueue('hello')").isEmpty());
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_TRUE(it1->isSet());
EXPECT_TRUE(it1->isValid());
EXPECT_FALSE(it1->isDone());
EXPECT_EQ("hello", it1->value());
EXPECT_FALSE(it2->isSet());
ASSERT_FALSE(evalWithPrintingError(&scope, "controller.close()").isEmpty());
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_TRUE(it1->isSet());
EXPECT_TRUE(it1->isValid());
EXPECT_FALSE(it1->isDone());
EXPECT_EQ("hello", it1->value());
EXPECT_TRUE(it2->isSet());
EXPECT_TRUE(it2->isValid());
EXPECT_TRUE(it2->isDone());
}
TEST(ReadableStreamOperationsTest, CreateReadableStreamWithCustomUnderlyingSourceAndStrategy)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
auto underlyingSource = new TestUnderlyingSource(scope.getScriptState());
ScriptValue strategy = ReadableStreamOperations::createCountQueuingStrategy(scope.getScriptState(), 10);
ASSERT_FALSE(strategy.isEmpty());
ScriptValue stream = ReadableStreamOperations::createReadableStream(scope.getScriptState(), underlyingSource, strategy);
ASSERT_FALSE(stream.isEmpty());
EXPECT_EQ(10, underlyingSource->desiredSize());
underlyingSource->enqueue(ScriptValue::from(scope.getScriptState(), "a"));
EXPECT_EQ(9, underlyingSource->desiredSize());
underlyingSource->enqueue(ScriptValue::from(scope.getScriptState(), "b"));
EXPECT_EQ(8, underlyingSource->desiredSize());
ScriptValue reader;
{
TrackExceptionState es;
reader = ReadableStreamOperations::getReader(scope.getScriptState(), stream, es);
ASSERT_FALSE(es.hadException());
}
ASSERT_FALSE(reader.isEmpty());
Iteration* it1 = new Iteration();
Iteration* it2 = new Iteration();
Iteration* it3 = new Iteration();
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader).then(Function::createFunction(scope.getScriptState(), it1), NotReached::createFunction(scope.getScriptState()));
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader).then(Function::createFunction(scope.getScriptState(), it2), NotReached::createFunction(scope.getScriptState()));
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader).then(Function::createFunction(scope.getScriptState(), it3), NotReached::createFunction(scope.getScriptState()));
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_EQ(10, underlyingSource->desiredSize());
EXPECT_TRUE(it1->isSet());
EXPECT_TRUE(it1->isValid());
EXPECT_FALSE(it1->isDone());
EXPECT_EQ("a", it1->value());
EXPECT_TRUE(it2->isSet());
EXPECT_TRUE(it2->isValid());
EXPECT_FALSE(it2->isDone());
EXPECT_EQ("b", it2->value());
EXPECT_FALSE(it3->isSet());
underlyingSource->close();
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_TRUE(it3->isSet());
EXPECT_TRUE(it3->isValid());
EXPECT_TRUE(it3->isDone());
}
TEST(ReadableStreamOperationsTest, UnderlyingSourceShouldHavePendingActivityWhenLockedAndControllerIsActive)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
auto underlyingSource = new TestUnderlyingSource(scope.getScriptState());
ScriptValue strategy = ReadableStreamOperations::createCountQueuingStrategy(scope.getScriptState(), 10);
ASSERT_FALSE(strategy.isEmpty());
ScriptValue stream = ReadableStreamOperations::createReadableStream(scope.getScriptState(), underlyingSource, strategy);
ASSERT_FALSE(stream.isEmpty());
v8::Local<v8::Object> global = scope.getScriptState()->context()->Global();
ASSERT_TRUE(global->Set(scope.context(), v8String(scope.isolate(), "stream"), stream.v8Value()).IsJust());
EXPECT_FALSE(underlyingSource->hasPendingActivity());
evalWithPrintingError(&scope, "let reader = stream.getReader();");
EXPECT_TRUE(underlyingSource->hasPendingActivity());
evalWithPrintingError(&scope, "reader.releaseLock();");
EXPECT_FALSE(underlyingSource->hasPendingActivity());
evalWithPrintingError(&scope, "reader = stream.getReader();");
EXPECT_TRUE(underlyingSource->hasPendingActivity());
underlyingSource->enqueue(ScriptValue(scope.getScriptState(), v8::Undefined(scope.isolate())));
underlyingSource->close();
EXPECT_FALSE(underlyingSource->hasPendingActivity());
}
TEST(ReadableStreamOperationsTest, IsReadable)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue readable = evalWithPrintingError(&scope, "new ReadableStream()");
ScriptValue closed = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.close()})");
ScriptValue errored = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.error()})");
ASSERT_FALSE(readable.isEmpty());
ASSERT_FALSE(closed.isEmpty());
ASSERT_FALSE(errored.isEmpty());
EXPECT_TRUE(ReadableStreamOperations::isReadable(scope.getScriptState(), readable));
EXPECT_FALSE(ReadableStreamOperations::isReadable(scope.getScriptState(), closed));
EXPECT_FALSE(ReadableStreamOperations::isReadable(scope.getScriptState(), errored));
}
TEST(ReadableStreamOperationsTest, IsClosed)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue readable = evalWithPrintingError(&scope, "new ReadableStream()");
ScriptValue closed = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.close()})");
ScriptValue errored = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.error()})");
ASSERT_FALSE(readable.isEmpty());
ASSERT_FALSE(closed.isEmpty());
ASSERT_FALSE(errored.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isClosed(scope.getScriptState(), readable));
EXPECT_TRUE(ReadableStreamOperations::isClosed(scope.getScriptState(), closed));
EXPECT_FALSE(ReadableStreamOperations::isClosed(scope.getScriptState(), errored));
}
TEST(ReadableStreamOperationsTest, IsErrored)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue readable = evalWithPrintingError(&scope, "new ReadableStream()");
ScriptValue closed = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.close()})");
ScriptValue errored = evalWithPrintingError(&scope, "new ReadableStream({start: c => c.error()})");
ASSERT_FALSE(readable.isEmpty());
ASSERT_FALSE(closed.isEmpty());
ASSERT_FALSE(errored.isEmpty());
EXPECT_FALSE(ReadableStreamOperations::isErrored(scope.getScriptState(), readable));
EXPECT_FALSE(ReadableStreamOperations::isErrored(scope.getScriptState(), closed));
EXPECT_TRUE(ReadableStreamOperations::isErrored(scope.getScriptState(), errored));
}
TEST(ReadableStreamOperationsTest, Tee)
{
V8TestingScope scope;
TryCatchScope tryCatchScope(scope.isolate());
ScriptValue original = evalWithPrintingError(&scope,
"var controller;"
"new ReadableStream({start: c => controller = c})");
ASSERT_FALSE(original.isEmpty());
ScriptValue new1, new2;
ReadableStreamOperations::tee(scope.getScriptState(), original, &new1, &new2);
NonThrowableExceptionState ec;
ScriptValue reader1 = ReadableStreamOperations::getReader(scope.getScriptState(), new1, ec);
ScriptValue reader2 = ReadableStreamOperations::getReader(scope.getScriptState(), new2, ec);
Iteration* it1 = new Iteration();
Iteration* it2 = new Iteration();
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader1).then(
Function::createFunction(scope.getScriptState(), it1),
NotReached::createFunction(scope.getScriptState()));
ReadableStreamOperations::defaultReaderRead(scope.getScriptState(), reader2).then(
Function::createFunction(scope.getScriptState(), it2),
NotReached::createFunction(scope.getScriptState()));
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_FALSE(it1->isSet());
EXPECT_FALSE(it2->isSet());
ASSERT_FALSE(evalWithPrintingError(&scope, "controller.enqueue('hello')").isEmpty());
v8::MicrotasksScope::PerformCheckpoint(scope.isolate());
EXPECT_TRUE(it1->isSet());
EXPECT_TRUE(it1->isValid());
EXPECT_FALSE(it1->isDone());
EXPECT_EQ("hello", it1->value());
EXPECT_TRUE(it2->isSet());
EXPECT_TRUE(it2->isValid());
EXPECT_FALSE(it2->isDone());
EXPECT_EQ("hello", it2->value());
}
} // namespace
} // namespace blink