blob: a5fa40773339d600c9344cecebac901c47bb9c26 [file] [log] [blame]
// Copyright 2014 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/bindings/core/v8/script_streamer.h"
#include <memory>
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/platform.h"
#include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h"
#include "third_party/blink/public/platform/web_url_loader_mock_factory.h"
#include "third_party/blink/renderer/bindings/core/v8/script_source_code.h"
#include "third_party/blink/renderer/bindings/core/v8/script_streamer_thread.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_script_runner.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/script/classic_pending_script.h"
#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/script/mock_script_element_base.h"
#include "third_party/blink/renderer/core/testing/dummy_page_holder.h"
#include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h"
#include "third_party/blink/renderer/platform/heap/handle.h"
#include "third_party/blink/renderer/platform/loader/fetch/resource_loader.h"
#include "third_party/blink/renderer/platform/loader/fetch/script_fetch_options.h"
#include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h"
#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
#include "third_party/blink/renderer/platform/wtf/text/text_encoding.h"
#include "v8/include/v8.h"
namespace blink {
namespace {
class ScriptStreamingTest : public testing::Test {
public:
ScriptStreamingTest()
: loading_task_runner_(scheduler::GetSingleThreadTaskRunnerForTesting()),
dummy_page_holder_(DummyPageHolder::Create(IntSize(800, 600))) {
dummy_page_holder_->GetPage().GetSettings().SetScriptEnabled(true);
MockScriptElementBase* element = MockScriptElementBase::Create();
// Basically we are not interested in ScriptElementBase* calls, just making
// the method(s) to return default values.
EXPECT_CALL(*element, IntegrityAttributeValue())
.WillRepeatedly(testing::Return(String()));
EXPECT_CALL(*element, GetDocument())
.WillRepeatedly(testing::ReturnRef(dummy_page_holder_->GetDocument()));
EXPECT_CALL(*element, Loader()).WillRepeatedly(testing::Return(nullptr));
KURL url("http://www.streaming-test.com/");
Platform::Current()->GetURLLoaderMockFactory()->RegisterURL(
url, WrappedResourceResponse(ResourceResponse()), "");
pending_script_ = ClassicPendingScript::Fetch(
url, dummy_page_holder_->GetDocument(), ScriptFetchOptions(),
UTF8Encoding(), element, FetchParameters::kNoDefer);
ScriptStreamer::SetSmallScriptThresholdForTesting(0);
Platform::Current()->GetURLLoaderMockFactory()->UnregisterURL(url);
ResourceResponse response(url);
response.SetHTTPStatusCode(200);
GetResource()->SetResponse(response);
}
~ScriptStreamingTest() override {
if (pending_script_)
pending_script_->Dispose();
}
ClassicPendingScript* GetPendingScript() const {
return pending_script_.Get();
}
Settings* GetSettings() const {
return &dummy_page_holder_->GetPage().GetSettings();
}
ScriptResource* GetResource() const {
return ToScriptResource(pending_script_->GetResource());
}
protected:
void AppendData(const char* data) {
GetResource()->AppendData(data, strlen(data));
// Yield control to the background thread, so that V8 gets a chance to
// process the data before the main thread adds more. Note that we
// cannot fully control in what kind of chunks the data is passed to V8
// (if V8 is not requesting more data between two appendData calls, it
// will get both chunks together).
test::YieldCurrentThread();
}
void AppendPadding() {
for (int i = 0; i < 10; ++i) {
AppendData(
" /* this is padding to make the script long enough, so "
"that V8's buffer gets filled and it starts processing "
"the data */ ");
}
}
void Finish() {
GetResource()->Loader()->DidFinishLoading(0.0, 0, 0, 0, false);
GetResource()->SetStatus(ResourceStatus::kCached);
}
void ProcessTasksUntilStreamingComplete() {
while (ScriptStreamerThread::Shared()->IsRunningTask()) {
test::RunPendingTasks();
}
// Once more, because the "streaming complete" notification might only
// now be in the task queue.
test::RunPendingTasks();
}
scoped_refptr<base::SingleThreadTaskRunner> loading_task_runner_;
// The PendingScript where we stream from. These don't really
// fetch any data outside the test; the test controls the data by calling
// ScriptResource::AppendData.
Persistent<ClassicPendingScript> pending_script_;
std::unique_ptr<DummyPageHolder> dummy_page_holder_;
};
class TestPendingScriptClient final
: public GarbageCollectedFinalized<TestPendingScriptClient>,
public PendingScriptClient {
USING_GARBAGE_COLLECTED_MIXIN(TestPendingScriptClient);
public:
TestPendingScriptClient() : finished_(false) {}
void PendingScriptFinished(PendingScript*) override { finished_ = true; }
bool Finished() const { return finished_; }
private:
bool finished_;
};
TEST_F(ScriptStreamingTest, CompilingStreamedScript) {
// Test that we can successfully compile a streamed script.
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
AppendData("function foo() {");
AppendPadding();
AppendData("return 5; }");
AppendPadding();
AppendData("foo();");
EXPECT_FALSE(client->Finished());
Finish();
// Process tasks on the main thread until the streaming background thread
// has completed its tasks.
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8ScriptRunner::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8ScriptRunner::GetCompileOptions(kV8CacheOptionsDefault, source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code, kSharableCrossOrigin,
compile_options, no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
TEST_F(ScriptStreamingTest, CompilingStreamedScriptWithParseError) {
// Test that scripts with parse errors are handled properly. In those cases,
// the V8 side typically finished before loading finishes: make sure we
// handle it gracefully.
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
AppendData("function foo() {");
AppendData("this is the part which will be a parse error");
// V8 won't realize the parse error until it actually starts parsing the
// script, and this happens only when its buffer is filled.
AppendPadding();
EXPECT_FALSE(client->Finished());
// Force the V8 side to finish before the loading.
ProcessTasksUntilStreamingComplete();
EXPECT_FALSE(client->Finished());
Finish();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8ScriptRunner::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8ScriptRunner::GetCompileOptions(kV8CacheOptionsDefault, source_code);
EXPECT_FALSE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code, kSharableCrossOrigin,
compile_options, no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_TRUE(try_catch.HasCaught());
}
TEST_F(ScriptStreamingTest, CancellingStreaming) {
// Test that the upper layers (PendingScript and up) can be ramped down
// while streaming is ongoing, and ScriptStreamer handles it gracefully.
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
AppendData("function foo() {");
// In general, we cannot control what the background thread is doing
// (whether it's parsing or waiting for more data). In this test, we have
// given it so little data that it's surely waiting for more.
// Simulate cancelling the network load (e.g., because the user navigated
// away).
EXPECT_FALSE(client->Finished());
GetPendingScript()->Dispose();
pending_script_ = nullptr; // This will destroy m_resource.
// The V8 side will complete too. This should not crash. We don't receive
// any results from the streaming and the client doesn't get notified.
ProcessTasksUntilStreamingComplete();
EXPECT_FALSE(client->Finished());
}
TEST_F(ScriptStreamingTest, SuppressingStreaming) {
// If we notice during streaming that there is a code cache, streaming
// is suppressed (V8 doesn't parse while the script is loading), and the
// upper layer (ScriptResourceClient) should get a notification when the
// script is loaded.
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
AppendData("function foo() {");
AppendPadding();
SingleCachedMetadataHandler* cache_handler = GetResource()->CacheHandler();
EXPECT_TRUE(cache_handler);
cache_handler->SetCachedMetadata(
V8ScriptRunner::TagForCodeCache(cache_handler), "X", 1,
CachedMetadataHandler::kCacheLocally);
AppendPadding();
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
// ScriptSourceCode doesn't refer to the streamer, since we have suppressed
// the streaming and resumed the non-streaming code path for script
// compilation.
EXPECT_FALSE(source_code.Streamer());
}
TEST_F(ScriptStreamingTest, EmptyScripts) {
// Empty scripts should also be streamed properly, that is, the upper layer
// (ScriptResourceClient) should be notified when an empty script has been
// loaded.
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
// Finish the script without sending any data.
Finish();
// The finished notification should arrive immediately and not be cycled
// through a background thread.
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_FALSE(source_code.Streamer());
}
TEST_F(ScriptStreamingTest, SmallScripts) {
// Small scripts shouldn't be streamed.
V8TestingScope scope;
ScriptStreamer::SetSmallScriptThresholdForTesting(100);
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
AppendData("function foo() { }");
Finish();
// The finished notification should arrive immediately and not be cycled
// through a background thread.
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_FALSE(source_code.Streamer());
}
TEST_F(ScriptStreamingTest, ScriptsWithSmallFirstChunk) {
// If a script is long enough, if should be streamed, even if the first data
// chunk is small.
V8TestingScope scope;
ScriptStreamer::SetSmallScriptThresholdForTesting(100);
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
// This is the first data chunk which is small.
AppendData("function foo() { }");
AppendPadding();
AppendPadding();
AppendPadding();
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8ScriptRunner::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8ScriptRunner::GetCompileOptions(kV8CacheOptionsDefault, source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code, kSharableCrossOrigin,
compile_options, no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
TEST_F(ScriptStreamingTest, EncodingChanges) {
// It's possible that the encoding of the Resource changes after we start
// loading it.
V8TestingScope scope;
GetResource()->SetEncodingForTest("windows-1252");
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
GetResource()->SetEncodingForTest("UTF-8");
// \xec\x92\x81 are the raw bytes for \uc481.
AppendData(
"function foo() { var foob\xec\x92\x81r = 13; return foob\xec\x92\x81r; "
"} foo();");
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8ScriptRunner::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8ScriptRunner::GetCompileOptions(kV8CacheOptionsDefault, source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code, kSharableCrossOrigin,
compile_options, no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
TEST_F(ScriptStreamingTest, EncodingFromBOM) {
// Byte order marks should be removed before giving the data to V8. They
// will also affect encoding detection.
V8TestingScope scope;
// This encoding is wrong on purpose.
GetResource()->SetEncodingForTest("windows-1252");
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
// \xef\xbb\xbf is the UTF-8 byte order mark. \xec\x92\x81 are the raw bytes
// for \uc481.
AppendData(
"\xef\xbb\xbf function foo() { var foob\xec\x92\x81r = 13; return "
"foob\xec\x92\x81r; } foo();");
Finish();
ProcessTasksUntilStreamingComplete();
EXPECT_TRUE(client->Finished());
bool error_occurred = false;
ScriptSourceCode source_code = GetPendingScript()
->GetSource(NullURL(), error_occurred)
->GetScriptSourceCode();
EXPECT_FALSE(error_occurred);
EXPECT_TRUE(source_code.Streamer());
v8::TryCatch try_catch(scope.GetIsolate());
v8::Local<v8::Script> script;
v8::ScriptCompiler::CompileOptions compile_options;
V8ScriptRunner::ProduceCacheOptions produce_cache_options;
v8::ScriptCompiler::NoCacheReason no_cache_reason;
std::tie(compile_options, produce_cache_options, no_cache_reason) =
V8ScriptRunner::GetCompileOptions(kV8CacheOptionsDefault, source_code);
EXPECT_TRUE(V8ScriptRunner::CompileScript(
scope.GetScriptState(), source_code, kSharableCrossOrigin,
compile_options, no_cache_reason, ReferrerScriptInfo())
.ToLocal(&script));
EXPECT_FALSE(try_catch.HasCaught());
}
// A test for crbug.com/711703. Should not crash.
TEST_F(ScriptStreamingTest, GarbageCollectDuringStreaming) {
V8TestingScope scope;
ScriptStreamer::StartStreaming(
GetPendingScript(), ScriptStreamer::kParsingBlocking, GetSettings(),
scope.GetScriptState(), loading_task_runner_);
TestPendingScriptClient* client = new TestPendingScriptClient;
GetPendingScript()->WatchForLoad(client);
EXPECT_FALSE(client->Finished());
pending_script_ = nullptr;
ThreadState::Current()->CollectGarbage(
BlinkGC::kNoHeapPointersOnStack, BlinkGC::kAtomicMarking,
BlinkGC::kEagerSweeping, BlinkGC::kForcedGC);
}
} // namespace
} // namespace blink