blob: ea0592058d2173673099b74d23109a01838b935e [file]
// Copyright (c) 2022, 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 'dart:async';
import 'package:async/async.dart';
import 'package:checks/checks.dart';
import 'package:checks/context.dart';
import 'package:test/scaffolding.dart';
import 'package:test_api/hooks.dart';
import '../test_shared.dart';
void main() {
group('FutureChecks', () {
group('completes', () {
test('succeeds for a future that completes to a value', () async {
await check(_futureSuccess()).completes((it) => it.equals(42));
});
test('rejects futures which complete as errors', () async {
await check(_futureFail()).isRejectedByAsync(
(it) => it.completes((it) => it.equals(1)),
actual: ['a future that completes as an error'],
which: ['threw <UnimplementedError> at:', ' fake trace'],
);
});
test('can be described', () async {
await check(
(Subject<Future> it) => it.completes(),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' completes to a value']),
);
await check(
(Subject<Future> it) => it.completes((it) => it.equals(42)),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([
' completes to a value that:',
' equals <42>',
]),
);
});
});
group('throws', () {
test(
'succeeds for a future that compeletes to an error of the expected type',
() async {
await check(_futureFail()).throws<UnimplementedError>(
(it) => it.has((p0) => p0.message, 'message').isNull(),
);
},
);
test('fails for futures that complete to a value', () async {
await check(_futureSuccess()).isRejectedByAsync(
(it) => it.throws(),
actual: ['completed to <42>'],
which: ['did not throw'],
);
});
test(
'failes for futures that complete to an error of the wrong type',
() async {
await check(_futureFail()).isRejectedByAsync(
(it) => it.throws<StateError>(),
actual: ['completed to error <UnimplementedError>'],
which: [
'threw an exception that is not a StateError at:',
' fake trace',
],
);
},
);
test('can be described', () async {
await check(
(Subject<Future<void>> it) => it.throws(),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' completes to an error']),
);
await check(
(Subject<Future<void>> it) => it.throws<StateError>(),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' completes to an error of type StateError']),
);
});
});
group('doesNotComplete', () {
test('succeeds for a Future that never completes', () async {
check(Completer<void>().future).doesNotComplete();
});
test('fails for a Future that completes as a value', () async {
Object? testFailure;
runZonedGuarded(
() {
final completer = Completer<String>();
check(completer.future).doesNotComplete();
completer.complete('value');
},
(e, st) {
testFailure = e;
},
);
await pumpEventQueue();
check(testFailure)
.isA<TestFailure>()
.has((f) => f.message, 'message')
.isNotNull()
.equals('''
Expected: a Future<String> that:
does not complete
Actual: a future that completed to 'value\'''');
});
test('fails for a Future that completes as an error', () async {
Object? testFailure;
runZonedGuarded(
() {
final completer = Completer<String>();
check(completer.future).doesNotComplete();
completer.completeError(
'error',
StackTrace.fromString('fake trace'),
);
},
(e, st) {
testFailure = e;
},
);
await pumpEventQueue();
check(testFailure)
.isA<TestFailure>()
.has((f) => f.message, 'message')
.isNotNull()
.equals('''
Expected: a Future<String> that:
does not complete
Actual: a future that completed as an error
Which: threw 'error' at:
fake trace''');
});
test('can be described', () async {
await check(
(Subject<Future<void>> it) => it.doesNotComplete(),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' does not complete']),
);
});
});
});
group('StreamChecks', () {
group('emits', () {
test('succeeds for a stream that emits a value', () async {
await check(_countingStream(5)).emits((it) => it.equals(0));
});
test('fails for a stream that closes without emitting', () async {
await check(_countingStream(0)).isRejectedByAsync(
(it) => it.emits(),
actual: ['a stream'],
which: ['closed without emitting enough values'],
);
});
test('fails for a stream that emits an error', () async {
await check(_countingStream(1, errorAt: 0)).isRejectedByAsync(
(it) => it.emits(),
actual: ['a stream with error <UnimplementedError: Error at 1>'],
which: ['emitted an error instead of a value at:', ' fake trace'],
);
});
test('can be described', () async {
await check(
(Subject<StreamQueue<void>> it) => it.emits(),
).hasAsyncDescriptionWhich((it) => it.deepEquals([' emits a value']));
await check(
(Subject<StreamQueue<int>> it) => it.emits((it) => it.equals(42)),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' emits a value that:', ' equals <42>']),
);
});
test('does not consume error', () async {
final queue = _countingStream(1, errorAt: 0);
await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emits());
await check(queue).emitsError();
});
});
group('emitsError', () {
test('succeeds for a stream that emits an error', () async {
await check(
_countingStream(1, errorAt: 0),
).emitsError<UnimplementedError>();
});
test(
'fails for a stream that closes without emitting an error',
() async {
await check(_countingStream(0)).isRejectedByAsync(
(it) => it.emitsError(),
actual: ['a stream'],
which: ['closed without emitting an expected error'],
);
},
);
test('fails for a stream that emits value', () async {
await check(_countingStream(1)).isRejectedByAsync(
(it) => it.emitsError(),
actual: ['a stream emitting value <0>'],
which: ['closed without emitting an error'],
);
});
test(
'fails for a stream that emits an error of the incorrect type',
() async {
await check(_countingStream(1, errorAt: 0)).isRejectedByAsync(
(it) => it.emitsError<StateError>(),
actual: ['a stream with error <UnimplementedError: Error at 1>'],
which: [
'emitted an error which is not StateError at:',
' fake trace',
],
);
},
);
test('can be described', () async {
await check(
(Subject<StreamQueue<void>> it) => it.emitsError(),
).hasAsyncDescriptionWhich((it) => it.deepEquals([' emits an error']));
await check(
(Subject<StreamQueue<void>> it) => it.emitsError<StateError>(),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' emits an error of type StateError']),
);
await check(
(Subject<StreamQueue<void>> it) => it
..emitsError<StateError>(
(it) => it.has((e) => e.message, 'message').equals('foo'),
),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([
' emits an error of type StateError that:',
' has message that:',
' equals \'foo\'',
]),
);
});
test('uses a transaction', () async {
final queue = _countingStream(1);
await softCheckAsync<StreamQueue<int>>(queue, (it) => it.emitsError());
await check(queue).emits((it) => it.equals(0));
});
});
group('emitsThrough', () {
test(
'succeeds for a stream that eventuall emits a matching value',
() async {
await check(_countingStream(5)).emitsThrough((it) => it.equals(4));
},
);
test(
'fails for a stream that closes without emitting a matching value',
() async {
await check(_countingStream(4)).isRejectedByAsync(
(it) => it.emitsThrough((it) => it.equals(5)),
actual: ['a stream'],
which: ['ended after emitting 4 elements with none matching'],
);
},
);
test('can be described', () async {
await check(
(Subject<StreamQueue<int>> it) =>
it.emitsThrough((it) => it.equals(42)),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([
' emits any values then emits a value that:',
' equals <42>',
]),
);
});
test('uses a transaction', () async {
final queue = _countingStream(1);
await softCheckAsync(
queue,
(Subject<StreamQueue<int>> it) =>
it.emitsThrough((it) => it.equals(42)),
);
await check(queue).emits((it) => it.equals(0));
});
test('consumes events', () async {
final queue = _countingStream(3);
await check(queue).emitsThrough((it) => it.equals(1));
await check(queue).emits((it) => it.equals(2));
});
});
group('emitsInOrder', () {
test('succeeds for happy case', () async {
await check(_countingStream(2)).inOrder([
(it) => it.emits((it) => it.equals(0)),
(it) => it.emits((it) => it.equals(1)),
(it) => it.isDone(),
]);
});
test('reports which condition failed', () async {
await check(_countingStream(1)).isRejectedByAsync(
(it) => it.inOrder([(it) => it.emits(), (it) => it.emits()]),
actual: ['a stream'],
which: [
'satisfied 1 conditions then',
'failed to satisfy the condition at index 1',
'because it closed without emitting enough values',
],
);
});
test('nestes the report for deep failures', () async {
await check(_countingStream(2)).isRejectedByAsync(
(it) => it.inOrder([
(it) => it.emits(),
(it) => it.emits((it) => it.equals(2)),
]),
actual: ['a stream'],
which: [
'satisfied 1 conditions then',
'failed to satisfy the condition at index 1',
'because it:',
' emits a value that:',
' Actual: <1>',
' Which: are not equal',
],
);
});
test('gets described with the number of conditions', () async {
await check(
(Subject<StreamQueue<int>> it) => it.inOrder([(_) {}, (_) {}]),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' satisfies 2 conditions in order']),
);
});
test('uses a transaction', () async {
final queue = _countingStream(3);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.inOrder([
(it) => it.emits((it) => it.equals(0)),
(it) => it.emits((it) => it.equals(1)),
(it) => it.emits((it) => it.equals(42)),
]),
);
await check(queue).inOrder([
(it) => it.emits((it) => it.equals(0)),
(it) => it.emits((it) => it.equals(1)),
(it) => it.emits((it) => it.equals(2)),
(it) => it.isDone(),
]);
});
test('consumes events', () async {
final queue = _countingStream(3);
await check(queue).inOrder([(it) => it.emits(), (it) => it.emits()]);
await check(queue).emits((it) => it.equals(2));
});
});
group('neverEmits', () {
test(
'succeeds for a stream that closes without emitting a matching value',
() async {
await check(_countingStream(5)).neverEmits((it) => it.equals(5));
},
);
test('fails for a stream that emits a matching value', () async {
await check(_countingStream(6)).isRejectedByAsync(
(it) => it.neverEmits((it) => it.equals(5)),
actual: ['a stream'],
which: ['emitted <5>', 'following 5 other items'],
);
});
test('can be described', () async {
await check(
(Subject<StreamQueue<int>> it) =>
it.neverEmits((it) => it.equals(42)),
).hasAsyncDescriptionWhich(
(it) =>
it.deepEquals([' never emits a value that:', ' equals <42>']),
);
});
test('uses a transaction', () async {
final queue = _countingStream(2);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.neverEmits((it) => it.equals(1)),
);
await check(queue).inOrder([
(it) => it.emits((it) => it.equals(0)),
(it) => it.emits((it) => it.equals(1)),
(it) => it.isDone(),
]);
});
});
group('mayEmit', () {
test('succeeds for a stream that emits a matching value', () async {
await check(_countingStream(1)).mayEmit((it) => it.equals(0));
});
test('succeeds for a stream that emits an error', () async {
await check(
_countingStream(1, errorAt: 0),
).mayEmit((it) => it.equals(0));
});
test('succeeds for a stream that closes', () async {
await check(_countingStream(0)).mayEmit((it) => it.equals(42));
});
test('consumes a matching event', () async {
final queue = _countingStream(2);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmit((it) => it.equals(0)),
);
await check(queue).emits((it) => it.equals(1));
});
test('consumes a matching async event', () async {
final queue = StreamQueue(Stream.value(Future.value(1)));
await softCheckAsync<StreamQueue<Future<int>>>(
queue,
(it) => it.mayEmit((it) => it.completes((it) => it.equals(1))),
);
await check(queue).isDone();
});
test('does not consume a non-matching event', () async {
final queue = _countingStream(2);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmit((it) => it.equals(1)),
);
await check(queue).emits((it) => it.equals(0));
});
test('does not consume an error', () async {
final queue = _countingStream(1, errorAt: 0);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmit((it) => it.equals(0)),
);
await check(queue).emitsError<UnimplementedError>(
(it) => it.has((e) => e.message, 'message').equals('Error at 1'),
);
});
test('can be described when condition is async', () async {
await check(
(Subject<StreamQueue<Future<int>>> it) =>
it.mayEmit((it) => it.completes()),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([
' may emit a value satisfying an asynchronous condition',
]),
);
});
});
group('mayEmitMultiple', () {
test('succeeds for a stream that emits a matching value', () async {
await check(_countingStream(1)).mayEmitMultiple((it) => it.equals(0));
});
test('succeeds for a stream that emits an error', () async {
await check(
_countingStream(1, errorAt: 0),
).mayEmitMultiple((it) => it.equals(0));
});
test('succeeds for a stream that closes', () async {
await check(_countingStream(0)).mayEmitMultiple((it) => it.equals(42));
});
test('consumes matching events', () async {
final queue = _countingStream(3);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmitMultiple((it) => it.isLessThan(2)),
);
await check(queue).emits((it) => it.equals(2));
});
test('consumes no events if no events match', () async {
final queue = _countingStream(2);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmitMultiple((it) => it.isLessThan(0)),
);
await check(queue).emits((it) => it.equals(0));
});
test('does not consume an error', () async {
final queue = _countingStream(1, errorAt: 0);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.mayEmitMultiple((it) => it.equals(0)),
);
await check(queue).emitsError<UnimplementedError>(
(it) => it.has((e) => e.message, 'message').equals('Error at 1'),
);
});
test('consumes a matching async event', () async {
final queue = StreamQueue(Stream.value(Future.value(1)));
await softCheckAsync<StreamQueue<Future<int>>>(
queue,
(it) =>
it.mayEmitMultiple((it) => it.completes((it) => it.equals(1))),
);
await check(queue).isDone();
});
test('can be described when condition is async', () async {
await check(
(Subject<StreamQueue<Future<int>>> it) =>
it.mayEmitMultiple((it) => it.completes()),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([
' may emit a value satisfying an asynchronous condition',
]),
);
});
});
group('isDone', () {
test('succeeds for an empty stream', () async {
await check(_countingStream(0)).isDone();
});
test('fails for a stream that emits a value', () async {
await check(_countingStream(1)).isRejectedByAsync(
(it) => it.isDone(),
actual: ['a stream'],
which: ['emitted an unexpected value: <0>'],
);
});
test('fails for a stream that emits an error', () async {
final controller = StreamController<void>();
controller.addError('sad', StackTrace.fromString('fake trace'));
await check(StreamQueue(controller.stream)).isRejectedByAsync(
(it) => it.isDone(),
actual: ['a stream'],
which: ['emitted an unexpected error: \'sad\' at:', ' fake trace'],
);
});
test('uses a transaction', () async {
final queue = _countingStream(1);
await softCheckAsync<StreamQueue<int>>(queue, (it) => it.isDone());
await check(queue).emits((it) => it.equals(0));
});
test('can be described', () async {
await check(
(Subject<StreamQueue<int>> it) => it.isDone(),
).hasAsyncDescriptionWhich((it) => it.deepEquals([' is done']));
});
});
group('emitsAnyOf', () {
test('succeeds for a stream that matches one condition', () async {
await check(_countingStream(1)).anyOf([
(it) => it.emits((it) => it.equals(42)),
(it) => it.emits((it) => it.equals(0)),
]);
});
test('fails for a stream that matches no conditions', () async {
await check(_countingStream(0)).isRejectedByAsync(
(it) => it.anyOf([
(it) => it.emits(),
(it) => it.emitsThrough((it) => it.equals(1)),
]),
actual: ['a stream'],
which: [
'failed to satisfy any condition',
'failed the condition at index 0 because it:',
' closed without emitting enough values',
'failed the condition at index 1 because it:',
' ended after emitting 0 elements with none matching',
],
);
});
test('includes nested details for nested failures', () async {
await check(_countingStream(1)).isRejectedByAsync(
(it) => it.anyOf([
(it) => it.emits((it) => it.equals(42)),
(it) => it.emitsThrough((it) => it.equals(10)),
]),
actual: ['a stream'],
which: [
'failed to satisfy any condition',
'failed the condition at index 0 because it:',
' emits a value that:',
' Actual: <0>',
' Which: are not equal',
'failed the condition at index 1 because it:',
' ended after emitting 1 elements with none matching',
],
);
});
test('gets described with the number of conditions', () async {
await check(
(Subject<StreamQueue<int>> it) =>
it..anyOf([(it) => it.emits(), (it) => it.emits()]),
).hasAsyncDescriptionWhich(
(it) => it.deepEquals([' satisfies any of 2 conditions']),
);
});
test('uses a transaction', () async {
final queue = _countingStream(1);
await softCheckAsync<StreamQueue<int>>(
queue,
(it) => it.anyOf([
(it) => it.emits((it) => it.equals(10)),
(it) => it.emitsThrough((it) => it.equals(42)),
]),
);
await check(queue).emits((it) => it.equals(0));
});
test('consumes events', () async {
final queue = _countingStream(3);
await check(queue).anyOf([
(it) => it.emits((it) => it.equals(1)),
(it) => it.emitsThrough((it) => it.equals(1)),
]);
await check(queue).emits((it) => it.equals(2));
});
});
});
group('StreamQueueWrap', () {
test('can wrap streams in a queue', () async {
await check(Stream.value(1)).withQueue.emits();
});
});
}
Future<int> _futureSuccess() => Future.microtask(() => 42);
Future<int> _futureFail() =>
Future.error(UnimplementedError(), StackTrace.fromString('fake trace'));
StreamQueue<int> _countingStream(int count, {int? errorAt}) => StreamQueue(
Stream.fromIterable(
Iterable<int>.generate(count, (index) {
if (index == errorAt) {
Error.throwWithStackTrace(
UnimplementedError('Error at $count'),
StackTrace.fromString('fake trace'),
);
}
return index;
}),
),
);