/* Copyright (c) 2001-2010, 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.DbBackup; import org.hsqldb.lib.tar.TarMalformatException; import junit.framework.Test; import junit.framework.TestSuite; public class TestDbBackup extends junit.framework.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. */ static public 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()); } DbBackup.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 { DbBackup.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()); } DbBackup.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 { DbBackup.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(); } } static public 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); } }; }