/* Copyright (c) 2001-2014, The HSQL Development Group
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the HSQL Development Group nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
* OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.hsqldb.test;
import java.io.
File;
import java.io.
FilenameFilter;
import java.io.
IOException;
import java.sql.
Connection;
import java.sql.
DriverManager;
import java.sql.
ResultSet;
import java.sql.
SQLException;
import java.sql.
Statement;
import java.text.
SimpleDateFormat;
import org.hsqldb.lib.tar.
DbBackupMain;
import org.hsqldb.lib.tar.
TarMalformatException;
import junit.
framework.
Test;
import junit.
framework.
TestSuite;
import junit.
framework.
TestCase;
public class
TestDbBackup extends
TestCase {
public
TestDbBackup() throws
IOException,
SQLException {}
static protected
File baseDir =
new
File(
System.
getProperty("java.io.tmpdir"),
"TestDbBackup-" +
System.
getProperty("user.name"));
static {
try {
Class.
forName("org.hsqldb.jdbc.JDBCDriver");
} catch (
ClassNotFoundException cnfe) {
throw new
RuntimeException(
"<clinit> failed. JDBC Driver class not in CLASSPATH");
}
}
/**
* Individual test methods may or may not need a Connection.
* If they do, they run setupConn() then use 'conn', and it will be
* automatically closed by the tearDown() method.
*
* @see #tearDown()
*/
protected void
setupConn(
String id) throws
SQLException {
conn =
getConnection(
id);
alreadyShut = false;
}
protected void
shutdownAndCloseConn() throws
SQLException {
if (
conn == null) {
return;
}
if (!
alreadyShut) {
conn.
createStatement().
executeUpdate("SHUTDOWN");
alreadyShut = true;
}
if (
verbose) {
System.
err.
println("Shut down 'db1'");
}
conn.
close();
conn = null;
}
/**
* Use setupConn() to set up this Connection for just this individual test.
*
* @see #setupConn(String)
*/
protected
Connection conn = null;
protected boolean
alreadyShut = false;
/**
* Remove the specified directory and all of it's descendants.
*
* @throws IOException if unable to completely remove the specified dir
*/
protected void
rmR(
File dir) throws
IOException {
if (!
dir.
exists()) {
throw new
IOException("Specified dir does not exist: "
+
dir.
getAbsolutePath());
}
File[]
children =
dir.
listFiles();
for (int
i = 0;
i <
children.length;
i++) {
if (
children[
i].
isDirectory()) {
rmR(
children[
i]);
} else if (!
children[
i].
delete()) {
throw new
IOException("Failed to remove '"
+
children[
i].
getAbsolutePath() + "'");
}
}
if (!
dir.
delete()) {
throw new
IOException("Failed to remove '" +
dir.
getAbsolutePath()
+ "'");
}
}
/**
* Accommodate JUnit's test-runner conventions.
*/
public
TestDbBackup(
String s) throws
IOException,
SQLException {
super(
s);
}
/**
* JUnit convention for cleanup.
*
* Called after each test*() method.
*/
protected void
tearDown() throws
IOException,
SQLException {
if (
baseDir.
exists()) {
rmR(
baseDir);
if (
verbose) {
System.
err.
println("Tore down");
}
}
}
static boolean
verbose =
Boolean.
getBoolean("VERBOSE");
/**
* Specifically, this method creates and populates "db1", then closes it.
*
* Invoked before each test*() invocation by JUnit.
*/
protected void
setUp() throws
IOException,
SQLException {
if (
verbose) {
System.
err.
println("Set-upping");
}
if (
baseDir.
exists()) {
throw new
IOException("Please wipe out work directory '" +
baseDir
+ ", which is probably left over from an "
+ "aborted test run");
}
try {
setupConn("db1");
Statement st =
conn.
createStatement();
st.
executeUpdate("CREATE TABLE t(i int);");
st.
executeUpdate("INSERT INTO t values(34);");
conn.
commit();
} catch (
SQLException se) {}
finally {
shutdownAndCloseConn();
}
}
/**
* Make sure to close after using the returned connection
* (like in a finally block).
*/
protected
Connection getConnection(
String id) throws
SQLException {
Connection c =
DriverManager.
getConnection("jdbc:hsqldb:file:"
+
baseDir.
getAbsolutePath() + '/' +
id + "/dbfile", "SA", "");
if (
verbose) {
System.
err.
println("Opening JDBC URL '" + "jdbc:hsqldb:file:"
+
baseDir.
getAbsolutePath() + '/' +
id
+ "/dbfile");
}
c.
setAutoCommit(false);
return
c;
}
/**
* This method allows to easily run this unit test independent of the other
* unit tests, and without dealing with Ant or unrelated test suites.
*/
public static void
main(
String[]
sa) {
if (
sa.length > 0 && !
sa[
sa.length - 1].
equals("-g")) {
TestDbBackup.
baseDir = new
File(
sa[0]);
if (
baseDir.
exists()) {
throw new
IllegalArgumentException(
"If you specify a work directory, it must not exist "
+ "yet. (This makes it much easier for us to clean up "
+ "after ourselves).");
}
System.
err.
println("Using user-specified base dir: "
+
baseDir.
getAbsolutePath());
}
junit.
textui.
TestRunner runner = new
junit.
textui.
TestRunner();
junit.
framework.
TestResult result =
runner.
run(
runner.
getTest(
TestDbBackup.class.
getName()));
System.
exit(
result.
wasSuccessful() ? 0
: 1);
}
public void
testSanity() throws
SQLException {
try {
setupConn("db1");
ResultSet rs =
conn.
createStatement().
executeQuery("SELECT * FROM t;");
rs.
next();
assertEquals("Wrong table 't' contents", 34,
rs.
getInt("i"));
} finally {
shutdownAndCloseConn();
}
}
public void
testBasicBackup()
throws
SQLException,
IOException,
TarMalformatException {
mainBackupAndRestore("basic.tar");
}
public void
testGzip()
throws
SQLException,
IOException,
TarMalformatException {
mainBackupAndRestore("compressed.tar.gz");
}
/**
* Test all forms of online backups with explicit filenames.
*/
public void
testOnlineBackup()
throws
SQLException,
IOException,
TarMalformatException {
onlineBackupAndRestore("online.tar", true, false, "db11");
onlineBackupAndRestore("online.tar.gz", false, true, "db12");
onlineBackupAndRestore("online.tgz", false, true, "db13");
}
public void
onlineBackupAndRestore(
String baseTarName, boolean
populate,
boolean
compress,
String restoreDest)
throws
SQLException,
IOException,
TarMalformatException {
try {
setupConn("db1");
conn.
createStatement().
executeUpdate("DELETE FROM t");
// For this case, we wipe the data that we so carefully set up,
// so that we can call this method repeatedly without worrying
// about left-over data from a previous run.
conn.
commit();
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(1)");
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(2)");
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(3)");
conn.
commit();
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(4)");
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(5)");
conn.
createStatement().
executeUpdate("BACKUP DATABASE TO '"
+
baseDir.
getAbsolutePath()
+ '/' +
baseTarName
+ "' BLOCKING"
+ (
compress ? ""
: " NOT COMPRESSED"));
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(6)");
conn.
commit();
conn.
createStatement().
executeUpdate("SHUTDOWN");
alreadyShut = true;
if (
verbose) {
System.
err.
println("Shut down 'db1'");
}
} finally {
shutdownAndCloseConn();
}
File destDir = new
File(
baseDir,
restoreDest);
if (!
destDir.
mkdir()) {
throw new
IOException("Failed to make new dir. to restore to: "
+
destDir.
getAbsolutePath());
}
DbBackupMain.
main(new
String[] {
"--extract",
baseDir.
getAbsolutePath() + '/' +
baseTarName,
destDir.
getAbsolutePath()
});
try {
setupConn(
restoreDest);
conn.
createStatement().
executeUpdate("ROLLBACK");
ResultSet rs =
conn.
createStatement().
executeQuery(
"SELECT count(*) c FROM t;");
rs.
next();
// 3 committed, 5 uncommited before saving:
assertEquals("Wrong table 't' contents", 5,
rs.
getInt("c"));
} finally {
shutdownAndCloseConn();
}
}
public void
mainBackupAndRestore(
String baseTarName)
throws
SQLException,
IOException,
TarMalformatException {
DbBackupMain.
main(new
String[] {
"--save",
baseDir.
getAbsolutePath() + '/' +
baseTarName,
baseDir.
getAbsolutePath() + "/db1/dbfile"
});
File destDir = new
File(
baseDir, "mainrestored");
if (!
destDir.
mkdir()) {
throw new
IOException("Failed to make new dir. to restore to: "
+
destDir.
getAbsolutePath());
}
DbBackupMain.
main(new
String[] {
"--extract",
baseDir.
getAbsolutePath() + '/' +
baseTarName,
destDir.
getAbsolutePath()
});
try {
setupConn("mainrestored");
ResultSet rs =
conn.
createStatement().
executeQuery("SELECT * FROM t;");
rs.
next();
assertEquals("Wrong table 't' contents", 34,
rs.
getInt("i"));
} finally {
shutdownAndCloseConn();
}
}
public void
testMainAlreadyOpen()
throws
SQLException,
IOException,
TarMalformatException {
try {
setupConn("db1");
try {
DbBackupMain.
main(new
String[] {
"--save",
baseDir.
getAbsolutePath() + "/mainOpen.tar",
baseDir.
getAbsolutePath() + "/db1/dbfile"
});
} catch (
IllegalStateException ioe) {
return;
}
} finally {
shutdownAndCloseConn();
}
fail("Backup from main() did not throw even though DB is open");
}
/**
* Test that bad explicit filenames are rejected for onilne backups.
*/
public void
testTarFileNames()
throws
SQLException,
IOException,
TarMalformatException {
boolean
caught;
try {
setupConn("db1");
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(2)");
conn.
commit();
// #1: COMPRESSED -> no-extension
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad' BLOCKING COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested compression "
+ "to file '/x/bad'");
}
// #2: NOT COMPRESSED -> no-extension
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad' BLOCKING NOT COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested "
+ "no-compression to file '/x/bad'");
}
// #3: COMPRESSED -> *.txt
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.txt' BLOCKING COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested compression "
+ "to file '/x/bad.txt'");
}
// #4: NOT COMPRESSED -> *.txt
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.txt' BLOCKING NOT COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested "
+ "no-compression to file '/x/bad.txt'");
}
// #5: DEFAULT -> *.tar
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.tar' BLOCKING");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested default "
+ "to file '/x/bad.tar'");
}
// #6: COMPRESSION -> *.tar
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.tar' BLOCKING COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested compression "
+ "to file '/x/bad.tar'");
}
// #7: NOT COMPRESSED -> *.tar.gz
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.tar.gz' BLOCKING NOT COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested "
+ "non-compression to file '/x/bad.tar.gz'");
}
// #8: NOT COMPRESSED -> *.tgz
caught = false;
try {
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/x/bad.tgz' BLOCKING NOT COMPRESSED");
} catch (
SQLException se) {
caught = true;
}
if (!
caught) {
fail("BACKUP did not throw even though requested "
+ "non-compression to file '/x/bad.tgz'");
}
// Finally run a test to ensure that the attempts above didn't
// fail for some unexpected reason.
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/positivetest.tar' BLOCKING NOT COMPRESSED");
} finally {
shutdownAndCloseConn();
}
}
/**
* Test that correct DB names are generated when user supplies just a
* directory.
*
* N.b. This test may not work right if tests are run at midnight.
* This limitation will be removed once we can update the FilenameFilters
* with Java 4's java.util.regex.
*/
public void
testAutoNaming()
throws
SQLException,
IOException,
TarMalformatException {
boolean
caught;
int
fileCount;
try {
setupConn("db1");
conn.
createStatement().
executeUpdate("INSERT INTO t VALUES(2)");
conn.
commit();
fileCount =
baseDir.
listFiles(
autoTarFilenameFilter).length;
if (
fileCount != 0) {
throw new
IllegalStateException(
Integer.
toString(
fileCount)
+ " auto-tar files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' before starting testAutoNaming");
}
fileCount =
baseDir.
listFiles(
autoTarGzFilenameFilter).length;
if (
fileCount != 0) {
throw new
IllegalStateException(
Integer.
toString(
fileCount)
+ " auto-tar.gz files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' before starting testAutoNaming");
}
conn.
createStatement().
executeUpdate(
"BACKUP DATABASE TO '" +
baseDir.
getAbsolutePath()
+ "/' BLOCKING NOT COMPRESSED");
fileCount =
baseDir.
listFiles(
autoTarFilenameFilter).length;
if (
fileCount != 1) {
fail(
Integer.
toString(
fileCount)
+ " auto-tar files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' after writing a non-compressed backup");
}
fileCount =
baseDir.
listFiles(
autoTarGzFilenameFilter).length;
if (
fileCount != 0) {
fail(
Integer.
toString(
fileCount)
+ " auto-tar.gz files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' after writing a non-compressed backup");
}
conn.
createStatement().
executeUpdate("BACKUP DATABASE TO '"
+
baseDir.
getAbsolutePath()
+ "/' BLOCKING COMPRESSED");
fileCount =
baseDir.
listFiles(
autoTarFilenameFilter).length;
if (
fileCount != 1) {
fail(
Integer.
toString(
fileCount)
+ " auto-tar files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' after writing both backups");
}
fileCount =
baseDir.
listFiles(
autoTarGzFilenameFilter).length;
if (
fileCount != 1) {
fail(
Integer.
toString(
fileCount)
+ " auto-tar.gz files exist in baseDir '"
+
baseDir.
getAbsolutePath()
+ "' after writing a compressed backup");
}
} finally {
shutdownAndCloseConn();
}
}
public static
Test suite() throws
IOException,
SQLException {
TestSuite newSuite = new
TestSuite();
newSuite.
addTest(new
TestDbBackup("testSanity"));
newSuite.
addTest(new
TestDbBackup("testBasicBackup"));
newSuite.
addTest(new
TestDbBackup("testMainAlreadyOpen"));
newSuite.
addTest(new
TestDbBackup("testGzip"));
newSuite.
addTest(new
TestDbBackup("testOnlineBackup"));
newSuite.
addTest(new
TestDbBackup("testTarFileNames"));
newSuite.
addTest(new
TestDbBackup("testAutoNaming"));
return
newSuite;
}
private
String autoMiddlingString =
"-" + new
SimpleDateFormat("yyyyMMdd").
format(new java.util.
Date())
+ 'T';
FilenameFilter autoTarFilenameFilter = new
FilenameFilter() {
private
String suffixFormat = "-yyyyMMddTHHmmss.tar";
public boolean
accept(
File dir,
String name) {
if (
name.
length() <
suffixFormat.
length() + 1) {
// Require variable name length >= 1 char
return false;
}
int
suffixPos =
name.
length() -
suffixFormat.
length();
// Would like to use Java 1.4's java.util.regex here.
return
name.
endsWith(".tar") &&
name.
substring(
suffixPos,
suffixPos +
autoMiddlingString.
length()).
equals(
autoMiddlingString);
}
};
FilenameFilter autoTarGzFilenameFilter = new
FilenameFilter() {
private
String suffixFormat = "-yyyyMMddTHHmmss.tar.gz";
public boolean
accept(
File dir,
String name) {
if (
name.
length() <
suffixFormat.
length() + 1) {
// Require variable name length >= 1 char
return false;
}
int
suffixPos =
name.
length() -
suffixFormat.
length();
// Would like to use Java 1.4's java.util.regex here.
return
name.
endsWith(".tar.gz") &&
name.
substring(
suffixPos,
suffixPos +
autoMiddlingString.
length()).
equals(
autoMiddlingString);
}
};
}