Skip to content

Commit 63612f6

Browse files
rvansasebersole
authored andcommitted
HHH-10058 Parameterized test runner compatible with CustomRunner
(cherry picked from commit d9b456b)
1 parent 92e328b commit 63612f6

File tree

2 files changed

+336
-2
lines changed

2 files changed

+336
-2
lines changed
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
/*
2+
* Hibernate, Relational Persistence for Idiomatic Java
3+
*
4+
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
5+
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
6+
*/
7+
package org.hibernate.testing.junit4;
8+
9+
import org.junit.runner.Runner;
10+
import org.junit.runner.manipulation.NoTestsRemainException;
11+
import org.junit.runner.notification.RunNotifier;
12+
import org.junit.runners.Parameterized;
13+
import org.junit.runners.Suite;
14+
import org.junit.runners.model.FrameworkField;
15+
import org.junit.runners.model.FrameworkMethod;
16+
import org.junit.runners.model.InitializationError;
17+
import org.junit.runners.model.Statement;
18+
19+
import java.lang.annotation.Annotation;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
import java.lang.reflect.Field;
25+
import java.text.MessageFormat;
26+
import java.util.ArrayList;
27+
import java.util.Arrays;
28+
import java.util.Collections;
29+
import java.util.List;
30+
import java.util.SortedMap;
31+
import java.util.TreeMap;
32+
33+
/**
34+
* Allows the {@link CustomRunner} features in parameterized tests.
35+
* This is mostly copy-paste from {@link Parameterized} since the methods could not be overridden.
36+
*
37+
* The static {@link org.junit.BeforeClass} and {@link org.junit.AfterClass} methods will be executed
38+
* only once before and after all tests (since these should prepare static members).
39+
* Hibernate-specific {@link org.hibernate.testing.BeforeClassOnce} and {@link org.hibernate.testing.AfterClassOnce}
40+
* will be executed before and after each set of tests with given parameters.
41+
*
42+
* Class can override the parameters list (annotated by {@link org.junit.runners.Parameterized.Parameters}
43+
* by defining static method of the same name in inheriting class (this works although usually static
44+
* methods cannot override each other in Java).
45+
*
46+
* When there are multiple methods providing the parameters list, the used parameters list is a cross product
47+
* of all the options, concatenating the argument list according to {@link Order} values.
48+
*
49+
* Contrary to {@link Parameterized}, non-static parameters methods are allowed, but the test class needs
50+
* to have parameterless constructor, and therefore use {@link org.junit.runners.Parameterized.Parameter}
51+
* for setting these parameters. This allow type-safe overriding of the method; note that only the base
52+
* method needs the {@link org.junit.runners.Parameterized.Parameters} annotation, overriding methods
53+
* are invoked automatically.
54+
*
55+
* @author Radim Vansa &lt;[email protected]&gt;
56+
*/
57+
public class CustomParameterized extends Suite {
58+
59+
private static final List<Runner> NO_RUNNERS = Collections.emptyList();
60+
61+
private final ArrayList<Runner> runners = new ArrayList<Runner>();
62+
63+
/**
64+
* Only called reflectively. Do not use programmatically.
65+
*/
66+
public CustomParameterized(Class<?> klass) throws Throwable {
67+
super(klass, NO_RUNNERS);
68+
List<FrameworkMethod> parametersMethods = getParametersMethods();
69+
createRunnersForParameters(allParameters(parametersMethods), concatNames(parametersMethods));
70+
}
71+
72+
private String concatNames(List<FrameworkMethod> parametersMethods) {
73+
StringBuilder sb = new StringBuilder();
74+
for (FrameworkMethod method : parametersMethods) {
75+
Parameterized.Parameters parameters = method.getAnnotation(Parameterized.Parameters.class);
76+
if (sb.length() != 0) {
77+
sb.append(", ");
78+
}
79+
sb.append(parameters.name());
80+
}
81+
return sb.toString();
82+
}
83+
84+
@Override
85+
protected List<Runner> getChildren() {
86+
return runners;
87+
}
88+
89+
private Iterable<Object[]> allParameters(List<FrameworkMethod> parametersMethods) throws Throwable {
90+
ArrayList<Iterable<Object[]>> returnedParameters = new ArrayList<Iterable<Object[]>>();
91+
ArrayList<Object[]> allParameters = new ArrayList<Object[]>();
92+
Object cachedInstance = null;
93+
for (FrameworkMethod method : parametersMethods) {
94+
Object parameters;
95+
if (method.isStatic()) {
96+
parameters = method.invokeExplosively(null);
97+
}
98+
else {
99+
if (cachedInstance == null) {
100+
cachedInstance = getTestClass().getOnlyConstructor().newInstance();
101+
}
102+
parameters = method.invokeExplosively(cachedInstance);
103+
}
104+
if (parameters instanceof Iterable) {
105+
returnedParameters.add((Iterable<Object[]>) parameters);
106+
}
107+
else {
108+
throw parametersMethodReturnedWrongType(method);
109+
}
110+
}
111+
for (Iterable<Object[]> parameters : returnedParameters) {
112+
if (allParameters.isEmpty()) {
113+
for (Object[] array : parameters) {
114+
allParameters.add(array);
115+
}
116+
}
117+
else {
118+
ArrayList<Object[]> newAllParameters = new ArrayList<Object[]>();
119+
for (Object[] prev : allParameters) {
120+
for (Object[] array : parameters) {
121+
Object[] next = Arrays.copyOf(prev, prev.length + array.length);
122+
System.arraycopy(array, 0, next, prev.length, array.length);
123+
newAllParameters.add(next);
124+
}
125+
}
126+
allParameters = newAllParameters;
127+
}
128+
}
129+
return allParameters;
130+
}
131+
132+
private List<FrameworkMethod> getParametersMethods() throws Exception {
133+
List<FrameworkMethod> methods = getTestClass().getAnnotatedMethods(
134+
Parameterized.Parameters.class);
135+
SortedMap<Integer, FrameworkMethod> sortedMethods = new TreeMap<Integer, FrameworkMethod>();
136+
for (FrameworkMethod each : methods) {
137+
if (each.isPublic()) {
138+
if (!each.isStatic()) {
139+
if (getTestClass().getOnlyConstructor().getParameterTypes().length != 0) {
140+
throw new Exception("Method " + each.getMethod() + " is annotated with @Parameters, it is not static and there is no parameter-less constructor!");
141+
}
142+
}
143+
Order order = each.getAnnotation(Order.class);
144+
int value = order == null ? 0 : order.value();
145+
FrameworkMethod prev = sortedMethods.put(value, each);
146+
if (prev != null) {
147+
throw new Exception(String.format("There are more methods annotated with @Parameters and @Order(value=%d): %s (%s) and %s (%s)",
148+
value, prev.getMethod(), prev.getAnnotation(Order.class), each.getMethod(), order));
149+
}
150+
}
151+
else {
152+
throw new Exception("Method " + each.getMethod() + " is annotated with @Parameters but it is not public!");
153+
}
154+
}
155+
if (sortedMethods.isEmpty()) {
156+
throw new Exception("No public static parameters method on class "
157+
+ getTestClass().getName());
158+
}
159+
return new ArrayList<FrameworkMethod>(sortedMethods.values());
160+
}
161+
162+
private void createRunnersForParameters(Iterable<Object[]> allParameters, String namePattern) throws Exception {
163+
int i = 0;
164+
for (Object[] parametersOfSingleTest : allParameters) {
165+
String name = nameFor(namePattern, i, parametersOfSingleTest);
166+
CustomRunnerForParameters runner = new CustomRunnerForParameters(
167+
getTestClass().getJavaClass(), parametersOfSingleTest,
168+
name);
169+
runners.add(runner);
170+
++i;
171+
}
172+
}
173+
174+
private String nameFor(String namePattern, int index, Object[] parameters) {
175+
String finalPattern = namePattern.replaceAll("\\{index\\}",
176+
Integer.toString(index));
177+
String name = MessageFormat.format(finalPattern, parameters);
178+
return "[" + name + "]";
179+
}
180+
181+
private Exception parametersMethodReturnedWrongType(FrameworkMethod method) throws Exception {
182+
String className = getTestClass().getName();
183+
String methodName = method.getName();
184+
String message = MessageFormat.format(
185+
"{0}.{1}() must return an Iterable of arrays.",
186+
className, methodName);
187+
return new Exception(message);
188+
}
189+
190+
private List<FrameworkField> getAnnotatedFieldsByParameter() {
191+
return getTestClass().getAnnotatedFields(Parameterized.Parameter.class);
192+
}
193+
194+
private boolean fieldsAreAnnotated() {
195+
return !getAnnotatedFieldsByParameter().isEmpty();
196+
}
197+
198+
private class CustomRunnerForParameters extends CustomRunner {
199+
private final Object[] parameters;
200+
private final String name;
201+
202+
CustomRunnerForParameters(Class<?> type, Object[] parameters, String name) throws InitializationError, NoTestsRemainException {
203+
super(type);
204+
this.parameters = parameters;
205+
this.name = name;
206+
}
207+
208+
@Override
209+
protected Object getTestInstance() throws Exception {
210+
if (testInstance == null) {
211+
if (fieldsAreAnnotated()) {
212+
testInstance = createTestUsingFieldInjection();
213+
}
214+
else {
215+
testInstance = createTestUsingConstructorInjection();
216+
}
217+
}
218+
return testInstance;
219+
}
220+
221+
private Object createTestUsingConstructorInjection() throws Exception {
222+
return getTestClass().getOnlyConstructor().newInstance(parameters);
223+
}
224+
225+
private Object createTestUsingFieldInjection() throws Exception {
226+
List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter();
227+
if (annotatedFieldsByParameter.size() != parameters.length) {
228+
throw new Exception("Wrong number of parameters and @Parameter fields." +
229+
" @Parameter fields counted: " + annotatedFieldsByParameter.size() + ", available parameters: " + parameters.length + ".");
230+
}
231+
Object testClassInstance = getTestClass().getJavaClass().newInstance();
232+
for (FrameworkField each : annotatedFieldsByParameter) {
233+
Field field = each.getField();
234+
Parameterized.Parameter annotation = field.getAnnotation(Parameterized.Parameter.class);
235+
int index = annotation.value();
236+
try {
237+
field.set(testClassInstance, parameters[index]);
238+
}
239+
catch (IllegalArgumentException iare) {
240+
throw new Exception(getTestClass().getName() + ": Trying to set " + field.getName() +
241+
" with the value " + parameters[index] +
242+
" that is not the right type (" + parameters[index].getClass().getSimpleName() + " instead of " +
243+
field.getType().getSimpleName() + ").", iare);
244+
}
245+
}
246+
return testClassInstance;
247+
}
248+
249+
@Override
250+
protected String getName() {
251+
return name;
252+
}
253+
254+
@Override
255+
protected String testName(FrameworkMethod method) {
256+
return method.getName() + getName();
257+
}
258+
259+
@Override
260+
protected void validateConstructor(List<Throwable> errors) {
261+
validateOnlyOneConstructor(errors);
262+
if (fieldsAreAnnotated()) {
263+
validateZeroArgConstructor(errors);
264+
}
265+
}
266+
267+
@Override
268+
protected void validateFields(List<Throwable> errors) {
269+
super.validateFields(errors);
270+
if (fieldsAreAnnotated()) {
271+
List<FrameworkField> annotatedFieldsByParameter = getAnnotatedFieldsByParameter();
272+
int[] usedIndices = new int[annotatedFieldsByParameter.size()];
273+
for (FrameworkField each : annotatedFieldsByParameter) {
274+
int index = each.getField().getAnnotation(Parameterized.Parameter.class).value();
275+
if (index < 0 || index > annotatedFieldsByParameter.size() - 1) {
276+
errors.add(
277+
new Exception("Invalid @Parameter value: " + index + ". @Parameter fields counted: " +
278+
annotatedFieldsByParameter.size() + ". Please use an index between 0 and " +
279+
(annotatedFieldsByParameter.size() - 1) + ".")
280+
);
281+
}
282+
else {
283+
usedIndices[index]++;
284+
}
285+
}
286+
for (int index = 0; index < usedIndices.length; index++) {
287+
int numberOfUse = usedIndices[index];
288+
if (numberOfUse == 0) {
289+
errors.add(new Exception("@Parameter(" + index + ") is never used."));
290+
}
291+
else if (numberOfUse > 1) {
292+
errors.add(new Exception("@Parameter(" + index + ") is used more than once (" + numberOfUse + ")."));
293+
}
294+
}
295+
}
296+
}
297+
298+
@Override
299+
protected Statement classBlock(RunNotifier notifier) {
300+
Statement statement = childrenInvoker(notifier);
301+
statement = withBeforeClasses(statement);
302+
statement = withAfterClasses(statement);
303+
// no class rules executed! These will be executed for the whole suite.
304+
return statement;
305+
}
306+
307+
@Override
308+
protected Statement withBeforeClasses(Statement statement) {
309+
if ( isAllTestsIgnored() ) {
310+
return statement;
311+
}
312+
return new BeforeClassCallbackHandler( this, statement );
313+
}
314+
315+
@Override
316+
protected Statement withAfterClasses(Statement statement) {
317+
if ( isAllTestsIgnored() ) {
318+
return statement;
319+
}
320+
return new AfterClassCallbackHandler( this, statement );
321+
}
322+
323+
@Override
324+
protected Annotation[] getRunnerAnnotations() {
325+
return new Annotation[0];
326+
}
327+
}
328+
329+
@Retention(value = RetentionPolicy.RUNTIME)
330+
@Target(ElementType.METHOD)
331+
public @interface Order {
332+
int value();
333+
}
334+
}

hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public TestClassMetadata getTestClassMetadata() {
6565

6666
private Boolean isAllTestsIgnored;
6767

68-
private boolean isAllTestsIgnored() {
68+
protected boolean isAllTestsIgnored() {
6969
if ( isAllTestsIgnored == null ) {
7070
if ( computeTestMethods().isEmpty() ) {
7171
isAllTestsIgnored = true;
@@ -132,7 +132,7 @@ protected Statement methodBlock(FrameworkMethod method) {
132132
);
133133
}
134134

135-
private Object testInstance;
135+
protected Object testInstance;
136136

137137
protected Object getTestInstance() throws Exception {
138138
if ( testInstance == null ) {

0 commit comments

Comments
 (0)