blob: ccdb7435d99ba817d4f98e8bdee491d7d9220b1c [file] [edit]
// 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 'dart:convert';
import 'package:meta/meta.dart' as meta;
import '../../context.dart';
extension FunctionChecks<T> on Subject<T Function()> {
/// Expects that a function throws synchronously when it is called.
///
/// If the function synchronously throws a value of type [E], return a
/// [Subject] to check further expectations on the error.
///
/// If the function does not throw synchronously, or if it throws an error
/// that is not of type [E], this expectation will fail.
///
/// If this function is async and returns a [Future], this expectation will
/// fail. Instead invoke the function and check the expectation on the
/// returned [Future].
///
/// {@example /example/function/function/throws.dart}
Subject<E> throws<E>([Condition<E>? that]) => context.nest<E>(
() {
var label = 'throws an error';
if (const Object() is! E) {
label = '$label of type $E';
}
return [label];
},
addPredicate: (predicateNoun) => 'throws $predicateNoun',
(actual) {
try {
final result = actual();
return Extracted.rejection(
actual: prefixFirst('a function that returned ', literal(result)),
which: ['did not throw'],
);
} catch (e, st) {
if (e is E) return Extracted.value(e as E);
return Extracted.rejection(
actual: prefixFirst('a function that threw error ', literal(e)),
which: [
'threw an exception that is not a $E at:',
...indent(LineSplitter.split(st.toString())),
],
);
}
},
nestedCondition: that,
);
/// Expects that the function returns without throwing.
///
/// If the function runs without exception, return a [Subject] to check
/// further expecations on the returned value.
///
/// If the function throws synchronously, this expectation will fail.
///
/// {@example /example/function/function/returns_normally.dart}
Subject<T> returnsNormally([Condition<T>? that]) => context.nest<T>(
() => ['returns a value'],
addPredicate: (predicateNoun) => 'returns $predicateNoun',
(actual) {
try {
return Extracted.value(actual());
} catch (e, st) {
return Extracted.rejection(
actual: ['a function that throws'],
which: [
...prefixFirst('threw ', postfixLast(' at:', literal(e))),
...indent(LineSplitter.split(st.toString())),
],
);
}
},
nestedCondition: that,
);
}
/// Expectation extensions which need to have lower precedence than
/// [AsyncFuntionChecks].
extension VoidFunctionChecks on Subject<void Function()> {
/// Expects that the function prints text when called.
///
/// Intercepts calls to [print] while the function is executed and returns
/// a [Subject] to check expectations on the captured printed output.
///
/// If the function throws synchronously, this expectation will fail.
///
/// {@example /example/function/function/prints.dart}
Subject<String> prints([Condition<String>? that]) {
return context.nest<String>(
() => ['prints'],
addPredicate: (predicateNoun) => 'prints $predicateNoun',
(actual) {
final buffer = StringBuffer();
final Object? result;
try {
result = runZoned(
actual,
zoneSpecification: ZoneSpecification(
print: (_, _, _, line) {
buffer.writeln(line);
},
),
);
} catch (e, st) {
return Extracted.rejection(
actual: ['a function that throws'],
which: [
...prefixFirst('threw ', postfixLast(' at:', literal(e))),
...indent(LineSplitter.split(st.toString())),
],
);
}
assert(
result is! Future,
'Function returned a Future. Provide a `Future<void> Function()` to '
'check asynchronous prints.',
);
return Extracted.value(buffer.toString());
},
nestedCondition: that,
);
}
}
extension AsyncFunctionChecks on Subject<Future<void> Function()> {
/// Expects that the asynchronous function prints text when called and
/// completed.
///
/// Intercepts calls to [print] while the function is executed and waits
/// for the returned [Future] to complete before checking expectations
/// on the captured printed output.
///
/// If the function throws synchronously or returns a [Future] that completes
/// with an error, this expectation will fail.
///
/// {@example /example/function/async_function/prints.dart}
@meta.awaitNotRequired
Future<Subject<String>> prints([Condition<String>? printCondition]) {
return context.nestAsync<String>(
() => ['prints'],
addPredicate: (predicateNoun) => 'prints $predicateNoun',
(actual) async {
final buffer = StringBuffer();
try {
await runZoned(
actual,
zoneSpecification: ZoneSpecification(
print: (_, _, _, line) {
buffer.writeln(line);
},
),
);
return Extracted.value(buffer.toString());
} catch (e, st) {
return Extracted.rejection(
actual: ['a function that throws'],
which: [
...prefixFirst('threw ', postfixLast(' at:', literal(e))),
...indent(LineSplitter.split(st.toString())),
],
);
}
},
printCondition,
);
}
}