/** * Powerunit - A JDK1.8 test framework * Copyright (C) 2014 Mathieu Boretti. * * This file is part of Powerunit * * Powerunit is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Powerunit is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Powerunit. If not, see <http://www.gnu.org/licenses/>. */ package ch.powerunit.impl; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; import ch.powerunit.Categories; import ch.powerunit.Ignore; import ch.powerunit.Parameter; import ch.powerunit.Parameters; import ch.powerunit.PowerUnitRunner; import ch.powerunit.Rule; import ch.powerunit.Statement; import ch.powerunit.Test; import ch.powerunit.TestContext; import ch.powerunit.TestDelegate; import ch.powerunit.TestInterface; import ch.powerunit.TestResultListener; import ch.powerunit.TestRule; import ch.powerunit.exception.AssumptionError; import ch.powerunit.exception.InternalError; import ch.powerunit.impl.validator.ParameterValidator; import ch.powerunit.impl.validator.ParametersValidator; import ch.powerunit.impl.validator.RuleValidator; import ch.powerunit.impl.validator.TestDelegateValidator; import ch.powerunit.impl.validator.TestValidator; public class DefaultPowerUnitRunnerImpl<T> implements PowerUnitRunner<T>, ParametersValidator, ParameterValidator, TestValidator, RuleValidator, TestDelegateValidator { private static final Map<Integer, TestContextImpl<Object>> contexts = new HashMap<>(); private static final ThreadLocal<TestContextImpl<Object>> threadContext = new ThreadLocal<TestContextImpl<Object>>(); private final List<TestResultListener<Object>> listeners = new ArrayList<>(); private final String parentGroups; private final T targetObject; private final String setName; private final Method singleMethod; private final Object externalParameter; static TestContextImpl<Object> getCurrentContext(Object underTest) { return Optional.ofNullable( contexts.get(System.identityHashCode(underTest))).orElse( threadContext.get()); } private static void setCurrentContext(Object underTest, TestContextImpl<Object> ctx) { contexts.put(System.identityHashCode(underTest), ctx); threadContext.set(ctx); } private static void resetCurrentContext(Object underTest) { contexts.remove(System.identityHashCode(underTest)); threadContext.set(null); } DefaultPowerUnitRunnerImpl(Class<T> testClass, Object externalParameter) { this(testClass, null, externalParameter); } public DefaultPowerUnitRunnerImpl(Class<T> testClass) { this(testClass, null); } public DefaultPowerUnitRunnerImpl(Class<T> testClass, Method singleMethod) { this(testClass, singleMethod, null); } DefaultPowerUnitRunnerImpl(Class<T> testClass, Method singleMethod, Object externalParameter) { Objects.requireNonNull(testClass); this.singleMethod = singleMethod; this.setName = testClass.getName(); this.externalParameter = externalParameter; Set<String> groups = findClass(testClass) .stream() .filter(c -> c.isAnnotationPresent(Categories.class)) .map(c -> Arrays.stream( c.getAnnotation(Categories.class).value()).collect( Collectors.toCollection(() -> new HashSet<>()))) .reduce((o, n) -> { o.addAll(n); return o; }).orElse(new HashSet<>()); this.parentGroups = groups.isEmpty() ? TestResultListener.ALL_GROUPS : Arrays.toString(groups.toArray()); try { targetObject = testClass.newInstance(); } catch (InstantiationException | IllegalAccessException e) { throw new InternalError("Unexpected error " + e.getMessage(), e); } if (testClass.isAnnotationPresent(Ignore.class)) { executableTests.put(setName, p -> { TestContextImpl<Object> ctx = new TestContextImpl<>( targetObject, setName, setName, null, parentGroups); notifyStartTest(ctx); notifyEndSkippedTest(ctx); }); return; } findTestsMethod(targetObject, testClass, parentGroups); findTestsRule(targetObject, testClass); findParametersMethod(targetObject, testClass); findDelegateTest(targetObject, testClass); computeExecutableStatements(); computeDelegateStatements(); } @Override public void run() { notifyStartTests(setName, parentGroups); try { if (parameters != null) { runAll(); } else { runOne(null); } } finally { notifyEndTests(setName, parentGroups); } } private void runAll() { testIndex = 0; try (Stream<?> params = (Stream<?>) (externalParameter == null ? parameters .invoke(targetObject) : parameters.invoke(targetObject, externalParameter))) { params.forEach(this::runOneParameter); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new InternalError("Unexpected error " + e.getMessage(), e); } } private int testIndex = 0; private void runOneParameter(Object op) { String formatter = parameters.getAnnotation(Parameters.class).value(); if ("".equals(formatter)) { formatter = "" + testIndex; } Object o[]; if (op != null && op.getClass().isArray()) { o = (Object[]) op; } else { o = new Object[] { op }; } String name = computeTestName(formatter, o); try { notifyStartParameter(setName, name); int pidx = 0; if (o.length != parameterFields.size()) { throw new InternalError( "Parameter fields count doesn't match with array size returned by parameters"); } for (Object p : o) { try { Field f = parameterFields.get(pidx); if (f == null) { throw new InternalError("Field " + pidx + " is not found"); } f.set(targetObject, p); } catch (IllegalArgumentException | IllegalAccessException e) { throw new InternalError("Unexpected error " + e.getMessage(), e); } pidx++; } runOne(name, o); testIndex++; } finally { notifyEndParameter(setName, name); } } @SuppressWarnings("unchecked") private void runOne(String name, Object... parameters) { executableTests .entrySet() .forEach( singleTest -> { try { boolean run = true; String tname = singleTest.getKey(); if (filterParameterField != null) { run = ((BiFunction<String, Object, Boolean>) filterParameterField .get(targetObject)).apply( testMethods.get(tname).getName(), parameters); } if (run) { if (parameters.length > 0) { tname = computeTestName(tname, parameters); } singleTest.getValue().run( new TestContextImpl<Object>( targetObject, setName, tname, name, parentGroups)); } } catch (Throwable e) {// NOSONAR // As we really want all error throw new InternalError("Unexpected error " + e.getMessage(), e); } }); delegateTests .entrySet() .forEach( singleTest -> { try { boolean run = true; String tname = singleTest.getKey(); if (filterParameterField != null) { run = ((BiFunction<String, Object, Boolean>) filterParameterField .get(targetObject)).apply(tname, parameters); } if (run) { if (parameters.length > 0) { tname = computeTestName(tname, parameters); } singleTest.getValue().run( new TestContextImpl<Object>( targetObject, setName, tname, name, parentGroups)); } } catch (Throwable e) {// NOSONAR // As we really want all error throw new InternalError("Unexpected error " + e.getMessage(), e); } }); } // public for test purpose public static String computeTestName(String formatter, Object... arguments) { if (formatter.matches(".*\\{[0-9]+\\}.*")) { return MessageFormat.format(formatter, arguments); } return String.format(formatter, arguments); } private final Map<String, Method> testMethods = new HashMap<>(); private TestRule testRules = null; private Map<String, Statement<TestContext<Object>, Throwable>> executableTests = new HashMap<>(); private Map<String, Statement<TestContext<Object>, Throwable>> delegateTests = new HashMap<>(); private Method parameters = null; private Map<Integer, Field> parameterFields; private Field filterParameterField = null; private Map<String, Supplier<Object>> delegateTest = new HashMap<>(); private void findDelegateTest(T targetObject, Class<T> testClass) { findClass(testClass) .stream() .forEach( cls -> Arrays .stream(cls.getDeclaredFields()) .filter(f -> f .isAnnotationPresent(TestDelegate.class)) .forEach( f -> { checkTestDelegateAnnotationForField(f); if (Supplier.class .isAssignableFrom(f .getType())) { try { delegateTest.put( f.getName(), (Supplier<Object>) f .get(targetObject)); return; } catch ( IllegalAccessException | IllegalArgumentException e) { throw new InternalError( "Unexpected error " + e.getMessage(), e); } } delegateTest.put( f.getName(), (Supplier<Object>) () -> { try { return f.get(targetObject); } catch ( IllegalAccessException | IllegalArgumentException e) { throw new InternalError( "Unexpected error " + e.getMessage(), e); } }); })); } private void findParametersMethod(T targetObject, Class<T> testClass) { parameters = Arrays .stream(testClass.getDeclaredMethods()) .filter(m -> m.isAnnotationPresent(Parameters.class)) .peek(m -> checkParametersAnnotationForMethod(m)) .reduce((o, n) -> { throw new InternalError( "@Parameters method can't only be once"); }).orElse(null); parameterFields = Arrays .stream(testClass.getDeclaredFields()) .filter(f -> f.isAnnotationPresent(Parameter.class)) .peek(f -> { if (parameters == null) { throw new InternalError( "@Parameter can't be used without @Parameters method"); } }) .peek(f -> checkParameterAnnotationForField(f)) .collect( Collectors .<Field, Integer, Field> toMap( (Field f) -> f.getAnnotation( Parameter.class).value(), (Field f) -> f, (f1, f2) -> { throw new InternalError( "@Parameter can't be used twice with the same value number"); })); if (parameters != null) { // assuming field numbering 0 to int size = parameterFields.size(); if (size == 0) { throw new InternalError("No @Parameter field found"); } int expected = size * (size - 1) / 2; int sum = parameterFields.keySet().stream().mapToInt(i -> i).sum(); if (sum != expected) { throw new InternalError( "@Parameter field number aren't continuus"); } parameterFields .values() .stream() .forEach( f -> { Parameter p = f.getAnnotation(Parameter.class); if (p.filter()) { if (filterParameterField != null) { throw new InternalError( "@Parameter filter attribute can only be used once per test class."); } if (!BiFunction.class.isAssignableFrom(f .getType())) { throw new InternalError( "@Parameter filter attribute can only be use on BiFunction."); } filterParameterField = f; } }); } } private void findTestsMethod(T targetObject, Class<T> testClass, String parentGroup) { findClass(testClass).forEach( cls -> { Arrays.stream(cls.getDeclaredMethods()) .filter(m -> m.isAnnotationPresent(Test.class)) .filter(m -> singleMethod == null || singleMethod.equals(m)).forEach(m -> { checkTestAnnotationForMethod(m); Test annotation = m.getAnnotation(Test.class); String testName = m.getName(); if (!"".equals(annotation.name())) { testName = annotation.name(); } testMethods.put(testName, m); }); }); } private void findTestsRule(T targetObject, Class<T> testClass) { testRules = findClass(testClass) .stream() .map(cls -> Arrays .stream(cls.getDeclaredFields()) .filter(f -> f.isAnnotationPresent(Rule.class)) .map(f -> { checkRuleAnnotationForField(f); try { TestRule tr1 = (TestRule) f.get(targetObject); if (tr1 == null) { throw new InternalError( "@Rule annotation is used on a null field. This is not allowed"); } return tr1; } catch (IllegalAccessException | IllegalArgumentException e) { throw new InternalError("Unexpected error " + e.getMessage(), e); } }) .reduce((o, n) -> { throw new InternalError( "@Rule annotation can only be used once on field"); }).orElse(null)).filter(i -> i != null) .reduce((o, n) -> o.around(n)).orElse(null); } private List<Class<?>> findClass(Class<T> testClass) { List<Class<?>> clazzs = new ArrayList<>(); Class<?> current = testClass; while (current != null) { clazzs.add(0, current); current = current.getSuperclass(); } return clazzs; } private void computeExecutableStatements() { executableTests = testMethods .entrySet() .stream() .collect( Collectors.toMap( test -> test.getKey(), test -> { Statement<TestContext<Object>, Throwable> stest; if (test.getValue().isAnnotationPresent( Ignore.class)) { stest = p -> { throw new AssumptionError( "Test method is annotated with @Ignore"); }; } else { Statement<TestContext<Object>, Throwable> itest = p -> { Statement .<TestContext<Object>, Throwable> reflectionMethod( targetObject, test.getValue()) .run(p); }; if (testRules != null) { stest = p -> testRules .computeStatement(itest) .run(p); } else { stest = itest; } } return p -> { ((TestContextImpl) p).setFastFail(test .getValue() .getAnnotation(Test.class) .fastFail()); setCurrentContext( p.getTestSuiteObject(), (TestContextImpl) p); notifyStartTest(p); try { stest.run(p); if (((TestContextImpl) p) .hasError() && !((TestContextImpl) p) .isFastFail()) { ((TestContextImpl) p).fail(); } notifyEndSuccessTest(p); } catch (InternalError e) { notifyEndFailureTest(p, e); } catch (AssertionError e) { notifyEndFailureTest(p, e); } catch (AssumptionError e) { notifyEndSkippedTest(p); } catch (Throwable e) {// NOSONAR // As we really want all error notifyEndFailureTest(p, e); } resetCurrentContext(p .getTestSuiteObject()); }; })); } private void computeDelegateStatements() { delegateTests = delegateTest.entrySet().stream() .collect(Collectors.toMap(test -> test.getKey(), test -> { Statement<TestContext<Object>, Throwable> stest; Statement<TestContext<Object>, Throwable> itest = p -> { new Statement<TestContext<Object>, Throwable>() { @Override public void run(TestContext<Object> parameter) throws Throwable { Supplier<Object> o = test.getValue(); Object target = o.get(); Class<?> delegator = target.getClass() .getAnnotation(TestInterface.class) .value(); DefaultPowerUnitRunnerImpl<?> dpu = new DefaultPowerUnitRunnerImpl<>( delegator, target); dpu.addListener((TestResultListener) new DelegationTestResultListener( parameter)); dpu.run(); } @Override public String getName() { return test.getKey(); } }.run(p); }; if (testRules != null) { stest = p -> testRules.computeStatement(itest).run(p); } else { stest = itest; } return p -> { try { stest.run(p); } catch (InternalError e) { notifyStartTest(p); notifyEndFailureTest(p, e); } catch (AssertionError e) { notifyStartTest(p); notifyEndFailureTest(p, e); } catch (AssumptionError e) { notifyStartTest(p); notifyEndSkippedTest(p); } catch (Throwable e) {// NOSONAR // As we really want all error notifyStartTest(p); notifyEndFailureTest(p, e); } }; })); } @Override public void addListener(TestResultListener<T> listener) { listeners.add((TestResultListener) listener); } private void notifyStartTests(String setName, String groups) { listeners.forEach(trl -> trl.notifySetStart(setName, groups)); } private void notifyEndTests(String setName, String groups) { listeners.forEach(trl -> trl.notifySetEnd(setName, groups)); } private void notifyStartParameter(String setName, String parameterName) { listeners.forEach(trl -> trl.notifyParameterStart(setName, parameterName)); } private void notifyEndParameter(String setName, String parameterName) { listeners .forEach(trl -> trl.notifyParameterEnd(setName, parameterName)); } private void notifyStartTest(TestContext<Object> context) { listeners.forEach(trl -> trl.notifyStart(context)); } private void notifyEndSuccessTest(TestContext<Object> context) { listeners.forEach(trl -> trl.notifySuccess(context)); } private void notifyEndSkippedTest(TestContext<Object> context) { listeners.forEach(trl -> trl.notifySkipped(context)); } private void notifyEndFailureTest(TestContext<Object> context, AssertionError cause) { listeners.forEach(trl -> trl.notifyFailure(context, cause)); } private void notifyEndFailureTest(TestContext<Object> context, InternalError cause) { listeners.forEach(trl -> trl.notifyError(context, cause)); } private void notifyEndFailureTest(TestContext<Object> context, Throwable cause) { listeners.forEach(trl -> trl.notifyError(context, cause)); } private class DelegationTestResultListener implements TestResultListener<Object> { private final TestContext<Object> parentContext; public DelegationTestResultListener(TestContext<Object> parentContext) { this.parentContext = parentContext; } @Override public void notifySetStart(String setName, String groups) { // ignore } @Override public void notifySetEnd(String setName, String groups) { // ignore } @Override public void notifyStart(TestContext<Object> context) { TestContext<Object> ctx = new TestContextImpl<Object>( parentContext.getTestSuiteObject(), parentContext.getSetName(), context.getLocalTestName(), parentContext.getParameterName(), parentContext.getTestCategories()); notifyStartTest(ctx); } @Override public void notifySuccess(TestContext<Object> context) { TestContext<Object> ctx = new TestContextImpl<Object>( parentContext.getTestSuiteObject(), parentContext.getSetName(), context.getLocalTestName(), parentContext.getParameterName(), parentContext.getTestCategories()); notifyEndSuccessTest(ctx); } @Override public void notifyFailure(TestContext<Object> context, Throwable cause) { TestContext<Object> ctx = new TestContextImpl<Object>( parentContext.getTestSuiteObject(), parentContext.getSetName(), context.getLocalTestName(), parentContext.getParameterName(), parentContext.getTestCategories()); notifyEndFailureTest(ctx, (AssertionError) cause); } @Override public void notifyError(TestContext<Object> context, Throwable cause) { TestContext<Object> ctx = new TestContextImpl<Object>( parentContext.getTestSuiteObject(), parentContext.getSetName(), context.getLocalTestName(), parentContext.getParameterName(), parentContext.getTestCategories()); notifyEndFailureTest(ctx, cause); } @Override public void notifySkipped(TestContext<Object> context) { TestContext<Object> ctx = new TestContextImpl<Object>( parentContext.getTestSuiteObject(), parentContext.getSetName(), context.getLocalTestName(), parentContext.getParameterName(), parentContext.getTestCategories()); notifyEndSkippedTest(ctx); } @Override public void notifyParameterStart(String setName, String parameterName) { // ignore } @Override public void notifyParameterEnd(String setName, String parameterName) { // ignore } } }