diff --git a/common/source/java/ch/systemsx/cisd/common/db/SqlUnitTestRunner.java b/common/source/java/ch/systemsx/cisd/common/db/SqlUnitTestRunner.java index cccd691d2a6e927c049984b157ad39dd177e1f77..f92743817eb2f7be2e231b496382d70277702173 100644 --- a/common/source/java/ch/systemsx/cisd/common/db/SqlUnitTestRunner.java +++ b/common/source/java/ch/systemsx/cisd/common/db/SqlUnitTestRunner.java @@ -17,18 +17,48 @@ package ch.systemsx.cisd.common.db; import java.io.File; +import java.io.FileFilter; import java.io.FilenameFilter; import java.io.PrintWriter; +import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import ch.systemsx.cisd.common.utilities.FileUtilities; import ch.systemsx.cisd.common.utilities.OSUtilities; /** + * Runner of SQL Unit tests. Needs an implementation of {@link ISqlScriptExecutor} to do the actual tests. + * The runner executes all test scripts found in the specified test scripts folder. The folder should have the + * following structure + * <pre> + * <<i>test script folder</i>> + * <<i>1. test case</i>> + * buildup.sql + * 1=<<i>first test</i>>.sql + * 2=<<i>second test</i>>.sql + * ... + * teardown.sql + * <<i>2. test case</i>> + * ... + * ... + * </pre> + * The test cases are executed in lexicographical order of their name. For each test case <code>buildup.sql</code> + * will be executed first. The test scripts follow the naming schema + * <pre> + * <<i>decimal number</i>>=<<i>test name</i>>.sql + * </pre> + * They are executed in ascending order of their numbers. Finally <code>teardown.sql</code> is executed. + * If execution of <code>buildup.sql</code> failed all test scripts and the tear down script are skipped. + * Note that <code>buildup.sql</code> and <code>teardown.sql</code> are optional. + * <p> + * A script fails if its execution throws an exception. Its innermost cause (usually a {@link SQLException}) will + * be recorded together with the name of the test case and the script. All failed scripts will be recorded. + * <p> + * The runner throws an {@link AssertionError} if at least one script failed. * - * * @author Franz-Josef Elmer */ public class SqlUnitTestRunner @@ -75,23 +105,37 @@ public class SqlUnitTestRunner private final ISqlScriptExecutor executor; private final PrintWriter writer; + /** + * Creates an instance for the specified SQL script executor and writer. + * + * @param executor SQL script executor. + * @param writer Writer used to monitor running progress by printing test and test case names. + */ public SqlUnitTestRunner(ISqlScriptExecutor executor, PrintWriter writer) { + assert executor != null : "Undefined SQL script executor."; + assert writer != null : "Undefined writer."; + this.executor = executor; this.writer = writer; } - public void run(File testScriptsFolder) + /** + * Executes all scripts in the specified folder. Does nothing if it does not exists or if it is empty. + * + * @throws AssertionError if at least one script failed. + */ + public void run(File testScriptsFolder) throws AssertionError { - if (testScriptsFolder.exists() == false) + if (testScriptsFolder == null || testScriptsFolder.exists() == false) { return; // no tests } - File[] testCases = testScriptsFolder.listFiles(new FilenameFilter() + File[] testCases = testScriptsFolder.listFiles(new FileFilter() { - public boolean accept(File dir, String name) + public boolean accept(File pathname) { - return name.startsWith(".") == false; + return pathname.isDirectory() && pathname.getName().startsWith(".") == false; } }); Arrays.sort(testCases); @@ -105,7 +149,9 @@ public class SqlUnitTestRunner { if (result.isOK() == false) { - builder.append("Test script ").append(getName(result.getTestScript())).append(" failed because of "); + File testScript = result.getTestScript(); + builder.append("Script '").append(testScript.getName()).append("' of test case '"); + builder.append(testScript.getParentFile().getName()).append("' failed because of "); builder.append(result.getThrowable()).append(OSUtilities.LINE_SEPARATOR); } } @@ -118,20 +164,19 @@ public class SqlUnitTestRunner private void runTestCase(File testCaseFolder, List<TestResult> results) { - writer.println("====== Test case " + testCaseFolder.getName() + " ======"); + writer.println("====== Test case: " + testCaseFolder.getName() + " ======"); File buildupFile = new File(testCaseFolder, "buildup.sql"); if (buildupFile.exists()) { - results.add(runScript(buildupFile)); - } - File[] testScripts = testCaseFolder.listFiles(new FilenameFilter() + TestResult result = runScript(buildupFile); + results.add(result); + if (result.isOK() == false) { - public boolean accept(File dir, String name) - { - return name.length() > 1 && name.charAt(1) == '='; - } - }); - Arrays.sort(testScripts); + writer.println(" script failed: skip test scripts and teardown script."); + return; + } + } + File[] testScripts = getTestScripts(testCaseFolder); for (File testScript : testScripts) { results.add(runScript(testScript)); @@ -142,6 +187,25 @@ public class SqlUnitTestRunner results.add(runScript(teardownFile)); } } + + private File[] getTestScripts(File testCaseFolder) + { + File[] testScripts = testCaseFolder.listFiles(new FilenameFilter() + { + public boolean accept(File dir, String name) + { + return getNumber(name) >= 0; + } + }); + Arrays.sort(testScripts, new Comparator<File>() + { + public int compare(File f1, File f2) + { + return getNumber(f1.getName()) - getNumber(f2.getName()); + } + }); + return testScripts; + } private TestResult runScript(File scriptFile) { @@ -160,9 +224,19 @@ public class SqlUnitTestRunner } } - private String getName(File testScript) + private int getNumber(String name) { - return testScript.getParentFile().getName() + File.separatorChar + testScript.getName(); + int index = name.indexOf('='); + if (index < 0) + { + return -1; + } + try + { + return Integer.parseInt(name.substring(0, index)); + } catch (NumberFormatException ex) + { + return -1; + } } - } \ No newline at end of file diff --git a/common/sourceTest/java/ch/systemsx/cisd/common/db/SqlUnitTestRunnerTest.java b/common/sourceTest/java/ch/systemsx/cisd/common/db/SqlUnitTestRunnerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..8599c31657607f71c833b7b29ceb5c5e60d98e0e --- /dev/null +++ b/common/sourceTest/java/ch/systemsx/cisd/common/db/SqlUnitTestRunnerTest.java @@ -0,0 +1,239 @@ +/* + * Copyright 2007 ETH Zuerich, CISD + * + * Licensed 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 ch.systemsx.cisd.common.db; + +import static org.testng.AssertJUnit.assertEquals; +import static org.testng.AssertJUnit.fail; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import org.apache.commons.io.FileUtils; +import org.jmock.Mockery; +import org.jmock.api.Action; +import org.jmock.internal.Cardinality; +import org.jmock.internal.InvocationExpectationBuilder; +import org.jmock.lib.action.ThrowAction; +import org.jmock.lib.action.VoidAction; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import ch.systemsx.cisd.common.utilities.FileUtilities; +import ch.systemsx.cisd.common.utilities.OSUtilities; + +/** + * + * + * @author Franz-Josef Elmer + */ +public class SqlUnitTestRunnerTest +{ + private static final File TEST_SCRIPTS_FOLDER = new File("temporary_test_scripts_folder"); + + private Mockery context; + private ISqlScriptExecutor executor; + private StringWriter monitor; + private SqlUnitTestRunner testRunner; + + @BeforeMethod + public void setup() + { + TEST_SCRIPTS_FOLDER.mkdir(); + context = new Mockery(); + executor = context.mock(ISqlScriptExecutor.class); + monitor = new StringWriter(); + testRunner = new SqlUnitTestRunner(executor, new PrintWriter(monitor)); + } + + @AfterMethod + public void teardown() + { + assert FileUtilities.deleteRecursively(TEST_SCRIPTS_FOLDER); + // To following line of code should also be called at the end of each test method. + // Otherwise one do not known which test failed. + context.assertIsSatisfied(); + } + + @Test + public void testNullFolder() + { + testRunner.run(null); + assertEquals("", monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testNotExistingFolder() + { + testRunner.run(new File("blabla")); + assertEquals("", monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testEmptyFolder() + { + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("", monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testNonEmptyFolderButNoTestCases() throws IOException + { + assert new File(TEST_SCRIPTS_FOLDER, "some file").createNewFile(); + assert new File(TEST_SCRIPTS_FOLDER, ".folder").mkdir(); + + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("", monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testEmptyTestCase() + { + assert new File(TEST_SCRIPTS_FOLDER, "my test case").mkdir(); + + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("====== Test case: my test case ======" + OSUtilities.LINE_SEPARATOR, monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testNonEmptyTestCaseButNoScripts() throws IOException + { + File testCaseFolder = new File(TEST_SCRIPTS_FOLDER, "my test case"); + assert testCaseFolder.mkdir(); + assert new File(testCaseFolder, "blabla.sql").createNewFile(); + assert new File(testCaseFolder, "folder").mkdir(); + + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("====== Test case: my test case ======" + OSUtilities.LINE_SEPARATOR, monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testTestCaseWithNoTestsButBuildupScript() throws IOException + { + File testCaseFolder = new File(TEST_SCRIPTS_FOLDER, "my test case"); + assert testCaseFolder.mkdir(); + createScriptPrepareExecutor(new File(testCaseFolder, "buildup.sql"), "-- build up\n", null); + + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("====== Test case: my test case ======" + OSUtilities.LINE_SEPARATOR + + " execute script buildup.sql" + OSUtilities.LINE_SEPARATOR, monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testTestCaseWithFailingBuildupScript() throws IOException + { + File testCaseFolder = new File(TEST_SCRIPTS_FOLDER, "my test case"); + assert testCaseFolder.mkdir(); + RuntimeException runtimeException = new RuntimeException("42"); + createScriptPrepareExecutor(new File(testCaseFolder, "buildup.sql"), "-- build up\n", runtimeException); + + try + { + testRunner.run(TEST_SCRIPTS_FOLDER); + fail("AssertionError expected"); + } catch (AssertionError e) + { + assertEquals("Script 'buildup.sql' of test case 'my test case' failed because of " + + runtimeException + OSUtilities.LINE_SEPARATOR, e.getMessage()); + } + assertEquals("====== Test case: my test case ======" + OSUtilities.LINE_SEPARATOR + + " execute script buildup.sql" + OSUtilities.LINE_SEPARATOR + + " script failed: skip test scripts and teardown script." + OSUtilities.LINE_SEPARATOR, + monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testOrderOfExecutingTestScripts() throws IOException + { + File testCaseFolder = new File(TEST_SCRIPTS_FOLDER, "my test case"); + assert testCaseFolder.mkdir(); + RuntimeException runtimeException = new RuntimeException("42"); + createScriptPrepareExecutor(new File(testCaseFolder, "9=b.sql"), "Select 9\n", runtimeException); + FileUtils.writeStringToFile(new File(testCaseFolder, "abc=abc.sql"), "Select abc\n"); + createScriptPrepareExecutor(new File(testCaseFolder, "10=c.sql"), "Select 10\n", null); + createScriptPrepareExecutor(new File(testCaseFolder, "1=a.sql"), "Select 1\n", null); + + try + { + testRunner.run(TEST_SCRIPTS_FOLDER); + fail("AssertionError expected"); + } catch (AssertionError e) + { + assertEquals("Script '9=b.sql' of test case 'my test case' failed because of " + + runtimeException + OSUtilities.LINE_SEPARATOR, e.getMessage()); + } + assertEquals("====== Test case: my test case ======" + OSUtilities.LINE_SEPARATOR + + " execute script 1=a.sql" + OSUtilities.LINE_SEPARATOR + + " execute script 9=b.sql" + OSUtilities.LINE_SEPARATOR + + " execute script 10=c.sql" + OSUtilities.LINE_SEPARATOR, + monitor.toString()); + + context.assertIsSatisfied(); + } + + @Test + public void testOrderOfExecutingTestCases() throws IOException + { + assert new File(TEST_SCRIPTS_FOLDER, "TC002").mkdir(); + File testCaseFolder1 = new File(TEST_SCRIPTS_FOLDER, "TC001"); + assert testCaseFolder1.mkdir(); + createScriptPrepareExecutor(new File(testCaseFolder1, "buildup.sql"), "create table\n", null); + createScriptPrepareExecutor(new File(testCaseFolder1, "1=a.sql"), "Select 1\n", null); + createScriptPrepareExecutor(new File(testCaseFolder1, "2=b.sql"), "Select 2\n", null); + createScriptPrepareExecutor(new File(testCaseFolder1, "teardown.sql"), "drop table\n", null); + + testRunner.run(TEST_SCRIPTS_FOLDER); + assertEquals("====== Test case: TC001 ======" + OSUtilities.LINE_SEPARATOR + + " execute script buildup.sql" + OSUtilities.LINE_SEPARATOR + + " execute script 1=a.sql" + OSUtilities.LINE_SEPARATOR + + " execute script 2=b.sql" + OSUtilities.LINE_SEPARATOR + + " execute script teardown.sql" + OSUtilities.LINE_SEPARATOR + + "====== Test case: TC002 ======" + OSUtilities.LINE_SEPARATOR, + monitor.toString()); + + context.assertIsSatisfied(); + } + + private void createScriptPrepareExecutor(File scriptFile, String script, Throwable throwable) throws IOException + { + FileUtils.writeStringToFile(scriptFile, script); + InvocationExpectationBuilder builder = new InvocationExpectationBuilder(); + builder.setCardinality(new Cardinality(1, 1)); + builder.of(executor).execute(script); + Action action = throwable == null ? new VoidAction() : new ThrowAction(throwable); + context.addExpectation(builder.toExpectation(action)); + } + +}