diff options
Diffstat (limited to 'src/main/java/org/junit/runners')
18 files changed, 962 insertions, 260 deletions
diff --git a/src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java b/src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java index 4d06199..455341a 100644 --- a/src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java +++ b/src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java @@ -3,8 +3,10 @@ package org.junit.runners; import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_METHOD_VALIDATOR; import static org.junit.internal.runners.rules.RuleMemberValidator.RULE_VALIDATOR; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -21,14 +23,18 @@ import org.junit.internal.runners.statements.InvokeMethod; import org.junit.internal.runners.statements.RunAfters; import org.junit.internal.runners.statements.RunBefores; import org.junit.rules.MethodRule; -import org.junit.rules.RunRules; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; +import org.junit.runners.model.FrameworkMember; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; +import org.junit.runners.model.MemberValueConsumer; import org.junit.runners.model.MultipleFailureException; import org.junit.runners.model.Statement; +import org.junit.runners.model.TestClass; +import org.junit.validator.PublicClassValidator; +import org.junit.validator.TestClassValidator; /** * Implements the JUnit 4 standard test case class model, as defined by the @@ -55,14 +61,27 @@ import org.junit.runners.model.Statement; * @since 4.5 */ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { - private final ConcurrentHashMap<FrameworkMethod, Description> methodDescriptions = new ConcurrentHashMap<FrameworkMethod, Description>(); + private static TestClassValidator PUBLIC_CLASS_VALIDATOR = new PublicClassValidator(); + + private final ConcurrentMap<FrameworkMethod, Description> methodDescriptions = new ConcurrentHashMap<FrameworkMethod, Description>(); + + /** + * Creates a BlockJUnit4ClassRunner to run {@code testClass} + * + * @throws InitializationError if the test class is malformed. + */ + public BlockJUnit4ClassRunner(Class<?> testClass) throws InitializationError { + super(testClass); + } + /** - * Creates a BlockJUnit4ClassRunner to run {@code klass} + * Creates a BlockJUnit4ClassRunner to run {@code testClass}. * * @throws InitializationError if the test class is malformed. + * @since 4.13 */ - public BlockJUnit4ClassRunner(Class<?> klass) throws InitializationError { - super(klass); + protected BlockJUnit4ClassRunner(TestClass testClass) throws InitializationError { + super(testClass); } // @@ -75,10 +94,16 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { if (isIgnored(method)) { notifier.fireTestIgnored(description); } else { - runLeaf(methodBlock(method), description, notifier); + Statement statement = new Statement() { + @Override + public void evaluate() throws Throwable { + methodBlock(method).evaluate(); + } + }; + runLeaf(statement, description, notifier); } } - + /** * Evaluates whether {@link FrameworkMethod}s are ignored based on the * {@link Ignore} annotation. @@ -123,6 +148,7 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { protected void collectInitializationErrors(List<Throwable> errors) { super.collectInitializationErrors(errors); + validatePublicConstructor(errors); validateNoNonStaticInnerClass(errors); validateConstructor(errors); validateInstanceMethods(errors); @@ -130,6 +156,12 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { validateMethods(errors); } + private void validatePublicConstructor(List<Throwable> errors) { + if (getTestClass().getJavaClass() != null) { + errors.addAll(PUBLIC_CLASS_VALIDATOR.validateTestClass(getTestClass())); + } + } + protected void validateNoNonStaticInnerClass(List<Throwable> errors) { if (getTestClass().isANonStaticInnerClass()) { String gripe = "The inner class " + getTestClass().getName() @@ -180,6 +212,7 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { * Adds to {@code errors} for each method annotated with {@code @Test}, * {@code @Before}, or {@code @After} that is not a public, void instance * method with no arguments. + * @deprecated */ @Deprecated protected void validateInstanceMethods(List<Throwable> errors) { @@ -187,7 +220,7 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { validatePublicVoidNoArgMethods(Before.class, false, errors); validateTestMethods(errors); - if (computeTestMethods().size() == 0) { + if (computeTestMethods().isEmpty()) { errors.add(new Exception("No runnable methods")); } } @@ -218,6 +251,16 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { } /** + * Returns a new fixture to run a particular test {@code method} against. + * Default implementation executes the no-argument {@link #createTest()} method. + * + * @since 4.13 + */ + protected Object createTest(FrameworkMethod method) throws Exception { + return createTest(); + } + + /** * Returns the name that describes {@code method} for {@link Description}s. * Default implementation is the method's name */ @@ -232,10 +275,10 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { * Here is an outline of the default implementation: * * <ul> - * <li>Invoke {@code method} on the result of {@code createTest()}, and + * <li>Invoke {@code method} on the result of {@link #createTest(org.junit.runners.model.FrameworkMethod)}, and * throw any exceptions thrown by either operation. - * <li>HOWEVER, if {@code method}'s {@code @Test} annotation has the {@code - * expecting} attribute, return normally only if the previous step threw an + * <li>HOWEVER, if {@code method}'s {@code @Test} annotation has the {@link Test#expected()} + * attribute, return normally only if the previous step threw an * exception of the correct type, and throw an exception otherwise. * <li>HOWEVER, if {@code method}'s {@code @Test} annotation has the {@code * timeout} attribute, throw an exception if the previous step takes more @@ -257,13 +300,13 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { * This can be overridden in subclasses, either by overriding this method, * or the implementations creating each sub-statement. */ - protected Statement methodBlock(FrameworkMethod method) { + protected Statement methodBlock(final FrameworkMethod method) { Object test; try { test = new ReflectiveCallable() { @Override protected Object runReflectiveCall() throws Throwable { - return createTest(); + return createTest(method); } }.run(); } catch (Throwable e) { @@ -276,6 +319,7 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { statement = withBefores(method, test, statement); statement = withAfters(method, test, statement); statement = withRules(method, test, statement); + statement = withInterruptIsolation(statement); return statement; } @@ -292,21 +336,22 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { /** * Returns a {@link Statement}: if {@code method}'s {@code @Test} annotation - * has the {@code expecting} attribute, return normally only if {@code next} + * has the {@link Test#expected()} attribute, return normally only if {@code next} * throws an exception of the correct type, and throw an exception * otherwise. */ protected Statement possiblyExpectingExceptions(FrameworkMethod method, Object test, Statement next) { Test annotation = method.getAnnotation(Test.class); - return expectsException(annotation) ? new ExpectException(next, - getExpectedException(annotation)) : next; + Class<? extends Throwable> expectedExceptionClass = getExpectedException(annotation); + return expectedExceptionClass != null ? new ExpectException(next, expectedExceptionClass) : next; } /** * Returns a {@link Statement}: if {@code method}'s {@code @Test} annotation * has the {@code timeout} attribute, throw an exception if {@code next} * takes more than the specified number of milliseconds. + * @deprecated */ @Deprecated protected Statement withPotentialTimeout(FrameworkMethod method, @@ -348,28 +393,23 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { target); } - private Statement withRules(FrameworkMethod method, Object target, - Statement statement) { - List<TestRule> testRules = getTestRules(target); - Statement result = statement; - result = withMethodRules(method, testRules, target, result); - result = withTestRules(method, testRules, result); - - return result; - } - - private Statement withMethodRules(FrameworkMethod method, List<TestRule> testRules, - Object target, Statement result) { - for (org.junit.rules.MethodRule each : getMethodRules(target)) { - if (!testRules.contains(each)) { - result = each.apply(result, method, target); + private Statement withRules(FrameworkMethod method, Object target, Statement statement) { + RuleContainer ruleContainer = new RuleContainer(); + CURRENT_RULE_CONTAINER.set(ruleContainer); + try { + List<TestRule> testRules = getTestRules(target); + for (MethodRule each : rules(target)) { + if (!(each instanceof TestRule && testRules.contains(each))) { + ruleContainer.add(each); + } } + for (TestRule rule : testRules) { + ruleContainer.add(rule); + } + } finally { + CURRENT_RULE_CONTAINER.remove(); } - return result; - } - - private List<org.junit.rules.MethodRule> getMethodRules(Object target) { - return rules(target); + return ruleContainer.apply(method, describeChild(method), target, statement); } /** @@ -378,27 +418,12 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { * test */ protected List<MethodRule> rules(Object target) { - List<MethodRule> rules = getTestClass().getAnnotatedMethodValues(target, - Rule.class, MethodRule.class); - - rules.addAll(getTestClass().getAnnotatedFieldValues(target, - Rule.class, MethodRule.class)); - - return rules; - } - - /** - * Returns a {@link Statement}: apply all non-static fields - * annotated with {@link Rule}. - * - * @param statement The base statement - * @return a RunRules statement if any class-level {@link Rule}s are - * found, or the base statement - */ - private Statement withTestRules(FrameworkMethod method, List<TestRule> testRules, - Statement statement) { - return testRules.isEmpty() ? statement : - new RunRules(statement, testRules, describeChild(method)); + RuleCollector<MethodRule> collector = new RuleCollector<MethodRule>(); + getTestClass().collectAnnotatedMethodValues(target, Rule.class, MethodRule.class, + collector); + getTestClass().collectAnnotatedFieldValues(target, Rule.class, MethodRule.class, + collector); + return collector.result; } /** @@ -407,13 +432,10 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { * test */ protected List<TestRule> getTestRules(Object target) { - List<TestRule> result = getTestClass().getAnnotatedMethodValues(target, - Rule.class, TestRule.class); - - result.addAll(getTestClass().getAnnotatedFieldValues(target, - Rule.class, TestRule.class)); - - return result; + RuleCollector<TestRule> collector = new RuleCollector<TestRule>(); + getTestClass().collectAnnotatedMethodValues(target, Rule.class, TestRule.class, collector); + getTestClass().collectAnnotatedFieldValues(target, Rule.class, TestRule.class, collector); + return collector.result; } private Class<? extends Throwable> getExpectedException(Test annotation) { @@ -424,14 +446,28 @@ public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> { } } - private boolean expectsException(Test annotation) { - return getExpectedException(annotation) != null; - } - private long getTimeout(Test annotation) { if (annotation == null) { return 0; } return annotation.timeout(); } + + private static final ThreadLocal<RuleContainer> CURRENT_RULE_CONTAINER = + new ThreadLocal<RuleContainer>(); + + private static class RuleCollector<T> implements MemberValueConsumer<T> { + final List<T> result = new ArrayList<T>(); + + public void accept(FrameworkMember<?> member, T value) { + Rule rule = member.getAnnotation(Rule.class); + if (rule != null) { + RuleContainer container = CURRENT_RULE_CONTAINER.get(); + if (container != null) { + container.setOrder(value, rule.order()); + } + } + result.add(value); + } + } } diff --git a/src/main/java/org/junit/runners/JUnit4.java b/src/main/java/org/junit/runners/JUnit4.java index 6ba28c2..28eafb3 100644 --- a/src/main/java/org/junit/runners/JUnit4.java +++ b/src/main/java/org/junit/runners/JUnit4.java @@ -1,6 +1,7 @@ package org.junit.runners; import org.junit.runners.model.InitializationError; +import org.junit.runners.model.TestClass; /** * Aliases the current default JUnit 4 class runner, for future-proofing. If @@ -19,6 +20,6 @@ public final class JUnit4 extends BlockJUnit4ClassRunner { * Constructs a new instance of the default runner */ public JUnit4(Class<?> klass) throws InitializationError { - super(klass); + super(new TestClass(klass)); } } diff --git a/src/main/java/org/junit/runners/Parameterized.java b/src/main/java/org/junit/runners/Parameterized.java index 829c8f0..d11b66a 100644 --- a/src/main/java/org/junit/runners/Parameterized.java +++ b/src/main/java/org/junit/runners/Parameterized.java @@ -1,5 +1,6 @@ package org.junit.runners; +import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -8,12 +9,18 @@ import java.lang.annotation.Target; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; +import org.junit.internal.AssumptionViolatedException; +import org.junit.runner.Description; +import org.junit.runner.Result; import org.junit.runner.Runner; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.FrameworkMethod; -import org.junit.runners.model.InitializationError; +import org.junit.runners.model.InvalidTestClassError; import org.junit.runners.model.TestClass; import org.junit.runners.parameterized.BlockJUnit4ClassRunnerWithParametersFactory; import org.junit.runners.parameterized.ParametersRunnerFactory; @@ -24,34 +31,37 @@ import org.junit.runners.parameterized.TestWithParameters; * When running a parameterized test class, instances are created for the * cross-product of the test methods and the test data elements. * <p> - * For example, to test a Fibonacci function, write: + * For example, to test the <code>+</code> operator, write: * <pre> * @RunWith(Parameterized.class) - * public class FibonacciTest { - * @Parameters(name= "{index}: fib[{0}]={1}") + * public class AdditionTest { + * @Parameters(name = "{index}: {0} + {1} = {2}") * public static Iterable<Object[]> data() { - * return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, - * { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); + * return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 }, + * { 3, 2, 5 }, { 4, 3, 7 } }); * } * - * private int fInput; + * private int firstSummand; * - * private int fExpected; + * private int secondSummand; * - * public FibonacciTest(int input, int expected) { - * fInput= input; - * fExpected= expected; + * private int sum; + * + * public AdditionTest(int firstSummand, int secondSummand, int sum) { + * this.firstSummand = firstSummand; + * this.secondSummand = secondSummand; + * this.sum = sum; * } * * @Test * public void test() { - * assertEquals(fExpected, Fibonacci.compute(fInput)); + * assertEquals(sum, firstSummand + secondSummand); * } * } * </pre> * <p> - * Each instance of <code>FibonacciTest</code> will be constructed using the - * two-argument constructor and the data values in the + * Each instance of <code>AdditionTest</code> will be constructed using the + * three-argument constructor and the data values in the * <code>@Parameters</code> method. * <p> * In order that you can easily identify the individual tests, you may provide a @@ -69,33 +79,36 @@ import org.junit.runners.parameterized.TestWithParameters; * </dl> * <p> * In the example given above, the <code>Parameterized</code> runner creates - * names like <code>[1: fib(3)=2]</code>. If you don't use the name parameter, + * names like <code>[2: 3 + 2 = 5]</code>. If you don't use the name parameter, * then the current parameter index is used as name. * <p> * You can also write: * <pre> * @RunWith(Parameterized.class) - * public class FibonacciTest { - * @Parameters - * public static Iterable<Object[]> data() { - * return Arrays.asList(new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, - * { 3, 2 }, { 4, 3 }, { 5, 5 }, { 6, 8 } }); - * } - * - * @Parameter(0) - * public int fInput; + * public class AdditionTest { + * @Parameters(name = "{index}: {0} + {1} = {2}") + * public static Iterable<Object[]> data() { + * return Arrays.asList(new Object[][] { { 0, 0, 0 }, { 1, 1, 2 }, + * { 3, 2, 5 }, { 4, 3, 7 } }); + * } + * + * @Parameter(0) + * public int firstSummand; * - * @Parameter(1) - * public int fExpected; + * @Parameter(1) + * public int secondSummand; * - * @Test - * public void test() { - * assertEquals(fExpected, Fibonacci.compute(fInput)); - * } + * @Parameter(2) + * public int sum; + * + * @Test + * public void test() { + * assertEquals(sum, firstSummand + secondSummand); + * } * } * </pre> * <p> - * Each instance of <code>FibonacciTest</code> will be constructed with the default constructor + * Each instance of <code>AdditionTest</code> will be constructed with the default constructor * and fields annotated by <code>@Parameter</code> will be initialized * with the data values in the <code>@Parameters</code> method. * @@ -105,8 +118,7 @@ import org.junit.runners.parameterized.TestWithParameters; * <pre> * @Parameters * public static Object[][] data() { - * return new Object[][] { { 0, 0 }, { 1, 1 }, { 2, 1 }, { 3, 2 }, { 4, 3 }, - * { 5, 5 }, { 6, 8 } }; + * return new Object[][] { { 0, 0, 0 }, { 1, 1, 2 }, { 3, 2, 5 }, { 4, 3, 7 } } }; * } * </pre> * @@ -130,6 +142,19 @@ import org.junit.runners.parameterized.TestWithParameters; * } * </pre> * + * <h3>Executing code before/after executing tests for specific parameters</h3> + * <p> + * If your test needs to perform some preparation or cleanup based on the + * parameters, this can be done by adding public static methods annotated with + * {@code @BeforeParam}/{@code @AfterParam}. Such methods should either have no + * parameters or the same parameters as the test. + * <pre> + * @BeforeParam + * public static void beforeTestsForParameter(String onlyParameter) { + * System.out.println("Testing " + onlyParameter); + * } + * </pre> + * * <h3>Create different runners</h3> * <p> * By default the {@code Parameterized} runner creates a slightly modified @@ -141,7 +166,7 @@ import org.junit.runners.parameterized.TestWithParameters; * The factory must have a public zero-arg constructor. * * <pre> - * public class YourRunnerFactory implements ParameterizedRunnerFactory { + * public class YourRunnerFactory implements ParametersRunnerFactory { * public Runner createRunnerForTestWithParameters(TestWithParameters test) * throws InitializationError { * return YourRunner(test); @@ -160,6 +185,21 @@ import org.junit.runners.parameterized.TestWithParameters; * } * </pre> * + * <h3>Avoid creating parameters</h3> + * <p>With {@link org.junit.Assume assumptions} you can dynamically skip tests. + * Assumptions are also supported by the <code>@Parameters</code> method. + * Creating parameters is stopped when the assumption fails and none of the + * tests in the test class is executed. JUnit reports a + * {@link Result#getAssumptionFailureCount() single assumption failure} for the + * whole test class in this case. + * <pre> + * @Parameters + * public static Iterable<? extends Object> data() { + * String os = System.getProperty("os.name").toLowerCase() + * Assume.assumeTrue(os.contains("win")); + * return Arrays.asList("first test", "second test"); + * } + * </pre> * @since 4.0 */ public class Parameterized extends Suite { @@ -170,7 +210,7 @@ public class Parameterized extends Suite { */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) - public static @interface Parameters { + public @interface Parameters { /** * Optional pattern to derive the test's name from the parameters. Use * numbers in braces to refer to the parameters or the additional data @@ -201,7 +241,7 @@ public class Parameterized extends Suite { */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) - public static @interface Parameter { + public @interface Parameter { /** * Method that returns the index of the parameter in the array * returned by the method annotated by <code>Parameters</code>. @@ -230,122 +270,235 @@ public class Parameterized extends Suite { Class<? extends ParametersRunnerFactory> value() default BlockJUnit4ClassRunnerWithParametersFactory.class; } - private static final ParametersRunnerFactory DEFAULT_FACTORY = new BlockJUnit4ClassRunnerWithParametersFactory(); - - private static final List<Runner> NO_RUNNERS = Collections.<Runner>emptyList(); + /** + * Annotation for {@code public static void} methods which should be executed before + * evaluating tests with particular parameters. + * + * @see org.junit.BeforeClass + * @see org.junit.Before + * @since 4.13 + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface BeforeParam { + } - private final List<Runner> runners; + /** + * Annotation for {@code public static void} methods which should be executed after + * evaluating tests with particular parameters. + * + * @see org.junit.AfterClass + * @see org.junit.After + * @since 4.13 + */ + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.METHOD) + public @interface AfterParam { + } /** * Only called reflectively. Do not use programmatically. */ public Parameterized(Class<?> klass) throws Throwable { - super(klass, NO_RUNNERS); - ParametersRunnerFactory runnerFactory = getParametersRunnerFactory( - klass); - Parameters parameters = getParametersMethod().getAnnotation( - Parameters.class); - runners = Collections.unmodifiableList(createRunnersForParameters( - allParameters(), parameters.name(), runnerFactory)); + this(klass, new RunnersFactory(klass)); } - private ParametersRunnerFactory getParametersRunnerFactory(Class<?> klass) - throws InstantiationException, IllegalAccessException { - UseParametersRunnerFactory annotation = klass - .getAnnotation(UseParametersRunnerFactory.class); - if (annotation == null) { - return DEFAULT_FACTORY; - } else { - Class<? extends ParametersRunnerFactory> factoryClass = annotation - .value(); - return factoryClass.newInstance(); - } + private Parameterized(Class<?> klass, RunnersFactory runnersFactory) throws Exception { + super(klass, runnersFactory.createRunners()); + validateBeforeParamAndAfterParamMethods(runnersFactory.parameterCount); } - @Override - protected List<Runner> getChildren() { - return runners; + private void validateBeforeParamAndAfterParamMethods(Integer parameterCount) + throws InvalidTestClassError { + List<Throwable> errors = new ArrayList<Throwable>(); + validatePublicStaticVoidMethods(Parameterized.BeforeParam.class, parameterCount, errors); + validatePublicStaticVoidMethods(Parameterized.AfterParam.class, parameterCount, errors); + if (!errors.isEmpty()) { + throw new InvalidTestClassError(getTestClass().getJavaClass(), errors); + } } - private TestWithParameters createTestWithNotNormalizedParameters( - String pattern, int index, Object parametersOrSingleParameter) { - Object[] parameters= (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter - : new Object[] { parametersOrSingleParameter }; - return createTestWithParameters(getTestClass(), pattern, index, - parameters); + private void validatePublicStaticVoidMethods( + Class<? extends Annotation> annotation, Integer parameterCount, + List<Throwable> errors) { + List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(annotation); + for (FrameworkMethod fm : methods) { + fm.validatePublicVoid(true, errors); + if (parameterCount != null) { + int methodParameterCount = fm.getMethod().getParameterTypes().length; + if (methodParameterCount != 0 && methodParameterCount != parameterCount) { + errors.add(new Exception("Method " + fm.getName() + + "() should have 0 or " + parameterCount + " parameter(s)")); + } + } + } } - @SuppressWarnings("unchecked") - private Iterable<Object> allParameters() throws Throwable { - Object parameters = getParametersMethod().invokeExplosively(null); - if (parameters instanceof Iterable) { - return (Iterable<Object>) parameters; - } else if (parameters instanceof Object[]) { - return Arrays.asList((Object[]) parameters); - } else { - throw parametersMethodReturnedWrongType(); + private static class AssumptionViolationRunner extends Runner { + private final Description description; + private final AssumptionViolatedException exception; + + AssumptionViolationRunner(TestClass testClass, String methodName, + AssumptionViolatedException exception) { + this.description = Description + .createTestDescription(testClass.getJavaClass(), + methodName + "() assumption violation"); + this.exception = exception; + } + + @Override + public Description getDescription() { + return description; + } + + @Override + public void run(RunNotifier notifier) { + notifier.fireTestAssumptionFailed(new Failure(description, exception)); } } - private FrameworkMethod getParametersMethod() throws Exception { - List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods( - Parameters.class); - for (FrameworkMethod each : methods) { - if (each.isStatic() && each.isPublic()) { - return each; + private static class RunnersFactory { + private static final ParametersRunnerFactory DEFAULT_FACTORY = new BlockJUnit4ClassRunnerWithParametersFactory(); + + private final TestClass testClass; + private final FrameworkMethod parametersMethod; + private final List<Object> allParameters; + private final int parameterCount; + private final Runner runnerOverride; + + private RunnersFactory(Class<?> klass) throws Throwable { + testClass = new TestClass(klass); + parametersMethod = getParametersMethod(testClass); + List<Object> allParametersResult; + AssumptionViolationRunner assumptionViolationRunner = null; + try { + allParametersResult = allParameters(testClass, parametersMethod); + } catch (AssumptionViolatedException e) { + allParametersResult = Collections.emptyList(); + assumptionViolationRunner = new AssumptionViolationRunner(testClass, + parametersMethod.getName(), e); } + allParameters = allParametersResult; + runnerOverride = assumptionViolationRunner; + parameterCount = + allParameters.isEmpty() ? 0 : normalizeParameters(allParameters.get(0)).length; } - throw new Exception("No public static parameters method on class " - + getTestClass().getName()); - } + private List<Runner> createRunners() throws Exception { + if (runnerOverride != null) { + return Collections.singletonList(runnerOverride); + } + Parameters parameters = parametersMethod.getAnnotation(Parameters.class); + return Collections.unmodifiableList(createRunnersForParameters( + allParameters, parameters.name(), + getParametersRunnerFactory())); + } - private List<Runner> createRunnersForParameters( - Iterable<Object> allParameters, String namePattern, - ParametersRunnerFactory runnerFactory) - throws InitializationError, - Exception { - try { - List<TestWithParameters> tests = createTestsForParameters( - allParameters, namePattern); - List<Runner> runners = new ArrayList<Runner>(); - for (TestWithParameters test : tests) { - runners.add(runnerFactory - .createRunnerForTestWithParameters(test)); + private ParametersRunnerFactory getParametersRunnerFactory() + throws InstantiationException, IllegalAccessException { + UseParametersRunnerFactory annotation = testClass + .getAnnotation(UseParametersRunnerFactory.class); + if (annotation == null) { + return DEFAULT_FACTORY; + } else { + Class<? extends ParametersRunnerFactory> factoryClass = annotation + .value(); + return factoryClass.newInstance(); } - return runners; - } catch (ClassCastException e) { - throw parametersMethodReturnedWrongType(); } - } - private List<TestWithParameters> createTestsForParameters( - Iterable<Object> allParameters, String namePattern) - throws Exception { - int i = 0; - List<TestWithParameters> children = new ArrayList<TestWithParameters>(); - for (Object parametersOfSingleTest : allParameters) { - children.add(createTestWithNotNormalizedParameters(namePattern, - i++, parametersOfSingleTest)); + private TestWithParameters createTestWithNotNormalizedParameters( + String pattern, int index, Object parametersOrSingleParameter) { + Object[] parameters = normalizeParameters(parametersOrSingleParameter); + return createTestWithParameters(testClass, pattern, index, parameters); } - return children; - } - private Exception parametersMethodReturnedWrongType() throws Exception { - String className = getTestClass().getName(); - String methodName = getParametersMethod().getName(); - String message = MessageFormat.format( - "{0}.{1}() must return an Iterable of arrays.", - className, methodName); - return new Exception(message); - } + private static Object[] normalizeParameters(Object parametersOrSingleParameter) { + return (parametersOrSingleParameter instanceof Object[]) ? (Object[]) parametersOrSingleParameter + : new Object[] { parametersOrSingleParameter }; + } - private static TestWithParameters createTestWithParameters( - TestClass testClass, String pattern, int index, Object[] parameters) { - String finalPattern = pattern.replaceAll("\\{index\\}", - Integer.toString(index)); - String name = MessageFormat.format(finalPattern, parameters); - return new TestWithParameters("[" + name + "]", testClass, - Arrays.asList(parameters)); + @SuppressWarnings("unchecked") + private static List<Object> allParameters( + TestClass testClass, FrameworkMethod parametersMethod) throws Throwable { + Object parameters = parametersMethod.invokeExplosively(null); + if (parameters instanceof List) { + return (List<Object>) parameters; + } else if (parameters instanceof Collection) { + return new ArrayList<Object>((Collection<Object>) parameters); + } else if (parameters instanceof Iterable) { + List<Object> result = new ArrayList<Object>(); + for (Object entry : ((Iterable<Object>) parameters)) { + result.add(entry); + } + return result; + } else if (parameters instanceof Object[]) { + return Arrays.asList((Object[]) parameters); + } else { + throw parametersMethodReturnedWrongType(testClass, parametersMethod); + } + } + + private static FrameworkMethod getParametersMethod(TestClass testClass) throws Exception { + List<FrameworkMethod> methods = testClass + .getAnnotatedMethods(Parameters.class); + for (FrameworkMethod each : methods) { + if (each.isStatic() && each.isPublic()) { + return each; + } + } + + throw new Exception("No public static parameters method on class " + + testClass.getName()); + } + + private List<Runner> createRunnersForParameters( + Iterable<Object> allParameters, String namePattern, + ParametersRunnerFactory runnerFactory) throws Exception { + try { + List<TestWithParameters> tests = createTestsForParameters( + allParameters, namePattern); + List<Runner> runners = new ArrayList<Runner>(); + for (TestWithParameters test : tests) { + runners.add(runnerFactory + .createRunnerForTestWithParameters(test)); + } + return runners; + } catch (ClassCastException e) { + throw parametersMethodReturnedWrongType(testClass, parametersMethod); + } + } + + private List<TestWithParameters> createTestsForParameters( + Iterable<Object> allParameters, String namePattern) + throws Exception { + int i = 0; + List<TestWithParameters> children = new ArrayList<TestWithParameters>(); + for (Object parametersOfSingleTest : allParameters) { + children.add(createTestWithNotNormalizedParameters(namePattern, + i++, parametersOfSingleTest)); + } + return children; + } + + private static Exception parametersMethodReturnedWrongType( + TestClass testClass, FrameworkMethod parametersMethod) throws Exception { + String className = testClass.getName(); + String methodName = parametersMethod.getName(); + String message = MessageFormat.format( + "{0}.{1}() must return an Iterable of arrays.", className, + methodName); + return new Exception(message); + } + + private TestWithParameters createTestWithParameters( + TestClass testClass, String pattern, int index, + Object[] parameters) { + String finalPattern = pattern.replaceAll("\\{index\\}", + Integer.toString(index)); + String name = MessageFormat.format(finalPattern, parameters); + return new TestWithParameters("[" + name + "]", testClass, + Arrays.asList(parameters)); + } } } diff --git a/src/main/java/org/junit/runners/ParentRunner.java b/src/main/java/org/junit/runners/ParentRunner.java index 92641bf..0a0e7cb 100755..100644 --- a/src/main/java/org/junit/runners/ParentRunner.java +++ b/src/main/java/org/junit/runners/ParentRunner.java @@ -1,21 +1,25 @@ package org.junit.runners; +import static org.junit.internal.Checks.notNull; import static org.junit.internal.runners.rules.RuleMemberValidator.CLASS_RULE_METHOD_VALIDATOR; import static org.junit.internal.runners.rules.RuleMemberValidator.CLASS_RULE_VALIDATOR; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.ClassRule; +import org.junit.FixMethodOrder; import org.junit.Ignore; import org.junit.Rule; import org.junit.internal.AssumptionViolatedException; @@ -28,18 +32,22 @@ import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runner.manipulation.Filterable; +import org.junit.runner.manipulation.Orderer; +import org.junit.runner.manipulation.InvalidOrderingException; import org.junit.runner.manipulation.NoTestsRemainException; -import org.junit.runner.manipulation.Sortable; +import org.junit.runner.manipulation.Orderable; import org.junit.runner.manipulation.Sorter; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; +import org.junit.runners.model.FrameworkMember; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; +import org.junit.runners.model.InvalidTestClassError; +import org.junit.runners.model.MemberValueConsumer; import org.junit.runners.model.RunnerScheduler; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import org.junit.validator.AnnotationsValidator; -import org.junit.validator.PublicClassValidator; import org.junit.validator.TestClassValidator; /** @@ -56,15 +64,15 @@ import org.junit.validator.TestClassValidator; * @since 4.5 */ public abstract class ParentRunner<T> extends Runner implements Filterable, - Sortable { - private static final List<TestClassValidator> VALIDATORS = Arrays.asList( - new AnnotationsValidator(), new PublicClassValidator()); + Orderable { + private static final List<TestClassValidator> VALIDATORS = Collections.<TestClassValidator>singletonList( + new AnnotationsValidator()); - private final Object childrenLock = new Object(); + private final Lock childrenLock = new ReentrantLock(); private final TestClass testClass; // Guarded by childrenLock - private volatile Collection<T> filteredChildren = null; + private volatile List<T> filteredChildren = null; private volatile RunnerScheduler scheduler = new RunnerScheduler() { public void schedule(Runnable childStatement) { @@ -84,6 +92,21 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, validate(); } + /** + * Constructs a new {@code ParentRunner} that will run the {@code TestClass}. + * + * @since 4.13 + */ + protected ParentRunner(TestClass testClass) throws InitializationError { + this.testClass = notNull(testClass); + validate(); + } + + /** + * @deprecated Please use {@link #ParentRunner(org.junit.runners.model.TestClass)}. + * @since 4.12 + */ + @Deprecated protected TestClass createTestClass(Class<?> testClass) { return new TestClass(testClass); } @@ -192,6 +215,7 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); + statement = withInterruptIsolation(statement); } return statement; } @@ -219,7 +243,7 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, /** * Returns a {@link Statement}: run all non-overridden {@code @AfterClass} methods on this class - * and superclasses before executing {@code statement}; all AfterClass methods are + * and superclasses after executing {@code statement}; all AfterClass methods are * always executed: exceptions thrown by previous steps are combined, if * necessary, with exceptions from AfterClass methods into a * {@link org.junit.runners.model.MultipleFailureException}. @@ -251,9 +275,10 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, * each method in the tested class. */ protected List<TestRule> classRules() { - List<TestRule> result = testClass.getAnnotatedMethodValues(null, ClassRule.class, TestRule.class); - result.addAll(testClass.getAnnotatedFieldValues(null, ClassRule.class, TestRule.class)); - return result; + ClassRuleCollector collector = new ClassRuleCollector(); + testClass.collectAnnotatedMethodValues(null, ClassRule.class, TestRule.class, collector); + testClass.collectAnnotatedFieldValues(null, ClassRule.class, TestRule.class, collector); + return collector.getOrderedRules(); } /** @@ -271,6 +296,22 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, } /** + * @return a {@link Statement}: clears interrupt status of current thread after execution of statement + */ + protected final Statement withInterruptIsolation(final Statement statement) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + statement.evaluate(); + } finally { + Thread.interrupted(); // clearing thread interrupted status for isolation + } + } + }; + } + + /** * Evaluates whether a child is ignored. The default implementation always * returns <code>false</code>. * @@ -346,8 +387,16 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, @Override public Description getDescription() { - Description description = Description.createSuiteDescription(getName(), - getRunnerAnnotations()); + Class<?> clazz = getTestClass().getJavaClass(); + Description description; + // if subclass overrides `getName()` then we should use it + // to maintain backwards compatibility with JUnit 4.12 + if (clazz == null || !clazz.getName().equals(getName())) { + description = Description.createSuiteDescription(getName(), getRunnerAnnotations()); + } else { + description = Description.createSuiteDescription(clazz, getRunnerAnnotations()); + } + for (T child : getFilteredChildren()) { description.addChild(describeChild(child)); } @@ -358,6 +407,7 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, public void run(final RunNotifier notifier) { EachTestNotifier testNotifier = new EachTestNotifier(notifier, getDescription()); + testNotifier.fireTestSuiteStarted(); try { Statement statement = classBlock(notifier); statement.evaluate(); @@ -367,6 +417,8 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, throw e; } catch (Throwable e) { testNotifier.addFailure(e); + } finally { + testNotifier.fireTestSuiteFinished(); } } @@ -375,7 +427,8 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, // public void filter(Filter filter) throws NoTestsRemainException { - synchronized (childrenLock) { + childrenLock.lock(); + try { List<T> children = new ArrayList<T>(getFilteredChildren()); for (Iterator<T> iter = children.iterator(); iter.hasNext(); ) { T each = iter.next(); @@ -389,21 +442,70 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, iter.remove(); } } - filteredChildren = Collections.unmodifiableCollection(children); + filteredChildren = Collections.unmodifiableList(children); if (filteredChildren.isEmpty()) { throw new NoTestsRemainException(); } + } finally { + childrenLock.unlock(); } } public void sort(Sorter sorter) { - synchronized (childrenLock) { + if (shouldNotReorder()) { + return; + } + + childrenLock.lock(); + try { for (T each : getFilteredChildren()) { sorter.apply(each); } List<T> sortedChildren = new ArrayList<T>(getFilteredChildren()); Collections.sort(sortedChildren, comparator(sorter)); - filteredChildren = Collections.unmodifiableCollection(sortedChildren); + filteredChildren = Collections.unmodifiableList(sortedChildren); + } finally { + childrenLock.unlock(); + } + } + + /** + * Implementation of {@link Orderable#order(Orderer)}. + * + * @since 4.13 + */ + public void order(Orderer orderer) throws InvalidOrderingException { + if (shouldNotReorder()) { + return; + } + + childrenLock.lock(); + try { + List<T> children = getFilteredChildren(); + // In theory, we could have duplicate Descriptions. De-dup them before ordering, + // and add them back at the end. + Map<Description, List<T>> childMap = new LinkedHashMap<Description, List<T>>( + children.size()); + for (T child : children) { + Description description = describeChild(child); + List<T> childrenWithDescription = childMap.get(description); + if (childrenWithDescription == null) { + childrenWithDescription = new ArrayList<T>(1); + childMap.put(description, childrenWithDescription); + } + childrenWithDescription.add(child); + orderer.apply(child); + } + + List<Description> inOrder = orderer.order(childMap.keySet()); + + children = new ArrayList<T>(children.size()); + for (Description description : inOrder) { + children.addAll(childMap.get(description)); + } + filteredChildren = Collections.unmodifiableList(children); + } finally { + childrenLock.unlock(); } } @@ -411,20 +513,29 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, // Private implementation // + private boolean shouldNotReorder() { + // If the test specifies a specific order, do not reorder. + return getDescription().getAnnotation(FixMethodOrder.class) != null; + } + private void validate() throws InitializationError { List<Throwable> errors = new ArrayList<Throwable>(); collectInitializationErrors(errors); if (!errors.isEmpty()) { - throw new InitializationError(errors); + throw new InvalidTestClassError(testClass.getJavaClass(), errors); } } - private Collection<T> getFilteredChildren() { + private List<T> getFilteredChildren() { if (filteredChildren == null) { - synchronized (childrenLock) { + childrenLock.lock(); + try { if (filteredChildren == null) { - filteredChildren = Collections.unmodifiableCollection(getChildren()); + filteredChildren = Collections.unmodifiableList( + new ArrayList<T>(getChildren())); } + } finally { + childrenLock.unlock(); } } return filteredChildren; @@ -449,4 +560,23 @@ public abstract class ParentRunner<T> extends Runner implements Filterable, public void setScheduler(RunnerScheduler scheduler) { this.scheduler = scheduler; } + + private static class ClassRuleCollector implements MemberValueConsumer<TestRule> { + final List<RuleContainer.RuleEntry> entries = new ArrayList<RuleContainer.RuleEntry>(); + + public void accept(FrameworkMember<?> member, TestRule value) { + ClassRule rule = member.getAnnotation(ClassRule.class); + entries.add(new RuleContainer.RuleEntry(value, RuleContainer.RuleEntry.TYPE_TEST_RULE, + rule != null ? rule.order() : null)); + } + + public List<TestRule> getOrderedRules() { + Collections.sort(entries, RuleContainer.ENTRY_COMPARATOR); + List<TestRule> result = new ArrayList<TestRule>(entries.size()); + for (RuleContainer.RuleEntry entry : entries) { + result.add((TestRule) entry.rule); + } + return result; + } + } } diff --git a/src/main/java/org/junit/runners/RuleContainer.java b/src/main/java/org/junit/runners/RuleContainer.java new file mode 100644 index 0000000..30ddd8d --- /dev/null +++ b/src/main/java/org/junit/runners/RuleContainer.java @@ -0,0 +1,113 @@ +package org.junit.runners; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.IdentityHashMap; +import java.util.List; + +import org.junit.Rule; +import org.junit.rules.MethodRule; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.FrameworkMethod; +import org.junit.runners.model.Statement; + +/** + * Data structure for ordering of {@link TestRule}/{@link MethodRule} instances. + * + * @since 4.13 + */ +class RuleContainer { + private final IdentityHashMap<Object, Integer> orderValues = new IdentityHashMap<Object, Integer>(); + private final List<TestRule> testRules = new ArrayList<TestRule>(); + private final List<MethodRule> methodRules = new ArrayList<MethodRule>(); + + /** + * Sets order value for the specified rule. + */ + public void setOrder(Object rule, int order) { + orderValues.put(rule, order); + } + + public void add(MethodRule methodRule) { + methodRules.add(methodRule); + } + + public void add(TestRule testRule) { + testRules.add(testRule); + } + + static final Comparator<RuleEntry> ENTRY_COMPARATOR = new Comparator<RuleEntry>() { + public int compare(RuleEntry o1, RuleEntry o2) { + int result = compareInt(o1.order, o2.order); + return result != 0 ? result : o1.type - o2.type; + } + + private int compareInt(int a, int b) { + return (a < b) ? 1 : (a == b ? 0 : -1); + } + }; + + /** + * Returns entries in the order how they should be applied, i.e. inner-to-outer. + */ + private List<RuleEntry> getSortedEntries() { + List<RuleEntry> ruleEntries = new ArrayList<RuleEntry>( + methodRules.size() + testRules.size()); + for (MethodRule rule : methodRules) { + ruleEntries.add(new RuleEntry(rule, RuleEntry.TYPE_METHOD_RULE, orderValues.get(rule))); + } + for (TestRule rule : testRules) { + ruleEntries.add(new RuleEntry(rule, RuleEntry.TYPE_TEST_RULE, orderValues.get(rule))); + } + Collections.sort(ruleEntries, ENTRY_COMPARATOR); + return ruleEntries; + } + + /** + * Applies all the rules ordered accordingly to the specified {@code statement}. + */ + public Statement apply(FrameworkMethod method, Description description, Object target, + Statement statement) { + if (methodRules.isEmpty() && testRules.isEmpty()) { + return statement; + } + Statement result = statement; + for (RuleEntry ruleEntry : getSortedEntries()) { + if (ruleEntry.type == RuleEntry.TYPE_TEST_RULE) { + result = ((TestRule) ruleEntry.rule).apply(result, description); + } else { + result = ((MethodRule) ruleEntry.rule).apply(result, method, target); + } + } + return result; + } + + /** + * Returns rule instances in the order how they should be applied, i.e. inner-to-outer. + * VisibleForTesting + */ + List<Object> getSortedRules() { + List<Object> result = new ArrayList<Object>(); + for (RuleEntry entry : getSortedEntries()) { + result.add(entry.rule); + } + return result; + } + + static class RuleEntry { + static final int TYPE_TEST_RULE = 1; + static final int TYPE_METHOD_RULE = 0; + + final Object rule; + final int type; + final int order; + + RuleEntry(Object rule, int type, Integer order) { + this.rule = rule; + this.type = type; + this.order = order != null ? order.intValue() : Rule.DEFAULT_ORDER; + } + } +} diff --git a/src/main/java/org/junit/runners/Suite.java b/src/main/java/org/junit/runners/Suite.java index b37179f..c2c8e58 100644 --- a/src/main/java/org/junit/runners/Suite.java +++ b/src/main/java/org/junit/runners/Suite.java @@ -47,7 +47,7 @@ public class Suite extends ParentRunner<Runner> { /** * @return the classes to be run */ - public Class<?>[] value(); + Class<?>[] value(); } private static Class<?>[] getAnnotatedClasses(Class<?> klass) throws InitializationError { @@ -88,7 +88,7 @@ public class Suite extends ParentRunner<Runner> { * @param suiteClasses the classes in the suite */ protected Suite(Class<?> klass, Class<?>[] suiteClasses) throws InitializationError { - this(new AllDefaultPossibilitiesBuilder(true), klass, suiteClasses); + this(new AllDefaultPossibilitiesBuilder(), klass, suiteClasses); } /** diff --git a/src/main/java/org/junit/runners/model/FrameworkField.java b/src/main/java/org/junit/runners/model/FrameworkField.java index 945e389..ea2b16f 100644 --- a/src/main/java/org/junit/runners/model/FrameworkField.java +++ b/src/main/java/org/junit/runners/model/FrameworkField.java @@ -14,12 +14,26 @@ import org.junit.runners.BlockJUnit4ClassRunner; public class FrameworkField extends FrameworkMember<FrameworkField> { private final Field field; - FrameworkField(Field field) { + /** + * Returns a new {@code FrameworkField} for {@code field}. + * + * <p>Access relaxed to {@code public} since version 4.13.1. + */ + public FrameworkField(Field field) { if (field == null) { throw new NullPointerException( "FrameworkField cannot be created without an underlying field."); } this.field = field; + + if (isPublic()) { + // This field could be a public field in a package-scope base class + try { + field.setAccessible(true); + } catch (SecurityException e) { + // We may get an IllegalAccessException when we try to access the field + } + } } @Override @@ -41,6 +55,11 @@ public class FrameworkField extends FrameworkMember<FrameworkField> { } @Override + boolean isBridgeMethod() { + return false; + } + + @Override protected int getModifiers() { return field.getModifiers(); } diff --git a/src/main/java/org/junit/runners/model/FrameworkMember.java b/src/main/java/org/junit/runners/model/FrameworkMember.java index 724f096..5634b3f 100644 --- a/src/main/java/org/junit/runners/model/FrameworkMember.java +++ b/src/main/java/org/junit/runners/model/FrameworkMember.java @@ -12,15 +12,29 @@ public abstract class FrameworkMember<T extends FrameworkMember<T>> implements Annotatable { abstract boolean isShadowedBy(T otherMember); - boolean isShadowedBy(List<T> members) { - for (T each : members) { - if (isShadowedBy(each)) { - return true; + T handlePossibleBridgeMethod(List<T> members) { + for (int i = members.size() - 1; i >=0; i--) { + T otherMember = members.get(i); + if (isShadowedBy(otherMember)) { + if (otherMember.isBridgeMethod()) { + /* + * We need to return the previously-encountered bridge method + * because JUnit won't be able to call the parent method, + * because the parent class isn't public. + */ + members.remove(i); + return otherMember; + } + // We found a shadowed member that isn't a bridge method. Ignore it. + return null; } } - return false; + // No shadow or bridge method found. The caller should add *this* member. + return (T) this; } + abstract boolean isBridgeMethod(); + protected abstract int getModifiers(); /** diff --git a/src/main/java/org/junit/runners/model/FrameworkMethod.java b/src/main/java/org/junit/runners/model/FrameworkMethod.java index 3580052..4471407 100644 --- a/src/main/java/org/junit/runners/model/FrameworkMethod.java +++ b/src/main/java/org/junit/runners/model/FrameworkMethod.java @@ -28,6 +28,15 @@ public class FrameworkMethod extends FrameworkMember<FrameworkMethod> { "FrameworkMethod cannot be created without an underlying method."); } this.method = method; + + if (isPublic()) { + // This method could be a public method in a package-scope base class + try { + method.setAccessible(true); + } catch (SecurityException e) { + // We may get an IllegalAccessException when we try to call the method + } + } } /** @@ -149,6 +158,11 @@ public class FrameworkMethod extends FrameworkMember<FrameworkMethod> { } @Override + boolean isBridgeMethod() { + return method.isBridge(); + } + + @Override public boolean equals(Object obj) { if (!FrameworkMethod.class.isInstance(obj)) { return false; diff --git a/src/main/java/org/junit/runners/model/InitializationError.java b/src/main/java/org/junit/runners/model/InitializationError.java index 841b565..dd9c8b3 100644 --- a/src/main/java/org/junit/runners/model/InitializationError.java +++ b/src/main/java/org/junit/runners/model/InitializationError.java @@ -14,7 +14,7 @@ public class InitializationError extends Exception { /* * We have to use the f prefix until the next major release to ensure * serialization compatibility. - * See https://github.com/junit-team/junit/issues/976 + * See https://github.com/junit-team/junit4/issues/976 */ private final List<Throwable> fErrors; diff --git a/src/main/java/org/junit/runners/model/InvalidTestClassError.java b/src/main/java/org/junit/runners/model/InvalidTestClassError.java new file mode 100644 index 0000000..57be610 --- /dev/null +++ b/src/main/java/org/junit/runners/model/InvalidTestClassError.java @@ -0,0 +1,39 @@ +package org.junit.runners.model; + +import java.util.List; + +/** + * Thrown by {@link org.junit.runner.Runner}s in case the class under test is not valid. + * <p> + * Its message conveniently lists all of the validation errors. + * + * @since 4.13 + */ +public class InvalidTestClassError extends InitializationError { + private static final long serialVersionUID = 1L; + + private final String message; + + public InvalidTestClassError(Class<?> offendingTestClass, List<Throwable> validationErrors) { + super(validationErrors); + this.message = createMessage(offendingTestClass, validationErrors); + } + + private static String createMessage(Class<?> testClass, List<Throwable> validationErrors) { + StringBuilder sb = new StringBuilder(); + sb.append(String.format("Invalid test class '%s':", testClass.getName())); + int i = 1; + for (Throwable error : validationErrors) { + sb.append("\n " + (i++) + ". " + error.getMessage()); + } + return sb.toString(); + } + + /** + * @return a message with a list of all of the validation errors + */ + @Override + public String getMessage() { + return message; + } +} diff --git a/src/main/java/org/junit/runners/model/MemberValueConsumer.java b/src/main/java/org/junit/runners/model/MemberValueConsumer.java new file mode 100644 index 0000000..a6157bf --- /dev/null +++ b/src/main/java/org/junit/runners/model/MemberValueConsumer.java @@ -0,0 +1,18 @@ +package org.junit.runners.model; + +/** + * Represents a receiver for values of annotated fields/methods together with the declaring member. + * + * @see TestClass#collectAnnotatedFieldValues(Object, Class, Class, MemberValueConsumer) + * @see TestClass#collectAnnotatedMethodValues(Object, Class, Class, MemberValueConsumer) + * @since 4.13 + */ +public interface MemberValueConsumer<T> { + /** + * Receives the next value and its declaring member. + * + * @param member declaring member ({@link FrameworkMethod} or {@link FrameworkField}) + * @param value the value of the next member + */ + void accept(FrameworkMember<?> member, T value); +} diff --git a/src/main/java/org/junit/runners/model/MultipleFailureException.java b/src/main/java/org/junit/runners/model/MultipleFailureException.java index 325c645..8e355a7 100644 --- a/src/main/java/org/junit/runners/model/MultipleFailureException.java +++ b/src/main/java/org/junit/runners/model/MultipleFailureException.java @@ -1,9 +1,13 @@ package org.junit.runners.model; +import java.io.PrintStream; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import org.junit.TestCouldNotBeSkippedException; +import org.junit.internal.AssumptionViolatedException; import org.junit.internal.Throwables; /** @@ -17,12 +21,22 @@ public class MultipleFailureException extends Exception { /* * We have to use the f prefix until the next major release to ensure * serialization compatibility. - * See https://github.com/junit-team/junit/issues/976 + * See https://github.com/junit-team/junit4/issues/976 */ private final List<Throwable> fErrors; public MultipleFailureException(List<Throwable> errors) { - this.fErrors = new ArrayList<Throwable>(errors); + if (errors.isEmpty()) { + throw new IllegalArgumentException( + "List of Throwables must not be empty"); + } + this.fErrors = new ArrayList<Throwable>(errors.size()); + for (Throwable error : errors) { + if (error instanceof AssumptionViolatedException) { + error = new TestCouldNotBeSkippedException((AssumptionViolatedException) error); + } + fErrors.add(error); + } } public List<Throwable> getFailures() { @@ -34,11 +48,32 @@ public class MultipleFailureException extends Exception { StringBuilder sb = new StringBuilder( String.format("There were %d errors:", fErrors.size())); for (Throwable e : fErrors) { - sb.append(String.format("\n %s(%s)", e.getClass().getName(), e.getMessage())); + sb.append(String.format("%n %s(%s)", e.getClass().getName(), e.getMessage())); } return sb.toString(); } + @Override + public void printStackTrace() { + for (Throwable e: fErrors) { + e.printStackTrace(); + } + } + + @Override + public void printStackTrace(PrintStream s) { + for (Throwable e: fErrors) { + e.printStackTrace(s); + } + } + + @Override + public void printStackTrace(PrintWriter s) { + for (Throwable e: fErrors) { + e.printStackTrace(s); + } + } + /** * Asserts that a list of throwables is empty. If it isn't empty, * will throw {@link MultipleFailureException} (if there are diff --git a/src/main/java/org/junit/runners/model/RunnerBuilder.java b/src/main/java/org/junit/runners/model/RunnerBuilder.java index 7d3eee3..ba7c9e2 100644 --- a/src/main/java/org/junit/runners/model/RunnerBuilder.java +++ b/src/main/java/org/junit/runners/model/RunnerBuilder.java @@ -6,7 +6,11 @@ import java.util.List; import java.util.Set; import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.runner.Description; +import org.junit.runner.OrderWith; import org.junit.runner.Runner; +import org.junit.runner.manipulation.InvalidOrderingException; +import org.junit.runner.manipulation.Ordering; /** * A RunnerBuilder is a strategy for constructing runners for classes. @@ -49,19 +53,39 @@ public abstract class RunnerBuilder { public abstract Runner runnerForClass(Class<?> testClass) throws Throwable; /** - * Always returns a runner, even if it is just one that prints an error instead of running tests. + * Always returns a runner for the given test class. + * + * <p>In case of an exception a runner will be returned that prints an error instead of running + * tests. + * + * <p>Note that some of the internal JUnit implementations of RunnerBuilder will return + * {@code null} from this method, but no RunnerBuilder passed to a Runner constructor will + * return {@code null} from this method. * * @param testClass class to be run * @return a Runner */ public Runner safeRunnerForClass(Class<?> testClass) { try { - return runnerForClass(testClass); + Runner runner = runnerForClass(testClass); + if (runner != null) { + configureRunner(runner); + } + return runner; } catch (Throwable e) { return new ErrorReportingRunner(testClass, e); } } + private void configureRunner(Runner runner) throws InvalidOrderingException { + Description description = runner.getDescription(); + OrderWith orderWith = description.getAnnotation(OrderWith.class); + if (orderWith != null) { + Ordering ordering = Ordering.definedBy(orderWith.value(), description); + ordering.apply(runner); + } + } + Class<?> addParent(Class<?> parent) throws InitializationError { if (!parents.add(parent)) { throw new InitializationError(String.format("class '%s' (possibly indirectly) contains itself as a SuiteClass", parent.getName())); @@ -96,7 +120,7 @@ public abstract class RunnerBuilder { } private List<Runner> runners(Class<?>[] children) { - ArrayList<Runner> runners = new ArrayList<Runner>(); + List<Runner> runners = new ArrayList<Runner>(); for (Class<?> each : children) { Runner childRunner = safeRunnerForClass(each); if (childRunner != null) { diff --git a/src/main/java/org/junit/runners/model/TestClass.java b/src/main/java/org/junit/runners/model/TestClass.java index c8a544d..5962c2b 100755..100644 --- a/src/main/java/org/junit/runners/model/TestClass.java +++ b/src/main/java/org/junit/runners/model/TestClass.java @@ -84,20 +84,21 @@ public class TestClass implements Annotatable { for (Annotation each : member.getAnnotations()) { Class<? extends Annotation> type = each.annotationType(); List<T> members = getAnnotatedMembers(map, type, true); - if (member.isShadowedBy(members)) { + T memberToAdd = member.handlePossibleBridgeMethod(members); + if (memberToAdd == null) { return; } if (runsTopToBottom(type)) { - members.add(0, member); + members.add(0, memberToAdd); } else { - members.add(member); + members.add(memberToAdd); } } } private static <T extends FrameworkMember<T>> Map<Class<? extends Annotation>, List<T>> makeDeeplyUnmodifiable(Map<Class<? extends Annotation>, List<T>> source) { - LinkedHashMap<Class<? extends Annotation>, List<T>> copy = + Map<Class<? extends Annotation>, List<T>> copy = new LinkedHashMap<Class<? extends Annotation>, List<T>>(); for (Map.Entry<Class<? extends Annotation>, List<T>> entry : source.entrySet()) { copy.put(entry.getKey(), Collections.unmodifiableList(entry.getValue())); @@ -168,7 +169,7 @@ public class TestClass implements Annotatable { } private static List<Class<?>> getSuperClasses(Class<?> testClass) { - ArrayList<Class<?>> results = new ArrayList<Class<?>>(); + List<Class<?>> results = new ArrayList<Class<?>>(); Class<?> current = testClass; while (current != null) { results.add(current); @@ -224,24 +225,59 @@ public class TestClass implements Annotatable { public <T> List<T> getAnnotatedFieldValues(Object test, Class<? extends Annotation> annotationClass, Class<T> valueClass) { - List<T> results = new ArrayList<T>(); + final List<T> results = new ArrayList<T>(); + collectAnnotatedFieldValues(test, annotationClass, valueClass, + new MemberValueConsumer<T>() { + public void accept(FrameworkMember<?> member, T value) { + results.add(value); + } + }); + return results; + } + + /** + * Finds the fields annotated with the specified annotation and having the specified type, + * retrieves the values and passes those to the specified consumer. + * + * @since 4.13 + */ + public <T> void collectAnnotatedFieldValues(Object test, + Class<? extends Annotation> annotationClass, Class<T> valueClass, + MemberValueConsumer<T> consumer) { for (FrameworkField each : getAnnotatedFields(annotationClass)) { try { Object fieldValue = each.get(test); if (valueClass.isInstance(fieldValue)) { - results.add(valueClass.cast(fieldValue)); + consumer.accept(each, valueClass.cast(fieldValue)); } } catch (IllegalAccessException e) { throw new RuntimeException( "How did getFields return a field we couldn't access?", e); } } - return results; } public <T> List<T> getAnnotatedMethodValues(Object test, Class<? extends Annotation> annotationClass, Class<T> valueClass) { - List<T> results = new ArrayList<T>(); + final List<T> results = new ArrayList<T>(); + collectAnnotatedMethodValues(test, annotationClass, valueClass, + new MemberValueConsumer<T>() { + public void accept(FrameworkMember<?> member, T value) { + results.add(value); + } + }); + return results; + } + + /** + * Finds the methods annotated with the specified annotation and returning the specified type, + * invokes it and pass the return value to the specified consumer. + * + * @since 4.13 + */ + public <T> void collectAnnotatedMethodValues(Object test, + Class<? extends Annotation> annotationClass, Class<T> valueClass, + MemberValueConsumer<T> consumer) { for (FrameworkMethod each : getAnnotatedMethods(annotationClass)) { try { /* @@ -254,14 +290,13 @@ public class TestClass implements Annotatable { */ if (valueClass.isAssignableFrom(each.getReturnType())) { Object fieldValue = each.invokeExplosively(test); - results.add(valueClass.cast(fieldValue)); + consumer.accept(each, valueClass.cast(fieldValue)); } } catch (Throwable e) { throw new RuntimeException( "Exception in " + each.getName(), e); } } - return results; } public boolean isPublic() { diff --git a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java index 1c49f84..5c70a75 100644 --- a/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java +++ b/src/main/java/org/junit/runners/parameterized/BlockJUnit4ClassRunnerWithParameters.java @@ -4,8 +4,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.List; +import org.junit.internal.runners.statements.RunAfters; +import org.junit.internal.runners.statements.RunBefores; +import org.junit.runner.RunWith; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; +import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; @@ -18,13 +22,17 @@ import org.junit.runners.model.Statement; */ public class BlockJUnit4ClassRunnerWithParameters extends BlockJUnit4ClassRunner { + private enum InjectionType { + CONSTRUCTOR, FIELD + } + private final Object[] parameters; private final String name; public BlockJUnit4ClassRunnerWithParameters(TestWithParameters test) throws InitializationError { - super(test.getTestClass().getJavaClass()); + super(test.getTestClass()); parameters = test.getParameters().toArray( new Object[test.getParameters().size()]); name = test.getName(); @@ -32,10 +40,15 @@ public class BlockJUnit4ClassRunnerWithParameters extends @Override public Object createTest() throws Exception { - if (fieldsAreAnnotated()) { - return createTestUsingFieldInjection(); - } else { - return createTestUsingConstructorInjection(); + InjectionType injectionType = getInjectionType(); + switch (injectionType) { + case CONSTRUCTOR: + return createTestUsingConstructorInjection(); + case FIELD: + return createTestUsingFieldInjection(); + default: + throw new IllegalStateException("The injection type " + + injectionType + " is not supported."); } } @@ -60,6 +73,13 @@ public class BlockJUnit4ClassRunnerWithParameters extends int index = annotation.value(); try { field.set(testClassInstance, parameters[index]); + } catch (IllegalAccessException e) { + IllegalAccessException wrappedException = new IllegalAccessException( + "Cannot set parameter '" + field.getName() + + "'. Ensure that the field '" + field.getName() + + "' is public."); + wrappedException.initCause(e); + throw wrappedException; } catch (IllegalArgumentException iare) { throw new Exception(getTestClass().getName() + ": Trying to set " + field.getName() @@ -86,7 +106,7 @@ public class BlockJUnit4ClassRunnerWithParameters extends @Override protected void validateConstructor(List<Throwable> errors) { validateOnlyOneConstructor(errors); - if (fieldsAreAnnotated()) { + if (getInjectionType() != InjectionType.CONSTRUCTOR) { validateZeroArgConstructor(errors); } } @@ -94,7 +114,7 @@ public class BlockJUnit4ClassRunnerWithParameters extends @Override protected void validateFields(List<Throwable> errors) { super.validateFields(errors); - if (fieldsAreAnnotated()) { + if (getInjectionType() == InjectionType.FIELD) { List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter(); int[] usedIndices = new int[annotatedFieldsByParameter.size()]; for (FrameworkField each : annotatedFieldsByParameter) { @@ -125,18 +145,74 @@ public class BlockJUnit4ClassRunnerWithParameters extends @Override protected Statement classBlock(RunNotifier notifier) { - return childrenInvoker(notifier); + Statement statement = childrenInvoker(notifier); + statement = withBeforeParams(statement); + statement = withAfterParams(statement); + return statement; + } + + private Statement withBeforeParams(Statement statement) { + List<FrameworkMethod> befores = getTestClass() + .getAnnotatedMethods(Parameterized.BeforeParam.class); + return befores.isEmpty() ? statement : new RunBeforeParams(statement, befores); + } + + private class RunBeforeParams extends RunBefores { + RunBeforeParams(Statement next, List<FrameworkMethod> befores) { + super(next, befores, null); + } + + @Override + protected void invokeMethod(FrameworkMethod method) throws Throwable { + int paramCount = method.getMethod().getParameterTypes().length; + method.invokeExplosively(null, paramCount == 0 ? (Object[]) null : parameters); + } + } + + private Statement withAfterParams(Statement statement) { + List<FrameworkMethod> afters = getTestClass() + .getAnnotatedMethods(Parameterized.AfterParam.class); + return afters.isEmpty() ? statement : new RunAfterParams(statement, afters); + } + + private class RunAfterParams extends RunAfters { + RunAfterParams(Statement next, List<FrameworkMethod> afters) { + super(next, afters, null); + } + + @Override + protected void invokeMethod(FrameworkMethod method) throws Throwable { + int paramCount = method.getMethod().getParameterTypes().length; + method.invokeExplosively(null, paramCount == 0 ? (Object[]) null : parameters); + } } @Override protected Annotation[] getRunnerAnnotations() { - return new Annotation[0]; + Annotation[] allAnnotations = super.getRunnerAnnotations(); + Annotation[] annotationsWithoutRunWith = new Annotation[allAnnotations.length - 1]; + int i = 0; + for (Annotation annotation: allAnnotations) { + if (!annotation.annotationType().equals(RunWith.class)) { + annotationsWithoutRunWith[i] = annotation; + ++i; + } + } + return annotationsWithoutRunWith; } private List<FrameworkField> getAnnotatedFieldsByParameter() { return getTestClass().getAnnotatedFields(Parameter.class); } + private InjectionType getInjectionType() { + if (fieldsAreAnnotated()) { + return InjectionType.FIELD; + } else { + return InjectionType.CONSTRUCTOR; + } + } + private boolean fieldsAreAnnotated() { return !getAnnotatedFieldsByParameter().isEmpty(); } diff --git a/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java b/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java index 16ea1f3..8123e83 100644 --- a/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java +++ b/src/main/java/org/junit/runners/parameterized/ParametersRunnerFactory.java @@ -4,7 +4,7 @@ import org.junit.runner.Runner; import org.junit.runners.model.InitializationError; /** - * A {@code ParameterizedRunnerFactory} creates a runner for a single + * A {@code ParametersRunnerFactory} creates a runner for a single * {@link TestWithParameters}. * * @since 4.12 diff --git a/src/main/java/org/junit/runners/parameterized/TestWithParameters.java b/src/main/java/org/junit/runners/parameterized/TestWithParameters.java index 1b86644..1c5abd9 100644 --- a/src/main/java/org/junit/runners/parameterized/TestWithParameters.java +++ b/src/main/java/org/junit/runners/parameterized/TestWithParameters.java @@ -1,6 +1,7 @@ package org.junit.runners.parameterized; import static java.util.Collections.unmodifiableList; +import static org.junit.internal.Checks.notNull; import java.util.ArrayList; import java.util.List; @@ -73,10 +74,4 @@ public class TestWithParameters { return testClass.getName() + " '" + name + "' with parameters " + parameters; } - - private static void notNull(Object value, String message) { - if (value == null) { - throw new NullPointerException(message); - } - } } |