blob: 06ae645d5ca7f1838ba5328bd0212754fa03bf77 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License 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 org.apache.openjpa.persistence.test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.persistence.Cache;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.apache.openjpa.jdbc.conf.JDBCConfiguration;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.kernel.AbstractBrokerFactory;
import org.apache.openjpa.kernel.Broker;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.persistence.JPAFacadeHelper;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactory;
import org.apache.openjpa.persistence.OpenJPAEntityManagerFactorySPI;
import org.apache.openjpa.persistence.OpenJPAPersistence;
import junit.framework.TestCase;
import junit.framework.TestResult;
/**
* Base class for Persistence TestCases. This class contains utility methods but does not maintain an EntityManager or
* EntityManagerFactory - these tasks are left for subclasses to handle. Extends junit.framework.TestCase and
* performs NO automatic clean up of EMFs created by createEMF() or createNamedEMF().
*/
public abstract class AbstractPersistenceTestCase extends TestCase {
public static final String FRESH_EMF = "Creates new EntityManagerFactory";
public static final String RETAIN_DATA = "Retain data after test run";
private boolean retainDataOnTearDown;
protected boolean _fresh = false;
private Boolean testsDisabled = Boolean.FALSE;
public static final String ALLOW_FAILURE_LOG = "log";
public static final String ALLOW_FAILURE_IGNORE = "ignore";
public static final String ALLOW_FAILURE_SYS_PROP = "tests.openjpa.allowfailure";
private static String allowFailureConfig = System.getProperty(ALLOW_FAILURE_SYS_PROP, ALLOW_FAILURE_IGNORE);
/**
* Marker object you pass to {@link #setUp} to indicate that the database table rows should be cleared.
*/
protected static final Object CLEAR_TABLES = new Object();
/**
* Marker object you pass to {@link #setUp} to indicate that the database table should be dropped and then
* recreated.
*/
protected static final Object DROP_TABLES = new Object();
/**
* The {@link TestResult} instance for the current test run.
*/
protected TestResult testResult;
/**
* Create an entity manager factory. Put {@link #CLEAR_TABLES} in this list to tell the test framework to delete all
* table contents before running the tests.
* NOTE: Caller must close the returned EMF.
*
* @param props
* list of persistent types used in testing and/or configuration values in the form
* key,value,key,value...
*/
protected OpenJPAEntityManagerFactorySPI createEMF(final Object... props) {
return createNamedEMF(getPersistenceUnitName(), props);
}
/**
* The name of the persistence unit that this test class should use by default. This defaults to "test".
*/
protected String getPersistenceUnitName() {
return "test";
}
/**
* Create an entity manager factory for persistence unit <code>pu</code>. Put {@link #CLEAR_TABLES} in this list to
* tell the test framework to delete all table contents before running the tests.
* NOTE: Caller must close the returned EMF.
*
* @param props
* list of persistent types used in testing and/or configuration values in the form
* key,value,key,value...
*/
protected OpenJPAEntityManagerFactorySPI createNamedEMF(final String pu, Object... props) {
Map<String, Object> map = getPropertiesMap(props);
OpenJPAEntityManagerFactorySPI oemf = null;
Map<Object, Object> config = new HashMap<>(System.getProperties());
config.putAll(map);
oemf = (OpenJPAEntityManagerFactorySPI) Persistence.createEntityManagerFactory(pu, config);
if (oemf == null) {
throw new NullPointerException("Expected an entity manager factory " + "for the persistence unit named: \""
+ pu + "\"");
}
return oemf;
}
/**
* Create an entity manager factory for persistence unit <code>pu</code>. Put {@link #CLEAR_TABLES} in this list to
* tell the test framework to delete all table contents before running the tests.
* NOTE: Caller must close the returned EMF.
*
* @param props
* list of persistent types used in testing and/or configuration values in the form
* key,value,key,value...
*/
protected OpenJPAEntityManagerFactorySPI createNamedOpenJPAEMF(final String pu,
String res, Map<String,Object> props) {
OpenJPAEntityManagerFactorySPI oemf = null;
Map<Object, Object> config = new HashMap<>(System.getProperties());
if (props != null)
config.putAll(props);
oemf = (OpenJPAEntityManagerFactorySPI) OpenJPAPersistence.createEntityManagerFactory(pu, res, props);
if (oemf == null) {
throw new NullPointerException("Expected an OpenJPA entity manager factory " +
"for the persistence unit named: \"" + pu + "\"");
}
return oemf;
}
protected Map<String, Object> getPropertiesMap(Object... props) {
Map<String, Object> map = new HashMap<>();
map.put("openjpa.DynamicEnhancementAgent", "false");
List<Class<?>> types = new ArrayList<>();
boolean prop = false;
for (int i = 0; props != null && i < props.length; i++) {
if (props[i] == FRESH_EMF) {
_fresh = true;
continue;
}
if (props[i] == RETAIN_DATA) {
retainDataOnTearDown = true;
continue;
}
if (prop) {
map.put((String) props[i - 1], props[i]);
prop = false;
} else if (props[i] == CLEAR_TABLES) {
map.put("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true,"
+ "SchemaAction='add,deleteTableContents')");
} else if (props[i] == DROP_TABLES) {
map.put("openjpa.jdbc.SynchronizeMappings", "buildSchema(ForeignKeys=true,"
+ "SchemaAction='drop,add')");
} else if (props[i] instanceof Class<?>) {
types.add((Class<?>) props[i]);
}
else if (props[i] instanceof Class<?>[]) {
for(Class<?> clss : (Class<?>[]) props[i]) {
types.add(clss);
}
}
else if (props[i] != null) {
prop = true;
}
}
if (!types.isEmpty()) {
StringBuilder buf = new StringBuilder();
for (Class<?> c : types) {
if (buf.length() > 0) {
buf.append(";");
}
buf.append(c.getName());
}
String oldValue =
map.containsKey("openjpa.MetaDataFactory") ? "," + map.get("openjpa.MetaDataFactory").toString() : "";
map.put("openjpa.MetaDataFactory", "jpa(Types=" + buf.toString() + oldValue + ")");
} else {
map.put("openjpa.MetaDataFactory", "jpa");
}
return map;
}
@Override
public void run(TestResult testResult) {
this.testResult = testResult;
super.run(testResult);
}
@Override
public void tearDown() throws Exception {
try {
super.tearDown();
} catch (Exception e) {
// if a test failed, swallow any exceptions that happen
// during tear-down, as these just mask the original problem.
if (testResult.wasSuccessful()) {
throw e;
}
}
}
/**
* Safely close the given factory.
*/
protected boolean closeEMF(EntityManagerFactory emf) {
boolean brc;
if (emf == null || !emf.isOpen()) {
return false;
}
try {
closeAllOpenEMs(emf);
} finally {
emf.close();
brc = !emf.isOpen();
if (!brc) {
System.err.println("AbstractPersistenceTestCase().closeEMF() - EMF is still open.");
}
}
return brc;
}
/**
* Safely close the given EM
*
* @param em
*/
protected boolean closeEM(EntityManager em) {
if (em == null || !em.isOpen()) {
return false;
}
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
em.close();
boolean brc = !em.isOpen();
if (!brc) {
System.err.println("AbstractPersistenceTestCase().closeEM() - EM is still open.");
}
return brc;
}
/**
* Closes all open entity managers after first rolling back any open transactions.
*/
protected void closeAllOpenEMs(EntityManagerFactory emf) {
if (emf == null || !emf.isOpen()) {
return;
}
for (Broker b : ((AbstractBrokerFactory) JPAFacadeHelper.toBrokerFactory(emf)).getOpenBrokers()) {
if (b != null && !b.isClosed()) {
EntityManager em = JPAFacadeHelper.toEntityManager(b);
if( em.getTransaction().isActive() ) {
try {
em.getTransaction().rollback();
} catch (Exception e) {
}
}
closeEM(em);
}
}
}
/**
* Delete all instances of the given types using bulk delete queries, but do not close any open entity managers.
*/
protected void clear(EntityManagerFactory emf, Class<?>... types) {
if (emf == null || types.length == 0) {
return;
}
List<ClassMetaData> metas = new ArrayList<>(types.length);
for (Class<?> c : types) {
ClassMetaData meta = JPAFacadeHelper.getMetaData(emf, c);
if (meta != null) {
metas.add(meta);
}
}
clear(emf, false, metas.toArray(new ClassMetaData[metas.size()]));
}
/**
* Delete all instances of the persistent types registered with the given factory using bulk delete queries, after
* first closing all open entity managers (and rolling back any open transactions).
*/
protected void clear(EntityManagerFactory emf) {
if (emf == null) {
return;
}
clear(emf, true, ((OpenJPAEntityManagerFactorySPI) emf).getConfiguration().getMetaDataRepositoryInstance()
.getMetaDatas());
}
/**
* Delete all instances of the given types using bulk delete queries.
*
* @param emf
* The EntityManagerFactory to use. A new EntityManager will be created from this EMF and used to execute
* bulk updates.
* @param closeEMs
* Whether any open EMs should be closed
* @param types
* the types that will be cleared.
*/
private void clear(EntityManagerFactory emf, boolean closeEMs, ClassMetaData... types) {
if (emf == null || types.length == 0) {
return;
}
// prevent deadlock by closing the open entity managers
// and rolling back any open transactions
// before issuing delete statements on a new entity manager.
if (closeEMs) {
closeAllOpenEMs(emf);
}
if (retainDataOnTearDown) {
return;
}
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
for (ClassMetaData meta : types) {
if (!meta.isMapped() || meta.isEmbeddedOnly()
|| Modifier.isAbstract(meta.getDescribedType().getModifiers())
&& !isBaseManagedInterface(meta, types)) {
continue;
}
em.createQuery("DELETE FROM " + meta.getTypeAlias() + " o").executeUpdate();
}
em.getTransaction().commit();
} catch (Exception e) {
// ignore
} finally {
closeEM(em);
}
}
protected DBDictionary getDbDictioary(EntityManagerFactory emf) {
return ((JDBCConfiguration)((OpenJPAEntityManagerFactory) emf).getConfiguration()).getDBDictionaryInstance();
}
/**
* Return the entity name for the given type.
*/
protected String entityName(EntityManagerFactory emf, Class<?> c) {
ClassMetaData meta = JPAFacadeHelper.getMetaData(emf, c);
return (meta == null) ? null : meta.getTypeAlias();
}
/**
* Determines if the class associated with the provided {@link ClassMetaData} is a managed interface and does not
* extend another managed interface.
*
* @param meta
* {@link ClassMetaData} for the class to examine
* @param types
* array of class meta data for persistent types
* @return true if the {@link ClassMetaData} is for an interface and the interface does not extend another managed
* interface
*/
private boolean isBaseManagedInterface(ClassMetaData meta, ClassMetaData... types) {
if (Modifier.isInterface(meta.getDescribedType().getModifiers()) && !isExtendedManagedInterface(meta, types)) {
return true;
}
return false;
}
/**
* Determines if the class associated with the provided {@link ClassMetaData} is an interface and if it extends
* another managed interface.
*
* @param meta
* {@link ClassMetaData} for the class to examine
* @param types
* array of class meta data for persistent types
* @return true if the {@link ClassMetaData} is for an interface and the interface extends another managed interface
*/
private boolean isExtendedManagedInterface(ClassMetaData meta, ClassMetaData... types) {
if (!Modifier.isInterface(meta.getDescribedType().getModifiers())) {
return false;
}
// Run through the interface this class extends. If any of them
// are managed/have class metadata, return true.
Class<?>[] ifaces = meta.getDescribedType().getInterfaces();
for (int i = 0; ifaces != null && i < ifaces.length; i++) {
for (ClassMetaData meta2 : types) {
if (ifaces[i].equals(meta2.getDescribedType())) {
return true;
}
}
}
return false;
}
public static void assertNotEquals(Object o1, Object o2) {
if (o1 == o2) {
fail("expected args to be different; were the same instance.");
} else if (o1 == null || o2 == null) {
return;
} else if (o1.equals(o2)) {
fail("expected args to be different; compared equal.");
}
}
/**
* Round-trip a serializable object to bytes.
*/
@SuppressWarnings("unchecked")
public static <T> T roundtrip(T o) throws ClassNotFoundException, IOException {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bytes);
out.writeObject(o);
out.flush();
ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes.toByteArray()));
return (T) in.readObject();
}
// ================================================
// Utility methods for exception handling
// ================================================
/**
* Asserts that the given targetType is assignable from given actual Throwable.
*/
protected void assertException(final Throwable actual, Class<?> targetType) {
assertException(actual, targetType, null);
}
/**
* Asserts that the given targetType is assignable from given actual Throwable. Asserts that the nestedType is
* nested (possibly recursively) within the given actual Throwable.
*
* @param actual
* is the actual throwable to be tested
* @param targetType
* is expected type or super type of actual. If null, then the check is omitted.
* @param nestedTargetType
* is expected type of exception nested within actual. If null this search is omitted.
*
*/
protected void assertException(final Throwable actual, Class<?> targetType, Class<?> nestedTargetType) {
assertNotNull(actual);
Class<?> actualType = actual.getClass();
if (targetType != null && !targetType.isAssignableFrom(actualType)) {
actual.printStackTrace();
fail(targetType.getName() + " is not assignable from " + actualType.getName());
}
if (nestedTargetType != null) {
Throwable nested = actual.getCause();
Class<?> nestedActualType = (nested == null) ? null : nested.getClass();
while (nestedActualType != null) {
if (nestedTargetType.isAssignableFrom(nestedActualType)) {
return;
} else {
Throwable next = nested.getCause();
if (next == null || next == nested) {
break;
}
nestedActualType = next.getClass();
nested = next;
}
}
actual.printStackTrace();
fail("No nested type " + nestedTargetType + " in " + actual);
}
}
/**
* Asserts that the given targetType is assignable from given actual Throwable and that the exception message
* contains the specified message or message fragments.
*/
protected void assertExceptionMessage(final Throwable actual, Class<?> targetType, String... messages) {
assertException(actual, targetType, null);
assertMessage(actual, messages);
}
/**
* Assert that each of given keys are present in the message of the given Throwable.
*/
protected void assertMessage(Throwable actual, String... keys) {
if (actual == null || keys == null) {
return;
}
String message = actual.getMessage();
for (String key : keys) {
assertTrue(key + " is not in " + message, message.contains(key));
}
}
public void printException(Throwable t) {
printException(t, 2);
}
public void printException(Throwable t, int tab) {
if (t == null) {
return;
}
for (int i = 0; i < tab * 4; i++) {
System.out.print(" ");
}
String sqlState =
(t instanceof SQLException) ? "(SQLState=" + ((SQLException) t).getSQLState() + ":" + t.getMessage() + ")"
: "";
System.out.println(t.getClass().getName() + sqlState);
if (t.getCause() == t) {
return;
}
printException(t.getCause(), tab + 2);
}
/**
* Overrides to allow tests annotated with @AllowFailure to fail.
* If @DatabasePlatform value matches the current JDBC driver or
* tests have been disabled, then the test will not be run.
* If the test is in error then the normal pathway is executed.
*/
@Override
public void runBare() throws Throwable {
if (!isRunsOnCurrentPlatform()) {
return;
}
runBare(getAllowFailure());
}
protected void runBare(AllowFailure allowFailureAnnotation) throws Throwable {
boolean allowFailureValue = allowFailureAnnotation == null ? false : allowFailureAnnotation.value();
if (allowFailureValue) {
if (ALLOW_FAILURE_IGNORE.equalsIgnoreCase(allowFailureConfig)) {
return; // skip this test
} else {
try {
super.runBare();
} catch (Throwable t) {
if (ALLOW_FAILURE_LOG.equalsIgnoreCase(allowFailureConfig)) {
System.err.println("*** FAILED (but ignored): " + this);
System.err.println("*** Reason : " + allowFailureAnnotation.message());
System.err.println("Stacktrace of failure");
t.printStackTrace();
} else {
throw t;
}
}
}
} else {
super.runBare();
}
}
/**
* Override to run the test and assert its state.
* @exception Throwable if any exception is thrown
*/
@Override
protected void runTest() throws Throwable {
if (isTestsDisabled()) {
return;
}
super.runTest();
}
/**
* Affirms if the test case or the test method is annotated with
*
* @AllowFailure. Method level annotation has higher precedence than Class level annotation.
*/
protected AllowFailure getAllowFailure() {
try {
Method runMethod = getClass().getMethod(getName(), (Class[]) null);
AllowFailure anno = runMethod.getAnnotation(AllowFailure.class);
if (anno != null) {
return anno;
}
} catch (SecurityException | NoSuchMethodException e) {
// ignore
}
return getClass().getAnnotation(AllowFailure.class);
}
/**
* Affirms if either this test has been annotated with @DatabasePlatform and at least one of the specified driver is
* available in the classpath, or no such annotation is used.
*
*/
protected boolean isRunsOnCurrentPlatform() {
DatabasePlatform anno = getClass().getAnnotation(DatabasePlatform.class);
if (anno == null) {
return true;
}
if (anno != null) {
String value = anno.value();
if (value == null || value.trim().length() == 0) {
return true;
}
String[] drivers = value.split("\\,");
for (String driver : drivers) {
try {
Class.forName(driver.trim(), false, Thread.currentThread().getContextClassLoader());
return true;
} catch (Throwable t) {
// ignore
}
}
}
return false;
}
/**
* Determines whether specified platform is the target database platform in use by the test framework.
*
* @param target
* platform name (derby, db2, oracle, etc.)
* @return true if the specified platform matches the platform in use
*/
public boolean isTargetPlatform(String target) {
String url = getPlatform();
return url != null && url.indexOf(target) != -1;
}
/**
* Returns the platform in use by the test framework
*
* @return the database platform
*/
public String getPlatform() {
return System.getProperty("platform", "derby");
}
/**
* Assert whether the Cache contains an instance of the specified class and id.
*
* @param cache
* The JPA Cache to verify
* @param clss
* The Entity type
* @param id
* ID of the entity
* @param expected
* Whether the class should be found in the cache
*/
protected void assertCached(Cache cache, Class<?> clss, Object id, boolean expected) {
if (expected) {
assertTrue(String.format("Expected %s:%s to exist in cache", clss, id), cache.contains(clss, id));
} else {
assertFalse(String.format("Expected %s:%s not to exist in cache", clss, id), cache.contains(clss, id));
}
}
protected void setTestsDisabled(boolean disable) {
synchronized (testsDisabled) {
testsDisabled = disable;
}
}
protected boolean isTestsDisabled() {
synchronized (testsDisabled) {
return testsDisabled;
}
}
protected Class<?> resolveEntityClass(JPAEntityClassEnum enumerationRef)
throws ClassNotFoundException
{
if (enumerationRef == null)
{
throw new IllegalArgumentException("Null value passed into the constructNewEntityObject method.");
}
String className = enumerationRef.getEntityClassName();
if (className == null)
{
throw new IllegalArgumentException("Enumeration toString() method implementation returned a null value.");
}
return Class.forName(className);
}
protected Object constructNewEntityObject(JPAEntityClassEnum enumerationRef)
throws ClassNotFoundException, SecurityException, NoSuchMethodException,
IllegalArgumentException, InstantiationException,
IllegalAccessException, InvocationTargetException
{
Class<?> classType = resolveEntityClass(enumerationRef);
Class<?> constructorArgSig[] = new Class[] {};
Object constructorArgs[] = new Object[] {};
Constructor<?> classConstructor = classType.getConstructor(constructorArgSig);
Object newEntity = classConstructor.newInstance(constructorArgs);
return newEntity;
}
protected Object constructNewEntityObject(Class<?> entityClass)
throws SecurityException, NoSuchMethodException,
IllegalArgumentException, InstantiationException,
IllegalAccessException, InvocationTargetException
{
Class<?> constructorArgSig[] = new Class[] {};
Object constructorArgs[] = new Object[] {};
Constructor<?> classConstructor = entityClass.getConstructor(constructorArgSig);
Object newEntity = classConstructor.newInstance(constructorArgs);
return newEntity;
}
}