| // 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. | |
| @TestOn('vm') | |
| library; | |
| import 'dart:async'; | |
| import 'package:pub/src/error_group.dart'; | |
| import 'package:test/test.dart'; | |
| late ErrorGroup errorGroup; | |
| // TODO(nweiz): once there's a global error handler, we should test that it does | |
| // and does not get called at appropriate times. See issue 5958. | |
| // | |
| // One particular thing we should test that has no tests now is that a stream | |
| // that has a subscription added and subsequently canceled counts as having no | |
| // listeners. | |
| void main() { | |
| group('with no futures or streams', () { | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| }); | |
| test('should pass signaled errors to .done', () { | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| }); | |
| test("shouldn't allow additional futures or streams once an error has been " | |
| 'signaled', () { | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| expect( | |
| () => errorGroup.registerFuture(Future<void>.value()), | |
| throwsStateError, | |
| ); | |
| expect( | |
| () => errorGroup.registerStream( | |
| StreamController<void>(sync: true).stream, | |
| ), | |
| throwsStateError, | |
| ); | |
| }); | |
| }); | |
| group('with a single future', () { | |
| late Completer completer; | |
| late Future future; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| completer = Completer(); | |
| future = errorGroup.registerFuture(completer.future); | |
| }); | |
| test('should pass through a value from the future', () { | |
| expect(future, completion(equals('value'))); | |
| expect(errorGroup.done, completes); | |
| completer.complete('value'); | |
| }); | |
| test("shouldn't allow additional futures or streams once .done has " | |
| 'been called', () { | |
| completer.complete('value'); | |
| expect( | |
| completer.future.then( | |
| (_) => errorGroup.registerFuture(Future<void>.value()), | |
| ), | |
| throwsStateError, | |
| ); | |
| expect( | |
| completer.future.then( | |
| (_) => errorGroup.registerStream( | |
| StreamController<void>(sync: true).stream, | |
| ), | |
| ), | |
| throwsStateError, | |
| ); | |
| }); | |
| test('should pass through an exception from the future if it has a ' | |
| 'listener', () { | |
| expect(future, throwsFormatException); | |
| // errorGroup shouldn't top-level the exception | |
| completer.completeError(const FormatException()); | |
| }); | |
| test('should notify the error group of an exception from the future even ' | |
| 'if it has a listener', () { | |
| expect(future, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| completer.completeError(const FormatException()); | |
| }); | |
| test('should pass a signaled exception to the future if it has a listener ' | |
| 'and should ignore a subsequent value from that future', () { | |
| expect(future, throwsFormatException); | |
| // errorGroup shouldn't top-level the exception | |
| errorGroup.signalError(const FormatException()); | |
| completer.complete('value'); | |
| }); | |
| test('should pass a signaled exception to the future if it has a listener ' | |
| 'and should ignore a subsequent exception from that future', () { | |
| expect(future, throwsFormatException); | |
| // errorGroup shouldn't top-level the exception | |
| errorGroup.signalError(const FormatException()); | |
| completer.completeError(ArgumentError()); | |
| }); | |
| test('should notify the error group of a signaled exception even if the ' | |
| 'future has a listener', () { | |
| expect(future, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| }); | |
| test('should complete .done if the future receives a value even if the ' | |
| "future doesn't have a listener", () { | |
| expect(errorGroup.done, completes); | |
| completer.complete('value'); | |
| // A listener added afterwards should receive the value | |
| expect(errorGroup.done.then((_) => future), completion(equals('value'))); | |
| }); | |
| test('should pipe an exception from the future to .done if the future ' | |
| "doesn't have a listener", () { | |
| expect(errorGroup.done, throwsFormatException); | |
| completer.completeError(const FormatException()); | |
| // A listener added afterwards should receive the exception | |
| expect( | |
| errorGroup.done.catchError((_) { | |
| expect(future, throwsFormatException); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| test("should pass a signaled exception to .done if the future doesn't have " | |
| 'a listener', () { | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| // A listener added afterwards should receive the exception | |
| expect( | |
| errorGroup.done.catchError((_) { | |
| completer.complete('value'); // should be ignored | |
| expect(future, throwsFormatException); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| }); | |
| group('with multiple futures', () { | |
| late Completer completer1; | |
| late Completer completer2; | |
| late Future future1; | |
| late Future future2; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| completer1 = Completer(); | |
| completer2 = Completer(); | |
| future1 = errorGroup.registerFuture(completer1.future); | |
| future2 = errorGroup.registerFuture(completer2.future); | |
| }); | |
| test('should pipe exceptions from one future to the other and to ' | |
| '.complete', () { | |
| expect(future1, throwsFormatException); | |
| expect(future2, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| completer1.completeError(const FormatException()); | |
| }); | |
| test('each future should be able to complete with a value ' | |
| 'independently', () { | |
| expect(future1, completion(equals('value1'))); | |
| expect(future2, completion(equals('value2'))); | |
| expect(errorGroup.done, completes); | |
| completer1.complete('value1'); | |
| completer2.complete('value2'); | |
| }); | |
| test("shouldn't throw a top-level exception if a future receives an error " | |
| 'after the other listened future completes', () { | |
| expect(future1, completion(equals('value'))); | |
| completer1.complete('value'); | |
| expect( | |
| future1.then((_) { | |
| // shouldn't cause a top-level exception | |
| completer2.completeError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| test("shouldn't throw a top-level exception if an error is signaled after " | |
| 'one listened future completes', () { | |
| expect(future1, completion(equals('value'))); | |
| completer1.complete('value'); | |
| expect( | |
| future1.then((_) { | |
| // shouldn't cause a top-level exception | |
| errorGroup.signalError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| }); | |
| group('with a single stream', () { | |
| late StreamController controller; | |
| late Stream stream; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| controller = StreamController.broadcast(sync: true); | |
| stream = errorGroup.registerStream(controller.stream); | |
| }); | |
| test('should pass through values from the stream', () { | |
| final iter = StreamIterator(stream); | |
| iter.moveNext().then((hasNext) { | |
| expect(hasNext, isTrue); | |
| expect(iter.current, equals(1)); | |
| iter.moveNext().then((hasNext) { | |
| expect(hasNext, isTrue); | |
| expect(iter.current, equals(2)); | |
| expect(iter.moveNext(), completion(isFalse)); | |
| }); | |
| }); | |
| expect(errorGroup.done, completes); | |
| controller | |
| ..add(1) | |
| ..add(2) | |
| ..close(); | |
| }); | |
| test('should pass through an error from the stream if it has a ' | |
| 'listener', () { | |
| expect(stream.first, throwsFormatException); | |
| // errorGroup shouldn't top-level the exception | |
| controller.addError(const FormatException()); | |
| }); | |
| test('should notify the error group of an exception from the stream even ' | |
| 'if it has a listener', () { | |
| expect(stream.first, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| controller.addError(const FormatException()); | |
| }); | |
| test('should pass a signaled exception to the stream if it has a listener ' | |
| 'and should unsubscribe that stream', () { | |
| // errorGroup shouldn't top-level the exception | |
| expect(stream.first, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| expect(() => controller.add('value'), returnsNormally); | |
| }); | |
| test('should notify the error group of a signaled exception even if the ' | |
| 'stream has a listener', () { | |
| expect(stream.first, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| }); | |
| test('should see one value and complete .done when the stream is done even ' | |
| "if the stream doesn't have a listener", () { | |
| expect(errorGroup.done, completes); | |
| controller.add('value'); | |
| controller.close(); | |
| // Now that broadcast controllers have been removed a listener should | |
| // see the value that has been put into the controller. | |
| expect( | |
| errorGroup.done.then((_) => stream.toList()), | |
| completion(equals(['value'])), | |
| ); | |
| }); | |
| }); | |
| group('with a single single-subscription stream', () { | |
| late StreamController controller; | |
| late Stream stream; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| controller = StreamController(sync: true); | |
| stream = errorGroup.registerStream(controller.stream); | |
| }); | |
| test('should complete .done when the stream is done even if the stream ' | |
| "doesn't have a listener", () { | |
| expect(errorGroup.done, completes); | |
| controller.add('value'); | |
| controller.close(); | |
| // A listener added afterwards should receive the value | |
| expect( | |
| errorGroup.done.then((_) => stream.toList()), | |
| completion(equals(['value'])), | |
| ); | |
| }); | |
| test('should pipe an exception from the stream to .done if the stream ' | |
| "doesn't have a listener", () { | |
| expect(errorGroup.done, throwsFormatException); | |
| controller.addError(const FormatException()); | |
| // A listener added afterwards should receive the exception | |
| expect( | |
| errorGroup.done.catchError((_) { | |
| controller.add('value'); // should be ignored | |
| expect(stream.first, throwsFormatException); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| test("should pass a signaled exception to .done if the stream doesn't " | |
| 'have a listener', () { | |
| expect(errorGroup.done, throwsFormatException); | |
| errorGroup.signalError(const FormatException()); | |
| // A listener added afterwards should receive the exception | |
| expect( | |
| errorGroup.done.catchError((_) { | |
| controller.add('value'); // should be ignored | |
| expect(stream.first, throwsFormatException); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| }); | |
| group('with multiple streams', () { | |
| late StreamController controller1; | |
| late StreamController controller2; | |
| late Stream stream1; | |
| late Stream stream2; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| controller1 = StreamController.broadcast(sync: true); | |
| controller2 = StreamController.broadcast(sync: true); | |
| stream1 = errorGroup.registerStream(controller1.stream); | |
| stream2 = errorGroup.registerStream(controller2.stream); | |
| }); | |
| test( | |
| 'should pipe exceptions from one stream to the other and to .done', | |
| () { | |
| expect(stream1.first, throwsFormatException); | |
| expect(stream2.first, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| controller1.addError(const FormatException()); | |
| }, | |
| ); | |
| test('each future should be able to emit values independently', () { | |
| expect(stream1.toList(), completion(equals(['value1.1', 'value1.2']))); | |
| expect(stream2.toList(), completion(equals(['value2.1', 'value2.2']))); | |
| expect(errorGroup.done, completes); | |
| controller1 | |
| ..add('value1.1') | |
| ..add('value1.2') | |
| ..close(); | |
| controller2 | |
| ..add('value2.1') | |
| ..add('value2.2') | |
| ..close(); | |
| }); | |
| test("shouldn't throw a top-level exception if a stream receives an error " | |
| 'after the other listened stream completes', () { | |
| final signal = Completer<void>(); | |
| expect( | |
| stream1.toList().whenComplete(signal.complete), | |
| completion(equals(['value1', 'value2'])), | |
| ); | |
| controller1 | |
| ..add('value1') | |
| ..add('value2') | |
| ..close(); | |
| expect( | |
| signal.future.then((_) { | |
| // shouldn't cause a top-level exception | |
| controller2.addError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| test("shouldn't throw a top-level exception if an error is signaled after " | |
| 'one listened stream completes', () { | |
| final signal = Completer<void>(); | |
| expect( | |
| stream1.toList().whenComplete(signal.complete), | |
| completion(equals(['value1', 'value2'])), | |
| ); | |
| controller1 | |
| ..add('value1') | |
| ..add('value2') | |
| ..close(); | |
| expect( | |
| signal.future.then((_) { | |
| // shouldn't cause a top-level exception | |
| errorGroup.signalError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| }); | |
| group('with a stream and a future', () { | |
| late StreamController controller; | |
| late Stream stream; | |
| late Completer completer; | |
| late Future future; | |
| setUp(() { | |
| errorGroup = ErrorGroup(); | |
| controller = StreamController.broadcast(sync: true); | |
| stream = errorGroup.registerStream(controller.stream); | |
| completer = Completer(); | |
| future = errorGroup.registerFuture(completer.future); | |
| }); | |
| test('should pipe exceptions from the stream to the future', () { | |
| expect(stream.first, throwsFormatException); | |
| expect(future, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| controller.addError(const FormatException()); | |
| }); | |
| test('should pipe exceptions from the future to the stream', () { | |
| expect(stream.first, throwsFormatException); | |
| expect(future, throwsFormatException); | |
| expect(errorGroup.done, throwsFormatException); | |
| completer.completeError(const FormatException()); | |
| }); | |
| test('the stream and the future should be able to complete/emit values ' | |
| 'independently', () { | |
| expect(stream.toList(), completion(equals(['value1.1', 'value1.2']))); | |
| expect(future, completion(equals('value2.0'))); | |
| expect(errorGroup.done, completes); | |
| controller | |
| ..add('value1.1') | |
| ..add('value1.2') | |
| ..close(); | |
| completer.complete('value2.0'); | |
| }); | |
| test( | |
| "shouldn't throw a top-level exception if the stream receives an error " | |
| 'after the listened future completes', | |
| () { | |
| expect(future, completion(equals('value'))); | |
| completer.complete('value'); | |
| expect( | |
| future.then((_) { | |
| // shouldn't cause a top-level exception | |
| controller.addError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }, | |
| ); | |
| test("shouldn't throw a top-level exception if the future receives an " | |
| 'error after the listened stream completes', () { | |
| final signal = Completer<void>(); | |
| expect( | |
| stream.toList().whenComplete(signal.complete), | |
| completion(equals(['value1', 'value2'])), | |
| ); | |
| controller | |
| ..add('value1') | |
| ..add('value2') | |
| ..close(); | |
| expect( | |
| signal.future.then((_) { | |
| // shouldn't cause a top-level exception | |
| completer.completeError(const FormatException()); | |
| }), | |
| completes, | |
| ); | |
| }); | |
| }); | |
| } |