blob: 5a92d14a4e466f7a91fc7a1ec9e9c82b1ce50b35 [file] [log] [blame]
// Copyright (c) 2015, 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.
library unittest.expected_function;
import '../unittest.dart';
import 'internal_test_case.dart';
/// An object used to detect unpassed arguments.
const _PLACEHOLDER = const Object();
// Functions used to check how many arguments a callback takes.
typedef _Func0();
typedef _Func1(a);
typedef _Func2(a, b);
typedef _Func3(a, b, c);
typedef _Func4(a, b, c, d);
typedef _Func5(a, b, c, d, e);
typedef _Func6(a, b, c, d, e, f);
typedef bool _IsDoneCallback();
/// A wrapper for a function that ensures that it's called the appropriate
/// number of times.
///
/// The containing test won't be considered to have completed successfully until
/// this function has been called the appropriate number of times.
///
/// The wrapper function is accessible via [func]. It supports up to six
/// optional and/or required positional arguments, but no named arguments.
class ExpectedFunction<T> {
/// The wrapped callback.
final Function _callback;
/// The minimum number of calls that are expected to be made to the function.
///
/// If fewer calls than this are made, the test will fail.
final int _minExpectedCalls;
/// The maximum number of calls that are expected to be made to the function.
///
/// If more calls than this are made, the test will fail.
final int _maxExpectedCalls;
/// A callback that should return whether the function is not expected to have
/// any more calls.
///
/// This will be called after every time the function is run. The test case
/// won't be allowed to terminate until it returns `true`.
///
/// This may be `null`. If so, the function is considered to be done after
/// it's been run once.
final _IsDoneCallback _isDone;
/// A descriptive name for the function.
final String _id;
/// An optional description of why the function is expected to be called.
///
/// If not passed, this will be an empty string.
final String _reason;
/// The number of times the function has been called.
int _actualCalls = 0;
/// The test case in which this function was wrapped.
final InternalTestCase _testCase;
/// Whether this function has been called the requisite number of times.
bool _complete;
/// Wraps [callback] in a function that asserts that it's called at least
/// [minExpected] times and no more than [maxExpected] times.
///
/// If passed, [id] is used as a descriptive name fo the function and [reason]
/// as a reason it's expected to be called. If [isDone] is passed, the test
/// won't be allowed to complete until it returns `true`.
ExpectedFunction(Function callback, int minExpected, int maxExpected,
{String id, String reason, bool isDone()})
: this._callback = callback,
_minExpectedCalls = minExpected,
_maxExpectedCalls = (maxExpected == 0 && minExpected > 0)
? minExpected
: maxExpected,
this._isDone = isDone,
this._reason = reason == null ? '' : '\n$reason',
this._testCase = currentTestCase as InternalTestCase,
this._id = _makeCallbackId(id, callback) {
ensureInitialized();
if (_testCase == null) {
throw new StateError("No valid test. Did you forget to run your test "
"inside a call to test()?");
}
if (isDone != null || minExpected > 0) {
_testCase.callbackFunctionsOutstanding++;
_complete = false;
} else {
_complete = true;
}
}
/// Tries to find a reasonable name for [callback].
///
/// If [id] is passed, uses that. Otherwise, tries to determine a name from
/// calling `toString`. If no name can be found, returns the empty string.
static String _makeCallbackId(String id, Function callback) {
if (id != null) return "$id ";
// If the callback is not an anonymous closure, try to get the
// name.
var toString = callback.toString();
var prefix = "Function '";
var start = toString.indexOf(prefix);
if (start == -1) return '';
start += prefix.length;
var end = toString.indexOf("'", start);
if (end == -1) return '';
return "${toString.substring(start, end)} ";
}
/// Returns a function that has the same number of positional arguments as the
/// wrapped function (up to a total of 6).
Function get func {
if (_callback is _Func6) return max6;
if (_callback is _Func5) return max5;
if (_callback is _Func4) return max4;
if (_callback is _Func3) return max3;
if (_callback is _Func2) return max2;
if (_callback is _Func1) return max1;
if (_callback is _Func0) return max0;
throw new ArgumentError(
'The wrapped function has more than 6 required arguments');
}
T max0() => max6();
// This indirection is critical. It ensures the returned function has an
// argument count of zero.
T max1([Object a0 = _PLACEHOLDER]) => max6(a0);
T max2([Object a0 = _PLACEHOLDER, Object a1 = _PLACEHOLDER]) => max6(a0, a1);
T max3(
[Object a0 = _PLACEHOLDER,
Object a1 = _PLACEHOLDER,
Object a2 = _PLACEHOLDER]) =>
max6(a0, a1, a2);
T max4(
[Object a0 = _PLACEHOLDER,
Object a1 = _PLACEHOLDER,
Object a2 = _PLACEHOLDER,
Object a3 = _PLACEHOLDER]) =>
max6(a0, a1, a2, a3);
T max5(
[Object a0 = _PLACEHOLDER,
Object a1 = _PLACEHOLDER,
Object a2 = _PLACEHOLDER,
Object a3 = _PLACEHOLDER,
Object a4 = _PLACEHOLDER]) =>
max6(a0, a1, a2, a3, a4);
T max6(
[Object a0 = _PLACEHOLDER,
Object a1 = _PLACEHOLDER,
Object a2 = _PLACEHOLDER,
Object a3 = _PLACEHOLDER,
Object a4 = _PLACEHOLDER,
Object a5 = _PLACEHOLDER]) =>
_run([a0, a1, a2, a3, a4, a5].where((a) => a != _PLACEHOLDER));
/// Runs the wrapped function with [args] and returns its return value.
///
/// This will pass any errors on to [_testCase] and return `null`.
T _run(Iterable args) {
try {
_actualCalls++;
if (_testCase.isComplete) {
// Don't run the callback if the test is done. We don't throw here as
// this is not the current test, but we do mark the old test as having
// an error if it previously passed.
if (_testCase.result == PASS) {
_testCase.error(
'Callback ${_id}called ($_actualCalls) after test case '
'${_testCase.description} had already been marked as '
'${_testCase.result}.$_reason');
}
return null;
} else if (_maxExpectedCalls >= 0 && _actualCalls > _maxExpectedCalls) {
throw new TestFailure('Callback ${_id}called more times than expected '
'($_maxExpectedCalls).$_reason');
}
return Function.apply(_callback, args.toList()) as T;
} catch (error, stackTrace) {
_testCase.registerException(error, stackTrace);
return null;
} finally {
_afterRun();
}
}
/// After each time the function is run, check to see if it's complete.
void _afterRun() {
if (_complete) return;
if (_minExpectedCalls > 0 && _actualCalls < _minExpectedCalls) return;
if (_isDone != null && !_isDone()) return;
// Mark this callback as complete and remove it from the test case's
// oustanding callback count; if that hits zero the test is done.
_complete = true;
_testCase.markCallbackComplete();
}
}