/*******************************************************************************
* This file is part of OpenNMS(R).
*
* Copyright (C) 2006-2011 The OpenNMS Group, Inc.
* OpenNMS(R) is Copyright (C) 1999-2011 The OpenNMS Group, Inc.
*
* OpenNMS(R) is a registered trademark of The OpenNMS Group, Inc.
*
* OpenNMS(R) is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published
* by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* OpenNMS(R) is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenNMS(R). If not, see:
* http://www.gnu.org/licenses/
*
* For more information contact:
* OpenNMS(R) Licensing <license@opennms.org>
* http://www.opennms.org/
* http://www.opennms.com/
*******************************************************************************/
package org.opennms.netmgt.dao.db;
import java.lang.reflect.UndeclaredThrowableException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Date;
import javax.sql.DataSource;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import org.springframework.jdbc.core.simple.SimpleJdbcTemplate;
/**
* <p>For each unit test method, creates a temporary database before the unit
* test is run and destroys the database after each test (optionally leaving
* around the test database, either always or on a test failure). Tests do
* get run by default, but the system property "mock.rundbtests" can be set
* to "false" to disable this (and the database won't be touched).</p>
*
* <p>If you get errors about not being able to delete a database because
* it is in use, make sure that your tests always close their database
* connections (even in case of failures).</p>
*
* @author djgregor
*/
public class TemporaryDatabaseTestCase extends TestCase {
protected SimpleJdbcTemplate jdbcTemplate;
private static final String TEST_DB_NAME_PREFIX = "opennms_test_";
private static final String RUN_PROPERTY = "mock.rundbtests";
private static final String LEAVE_PROPERTY = "mock.leaveDatabase";
private static final String LEAVE_ON_FAILURE_PROPERTY =
"mock.leaveDatabaseOnFailure";
private static final String DRIVER_PROPERTY = "mock.db.driver";
private static final String URL_PROPERTY = "mock.db.url";
private static final String ADMIN_USER_PROPERTY = "mock.db.adminUser";
private static final String ADMIN_PASSWORD_PROPERTY = "mock.db.adminPassword";
private static final String DEFAULT_DRIVER = "org.postgresql.Driver";
private static final String DEFAULT_URL = "jdbc:postgresql://localhost:5432/";
private static final String DEFAULT_ADMIN_USER = "postgres";
private static final String DEFAULT_ADMIN_PASSWORD = "";
private static final int MAX_DATABASE_DROP_ATTEMPTS = 10;
private String m_testDatabase;
private boolean m_leaveDatabase = false;
private boolean m_leaveDatabaseOnFailure = false;
private Throwable m_throwable = null;
private boolean m_destroyed = false;
private String m_driver;
private String m_url;
private String m_adminUser;
private String m_adminPassword;
private DataSource m_dataSource;
private DataSource m_adminDataSource;
public TemporaryDatabaseTestCase() {
this(System.getProperty(DRIVER_PROPERTY, DEFAULT_DRIVER),
System.getProperty(URL_PROPERTY, DEFAULT_URL),
System.getProperty(ADMIN_USER_PROPERTY, DEFAULT_ADMIN_USER),
System.getProperty(ADMIN_PASSWORD_PROPERTY, DEFAULT_ADMIN_PASSWORD));
}
public TemporaryDatabaseTestCase(String driver, String url,
String adminUser, String adminPassword) {
m_driver = driver;
m_url = url;
m_adminUser = adminUser;
m_adminPassword = adminPassword;
}
/*
* TODO: Should we make this final, and let extending classes override
* something like afterSetUp() (like the Spring transactional tests do)
*/
@Override
protected void setUp() throws Exception {
super.setUp();
// Reset any previous test failures
setTestFailureThrowable(null);
if (!isEnabled()) {
return;
}
m_leaveDatabase = "true".equals(System.getProperty(LEAVE_PROPERTY));
m_leaveDatabaseOnFailure =
"true".equals(System.getProperty(LEAVE_ON_FAILURE_PROPERTY));
setTestDatabase(getTestDatabaseName());
setDataSource(new SimpleDataSource(m_driver, m_url + getTestDatabase(),
m_adminUser, m_adminPassword));
setAdminDataSource(new SimpleDataSource(m_driver, m_url + "template1",
m_adminUser, m_adminPassword));
createTestDatabase();
// Test connecting to test database.
Connection connection = getConnection();
connection.close();
}
private void setTestDatabase(String testDatabase) {
m_testDatabase = testDatabase;
}
@Override
protected void runTest() throws Throwable {
if (!isEnabled()) {
notifyTestDisabled(getName());
return;
}
try {
super.runTest();
} catch (Throwable t) {
setTestFailureThrowable(t);
throw t;
}
}
@Override
protected void tearDown() throws Exception {
if (isEnabled()) {
try {
destroyTestDatabase();
} catch (Throwable t) {
/*
* Do some fancy footwork to catch and reasonably report cases
* where both the test method and destroyTestDatabase throw
* exceptions. Otherwise, a test that fails in a really
* funky way may cause destroyTestDatabase() to throw an
* exception, which would mask the root cause, since JUnit
* will only report the latter exception.
*/
if (hasTestFailed()) {
throw new TestFailureAndTearDownErrorException(getTestFailureThrowable(), t);
} else {
if (t instanceof Exception) {
throw (Exception) t;
} else {
throw new UndeclaredThrowableException(t);
}
}
}
}
super.tearDown();
}
public void testNothing() {
}
protected String getTestDatabaseName() {
return TEST_DB_NAME_PREFIX + System.currentTimeMillis();
}
public String getTestDatabase() {
return m_testDatabase;
}
public void setDataSource(DataSource dataSource) {
m_dataSource = dataSource;
jdbcTemplate = new SimpleJdbcTemplate(dataSource);
}
public DataSource getDataSource() {
return m_dataSource;
}
private void setAdminDataSource(DataSource dataSource) {
m_adminDataSource = dataSource;
}
protected DataSource getAdminDataSource() {
return m_adminDataSource;
}
public Connection getConnection() throws SQLException {
return getDataSource().getConnection();
}
public String getDriver() {
return m_driver;
}
public String getUrl() {
return m_url;
}
public String getAdminUser() {
return m_adminUser;
}
public String getAdminPassword() {
return m_adminPassword;
}
public void setTestFailureThrowable(Throwable t) {
m_throwable = t;
}
public Throwable getTestFailureThrowable() {
return m_throwable;
}
public boolean hasTestFailed() {
return m_throwable != null;
}
/**
* Defaults to true.
*
* @return w00t
*/
public static boolean isEnabled() {
String property = System.getProperty(RUN_PROPERTY, "true");
return "true".equals(property);
}
public static void notifyTestDisabled(String testMethodName) {
System.out.println("Test '" + testMethodName
+ "' disabled. Set '"
+ RUN_PROPERTY
+ "' property from 'false' to 'true' to enable.");
}
private void createTestDatabase() throws Exception {
Connection adminConnection = getAdminDataSource().getConnection();
Statement st = null;
try {
st = adminConnection.createStatement();
st.execute("CREATE DATABASE " + getTestDatabase()
+ " WITH ENCODING='UNICODE'");
} finally {
if (st != null) {
st.close();
}
adminConnection.close();
}
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
try {
destroyTestDatabase();
} catch(Throwable e) {
e.printStackTrace();
}
}
});
}
private void destroyTestDatabase() throws Exception {
if (m_destroyed) {
// database already destroyed
return;
}
if (m_leaveDatabase
|| (m_leaveDatabaseOnFailure && hasTestFailed())) {
System.err.println("Not dropping database '" + getTestDatabase()
+ "' for test '" + getName() + "'");
return;
}
/*
* Sleep before destroying the test database because PostgreSQL
* doesn't seem to notice immediately clients have disconnected. Yeah,
* it's a hack.
*/
Thread.sleep(100);
Connection adminConnection = getAdminDataSource().getConnection();
try {
for (int dropAttempt = 0; dropAttempt < MAX_DATABASE_DROP_ATTEMPTS; dropAttempt++) {
Statement st = null;
try {
st = adminConnection.createStatement();
st.execute("DROP DATABASE " + getTestDatabase());
break;
} catch (SQLException e) {
if ((dropAttempt + 1) >= MAX_DATABASE_DROP_ATTEMPTS) {
final String message = "Failed to drop test database on last attempt " + (dropAttempt + 1) + ": " + e;
System.err.println(new Date().toString() + ": " + message);
TemporaryDatabase.dumpThreads();
SQLException newException = new SQLException(message);
newException.initCause(e);
throw newException;
} else {
System.err.println(new Date().toString() + ": Failed to drop test database on attempt " + (dropAttempt + 1) + ": " + e);
Thread.sleep(1000);
}
} finally {
if (st != null) {
st.close();
st = null;
}
}
}
} finally {
/*
* Since we are already going to be throwing an exception at this
* point, print any further errors to stdout so we don't mask
* the first failure.
*/
try {
adminConnection.close();
} catch (SQLException e) {
System.err.println("Error closing administrative database "
+ "connection after attempting to drop "
+ "test database");
e.printStackTrace();
}
/*
* Sleep after disconnecting from template1, otherwise creating
* a new test database in future tests may fail. Man, I hate this.
*/
Thread.sleep(100);
}
m_destroyed = true;
}
public void executeSQL(String command) {
executeSQL(new String[] { command });
}
public void executeSQL(String[] commands) {
Connection connection = null;
Statement st = null;
try {
connection = getConnection();
} catch (Throwable e) {
fail("Could not get connection", e);
}
try {
try {
st = connection.createStatement();
} catch (SQLException e) {
fail("Could not create statement", e);
}
for (String command : commands) {
try {
st.execute(command);
} catch (SQLException e) {
fail("Could not execute statement: '" + command + "'", e);
}
}
} finally {
/*
* Since we are already going to be throwing an exception at this
* point, print any further errors to stdout so we don't mask
* the first failure.
*/
if (st != null) {
try {
st.close();
} catch (SQLException e) {
System.err.println("Could not close statement in executeSQL");
e.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
System.err.println("Could not close connection in executeSQL");
e.printStackTrace();
}
}
}
}
public void fail(String message, Throwable t) throws AssertionFailedError {
AssertionFailedError e = new AssertionFailedError(message + ": "
+ t.getMessage());
e.initCause(t);
throw e;
}
/**
* Represents a failure both in a unit test method (e.g.: testFoo) and
* in the tearDown method.
*
* @author djgregor
*/
public class TestFailureAndTearDownErrorException extends Exception {
private static final long serialVersionUID = -5664844942506660064L;
private Throwable m_tearDownError;
public TestFailureAndTearDownErrorException(Throwable testFailure,
Throwable tearDownError) {
super(testFailure);
m_tearDownError = tearDownError;
}
public String toString() {
return super.toString()
+ "\nAlso received error on tearDown: "
+ m_tearDownError.toString();
}
}
public SimpleJdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
}