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