fb29d0e96d
This allows to break a circular dependency between package:expect and package:async_helper, which will simplify the support for modular tests using package:modular_test. Change-Id: Ie48723d3f35d51a8fbe622e0158450e8104fe3f5 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/102140 Reviewed-by: Johnni Winther <johnniwinther@google.com>
373 lines
8.6 KiB
Dart
373 lines
8.6 KiB
Dart
// 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, {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}) {
|
|
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);
|
|
}
|
|
|
|
void isNotNull(Object o) {
|
|
Expect.isNotNull(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");
|
|
});
|
|
}
|
|
}
|
|
}
|