1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker.h"
6
7 #include "testing/gtest/include/gtest/gtest.h"
8 #include "third_party/blink/public/platform/platform.h"
9 #include "third_party/blink/public/platform/scheduler/web_thread_scheduler.h"
10 #include "third_party/blink/public/platform/web_url_request.h"
11 #include "third_party/blink/renderer/bindings/core/v8/boxed_v8_module.h"
12 #include "third_party/blink/renderer/bindings/core/v8/module_record.h"
13 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
14 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
15 #include "third_party/blink/renderer/core/loader/modulescript/module_script_fetch_request.h"
16 #include "third_party/blink/renderer/core/loader/modulescript/module_tree_linker_registry.h"
17 #include "third_party/blink/renderer/core/script/js_module_script.h"
18 #include "third_party/blink/renderer/core/script/modulator.h"
19 #include "third_party/blink/renderer/core/testing/dummy_modulator.h"
20 #include "third_party/blink/renderer/core/testing/page_test_base.h"
21 #include "third_party/blink/renderer/platform/bindings/script_state.h"
22 #include "third_party/blink/renderer/platform/bindings/v8_throw_exception.h"
23 #include "third_party/blink/renderer/platform/heap/handle.h"
24 #include "third_party/blink/renderer/platform/loader/fetch/fetch_client_settings_object_snapshot.h"
25 #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
26 #include "third_party/blink/renderer/platform/weborigin/kurl.h"
27 #include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
28
29 namespace blink {
30
31 namespace {
32
33 class TestModuleTreeClient final : public ModuleTreeClient {
34 public:
35 TestModuleTreeClient() = default;
36
Trace(Visitor * visitor)37 void Trace(Visitor* visitor) override {
38 visitor->Trace(module_script_);
39 ModuleTreeClient::Trace(visitor);
40 }
41
NotifyModuleTreeLoadFinished(ModuleScript * module_script)42 void NotifyModuleTreeLoadFinished(ModuleScript* module_script) override {
43 was_notify_finished_ = true;
44 module_script_ = module_script;
45 }
46
WasNotifyFinished() const47 bool WasNotifyFinished() const { return was_notify_finished_; }
GetModuleScript()48 ModuleScript* GetModuleScript() { return module_script_; }
49
50 private:
51 bool was_notify_finished_ = false;
52 Member<ModuleScript> module_script_;
53 };
54
55 } // namespace
56
57 class ModuleTreeLinkerTestModulator final : public DummyModulator {
58 public:
ModuleTreeLinkerTestModulator(ScriptState * script_state)59 ModuleTreeLinkerTestModulator(ScriptState* script_state)
60 : script_state_(script_state) {}
61 ~ModuleTreeLinkerTestModulator() override = default;
62
63 void Trace(Visitor*) override;
64
65 enum class ResolveResult { kFailure, kSuccess };
66
67 // Resolve last |Modulator::FetchSingle()| call.
ResolveSingleModuleScriptFetch(const KURL & url,const Vector<String> & dependency_module_specifiers,bool parse_error=false)68 ModuleScript* ResolveSingleModuleScriptFetch(
69 const KURL& url,
70 const Vector<String>& dependency_module_specifiers,
71 bool parse_error = false) {
72 ScriptState::Scope scope(script_state_);
73
74 StringBuilder source_text;
75 for (const auto& specifier : dependency_module_specifiers) {
76 source_text.Append("import '");
77 source_text.Append(specifier);
78 source_text.Append("';\n");
79 }
80 source_text.Append("export default 'grapes';");
81
82 v8::Local<v8::Module> module_record = ModuleRecord::Compile(
83 script_state_->GetIsolate(), source_text.ToString(), url, url,
84 ScriptFetchOptions(), TextPosition::MinimumPosition(),
85 ASSERT_NO_EXCEPTION);
86 auto* module_script =
87 JSModuleScript::CreateForTest(this, module_record, url);
88
89 auto result_map = module_map_.insert(url, module_script);
90 EXPECT_TRUE(result_map.is_new_entry);
91
92 if (parse_error) {
93 v8::Local<v8::Value> error = V8ThrowException::CreateError(
94 script_state_->GetIsolate(), "Parse failure.");
95 module_script->SetParseErrorAndClearRecord(
96 ScriptValue(script_state_->GetIsolate(), error));
97 }
98
99 EXPECT_TRUE(pending_clients_.Contains(url));
100 pending_clients_.Take(url)->NotifyModuleLoadFinished(module_script);
101
102 return module_script;
103 }
104
ResolveDependentTreeFetch(const KURL & url,ResolveResult result)105 void ResolveDependentTreeFetch(const KURL& url, ResolveResult result) {
106 ResolveSingleModuleScriptFetch(url, Vector<String>(),
107 result == ResolveResult::kFailure);
108 }
109
SetInstantiateShouldFail(bool b)110 void SetInstantiateShouldFail(bool b) { instantiate_should_fail_ = b; }
111
HasInstantiated(ModuleScript * module_script) const112 bool HasInstantiated(ModuleScript* module_script) const {
113 v8::Isolate* isolate = script_state_->GetIsolate();
114 v8::HandleScope scope(isolate);
115
116 return instantiated_records_.Contains(MakeGarbageCollected<BoxedV8Module>(
117 isolate, module_script->V8Module()));
118 }
119
120 private:
121 // Implements Modulator:
122
GetScriptState()123 ScriptState* GetScriptState() override { return script_state_; }
124
ResolveModuleSpecifier(const String & module_request,const KURL & base_url,String * failure_reason)125 KURL ResolveModuleSpecifier(const String& module_request,
126 const KURL& base_url,
127 String* failure_reason) final {
128 return KURL(base_url, module_request);
129 }
ClearIsAcquiringImportMaps()130 void ClearIsAcquiringImportMaps() final {}
131
FetchSingle(const ModuleScriptFetchRequest & request,ResourceFetcher *,ModuleGraphLevel,ModuleScriptCustomFetchType,SingleModuleClient * client)132 void FetchSingle(const ModuleScriptFetchRequest& request,
133 ResourceFetcher*,
134 ModuleGraphLevel,
135 ModuleScriptCustomFetchType,
136 SingleModuleClient* client) override {
137 EXPECT_FALSE(pending_clients_.Contains(request.Url()));
138 pending_clients_.Set(request.Url(), client);
139 }
140
GetFetchedModuleScript(const KURL & url)141 ModuleScript* GetFetchedModuleScript(const KURL& url) override {
142 const auto& it = module_map_.find(url);
143 if (it == module_map_.end())
144 return nullptr;
145
146 return it->value;
147 }
148
InstantiateModule(v8::Local<v8::Module> record,const KURL & source_url)149 ScriptValue InstantiateModule(v8::Local<v8::Module> record,
150 const KURL& source_url) override {
151 if (instantiate_should_fail_) {
152 ScriptState::Scope scope(script_state_);
153 v8::Local<v8::Value> error = V8ThrowException::CreateError(
154 script_state_->GetIsolate(), "Instantiation failure.");
155 return ScriptValue(script_state_->GetIsolate(), error);
156 }
157 instantiated_records_.insert(MakeGarbageCollected<BoxedV8Module>(
158 script_state_->GetIsolate(), record));
159 return ScriptValue();
160 }
161
ModuleRequestsFromModuleRecord(v8::Local<v8::Module> module_record)162 Vector<ModuleRequest> ModuleRequestsFromModuleRecord(
163 v8::Local<v8::Module> module_record) override {
164 ScriptState::Scope scope(script_state_);
165 Vector<String> specifiers =
166 ModuleRecord::ModuleRequests(script_state_, module_record);
167 Vector<TextPosition> positions =
168 ModuleRecord::ModuleRequestPositions(script_state_, module_record);
169 DCHECK_EQ(specifiers.size(), positions.size());
170 Vector<ModuleRequest> requests;
171 requests.ReserveInitialCapacity(specifiers.size());
172 for (wtf_size_t i = 0; i < specifiers.size(); ++i) {
173 requests.emplace_back(specifiers[i], positions[i]);
174 }
175 return requests;
176 }
177
178 Member<ScriptState> script_state_;
179 HeapHashMap<KURL, Member<SingleModuleClient>> pending_clients_;
180 HeapHashMap<KURL, Member<ModuleScript>> module_map_;
181 HeapHashSet<Member<BoxedV8Module>> instantiated_records_;
182 bool instantiate_should_fail_ = false;
183 };
184
Trace(Visitor * visitor)185 void ModuleTreeLinkerTestModulator::Trace(Visitor* visitor) {
186 visitor->Trace(script_state_);
187 visitor->Trace(pending_clients_);
188 visitor->Trace(module_map_);
189 visitor->Trace(instantiated_records_);
190 DummyModulator::Trace(visitor);
191 }
192
193 class ModuleTreeLinkerTest : public PageTestBase {
194 DISALLOW_COPY_AND_ASSIGN(ModuleTreeLinkerTest);
195
196 public:
197 ModuleTreeLinkerTest() = default;
198 void SetUp() override;
199
GetModulator()200 ModuleTreeLinkerTestModulator* GetModulator() { return modulator_.Get(); }
201
202 protected:
203 Persistent<ModuleTreeLinkerTestModulator> modulator_;
204 };
205
SetUp()206 void ModuleTreeLinkerTest::SetUp() {
207 PageTestBase::SetUp(IntSize(500, 500));
208 ScriptState* script_state = ToScriptStateForMainWorld(&GetFrame());
209 modulator_ =
210 MakeGarbageCollected<ModuleTreeLinkerTestModulator>(script_state);
211 }
212
TEST_F(ModuleTreeLinkerTest,FetchTreeNoDeps)213 TEST_F(ModuleTreeLinkerTest, FetchTreeNoDeps) {
214 auto* registry = MakeGarbageCollected<ModuleTreeLinkerRegistry>();
215
216 KURL url("http://example.com/root.js");
217 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
218 ModuleTreeLinker::Fetch(
219 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
220 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
221 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
222
223 EXPECT_FALSE(client->WasNotifyFinished())
224 << "ModuleTreeLinker should always finish asynchronously.";
225 EXPECT_FALSE(client->GetModuleScript());
226
227 GetModulator()->ResolveSingleModuleScriptFetch(url, {});
228 EXPECT_TRUE(client->WasNotifyFinished());
229 ASSERT_TRUE(client->GetModuleScript());
230 EXPECT_TRUE(GetModulator()->HasInstantiated(client->GetModuleScript()));
231 }
232
TEST_F(ModuleTreeLinkerTest,FetchTreeInstantiationFailure)233 TEST_F(ModuleTreeLinkerTest, FetchTreeInstantiationFailure) {
234 GetModulator()->SetInstantiateShouldFail(true);
235
236 ModuleTreeLinkerRegistry* registry =
237 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
238
239 KURL url("http://example.com/root.js");
240 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
241 ModuleTreeLinker::Fetch(
242 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
243 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
244 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
245
246 EXPECT_FALSE(client->WasNotifyFinished())
247 << "ModuleTreeLinker should always finish asynchronously.";
248 EXPECT_FALSE(client->GetModuleScript());
249
250 GetModulator()->ResolveSingleModuleScriptFetch(url, {});
251
252 // Modulator::InstantiateModule() fails here, as
253 // we SetInstantiateShouldFail(true) earlier.
254
255 EXPECT_TRUE(client->WasNotifyFinished());
256 ASSERT_TRUE(client->GetModuleScript());
257 EXPECT_TRUE(client->GetModuleScript()->HasErrorToRethrow())
258 << "Expected errored module script but got "
259 << *client->GetModuleScript();
260 }
261
TEST_F(ModuleTreeLinkerTest,FetchTreeWithSingleDependency)262 TEST_F(ModuleTreeLinkerTest, FetchTreeWithSingleDependency) {
263 ModuleTreeLinkerRegistry* registry =
264 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
265
266 KURL url("http://example.com/root.js");
267 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
268 ModuleTreeLinker::Fetch(
269 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
270 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
271 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
272
273 EXPECT_FALSE(client->WasNotifyFinished())
274 << "ModuleTreeLinker should always finish asynchronously.";
275 EXPECT_FALSE(client->GetModuleScript());
276
277 GetModulator()->ResolveSingleModuleScriptFetch(url, {"./dep1.js"});
278 EXPECT_FALSE(client->WasNotifyFinished());
279
280 KURL url_dep1("http://example.com/dep1.js");
281
282 GetModulator()->ResolveDependentTreeFetch(
283 url_dep1, ModuleTreeLinkerTestModulator::ResolveResult::kSuccess);
284 EXPECT_TRUE(client->WasNotifyFinished());
285
286 ASSERT_TRUE(client->GetModuleScript());
287 EXPECT_TRUE(GetModulator()->HasInstantiated(client->GetModuleScript()));
288 }
289
TEST_F(ModuleTreeLinkerTest,FetchTreeWith3Deps)290 TEST_F(ModuleTreeLinkerTest, FetchTreeWith3Deps) {
291 ModuleTreeLinkerRegistry* registry =
292 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
293
294 KURL url("http://example.com/root.js");
295 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
296 ModuleTreeLinker::Fetch(
297 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
298 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
299 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
300
301 EXPECT_FALSE(client->WasNotifyFinished())
302 << "ModuleTreeLinker should always finish asynchronously.";
303 EXPECT_FALSE(client->GetModuleScript());
304
305 GetModulator()->ResolveSingleModuleScriptFetch(
306 url, {"./dep1.js", "./dep2.js", "./dep3.js"});
307 EXPECT_FALSE(client->WasNotifyFinished());
308
309 Vector<KURL> url_deps;
310 for (int i = 1; i <= 3; ++i) {
311 StringBuilder url_dep_str;
312 url_dep_str.Append("http://example.com/dep");
313 url_dep_str.AppendNumber(i);
314 url_dep_str.Append(".js");
315
316 KURL url_dep(url_dep_str.ToString());
317 url_deps.push_back(url_dep);
318 }
319
320 for (const auto& url_dep : url_deps) {
321 EXPECT_FALSE(client->WasNotifyFinished());
322 GetModulator()->ResolveDependentTreeFetch(
323 url_dep, ModuleTreeLinkerTestModulator::ResolveResult::kSuccess);
324 }
325
326 EXPECT_TRUE(client->WasNotifyFinished());
327 ASSERT_TRUE(client->GetModuleScript());
328 EXPECT_TRUE(GetModulator()->HasInstantiated(client->GetModuleScript()));
329 }
330
TEST_F(ModuleTreeLinkerTest,FetchTreeWith3Deps1Fail)331 TEST_F(ModuleTreeLinkerTest, FetchTreeWith3Deps1Fail) {
332 ModuleTreeLinkerRegistry* registry =
333 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
334
335 KURL url("http://example.com/root.js");
336 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
337 ModuleTreeLinker::Fetch(
338 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
339 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
340 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
341
342 EXPECT_FALSE(client->WasNotifyFinished())
343 << "ModuleTreeLinker should always finish asynchronously.";
344 EXPECT_FALSE(client->GetModuleScript());
345
346 GetModulator()->ResolveSingleModuleScriptFetch(
347 url, {"./dep1.js", "./dep2.js", "./dep3.js"});
348 EXPECT_FALSE(client->WasNotifyFinished());
349
350 Vector<KURL> url_deps;
351 for (int i = 1; i <= 3; ++i) {
352 StringBuilder url_dep_str;
353 url_dep_str.Append("http://example.com/dep");
354 url_dep_str.AppendNumber(i);
355 url_dep_str.Append(".js");
356
357 KURL url_dep(url_dep_str.ToString());
358 url_deps.push_back(url_dep);
359 }
360
361 for (const auto& url_dep : url_deps) {
362 SCOPED_TRACE(url_dep.GetString());
363 }
364
365 auto url_dep = url_deps.back();
366 url_deps.pop_back();
367 GetModulator()->ResolveDependentTreeFetch(
368 url_dep, ModuleTreeLinkerTestModulator::ResolveResult::kSuccess);
369 EXPECT_FALSE(client->WasNotifyFinished());
370 url_dep = url_deps.back();
371 url_deps.pop_back();
372 GetModulator()->ResolveDependentTreeFetch(
373 url_dep, ModuleTreeLinkerTestModulator::ResolveResult::kFailure);
374
375 // TODO(kouhei): This may not hold once we implement early failure reporting.
376 EXPECT_FALSE(client->WasNotifyFinished());
377
378 // Check below doesn't crash.
379 url_dep = url_deps.back();
380 url_deps.pop_back();
381 GetModulator()->ResolveDependentTreeFetch(
382 url_dep, ModuleTreeLinkerTestModulator::ResolveResult::kSuccess);
383 EXPECT_TRUE(url_deps.IsEmpty());
384
385 EXPECT_TRUE(client->WasNotifyFinished());
386 ASSERT_TRUE(client->GetModuleScript());
387 EXPECT_FALSE(client->GetModuleScript()->HasParseError());
388 EXPECT_TRUE(client->GetModuleScript()->HasErrorToRethrow());
389 }
390
TEST_F(ModuleTreeLinkerTest,FetchDependencyTree)391 TEST_F(ModuleTreeLinkerTest, FetchDependencyTree) {
392 ModuleTreeLinkerRegistry* registry =
393 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
394
395 KURL url("http://example.com/depth1.js");
396 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
397 ModuleTreeLinker::Fetch(
398 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
399 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
400 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
401
402 EXPECT_FALSE(client->WasNotifyFinished())
403 << "ModuleTreeLinker should always finish asynchronously.";
404 EXPECT_FALSE(client->GetModuleScript());
405
406 GetModulator()->ResolveSingleModuleScriptFetch(url, {"./depth2.js"});
407
408 KURL url_dep2("http://example.com/depth2.js");
409
410 GetModulator()->ResolveDependentTreeFetch(
411 url_dep2, ModuleTreeLinkerTestModulator::ResolveResult::kSuccess);
412
413 EXPECT_TRUE(client->WasNotifyFinished());
414 ASSERT_TRUE(client->GetModuleScript());
415 EXPECT_TRUE(GetModulator()->HasInstantiated(client->GetModuleScript()));
416 }
417
TEST_F(ModuleTreeLinkerTest,FetchDependencyOfCyclicGraph)418 TEST_F(ModuleTreeLinkerTest, FetchDependencyOfCyclicGraph) {
419 ModuleTreeLinkerRegistry* registry =
420 MakeGarbageCollected<ModuleTreeLinkerRegistry>();
421
422 KURL url("http://example.com/a.js");
423 TestModuleTreeClient* client = MakeGarbageCollected<TestModuleTreeClient>();
424 ModuleTreeLinker::Fetch(
425 url, GetDocument().Fetcher(), mojom::RequestContextType::SCRIPT,
426 network::mojom::RequestDestination::kScript, ScriptFetchOptions(),
427 GetModulator(), ModuleScriptCustomFetchType::kNone, registry, client);
428
429 EXPECT_FALSE(client->WasNotifyFinished())
430 << "ModuleTreeLinker should always finish asynchronously.";
431 EXPECT_FALSE(client->GetModuleScript());
432
433 GetModulator()->ResolveSingleModuleScriptFetch(url, {"./a.js"});
434
435 EXPECT_TRUE(client->WasNotifyFinished());
436 ASSERT_TRUE(client->GetModuleScript());
437 EXPECT_TRUE(GetModulator()->HasInstantiated(client->GetModuleScript()));
438 }
439
440 } // namespace blink
441