// 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 Function() f, {int count = 1}) { asyncStart(count); return () { var result = f(); asyncEnd(); return result; }; } R Function(A) expectAsync1(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 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(v); num value = v; if (value > n) return; Expect.fail("$v is not greater than $n"); }; Matcher greaterThanOrEqualTo(num n) => (dynamic v) { Expect.type(v); num value = v; if (value >= n) return; Expect.fail("$v is not greater than $n"); }; Matcher lessThan(num n) => (dynamic v) { Expect.type(v); num value = v; if (value < n) return; Expect.fail("$v is not less than $n"); }; Matcher lessThanOrEqualTo(num n) => (dynamic v) { Expect.type(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 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(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(v, (e) { onError(e); return true; }); } void returnsNormally(dynamic o) { try { Expect.type(o); o(); } catch (error, trace) { Expect.fail( "Expected function to return normally, but threw:\n$error\n\n$trace"); } } void throws(dynamic v) { _checkThrow(v, (_) {}); } Matcher throwsA(matcher) => (dynamic o) { _checkThrow(o, (error) { expect(error, matcher); }); }; Matcher completion(matcher) => (dynamic o) { Expect.type(o); Future future = o; asyncStart(); future.then((value) { expect(value, matcher); asyncEnd(); }); }; void completes(dynamic o) { Expect.type(o); Future future = o; asyncStart(); future.then(asyncSuccess); } void isMap(dynamic o) { Expect.type(o); } void isList(dynamic o) { Expect.type(o); } void isNotNull(dynamic o) { Expect.isNotNull(o); } abstract class _Matcher { void call(dynamic o); } class isInstanceOf implements _Matcher { void call(dynamic o) { Expect.type(o); } } void throwsArgumentError(dynamic v) { _checkThrow(v, (_) {}); } void throwsStateError(dynamic v) { _checkThrow(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; }