// Copyright (c) 2016, 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. // @dart = 2.9 import 'package:kernel/ast.dart'; import 'package:kernel/text/ast_to_text.dart'; import 'package:kernel/verifier.dart'; import 'package:test/test.dart'; /// Checks that the verifier correctly find errors in invalid components. /// /// The frontend should never generate invalid components, so we have to test /// these by manually constructing invalid ASTs. /// /// We mostly test negative cases here, as we get plenty of positive cases by /// compiling the Dart test suite with the verifier enabled. main() { positiveTest( 'Test harness has no errors', (TestHarness test) { test.addNode(NullLiteral()); }, ); negative1Test( 'VariableGet out of scope', (TestHarness test) { VariableDeclaration node = test.makeVariable(); test.addNode(VariableGet(node)); return node; }, (Node node) => "Variable '$node' used out of scope.", ); negative1Test( 'VariableSet out of scope', (TestHarness test) { VariableDeclaration variable = test.makeVariable(); test.addNode(VariableSet(variable, new NullLiteral())); return variable; }, (Node node) => "Variable '$node' used out of scope.", ); negative1Test( 'Variable block scope', (TestHarness test) { VariableDeclaration variable = test.makeVariable(); test.addNode(Block([ new Block([variable]), new ReturnStatement(new VariableGet(variable)) ])); return variable; }, (Node node) => "Variable '$node' used out of scope.", ); negative1Test( 'Variable let scope', (TestHarness test) { VariableDeclaration variable = test.makeVariable(); test.addNode(LogicalExpression( new Let(variable, new VariableGet(variable)), LogicalExpressionOperator.AND, new VariableGet(variable))); return variable; }, (Node node) => "Variable '$node' used out of scope.", ); negative1Test( 'Variable redeclared', (TestHarness test) { VariableDeclaration variable = test.makeVariable(); test.addNode(Block([variable, variable])); return variable; }, (Node node) => "Variable '$node' declared more than once.", ); negative1Test( 'Member redeclared', (TestHarness test) { Field field = new Field.mutable(new Name('field'), initializer: new NullLiteral(), fileUri: dummyUri); test.addNode(Class( name: 'Test', supertype: test.objectClass.asRawSupertype, fields: [field, field], fileUri: dummyUri)); return field; }, (Node node) => "Member '$node' has been declared more than once.", ); negative1Test( 'Class redeclared', (TestHarness test) { Class otherClass = test.otherClass; test.addNode( otherClass); // Test harness also adds otherClass to component. return test.otherClass; }, (Node node) => "Class '$node' declared more than once.", ); negative1Test( 'Class type parameter redeclared', (TestHarness test) { TypeParameter parameter = test.makeTypeParameter(); test.addNode(Class( name: 'Test', supertype: test.objectClass.asRawSupertype, typeParameters: [parameter, parameter], fileUri: dummyUri)); return parameter; }, (Node node) => "Type parameter '$node' redeclared.", ); negative1Test( 'Member type parameter redeclared', (TestHarness test) { TypeParameter parameter = test.makeTypeParameter(); test.addNode(Procedure( new Name('bar'), ProcedureKind.Method, new FunctionNode(new ReturnStatement(new NullLiteral()), typeParameters: [parameter, parameter]), fileUri: dummyUri)); return parameter; }, (Node node) => "Type parameter '$node' redeclared.", ); negative2Test( 'Type parameter out of scope', (TestHarness test) { TypeParameter parameter = test.makeTypeParameter(); test.addNode(ListLiteral([], typeArgument: new TypeParameterType(parameter, Nullability.legacy))); return [parameter, null]; }, (Node node, Node parent) => "Type parameter '$node' referenced out of scope," " parent is: '$parent'.", ); negative2Test( 'Class type parameter from another class', (TestHarness test) { TypeParameter node = test.otherClass.typeParameters[0]; test.addNode( TypeLiteral(new TypeParameterType(node, Nullability.legacy))); return [node, test.otherClass]; }, (Node node, Node parent) => "Type parameter '$node' referenced out of scope," " parent is: '$parent'.", ); negative2Test( 'Class type parameter in static method', (TestHarness test) { TypeParameter node = test.classTypeParameter; test.addNode(Procedure( new Name('bar'), ProcedureKind.Method, new FunctionNode(new ReturnStatement(new TypeLiteral( new TypeParameterType(node, Nullability.legacy)))), isStatic: true, fileUri: dummyUri)); return [node, test.enclosingClass]; }, (Node node, Node parent) => "Type parameter '$node' referenced from static context," " parent is: '$parent'.", ); negative2Test( 'Class type parameter in static field', (TestHarness test) { TypeParameter node = test.classTypeParameter; test.addNode(Field.mutable(new Name('field'), initializer: new TypeLiteral(new TypeParameterType(node, Nullability.legacy)), isStatic: true, fileUri: dummyUri)); return [node, test.enclosingClass]; }, (Node node, Node parent) => "Type parameter '$node' referenced from static context," " parent is: '$parent'.", ); negative2Test( 'Method type parameter out of scope', (TestHarness test) { TypeParameter parameter = test.makeTypeParameter(); FunctionNode parent = new FunctionNode(new EmptyStatement(), typeParameters: [parameter]); test.addNode(Class( name: 'Test', supertype: test.objectClass.asRawSupertype, procedures: [ new Procedure(new Name('generic'), ProcedureKind.Method, parent, fileUri: dummyUri), new Procedure( new Name('use'), ProcedureKind.Method, new FunctionNode(new ReturnStatement(new TypeLiteral( new TypeParameterType(parameter, Nullability.legacy)))), fileUri: dummyUri) ], fileUri: dummyUri)); return [parameter, parent]; }, (Node node, Node parent) => "Type parameter '$node' referenced out of scope," " parent is: '$parent'.", ); negative1Test( 'Interface type arity too low', (TestHarness test) { InterfaceType node = new InterfaceType(test.otherClass, Nullability.legacy, []); test.addNode(TypeLiteral(node)); return node; }, (Node node) => "Type $node provides 0 type arguments," " but the class declares 1 parameters.", ); negative1Test( 'Interface type arity too high', (TestHarness test) { InterfaceType node = new InterfaceType(test.otherClass, Nullability.legacy, [new DynamicType(), new DynamicType()]); test.addNode(TypeLiteral(node)); return node; }, (Node node) => "Type $node provides 2 type arguments," " but the class declares 1 parameters.", ); negative1Test( 'Dangling interface type', (TestHarness test) { Class orphan = new Class(name: 'Class', fileUri: dummyUri); test.addNode( new TypeLiteral(new InterfaceType(orphan, Nullability.legacy))); return orphan; }, (Node node) => "Dangling reference to '$node', parent is: 'null'.", ); negative1Test( 'Dangling field get', (TestHarness test) { Field orphan = new Field.mutable(new Name('foo'), fileUri: dummyUri); test.addNode(new PropertyGet(new NullLiteral(), orphan.name, orphan)); return orphan; }, (Node node) => "Dangling reference to '$node', parent is: 'null'.", ); simpleNegativeTest( 'Missing block parent pointer', "Incorrect parent pointer on ReturnStatement:" " expected 'Block', but found: 'Null'.", (TestHarness test) { var block = new Block([]); block.statements.add(new ReturnStatement()); test.addNode(block); }, ); simpleNegativeTest( 'Missing function parent pointer', "Incorrect parent pointer on FunctionNode:" " expected 'Procedure', but found: 'Null'.", (TestHarness test) { var procedure = new Procedure( new Name('bar'), ProcedureKind.Method, dummyFunctionNode, fileUri: dummyUri); procedure.function = new FunctionNode(new EmptyStatement()); test.addNode(procedure); }, ); positiveTest( 'Correct StaticInvocation', (TestHarness test) { var method = new Procedure( new Name('foo'), ProcedureKind.Method, new FunctionNode(new EmptyStatement(), positionalParameters: [new VariableDeclaration('p')]), isStatic: true, fileUri: dummyUri); test.enclosingClass.addProcedure(method); test.addNode( StaticInvocation(method, new Arguments([new NullLiteral()]))); }, ); negative1Test( 'StaticInvocation with too many parameters', (TestHarness test) { var method = new Procedure(new Name('bar'), ProcedureKind.Method, new FunctionNode(new EmptyStatement()), isStatic: true, fileUri: dummyUri); test.enclosingClass.addProcedure(method); test.addNode( StaticInvocation(method, new Arguments([new NullLiteral()]))); return method; }, (Node node) => "StaticInvocation with incompatible arguments for" " '$node'.", ); negative1Test( 'StaticInvocation with too few parameters', (TestHarness test) { var method = new Procedure( new Name('bar'), ProcedureKind.Method, new FunctionNode(new EmptyStatement(), positionalParameters: [new VariableDeclaration('p')]), isStatic: true, fileUri: dummyUri); test.enclosingClass.addProcedure(method); test.addNode(StaticInvocation(method, new Arguments.empty())); return method; }, (Node node) => "StaticInvocation with incompatible arguments for '$node'.", ); negative1Test( 'StaticInvocation with unmatched named parameter', (TestHarness test) { var method = new Procedure(new Name('bar'), ProcedureKind.Method, new FunctionNode(new EmptyStatement()), isStatic: true, fileUri: dummyUri); test.enclosingClass.addProcedure(method); test.addNode(StaticInvocation( method, new Arguments([], named: [new NamedExpression('p', new NullLiteral())]))); return method; }, (Node node) => "StaticInvocation with incompatible arguments for" " '$node'.", ); negative1Test( 'StaticInvocation with missing type argument', (TestHarness test) { Procedure method = new Procedure( new Name('bar'), ProcedureKind.Method, new FunctionNode(new EmptyStatement(), typeParameters: [test.makeTypeParameter()]), isStatic: true, fileUri: dummyUri); test.enclosingClass.addProcedure(method); test.addNode(StaticInvocation(method, new Arguments.empty())); return method; }, (Node node) => "StaticInvocation with wrong number of type arguments for" " '$node'.", ); negative1Test( 'ConstructorInvocation with missing type argument', (TestHarness test) { var constructor = new Constructor(new FunctionNode(new EmptyStatement()), name: new Name('foo'), fileUri: dummyUri); test.enclosingClass.addConstructor(constructor); test.addNode(ConstructorInvocation(constructor, new Arguments.empty())); return constructor; }, (Node node) => "ConstructorInvocation with wrong number of type arguments for" " '$node'.", ); positiveTest( 'Valid typedef Foo = `(C) => void`', (TestHarness test) { var typedef_ = new Typedef( 'Foo', new FunctionType( [test.otherLegacyRawType], const VoidType(), Nullability.legacy), fileUri: dummyUri); test.addNode(typedef_); }, ); positiveTest( 'Valid typedef Foo = C', (TestHarness test) { var typedef_ = new Typedef( 'Foo', new InterfaceType( test.otherClass, Nullability.legacy, [const DynamicType()]), fileUri: dummyUri); test.addNode(typedef_); }, ); positiveTest( 'Valid typedefs Foo = Bar, Bar = C', (TestHarness test) { var foo = new Typedef('Foo', null, fileUri: dummyUri); var bar = new Typedef('Bar', null, fileUri: dummyUri); foo.type = new TypedefType(bar, Nullability.legacy); bar.type = test.otherLegacyRawType; test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); }, ); positiveTest( 'Valid typedefs Foo = C, Bar = C', (TestHarness test) { var foo = new Typedef('Foo', null, fileUri: dummyUri); var bar = new Typedef('Bar', null, fileUri: dummyUri); foo.type = new InterfaceType(test.otherClass, Nullability.legacy, [new TypedefType(bar, Nullability.legacy)]); bar.type = test.otherLegacyRawType; test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); }, ); positiveTest( 'Valid typedef type in field', (TestHarness test) { var typedef_ = new Typedef( 'Foo', new FunctionType( [test.otherLegacyRawType], const VoidType(), Nullability.legacy), fileUri: dummyUri); var field = new Field.mutable(new Name('field'), type: new TypedefType(typedef_, Nullability.legacy), isStatic: true, fileUri: dummyUri); test.enclosingLibrary.addTypedef(typedef_); test.enclosingLibrary.addField(field); }, ); negative1Test( 'Invalid typedef Foo = Foo', (TestHarness test) { var typedef_ = new Typedef('Foo', null, fileUri: dummyUri); typedef_.type = new TypedefType(typedef_, Nullability.legacy); test.addNode(typedef_); return typedef_; }, (Node node) => "The typedef '$node' refers to itself", ); negative1Test( 'Invalid typedef Foo = `(Foo) => void`', (TestHarness test) { var typedef_ = new Typedef('Foo', null, fileUri: dummyUri); typedef_.type = new FunctionType( [new TypedefType(typedef_, Nullability.legacy)], const VoidType(), Nullability.legacy); test.addNode(typedef_); return typedef_; }, (Node node) => "The typedef '$node' refers to itself", ); negative1Test( 'Invalid typedef Foo = `() => Foo`', (TestHarness test) { var typedef_ = new Typedef('Foo', null, fileUri: dummyUri); typedef_.type = new FunctionType([], new TypedefType(typedef_, Nullability.legacy), Nullability.legacy); test.addNode(typedef_); return typedef_; }, (Node node) => "The typedef '$node' refers to itself", ); negative1Test( 'Invalid typedef Foo = C', (TestHarness test) { var typedef_ = new Typedef('Foo', null, fileUri: dummyUri); typedef_.type = new InterfaceType(test.otherClass, Nullability.legacy, [new TypedefType(typedef_, Nullability.legacy)]); test.addNode(typedef_); return typedef_; }, (Node node) => "The typedef '$node' refers to itself", ); negative1Test( 'Invalid typedefs Foo = Bar, Bar = Foo', (TestHarness test) { var foo = new Typedef('Foo', null, fileUri: dummyUri); var bar = new Typedef('Bar', null, fileUri: dummyUri); foo.type = new TypedefType(bar, Nullability.legacy); bar.type = new TypedefType(foo, Nullability.legacy); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); return foo; }, (Node foo) => "The typedef '$foo' refers to itself", ); negative1Test( 'Invalid typedefs Foo = Bar, Bar = C', (TestHarness test) { var foo = new Typedef('Foo', null, fileUri: dummyUri); var bar = new Typedef('Bar', null, fileUri: dummyUri); foo.type = new TypedefType(bar, Nullability.legacy); bar.type = new InterfaceType(test.otherClass, Nullability.legacy, [new TypedefType(foo, Nullability.legacy)]); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); return foo; }, (Node foo) => "The typedef '$foo' refers to itself", ); negative1Test( 'Invalid typedefs Foo = C, Bar = C', (TestHarness test) { var foo = new Typedef('Foo', null, fileUri: dummyUri); var bar = new Typedef('Bar', null, fileUri: dummyUri); foo.type = new InterfaceType(test.otherClass, Nullability.legacy, [new TypedefType(bar, Nullability.legacy)]); bar.type = new InterfaceType(test.otherClass, Nullability.legacy, [new TypedefType(foo, Nullability.legacy)]); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); return foo; }, (Node foo) => "The typedef '$foo' refers to itself", ); positiveTest( 'Valid long typedefs C20 = C19 = ... = C1 = C0 = dynamic', (TestHarness test) { var typedef_ = new Typedef('C0', const DynamicType(), fileUri: dummyUri); test.enclosingLibrary.addTypedef(typedef_); for (int i = 1; i < 20; ++i) { typedef_ = new Typedef( 'C$i', new TypedefType(typedef_, Nullability.legacy), fileUri: dummyUri); test.enclosingLibrary.addTypedef(typedef_); } }, ); negative1Test( 'Invalid long typedefs C20 = C19 = ... = C1 = C0 = C20', (TestHarness test) { Typedef firstTypedef = new Typedef('C0', null, fileUri: dummyUri); Typedef typedef_ = firstTypedef; test.enclosingLibrary.addTypedef(typedef_); var first = typedef_; for (int i = 1; i < 20; ++i) { typedef_ = new Typedef( 'C$i', new TypedefType(typedef_, Nullability.legacy), fileUri: dummyUri); test.enclosingLibrary.addTypedef(typedef_); } first.type = new TypedefType(typedef_, Nullability.legacy); return firstTypedef; }, (Node node) => "The typedef '$node' refers to itself", ); positiveTest( 'Valid typedef Foo = C', (TestHarness test) { var param = new TypeParameter('T', test.otherLegacyRawType); var foo = new Typedef( 'Foo', new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(param, Nullability.legacy)]), typeParameters: [param], fileUri: dummyUri); test.addNode(foo); }, ); positiveTest( 'Valid typedef Foo> = C', (TestHarness test) { var param = new TypeParameter('T', test.otherLegacyRawType); param.bound = new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(param, Nullability.legacy)]); var foo = new Typedef( 'Foo', new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(param, Nullability.legacy)]), typeParameters: [param], fileUri: dummyUri); test.addNode(foo); }, ); positiveTest( 'Valid typedef Foo = dynamic, Bar> = C', (TestHarness test) { var fooParam = test.makeTypeParameter('T'); var foo = new Typedef('Foo', const DynamicType(), typeParameters: [fooParam], fileUri: dummyUri); var barParam = new TypeParameter('T', null); barParam.bound = new TypedefType(foo, Nullability.legacy, [new TypeParameterType(barParam, Nullability.legacy)]); var bar = new Typedef( 'Bar', new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(barParam, Nullability.legacy)]), typeParameters: [barParam], fileUri: dummyUri); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); }, ); negative1Test( 'Invalid typedefs Foo>, Bar>', (TestHarness test) { var fooParam = test.makeTypeParameter('T'); var foo = new Typedef('Foo', const DynamicType(), typeParameters: [fooParam], fileUri: dummyUri); var barParam = new TypeParameter('T', null); barParam.bound = new TypedefType(foo, Nullability.legacy, [new TypeParameterType(barParam, Nullability.legacy)]); var bar = new Typedef( 'Bar', new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(barParam, Nullability.legacy)]), typeParameters: [barParam], fileUri: dummyUri); fooParam.bound = new TypedefType(bar, Nullability.legacy, [new TypeParameterType(fooParam, Nullability.legacy)]); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addTypedef(bar); return foo; }, (Node foo) => "The typedef '$foo' refers to itself", ); negative1Test( 'Invalid typedef Foo = C', (TestHarness test) { var param = new TypeParameter('T', null); var foo = new Typedef( 'Foo', new InterfaceType(test.otherClass, Nullability.legacy, [new TypeParameterType(param, Nullability.legacy)]), typeParameters: [param], fileUri: dummyUri); param.bound = new TypedefType(foo, Nullability.legacy, [const DynamicType()]); test.addNode(foo); return foo; }, (Node foo) => "The typedef '$foo' refers to itself", ); negative1Test( 'Typedef arity error', (TestHarness test) { var param = test.makeTypeParameter('T'); var foo = new Typedef('Foo', test.otherLegacyRawType, typeParameters: [param], fileUri: dummyUri); var typedefType = new TypedefType(foo, Nullability.legacy, []); var field = new Field.mutable(new Name('field'), type: typedefType, isStatic: true, fileUri: dummyUri); test.enclosingLibrary.addTypedef(foo); test.enclosingLibrary.addField(field); return typedefType; }, (Node typedefType) => "The typedef type $typedefType provides 0 type arguments," " but the typedef declares 1 parameters.", ); negative1Test( 'Dangling typedef reference', (TestHarness test) { var foo = new Typedef('Foo', test.otherLegacyRawType, typeParameters: [], fileUri: dummyUri); var field = new Field.mutable(new Name('field'), type: new TypedefType(foo, Nullability.legacy, []), isStatic: true, fileUri: dummyUri); test.enclosingLibrary.addField(field); return foo; }, (Node foo) => "Dangling reference to '$foo', parent is: 'null'", ); negative1Test( 'Non-static top-level field', (TestHarness test) { var field = new Field.mutable(new Name('field'), fileUri: dummyUri); test.enclosingLibrary.addField(field); return null; }, (Node node) => "The top-level field 'field' should be static", ); } checkHasError(Component component, Matcher matcher) { try { verifyComponent(component); } on VerificationError catch (e) { expect(e.details, matcher); return; } fail('Failed to reject invalid component:\n${componentToString(component)}'); } class TestHarness { Component component; Class objectClass; Library stubLibrary; TypeParameter classTypeParameter; Library enclosingLibrary; Class enclosingClass; Procedure enclosingMember; Class otherClass; InterfaceType objectLegacyRawType; InterfaceType enclosingLegacyRawType; InterfaceType otherLegacyRawType; void addNode(TreeNode node) { if (node is Expression) { addExpression(node); } else if (node is Statement) { addStatement(node); } else if (node is Member) { addClassMember(node); } else if (node is Class) { addClass(node); } else if (node is Typedef) { addTypedef(node); } } void addExpression(Expression node) { addStatement(new ReturnStatement(node)); } void addStatement(Statement node) { var function = enclosingMember.function; function.body = node..parent = function; } void addClassMember(Member node) { if (node is Procedure) { enclosingClass.addProcedure(node); } else if (node is Field) { enclosingClass.addField(node); } else if (node is Constructor) { enclosingClass.addConstructor(node); } else if (node is RedirectingFactoryConstructor) { enclosingClass.addRedirectingFactoryConstructor(node); } else { throw "Unexpected class member: ${node.runtimeType}"; } } void addTopLevelMember(Member node) { if (node is Procedure) { enclosingLibrary.addProcedure(node); } else if (node is Field) { enclosingLibrary.addField(node); } else { throw "Unexpected top level member: ${node.runtimeType}"; } } void addClass(Class node) { enclosingLibrary.addClass(node); } void addTypedef(Typedef node) { enclosingLibrary.addTypedef(node); } VariableDeclaration makeVariable() => new VariableDeclaration(null); TypeParameter makeTypeParameter([String name]) { return new TypeParameter(name, objectLegacyRawType, const DynamicType()); } TestHarness() { setupComponent(); } void setupComponent() { component = new Component(); Uri dartCoreUri = Uri.parse('dart:core'); stubLibrary = new Library(dartCoreUri, fileUri: dartCoreUri); component.libraries.add(stubLibrary..parent = component); stubLibrary.name = 'dart.core'; objectClass = new Class(name: 'Object', fileUri: dartCoreUri); objectLegacyRawType = new InterfaceType(objectClass, Nullability.legacy, const []); stubLibrary.addClass(objectClass); Uri testUri = Uri.parse('file://test.dart'); enclosingLibrary = new Library(testUri, fileUri: testUri); component.libraries.add(enclosingLibrary..parent = component); enclosingLibrary.name = 'test_lib'; classTypeParameter = makeTypeParameter('T'); enclosingClass = new Class( name: 'TestClass', typeParameters: [classTypeParameter], supertype: objectClass.asRawSupertype, fileUri: testUri); enclosingLegacyRawType = new InterfaceType(enclosingClass, Nullability.legacy, const [const DynamicType()]); enclosingLibrary.addClass(enclosingClass); enclosingMember = new Procedure(new Name('test'), ProcedureKind.Method, new FunctionNode(new EmptyStatement()), fileUri: dummyUri); enclosingClass.addProcedure(enclosingMember); otherClass = new Class( name: 'OtherClass', typeParameters: [makeTypeParameter('OtherT')], supertype: objectClass.asRawSupertype, fileUri: testUri); otherLegacyRawType = new InterfaceType( otherClass, Nullability.legacy, const [const DynamicType()]); enclosingLibrary.addClass(otherClass); } } negative1Test(String name, Node Function(TestHarness test) nodeProvider, dynamic Function(Node node) matcher) { TestHarness testHarness = new TestHarness(); Node node = nodeProvider(testHarness); test( name, () { dynamic matcherResult = matcher(node); if (matcherResult is String) { matcherResult = equals(matcherResult); } checkHasError(testHarness.component, matcherResult); }, ); } negative2Test(String name, List Function(TestHarness test) nodeProvider, dynamic Function(Node node, Node other) matcher) { TestHarness testHarness = new TestHarness(); List nodes = nodeProvider(testHarness); if (nodes.length != 2) throw "Needs exactly 2 nodes: Node and other!"; test( name, () { dynamic matcherResult = matcher(nodes[0], nodes[1]); if (matcherResult is String) { matcherResult = equals(matcherResult); } checkHasError(testHarness.component, matcherResult); }, ); } simpleNegativeTest(String name, dynamic matcher, void Function(TestHarness test) makeTestCase) { TestHarness testHarness = new TestHarness(); test( name, () { makeTestCase(testHarness); if (matcher is String) { matcher = equals(matcher); } checkHasError(testHarness.component, matcher); }, ); } positiveTest(String name, void makeTestCase(TestHarness test)) { test( name, () { var test = new TestHarness(); makeTestCase(test); verifyComponent(test.component); }, ); }