Task: A tester program based on annotations

Before performing the exercise of this laboratory you should be familiar with Java annotations.  Consider the example for a simple testing tool given in Oracle's technical guide on Java annotations.  The goal is to start with the example and extend it with the following additional functionality:

The @Setup annotation will complement the @Test annotation.  The @Setup contains a single attribute, that defines its name, e.g. @Setup("a").  The @Test annotation is enhanced to support a list of "setup methods" that must be executed before the test method is executed.  For example, @Test({"a", "b"}) indicates that the setup methods annotated with the name "a" and "b" need to be executed, in the given order, before the test method is executed.  The default value for the list of setup methods in the @Test annotation is "*", which means that all setup methods will be executed, in any order.

Consider only static methods that return void and take zero arguments for both @Setup and @Test annotations.  The annotated methods can be private, protected or public.

When testing a hierarchy of annotated classes, the tests in the superclass should run first.

The following test classes provide some examples of the expected behaviour.

public class TestSimple {
    @Test public static void m1() { System.out.println("m1"); }
    @Test private static void m2() { System.out.println("m2"); }
    @Test private static void m3() { throw new RuntimeException("problem"); }
    public static void m4() { System.out.println("m4"); }
}

$ java RunTests TestSimple
m1
Test public static void TestSimple.m1() OK!
m2
Test private static void TestSimple.m2() OK!
Test private static void TestSimple.m3() failed
Passed: 2, Failed 1

public class TestWithSetup {
    @Setup("s1") private static void s1() { System.out.println("s1"); }
    @Setup("s2") protected static void s2() { System.out.println("s2"); }
    @Setup("s3") private static void s3() { throw new RuntimeException("fail"); }

    @Test public static void m5() { System.out.println("m5"); }
    @Test({"s2", "s1"}) private static void m6() { System.out.println("m6"); }
    @Test({"s1", "s2"}) private static void m7() { throw new RuntimeException("problem"); }
    @Test("s1") private static void m8() { System.out.println("m8"); }
    public static void m9() { System.out.println("m9"); }
}

$ java RunTests TestWithSetup
s2
s1
Test public static void TestWithSetup.m5() failed
s2
s1
m6
Test private static void TestWithSetup.m6() OK!
s1
s2
Test private static void TestWithSetup.m7() failed
s1
m8
Test private static void TestWithSetup.m8() OK!
Passed: 2, Failed 2

public class TestInheritance extends TestWithSetup {
    @Setup("s4") protected static void s5() { System.out.println("s4"); }

    @Test("*") public static void m10() { System.out.println("m10"); }
    @Test("s2") public void m11() { System.out.println("m11"); }
    @Test("s1") public static void m12() { System.out.println("m12"); }
    public static void m13() { System.out.println("m13"); }
    @Test({"s1", "s4"}) public static void m14() { System.out.println("m14"); }
}

$ java RunTests TestInheritance
s2
s1
Test public static void TestWithSetup.m5() failed
s2
s1
m6
Test private static void TestWithSetup.m6() OK!
s1
s2
Test private static void TestWithSetup.m7() failed
s1
m8
Test private static void TestWithSetup.m8() OK!
s2
s1
Test public static void TestInheritance.m10() failed
s1
m12
Test public static void TestInheritance.m12() OK!
s1
s4
m14
Test public static void TestInheritance.m14() OK!
Passed: 4, Failed 3