1 /* 2 * Copyright (C) 2023 The Android Open Source Project 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 at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * 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 and 14 * limitations under the License. 15 */ 16 17 package platform.test.runner.parameterized; 18 19 import static platform.test.runner.parameterized.ParameterizedAndroidJunit4.isRunningOnAndroid; 20 21 import org.junit.Assert; 22 import org.junit.runners.model.FrameworkField; 23 import org.junit.runners.model.FrameworkMethod; 24 import org.junit.runners.model.TestClass; 25 26 import java.lang.annotation.Annotation; 27 import java.lang.reflect.Constructor; 28 import java.lang.reflect.Field; 29 import java.lang.reflect.Modifier; 30 import java.util.HashSet; 31 import java.util.List; 32 import java.util.Locale; 33 34 /** 35 * Encapsulates reflection operations needed to instantiate and parameterize a test instance for 36 * tests run with {@link ParameterizedAndroidJunit4}. This logic is independent of the platform/ 37 * environment on which the test is running, so it's shared by all of the runners that {@link 38 * ParameterizedAndroidJunit4} delegates to. 39 * 40 * @see org.junit.runners.Parameterized 41 * @see org.robolectric.ParameterizedRobolectricTestRunner 42 * @see com.google.android.testing.rsl.robolectric.junit.ParametrizedRslTestRunner 43 */ 44 class ParameterizedRunnerDelegate { 45 46 private final int mParametersIndex; 47 private final String mName; 48 ParameterizedRunnerDelegate(int parametersIndex, String name)49 ParameterizedRunnerDelegate(int parametersIndex, String name) { 50 this.mParametersIndex = parametersIndex; 51 this.mName = name; 52 } 53 getName()54 public String getName() { 55 return mName; 56 } 57 createTestInstance(final TestClass testClass)58 public Object createTestInstance(final TestClass testClass) throws Exception { 59 Class<?> bootstrappedClass = testClass.getJavaClass(); 60 Constructor<?>[] constructors = bootstrappedClass.getConstructors(); 61 Assert.assertEquals(1, constructors.length); 62 if (!fieldsAreAnnotated(testClass)) { 63 return constructors[0].newInstance(computeParams(testClass)); 64 } else { 65 Object instance = constructors[0].newInstance(); 66 injectParametersIntoFields(instance, testClass); 67 return instance; 68 } 69 } 70 computeParams(final TestClass testClass)71 private Object[] computeParams(final TestClass testClass) throws Exception { 72 // Robolectric uses a different class loader when running the tests, so the parameters 73 // objects 74 // created by the test runner are not compatible with the parameters required by the test. 75 // Instead, we compute the parameters within the test's class loader. 76 try { 77 List<Object> parametersList = getParametersList(testClass); 78 if (mParametersIndex >= parametersList.size()) { 79 throw new Exception( 80 "Re-computing the parameter list returned a different number of parameters" 81 + " values. Is the data() method of your test non-deterministic?"); 82 } 83 Object parametersObj = parametersList.get(mParametersIndex); 84 return (parametersObj instanceof Object[]) 85 ? (Object[]) parametersObj 86 : new Object[] {parametersObj}; 87 } catch (ClassCastException e) { 88 throw new Exception( 89 String.format( 90 "%s.%s() must return a Collection of arrays.", 91 testClass.getName(), mName), 92 e); 93 } catch (Exception exception) { 94 throw exception; 95 } catch (Throwable throwable) { 96 throw new Exception(throwable); 97 } 98 } 99 100 @SuppressWarnings("unchecked") injectParametersIntoFields(Object testClassInstance, final TestClass testClass)101 private void injectParametersIntoFields(Object testClassInstance, final TestClass testClass) 102 throws Exception { 103 // Robolectric uses a different class loader when running the tests, so referencing 104 // Parameter 105 // directly causes type mismatches. Instead, we find its class within the test's class 106 // loader. 107 Object[] parameters = computeParams(testClass); 108 HashSet<Integer> parameterFieldsFound = new HashSet<>(); 109 for (Field field : testClassInstance.getClass().getFields()) { 110 Annotation parameter = field.getAnnotation(Parameter.class); 111 if (parameter != null) { 112 int index = (int) parameter.annotationType().getMethod("value").invoke(parameter); 113 parameterFieldsFound.add(index); 114 try { 115 field.set(testClassInstance, parameters[index]); 116 } catch (IllegalArgumentException e) { 117 throw new Exception( 118 String.format( 119 "%s: Trying to set %s with the value %s that is not the right" 120 + " type (%s instead of %s).", 121 testClass.getName(), 122 field.getName(), 123 parameters[index], 124 parameters[index].getClass().getSimpleName(), 125 field.getType().getSimpleName()), 126 e); 127 } 128 } 129 } 130 if (parameterFieldsFound.size() != parameters.length) { 131 throw new IllegalStateException( 132 String.format( 133 "Provided %d parameters, but only found fields for parameters: %s", 134 parameters.length, parameterFieldsFound)); 135 } 136 } 137 validateFields(List<Throwable> errors, TestClass testClass)138 static void validateFields(List<Throwable> errors, TestClass testClass) { 139 // Ensure that indexes for parameters are correctly defined 140 if (fieldsAreAnnotated(testClass)) { 141 List<FrameworkField> annotatedFieldsByParameter = 142 getAnnotatedFieldsByParameter(testClass); 143 int[] usedIndices = new int[annotatedFieldsByParameter.size()]; 144 for (FrameworkField each : annotatedFieldsByParameter) { 145 int index = each.getField().getAnnotation(Parameter.class).value(); 146 if (index < 0 || index > annotatedFieldsByParameter.size() - 1) { 147 errors.add( 148 new Exception( 149 String.format( 150 Locale.US, 151 "Invalid @Parameter value: %d. @Parameter fields" 152 + " counted: %d. Please use an index between 0 and" 153 + " %d.", 154 index, 155 annotatedFieldsByParameter.size(), 156 annotatedFieldsByParameter.size() - 1))); 157 } else { 158 usedIndices[index]++; 159 } 160 } 161 for (int index = 0; index < usedIndices.length; index++) { 162 int numberOfUses = usedIndices[index]; 163 if (numberOfUses == 0) { 164 errors.add( 165 new Exception( 166 String.format( 167 Locale.US, "@Parameter(%d) is never used.", index))); 168 } else if (numberOfUses > 1) { 169 errors.add( 170 new Exception( 171 String.format( 172 Locale.US, 173 "@Parameter(%d) is used more than once (%d).", 174 index, 175 numberOfUses))); 176 } 177 } 178 } 179 } 180 181 @SuppressWarnings("unchecked") getAnnotatedFieldsByParameter(TestClass testClass)182 private static List<FrameworkField> getAnnotatedFieldsByParameter(TestClass testClass) { 183 return testClass.getAnnotatedFields(Parameter.class); 184 } 185 fieldsAreAnnotated(TestClass testClass)186 static boolean fieldsAreAnnotated(TestClass testClass) { 187 return !getAnnotatedFieldsByParameter(testClass).isEmpty(); 188 } 189 190 @SuppressWarnings("unchecked") getParametersList(TestClass testClass)191 static List<Object> getParametersList(TestClass testClass) throws Throwable { 192 return (List<Object>) getParametersMethod(testClass).invokeExplosively(null); 193 } 194 195 @SuppressWarnings("unchecked") getParametersMethod(TestClass testClass)196 static FrameworkMethod getParametersMethod(TestClass testClass) throws Exception { 197 List<FrameworkMethod> methods = testClass.getAnnotatedMethods(Parameters.class); 198 FrameworkMethod fallback = null; 199 boolean isRunningOnDevice = isRunningOnAndroid(); 200 201 for (FrameworkMethod each : methods) { 202 int modifiers = each.getMethod().getModifiers(); 203 if (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) { 204 switch (each.getAnnotation(Parameters.class).target()) { 205 case ALL -> fallback = each; 206 case DEVICE -> { 207 if (isRunningOnDevice) return each; 208 } 209 case DEVICE_LESS -> { 210 if (!isRunningOnDevice) return each; 211 } 212 } 213 } 214 } 215 if (fallback != null) { 216 return fallback; 217 } 218 throw new Exception( 219 String.format( 220 "No public static parameters method on class %s", testClass.getName())); 221 } 222 } 223