blob: 6d135bc4294f883cd973d1afa6c44ac699c08cf7 [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 collect errors in the
/// correct test.
/// 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);
var test = new _Test(_currentName)..asyncWait();
var result =
runZoned(body, zoneValues: {_testToken: test}, onError: test.fail);
if (result is Future) {
result.then((_) {
test.asyncDone();
}, onError: test.fail);
} else {
// Ensure all tests get to be set up first.
scheduleMicrotask(test.asyncDone);
}
_popName(oldName);
}
void expect(Object value, Object matcher) {
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}) {
var test = _currentTest..asyncWait(count);
return () {
var result = f();
test.asyncDone();
return result;
};
}
R Function(A) expectAsync1<R, A>(R Function(A) f, {int count = 1}) {
var test = _currentTest..asyncWait(count);
return (A a) {
var result = f(a);
test.asyncDone();
return result;
};
}
R Function(A, B) expectAsync2<R, A, B>(R Function(A, B) f, {int count = 1}) {
var test = _currentTest..asyncWait(count);
return (A a, B b) {
var result = f(a, b);
test.asyncDone();
return result;
};
}
Function expectAsync(Function f, {int count = 1}) {
var f2 = f; // Avoid type-promoting f, we want dynamic invocations.
var test = _currentTest;
if (f2 is Function(Null, Null, Null, Null, Null)) {
test.asyncWait(count);
return ([a, b, c, d, e]) {
var result = f(a, b, c, d, e);
test.asyncDone();
return result;
};
}
if (f2 is Function(Null, Null, Null, Null)) {
test.asyncWait(count);
return ([a, b, c, d]) {
var result = f(a, b, c, d);
test.asyncDone();
return result;
};
}
if (f2 is Function(Null, Null, Null)) {
test.asyncWait(count);
return ([a, b, c]) {
var result = f(a, b, c);
test.asyncDone();
return result;
};
}
if (f2 is Function(Null, Null)) {
test.asyncWait(count);
return ([a, b]) {
var result = f(a, b);
test.asyncDone();
return result;
};
}
if (f2 is Function(Null)) {
test.asyncWait(count);
return ([a]) {
var result = f(a);
test.asyncDone();
return result;
};
}
if (f2 is Function()) {
test.asyncWait(count);
return () {
var result = f();
test.asyncDone();
return result;
};
}
throw new UnsupportedError(
"expectAsync only accepts up to five arguemnt functions");
}
// Matchers
typedef Matcher = void Function(Object);
Matcher same(Object o) => (v) {
Expect.identical(o, v);
};
Matcher equals(Object o) => (v) {
Expect.deepEquals(o, v);
};
Matcher greaterThan(num n) => (Object v) {
Expect.type<num>(v);
num value = v;
if (value > n) return;
Expect.fail("$v is not greater than $n");
};
Matcher greaterThanOrEqualTo(num n) => (Object v) {
Expect.type<num>(v);
num value = v;
if (value >= n) return;
Expect.fail("$v is not greater than $n");
};
Matcher lessThan(num n) => (Object v) {
Expect.type<num>(v);
num value = v;
if (value < n) return;
Expect.fail("$v is not less than $n");
};
Matcher lessThanOrEqualTo(num n) => (Object v) {
Expect.type<num>(v);
num value = v;
if (value <= n) return;
Expect.fail("$v is not less than $n");
};
void isTrue(Object v) {
Expect.isTrue(v);
}
void isFalse(Object v) {
Expect.isFalse(v);
}
void isNull(Object o) {
Expect.isNull(o);
}
bool isStateError(Object o) {
Expect.type<StateError>(o);
}
void _checkThrow<T>(Object v, void onError(error)) {
if (v is Future) {
var test = _currentTest..asyncWait();
v.then((_) {
Expect.fail("Did not throw");
}, onError: (e, s) {
if (e is! T) throw e;
if (onError != null) onError(e);
test.asyncDone();
});
return;
}
Expect.throws<T>(v, (e) {
onError(e);
return true;
});
}
void throws(Object v) {
_checkThrow<Object>(v, null);
}
Matcher throwsA(matcher) => (Object o) {
_checkThrow<Object>(o, (Object error) {
expect(error, matcher);
});
};
Matcher completion(matcher) => (Object o) {
Expect.type<Future>(o);
Future future = o;
_currentTest.asyncWait();
future.then((value) {
expect(value, matcher);
_currentTest.asyncDone();
});
};
void completes(Object o) {
Expect.type<Future>(o);
Future future = o;
_currentTest.asyncWait();
future.then((_) {
_currentTest.asyncDone();
});
}
void isMap(Object o) {
Expect.type<Map>(o);
}
void isList(Object o) {
Expect.type<List>(o);
}
abstract class _Matcher {
void call(Object o);
}
class isInstanceOf<T> implements _Matcher {
void call(Object o) {
Expect.type<T>(o);
}
}
void throwsArgumentError(Object v) {
_checkThrow<ArgumentError>(v, null);
}
String fail(String message) {
Expect.fail("$message");
}
// Internal helpers.
// The current combined name of the nesting [group] or [test].
String _currentName = null;
String _pushName(String newName) {
var oldName = _currentName;
if (oldName == null) {
_currentName = newName;
} else {
_currentName = "$oldName $newName";
}
return oldName;
}
void _popName(String oldName) {
_currentName = oldName;
}
// Key for zone value holding current test object.
final Object _testToken = new Object();
_Test get _currentTest =>
Zone.current[_testToken] ?? (throw new StateError("Not inside test!"));
class _Test {
static int activeTests = 0;
static int failedTests = 0;
final String name;
bool completed = false;
bool failed = false;
int asyncExpected = 0;
_Test(this.name) {
activeTests++;
}
void asyncWait([int n = 1]) {
if (completed) {
print("ERROR: $name: New operations started after completion."
"${StackTrace.current}");
} else if (asyncExpected == 0) {
asyncStart(); // Matched by asyncEnd in [_complete];
}
asyncExpected += n;
}
void asyncDone() {
if (asyncExpected == 0) {
print("ERROR: $name: More asyncEnds than asyncStarts.\n"
"${StackTrace.current}");
} else {
asyncExpected--;
if (asyncExpected == 0 && !completed) {
print("SUCCESS: $name");
_complete();
}
}
}
void fail(Object error, StackTrace stack) {
if (!completed) {
failed = true;
failedTests++;
print("FAILURE: $name: $error\n$stack");
_complete();
} else {
if (!failed) {
failed = true;
failedTests++;
}
print("FAILURE: $name: (after completion) $error\n$stack");
}
}
void _complete() {
assert(!completed);
completed = true;
activeTests--;
if (failedTests == 0) {
asyncEnd();
} else if (activeTests == 0) {
Zone.root.scheduleMicrotask(() {
Expect.fail("$failedTests tests failed");
});
}
}
}