// Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:expect/expect.dart'; import 'package:async_helper/async_helper.dart'; import 'dart:async'; import 'event_helper.dart'; class DecrementingTransformerSink implements EventSink { final outSink; DecrementingTransformerSink(this.outSink); void add(dynamic i) => outSink.add(i - 1); void addError(dynamic e, [st]) => outSink.addError(e - 1, st); void close() => outSink.close(); } class FutureWaitingTransformerSink implements EventSink { final outSink; final closeFuture; FutureWaitingTransformerSink(this.outSink, this.closeFuture); void add(dynamic future) { future.then(outSink.add); } void addError(dynamic e, [st]) { e.then((val) { outSink.addError(val, st); }); } void close() { closeFuture.whenComplete(outSink.close); } } class ZoneTransformerSink implements EventSink { final outSink; ZoneTransformerSink(this.outSink); void add(_) { outSink.add(Zone.current); } void addError(_, [st]) { outSink.add(Zone.current); } void close() { outSink.add(Zone.current); outSink.close(); } } class TypeChangingSink implements EventSink { final EventSink outSink; TypeChangingSink(this.outSink); void add(int data) { outSink.add(data.toString()); } void addError(error, [st]) { outSink.addError(error, st); } void close() { outSink.close(); } } class SinkTransformer extends StreamTransformerBase { final Function sinkMapper; SinkTransformer(this.sinkMapper); Stream bind(Stream stream) { return new Stream.eventTransformed(stream, sinkMapper); } } get currentStackTrace { try { throw 0; } catch (e, st) { return st; } } // In most cases the callback will be 'asyncEnd'. Errors are reported // asynchronously. We want to give them time to surface before reporting // asynchronous tests as done. void delayCycles(callback, int nbCycles) { if (nbCycles == 0) { callback(); return; } Timer.run(() { delayCycles(callback, nbCycles - 1); }); } main() { { // Simple test: use the SinkTransformer (using the Stream.eventTransformed // constructor) to transform a sequence of numbers. This is basically // similar to a map. asyncStart(); new Stream.fromIterable([1, 2, 3]) .transform(new SinkTransformer( (sink) => new DecrementingTransformerSink(sink))) .toList() .then((list) { Expect.listEquals([0, 1, 2], list); asyncEnd(); }); } { // Similar test as above: but this time also transform errors. Also // checks that the stack trace is correctly passed through. asyncStart(); var controller; var events = []; var stackTrace = currentStackTrace; controller = new StreamController(onListen: () { controller.add(499); controller.addError(42, stackTrace); controller.close(); }); controller.stream .transform(new SinkTransformer( (sink) => new DecrementingTransformerSink(sink))) .listen((data) { events.add(data); }, onError: (e, st) { events.add(e); events.add(st); }, onDone: () { Expect.listEquals([498, 41, stackTrace], events); asyncEnd(); }); } { // Test that the output sink of the transformer can be used asynchronously. asyncStart(); var controller; var events = []; var stackTrace = currentStackTrace; var completer1 = new Completer(); var completer2 = new Completer(); var completer3 = new Completer(); var closeCompleter = new Completer(); controller = new StreamController(onListen: () { controller.add(completer1.future); controller.addError(completer2.future, stackTrace); controller.add(completer3.future); controller.close(); }); controller.stream .transform(new SinkTransformer((sink) => new FutureWaitingTransformerSink(sink, closeCompleter.future))) .listen((data) { events.add(data); }, onError: (e, st) { events.add(e); events.add(st); }, onDone: () { Expect.listEquals(["error2", stackTrace, "future3", "future1"], events); asyncEnd(); }); Timer.run(() { completer2.complete("error2"); Timer.run(() { completer3.complete("future3"); Timer.run(() { completer1.complete("future1"); scheduleMicrotask(closeCompleter.complete); }); }); }); } { // Test that the output sink of the transformer can be used asynchronously // and that events are paused if necessary. asyncStart(); var controller; var events = []; var stackTrace = currentStackTrace; var completer1 = new Completer.sync(); var completer2 = new Completer.sync(); var completer3 = new Completer.sync(); var closeCompleter = new Completer(); controller = new StreamController(onListen: () { controller.add(completer1.future); controller.addError(completer2.future, stackTrace); controller.add(completer3.future); controller.close(); }); var subscription; completer1.future.then((_) { Expect.isTrue(subscription.isPaused); }); completer2.future.then((_) { Expect.isTrue(subscription.isPaused); }); completer3.future.then((_) { Expect.isTrue(subscription.isPaused); }); subscription = controller.stream .transform(new SinkTransformer((sink) => new FutureWaitingTransformerSink(sink, closeCompleter.future))) .listen((data) { Expect.isFalse(subscription.isPaused); events.add(data); }, onError: (e, st) { events.add(e); events.add(st); }, onDone: () { Expect.listEquals(["error2", stackTrace, "future3", "future1"], events); asyncEnd(); }); Timer.run(() { subscription.pause(); completer2.complete("error2"); Timer.run(() { subscription.resume(); Timer.run(() { Expect.listEquals(["error2", stackTrace], events); subscription.pause(); completer3.complete("future3"); Timer.run(() { subscription.resume(); Timer.run(() { Expect.listEquals(["error2", stackTrace, "future3"], events); subscription.pause(); completer1.complete("future1"); subscription.resume(); scheduleMicrotask(closeCompleter.complete); }); }); }); }); }); } { // Test that the output sink of the transformer reports errors when the // stream is already closed. asyncStart(); var controller; var events = []; var stackTrace = currentStackTrace; var completer1 = new Completer(); var completer2 = new Completer(); var completer3 = new Completer(); var closeCompleter = new Completer(); controller = new StreamController(onListen: () { controller.add(completer1.future); controller.addError(completer2.future, stackTrace); controller.add(completer3.future); controller.close(); }); bool streamIsDone = false; int errorCount = 0; runZonedGuarded(() { controller.stream .transform(new SinkTransformer((sink) => new FutureWaitingTransformerSink(sink, closeCompleter.future))) .listen((data) { events.add(data); }, onError: (e, st) { events.add(e); events.add(st); }, onDone: () { Expect.listEquals([], events); streamIsDone = true; }); }, (e, s) { Expect.isTrue(e is StateError); errorCount++; }); closeCompleter.complete(); Timer.run(() { Expect.isTrue(streamIsDone); // Each of the delayed completions should trigger an unhandled error // in the zone the stream was listened to. Timer.run(() { completer1.complete(499); }); Timer.run(() { completer2.complete(42); }); Timer.run(() { completer3.complete(99); }); delayCycles(() { Expect.equals(3, errorCount); asyncEnd(); }, 5); }); } { // Test that the transformer is executed in the zone it was listened to. asyncStart(); var stackTrace = currentStackTrace; var events = []; var controller; controller = new StreamController(onListen: () { // Events are added outside the zone. controller.add(499); controller.addError(42, stackTrace); controller.close(); }); Zone zone = Zone.current.fork(); var stream = controller.stream.transform( new SinkTransformer((sink) => new ZoneTransformerSink(sink))); zone.run(() { stream.listen((data) { events.add(data); }, onDone: () { Expect.listEquals([zone, zone, zone], events); delayCycles(asyncEnd, 3); }); }); } { // Just make sure that the generic types are correct everywhere. asyncStart(); new Stream.fromIterable([1, 2, 3]) .transform(new SinkTransformer( (sink) => new TypeChangingSink(sink))) .toList() .then((list) { Expect.listEquals(["1", "2", "3"], list); asyncEnd(); }); } }