/** * Copyright 2015 Palantir Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.palantir.giraffe.test.runner; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.junit.rules.TestRule; import org.junit.runner.Runner; import org.junit.runner.manipulation.Filter; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Suite; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; /** * Test runner for file system implementation tests. * <p> * Test classes or suites run with this runner must have a * {@link SystemSuite.SystemRule} annotation and define a {@code ClassRule} of * the type specified by the annotation. All test classes run by this runner * must have a single-argument constructor that accepts objects of the type * given by the {@code @SystemRule} annotation. Use the {@code @SuiteClasses} * annotation to specify the classes to run as part of a suite. * <p> * Test classes or suites may also implement the {@link SystemSuite.Filterable} * interface to provide a filter that removes specific tests. * * @author bkeyes */ public class SystemSuite extends Suite { private static final List<Runner> NO_RUNNERS = Collections.emptyList(); private static Class<?>[] getSuiteClasses(Class<?> klass) throws InitializationError { SuiteClasses classes = klass.getAnnotation(SuiteClasses.class); if (classes == null) { return new Class<?>[] { klass }; } else { return classes.value(); } } /** * Specified the type of {@code @ClassRule} used by this runner. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface SystemRule { Class<? extends SystemTestRule> value(); } /** * Implementations provide a filter which is used by the runner to keep or * remove specific tests. */ public interface Filterable { Filter getFilter(); } private final Class<? extends SystemTestRule> ruleClass; private final List<Runner> runners = new ArrayList<>(); public SystemSuite(Class<?> klass) throws InitializationError { super(klass, NO_RUNNERS); SystemRule ruleAnnotation = klass.getAnnotation(SystemRule.class); if (ruleAnnotation != null) { ruleClass = ruleAnnotation.value(); } else { throw new InitializationError(String.format( "class '%s' must have a @SystemRule annotation", klass.getName())); } createRunners(getSuiteClasses(klass), getRule()); if (Filterable.class.isAssignableFrom(klass)) { try { Object suite = getTestClass().getOnlyConstructor().newInstance(); filter(((Filterable) suite).getFilter()); } catch (Exception e) { throw new InitializationError(e); } } } @Override protected List<Runner> getChildren() { return runners; } private SystemTestRule getRule() throws InitializationError { SystemTestRule rule = null; for (TestRule candidate : classRules()) { if (ruleClass.isInstance(candidate)) { if (rule != null) { throw ruleError(); } rule = (SystemTestRule) candidate; } } if (rule == null) { throw ruleError(); } return rule; } private InitializationError ruleError() throws InitializationError { String testName = getTestClass().getName(); throw new InitializationError(String.format( "class '%s' must have a single @ClassRule of type '%s'", testName, ruleClass.getName())); } private void createRunners(Class<?>[] classes, SystemTestRule fsRule) throws InitializationError { for (Class<?> test : classes) { String name = "[" + fsRule.name() + "]"; runners.add(new TestClassRunnerForSystem(test, fsRule, name)); } } private class TestClassRunnerForSystem extends BlockJUnit4ClassRunner { private final SystemTestRule rule; private final String name; TestClassRunnerForSystem(Class<?> type, SystemTestRule rule, String name) throws InitializationError { super(type); this.rule = rule; this.name = name; if (Filterable.class.isAssignableFrom(type)) { try { filter(((Filterable) createTest()).getFilter()); } catch (Exception e) { throw new InitializationError(e); } } } @Override protected Object createTest() throws Exception { return getTestClass().getOnlyConstructor().newInstance(rule); } @Override protected String testName(FrameworkMethod method) { return method.getName() + name; } @Override protected void validateConstructor(List<Throwable> errors) { validateOnlyOneConstructor(errors); validateConstructorArgType(errors); } private void validateConstructorArgType(List<Throwable> errors) { Class<?>[] params = getTestClass().getOnlyConstructor().getParameterTypes(); if (params.length != 1 || !params[0].isAssignableFrom(ruleClass)) { String gripe = String.format( "Test class constructor must take exactly one argument " + "assignable from '%s'", ruleClass.getName()); errors.add(new Exception(gripe)); } } } }