1 // Copyright 2018 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/streams/transform_stream.h"
6
7 #include "testing/gmock/include/gmock/gmock.h"
8 #include "testing/gtest/include/gtest/gtest.h"
9 #include "third_party/blink/renderer/bindings/core/v8/script_function.h"
10 #include "third_party/blink/renderer/bindings/core/v8/script_promise.h"
11 #include "third_party/blink/renderer/bindings/core/v8/script_promise_resolver.h"
12 #include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
13 #include "third_party/blink/renderer/bindings/core/v8/script_value.h"
14 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
15 #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
16 #include "third_party/blink/renderer/bindings/core/v8/v8_extras_test_utils.h"
17 #include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
18 #include "third_party/blink/renderer/bindings/core/v8/v8_iterator_result_value.h"
19 #include "third_party/blink/renderer/core/streams/readable_stream.h"
20 #include "third_party/blink/renderer/core/streams/transform_stream_default_controller.h"
21 #include "third_party/blink/renderer/core/streams/transform_stream_transformer.h"
22 #include "third_party/blink/renderer/core/streams/writable_stream.h"
23 #include "third_party/blink/renderer/platform/bindings/exception_state.h"
24 #include "third_party/blink/renderer/platform/bindings/microtask.h"
25 #include "third_party/blink/renderer/platform/bindings/script_state.h"
26 #include "third_party/blink/renderer/platform/bindings/to_v8.h"
27 #include "third_party/blink/renderer/platform/bindings/v8_binding.h"
28 #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h"
29 #include "v8/include/v8.h"
30
31 namespace blink {
32
33 namespace {
34
35 using ::testing::_;
36 using ::testing::ByMove;
37 using ::testing::Mock;
38 using ::testing::Return;
39
40 class TransformStreamTest : public ::testing::Test {
41 public:
TransformStreamTest()42 TransformStreamTest() {}
43
Stream() const44 TransformStream* Stream() const { return stream_; }
45
Init(TransformStreamTransformer * transformer,ScriptState * script_state,ExceptionState & exception_state)46 void Init(TransformStreamTransformer* transformer,
47 ScriptState* script_state,
48 ExceptionState& exception_state) {
49 stream_ =
50 TransformStream::Create(script_state, transformer, exception_state);
51 }
52
53 // This takes the |readable| and |writable| properties of the TransformStream
54 // and copies them onto the global object so they can be accessed by Eval().
CopyReadableAndWritableToGlobal(const V8TestingScope & scope)55 void CopyReadableAndWritableToGlobal(const V8TestingScope& scope) {
56 auto* script_state = scope.GetScriptState();
57 ReadableStream* readable = Stream()->Readable();
58 WritableStream* writable = Stream()->Writable();
59 v8::Local<v8::Object> global = script_state->GetContext()->Global();
60 EXPECT_TRUE(global
61 ->Set(scope.GetContext(),
62 V8String(scope.GetIsolate(), "readable"),
63 ToV8(readable, script_state))
64 .IsJust());
65 EXPECT_TRUE(global
66 ->Set(scope.GetContext(),
67 V8String(scope.GetIsolate(), "writable"),
68 ToV8(writable, script_state))
69 .IsJust());
70 }
71
72 private:
73 Persistent<TransformStream> stream_;
74 };
75
76 // A convenient base class to make tests shorter. Subclasses need not implement
77 // both Transform() and Flush(), and can override the void versions to avoid the
78 // need to create a promise to return. Not appropriate for use in production.
79 class TestTransformer : public TransformStreamTransformer {
80 public:
TestTransformer(ScriptState * script_state)81 explicit TestTransformer(ScriptState* script_state)
82 : script_state_(script_state) {}
83
TransformVoid(v8::Local<v8::Value>,TransformStreamDefaultController *,ExceptionState &)84 virtual void TransformVoid(v8::Local<v8::Value>,
85 TransformStreamDefaultController*,
86 ExceptionState&) {}
87
Transform(v8::Local<v8::Value> chunk,TransformStreamDefaultController * controller,ExceptionState & exception_state)88 ScriptPromise Transform(v8::Local<v8::Value> chunk,
89 TransformStreamDefaultController* controller,
90 ExceptionState& exception_state) override {
91 TransformVoid(chunk, controller, exception_state);
92 return ScriptPromise::CastUndefined(script_state_);
93 }
94
FlushVoid(TransformStreamDefaultController *,ExceptionState &)95 virtual void FlushVoid(TransformStreamDefaultController*, ExceptionState&) {}
96
Flush(TransformStreamDefaultController * controller,ExceptionState & exception_state)97 ScriptPromise Flush(TransformStreamDefaultController* controller,
98 ExceptionState& exception_state) override {
99 FlushVoid(controller, exception_state);
100 return ScriptPromise::CastUndefined(script_state_);
101 }
102
GetScriptState()103 ScriptState* GetScriptState() override { return script_state_; }
104
Trace(Visitor * visitor) const105 void Trace(Visitor* visitor) const override {
106 visitor->Trace(script_state_);
107 TransformStreamTransformer::Trace(visitor);
108 }
109
110 private:
111 const Member<ScriptState> script_state_;
112 };
113
114 class IdentityTransformer final : public TestTransformer {
115 public:
IdentityTransformer(ScriptState * script_state)116 explicit IdentityTransformer(ScriptState* script_state)
117 : TestTransformer(script_state) {}
118
TransformVoid(v8::Local<v8::Value> chunk,TransformStreamDefaultController * controller,ExceptionState & exception_state)119 void TransformVoid(v8::Local<v8::Value> chunk,
120 TransformStreamDefaultController* controller,
121 ExceptionState& exception_state) override {
122 controller->enqueue(GetScriptState(),
123 ScriptValue(GetScriptState()->GetIsolate(), chunk),
124 exception_state);
125 }
126 };
127
128 class MockTransformStreamTransformer : public TransformStreamTransformer {
129 public:
MockTransformStreamTransformer(ScriptState * script_state)130 explicit MockTransformStreamTransformer(ScriptState* script_state)
131 : script_state_(script_state) {}
132
133 MOCK_METHOD3(Transform,
134 ScriptPromise(v8::Local<v8::Value> chunk,
135 TransformStreamDefaultController*,
136 ExceptionState&));
137 MOCK_METHOD2(Flush,
138 ScriptPromise(TransformStreamDefaultController*,
139 ExceptionState&));
140
GetScriptState()141 ScriptState* GetScriptState() override { return script_state_; }
142
Trace(Visitor * visitor) const143 void Trace(Visitor* visitor) const override {
144 visitor->Trace(script_state_);
145 TransformStreamTransformer::Trace(visitor);
146 }
147
148 private:
149 const Member<ScriptState> script_state_;
150 };
151
152 // If this doesn't work then nothing else will.
TEST_F(TransformStreamTest,Construct)153 TEST_F(TransformStreamTest, Construct) {
154 V8TestingScope scope;
155 Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
156 scope.GetScriptState(), ASSERT_NO_EXCEPTION);
157 EXPECT_TRUE(Stream());
158 }
159
TEST_F(TransformStreamTest,Accessors)160 TEST_F(TransformStreamTest, Accessors) {
161 V8TestingScope scope;
162 Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
163 scope.GetScriptState(), ASSERT_NO_EXCEPTION);
164 ReadableStream* readable = Stream()->Readable();
165 WritableStream* writable = Stream()->Writable();
166 EXPECT_TRUE(readable);
167 EXPECT_TRUE(writable);
168 }
169
TEST_F(TransformStreamTest,TransformIsCalled)170 TEST_F(TransformStreamTest, TransformIsCalled) {
171 V8TestingScope scope;
172 auto* mock = MakeGarbageCollected<MockTransformStreamTransformer>(
173 scope.GetScriptState());
174 Init(mock, scope.GetScriptState(), ASSERT_NO_EXCEPTION);
175 // Need to run microtasks so the startAlgorithm promise resolves.
176 v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
177 CopyReadableAndWritableToGlobal(scope);
178
179 EXPECT_CALL(*mock, Transform(_, _, _))
180 .WillOnce(
181 Return(ByMove(ScriptPromise::CastUndefined(scope.GetScriptState()))));
182
183 // The initial read is needed to relieve backpressure.
184 EvalWithPrintingError(&scope,
185 "readable.getReader().read();\n"
186 "const writer = writable.getWriter();\n"
187 "writer.write('a');\n");
188
189 Mock::VerifyAndClear(mock);
190 Mock::AllowLeak(mock);
191 }
192
TEST_F(TransformStreamTest,FlushIsCalled)193 TEST_F(TransformStreamTest, FlushIsCalled) {
194 V8TestingScope scope;
195 auto* mock = MakeGarbageCollected<MockTransformStreamTransformer>(
196 scope.GetScriptState());
197 Init(mock, scope.GetScriptState(), ASSERT_NO_EXCEPTION);
198 // Need to run microtasks so the startAlgorithm promise resolves.
199 v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
200 CopyReadableAndWritableToGlobal(scope);
201
202 EXPECT_CALL(*mock, Flush(_, _))
203 .WillOnce(
204 Return(ByMove(ScriptPromise::CastUndefined(scope.GetScriptState()))));
205
206 EvalWithPrintingError(&scope,
207 "const writer = writable.getWriter();\n"
208 "writer.close();\n");
209
210 Mock::VerifyAndClear(mock);
211 Mock::AllowLeak(mock);
212 }
213
IsIteratorForStringMatching(ScriptState * script_state,ScriptValue value,const String & expected)214 bool IsIteratorForStringMatching(ScriptState* script_state,
215 ScriptValue value,
216 const String& expected) {
217 if (!value.IsObject()) {
218 return false;
219 }
220 bool done = false;
221 auto chunk = V8UnpackIteratorResult(
222 script_state,
223 value.V8Value()->ToObject(script_state->GetContext()).ToLocalChecked(),
224 &done);
225 if (done || chunk.IsEmpty())
226 return false;
227 return ToCoreStringWithUndefinedOrNullCheck(chunk.ToLocalChecked()) ==
228 expected;
229 }
230
IsTypeError(ScriptState * script_state,ScriptValue value,const String & message)231 bool IsTypeError(ScriptState* script_state,
232 ScriptValue value,
233 const String& message) {
234 v8::Local<v8::Object> object;
235 if (!value.V8Value()->ToObject(script_state->GetContext()).ToLocal(&object)) {
236 return false;
237 }
238 if (!object->IsNativeError())
239 return false;
240
241 const auto& Has = [script_state, object](const String& key,
242 const String& value) -> bool {
243 v8::Local<v8::Value> actual;
244 return object
245 ->Get(script_state->GetContext(),
246 V8AtomicString(script_state->GetIsolate(), key))
247 .ToLocal(&actual) &&
248 ToCoreStringWithUndefinedOrNullCheck(actual) == value;
249 };
250
251 return Has("name", "TypeError") && Has("message", message);
252 }
253
TEST_F(TransformStreamTest,EnqueueFromTransform)254 TEST_F(TransformStreamTest, EnqueueFromTransform) {
255 V8TestingScope scope;
256 auto* script_state = scope.GetScriptState();
257 Init(MakeGarbageCollected<IdentityTransformer>(scope.GetScriptState()),
258 script_state, ASSERT_NO_EXCEPTION);
259
260 CopyReadableAndWritableToGlobal(scope);
261
262 EvalWithPrintingError(&scope,
263 "const writer = writable.getWriter();\n"
264 "writer.write('a');\n");
265
266 ReadableStream* readable = Stream()->Readable();
267 auto* reader =
268 readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION);
269 ScriptPromiseTester tester(script_state,
270 reader->read(script_state, ASSERT_NO_EXCEPTION));
271 tester.WaitUntilSettled();
272 EXPECT_TRUE(tester.IsFulfilled());
273 EXPECT_TRUE(IsIteratorForStringMatching(script_state, tester.Value(), "a"));
274 }
275
TEST_F(TransformStreamTest,EnqueueFromFlush)276 TEST_F(TransformStreamTest, EnqueueFromFlush) {
277 class EnqueueFromFlushTransformer final : public TestTransformer {
278 public:
279 explicit EnqueueFromFlushTransformer(ScriptState* script_state)
280 : TestTransformer(script_state) {}
281
282 void FlushVoid(TransformStreamDefaultController* controller,
283 ExceptionState& exception_state) override {
284 controller->enqueue(GetScriptState(),
285 ScriptValue::From(GetScriptState(), "a"),
286 exception_state);
287 }
288 };
289
290 V8TestingScope scope;
291 auto* script_state = scope.GetScriptState();
292 Init(MakeGarbageCollected<EnqueueFromFlushTransformer>(script_state),
293 script_state, ASSERT_NO_EXCEPTION);
294
295 CopyReadableAndWritableToGlobal(scope);
296
297 EvalWithPrintingError(&scope,
298 "const writer = writable.getWriter();\n"
299 "writer.close();\n");
300
301 ReadableStream* readable = Stream()->Readable();
302 auto* reader =
303 readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION);
304 ScriptPromiseTester tester(script_state,
305 reader->read(script_state, ASSERT_NO_EXCEPTION));
306 tester.WaitUntilSettled();
307 EXPECT_TRUE(tester.IsFulfilled());
308 EXPECT_TRUE(IsIteratorForStringMatching(script_state, tester.Value(), "a"));
309 }
310
TEST_F(TransformStreamTest,ThrowFromTransform)311 TEST_F(TransformStreamTest, ThrowFromTransform) {
312 static constexpr char kMessage[] = "errorInTransform";
313 class ThrowFromTransformTransformer final : public TestTransformer {
314 public:
315 explicit ThrowFromTransformTransformer(ScriptState* script_state)
316 : TestTransformer(script_state) {}
317
318 void TransformVoid(v8::Local<v8::Value>,
319 TransformStreamDefaultController*,
320 ExceptionState& exception_state) override {
321 exception_state.ThrowTypeError(kMessage);
322 }
323 };
324
325 V8TestingScope scope;
326 auto* script_state = scope.GetScriptState();
327 Init(MakeGarbageCollected<ThrowFromTransformTransformer>(
328 scope.GetScriptState()),
329 script_state, ASSERT_NO_EXCEPTION);
330
331 CopyReadableAndWritableToGlobal(scope);
332
333 ScriptValue promise =
334 EvalWithPrintingError(&scope,
335 "const writer = writable.getWriter();\n"
336 "writer.write('a');\n");
337
338 ReadableStream* readable = Stream()->Readable();
339 auto* reader =
340 readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION);
341 ScriptPromiseTester read_tester(
342 script_state, reader->read(script_state, ASSERT_NO_EXCEPTION));
343 read_tester.WaitUntilSettled();
344 EXPECT_TRUE(read_tester.IsRejected());
345 EXPECT_TRUE(IsTypeError(script_state, read_tester.Value(), kMessage));
346 ScriptPromiseTester write_tester(script_state,
347 ScriptPromise::Cast(script_state, promise));
348 write_tester.WaitUntilSettled();
349 EXPECT_TRUE(write_tester.IsRejected());
350 EXPECT_TRUE(IsTypeError(script_state, write_tester.Value(), kMessage));
351 }
352
TEST_F(TransformStreamTest,ThrowFromFlush)353 TEST_F(TransformStreamTest, ThrowFromFlush) {
354 static constexpr char kMessage[] = "errorInFlush";
355 class ThrowFromFlushTransformer final : public TestTransformer {
356 public:
357 explicit ThrowFromFlushTransformer(ScriptState* script_state)
358 : TestTransformer(script_state) {}
359
360 void FlushVoid(TransformStreamDefaultController*,
361 ExceptionState& exception_state) override {
362 exception_state.ThrowTypeError(kMessage);
363 }
364 };
365 V8TestingScope scope;
366 auto* script_state = scope.GetScriptState();
367 Init(MakeGarbageCollected<ThrowFromFlushTransformer>(scope.GetScriptState()),
368 script_state, ASSERT_NO_EXCEPTION);
369
370 CopyReadableAndWritableToGlobal(scope);
371
372 ScriptValue promise =
373 EvalWithPrintingError(&scope,
374 "const writer = writable.getWriter();\n"
375 "writer.close();\n");
376
377 ReadableStream* readable = Stream()->Readable();
378 auto* reader =
379 readable->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION);
380 ScriptPromiseTester read_tester(
381 script_state, reader->read(script_state, ASSERT_NO_EXCEPTION));
382 read_tester.WaitUntilSettled();
383 EXPECT_TRUE(read_tester.IsRejected());
384 EXPECT_TRUE(IsTypeError(script_state, read_tester.Value(), kMessage));
385 ScriptPromiseTester write_tester(script_state,
386 ScriptPromise::Cast(script_state, promise));
387 write_tester.WaitUntilSettled();
388 EXPECT_TRUE(write_tester.IsRejected());
389 EXPECT_TRUE(IsTypeError(script_state, write_tester.Value(), kMessage));
390 }
391
TEST_F(TransformStreamTest,CreateFromReadableWritablePair)392 TEST_F(TransformStreamTest, CreateFromReadableWritablePair) {
393 V8TestingScope scope;
394 ReadableStream* readable =
395 ReadableStream::Create(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
396 WritableStream* writable =
397 WritableStream::Create(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
398 TransformStream transform(readable, writable);
399 EXPECT_EQ(readable, transform.Readable());
400 EXPECT_EQ(writable, transform.Writable());
401 }
402
TEST_F(TransformStreamTest,WaitInTransform)403 TEST_F(TransformStreamTest, WaitInTransform) {
404 class WaitInTransformTransformer final : public TestTransformer {
405 public:
406 explicit WaitInTransformTransformer(ScriptState* script_state)
407 : TestTransformer(script_state),
408 transform_promise_resolver_(
409 MakeGarbageCollected<ScriptPromiseResolver>(script_state)) {}
410
411 ScriptPromise Transform(v8::Local<v8::Value>,
412 TransformStreamDefaultController*,
413 ExceptionState&) override {
414 return transform_promise_resolver_->Promise();
415 }
416
417 void FlushVoid(TransformStreamDefaultController*,
418 ExceptionState&) override {
419 flush_called_ = true;
420 }
421
422 void ResolvePromise() { transform_promise_resolver_->Resolve(); }
423 bool FlushCalled() const { return flush_called_; }
424
425 void Trace(Visitor* visitor) const override {
426 visitor->Trace(transform_promise_resolver_);
427 TestTransformer::Trace(visitor);
428 }
429
430 private:
431 const Member<ScriptPromiseResolver> transform_promise_resolver_;
432 bool flush_called_ = false;
433 };
434
435 V8TestingScope scope;
436 auto* script_state = scope.GetScriptState();
437 auto* transformer =
438 MakeGarbageCollected<WaitInTransformTransformer>(script_state);
439 Init(transformer, script_state, ASSERT_NO_EXCEPTION);
440 CopyReadableAndWritableToGlobal(scope);
441
442 ScriptValue promise =
443 EvalWithPrintingError(&scope,
444 "const writer = writable.getWriter();\n"
445 "const promise = writer.write('a');\n"
446 "writer.close();\n"
447 "promise;\n");
448 // Need to read to relieve backpressure.
449 Stream()
450 ->Readable()
451 ->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION)
452 ->read(script_state, ASSERT_NO_EXCEPTION);
453
454 ScriptPromiseTester write_tester(script_state,
455 ScriptPromise::Cast(script_state, promise));
456
457 // Give Transform() the opportunity to be called.
458 v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
459
460 EXPECT_FALSE(write_tester.IsFulfilled());
461 EXPECT_FALSE(transformer->FlushCalled());
462
463 transformer->ResolvePromise();
464
465 write_tester.WaitUntilSettled();
466 EXPECT_TRUE(write_tester.IsFulfilled());
467 EXPECT_TRUE(transformer->FlushCalled());
468 }
469
TEST_F(TransformStreamTest,WaitInFlush)470 TEST_F(TransformStreamTest, WaitInFlush) {
471 class WaitInFlushTransformer final : public TestTransformer {
472 public:
473 explicit WaitInFlushTransformer(ScriptState* script_state)
474 : TestTransformer(script_state),
475 flush_promise_resolver_(
476 MakeGarbageCollected<ScriptPromiseResolver>(script_state)) {}
477
478 ScriptPromise Flush(TransformStreamDefaultController*,
479 ExceptionState&) override {
480 return flush_promise_resolver_->Promise();
481 }
482
483 void ResolvePromise() { flush_promise_resolver_->Resolve(); }
484
485 void Trace(Visitor* visitor) const override {
486 visitor->Trace(flush_promise_resolver_);
487 TestTransformer::Trace(visitor);
488 }
489
490 private:
491 const Member<ScriptPromiseResolver> flush_promise_resolver_;
492 };
493
494 V8TestingScope scope;
495 auto* script_state = scope.GetScriptState();
496 auto* transformer =
497 MakeGarbageCollected<WaitInFlushTransformer>(script_state);
498 Init(transformer, script_state, ASSERT_NO_EXCEPTION);
499 CopyReadableAndWritableToGlobal(scope);
500
501 ScriptValue promise =
502 EvalWithPrintingError(&scope,
503 "const writer = writable.getWriter();\n"
504 "writer.close();\n");
505
506 // Need to read to relieve backpressure.
507 Stream()
508 ->Readable()
509 ->GetDefaultReaderForTesting(script_state, ASSERT_NO_EXCEPTION)
510 ->read(script_state, ASSERT_NO_EXCEPTION);
511
512 ScriptPromiseTester close_tester(script_state,
513 ScriptPromise::Cast(script_state, promise));
514
515 // Give Flush() the opportunity to be called.
516 v8::MicrotasksScope::PerformCheckpoint(scope.GetIsolate());
517
518 EXPECT_FALSE(close_tester.IsFulfilled());
519 transformer->ResolvePromise();
520 close_tester.WaitUntilSettled();
521 EXPECT_TRUE(close_tester.IsFulfilled());
522 }
523
524 } // namespace
525 } // namespace blink
526