From 81225eaaae51fa60a609dde509e680fcd5fcea06 Mon Sep 17 00:00:00 2001 From: Tod Bachman Date: Wed, 28 Jun 2017 11:47:37 -0600 Subject: [PATCH] RAP-2123 Enable mixing in browser version of Disposable --- lib/src/browser/disposable_browser.dart | 187 +++++++++- lib/src/common/disposable.dart | 17 +- test/unit/browser/browser_stubs.dart | 7 +- .../unit/browser/disposable_browser_test.dart | 87 +++-- test/unit/disposable_common.dart | 347 ++++++++++++++++++ test/unit/vm/disposable_vm_test.dart | 335 +---------------- test/unit/vm/vm_stubs.dart | 2 +- 7 files changed, 583 insertions(+), 399 deletions(-) create mode 100644 test/unit/disposable_common.dart diff --git a/lib/src/browser/disposable_browser.dart b/lib/src/browser/disposable_browser.dart index e82b424b..3146b9df 100644 --- a/lib/src/browser/disposable_browser.dart +++ b/lib/src/browser/disposable_browser.dart @@ -12,11 +12,191 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:async'; import 'dart:html'; +import 'package:meta/meta.dart'; +import 'package:w_common/func.dart'; + import 'package:w_common/src/common/disposable.dart' as disposable_common; -class Disposable extends disposable_common.Disposable { +class _InnerDisposable extends disposable_common.Disposable { + Func> onDisposeHandler; + + @override + Future onDispose() { + return onDisposeHandler(); + } +} + +/// Allows the creation of managed objects, including helpers for common +/// patterns. +/// +/// There are four ways to consume this class: as a mixin, a base class, +/// an interface, and a concrete class used as a proxy. All should work +/// fine but the first is the simplest and most powerful. Using the class +/// as an interface will require significant effort. +/// +/// In the case below, the class is used as a mixin. This provides both +/// default implementations and flexibility since it does not occupy +/// a spot in the class hierarchy. +/// +/// Helper methods, such as [manageStreamSubscription] allow certain +/// cleanup to be automated. Managed subscriptions will be automatically +/// canceled when [dispose] is called on the object. +/// +/// class MyDisposable extends Object with Disposable { +/// StreamController _controller = new StreamController(); +/// +/// MyDisposable(Stream someStream) { +/// manageStreamSubscription(someStream.listen((_) => print('some stream'))); +/// manageStreamController(_controller); +/// } +/// +/// Future onDispose() { +/// // Other cleanup +/// } +/// } +/// +/// The [manageDisposer] helper allows you to clean up arbitrary objects +/// on dispose so that you can avoid keeping track of them yourself. To +/// use it, simply provide a callback that returns a [Future] of any +/// kind. For example: +/// +/// class MyDisposable extends Object with Disposable { +/// StreamController _controller = new StreamController(); +/// +/// MyDisposable() { +/// var thing = new ThingThatRequiresCleanup(); +/// manageDisposer(() { +/// thing.cleanUp(); +/// return new Future(() {}); +/// }); +/// } +/// } +/// +/// Cleanup will then be automatically performed when the containing +/// object is disposed. If returning a future is inconvenient or +/// otherwise undesirable, you may also return `null` explicitly. +/// +/// Implementing the [onDispose] method is entirely optional and is only +/// necessary if there is cleanup required that is not covered by one of +/// the helpers. +/// +/// It is possible to schedule a callback to be called after the object +/// is disposed for purposes of further, external, cleanup or bookkeeping +/// (for example, you might want to remove any objects that are disposed +/// from a cache). To do this, use the [didDispose] future: +/// +/// var myDisposable = new MyDisposable(); +/// myDisposable.didDispose.then((_) { +/// // External cleanup +/// }); +/// +/// Below is an example of using the class as a concrete proxy. +/// +/// class MyLifecycleThing implements DisposableManager { +/// Disposable _disposable = new Disposable(); +/// +/// MyLifecycleThing() { +/// _disposable.manageStreamSubscription(someStream.listen(() => null)); +/// } +/// +/// @override +/// void manageStreamSubscription(StreamSubscription sub) { +/// _disposable.manageStreamSubscription(sub); +/// } +/// +/// // ...more methods +/// +/// Future unload() async { +/// await _disposable.dispose(); +/// } +/// } +/// +/// In this case, we want `MyLifecycleThing` to have its own lifecycle +/// without explicit reference to [Disposable]. To do this, we use +/// composition to include the [Disposable] machinery without changing +/// the public interface of our class or polluting its lifecycle. +class Disposable implements disposable_common.Disposable { + /// Disables logging enabled by [enableDebugMode]. + static void disableDebugMode() => + disposable_common.Disposable.disableDebugMode(); + + /// Causes messages to be logged for various lifecycle and management events. + /// + /// This should only be used for debugging and profiling as it can result + /// in a huge number of messages being generated. + static void enableDebugMode() => + disposable_common.Disposable.enableDebugMode(); + + final _InnerDisposable _disposable = new _InnerDisposable(); + + @override + Future get didDispose => _disposable.didDispose; + + @override + int get disposalTreeSize => _disposable.disposalTreeSize; + + @override + bool get isDisposed => _disposable.isDisposed; + + @override + bool get isDisposedOrDisposing => _disposable.isDisposedOrDisposing; + + @override + bool get isDisposing => _disposable.isDisposing; + + @override + Future awaitBeforeDispose(Future future) => _disposable + .awaitBeforeDispose(future); + + @override + Future dispose() { + _disposable.onDisposeHandler = this.onDispose; + return _disposable.dispose(); + } + + @override + Future getManagedDelayedFuture(Duration duration, T callback()) => + _disposable.getManagedDelayedFuture(duration, callback); + + @override + Timer getManagedPeriodicTimer( + Duration duration, void callback(Timer timer)) => + _disposable.getManagedPeriodicTimer(duration, callback); + + @override + Timer getManagedTimer(Duration duration, void callback()) => + _disposable.getManagedTimer(duration, callback); + + @override + Completer manageCompleter(Completer completer) => _disposable + .manageCompleter(completer); + + @override + void manageDisposable(disposable_common.Disposable disposable) => + _disposable.manageDisposable(disposable); + + @override + void manageDisposer(disposable_common.Disposer disposer) => + _disposable.manageDisposer(disposer); + + @override + void manageStreamController(StreamController controller) => + _disposable.manageStreamController(controller); + + @override + void manageStreamSubscription(StreamSubscription subscription) => + _disposable.manageStreamSubscription(subscription); + + /// Callback to allow arbitrary cleanup on dispose. + @protected + @override + Future onDispose() async { + return null; + } + /// Adds an event listener to the document object and removes the event /// listener upon disposal. /// @@ -60,11 +240,8 @@ class Disposable extends disposable_common.Disposable { void _subscribeToEvent(EventTarget eventTarget, String event, EventListener callback, bool useCapture) { eventTarget.addEventListener(event, callback, useCapture); - - var disposable = new disposable_common.InternalDisposable(() { + _disposable.manageDisposer(() { eventTarget.removeEventListener(event, callback, useCapture); }); - - disposable_common.addInternalDisposable(this, disposable); } } diff --git a/lib/src/common/disposable.dart b/lib/src/common/disposable.dart index 5d68833f..53e6d010 100644 --- a/lib/src/common/disposable.dart +++ b/lib/src/common/disposable.dart @@ -77,13 +77,13 @@ class _ObservableTimer implements Timer { /// A function that, when called, disposes of one or more objects. typedef Future Disposer(); -/// Allows the creation of managed objects, including helpers for common patterns. +/// Allows the creation of managed objects, including helpers for common +/// patterns. /// /// There are four ways to consume this class: as a mixin, a base class, /// an interface, and a concrete class used as a proxy. All should work -/// fine but the first is the simplest -/// and most powerful. Using the class as an interface will require -/// significant effort. +/// fine but the first is the simplest and most powerful. Using the class +/// as an interface will require significant effort. /// /// In the case below, the class is used as a mixin. This provides both /// default implementations and flexibility since it does not occupy @@ -480,12 +480,3 @@ class Disposable implements _Disposable, DisposableManagerV3 { } } } - -// TODO: Refactor so that we don't have to use this hacky solution to access -// _internalDisposables from our browser Disposable class. Our original thought -// was to use a factory to return an implementation class, but because consumers -// use Disposable as a mixin, we can't give it a constructor. -void addInternalDisposable( - Disposable disposable, InternalDisposable internalDisposable) { - disposable._internalDisposables.add(internalDisposable); -} diff --git a/test/unit/browser/browser_stubs.dart b/test/unit/browser/browser_stubs.dart index d3256a78..c85aac39 100644 --- a/test/unit/browser/browser_stubs.dart +++ b/test/unit/browser/browser_stubs.dart @@ -12,8 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import 'dart:html'; + +import 'package:mockito/mockito.dart'; import 'package:w_common/disposable_browser.dart'; import '../stubs.dart'; -class DisposableThing extends Disposable with StubDisposable {} +class BrowserDisposable extends Object with Disposable, StubDisposable {} + +class MockEventTarget extends Mock implements EventTarget {} diff --git a/test/unit/browser/disposable_browser_test.dart b/test/unit/browser/disposable_browser_test.dart index d353b6ed..572648ef 100644 --- a/test/unit/browser/disposable_browser_test.dart +++ b/test/unit/browser/disposable_browser_test.dart @@ -17,22 +17,21 @@ import 'dart:html'; import 'package:test/test.dart'; import 'package:mockito/mockito.dart'; +import '../disposable_common.dart'; import './browser_stubs.dart'; void main() { group('Browser Disposable', () { - DisposableThing thing; - - setUp(() { - thing = new DisposableThing(); - }); + testCommonDisposable(() => new BrowserDisposable()); group('events on global singleton', () { + BrowserDisposable disposable; String eventName; bool useCapture; EventListener callback; setUp(() { + disposable = new BrowserDisposable(); callback = (_) {}; eventName = 'event'; useCapture = true; @@ -43,10 +42,10 @@ void main() { () async { var document = new MockEventTarget(); - thing.subscribeToDocumentEvent(eventName, callback, + disposable.subscribeToDocumentEvent(eventName, callback, documentObject: document, useCapture: useCapture); verify(document.addEventListener(eventName, callback, useCapture)); - await thing.dispose(); + await disposable.dispose(); verify(document.removeEventListener(eventName, callback, useCapture)); }); @@ -55,53 +54,49 @@ void main() { () async { var window = new MockEventTarget(); - thing.subscribeToWindowEvent(eventName, callback, + disposable.subscribeToWindowEvent(eventName, callback, windowObject: window, useCapture: useCapture); verify(window.addEventListener(eventName, callback, useCapture)); - await thing.dispose(); + await disposable.dispose(); verify(window.removeEventListener(eventName, callback, useCapture)); }); }); - test( - 'subscribeToDomElementEvent should remove listener when thing is disposed', - () async { - var element = new Element.div(); - var event = new Event('event'); - var eventName = 'event'; - int numberOfEventCallbacks = 0; - EventListener eventCallback = (_) { - numberOfEventCallbacks++; - }; - var shouldNotListenEvent = new Event('shouldNotListenEvent'); - - thing.subscribeToDomElementEvent(element, eventName, eventCallback); - expect(numberOfEventCallbacks, equals(0)); - - element.dispatchEvent(shouldNotListenEvent); - expect(numberOfEventCallbacks, equals(0)); - - element.dispatchEvent(event); - expect(numberOfEventCallbacks, equals(1)); - - await thing.dispose(); - - element.dispatchEvent(event); - expect(numberOfEventCallbacks, equals(1)); - - thing.subscribeToDomElementEvent(element, eventName, eventCallback); - expect(numberOfEventCallbacks, equals(1)); + group('events on DOM element', () { + BrowserDisposable disposable; - element.dispatchEvent(event); - expect(numberOfEventCallbacks, equals(2)); - - element.dispatchEvent(event); - expect(numberOfEventCallbacks, equals(3)); + setUp(() { + disposable = new BrowserDisposable(); + }); - element.dispatchEvent(shouldNotListenEvent); - expect(numberOfEventCallbacks, equals(3)); + test( + 'subscribeToDomElementEvent should remove listener when thing is disposed', + () async { + var element = new Element.div(); + var event = new Event('event'); + var eventName = 'event'; + int numberOfEventCallbacks = 0; + EventListener eventCallback = (_) { + numberOfEventCallbacks++; + }; + var shouldNotListenEvent = new Event('shouldNotListenEvent'); + + disposable.subscribeToDomElementEvent( + element, eventName, eventCallback); + expect(numberOfEventCallbacks, equals(0)); + + element.dispatchEvent(shouldNotListenEvent); + expect(numberOfEventCallbacks, equals(0)); + + element.dispatchEvent(event); + expect(numberOfEventCallbacks, equals(1)); + + await disposable.dispose(); + numberOfEventCallbacks = 0; + + element.dispatchEvent(event); + expect(numberOfEventCallbacks, equals(0)); + }); }); }); } - -class MockEventTarget extends Mock implements EventTarget {} diff --git a/test/unit/disposable_common.dart b/test/unit/disposable_common.dart new file mode 100644 index 00000000..b1614c43 --- /dev/null +++ b/test/unit/disposable_common.dart @@ -0,0 +1,347 @@ +import 'dart:async'; + +import 'package:test/test.dart'; +import 'package:w_common/disposable.dart'; +import 'package:w_common/func.dart'; + +import 'stubs.dart'; + +void testCommonDisposable(Func disposableFactory) { + StubDisposable disposable; + + setUp(() { + disposable = disposableFactory(); + }); + + group('disposalTreeSize', () { + test('should count all managed objects', () { + var controller = new StreamController(); + var subscription = controller.stream.listen((_) {}); + disposable.manageStreamController(controller); + disposable.manageStreamSubscription(subscription); + disposable.manageDisposable(disposableFactory()); + disposable.manageCompleter(new Completer()); + disposable.getManagedTimer(new Duration(days: 1), () {}); + disposable + .getManagedDelayedFuture(new Duration(days: 1), () {}) + .catchError((_) {}); // Because we dispose prematurely. + disposable.getManagedPeriodicTimer(new Duration(days: 1), (_) {}); + expect(disposable.disposalTreeSize, 8); + disposable.dispose().then(expectAsync1((_) { + expect(disposable.disposalTreeSize, 1); + })); + }); + + test('should count nested objects', () { + var nestedThing = disposableFactory(); + nestedThing.manageDisposable(disposableFactory()); + disposable.manageDisposable(nestedThing); + expect(disposable.disposalTreeSize, 3); + disposable.dispose().then(expectAsync1((_) { + expect(disposable.disposalTreeSize, 1); + })); + }); + }); + + group('getManagedDelayedFuture', () { + test('should complete after specified duration', () async { + var start = new DateTime.now().millisecondsSinceEpoch; + await disposable.getManagedDelayedFuture( + new Duration(milliseconds: 10), () => null); + var end = new DateTime.now().millisecondsSinceEpoch; + expect(end - start, greaterThanOrEqualTo(10)); + }); + + test('should complete with an error on premature dispose', () { + var future = + disposable.getManagedDelayedFuture(new Duration(days: 1), () => null); + future.catchError((e) { + expect(e, new isInstanceOf()); + }); + disposable.dispose(); + }); + }); + + group('getManagedTimer', () { + TimerHarness harness; + Timer timer; + + setUp(() { + harness = new TimerHarness(); + timer = + disposable.getManagedTimer(harness.duration, harness.getCallback()); + }); + + test('should cancel timer if disposed before completion', () async { + expect(timer.isActive, isTrue); + await disposable.dispose(); + expect(await harness.didCancelTimer, isTrue); + expect(await harness.didCompleteTimer, isFalse); + }); + + test('disposing should have no effect on timer after it has completed', + () async { + await harness.didConclude; + expect(timer.isActive, isFalse); + await disposable.dispose(); + expect(await harness.didCancelTimer, isFalse); + expect(await harness.didCompleteTimer, isTrue); + }); + + test('should return a timer that can call cancel multiple times', () { + expect(() { + timer.cancel(); + timer.cancel(); + }, returnsNormally); + }); + }); + + group('getManagedPeriodicTimer', () { + TimerHarness harness; + Timer timer; + + setUp(() { + harness = new TimerHarness(); + timer = disposable.getManagedPeriodicTimer( + harness.duration, harness.getPeriodicCallback()); + }); + + test('should cancel timer if disposed before completion', () async { + expect(timer.isActive, isTrue); + await disposable.dispose(); + expect(await harness.didCancelTimer, isTrue); + expect(await harness.didCompleteTimer, isFalse); + }); + + test( + 'disposing should have no effect on timer after it has cancelled by' + ' the consumer', () async { + await harness.didConclude; + expect(timer.isActive, isFalse); + + await disposable.dispose(); + expect(await harness.didCancelTimer, isFalse); + expect(await harness.didCompleteTimer, isTrue); + }); + + test('should return a timer that can call cancel multiple times', () { + expect(() { + timer.cancel(); + timer.cancel(); + }, returnsNormally); + }); + }); + + group('onDispose', () { + test('should be called when dispose() is called', () async { + expect(disposable.wasOnDisposeCalled, isFalse); + await disposable.dispose(); + expect(disposable.wasOnDisposeCalled, isTrue); + }); + }); + + void testManageMethod( + String methodName, callback(dynamic argument), dynamic argument, + {bool doesCallbackReturn: true}) { + if (doesCallbackReturn) { + test('should return the argument', () { + expect(callback(argument), same(argument)); + }); + } + + test('should throw if called with a null argument', () { + expect(() => callback(null), throwsArgumentError); + }); + + test('should throw if object is disposing', () async { + disposable.manageDisposer(() async { + expect(() => callback(argument), throwsStateError); + }); + await disposable.dispose(); + }); + + test('should throw if object has been disposed', () async { + await disposable.dispose(); + expect(() => callback(argument), throwsStateError); + }); + } + + group('awaitBeforeDispose', () { + test('should wait for the future to complete before disposing', () async { + var completer = new Completer(); + var awaitedFuture = disposable.awaitBeforeDispose(completer.future); + var disposeFuture = disposable.dispose().then((_) { + expect(disposable.isDisposing, isFalse, + reason: 'isDisposing post-complete'); + expect(disposable.isDisposed, isTrue, + reason: 'isDisposed post-complete'); + }); + await new Future(() {}); + expect(disposable.isDisposing, isTrue, + reason: 'isDisposing pre-complete'); + expect(disposable.isDisposed, isFalse, reason: 'isDisposed pre-complete'); + completer.complete(); + // It's simpler to do this than ignore a bunch of lints. + await Future.wait([awaitedFuture, disposeFuture]); + }); + + testManageMethod( + 'waitBeforeDispose', + (argument) => disposable.awaitBeforeDispose(argument), + new Future(() {})); + }); + + group('manageCompleter', () { + test('should complete with an error when parent is disposed', () { + var completer = new Completer(); + completer.future.catchError(expectAsync1((exception) { + expect(exception, new isInstanceOf()); + })); + disposable.manageCompleter(completer); + disposable.dispose(); + }); + + test('should be unmanaged after completion', () { + var completer = new Completer(); + disposable.manageCompleter(completer); + completer.complete(null); + expect(() => disposable.dispose(), returnsNormally); + }); + + testManageMethod( + 'manageCompleter', + (argument) => disposable.manageCompleter(argument), + new Completer()); + }); + + group('manageDisposable', () { + test('should dispose child when parent is disposed', () async { + var childThing = disposableFactory(); + disposable.manageDisposable(childThing); + expect(childThing.isDisposed, isFalse); + await disposable.dispose(); + expect(childThing.isDisposed, isTrue); + }); + + test('should remove disposable from internal collection if disposed', + () async { + var disposeCounter = new DisposeCounter(); + + // Manage the disposable child and dispose of it independently + disposable.manageDisposable(disposeCounter); + await disposeCounter.dispose(); + await disposable.dispose(); + + expect(disposeCounter.disposeCount, 1); + }); + + testManageMethod( + 'manageDisposable', + (argument) => disposable.manageDisposable(argument), + disposableFactory(), + doesCallbackReturn: false); + }); + + group('manageDisposer', () { + test( + 'should call callback and accept null return value' + 'when parent is disposed', () async { + disposable.manageDisposer(expectAsync0(() => null)); + await disposable.dispose(); + }); + + test( + 'should call callback and accept Future return value' + 'when parent is disposed', () async { + disposable.manageDisposer(expectAsync0(() => new Future(() {}))); + await disposable.dispose(); + }); + + testManageMethod('manageDisposer', + (argument) => disposable.manageDisposer(argument), () async => null, + doesCallbackReturn: false); + }); + + group('manageStreamController', () { + test('should close a broadcast stream when parent is disposed', () async { + var controller = new StreamController.broadcast(); + disposable.manageStreamController(controller); + expect(controller.isClosed, isFalse); + await disposable.dispose(); + expect(controller.isClosed, isTrue); + }); + + test('should close a single-subscription stream when parent is disposed', + () async { + var controller = new StreamController(); + var subscription = + controller.stream.listen(expectAsync1(([_]) {}, count: 0)); + subscription.onDone(expectAsync1(([_]) {})); + disposable.manageStreamController(controller); + expect(controller.isClosed, isFalse); + await disposable.dispose(); + expect(controller.isClosed, isTrue); + await subscription.cancel(); + await controller.close(); + }); + + test( + 'should complete normally for a single-subscription stream, with ' + 'a listener, that has been closed when parent is disposed', () async { + var controller = new StreamController(); + var sub = controller.stream.listen(expectAsync1((_) {}, count: 0)); + disposable.manageStreamController(controller); + await controller.close(); + await disposable.dispose(); + await sub.cancel(); + }); + + test( + 'should complete normally for a single-subscription stream with a ' + 'canceled listener when parent is disposed', () async { + var controller = new StreamController(); + var sub = controller.stream.listen(expectAsync1((_) {}, count: 0)); + disposable.manageStreamController(controller); + await sub.cancel(); + await disposable.dispose(); + }); + + test( + 'should close a single-subscription stream that never had a ' + 'listener when parent is disposed', () async { + var controller = new StreamController(); + disposable.manageStreamController(controller); + expect(controller.isClosed, isFalse); + await disposable.dispose(); + expect(controller.isClosed, isTrue); + }); + + testManageMethod( + 'manageStreamController', + (argument) => disposable.manageStreamController(argument), + new StreamController(), + doesCallbackReturn: false); + }); + + group('manageStreamSubscription', () { + test('should cancel subscription when parent is disposed', () async { + var controller = new StreamController(); + controller.onCancel = expectAsync1(([_]) {}); + var subscription = + controller.stream.listen(expectAsync1((_) {}, count: 0)); + disposable.manageStreamSubscription(subscription); + await disposable.dispose(); + controller.add(null); + await subscription.cancel(); + await controller.close(); + }); + + var controller = new StreamController(); + testManageMethod( + 'manageStreamSubscription', + (argument) => disposable.manageStreamSubscription(argument), + controller.stream.listen((_) {}), + doesCallbackReturn: false); + controller.close(); + }); +} diff --git a/test/unit/vm/disposable_vm_test.dart b/test/unit/vm/disposable_vm_test.dart index e1b6d73a..b2576463 100644 --- a/test/unit/vm/disposable_vm_test.dart +++ b/test/unit/vm/disposable_vm_test.dart @@ -12,344 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import 'dart:async'; - import 'package:test/test.dart'; -import 'package:w_common/disposable.dart'; - -import '../stubs.dart'; +import '../disposable_common.dart'; import './vm_stubs.dart'; void main() { group('Disposable', () { - DisposableThing thing; - - setUp(() { - thing = new DisposableThing(); - }); - - group('disposalTreeSize', () { - test('should count all managed objects', () { - var controller = new StreamController(); - var subscription = controller.stream.listen((_) {}); - thing.manageStreamController(controller); - thing.manageStreamSubscription(subscription); - thing.manageDisposable(new DisposableThing()); - thing.manageCompleter(new Completer()); - thing.getManagedTimer(new Duration(days: 1), () {}); - thing - .getManagedDelayedFuture(new Duration(days: 1), () {}) - .catchError((_) {}); // Because we dispose prematurely. - thing.getManagedPeriodicTimer(new Duration(days: 1), (_) {}); - expect(thing.disposalTreeSize, 8); - thing.dispose().then(expectAsync1((_) { - expect(thing.disposalTreeSize, 1); - })); - }); - - test('should count nested objects', () { - var nestedThing = new DisposableThing(); - nestedThing.manageDisposable(new DisposableThing()); - thing.manageDisposable(nestedThing); - expect(thing.disposalTreeSize, 3); - thing.dispose().then(expectAsync1((_) { - expect(thing.disposalTreeSize, 1); - })); - }); - }); - - group('getManagedDelayedFuture', () { - test('should complete after specified duration', () async { - var start = new DateTime.now().millisecondsSinceEpoch; - await thing.getManagedDelayedFuture( - new Duration(milliseconds: 10), () => null); - var end = new DateTime.now().millisecondsSinceEpoch; - expect(end - start, greaterThanOrEqualTo(10)); - }); - - test('should complete with an error on premature dispose', () { - var future = - thing.getManagedDelayedFuture(new Duration(days: 1), () => null); - future.catchError((e) { - expect(e, new isInstanceOf()); - }); - thing.dispose(); - }); - }); - - group('getManagedTimer', () { - TimerHarness harness; - Timer timer; - - setUp(() { - harness = new TimerHarness(); - timer = thing.getManagedTimer(harness.duration, harness.getCallback()); - }); - - test('should cancel timer if disposed before completion', () async { - expect(timer.isActive, isTrue); - await thing.dispose(); - expect(await harness.didCancelTimer, isTrue); - expect(await harness.didCompleteTimer, isFalse); - }); - - test('disposing should have no effect on timer after it has completed', - () async { - await harness.didConclude; - expect(timer.isActive, isFalse); - await thing.dispose(); - expect(await harness.didCancelTimer, isFalse); - expect(await harness.didCompleteTimer, isTrue); - }); - - test('should return a timer that can call cancel multiple times', () { - expect(() { - timer.cancel(); - timer.cancel(); - }, returnsNormally); - }); - }); - - group('getManagedPeriodicTimer', () { - TimerHarness harness; - Timer timer; - - setUp(() { - harness = new TimerHarness(); - timer = thing.getManagedPeriodicTimer( - harness.duration, harness.getPeriodicCallback()); - }); - - test('should cancel timer if disposed before completion', () async { - expect(timer.isActive, isTrue); - await thing.dispose(); - expect(await harness.didCancelTimer, isTrue); - expect(await harness.didCompleteTimer, isFalse); - }); - - test( - 'disposing should have no effect on timer after it has cancelled by' - ' the consumer', () async { - await harness.didConclude; - expect(timer.isActive, isFalse); - - await thing.dispose(); - expect(await harness.didCancelTimer, isFalse); - expect(await harness.didCompleteTimer, isTrue); - }); - - test('should return a timer that can call cancel multiple times', () { - expect(() { - timer.cancel(); - timer.cancel(); - }, returnsNormally); - }); - }); - - group('onDispose', () { - test('should be called when dispose() is called', () async { - expect(thing.wasOnDisposeCalled, isFalse); - await thing.dispose(); - expect(thing.wasOnDisposeCalled, isTrue); - }); - }); - - void testManageMethod( - String methodName, callback(dynamic argument), dynamic argument, - {bool doesCallbackReturn: true}) { - if (doesCallbackReturn) { - test('should return the argument', () { - expect(callback(argument), same(argument)); - }); - } - - test('should throw if called with a null argument', () { - expect(() => callback(null), throwsArgumentError); - }); - - test('should throw if object is disposing', () async { - thing.manageDisposer(() async { - expect(() => callback(argument), throwsStateError); - }); - await thing.dispose(); - }); - - test('should throw if object has been disposed', () async { - await thing.dispose(); - expect(() => callback(argument), throwsStateError); - }); - } - - group('awaitBeforeDispose', () { - test('should wait for the future to complete before disposing', () async { - var completer = new Completer(); - var awaitedFuture = thing.awaitBeforeDispose(completer.future); - var disposeFuture = thing.dispose().then((_) { - expect(thing.isDisposing, isFalse, - reason: 'isDisposing post-complete'); - expect(thing.isDisposed, isTrue, reason: 'isDisposed post-complete'); - }); - await new Future(() {}); - expect(thing.isDisposing, isTrue, reason: 'isDisposing pre-complete'); - expect(thing.isDisposed, isFalse, reason: 'isDisposed pre-complete'); - completer.complete(); - // It's simpler to do this than ignore a bunch of lints. - await Future.wait([awaitedFuture, disposeFuture]); - }); - - testManageMethod('waitBeforeDispose', - (argument) => thing.awaitBeforeDispose(argument), new Future(() {})); - }); - - group('manageCompleter', () { - test('should complete with an error when parent is disposed', () { - var completer = new Completer(); - completer.future.catchError(expectAsync1((exception) { - expect(exception, new isInstanceOf()); - })); - thing.manageCompleter(completer); - thing.dispose(); - }); - - test('should be unmanaged after completion', () { - var completer = new Completer(); - thing.manageCompleter(completer); - completer.complete(null); - expect(() => thing.dispose(), returnsNormally); - }); - - testManageMethod('manageCompleter', - (argument) => thing.manageCompleter(argument), new Completer()); - }); - - group('manageDisposable', () { - test('should dispose child when parent is disposed', () async { - var childThing = new DisposableThing(); - thing.manageDisposable(childThing); - expect(childThing.isDisposed, isFalse); - await thing.dispose(); - expect(childThing.isDisposed, isTrue); - }); - - test('should remove disposable from internal collection if disposed', - () async { - var disposable = new DisposeCounter(); - - // Manage the disposable child and dispose of it independently - thing.manageDisposable(disposable); - await disposable.dispose(); - await thing.dispose(); - - expect(disposable.disposeCount, 1); - }); - - testManageMethod('manageDisposable', - (argument) => thing.manageDisposable(argument), new DisposableThing(), - doesCallbackReturn: false); - }); - - group('manageDisposer', () { - test( - 'should call callback and accept null return value' - 'when parent is disposed', () async { - thing.manageDisposer(expectAsync0(() => null)); - await thing.dispose(); - }); - - test( - 'should call callback and accept Future return value' - 'when parent is disposed', () async { - thing.manageDisposer(expectAsync0(() => new Future(() {}))); - await thing.dispose(); - }); - - testManageMethod('manageDisposer', - (argument) => thing.manageDisposer(argument), () async => null, - doesCallbackReturn: false); - }); - - group('manageStreamController', () { - test('should close a broadcast stream when parent is disposed', () async { - var controller = new StreamController.broadcast(); - thing.manageStreamController(controller); - expect(controller.isClosed, isFalse); - await thing.dispose(); - expect(controller.isClosed, isTrue); - }); - - test('should close a single-subscription stream when parent is disposed', - () async { - var controller = new StreamController(); - var subscription = - controller.stream.listen(expectAsync1(([_]) {}, count: 0)); - subscription.onDone(expectAsync1(([_]) {})); - thing.manageStreamController(controller); - expect(controller.isClosed, isFalse); - await thing.dispose(); - expect(controller.isClosed, isTrue); - await subscription.cancel(); - await controller.close(); - }); - - test( - 'should complete normally for a single-subscription stream, with ' - 'a listener, that has been closed when parent is disposed', () async { - var controller = new StreamController(); - var sub = controller.stream.listen(expectAsync1((_) {}, count: 0)); - thing.manageStreamController(controller); - await controller.close(); - await thing.dispose(); - await sub.cancel(); - }); - - test( - 'should complete normally for a single-subscription stream with a ' - 'canceled listener when parent is disposed', () async { - var controller = new StreamController(); - var sub = controller.stream.listen(expectAsync1((_) {}, count: 0)); - thing.manageStreamController(controller); - await sub.cancel(); - await thing.dispose(); - }); - - test( - 'should close a single-subscription stream that never had a ' - 'listener when parent is disposed', () async { - var controller = new StreamController(); - thing.manageStreamController(controller); - expect(controller.isClosed, isFalse); - await thing.dispose(); - expect(controller.isClosed, isTrue); - }); - - testManageMethod( - 'manageStreamController', - (argument) => thing.manageStreamController(argument), - new StreamController(), - doesCallbackReturn: false); - }); - - group('manageStreamSubscription', () { - test('should cancel subscription when parent is disposed', () async { - var controller = new StreamController(); - controller.onCancel = expectAsync1(([_]) {}); - var subscription = - controller.stream.listen(expectAsync1((_) {}, count: 0)); - thing.manageStreamSubscription(subscription); - await thing.dispose(); - controller.add(null); - await subscription.cancel(); - await controller.close(); - }); - - var controller = new StreamController(); - testManageMethod( - 'manageStreamSubscription', - (argument) => thing.manageStreamSubscription(argument), - controller.stream.listen((_) {}), - doesCallbackReturn: false); - controller.close(); - }); + testCommonDisposable(() => new VMDisposable()); }); } diff --git a/test/unit/vm/vm_stubs.dart b/test/unit/vm/vm_stubs.dart index 3b2240dd..0a5d73b6 100644 --- a/test/unit/vm/vm_stubs.dart +++ b/test/unit/vm/vm_stubs.dart @@ -16,4 +16,4 @@ import 'package:w_common/disposable.dart'; import '../stubs.dart'; -class DisposableThing extends Disposable with StubDisposable {} +class VMDisposable extends Object with Disposable, StubDisposable {}