diff --git a/src/main/java/org/junit/internal/requests/GlobalRuleRequest.java b/src/main/java/org/junit/internal/requests/GlobalRuleRequest.java new file mode 100644 index 000000000000..10aa1f0f318e --- /dev/null +++ b/src/main/java/org/junit/internal/requests/GlobalRuleRequest.java @@ -0,0 +1,37 @@ +package org.junit.internal.requests; + +import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.runner.Request; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.GlobalRuleRunner; + +/** + * A {@link Request} that adds global rules to the {@link Runner}. + */ +public final class GlobalRuleRequest extends Request { + private final Request request; + private final GlobalRuleRunner ruleRunner; + + /** + * Creates a Request with global rules + * + * @param request a {@link Request} describing your Tests + * @param ruleRunner {@link GlobalRuleRunner} to apply to the Tests described in + * request + */ + public GlobalRuleRequest(Request request, GlobalRuleRunner ruleRunner) { + this.request = request; + this.ruleRunner = ruleRunner; + } + + @Override + public Runner getRunner() { + try { + Runner runner = request.getRunner(); + ruleRunner.apply(runner); + return runner; + } catch (Exception e) { + return new ErrorReportingRunner(GlobalRuleRunner.class, e); + } + } +} \ No newline at end of file diff --git a/src/main/java/org/junit/runner/JUnitCommandLineParseResult.java b/src/main/java/org/junit/runner/JUnitCommandLineParseResult.java index 338340789ee8..2b5f26e23591 100644 --- a/src/main/java/org/junit/runner/JUnitCommandLineParseResult.java +++ b/src/main/java/org/junit/runner/JUnitCommandLineParseResult.java @@ -5,14 +5,17 @@ import java.util.List; import org.junit.internal.Classes; +import org.junit.rules.TestRule; import org.junit.runner.FilterFactory.FilterNotCreatedException; import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.GlobalRuleRunner; import org.junit.runners.model.InitializationError; class JUnitCommandLineParseResult { private final List filterSpecs = new ArrayList(); private final List> classes = new ArrayList>(); private final List parserErrors = new ArrayList(); + private final List> globalRules = new ArrayList>(); /** * Do not use. Testing purposes only. @@ -33,6 +36,13 @@ public List> getClasses() { return Collections.unmodifiableList(classes); } + /** + * Returns global rules classes parsed from command line. + */ + public List> getGlobalRules() { + return Collections.unmodifiableList(globalRules); + } + /** * Parses the arguments. * @@ -73,6 +83,27 @@ String[] parseOptions(String... args) { } filterSpecs.add(filterSpec); + } else if (arg.equals("--global-rule")) { + ++i; + + if (i < args.length) { + try { + Class clazz = Classes.getClass(args[i]); + + if (TestRule.class.isAssignableFrom(clazz)) { + globalRules.add(clazz); + } else { + parserErrors.add(new CommandLineParserError(args[i] + " does not implement TestRule")); + break; + } + } catch (ClassNotFoundException e) { + parserErrors.add(new CommandLineParserError(args[i] + " is not a valid value for global rule")); + break; + } + } else { + parserErrors.add(new CommandLineParserError(arg + " value not specified")); + break; + } } else { parserErrors.add(new CommandLineParserError("JUnit knows nothing about the " + arg + " option")); } @@ -115,12 +146,20 @@ public Request createRequest(Computer computer) { if (parserErrors.isEmpty()) { Request request = Request.classes( computer, classes.toArray(new Class[classes.size()])); - return applyFilterSpecs(request); + return applyFilterSpecs(applyGlobalRules(request)); } else { return errorReport(new InitializationError(parserErrors)); } } + private Request applyGlobalRules(Request request) { + if (!globalRules.isEmpty()) { + return request.withGlobalRules(new GlobalRuleRunner(globalRules)); + } else { + return request; + } + } + private Request applyFilterSpecs(Request request) { try { for (String filterSpec : filterSpecs) { diff --git a/src/main/java/org/junit/runner/Request.java b/src/main/java/org/junit/runner/Request.java index 264489217f74..474e8a814fb3 100644 --- a/src/main/java/org/junit/runner/Request.java +++ b/src/main/java/org/junit/runner/Request.java @@ -5,9 +5,12 @@ import org.junit.internal.builders.AllDefaultPossibilitiesBuilder; import org.junit.internal.requests.ClassRequest; import org.junit.internal.requests.FilterRequest; +import org.junit.internal.requests.GlobalRuleRequest; import org.junit.internal.requests.SortingRequest; import org.junit.internal.runners.ErrorReportingRunner; +import org.junit.rules.TestRule; import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.GlobalRuleRunner; import org.junit.runners.model.InitializationError; /** @@ -169,4 +172,15 @@ public Request filterWith(Description desiredDescription) { public Request sortWith(Comparator comparator) { return new SortingRequest(this, comparator); } + + /** + * Returns a Request with global {@link TestRule}s that are applied + * to every test being run with this Request + * + * @param ruleRunner The {@link GlobalRuleRunner} to apply to this Request + * @return a Request with applied global {@link TestRule}s + */ + public Request withGlobalRules(GlobalRuleRunner ruleRunner) { + return new GlobalRuleRequest(this, ruleRunner); + } } diff --git a/src/main/java/org/junit/runner/manipulation/GlobalRuleRunnable.java b/src/main/java/org/junit/runner/manipulation/GlobalRuleRunnable.java new file mode 100644 index 000000000000..c32219d6f3d3 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/GlobalRuleRunnable.java @@ -0,0 +1,20 @@ +package org.junit.runner.manipulation; + +/** + * Runners that allow global {@link org.junit.rules.TestRule} should implement this interface. + * Implement {@link #setGlobalRules(GlobalRuleRunner)}} to receive the list of classes assignable to + * {@link org.junit.rules.TestRule} to apply on every test. + * + * @since 4.13 + */ +public interface GlobalRuleRunnable { + + /** + * Instantiate and apply global {@link org.junit.rules.TestRule}s to every test. + * + * @param ruleRunner the {@link GlobalRuleRunner} to apply + * @throws Exception if any class does not meet {@link org.junit.runner.Runner}'s requirements + */ + void setGlobalRules(GlobalRuleRunner ruleRunner) throws Exception; + +} diff --git a/src/main/java/org/junit/runner/manipulation/GlobalRuleRunner.java b/src/main/java/org/junit/runner/manipulation/GlobalRuleRunner.java new file mode 100644 index 000000000000..d833c7ca72a3 --- /dev/null +++ b/src/main/java/org/junit/runner/manipulation/GlobalRuleRunner.java @@ -0,0 +1,33 @@ +package org.junit.runner.manipulation; + +import java.util.List; + +/** + * A GlobalRuleRunner passes global {@link org.junit.rules.TestRule}s to the {@link org.junit.runner.Runner}. + * In general you will not need to use a GlobalRuleRunner directly. Instead, + * use {@link org.junit.runner.Request#withGlobalRules(GlobalRuleRunner)}. + * + * @since 4.13 + */ +public class GlobalRuleRunner { + + private final List> rules; + + public GlobalRuleRunner(List> rules) { + this.rules = rules; + } + + /** + * Applies provided global {@link org.junit.rules.TestRule}s to the runner + */ + public void apply(Object object) throws Exception { + if (object instanceof GlobalRuleRunnable) { + GlobalRuleRunnable runnable = (GlobalRuleRunnable) object; + runnable.setGlobalRules(this); + } + } + + public List> getRules() { + return rules; + } +} diff --git a/src/main/java/org/junit/runners/ParentRunner.java b/src/main/java/org/junit/runners/ParentRunner.java index 4949c242e5b2..b77b928c6315 100644 --- a/src/main/java/org/junit/runners/ParentRunner.java +++ b/src/main/java/org/junit/runners/ParentRunner.java @@ -5,6 +5,7 @@ import static org.junit.internal.runners.rules.RuleMemberValidator.CLASS_RULE_VALIDATOR; import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -16,11 +17,7 @@ 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.Ignore; -import org.junit.Rule; +import org.junit.*; import org.junit.internal.AssumptionViolatedException; import org.junit.internal.runners.model.EachTestNotifier; import org.junit.internal.runners.statements.RunAfters; @@ -29,11 +26,7 @@ import org.junit.rules.TestRule; 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.NoTestsRemainException; -import org.junit.runner.manipulation.Sortable; -import org.junit.runner.manipulation.Sorter; +import org.junit.runner.manipulation.*; import org.junit.runner.notification.RunNotifier; import org.junit.runner.notification.StoppedByUserException; import org.junit.runners.model.FrameworkMember; @@ -55,13 +48,13 @@ * must implement finding the children of the node, describing each child, and * running each child. ParentRunner will filter and sort children, handle * {@code @BeforeClass} and {@code @AfterClass} methods, - * handle annotated {@link ClassRule}s, create a composite - * {@link Description}, and run children sequentially. + * handle annotated {@link ClassRule}s, handle provided global {@link TestRule}s, + * create a composite {@link Description}, and run children sequentially. * * @since 4.5 */ public abstract class ParentRunner extends Runner implements Filterable, - Sortable { + Sortable, GlobalRuleRunnable { private static final List VALIDATORS = Arrays.asList( new AnnotationsValidator()); @@ -71,6 +64,8 @@ public abstract class ParentRunner extends Runner implements Filterable, // Guarded by childrenLock private volatile Collection filteredChildren = null; + private List globalRules = new ArrayList(); + private volatile RunnerScheduler scheduler = new RunnerScheduler() { public void schedule(Runnable childStatement) { childStatement.run(); @@ -192,6 +187,7 @@ private void validateClassRules(List errors) { * construct a statement that will: *
    *
  1. Apply all {@code ClassRule}s on the test-class and superclasses.
  2. + *
  3. Apply all global {@code TestRule}s on the test-class and superclasses.
  4. *
  5. Run all non-overridden {@code @BeforeClass} methods on the test-class * and superclasses; if any throws an Exception, stop execution and pass the * exception on.
  6. @@ -212,6 +208,7 @@ protected Statement classBlock(final RunNotifier notifier) { statement = withBeforeClasses(statement); statement = withAfterClasses(statement); statement = withClassRules(statement); + statement = withGlobalRules(statement); } return statement; } @@ -266,6 +263,20 @@ private Statement withClassRules(Statement statement) { new RunRules(statement, classRules, getDescription()); } + /** + * Returns a {@link Statement}: apply all + * classes assignable to {@link TestRule} provided + * by command line argument {@code --global-rule}. + * + * @param statement the base statement + * @return a RunRules statement if any global-level {@link TestRule}s are + * provided, or the base statement + */ + private Statement withGlobalRules(Statement statement) { + return globalRules.isEmpty() ? statement : + new RunRules(statement, globalRules, getDescription()); + } + /** * @return the {@code ClassRule}s that can transform the block that runs * each method in the tested class. @@ -403,7 +414,7 @@ public void run(final RunNotifier notifier) { } // - // Implementation of Filterable and Sortable + // Implementation of Filterable, Sortable and GlobalRuleRunnable // public void filter(Filter filter) throws NoTestsRemainException { @@ -445,6 +456,29 @@ public void sort(Sorter sorter) { } } + public void setGlobalRules(GlobalRuleRunner rules) throws Exception { + childrenLock.lock(); + try { + for (T each : getFilteredChildren()) { + rules.apply(each); + } + for (Class clazz : rules.getRules()) { + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length > 1) { + throw new IllegalArgumentException("Global TestRule can only have one constructor"); + } + Assert.assertEquals(1, constructors.length); + Constructor constructor = clazz.getConstructors()[0]; + if (constructor.getParameterTypes().length > 0) { + throw new IllegalArgumentException("Global TestRule constructor cannot have parameters"); + } + globalRules.add(TestRule.class.cast(constructor.newInstance())); + } + } finally { + childrenLock.unlock(); + } + } + // // Private implementation // diff --git a/src/test/java/org/junit/rules/AllRulesTests.java b/src/test/java/org/junit/rules/AllRulesTests.java index 8b63e1510db3..efe356fd4195 100644 --- a/src/test/java/org/junit/rules/AllRulesTests.java +++ b/src/test/java/org/junit/rules/AllRulesTests.java @@ -8,6 +8,7 @@ @SuiteClasses({ BlockJUnit4ClassRunnerOverrideTest.class, ClassRulesTest.class, + GlobalRulesTest.class, DisableOnDebugTest.class, ErrorCollectorTest.class, ExpectedExceptionTest.class, diff --git a/src/test/java/org/junit/rules/GlobalRulesTest.java b/src/test/java/org/junit/rules/GlobalRulesTest.java new file mode 100644 index 000000000000..46de1bc9a131 --- /dev/null +++ b/src/test/java/org/junit/rules/GlobalRulesTest.java @@ -0,0 +1,60 @@ +package org.junit.rules; + +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.JUnitCore; +import org.junit.runner.Request; +import org.junit.runner.manipulation.GlobalRuleRunner; +import org.junit.runners.model.Statement; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * Tests to exercise global-level rules. + */ +public class GlobalRulesTest { + private static int count = 0; + + public static class Counter implements TestRule { + public Statement apply(Statement base, Description description) { + count++; + return base; + } + } + + public static class ExampleTest { + @Test + public void firstTest() { + assertEquals(2, count); + } + + @Test + public void secondTest() { + assertEquals(2, count); + } + } + + public static class SecondExampleTest { + @Test + public void firstTest() { + assertEquals(3, count); + } + + @Test + public void secondTest() { + assertEquals(3, count); + } + } + + @Test + public void rulesAreAppliedOnEveryTest() { + List> rules = new ArrayList>(); + rules.add(Counter.class); + new JUnitCore().run(Request.classes(ExampleTest.class, SecondExampleTest.class) + .withGlobalRules(new GlobalRuleRunner(rules))); + assertEquals(3, count); + } +} diff --git a/src/test/java/org/junit/runner/JUnitCommandLineParseResultTest.java b/src/test/java/org/junit/runner/JUnitCommandLineParseResultTest.java index ffd1f1c9552f..e76d532b3770 100644 --- a/src/test/java/org/junit/runner/JUnitCommandLineParseResultTest.java +++ b/src/test/java/org/junit/runner/JUnitCommandLineParseResultTest.java @@ -11,7 +11,9 @@ import org.junit.Test; import org.junit.experimental.categories.IncludeCategories; import org.junit.rules.ExpectedException; +import org.junit.rules.TestRule; import org.junit.runner.manipulation.Filter; +import org.junit.runners.model.Statement; public class JUnitCommandLineParseResultTest { @Rule @@ -47,6 +49,16 @@ public void shouldCreateFailureUponBaldFilterOptionNotFollowedByValue() { assertThat(description.toString(), containsString("initializationError")); } + @Test + public void shouldCreateFailureUponBaldGlobalRuleOptionNotFollowedByValue() { + jUnitCommandLineParseResult.parseOptions("--global-rule"); + + Runner runner = jUnitCommandLineParseResult.createRequest(new Computer()).getRunner(); + Description description = runner.getDescription().getChildren().get(0); + + assertThat(description.toString(), containsString("initializationError")); + } + @Test public void shouldParseFilterArgInWhichValueIsASeparateArg() throws Exception { String value= IncludeCategories.class.getName() + "=" + DummyCategory0.class.getName(); @@ -116,6 +128,19 @@ public void shouldAddToClasses() { assertThat(testClass.getName(), is(DummyTest.class.getName())); } + @Test + public void shouldAddToGlobalRules() { + jUnitCommandLineParseResult.parseOptions(new String[]{ + "--global-rule", + DummyRule.class.getName() + }); + + List> classes = jUnitCommandLineParseResult.getGlobalRules(); + Class ruleClass = classes.get(0); + + assertThat(ruleClass.getName(), is(DummyRule.class.getName())); + } + @Test public void shouldCreateFailureUponUnknownTestClass() throws Exception { String unknownTestClass = "UnknownTestClass"; @@ -129,6 +154,33 @@ public void shouldCreateFailureUponUnknownTestClass() throws Exception { assertThat(description.toString(), containsString("initializationError")); } + @Test + public void shouldCreateFailureUponUnknownGlobalRule() throws Exception { + String unknownRuleClass = "UnknownRuleClass"; + jUnitCommandLineParseResult.parseOptions(new String[]{ + "--global-rule", + unknownRuleClass + }); + + Runner runner = jUnitCommandLineParseResult.createRequest(new Computer()).getRunner(); + Description description = runner.getDescription().getChildren().get(0); + + assertThat(description.toString(), containsString("initializationError")); + } + + @Test + public void shouldCreateFailureUponNonAssignableToTestRuleGlobalRule() throws Exception { + jUnitCommandLineParseResult.parseOptions(new String[]{ + "--global-rule", + DummyTest.class.getName() + }); + + Runner runner = jUnitCommandLineParseResult.createRequest(new Computer()).getRunner(); + Description description = runner.getDescription().getChildren().get(0); + + assertThat(description.toString(), containsString("initializationError")); + } + public static class FilterFactoryStub implements FilterFactory { public Filter createFilter(FilterFactoryParams params) throws FilterNotCreatedException { throw new FilterNotCreatedException(new Exception("stub")); @@ -143,4 +195,10 @@ public static class DummyTest { public void dummyTest() { } } + + public static class DummyRule implements TestRule { + public Statement apply(Statement base, Description description) { + return base; + } + } }