SqlTest.java

/***************************************************************************
   Copyright 2014 Emily Estes

   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 net.metanotion.sqltest;


import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.HashMap;
import java.util.Map;
import javax.inject.Named;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.metanotion.io.JavaFileSystem;
import net.metanotion.scripting.StructManager;
import net.metanotion.sql.DbUtil;
import net.metanotion.sql.SchemaGenerator;
import net.metanotion.sqlc.SQLObjectServer;
import net.metanotion.util.ArgsToStruct;
import net.metanotion.util.Description;

/** This class represents a simple SQL "testing" framework based on the idea of running a sequence of SQL statements
and queries and comparing the output to it's expected output. The test program works by via 5 command line arguments:
<ol>
	<li>A Test harness script(described later)</li>
	<li>A SQLC class path</li>
	<li>JDBC DB connection string</li>
	<li>JDBC username</li>
	<li>JDBC password</li>
</ol>
*/
public final class SqlTest {
	private static final Logger logger = LoggerFactory.getLogger(SqlTest.class);

	/** This class represents the command line options used by SqlTest. */
	public static final class Args {
		@Description("The JDBC connection URL for the database server.")
		public String url;
		@Description("The username for the database server.")
		public String user;
		@Description("The password for the database server.")
		public String pass;
		@Description("The name of the database to optionally create and then test.")
		public String database;
		@Description("If this option is set, create the database first.")
		public boolean create;
		@Description("If this option is set along with -create, attempt to drop the database if it already exists "
			+ "before creating the database.")
		public boolean dropFirst;
		@Description("Attempt to create the schema specified in the schema search path.")
		public boolean createSchema;
		@Description("Set the schema search path to this schema before running the test and running the database DDL.")
		public String schema;

		@Description("Load the class specified and generate the database DDL from the SchemaGenerator instance "
			+ "obtained from calling the zero argument static method 'schemaFactory()'")
		@Named("class") public String klazz;
		@Description("Generate the database DDL from the classpath resource specified.")
		public String resource;
		@Description("Generate the database DDL from the SQL contained in the file specified.")
		public File file;

		@Description("The test transcript to execute after any database initializations requested are performed.")
		public String transcript;

		@Description("The 'class path' to search for any SqlC classes imported by the test transcript.")
		public String sqlc;

		@Description("If this switch is set, if the test transcript fails, call System.exit(-1) and terminate the "
			+ "Java process. If this is not set(the default) let the exception generated by test failure propagate. "
			+ "This allows SqlTest to be ran as a net.metanotion.util.TestRunner test, and otherwise be more "
			+ "flexible. If ran standlone though, System.exit(-1) allows it cause 'build failures' as part a scripted "
			+ "set of test routines.")
		public boolean exitOnFail = false;
	}

	/** Run the SqlTest application.
		@param args The command line arguments for the applcation.
		@throws Exception if the test fails unless the -exitOnFail command line switch was set, then it exits with an
			error status (-1).
	*/
	public static void main(final String[] args) throws Exception {
		final Args arg = (new ArgsToStruct<>(Args.class)).getInstance(args);

		try {
			// Set up the database if the command line arguments ask for it.
			if(arg.create) {
				try (final Connection conn = DriverManager.getConnection(arg.url, arg.user, arg.pass)) {
					DbUtil.createDatabase(conn, arg.database, arg.user, arg.dropFirst);
				}
			}

			final SchemaGenerator schemaGen = (arg.klazz != null) ? DbUtil.reflectSchemaGenerator(Class.forName(arg.klazz)) :
				((arg.file != null) ? DbUtil.fileSchemaGenerator(arg.file) :
					((arg.resource != null) ? DbUtil.resourceSchemaGenerator(SqlTest.class.getClassLoader(), arg.resource) :
						null));

			try (final Connection conn = DriverManager.getConnection(arg.url + arg.database, arg.user, arg.pass)) {
				if(arg.schema != null) {
					if(arg.createSchema) {
						DbUtil.createSchema(conn, arg.schema, false);
					}
					DbUtil.setSearchPath(conn, arg.schema);
				}
				if(schemaGen != null) {
					DbUtil.runSchema(conn, schemaGen);
				}

				try (final InputStream testTranscript = new FileInputStream(arg.transcript)) {
					final StructManager sm = new StructManager();
					final SQLObjectServer os = (arg.sqlc != null) ? new SQLObjectServer(new JavaFileSystem(arg.sqlc), sm) : null;
					runTests(conn, testTranscript, os);
				}
			}
		} catch (final Throwable ex) {
			if(arg.exitOnFail) {
				System.out.println("Failed test");
				ex.printStackTrace();
				System.exit(-1);
			} else {
				throw ex;
			}
		}
	}

	/** Parse a SQL test transcript and execute the tests against the database provided.
		@param conn The database connection to use for the tests.
		@param testTranscript The test transcript to run.
		@param os The ObjectServer that will provide any SQLC scripts imported.
		@throws Exception if the any of the tests fail.
	*/
	public static void runTests(final Connection conn,
			final InputStream testTranscript,
			final SQLObjectServer os) throws Exception {
		final Map<String,Value> environment = new HashMap<>();
		final Map<String,String> imports = new HashMap<>();
		final Iterable<Test> tests = (new Parser()).load(imports, testTranscript);
		for(final Test test: tests) {
			test.eval(conn, os, environment);
		}
	}
}