blob: 86d1d60632d443e5011b9451345fdd1265dba332 [file] [log] [blame]
/*
* Copyright (c) 2006, 2021 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0,
* or the Eclipse Distribution License v. 1.0 which is available at
* http://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause
*/
// Contributors:
// Oracle - initial API and implementation
//
package org.eclipse.persistence.jpa.tests.jpql;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.persistence.jpa.jpql.tools.model.AbstractActualJPQLQueryFormatter;
import org.eclipse.persistence.jpa.jpql.tools.model.BaseJPQLQueryFormatter;
import org.eclipse.persistence.jpa.jpql.tools.model.IJPQLQueryBuilder;
import org.eclipse.persistence.jpa.tests.jpql.parser.JPQLGrammarTestHelper;
import org.eclipse.persistence.jpa.tests.jpql.tools.model.IJPQLQueryBuilderTestHelper;
import org.eclipse.persistence.jpa.tests.jpql.tools.model.IJPQLQueryFormatterTestHelper;
import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
import org.junit.internal.runners.ErrorReportingRunner;
import org.junit.runner.Description;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.ParentRunner;
import org.junit.runners.Suite.SuiteClasses;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
/**
* This JUnit runner is the sole runner of Hermes unit-tests because it modifies the default
* behavior by adding the following support:
* <ul>
* <li>Adds the ability to inject objects that are instantiated by a test suite into the unit-test
* before they are run.
* <li>Because one unit-test can be run more than once with different state of any given object,
* this cause issues in Eclipse because the IDE uses {@link Object#toString()} and cannot
* discriminate between those identical tests. This runner changes the test's description by adding
* enough information in order to make each test's description unique.
* </ul>
*
* @version 2.4
* @since 2.4
* @author Pascal Filion
*/
@SuppressWarnings({ "nls", "restriction" })
public class JPQLTestRunner extends ParentRunner<Runner> {
/**
* The cached {@link Description} so it's not recreated every time.
*/
private Description description;
/**
* This contains the helpers that will be injected into each test.
*/
private DescriptionHelper descriptionHelper;
/**
* The display string of this test suite.
*/
private String name;
/**
* The {@link Runner runners} for the test classes defined in {@link org.junit.runners.Suite.SuiteClasses
* &#64;SuiteClasses}.
*/
private List<Runner> runners;
/**
* The parent {@link SuiteHelper} or <code>null</code> if none was defined yet.
*/
private SuiteHelper suiteHelper;
/**
* The list of registered helpers that inject values from the test suite into the unit-tests
* before they are running.
*/
private static final Map<Class<? extends Annotation>, DescriptionBuilder> testRunnerHelpers;
/**
* Registers the supported test runner helpers.
*/
static {
testRunnerHelpers = new HashMap<>();
testRunnerHelpers.put(IJPQLQueryBuilderTestHelper.class, buildJPQLQueryBuilderTestHelperDescriptionBuilder());
testRunnerHelpers.put(IJPQLQueryFormatterTestHelper.class, buildJPQLQueryFormatterTestHelperDescriptionBuilder());
testRunnerHelpers.put(JPQLGrammarTestHelper.class, buildJPQLGrammarTestHelperDescriptionBuilder());
testRunnerHelpers.put(JPQLQueryHelperTestHelper.class, buildJPQLQueryHelperTestHelperDescriptionBuilder());
testRunnerHelpers.put(JPQLQueryTestHelperTestHelper.class, buildJPQLQueryTestHelperTestHelperDescriptionBuilder());
}
/**
* Creates a new <code>JPQLTestRunner</code>.
*
* @param testClass The class that is either a test suite or a unit-tests
* @throws InitializationError If the given test class is malformed
*/
public JPQLTestRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
/**
* Creates a new <code>JPQLTestRunner</code>.
*
* @param testClass The class that is either a test suite or a unit-tests
* @param suiteHelper The parent {@link SuiteHelper} or {@code null} if none was defined yet
* @throws InitializationError If the given test class is malformed
*/
public JPQLTestRunner(Class<?> testClass, SuiteHelper suiteHelper) throws InitializationError {
this(testClass);
this.suiteHelper = suiteHelper;
}
private static DescriptionBuilder buildJPQLGrammarTestHelperDescriptionBuilder() {
return new DescriptionBuilder() {
@Override
public String toString(Object object) {
return object.toString();
}
};
}
private static DescriptionBuilder buildJPQLQueryBuilderTestHelperDescriptionBuilder() {
return new DescriptionBuilder() {
@Override
public String toString(Object object) {
IJPQLQueryBuilder builder = (IJPQLQueryBuilder) object;
StringBuilder sb = new StringBuilder();
sb.append(builder.getClass().getSimpleName());
sb.append("[");
sb.append(builder.getGrammar().toString());
sb.append("]");
return sb.toString();
}
};
}
private static DescriptionBuilder buildJPQLQueryFormatterTestHelperDescriptionBuilder() {
return new DescriptionBuilder() {
@Override
public String toString(Object object) {
BaseJPQLQueryFormatter formatter = (BaseJPQLQueryFormatter) object;
StringBuilder sb = new StringBuilder();
sb.append(formatter.getClass().getSimpleName());
sb.append("[");
sb.append(formatter.getIdentifierStyle().name());
if (object instanceof AbstractActualJPQLQueryFormatter) {
AbstractActualJPQLQueryFormatter actualFormatter = (AbstractActualJPQLQueryFormatter) object;
sb.append("|");
sb.append(actualFormatter.isUsingExactMatch());
}
sb.append("]");
return sb.toString();
}
};
}
private static DescriptionBuilder buildJPQLQueryHelperTestHelperDescriptionBuilder() {
return new DescriptionBuilder() {
@Override
public String toString(Object object) {
return object.getClass().getSimpleName();
}
};
}
private static DescriptionBuilder buildJPQLQueryTestHelperTestHelperDescriptionBuilder() {
return new DescriptionBuilder() {
@Override
public String toString(Object object) {
return object.getClass().getSimpleName();
}
};
}
private List<Runner> buildChildren() {
SuiteClasses suiteClasses = findSuiteClasses(getTestClass().getJavaClass());
if (suiteClasses == null) {
return Collections.emptyList();
}
List<Runner> runners = new ArrayList<>();
for (Class<?> test : suiteClasses.value()) {
if (descriptionHelper.helpers.isEmpty()) {
Runner runner = buildRunner(test, suiteHelper);
runners.add(runner);
}
else {
for (SuiteHelper suiteHelper : buildSuiteHelpers()) {
Runner runner = buildRunner(test, suiteHelper);
runners.add(runner);
}
}
}
Collections.sort(runners, buildRunnerComparator());
return runners;
}
private String buildDisplayString() {
String displayString = super.getName();
if (suiteHelper != null) {
StringBuilder writer = new StringBuilder();
writer.append(displayString);
suiteHelper.addAdditionalInfo(writer);
displayString = writer.toString();
}
return displayString;
}
private Runner buildRunner(Class<?> testClass, SuiteHelper suiteHelper) {
try {
// Create a runner for multiple unit-tests
SuiteClasses suiteClasses = findSuiteClasses(testClass);
if (suiteClasses != null) {
return new JPQLTestRunner(testClass, suiteHelper);
}
// Create a runner for a single unit-test
if (JPQLBasicTest.class.isAssignableFrom(testClass)) {
return new JPQLBasicTestRunner(testClass, suiteHelper);
}
// Create the default runner
return new AllDefaultPossibilitiesBuilder().runnerForClass(testClass);
}
catch (Throwable e) {
return new ErrorReportingRunner(testClass, e);
}
}
private Comparator<Runner> buildRunnerComparator() {
return new Comparator<Runner>() {
@Override
public int compare(Runner runner1, Runner runner2) {
String displayName1 = runner1.getDescription().getDisplayName();
String displayName2 = runner1.getDescription().getDisplayName();
return displayName1.compareTo(displayName2);
}
};
}
private List<SuiteHelper> buildSuiteHelpers() {
List<SuiteHelper> suiteHelpers = new ArrayList<>();
Map<Class<? extends Annotation>, Object> singleHelpers = new HashMap<>();
Collection<Class<? extends Annotation>> multipleHelpers = retrieveMultipleHelpers();
for (Map.Entry<Class<? extends Annotation>, Object[]> helper : descriptionHelper.helpers.entrySet()) {
if (!multipleHelpers.contains(helper.getKey())) {
singleHelpers.put(helper.getKey(), helper.getValue()[0]);
}
}
if (multipleHelpers.size() > 1) {
for (Class<? extends Annotation> firstHelperKey : multipleHelpers) {
for (Class<? extends Annotation> secondHelperKey : multipleHelpers) {
if (firstHelperKey != secondHelperKey) {
for (Object firstHelper : descriptionHelper.helpers.get(firstHelperKey)) {
for (Object secondHelper : descriptionHelper.helpers.get(secondHelperKey)) {
Map<Class<? extends Annotation>, Object> copy = new HashMap<>();
copy.putAll(singleHelpers);
copy.put(firstHelperKey, firstHelper);
copy.put(secondHelperKey, secondHelper);
List<Class<? extends Annotation>> keys = new ArrayList<>();
keys.add(firstHelperKey);
keys.add(secondHelperKey);
suiteHelpers.add(new SuiteHelper(suiteHelper, copy, keys));
}
}
}
}
}
}
else if (multipleHelpers.size() == 1) {
for (Class<? extends Annotation> firstHelperKey : multipleHelpers) {
for (Object firstHelper : descriptionHelper.helpers.get(firstHelperKey)) {
Map<Class<? extends Annotation>, Object> copy = new HashMap<>();
copy.putAll(singleHelpers);
copy.put(firstHelperKey, firstHelper);
List<Class<? extends Annotation>> keys = new ArrayList<>();
keys.add(firstHelperKey);
suiteHelpers.add(new SuiteHelper(suiteHelper, copy, keys));
}
}
}
else {
suiteHelpers.add(new SuiteHelper(suiteHelper, singleHelpers));
}
return suiteHelpers;
}
@Override
protected void collectInitializationErrors(List<Throwable> errors) {
super.collectInitializationErrors(errors);
initializeDescriptionHelper(errors);
}
@Override
protected Description describeChild(Runner child) {
return child.getDescription();
}
private SuiteClasses findSuiteClasses(Class<?> testClass) {
if (testClass == Object.class) {
return null;
}
SuiteClasses suiteClasses = testClass.getAnnotation(SuiteClasses.class);
if (suiteClasses != null) {
return suiteClasses;
}
return findSuiteClasses(testClass.getSuperclass());
}
@Override
protected List<Runner> getChildren() {
// Cache the Description since JUnit always recreate it, this will increase performance
if (runners == null) {
runners = buildChildren();
}
return runners;
}
@Override
public Description getDescription() {
// Cache the Description since JUnit always recreate it, this will increase performance
if (description == null) {
description = super.getDescription();
}
return description;
}
@Override
protected String getName() {
// Cache the Description since JUnit always recreate it, this will increase performance and
// also, add the extra information otherwise Eclipse will not be able to update the status of
// the tests, it uses the display string to retrieve the node from the JUnit view, if two
// nodes have the same display string, then only the last one is updated
if (name == null) {
name = buildDisplayString();
}
return name;
}
private void initializeDescriptionHelper(List<Throwable> errors) {
descriptionHelper = new DescriptionHelper();
Class<?> unitTest = getTestClass().getJavaClass();
for (Method method : unitTest.getDeclaredMethods()) {
if (isHelperMethod(method)) {
for (Class<? extends Annotation> annotation : testRunnerHelpers.keySet()) {
if (method.isAnnotationPresent(annotation)) {
try {
method.setAccessible(true);
Object value = method.invoke(null);
descriptionHelper.helpers.put(
annotation,
value.getClass().isArray() ? (Object[]) value : new Object[] { value }
);
}
catch (Exception e) {
errors.add(e);
}
}
}
}
}
}
private boolean isHelperMethod(Method method) {
return Modifier.isStatic(method.getModifiers());
}
private Collection<Class<? extends Annotation>> retrieveMultipleHelpers() {
Collection<Class<? extends Annotation>> keys = new ArrayList<>();
for (Map.Entry<Class<? extends Annotation>, Object[]> helper : descriptionHelper.helpers.entrySet()) {
if (helper.getValue().length > 1) {
keys.add(helper.getKey());
}
}
return keys;
}
@Override
protected void runChild(Runner child, RunNotifier notifier) {
child.run(notifier);
}
/**
* This interface is used to create the description of a unit-tests.
*/
private interface DescriptionBuilder {
/**
* Creates a string representation of the given object.
*
* @param object The object to convert into a human readable string
* @return A unique description for the given object
*/
String toString(Object object);
}
private static class DescriptionHelper {
private Map<Class<? extends Annotation>, Object[]> helpers;
DescriptionHelper() {
super();
helpers = new HashMap<>();
}
}
private static class JPQLBasicTestRunner extends BlockJUnit4ClassRunner {
private Description description;
private SuiteHelper suiteHelper;
private JPQLBasicTest test;
private boolean uniquenessRequired;
JPQLBasicTestRunner(Class<?> testClass, SuiteHelper suiteHelper) throws InitializationError {
super(testClass);
this.suiteHelper = suiteHelper;
// Check to see if the signature of the test methods needs to be unique,
// this is required when the same test is run more than once and the
// generated signature remains identical
uniquenessRequired = testClass.isAnnotationPresent(UniqueSignature.class);
}
private Description buildDescription() {
Description description = Description.createSuiteDescription(
buildDisplayString(),
getTestClass().getAnnotations()
);
Description superDescription = super.getDescription();
description.getChildren().addAll(superDescription.getChildren());
return description;
}
private String buildDisplayString() {
if (suiteHelper != null) {
StringBuilder writer = new StringBuilder();
writer.append(getName());
suiteHelper.addAdditionalInfo(writer);
return writer.toString();
}
return getName();
}
private Comparator<FrameworkMethod> buildMethodComparator() {
return new Comparator<FrameworkMethod>() {
@Override
public int compare(FrameworkMethod method1, FrameworkMethod method2) {
return method1.getName().compareTo(method2.getName());
}
};
}
@Override
protected Statement classBlock(RunNotifier notifier) {
Statement statement = new CreateTestStatement();
statement = new SetUpClassStatement(statement);
statement = new CompositeStatement(statement, childrenInvoker(notifier));
statement = new TearDownClassStatement(statement);
return statement;
}
@Override
protected Object createTest() {
return test;
}
@Override
protected List<FrameworkMethod> getChildren() {
List<FrameworkMethod> methods = new ArrayList<>(super.getChildren());
Collections.sort(methods, buildMethodComparator());
return methods;
}
@Override
public Description getDescription() {
if (description == null) {
description = buildDescription();
}
return description;
}
private void instantiateTest() throws Throwable {
Class<?> testClass = getTestClass().getJavaClass();
Constructor<?> constructor = testClass.getConstructor();
constructor.setAccessible(true);
test = (JPQLBasicTest) constructor.newInstance();
// Inject the SuiteHelper' values into the test
if (suiteHelper != null) {
suiteHelper.injectValues(test);
}
}
@Override
protected Statement methodBlock(FrameworkMethod method) {
Statement statement = new SetUpStatement();
statement = new CompositeStatement(statement, super.methodBlock(method));
statement = new TearDownStatement(statement);
return statement;
}
@Override
protected Statement methodInvoker(FrameworkMethod method, Object test) {
this.test = (JPQLBasicTest) test;
return super.methodInvoker(method, test);
}
@Override
protected String testName(FrameworkMethod method) {
// Create the signature of the method, which will have the helpers' additional information
StringBuilder writer = new StringBuilder();
writer.append(method.getName());
if (suiteHelper != null) {
suiteHelper.addAdditionalInfo(writer);
}
// It is possible two signatures maybe be identical, add something unique
if (uniquenessRequired) {
writer.append(" (");
writer.append(hashCode());
writer.append(")");
}
return writer.toString();
}
private class CompositeStatement extends Statement {
private Statement statement1;
private Statement statement2;
CompositeStatement(Statement statement1, Statement statement2) {
super();
this.statement1 = statement1;
this.statement2 = statement2;
}
@Override
public void evaluate() throws Throwable {
statement1.evaluate();
statement2.evaluate();
}
}
/**
* This {@link Statement} evaluates the wrapped {@link Statement} and then invoke {@link
* JPQLBasicTest#setUpClass()}.
*/
private class CreateTestStatement extends Statement {
@Override
public void evaluate() throws Throwable {
instantiateTest();
}
}
/**
* This {@link Statement} evaluates the wrapped {@link Statement} and then invoke {@link
* JPQLBasicTest#setUpClass()}.
*/
private class SetUpClassStatement extends Statement {
private Statement statement;
SetUpClassStatement(Statement statement) {
super();
this.statement = statement;
}
@Override
public void evaluate() throws Throwable {
statement.evaluate();
test.setUpClass();
}
}
/**
* This {@link Statement} evaluates the wrapped {@link Statement} and then invoke {@link
* JPQLBasicTest#setUp()}.
*/
private class SetUpStatement extends Statement {
@Override
public void evaluate() throws Throwable {
test.setUp();
}
}
/**
* This {@link Statement} evaluates the wrapped {@link Statement} and then invoke {@link
* JPQLBasicTest#tearDownClass()}.
*/
private class TearDownClassStatement extends Statement {
private Statement statement;
TearDownClassStatement(Statement statement) {
super();
this.statement = statement;
}
@Override
public void evaluate() throws Throwable {
try {
statement.evaluate();
}
finally {
if (test != null) {
test.tearDownClass();
}
}
}
}
/**
* This {@link Statement} evaluates the wrapped {@link Statement} and then invoke {@link
* JPQLBasicTest#tearDown()}.
*/
private class TearDownStatement extends Statement {
private Statement statement;
TearDownStatement(Statement statement) {
super();
this.statement = statement;
}
@Override
public void evaluate() throws Throwable {
try {
statement.evaluate();
}
finally {
if (test != null) {
test.tearDown();
}
}
}
}
}
private static class SuiteHelper {
private Map<Class<? extends Annotation>, Object> helpers;
private SuiteHelper parent;
private List<Class<? extends Annotation>> primaryKeys;
SuiteHelper(SuiteHelper parent,
Map<Class<? extends Annotation>, Object> helpers) {
this(parent, helpers, Collections.<Class<? extends Annotation>>emptyList());
}
SuiteHelper(SuiteHelper parent,
Map<Class<? extends Annotation>, Object> helpers,
List<Class<? extends Annotation>> primaryKeys) {
super();
this.parent = parent;
this.helpers = helpers;
this.primaryKeys = primaryKeys;
}
void addAdditionalInfo(StringBuilder writer) {
for (Class<? extends Annotation> primaryKey : primaryKeys) {
writer.append(" - ");
Object helper = helpers.get(primaryKey);
DescriptionBuilder descriptionBuilder = testRunnerHelpers.get(primaryKey);
writer.append(descriptionBuilder.toString(helper));
}
if (parent != null) {
parent.addAdditionalInfo(writer);
}
}
private void injectValues(Class<?> testClass, JPQLBasicTest test) throws Exception {
if (testClass == Object.class) {
return;
}
Field[] fields = testClass.getDeclaredFields();
for (Field field : fields) {
for (Map.Entry<Class<? extends Annotation>, Object> helper : helpers.entrySet()) {
if (field.isAnnotationPresent(helper.getKey())) {
field.setAccessible(true);
field.set(test, helper.getValue());
}
}
}
injectValues(testClass.getSuperclass(), test);
}
void injectValues(JPQLBasicTest test) throws Exception {
injectValues(test.getClass(), test);
if (parent != null) {
parent.injectValues(test);
}
}
}
}