/* * Copyright 2007-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://www.apache.org/licenses/LICENSE-2.0 * * or in the "license" file accompanying this file. This file 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.amazon.ion.junit; import java.beans.BeanInfo; import java.beans.Introspector; import java.beans.PropertyDescriptor; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Parameterized; import org.junit.runners.Suite; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; /** * A JUnit 4 {@link Runner} that injects one or more JavaBeans properties of * the test fixture with a set of configured values. This approach is similar * to {@link Parameterized} but utilizes setter injection instead of using * constructors, so its easier to reuse the injection via inheritance. */ public class Injected extends Suite { /** * Annotation for a public static field which provides values to be * injected into the fixture. The {@code value} element of this annotation * must be the name of a writable property of the fixture. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public static @interface Inject { /** * The name of the property to inject. */ String value(); } private static final class Dimension { String property; PropertyDescriptor descriptor; Object[] values; } private static class InjectedRunner extends BlockJUnit4ClassRunner { private final Dimension[] myDimensions; private final int[] myValueIndices; private String myName; /** * @param klass * @throws InitializationError */ public InjectedRunner(Class<?> klass, Dimension[] dimensions, int... indices) throws InitializationError { super(klass); assert dimensions.length == indices.length; myDimensions = dimensions; myValueIndices = indices; } @Override public Object createTest() throws Exception { Object test = getTestClass().getOnlyConstructor().newInstance(); for (int i = 0; i < myDimensions.length; i++) { inject(test, myDimensions[i], myValueIndices[i]); } return test; } public void inject(Object target, Dimension dimension, int valueIndex) throws Exception { Method method = dimension.descriptor.getWriteMethod(); Object value = dimension.values[valueIndex]; method.invoke(target, value); } @Override protected synchronized String getName() { if (myName == null) { StringBuilder buf = new StringBuilder("["); for (int i = 0; i < myDimensions.length; i++) { if (i != 0) buf.append(','); Dimension dim = myDimensions[i]; int valueIndex = myValueIndices[i]; buf.append(dim.values[valueIndex]); } buf.append(']'); myName = buf.toString(); } return myName; } @Override protected String testName(FrameworkMethod method) { // Eclipse (Helios) can't display results properly if the names // are not unique. return method.getName() + getName(); } @Override protected void validateConstructor(List<Throwable> errors) { validateOnlyOneConstructor(errors); } @Override protected Statement classBlock(RunNotifier notifier) { return childrenInvoker(notifier); } } /** * Only called reflectively. Do not use programmatically. */ public Injected(Class<?> klass) throws Throwable { super(klass, fanout(klass)); } private static List<Runner> fanout(Class<?> klass) throws Throwable { Dimension[] dimensions = findDimensions(new TestClass(klass)); assert dimensions.length != 0; return fanout(klass, new ArrayList<Runner>(), dimensions, new int[dimensions.length], 0); } private static List<Runner> fanout(Class<?> klass, List<Runner> runners, Dimension[] dimensions, int[] valueIndices, int dimensionIndex) throws InitializationError { assert dimensions.length == valueIndices.length; if (dimensionIndex == dimensions.length) { InjectedRunner runner = new InjectedRunner(klass, dimensions, valueIndices); runners.add(runner); } else { Dimension dim = dimensions[dimensionIndex]; int width = dim.values.length; for (int i = 0; i < width; i++) { int[] childIndexes = valueIndices.clone(); childIndexes[dimensionIndex] = i; fanout(klass, runners, dimensions, childIndexes, dimensionIndex + 1); } } return runners; } private static Dimension[] findDimensions(TestClass testClass) throws Throwable { List<FrameworkField> fields = testClass.getAnnotatedFields(Inject.class); if (fields.isEmpty()) { throw new Exception("No fields of " + testClass.getName() + " have the @Inject annotation"); } BeanInfo beanInfo = Introspector.getBeanInfo(testClass.getJavaClass()); PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); Dimension[] dimensions = new Dimension[fields.size()]; int i = 0; for (FrameworkField field : fields) { int modifiers = field.getField().getModifiers(); if (! Modifier.isPublic(modifiers) || ! Modifier.isStatic(modifiers)) { throw new Exception("@Inject " + testClass.getName() + '.' + field.getField().getName() + " must be public static"); } Dimension dim = new Dimension(); dim.property = field.getField().getAnnotation(Inject.class).value(); dim.descriptor = findDescriptor(testClass, descriptors, field, dim.property); dim.values = (Object[]) field.get(null); dimensions[i++] = dim; } return dimensions; } private static PropertyDescriptor findDescriptor(TestClass testClass, PropertyDescriptor[] descriptors, FrameworkField field, String name) throws Exception { for (PropertyDescriptor d : descriptors) { if (d.getName().equals(name)) { if (d.getWriteMethod() == null) break; // To throw error return d; } } throw new Exception("@Inject value '" + name + "' doesn't match a writeable property near " + testClass.getName() + '.' + field.getField().getName()); } }