UnitTester.java
package autumn.util.test;
import autumn.lang.Delegate;
import autumn.lang.Module;
import autumn.util.F;
import com.mackenziehigh.autumn.util.T;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.mackenziehigh.autumn.resources.Finished;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
/**
* An instance of this class runs unit-tests contained in a group of modules.
*
* @author Mackenzie High
*/
@Finished("2014/08/21")
public final class UnitTester
implements Tester
{
/**
* These are the modules that contain test functions.
*/
private Set<Module> modules = Sets.newHashSet();
/**
* This method adds a module to the set of modules that will be unit-tested.
*
* <p>
* The singleton instance of the module will be retrieved via reflection.
* </p>
*
* @param module is the class-object that represents the type of the module.
* @throws NullPointerException if module is null.
* @throws IllegalArgumentException if an instance of the module could not be obtained.
*/
public void add(final Class module)
{
Preconditions.checkNotNull(module);
try
{
final Method method = module.getDeclaredMethod("instance");
final Module instance = (Module) method.invoke(null);
add(instance);
}
catch (NoSuchMethodException ex)
{
throw new IllegalArgumentException("No instance() method was found.", ex);
}
catch (IllegalAccessException ex)
{
throw new IllegalArgumentException("The instance() method is inaccessible.", ex);
}
catch (InvocationTargetException ex)
{
// This should never happen in reality.
throw new IllegalArgumentException("Something went wrong in the instance() method.", ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public void add(final Module module)
{
Preconditions.checkNotNull(module);
modules.add(module);
}
/**
* {@inheritDoc}
*/
@Override
public Set<Module> modules()
{
return Collections.unmodifiableSet(modules);
}
/**
* {@inheritDoc}
*/
@Override
public TestResults run()
{
// The test-results will be added to this list as the test-cases are run.
final List<TestResult> results = Lists.newLinkedList();
// This will be used to count the number of failed tests.
final AtomicInteger failed = new AtomicInteger(0);
// This will be used to sum the amount of time that the test-cases spent executing.
final AtomicLong time = new AtomicLong(0);
/**
* Find and execute the test-cases.
*/
for (Module module : modules)
{
test(results, failed, time, module);
}
/**
* Create the object that describes the test-results.
*/
return new TestResults()
{
@Override
public boolean passed()
{
return !failed();
}
@Override
public boolean failed()
{
return failed.get() != 0;
}
@Override
public long executionTime()
{
return time.get();
}
@Override
public void print(PrintStream out)
{
for (TestResult result : results)
{
result.print(out);
out.println();
}
out.println("Execution Time = " + executionTime() + " Milliseconds");
out.println();
// Print the number of test-cases that passed and failed.
out.println(this);
out.println();
}
@Override
public TestResult find(final String name)
{
Preconditions.checkNotNull(name);
for (TestResult result : results())
{
if (result.name().equals(name))
{
return result;
}
}
throw new NoSuchElementException(name);
}
@Override
public List<TestResult> results()
{
return Collections.unmodifiableList(results);
}
@Override
public Iterator<TestResult> iterator()
{
return results().iterator();
}
@Override
public String toString()
{
// This is the number of test-cases that failed.
final int d2 = failed.get();
// This is the number of test-cases that passed.
final int d1 = d2 - results.size();
return d2 == 0
? "All Tests Passed!"
: String.format("Test Results: Passed = %d, Failed = %d", d1, d2);
}
};
}
/**
* This method runs the test functions in a single module.
*
* @param results is the mutable list to append the result onto.
* @param failed is the running tally of failed test-cases.
* @param time is the running sum of the execution times.
* @param module is the module that contains the test-cases.
*/
private void test(final List<TestResult> results,
final AtomicInteger failed,
final AtomicLong time,
final Module module)
{
for (Delegate function : module.info().functions())
{
// Get the reflective view of the function.
final Method method = function.method();
// The function is a test-case, if the @Test annotation is applied to it.
if (method.isAnnotationPresent(Test.class))
{
// Execute the test-case.
test(results, failed, time, function);
}
}
}
/**
* This method runs the a single test-case.
*
* @param results is the mutable list to append the result onto.
* @param failed is the running tally of failed test-cases.
* @param time is the running sum of the execution times.
* @param delegate is a delegate that refers to the test function.
*/
private void test(final List<TestResult> results,
final AtomicInteger failed,
final AtomicLong time,
final Delegate function)
{
// This will store the name of the test-case.
final AtomicReference<String> test_name = new AtomicReference<String>();
// This will store the description of the test-case.
final AtomicReference<String> test_description = new AtomicReference<String>();
// This will store the expected type of exception.
final AtomicReference<Class> test_exception = new AtomicReference<Class>();
// The default name of the test-case is of the form <module-class>::<function>.
test_name.set(function.module().getClass().getSimpleName() + "::" + function.name());
/**
* This class provides a concrete implementation of the TestCase interface.
*/
final TestCase testcase = new TestCase()
{
@Override
public Delegate function()
{
return function;
}
@Override
public void name(final String name)
{
Preconditions.checkNotNull(name);
test_name.set(name);
}
@Override
public void describe(final String text)
{
test_description.set(text);
}
@Override
public void expect(final Class expected)
{
test_exception.set(expected);
}
@Override
public String toString()
{
return "Test Case: " + test_name.get();
}
};
long start_time = 0;
Throwable ex = null;
/**
* Execute the test-case.
*/
try
{
start_time = System.currentTimeMillis();
validate(function);
T.apply(function, Collections.singleton(testcase));
}
catch (Throwable t)
{
ex = t;
}
// Record the exception that was thrown, if any.
final Throwable thrown = ex;
// Calculate the total time this test-case spent executing.
final long total_time = System.currentTimeMillis() - start_time;
// Add the time this test-case spent executing to the total execution time.
time.addAndGet(total_time);
// Determine whether the test-case passed.
final boolean case1 = thrown != null && test_exception.get() != null && F.isSubtypeOf(thrown.getClass(), test_exception.get());
final boolean case2 = thrown == null && test_exception.get() == null;
final boolean passed = case1 || case2;
// If the test failed, record the failure.
failed.set(passed ? failed.get() : failed.get() + 1);
/**
* Create the object that represents the result of the test-case.
*/
final TestResult result = new TestResult()
{
@Override
public Delegate function()
{
return function;
}
@Override
public String name()
{
return test_name.get();
}
@Override
public String description()
{
return test_description.get();
}
@Override
public Class expected()
{
return test_exception.get();
}
@Override
public Throwable thrown()
{
return thrown;
}
@Override
public boolean passed()
{
return passed;
}
@Override
public boolean failed()
{
return !passed;
}
@Override
public long executionTime()
{
return total_time;
}
@Override
public void print(final PrintStream out)
{
if (passed())
{
out.println("Test Passed: " + name());
}
else
{
out.println("Test Failed: " + name());
thrown().printStackTrace(out);
}
}
@Override
public String toString()
{
return (passed() ? "Test Passed: " : "Test Failed: ") + name();
}
};
// Record the result.
results.add(result);
}
/**
* This method ensures that a test function is acceptable.
*
* @param function is the function to validate.
*/
private void validate(final Delegate function)
{
if (function.returnType().equals(void.class) == false)
{
throw new MalformedTestException("The return-type of a test function must be void.");
}
if (function.parameterTypes().size() != 1)
{
throw new MalformedTestException("A test function must take exactly one parameter.");
}
if (F.isSubtypeOf(function.parameterTypes().get(0), TestCase.class) == false)
{
throw new MalformedTestException("The type of a test function's only parameter must be TestCase.");
}
}
}