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