blob: 6e3c945a2778a7b7ba82ae05733c300f0a81c533 [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 test_reflective_loader;
import 'dart:async';
@MirrorsUsed(metaTargets: 'ReflectiveTest')
import 'dart:mirrors';
import 'package:unittest/unittest.dart';
/**
* A marker annotation used to annotate overridden test methods (so we cannot
* rename them to `fail_`) which are expected to fail at `assert` in the
* checked mode.
*/
const _AssertFailingTest assertFailingTest = const _AssertFailingTest();
/**
* A marker annotation used to annotate overridden test methods (so we cannot
* rename them to `fail_`) which are expected to fail.
*/
const _FailingTest failingTest = const _FailingTest();
/**
* A marker annotation used to instruct dart2js to keep reflection information
* for the annotated classes.
*/
const ReflectiveTest reflectiveTest = const ReflectiveTest();
/**
* Test classes annotated with this annotation are run using [solo_group].
*/
const _SoloTest soloTest = const _SoloTest();
/**
* Is `true` the application is running in the checked mode.
*/
final bool _isCheckedMode = () {
try {
assert(false);
return false;
} catch (_) {
return true;
}
}();
/**
* Runs test methods existing in the given [type].
*
* Methods with names starting with `test` are run using [test] function.
* Methods with names starting with `solo_test` are run using [solo_test] function.
*
* Each method is run with a new instance of [type].
* So, [type] should have a default constructor.
*
* If [type] declares method `setUp`, it methods will be invoked before any test
* method invocation.
*
* If [type] declares method `tearDown`, it will be invoked after any test
* method invocation. If method returns [Future] to test some asynchronous
* behavior, then `tearDown` will be invoked in `Future.complete`.
*/
void defineReflectiveTests(Type type) {
ClassMirror classMirror = reflectClass(type);
if (!classMirror.metadata.any((InstanceMirror annotation) =>
annotation.type.reflectedType == ReflectiveTest)) {
String name = MirrorSystem.getName(classMirror.qualifiedName);
throw new Exception('Class $name must have annotation "@reflectiveTest" '
'in order to be run by runReflectiveTests.');
}
void runMembers() {
classMirror.instanceMembers
.forEach((Symbol symbol, MethodMirror memberMirror) {
// we need only methods
if (memberMirror is! MethodMirror || !memberMirror.isRegularMethod) {
return;
}
String memberName = MirrorSystem.getName(symbol);
// test_
if (memberName.startsWith('test_')) {
test(memberName, () {
if (_hasFailingTestAnnotation(memberMirror) ||
_isCheckedMode && _hasAssertFailingTestAnnotation(memberMirror)) {
return _runFailingTest(classMirror, symbol);
} else {
return _runTest(classMirror, symbol);
}
});
return;
}
// solo_test_
if (memberName.startsWith('solo_test_')) {
solo_test(memberName, () {
return _runTest(classMirror, symbol);
});
}
// fail_test_
if (memberName.startsWith('fail_')) {
test(memberName, () {
return _runFailingTest(classMirror, symbol);
});
}
// solo_fail_test_
if (memberName.startsWith('solo_fail_')) {
solo_test(memberName, () {
return _runFailingTest(classMirror, symbol);
});
}
});
}
String className = MirrorSystem.getName(classMirror.simpleName);
if (_hasAnnotationInstance(classMirror, soloTest)) {
solo_group(className, runMembers);
} else {
group(className, runMembers);
}
}
bool _hasAnnotationInstance(DeclarationMirror declaration, instance) =>
declaration.metadata.any((InstanceMirror annotation) =>
identical(annotation.reflectee, instance));
bool _hasAssertFailingTestAnnotation(MethodMirror method) =>
_hasAnnotationInstance(method, assertFailingTest);
bool _hasFailingTestAnnotation(MethodMirror method) =>
_hasAnnotationInstance(method, failingTest);
Future _invokeSymbolIfExists(InstanceMirror instanceMirror, Symbol symbol) {
var invocationResult = null;
InstanceMirror closure;
try {
closure = instanceMirror.getField(symbol);
} on NoSuchMethodError {}
if (closure is ClosureMirror) {
invocationResult = closure.apply([]).reflectee;
}
return new Future.value(invocationResult);
}
/**
* Run a test that is expected to fail, and confirm that it fails.
*
* This properly handles the following cases:
* - The test fails by throwing an exception
* - The test returns a future which completes with an error.
*
* However, it does not handle the case where the test creates an asynchronous
* callback using expectAsync(), and that callback generates a failure.
*/
Future _runFailingTest(ClassMirror classMirror, Symbol symbol) {
return new Future(() => _runTest(classMirror, symbol)).then((_) {
fail('Test passed - expected to fail.');
}, onError: (_) {});
}
_runTest(ClassMirror classMirror, Symbol symbol) {
InstanceMirror instanceMirror = classMirror.newInstance(new Symbol(''), []);
return _invokeSymbolIfExists(instanceMirror, #setUp)
.then((_) => instanceMirror.invoke(symbol, []).reflectee)
.whenComplete(() => _invokeSymbolIfExists(instanceMirror, #tearDown));
}
/**
* A marker annotation used to instruct dart2js to keep reflection information
* for the annotated classes.
*/
class ReflectiveTest {
const ReflectiveTest();
}
/**
* A marker annotation used to annotate overridden test methods (so we cannot
* rename them to `fail_`) which are expected to fail at `assert` in the
* checked mode.
*/
class _AssertFailingTest {
const _AssertFailingTest();
}
/**
* A marker annotation used to annotate overridden test methods (so we cannot
* rename them to `fail_`) which are expected to fail.
*/
class _FailingTest {
const _FailingTest();
}
/**
* A marker annotation used to annotate a test class to run it using
* [solo_group].
*/
class _SoloTest {
const _SoloTest();
}