blob: fa5d319cf0240f2358d5bc0cfd8e6a9ace32d58b [file] [log] [blame]
// Copyright (c) 2018, 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.
/// A minimal, dependency-free emulation layer for a subset of the
/// unittest/test API used by the language and core library.
///
/// Compared with `minitest.dart`, this library supports and expects
/// asynchronous tests. It uses a `Zone` per test to associate a test name with
/// the failure.
///
/// It does not support `setUp` or `tearDown` methods,
/// and matchers are severely restricted.
///
/// A number of our language and core library tests were written against the
/// unittest package, which is now deprecated in favor of the new test package.
/// The latter is much better feature-wise, but is also quite complex and has
/// many dependencies. For low-level language and core library tests, we don't
/// want to have to pull in a large number of dependencies and be able to run
/// them correctly in order to run a test, so we want to test them against
/// something simpler.
///
/// When possible, we just use the tiny expect library. But to avoid rewriting
/// all of the existing tests that use unittest, they can instead use this,
/// which shims the unittest API with as little code as possible and calls into
/// expect.
///
/// Eventually, it would be good to refactor those tests to use the expect
/// package directly and remove this.
import 'dart:async';
import 'package:async_helper/async_helper.dart';
import 'package:expect/expect.dart';
void group(String name, body()) {
var oldName = _pushName(name);
try {
body();
} finally {
_popName(oldName);
}
}
void test(String name, body()) {
var oldName = _pushName(name);
asyncStart();
var result = runZoned(body, zoneValues: {_testToken: _currentName});
if (result is Future) {
result.then((_) {
asyncEnd();
});
} else {
// Ensure all tests get to be set up first.
scheduleMicrotask(asyncEnd);
}
_popName(oldName);
}
void expect(dynamic value, dynamic matcher, {String reason = ""}) {
Matcher m;
if (matcher is _Matcher) {
m = matcher.call;
} else if (matcher is! Matcher) {
m = equals(matcher);
} else {
m = matcher;
}
m(value);
}
R Function() expectAsync0<R>(R Function() f, {int count = 1}) {
asyncStart(count);
return () {
var result = f();
asyncEnd();
return result;
};
}
R Function(A) expectAsync1<R, A>(R Function(A) f, {int count = 1}) {
asyncStart(count);
return (A a) {
var result = f(a);
asyncEnd();
return result;
};
}
R Function(A, B) expectAsync2<R, A, B>(R Function(A, B) f, {int count = 1}) {
asyncStart(count);
return (A a, B b) {
var result = f(a, b);
asyncEnd();
return result;
};
}
dynamic expectAsync(Function f, {int count = 1}) {
var f2 = f; // Avoid type-promoting f, we want dynamic invocations.
if (f2 is Function(Never, Never, Never, Never, Never)) {
asyncStart(count);
return ([a, b, c, d, e]) {
var result = f(a, b, c, d, e);
asyncEnd();
return result;
};
}
if (f2 is Function(Never, Never, Never, Never)) {
asyncStart(count);
return ([a, b, c, d]) {
var result = f(a, b, c, d);
asyncEnd();
return result;
};
}
if (f2 is Function(Never, Never, Never)) {
asyncStart(count);
return ([a, b, c]) {
var result = f(a, b, c);
asyncEnd();
return result;
};
}
if (f2 is Function(Never, Never)) {
asyncStart(count);
return ([a, b]) {
var result = f(a, b);
asyncEnd();
return result;
};
}
if (f2 is Function(Never)) {
asyncStart(count);
return ([a]) {
var result = f(a);
asyncEnd();
return result;
};
}
if (f2 is Function()) {
asyncStart(count);
return () {
var result = f();
asyncEnd();
return result;
};
}
throw new UnsupportedError(
"expectAsync only accepts up to five argument functions");
}
// Matchers
typedef Matcher = void Function(dynamic);
Matcher same(dynamic o) => (v) {
Expect.identical(o, v);
};
Matcher equals(dynamic o) => (v) {
Expect.deepEquals(o, v);
};
Matcher greaterThan(num n) => (dynamic v) {
Expect.type<num>(v);
num value = v;
if (value > n) return;
Expect.fail("$v is not greater than $n");
};
Matcher greaterThanOrEqualTo(num n) => (dynamic v) {
Expect.type<num>(v);
num value = v;
if (value >= n) return;
Expect.fail("$v is not greater than $n");
};
Matcher lessThan(num n) => (dynamic v) {
Expect.type<num>(v);
num value = v;
if (value < n) return;
Expect.fail("$v is not less than $n");
};
Matcher lessThanOrEqualTo(num n) => (dynamic v) {
Expect.type<num>(v);
num value = v;
if (value <= n) return;
Expect.fail("$v is not less than $n");
};
Matcher predicate(bool fn(dynamic value), [String description = ""]) =>
(dynamic v) {
Expect.isTrue(fn(v), description);
};
Matcher anyOf(List<String> expected) => (dynamic actual) {
for (var string in expected) {
if (actual == string) return;
}
Expect.fail("Expected $actual to be one of $expected.");
};
void isTrue(dynamic v) {
Expect.isTrue(v);
}
void isFalse(dynamic v) {
Expect.isFalse(v);
}
void isNull(dynamic o) {
Expect.isNull(o);
}
void _checkThrow<T>(dynamic v, void onError(error)) {
if (v is Future) {
asyncStart();
v.then((_) {
Expect.fail("Did not throw");
}, onError: (e, s) {
if (e is! T) throw e;
if (onError != null) onError(e);
asyncEnd();
});
return;
}
Expect.throws<T>(v, (e) {
onError(e);
return true;
});
}
void returnsNormally(dynamic o) {
try {
Expect.type<Function()>(o);
o();
} catch (error, trace) {
Expect.fail(
"Expected function to return normally, but threw:\n$error\n\n$trace");
}
}
void throws(dynamic v) {
_checkThrow<Object>(v, (_) {});
}
Matcher throwsA(matcher) => (dynamic o) {
_checkThrow<Object>(o, (error) {
expect(error, matcher);
});
};
Matcher completion(matcher) => (dynamic o) {
Expect.type<Future>(o);
Future future = o;
asyncStart();
future.then((value) {
expect(value, matcher);
asyncEnd();
});
};
void completes(dynamic o) {
Expect.type<Future>(o);
Future future = o;
asyncStart();
future.then(asyncSuccess);
}
void isMap(dynamic o) {
Expect.type<Map>(o);
}
void isList(dynamic o) {
Expect.type<List>(o);
}
void isNotNull(dynamic o) {
Expect.isNotNull(o);
}
abstract class _Matcher {
void call(dynamic o);
}
class isInstanceOf<T> implements _Matcher {
void call(dynamic o) {
Expect.type<T>(o);
}
}
void throwsArgumentError(dynamic v) {
_checkThrow<ArgumentError>(v, (_) {});
}
void throwsStateError(dynamic v) {
_checkThrow<StateError>(v, (_) {});
}
void fail(String message) {
Expect.fail("$message");
}
/// Key for zone value holding current test name.
final _testToken = Object();
bool _initializedTestNameCallback = false;
/// The current combined name of the nesting [group] or [test].
String _currentName = "";
String _pushName(String newName) {
// Look up the current test name from the zone created for the test.
if (!_initializedTestNameCallback) {
ExpectException.setTestNameCallback(() => Zone.current[_testToken]);
_initializedTestNameCallback = true;
}
var oldName = _currentName;
if (oldName == "") {
_currentName = newName;
} else {
_currentName = "$oldName $newName";
}
return oldName;
}
void _popName(String oldName) {
_currentName = oldName;
}