1/*2 * Copyright 2012 the original author or authors.3 *4 * Licensed under the Apache License, Version 2.0 (the "License");5 * you may not use this file except in compliance with the License.6 * You may obtain a copy of the License at7 *8 * http://www.apache.org/licenses/LICENSE-2.09 *10 * Unless required by applicable law or agreed to in writing, software11 * distributed under the License is distributed on an "AS IS" BASIS,12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.13 * See the License for the specific language governing permissions and14 * limitations under the License.15 */16package org.gradle.integtests.fixtures;17import org.gradle.api.Nullable;18import org.gradle.internal.UncheckedException;19import org.junit.runner.Description;20import org.junit.runner.RunWith;21import org.junit.runner.Runner;22import org.junit.runner.manipulation.Filter;23import org.junit.runner.manipulation.Filterable;24import org.junit.runner.manipulation.NoTestsRemainException;25import org.junit.runner.notification.Failure;26import org.junit.runner.notification.RunListener;27import org.junit.runner.notification.RunNotifier;28import org.junit.runners.BlockJUnit4ClassRunner;29import org.junit.runners.Suite;30import org.junit.runners.model.InitializationError;31import org.junit.runners.model.RunnerBuilder;32import java.lang.annotation.Annotation;33import java.lang.reflect.InvocationTargetException;34import java.util.*;35/**36 * A base class for those test runners which execute a test multiple times.37 */38public abstract class AbstractMultiTestRunner extends Runner implements Filterable {39 protected final Class<?> target;40 private final List<Execution> executions = new ArrayList<Execution>();41 private Description description;42 private Description templateDescription;43 protected AbstractMultiTestRunner(Class<?> target) {44 this.target = target;45 }46 @Override47 public Description getDescription() {48 initDescription();49 return description;50 }51 @Override52 public void run(RunNotifier notifier) {53 initDescription();54 for (Execution execution : executions) {55 execution.run(notifier);56 }57 }58 public void filter(Filter filter) throws NoTestsRemainException {59 initExecutions();60 for (Execution execution : executions) {61 execution.filter(filter);62 }63 invalidateDescription();64 }65 private void initExecutions() {66 if (executions.isEmpty()) {67 try {68 Runner descriptionProvider = createRunnerFor(Arrays.asList(target), Collections.<Filter>emptyList());69 templateDescription = descriptionProvider.getDescription();70 } catch (InitializationError initializationError) {71 throw UncheckedException.throwAsUncheckedException(initializationError);72 }73 createExecutions();74 for (Execution execution : executions) {75 execution.init(target, templateDescription);76 }77 }78 }79 private void initDescription() {80 initExecutions();81 if (description == null) {82 description = Description.createSuiteDescription(target);83 for (Execution execution : executions) {84 execution.addDescriptions(description);85 }86 }87 }88 private void invalidateDescription() {89 description = null;90 templateDescription = null;91 }92 protected abstract void createExecutions();93 protected void add(Execution execution) {94 executions.add(execution);95 }96 private static Runner createRunnerFor(List<? extends Class<?>> targetClasses, final List<Filter> filters) throws InitializationError {97 RunnerBuilder runnerBuilder = new RunnerBuilder() {98 @Override99 public Runner runnerForClass(Class<?> testClass) throws Throwable {100 for (Class<?> candidate = testClass; candidate != null; candidate = candidate.getSuperclass()) {101 RunWith runWith = candidate.getAnnotation(RunWith.class);102 if (runWith != null && !AbstractMultiTestRunner.class.isAssignableFrom(runWith.value())) {103 try {104 Runner r = (Runner) runWith.value().getConstructors()[0].newInstance(testClass);105 return filter(r);106 } catch (InvocationTargetException e) {107 throw e.getTargetException();108 }109 }110 }111 return filter(new BlockJUnit4ClassRunner(testClass));112 }113 //we need to filter at the level child runners because the suite is not doing the right thing here114 private Runner filter(Runner r) {115 for (Filter filter : filters) {116 try {117 ((Filterable)r).filter(filter);118 } catch (NoTestsRemainException e) {119 //ignore120 }121 }122 return r;123 }124 };125 return new Suite(runnerBuilder, targetClasses.toArray(new Class<?>[targetClasses.size()]));126 }127 protected static abstract class Execution implements Filterable {128 protected Class<?> target;129 private Description templateDescription;130 private final Map<Description, Description> descriptionTranslations = new HashMap<Description, Description>();131 private final Set<Description> enabledTests = new LinkedHashSet<Description>();132 private final Set<Description> disabledTests = new LinkedHashSet<Description>();133 private final List<Filter> filters = new LinkedList<Filter>();134 final void init(Class<?> target, Description templateDescription) {135 this.target = target;136 this.templateDescription = templateDescription;137 }138 private Runner createExecutionRunner() throws InitializationError {139 List<? extends Class<?>> targetClasses = loadTargetClasses();140 return createRunnerFor(targetClasses, filters);141 }142 final void addDescriptions(Description parent) {143 map(templateDescription, parent);144 }145 final void run(final RunNotifier notifier) {146 RunNotifier nested = new RunNotifier();147 NestedRunListener nestedListener = new NestedRunListener(notifier);148 nested.addListener(nestedListener);149 try {150 runEnabledTests(nested);151 } finally {152 nestedListener.cleanup();153 }154 for (Description disabledTest : disabledTests) {155 nested.fireTestStarted(disabledTest);156 nested.fireTestIgnored(disabledTest);157 }158 }159 private void runEnabledTests(RunNotifier nested) {160 if (enabledTests.isEmpty()) {161 return;162 }163 Runner runner;164 try {165 runner = createExecutionRunner();166 } catch (Throwable t) {167 runner = new CannotExecuteRunner(getDisplayName(), target, t);168 }169 try {170 if (!disabledTests.isEmpty()) {171 ((Filterable) runner).filter(new Filter() {172 @Override173 public boolean shouldRun(Description description) {174 return !disabledTests.contains(description);175 }176 @Override177 public String describe() {178 return "disabled tests";179 }180 });181 }182 } catch (NoTestsRemainException e) {183 return;184 }185 runner.run(nested);186 }187 private Description translateDescription(Description description) {188 return descriptionTranslations.containsKey(description) ? descriptionTranslations.get(description) : description;189 }190 public void filter(Filter filter) throws NoTestsRemainException {191 filters.add(filter);192 for (Map.Entry<Description, Description> entry : descriptionTranslations.entrySet()) {193 if (!filter.shouldRun(entry.getKey())) {194 enabledTests.remove(entry.getValue());195 disabledTests.remove(entry.getValue());196 }197 }198 }199 protected void before() {200 }201 protected void after() {202 }203 private void map(Description source, Description parent) {204 for (Description child : source.getChildren()) {205 Description mappedChild;206 if (child.getMethodName() != null) {207 mappedChild = Description.createSuiteDescription(String.format("%s [%s](%s)", child.getMethodName(), getDisplayName(), child.getClassName()));208 parent.addChild(mappedChild);209 if (!isTestEnabled(new TestDescriptionBackedTestDetails(source, child))) {210 disabledTests.add(child);211 } else {212 enabledTests.add(child);213 }214 } else {215 mappedChild = Description.createSuiteDescription(child.getClassName());216 }217 descriptionTranslations.put(child, mappedChild);218 map(child, parent);219 }220 }221 /**222 * Returns a display name for this execution. Used in the JUnit descriptions for test execution.223 */224 protected abstract String getDisplayName();225 /**226 * Returns true if the given test should be executed, false if it should be ignored. Default is true.227 */228 protected boolean isTestEnabled(TestDetails testDetails) {229 return true;230 }231 /**232 * Checks that this execution can be executed, throwing an exception if not.233 */234 protected void assertCanExecute() {235 }236 /**237 * Loads the target classes for this execution. Default is the target class that this runner was constructed with.238 */239 protected List<? extends Class<?>> loadTargetClasses() {240 return Collections.singletonList(target);241 }242 private static class CannotExecuteRunner extends Runner {243 private final Description description;244 private final Throwable failure;245 public CannotExecuteRunner(String displayName, Class<?> testClass, Throwable failure) {246 description = Description.createSuiteDescription(String.format("%s(%s)", displayName, testClass.getName()));247 this.failure = failure;248 }249 @Override250 public Description getDescription() {251 return description;252 }253 @Override254 public void run(RunNotifier notifier) {255 Description description = getDescription();256 notifier.fireTestStarted(description);257 notifier.fireTestFailure(new Failure(description, failure));258 notifier.fireTestFinished(description);259 }260 }261 private class NestedRunListener extends RunListener {262 private final RunNotifier notifier;263 boolean started;264 boolean complete;265 public NestedRunListener(RunNotifier notifier) {266 this.notifier = notifier;267 }268 @Override269 public void testStarted(Description description) {270 Description translated = translateDescription(description);271 notifier.fireTestStarted(translated);272 if (!started && !complete) {273 try {274 assertCanExecute();275 started = true;276 before();277 } catch (Throwable t) {278 notifier.fireTestFailure(new Failure(translated, t));279 }280 }281 }282 @Override283 public void testFailure(Failure failure) {284 Description translated = translateDescription(failure.getDescription());285 notifier.fireTestFailure(new Failure(translated, failure.getException()));286 }287 @Override288 public void testAssumptionFailure(Failure failure) {289 Description translated = translateDescription(failure.getDescription());290 notifier.fireTestAssumptionFailed(new Failure(translated, failure.getException()));291 }292 @Override293 public void testIgnored(Description description) {294 Description translated = translateDescription(description);295 notifier.fireTestIgnored(translated);296 }297 @Override298 public void testFinished(Description description) {299 Description translated = translateDescription(description);300 notifier.fireTestFinished(translated);301 }302 public void cleanup() {303 if (started) {304 after();305 }306 // Prevent further tests (ignored) from triggering start actions307 complete = true;308 }309 }310 }311 public interface TestDetails {312 /**313 * Locates the given annotation for the test. May be inherited from test class.314 */315 @Nullable316 <A extends Annotation> A getAnnotation(Class<A> type);317 }318 private static class TestDescriptionBackedTestDetails implements TestDetails {319 private final Description parent;320 private final Description test;321 private TestDescriptionBackedTestDetails(Description parent, Description test) {322 this.parent = parent;323 this.test = test;324 }325 @Override326 public String toString() {327 return test.toString();328 }329 public <A extends Annotation> A getAnnotation(Class<A> type) {330 A annotation = test.getAnnotation(type);331 if (annotation != null) {332 return annotation;333 }334 return parent.getAnnotation(type);335 }336 }337}...