From 92f9280af068f0fe463fc40890b0d0fc85811f30 Mon Sep 17 00:00:00 2001 From: jeb228 Date: Mon, 17 May 2010 14:17:01 +0000 Subject: [PATCH] NIHVIVO-222 Second step at coding the Java-based SeleniumRunner. --- .../utilities/testrunner/CommandRunner.java | 50 ++++- .../utilities/testrunner/FatalException.java | 26 +++ .../vitro/utilities/testrunner/Listener.java | 209 ++++++++++++++++++ .../vitro/utilities/testrunner/Logger.java | 117 ---------- .../utilities/testrunner/ModelCleaner.java | 208 ++++++++++++++++- .../testrunner/ModelCleanerProperties.java | 138 ++++++++++++ .../utilities/testrunner/SeleniumRunner.java | 44 ++-- .../testrunner/SeleniumRunnerParameters.java | 171 ++++++++++++-- .../utilities/testrunner/SuiteRunner.java | 60 ++++- .../testrunner/UploadAreaCleaner.java | 10 +- .../testrunner/ModelCleanerTest.java | 51 +++++ 11 files changed, 908 insertions(+), 176 deletions(-) create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/FatalException.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Listener.java delete mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerProperties.java create mode 100644 utilities/testrunner/test/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerTest.java diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java index b724ed8c2..f54554a51 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java @@ -34,12 +34,12 @@ public class CommandRunner { private File workingDirectory; /* Gets informed of output as it arrives. Never null. */ - private final Logger logger; + private final Listener listener; private final Map environmentAdditions = new HashMap(); public CommandRunner(SeleniumRunnerParameters parms) { - this.logger = parms.getLogger(); + this.listener = parms.getListener(); } /** Set the directory that the command will run in. */ @@ -61,7 +61,7 @@ public class CommandRunner { * {@link java.lang.ProcessBuilder#ProcessBuilder(List)}. */ public void run(List command) throws CommandRunnerException { - logger.subProcessStart(command); + listener.subProcessStart(command); try { ProcessBuilder builder = new ProcessBuilder(command); @@ -93,7 +93,43 @@ public class CommandRunner { throw new CommandRunnerException( "Exception when handling sub-process:", e); } - logger.subProcessStop(command); + listener.subProcessStop(command); + } + + /** + * Run the command and don't wait for it to complete. {@link #stdErr} and + * {@link #stdOut} will not be set, but output from the process may be sent + * to the listener at any time. + * + * @param command + * a list containing the operating system program and its + * arguments. See + * {@link java.lang.ProcessBuilder#ProcessBuilder(List)}. + */ + public void runAsBackground(List command) + throws CommandRunnerException { + listener.subProcessStartInBackground(command); + try { + ProcessBuilder builder = new ProcessBuilder(command); + + if (workingDirectory != null) { + builder.directory(workingDirectory); + } + + if (!environmentAdditions.isEmpty()) { + builder.environment().putAll(this.environmentAdditions); + } + + Process process = builder.start(); + StreamEater outputEater = new StreamEater(process.getInputStream(), + false); + StreamEater errorEater = new StreamEater(process.getErrorStream(), + true); + + } catch (IOException e) { + throw new CommandRunnerException( + "Exception when handling sub-process:", e); + } } public int getReturnCode() { @@ -136,11 +172,11 @@ public class CommandRunner { if (howMany > 0) { String string = new String(buffer, 0, howMany); contents.write(string); - + if (isError) { - logger.subProcessErrout(string); + listener.subProcessErrout(string); } else { - logger.subProcessStdout(string); + listener.subProcessStdout(string); } } else if (howMany == 0) { Thread.yield(); diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/FatalException.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/FatalException.java new file mode 100644 index 000000000..42b11ef7b --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/FatalException.java @@ -0,0 +1,26 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.testrunner; + +/** + * Indicates a problem so severe that we might as well stop now. + */ +public class FatalException extends RuntimeException { + + public FatalException() { + super(); + } + + public FatalException(String message) { + super(message); + } + + public FatalException(Throwable cause) { + super(cause); + } + + public FatalException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Listener.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Listener.java new file mode 100644 index 000000000..54556f7f0 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Listener.java @@ -0,0 +1,209 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.testrunner; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintStream; +import java.io.PrintWriter; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * A listener for all events that occur during the run. In this basic + * implementation, each event is simply formatted and written to a log file or + * {@link PrintStream}. + */ +public class Listener { + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss.SSS"); + + private final Writer writer; + + // ---------------------------------------------------------------------- + // Listener methods + // ---------------------------------------------------------------------- + + public Listener(PrintStream out) { + this.writer = new OutputStreamWriter(out); + } + + public Listener(File logFile) throws IOException { + this.writer = new FileWriter(logFile); + } + + // ---------------------------------------------------------------------- + // Listener methods + // ---------------------------------------------------------------------- + + public void runStarted() { + log("Run started."); + } + + public void runFailed(Exception e) { + log("Run failed - fatal error"); + log(e); + } + + public void runStopped() { + log("Run stopped."); + } + + public void webappStopping(String tomcatStopCommand) { + log("Stopping tomcat: " + tomcatStopCommand); + } + + public void webappStopFailed(int returnCode) { + log("Failed to stop tomcat; return code was " + returnCode); + } + + public void webappWaitingForStop(int tomcatStopDelay) { + log("Waiting " + tomcatStopDelay + " seconds for tomcat to stop."); + } + + public void webappStopped() { + log("Tomcat stopped."); + } + + public void dropDatabaseStarting(String statement) { + log("Dropping database: " + statement); + } + + public void dropDatabaseFailed(int returnCode) { + log("Failed to drop the database; return code was " + returnCode); + } + + public void dropDatabaseComplete() { + log("Dropped database."); + } + + public void loadDatabaseStarting(String statement) { + log("Loading the database: " + statement); + } + + public void loadDatabaseFailed(int returnCode) { + log("Failed to load the database; return code was " + returnCode); + } + + public void loadDatabaseComplete() { + log("Loaded the database."); + } + + public void webappStarting(String tomcatStartCommand) { + log("Starting tomcat: " + tomcatStartCommand); + } + + public void webappStartFailed(int returnCode) { + log("Failed to start tomcat; return code was " + returnCode); + } + + public void webappWaitingForStart(int tomcatStartDelay) { + log("Waiting " + tomcatStartDelay + " seconds for tomcat to start."); + } + + public void webappStarted() { + log("Tomcat started."); + } + + public void subProcessStart(List command) { + log("Subprocess started: " + command); + } + + public void subProcessStartInBackground(List command) { + log("Subprocess started in background: " + command); + } + + public void subProcessStdout(String string) { + logRawText(string); + } + + public void subProcessErrout(String string) { + logRawText(string); + } + + public void subProcessStop(List command) { + log("Subprocess stopped: " + command); + } + + public void suiteStarted(File suiteDir) { + log("Suite started: " + suiteDir.getName()); + } + + public void suiteTestingStarted(File suiteDir) { + log("Suite testing started: " + suiteDir.getName()); + } + + public void suiteFailed(File suiteDir, int returnCode) { + log("Suite failed: " + suiteDir.getName() + ", returnCode=" + + returnCode); + } + + public void suiteFailed(File suiteDir, Exception e) { + log("Suite failed: " + suiteDir.getName()); + log(e); + } + + public void suiteTestingStopped(File suiteDir) { + log("Suite testing stopped: " + suiteDir.getName()); + } + + public void suiteStopped(File suiteDir) { + log("Suite stopped: " + suiteDir.getName()); + } + + public void cleanUploadStart(File uploadDirectory) { + log("Upload cleaning started: " + uploadDirectory.getPath()); + } + + public void cleanUploadFailed(File uploadDirectory, IOException e) { + log("Upload cleaning failed: " + uploadDirectory.getPath()); + log(e); + } + + public void cleanUploadStop(File uploadDirectory) { + log("Upload cleaning stopped: " + uploadDirectory.getPath()); + } + + // ---------------------------------------------------------------------- + // Helper methods + // ---------------------------------------------------------------------- + + private void logRawText(String rawText) { + try { + writer.write(rawText); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void log(String message) { + try { + writer.write(timeStamp() + " " + message + "\n"); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private void log(Throwable t) { + try { + t.printStackTrace(new PrintWriter(writer)); + writer.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Convert the current date and time to a string for the log. + */ + private String timeStamp() { + return DATE_FORMAT.format(new Date()); + } + +} diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java deleted file mode 100644 index 9212cf447..000000000 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java +++ /dev/null @@ -1,117 +0,0 @@ -/* $This file is distributed under the terms of the license in /doc/license.txt$ */ - -package edu.cornell.mannlib.vitro.utilities.testrunner; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintStream; -import java.io.PrintWriter; -import java.io.Writer; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; - -/** - * TODO - */ -public class Logger { - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat( - "yyyy-MM-dd HH:mm:ss.SSS"); - - private final Writer writer; - - public Logger(PrintStream out) { - this.writer = new OutputStreamWriter(out); - } - - public Logger(File logFile) throws IOException { - this.writer = new FileWriter(logFile); - } - - public void runStarted() { - log("Run started."); - } - - public void runStopped() { - log("Run stopped."); - } - - public void subProcessStart(List command) { - log("Subprocess started: " + command); - } - - public void subProcessStdout(String string) { - logRawText(string); - } - - public void subProcessErrout(String string) { - logRawText(string); - } - - public void subProcessStop(List command) { - log("Subprocess stopped: " + command); - } - - public void suiteStarted(File suiteDir) { - log("Suite started: " + suiteDir.getName()); - } - - public void suiteFailed(File suiteDir, IOException e) { - log("Suite failed: " + suiteDir.getName()); - log(e); - } - - public void suiteStopped(File suiteDir) { - log("Suite stopped: " + suiteDir.getName()); - } - - public void cleanUploadStart(File uploadDirectory) { - log("Upload cleaning started: " + uploadDirectory.getPath()); - } - - public void cleanUploadFailed(File uploadDirectory, IOException e) { - log("Upload cleaning failed: " + uploadDirectory.getPath()); - log(e); - } - - public void cleanUploadStop(File uploadDirectory) { - log("Upload cleaning stopped: " + uploadDirectory.getPath()); - } - - private void logRawText(String rawText) { - try { - writer.write(rawText); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void log(String message) { - try { - writer.write(timeStamp() + " " + message + "\n"); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - private void log(Throwable t) { - try { - t.printStackTrace(new PrintWriter(writer)); - writer.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * Convert the current date and time to a string for the log. - */ - private String timeStamp() { - return DATE_FORMAT.format(new Date()); - } - -} diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java index c6bffa09f..e10d3b7d1 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.java @@ -2,25 +2,215 @@ package edu.cornell.mannlib.vitro.utilities.testrunner; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + /** - * TODO + * Resets the RDF-Model to a known state, in preparation for the next Selenium + * test suite. */ public class ModelCleaner { + private final ModelCleanerProperties properties; + private final CommandRunner runner; + private final Listener listener; - /** - * @param parms - */ public ModelCleaner(SeleniumRunnerParameters parms) { - // TODO Auto-generated constructor stub - throw new RuntimeException("ModelCleaner Constructor not implemented."); + this.properties = parms.getModelCleanerProperties(); + this.listener = parms.getListener(); + this.runner = new CommandRunner(parms); + + sanityCheck(); + } + + private void sanityCheck() { + executeMysqlStatement("show databases;"); + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + throw new FatalException( + "sanityCheck: Failed to execute a MySQL statement: " + + "return code=" + returnCode); + } } /** + * Reset the RDF-Model to a known state, according to the parameters in the + * properties file. * + * @throws CommandRunnerException + * if a problem occurs in a sub-process. */ - public void clean() { - // TODO Auto-generated method stub - throw new RuntimeException("ModelCleaner.clean() not implemented."); + public void clean() throws CommandRunnerException { + stopTheWebapp(); + dropDatabase(); + createAndLoadDatabase(); + startTheWebapp(); } + /** + * Stop Tomcat and wait the prescribed number of seconds for it to clean up. + */ + private void stopTheWebapp() throws CommandRunnerException { + String tomcatStopCommand = properties.getTomcatStopCommand(); + int tomcatStopDelay = properties.getTomcatStopDelay(); + + listener.webappStopping(tomcatStopCommand); + runner.run(parseCommandLine(tomcatStopCommand)); + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + listener.webappStopFailed(returnCode); + // Throw no exception - this can happen if Tomcat isn't running. + } + + listener.webappWaitingForStop(tomcatStopDelay); + try { + Thread.sleep(tomcatStopDelay * 1000L); + } catch (InterruptedException e) { + // Just continue. + } + + listener.webappStopped(); + } + + /** + * Delete the database. + */ + private void dropDatabase() { + String mysqlStatement = "drop database " + properties.getMysqlDbName() + + "; create database " + properties.getMysqlDbName() + + " character set utf8;"; + + listener.dropDatabaseStarting(mysqlStatement); + executeMysqlStatement(mysqlStatement); + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + listener.dropDatabaseFailed(returnCode); + throw new FatalException("dropDatabase() failed: return code=" + + returnCode); + } + + listener.dropDatabaseComplete(); + } + + /** + * Rebuild the database. + */ + private void createAndLoadDatabase() { + String mysqlStatement = "source " + + convertBackslashes(properties.getMysqlDumpfile()) + ";"; + + listener.loadDatabaseStarting(mysqlStatement); + executeMysqlStatement(mysqlStatement); + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + listener.loadDatabaseFailed(returnCode); + throw new FatalException("loadDatabase() failed: return code=" + + returnCode); + } + + listener.loadDatabaseComplete(); + } + + /** + * Start Tomcat and wait for it to initialize. + */ + private void startTheWebapp() { + String tomcatStartCommand = properties.getTomcatStartCommand(); + int tomcatStartDelay = properties.getTomcatStartDelay(); + + listener.webappStarting(tomcatStartCommand); + try { + runner.runAsBackground(parseCommandLine(tomcatStartCommand)); + } catch (CommandRunnerException e) { + throw new FatalException(e); + } + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + listener.webappStartFailed(returnCode); + throw new FatalException("startTheWebapp() failed: return code=" + + returnCode); + } + + listener.webappWaitingForStart(tomcatStartDelay); + try { + Thread.sleep(tomcatStartDelay * 1000L); + } catch (InterruptedException e) { + // Just continue. + } + + listener.webappStarted(); + } + + /** + * Tell MySQL to execute this statement. If it fails, throw a fatal + * exception. + */ + private void executeMysqlStatement(String mysqlStatement) { + List cmd = new ArrayList(); + cmd.add("mysql"); + cmd.add("--user=" + properties.getMysqlUsername()); + cmd.add("--password=" + properties.getMysqlPassword()); + cmd.add("--database=" + properties.getMysqlDbName()); + cmd.add("--execute=" + mysqlStatement); + + try { + runner.run(cmd); + } catch (CommandRunnerException e) { + throw new FatalException(e); + } + } + + /** + * A command line must be broken into separate arguments, where arguments + * are delimited by blanks unless the blank (and the argument) is enclosed + * in quotes. + */ + static List parseCommandLine(String commandLine) { + List pieces = new ArrayList(); + StringBuilder piece = null; + boolean inDelimiter = true; + boolean inQuotes = false; + for (int i = 0; i < commandLine.length(); i++) { + char thisChar = commandLine.charAt(i); + if ((thisChar == ' ') && !inQuotes) { + if (inDelimiter) { + // No effect. + } else { + inDelimiter = true; + pieces.add(piece.toString()); + } + } else if (thisChar == '"') { + // Quotes are not carried into the parsed strings. + inQuotes = !inQuotes; + } else { // Not a blank or a quote. + if (inDelimiter) { + inDelimiter = false; + piece = new StringBuilder(); + } + piece.append(thisChar); + } + } + + // There is an implied delimiter at the end of the command line. + if (!inDelimiter) { + pieces.add(piece.toString()); + } + + // Quotes must appear in pairs + if (inQuotes) { + throw new IllegalArgumentException( + "Command line contains mismatched quotes: " + commandLine); + } + + return pieces; + } + + static String convertBackslashes(File file) { + return file.getPath().replace("\\", "/"); + } } diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerProperties.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerProperties.java new file mode 100644 index 000000000..310051905 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerProperties.java @@ -0,0 +1,138 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.testrunner; + +import java.io.File; +import java.util.Properties; + +/** + * Hold the runtime properties that pertain specifically to cleaning the data + * model. + */ +public class ModelCleanerProperties { + public static final String PROP_TOMCAT_START_COMMAND = "tomcat_start_command"; + public static final String PROP_TOMCAT_START_DELAY = "tomcat_start_delay"; + public static final String PROP_TOMCAT_STOP_COMMAND = "tomcat_stop_command"; + public static final String PROP_TOMCAT_STOP_DELAY = "tomcat_stop_delay"; + public static final String PROP_MYSQL_USERNAME = "mysql_username"; + public static final String PROP_MYSQL_PASSWORD = "mysql_password"; + public static final String PROP_MYSQL_DB_NAME = "mysql_db_name"; + public static final String PROP_MYSQL_DUMPFILE = "mysql_dumpfile"; + + private final String tomcatStartCommand; + private final int tomcatStartDelay; + private final String tomcatStopCommand; + private final int tomcatStopDelay; + private final String mysqlUsername; + private final String mysqlPassword; + private final String mysqlDbName; + private final File mysqlDumpfile; + + /** + * Confirm that we have the expected properties, and that their values seem + * reasonable. + */ + public ModelCleanerProperties(Properties props) { + this.tomcatStartCommand = getRequiredProperty(props, + PROP_TOMCAT_START_COMMAND); + this.tomcatStartDelay = getRequiredIntegerProperty(props, + PROP_TOMCAT_START_DELAY); + + this.tomcatStopCommand = getRequiredProperty(props, + PROP_TOMCAT_STOP_COMMAND); + this.tomcatStopDelay = getRequiredIntegerProperty(props, + PROP_TOMCAT_STOP_DELAY); + + this.mysqlUsername = getRequiredProperty(props, PROP_MYSQL_USERNAME); + this.mysqlPassword = getRequiredProperty(props, PROP_MYSQL_PASSWORD); + this.mysqlDbName = getRequiredProperty(props, PROP_MYSQL_DB_NAME); + + this.mysqlDumpfile = confirmDumpfile(props); + } + + public String getTomcatStartCommand() { + return tomcatStartCommand; + } + + public int getTomcatStartDelay() { + return tomcatStartDelay; + } + + public String getTomcatStopCommand() { + return tomcatStopCommand; + } + + public int getTomcatStopDelay() { + return tomcatStopDelay; + } + + public String getMysqlUsername() { + return mysqlUsername; + } + + public String getMysqlPassword() { + return mysqlPassword; + } + + public String getMysqlDbName() { + return mysqlDbName; + } + + public File getMysqlDumpfile() { + return mysqlDumpfile; + } + + /** + * Get the value for this property. If there isn't one, or if it's empty, + * complain. + */ + private String getRequiredProperty(Properties props, String key) { + String value = props.getProperty(key); + if ((value == null) || (value.trim().length() == 0)) { + throw new IllegalArgumentException( + "Property file must provide a value for '" + key + "'"); + } + return value; + } + + private int getRequiredIntegerProperty(Properties props, String key) { + String value = getRequiredProperty(props, key); + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property value for '" + key + + "' is not a valid integer: " + value); + } + } + + /** + * The dumpfile parameter must point to an existing file. + */ + private File confirmDumpfile(Properties props) { + String filename = getRequiredProperty(props, PROP_MYSQL_DUMPFILE); + File dumpfile = new File(filename); + if (!dumpfile.exists()) { + throw new IllegalArgumentException("Invalid value for '" + + PROP_MYSQL_DUMPFILE + "': file '" + filename + + "' does not exist."); + } + if (!dumpfile.isFile()) { + throw new IllegalArgumentException("Invalid value for '" + + PROP_MYSQL_DUMPFILE + "': '" + filename + + "' is not a file."); + } + if (!dumpfile.canRead()) { + throw new IllegalArgumentException("Invalid value for '" + + PROP_MYSQL_DUMPFILE + "': file '" + filename + + "' is not readable."); + } + return dumpfile; + } + + public String toString() { + return "\n tomcatStartCommand: " + tomcatStartCommand + + "\n tomcatStartDelay: " + tomcatStartDelay + + "\n tomcatStopCommand: " + tomcatStopCommand + + "\n tomcatStopDelay: " + tomcatStopDelay; + } +} diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java index b6aaf6b15..6dcd52c62 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java @@ -2,6 +2,8 @@ package edu.cornell.mannlib.vitro.utilities.testrunner; +import static edu.cornell.mannlib.vitro.utilities.testrunner.SeleniumRunnerParameters.LOGFILE_NAME; + import java.io.File; import java.io.IOException; import java.util.ArrayList; @@ -13,23 +15,23 @@ import java.util.List; */ public class SeleniumRunner { private final SeleniumRunnerParameters parms; - private final Logger logger; + private final Listener listener; private final UploadAreaCleaner uploadCleaner; private final ModelCleaner modelCleaner; private final SuiteRunner suiteRunner; public SeleniumRunner(SeleniumRunnerParameters parms) { this.parms = parms; - this.logger = parms.getLogger(); + this.listener = parms.getListener(); this.uploadCleaner = new UploadAreaCleaner(parms); this.modelCleaner = new ModelCleaner(parms); this.suiteRunner = new SuiteRunner(parms); } public void runSelectedSuites() { - logger.runStarted(); + listener.runStarted(); for (File suiteDir : parms.getSelectedSuites()) { - logger.suiteStarted(suiteDir); + listener.suiteStarted(suiteDir); try { if (parms.isCleanModel()) { modelCleaner.clean(); @@ -39,11 +41,17 @@ public class SeleniumRunner { } suiteRunner.runSuite(suiteDir); } catch (IOException e) { - logger.suiteFailed(suiteDir, e); + listener.suiteFailed(suiteDir, e); + } catch (CommandRunnerException e) { + listener.suiteFailed(suiteDir, e); + } catch (FatalException e) { + listener.runFailed(e); + e.printStackTrace(); + break; } - logger.suiteStopped(suiteDir); + listener.suiteStopped(suiteDir); } - logger.runStopped(); + listener.runStopped(); } private static void selectAllSuites(SeleniumRunnerParameters parms) { @@ -54,9 +62,13 @@ public class SeleniumRunner { parms.setSelectedSuites(suites); } - /** - * @param args - */ + private static void usage(String message) { + System.out.println(message); + System.out.println("Usage is: SeleniumRunner " + + "[\"interactive\"]"); + System.exit(1); + } + public static void main(String[] args) { SeleniumRunnerParameters parms = null; boolean interactive = false; @@ -82,20 +94,20 @@ public class SeleniumRunner { // TODO hook up the GUI. throw new RuntimeException("interactive mode not implemented."); } else { + File logFile = new File(parms.getOutputDirectory(), LOGFILE_NAME); + System.out.println("Log file is '" + logFile.getPath() + "'"); + // Run all of the suites. // For each suite, clean the model and the upload area. selectAllSuites(parms); parms.setCleanModel(true); parms.setCleanUploads(true); + + System.out.println(parms); + SeleniumRunner runner = new SeleniumRunner(parms); runner.runSelectedSuites(); } } - private static void usage(String message) { - System.out.println(message); - System.out.println("Usage is: SeleniumRunner " - + "[\"interactive\"]"); - System.exit(1); - } } diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java index 54359a8d9..9f0f13eb9 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java @@ -19,22 +19,32 @@ import java.util.Properties; * with modifications from the GUI if we are running interactively. */ public class SeleniumRunnerParameters { - private static final String PROP_OUTPUT_DIRECTORY = "output_directory"; - private static final String PROP_UPLOAD_DIRECTORY = "upload_directory"; - private static final String PROP_SUITE_DIRECTORIES = "suite_parent_directories"; + public static final String PROP_OUTPUT_DIRECTORY = "output_directory"; + public static final String PROP_UPLOAD_DIRECTORY = "upload_directory"; + public static final String PROP_SUITE_DIRECTORIES = "suite_parent_directories"; + public static final String PROP_WEBSITE_URL = "website_url"; + public static final String PROP_USER_EXTENSIONS_PATH = "user_extensions_path"; + public static final String PROP_FIREFOX_PROFILE_PATH = "firefox_profile_template_path"; + public static final String PROP_SUITE_TIMEOUT_LIMIT = "suite_timeout_limit"; + public static final String PROP_SELENIUM_JAR_PATH = "selenium_jar_path"; - private static final String LOGFILE_NAME = "log_file.txt"; + public static final String LOGFILE_NAME = "log_file.txt"; + private final String websiteUrl; + private final File userExtensionsFile; + private final File firefoxProfileDir; + private final int suiteTimeoutLimit; + private final File seleniumJarPath; private final File uploadDirectory; private final File outputDirectory; private final File logFile; - private final Collection suiteParentDirectories; + private final ModelCleanerProperties modelCleanerProperties; private Collection selectedSuites = Collections.emptySet(); private boolean cleanModel = true; private boolean cleanUploads = true; - private Logger logger = new Logger(System.out); + private Listener listener = new Listener(System.out); /** * Read the required properties from the property file, and do some checks @@ -48,15 +58,26 @@ public class SeleniumRunnerParameters { Properties props = new Properties(); props.load(propsReader); + this.websiteUrl = getRequiredProperty(props, PROP_WEBSITE_URL); + this.userExtensionsFile = checkReadableFile(props, + PROP_USER_EXTENSIONS_PATH); + this.firefoxProfileDir = checkOptionalReadableDirectory(props, + PROP_FIREFOX_PROFILE_PATH); + this.suiteTimeoutLimit = getRequiredIntegerProperty(props, + PROP_SUITE_TIMEOUT_LIMIT); + this.seleniumJarPath = checkReadableFile(props, + PROP_SELENIUM_JAR_PATH); this.uploadDirectory = checkReadWriteDirectory(props, PROP_UPLOAD_DIRECTORY); + this.outputDirectory = checkReadWriteDirectory(props, PROP_OUTPUT_DIRECTORY); this.logFile = new File(this.outputDirectory, LOGFILE_NAME); - this.logger = new Logger(this.logFile); + this.listener = new Listener(this.logFile); this.suiteParentDirectories = checkSuiteParentDirectories(props); + this.modelCleanerProperties = new ModelCleanerProperties(props); } finally { if (propsReader != null) { try { @@ -68,6 +89,36 @@ public class SeleniumRunnerParameters { } } + /** + * If there is a parameter for this key, it should point to a readable + * directory. + */ + private File checkOptionalReadableDirectory(Properties props, String key) { + String value = props.getProperty(key); + if (value == null) { + return null; + } + + File dir = new File(value); + + if (!dir.exists()) { + throw new IllegalArgumentException("Directory " + key + " '" + + value + "' does not exist."); + } + + if (!dir.isDirectory()) { + throw new IllegalArgumentException("Directory " + key + " '" + + value + "' is not a directory."); + } + + if (!dir.canRead()) { + throw new IllegalArgumentException("Directory " + key + " '" + + value + "' is not readable."); + } + + return dir; + } + /** * Check that there is a property for the output directory, and that it * points to a valid directory. @@ -99,6 +150,26 @@ public class SeleniumRunnerParameters { return dir; } + private File checkReadableFile(Properties props, String key) { + String value = getRequiredProperty(props, key); + + File file = new File(value); + + if (!file.exists()) { + throw new IllegalArgumentException("File " + key + + ": '' does not exist."); + } + if (!file.isFile()) { + throw new IllegalArgumentException("File " + key + + ": '' is not a file."); + } + if (!file.canRead()) { + throw new IllegalArgumentException("File " + key + + ": '' is not readable."); + } + return file; + } + /** * Get the property for the suite directories and ensure that each one is * indeed a readable directory. @@ -123,6 +194,7 @@ public class SeleniumRunnerParameters { throw new IllegalArgumentException("Suite directory '" + dir.getPath() + "' is not readable."); } + dirs.add(dir); } return dirs; } @@ -140,22 +212,67 @@ public class SeleniumRunnerParameters { return value; } - public Logger getLogger() { - return logger; + /** + * This required property must be a valid integer. + */ + private int getRequiredIntegerProperty(Properties props, String key) { + String value = getRequiredProperty(props, key); + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Property value for '" + key + + "' is not a valid integer: " + value); + } } - public void setLogger(Logger logger) { - this.logger = logger; + public String getWebsiteUrl() { + return websiteUrl; + } + + public File getUserExtensionsFile() { + return userExtensionsFile; + } + + public boolean hasFirefoxProfileDir() { + return firefoxProfileDir != null; + } + + public File getFirefoxProfileDir() { + return firefoxProfileDir; + } + + public int getSuiteTimeoutLimit() { + return suiteTimeoutLimit; + } + + public File getSeleniumJarPath() { + return seleniumJarPath; + } + + public Listener getListener() { + return listener; + } + + public void setListener(Listener logger) { + this.listener = logger; } public File getUploadDirectory() { return uploadDirectory; } + public File getOutputDirectory() { + return outputDirectory; + } + public Collection getSuiteParentDirectories() { return suiteParentDirectories; } + public ModelCleanerProperties getModelCleanerProperties() { + return modelCleanerProperties; + } + public void setSelectedSuites(Collection selectedSuites) { this.selectedSuites = selectedSuites; } @@ -180,22 +297,50 @@ public class SeleniumRunnerParameters { this.cleanUploads = cleanUploads; } + public String toString() { + return "Parameters:" + "\n websiteUrl: " + websiteUrl + + "\n userExtensionsFile: " + userExtensionsFile.getPath() + + "\n firefoxProfileDir: " + firefoxProfileDir.getPath() + + "\n suiteTimeoutLimit: " + suiteTimeoutLimit + + "\n seleniumJarPath: " + seleniumJarPath.getPath() + + "\n uploadDirectory: " + uploadDirectory.getPath() + + "\n outputDirectory: " + outputDirectory.getPath() + + "\n suiteParentDirectories: " + suiteParentDirectories + + "\n modelCleanerProperties: " + modelCleanerProperties + + "\n\n selectedSuites: " + showSelectedSuites() + + "\n cleanModel: " + cleanModel + "\n cleanUploads: " + + cleanUploads; + } + + private String showSelectedSuites() { + StringBuilder buffer = new StringBuilder(); + for (File suite : selectedSuites) { + buffer.append("\n ").append(suite.getPath()); + } + return buffer.toString(); + } + /** * Look inside this parent directory and find any suite directories. You can * recognize a suite directory because it contains a file named Suite.html. */ public Collection findSuiteDirs(File parentDir) { + System.out.println("parentDir: " + parentDir); return Arrays.asList(parentDir.listFiles(new FileFilter() { public boolean accept(File pathname) { if (!pathname.isDirectory()) { return false; } + if (pathname.getName().charAt(0) == '.') { + return false; + } + File suiteFile = new File(pathname, "Suite.html"); if (suiteFile.exists()) { return true; } else { - logger.subProcessErrout("Warning: suite file '" + suiteFile.getPath() - + "' does not exist.\n"); + listener.subProcessErrout("Warning: suite file '" + + suiteFile.getPath() + "' does not exist.\n"); return false; } } diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java index c5d39e13f..9f5febf6c 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java @@ -3,26 +3,68 @@ package edu.cornell.mannlib.vitro.utilities.testrunner; import java.io.File; +import java.util.ArrayList; +import java.util.List; /** - * TODO + * Run a Selenium TestSuite in a sub-process. */ public class SuiteRunner { - /** - * @param parms - */ + private final SeleniumRunnerParameters parms; + private final CommandRunner runner; + private final Listener listener; + public SuiteRunner(SeleniumRunnerParameters parms) { - // TODO Auto-generated constructor stub - throw new RuntimeException("SuiteRunner Constructor not implemented."); + this.parms = parms; + this.runner = new CommandRunner(parms); + this.listener = parms.getListener(); } /** - * @param suiteDir + * Run the suite. */ public void runSuite(File suiteDir) { - // TODO Auto-generated method stub - throw new RuntimeException("SuiteRunner.runSuite() not implemented."); + listener.suiteTestingStarted(suiteDir); + + List cmd = new ArrayList(); + cmd.add("java"); + cmd.add("-jar"); + cmd.add(parms.getSeleniumJarPath().getPath()); + cmd.add("-singleWindow"); + cmd.add("-timeout"); + cmd.add(String.valueOf(parms.getSuiteTimeoutLimit())); + cmd.add("-userExtensions"); + cmd.add(parms.getUserExtensionsFile().getPath()); + + if (parms.hasFirefoxProfileDir()) { + cmd.add("-firefoxProfileTemplate"); + cmd.add(parms.getFirefoxProfileDir().getPath()); + } + + String suiteName = suiteDir.getName(); + File outputFile = new File(parms.getOutputDirectory(), suiteName + + ".html"); + File suiteFile = new File(suiteDir, "Suite.html"); + + cmd.add("-htmlSuite"); + cmd.add("*firefox"); + cmd.add(parms.getWebsiteUrl()); + cmd.add(suiteFile.getPath()); + cmd.add(outputFile.getPath()); + + try { + runner.run(cmd); + } catch (CommandRunnerException e) { + throw new FatalException(e); + } + + int returnCode = runner.getReturnCode(); + if (returnCode != 0) { + listener.suiteFailed(suiteDir, returnCode); + } + + listener.suiteTestingStopped(suiteDir); } } diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java index 4ab73a435..921715b8a 100644 --- a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java @@ -11,11 +11,11 @@ import java.io.IOException; */ public class UploadAreaCleaner { private final SeleniumRunnerParameters parms; - private final Logger logger; + private final Listener listener; public UploadAreaCleaner(SeleniumRunnerParameters parms) { this.parms = parms; - this.logger = parms.getLogger(); + this.listener = parms.getListener(); } /** @@ -29,7 +29,7 @@ public class UploadAreaCleaner { + "' is not a directory."); } - logger.cleanUploadStart(uploadDirectory); + listener.cleanUploadStart(uploadDirectory); try { for (File file : uploadDirectory.listFiles()) { @@ -40,10 +40,10 @@ public class UploadAreaCleaner { } } } catch (IOException e) { - logger.cleanUploadFailed(uploadDirectory, e); + listener.cleanUploadFailed(uploadDirectory, e); throw e; } finally { - logger.cleanUploadStop(uploadDirectory); + listener.cleanUploadStop(uploadDirectory); } } diff --git a/utilities/testrunner/test/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerTest.java b/utilities/testrunner/test/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerTest.java new file mode 100644 index 000000000..c626a280d --- /dev/null +++ b/utilities/testrunner/test/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleanerTest.java @@ -0,0 +1,51 @@ +/* $This file is distributed under the terms of the license in /doc/license.txt$ */ + +package edu.cornell.mannlib.vitro.utilities.testrunner; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * TODO + */ +public class ModelCleanerTest { + + // ---------------------------------------------------------------------- + // Tests for parseCommandLine() + // ---------------------------------------------------------------------- + + @Test + public void oneArgument() { + assertExpectedParsing("oneArgument", "oneArgument"); + } + + @Test + public void multipleArguments() { + assertExpectedParsing("more than one", "more", "than", "one"); + } + + @Test + public void quotedArgument() { + assertExpectedParsing("contains \"quoted blank\" string", "contains", + "quoted blank", "string"); + } + + @Test(expected = IllegalArgumentException.class) + public void mismatchedQuotes() { + assertExpectedParsing("contains mismatched \"quote"); + } + + @Test + public void emptyLine() { + assertExpectedParsing(""); + } + + private void assertExpectedParsing(String commandLine, + String... expectedPieces) { + assertEquals("parse", Arrays.asList(expectedPieces), ModelCleaner + .parseCommandLine(commandLine)); + } +}