From 72ee86f0a7b33d6a750d4b33fbf161fa48f97911 Mon Sep 17 00:00:00 2001 From: jeb228 Date: Tue, 11 May 2010 14:06:28 +0000 Subject: [PATCH] NIHVIVO-222 First step at coding the Java-based SeleniumRunner. --- .../utilities/testrunner/CommandRunner.java | 167 ++++++++++++++ .../testrunner/CommandRunnerException.java | 26 +++ .../vitro/utilities/testrunner/Logger.java | 117 ++++++++++ .../utilities/testrunner/ModelCleaner.java | 26 +++ .../utilities/testrunner/SeleniumRunner.java | 101 +++++++++ .../testrunner/SeleniumRunnerParameters.java | 205 ++++++++++++++++++ .../utilities/testrunner/SuiteRunner.java | 28 +++ .../testrunner/UploadAreaCleaner.java | 99 +++++++++ 8 files changed, 769 insertions(+) create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java create 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/ModelCleaner.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java create mode 100644 utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.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 new file mode 100644 index 000000000..b724ed8c2 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunner.java @@ -0,0 +1,167 @@ +/* $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.IOException; +import java.io.InputStream; +import java.io.StringWriter; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

+ * A harness that runs a system-level command. + *

+ *

+ * No provision is made for standard input. + *

+ *

+ * The standard output and standard error streams are asynchronously read, so + * the sub-process will not block on full buffers. Warning: if either of these + * streams contain more data than can fit into a String, then we will have a + * problem. + *

+ * + * @author jblake + */ +public class CommandRunner { + + private int returnCode; + private String stdOut = ""; + private String stdErr = ""; + private File workingDirectory; + + /* Gets informed of output as it arrives. Never null. */ + private final Logger logger; + + private final Map environmentAdditions = new HashMap(); + + public CommandRunner(SeleniumRunnerParameters parms) { + this.logger = parms.getLogger(); + } + + /** Set the directory that the command will run in. */ + public void setWorkingDirectory(File workingDirectory) { + this.workingDirectory = workingDirectory; + } + + /** Add (or replace) any environment variable. */ + public void setEnvironmentVariable(String key, String value) { + this.environmentAdditions.put(key, value); + } + + /** + * Run the command. + * + * @param command + * a list containing the operating system program and its + * arguments. See + * {@link java.lang.ProcessBuilder#ProcessBuilder(List)}. + */ + public void run(List command) throws CommandRunnerException { + logger.subProcessStart(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); + + this.returnCode = process.waitFor(); + + outputEater.join(); + this.stdOut = outputEater.getContents(); + + errorEater.join(); + this.stdErr = errorEater.getContents(); + } catch (IOException e) { + throw new CommandRunnerException( + "Exception when handling sub-process:", e); + } catch (InterruptedException e) { + throw new CommandRunnerException( + "Exception when handling sub-process:", e); + } + logger.subProcessStop(command); + } + + public int getReturnCode() { + return returnCode; + } + + public String getStdErr() { + return stdErr; + } + + public String getStdOut() { + return stdOut; + } + + /** + * A thread that reads an InputStream until it reaches end of file, then + * closes the stream. Designated as error stream or not, so it can tell the + * logger. + */ + private class StreamEater extends Thread { + private final InputStream stream; + private final boolean isError; + + private final StringWriter contents = new StringWriter(); + + private final byte[] buffer = new byte[4096]; + + public StreamEater(InputStream stream, boolean isError) { + this.stream = stream; + this.isError = isError; + this.start(); + } + + @Override + public void run() { + try { + int howMany = 0; + while (true) { + howMany = stream.read(buffer); + if (howMany > 0) { + String string = new String(buffer, 0, howMany); + contents.write(string); + + if (isError) { + logger.subProcessErrout(string); + } else { + logger.subProcessStdout(string); + } + } else if (howMany == 0) { + Thread.yield(); + } else { + break; + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + stream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + public String getContents() { + return contents.toString(); + } + } + +} diff --git a/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.java new file mode 100644 index 000000000..8550abbb7 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/CommandRunnerException.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 with the attempt to run a command in a sub-process. + */ +public class CommandRunnerException extends Exception { + + public CommandRunnerException() { + super(); + } + + public CommandRunnerException(String message) { + super(message); + } + + public CommandRunnerException(Throwable cause) { + super(cause); + } + + public CommandRunnerException(String message, Throwable cause) { + super(message, cause); + } + +} 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 new file mode 100644 index 000000000..9212cf447 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/Logger.java @@ -0,0 +1,117 @@ +/* $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 new file mode 100644 index 000000000..c6bffa09f --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/ModelCleaner.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; + +/** + * TODO + */ +public class ModelCleaner { + + /** + * @param parms + */ + public ModelCleaner(SeleniumRunnerParameters parms) { + // TODO Auto-generated constructor stub + throw new RuntimeException("ModelCleaner Constructor not implemented."); + } + + /** + * + */ + public void clean() { + // TODO Auto-generated method stub + throw new RuntimeException("ModelCleaner.clean() not implemented."); + } + +} 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 new file mode 100644 index 000000000..b6aaf6b15 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunner.java @@ -0,0 +1,101 @@ +/* $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.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Run the Selenium test suites. Provide the properties file and perhaps an + * "interactive" flag. + */ +public class SeleniumRunner { + private final SeleniumRunnerParameters parms; + private final Logger logger; + private final UploadAreaCleaner uploadCleaner; + private final ModelCleaner modelCleaner; + private final SuiteRunner suiteRunner; + + public SeleniumRunner(SeleniumRunnerParameters parms) { + this.parms = parms; + this.logger = parms.getLogger(); + this.uploadCleaner = new UploadAreaCleaner(parms); + this.modelCleaner = new ModelCleaner(parms); + this.suiteRunner = new SuiteRunner(parms); + } + + public void runSelectedSuites() { + logger.runStarted(); + for (File suiteDir : parms.getSelectedSuites()) { + logger.suiteStarted(suiteDir); + try { + if (parms.isCleanModel()) { + modelCleaner.clean(); + } + if (parms.isCleanUploads()) { + uploadCleaner.clean(); + } + suiteRunner.runSuite(suiteDir); + } catch (IOException e) { + logger.suiteFailed(suiteDir, e); + } + logger.suiteStopped(suiteDir); + } + logger.runStopped(); + } + + private static void selectAllSuites(SeleniumRunnerParameters parms) { + List suites = new ArrayList(); + for (File parentDir : parms.getSuiteParentDirectories()) { + suites.addAll(parms.findSuiteDirs(parentDir)); + } + parms.setSelectedSuites(suites); + } + + /** + * @param args + */ + public static void main(String[] args) { + SeleniumRunnerParameters parms = null; + boolean interactive = false; + + if ((args.length != 1) && (args.length != 2)) { + usage("Wrong number of arguments."); + } + + if (args.length == 2) { + if (!"interactive".equalsIgnoreCase(args[1])) { + usage("Invalid argument '" + args[1] + "'"); + } + interactive = true; + } + + try { + parms = new SeleniumRunnerParameters(args[0]); + } catch (IOException e) { + usage("Can't read properties file: " + e); + } + + if (interactive) { + // TODO hook up the GUI. + throw new RuntimeException("interactive mode not implemented."); + } else { + // Run all of the suites. + // For each suite, clean the model and the upload area. + selectAllSuites(parms); + parms.setCleanModel(true); + parms.setCleanUploads(true); + 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 new file mode 100644 index 000000000..54359a8d9 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SeleniumRunnerParameters.java @@ -0,0 +1,205 @@ +/* $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.FileFilter; +import java.io.FileReader; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * Holds the runtime parameters that are read from the properties file, perhaps + * 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"; + + private static final String LOGFILE_NAME = "log_file.txt"; + + private final File uploadDirectory; + private final File outputDirectory; + private final File logFile; + + private final Collection suiteParentDirectories; + + private Collection selectedSuites = Collections.emptySet(); + private boolean cleanModel = true; + private boolean cleanUploads = true; + private Logger logger = new Logger(System.out); + + /** + * Read the required properties from the property file, and do some checks + * on them. + */ + public SeleniumRunnerParameters(String propertiesFilepath) + throws IOException { + Reader propsReader = null; + try { + propsReader = new FileReader(new File(propertiesFilepath)); + Properties props = new Properties(); + props.load(propsReader); + + 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.suiteParentDirectories = checkSuiteParentDirectories(props); + + } finally { + if (propsReader != null) { + try { + propsReader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + /** + * Check that there is a property for the output directory, and that it + * points to a valid directory. + */ + private File checkReadWriteDirectory(Properties props, String key) { + String value = getRequiredProperty(props, key); + + 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."); + } + + if (!dir.canWrite()) { + throw new IllegalArgumentException("Directory " + key + " '" + + value + "' is not writeable."); + } + return dir; + } + + /** + * Get the property for the suite directories and ensure that each one is + * indeed a readable directory. + */ + private Collection checkSuiteParentDirectories(Properties props) { + String value = getRequiredProperty(props, PROP_SUITE_DIRECTORIES); + + List dirs = new ArrayList(); + String[] paths = value.split("[:;]"); + for (String path : paths) { + File dir = new File(path.trim()); + + if (!dir.exists()) { + throw new IllegalArgumentException("Suite directory '" + + dir.getPath() + "' does not exist."); + } + if (!dir.isDirectory()) { + throw new IllegalArgumentException("Suite directory '" + + dir.getPath() + "' is not a directory."); + } + if (!dir.canRead()) { + throw new IllegalArgumentException("Suite directory '" + + dir.getPath() + "' is not readable."); + } + } + return dirs; + } + + /** + * 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; + } + + public Logger getLogger() { + return logger; + } + + public void setLogger(Logger logger) { + this.logger = logger; + } + + public File getUploadDirectory() { + return uploadDirectory; + } + + public Collection getSuiteParentDirectories() { + return suiteParentDirectories; + } + + public void setSelectedSuites(Collection selectedSuites) { + this.selectedSuites = selectedSuites; + } + + public Collection getSelectedSuites() { + return new ArrayList(this.selectedSuites); + } + + public boolean isCleanModel() { + return cleanModel; + } + + public void setCleanModel(boolean cleanModel) { + this.cleanModel = cleanModel; + } + + public boolean isCleanUploads() { + return cleanUploads; + } + + public void setCleanUploads(boolean cleanUploads) { + this.cleanUploads = cleanUploads; + } + + /** + * 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) { + return Arrays.asList(parentDir.listFiles(new FileFilter() { + public boolean accept(File pathname) { + if (!pathname.isDirectory()) { + 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"); + 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 new file mode 100644 index 000000000..c5d39e13f --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/SuiteRunner.java @@ -0,0 +1,28 @@ +/* $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; + +/** + * TODO + */ +public class SuiteRunner { + + /** + * @param parms + */ + public SuiteRunner(SeleniumRunnerParameters parms) { + // TODO Auto-generated constructor stub + throw new RuntimeException("SuiteRunner Constructor not implemented."); + } + + /** + * @param suiteDir + */ + public void runSuite(File suiteDir) { + // TODO Auto-generated method stub + throw new RuntimeException("SuiteRunner.runSuite() not implemented."); + } + +} 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 new file mode 100644 index 000000000..4ab73a435 --- /dev/null +++ b/utilities/testrunner/src/edu/cornell/mannlib/vitro/utilities/testrunner/UploadAreaCleaner.java @@ -0,0 +1,99 @@ +/* $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.FileFilter; +import java.io.IOException; + +/** + * Clean out the file upload area, so the next suite will start with no uploads. + */ +public class UploadAreaCleaner { + private final SeleniumRunnerParameters parms; + private final Logger logger; + + public UploadAreaCleaner(SeleniumRunnerParameters parms) { + this.parms = parms; + this.logger = parms.getLogger(); + } + + /** + * Delete all of the directories and files in the upload directory. Don't + * delete the upload directory itself. + */ + public void clean() throws IOException { + File uploadDirectory = parms.getUploadDirectory(); + if (!uploadDirectory.isDirectory()) { + throw new IllegalArgumentException("'" + uploadDirectory.getPath() + + "' is not a directory."); + } + + logger.cleanUploadStart(uploadDirectory); + + try { + for (File file : uploadDirectory.listFiles()) { + if (file.isFile()) { + deleteFile(file); + } else { + purgeDirectoryRecursively(uploadDirectory); + } + } + } catch (IOException e) { + logger.cleanUploadFailed(uploadDirectory, e); + throw e; + } finally { + logger.cleanUploadStop(uploadDirectory); + } + } + + /** + * Delete all of the files in a directory, any sub-directories, and the + * directory itself. + */ + protected static void purgeDirectoryRecursively(File directory) + throws IOException { + File[] files = directory.listFiles(); + for (File file : files) { + if (file.isDirectory()) { + purgeDirectoryRecursively(file); + } else { + deleteFile(file); + } + } + deleteFile(directory); + } + + /** + * Delete a file, either before or after the test. If it can't be deleted, + * complain. + */ + protected static void deleteFile(File file) throws IOException { + if (file.exists()) { + file.delete(); + } + if (!file.exists()) { + return; + } + + /* + * If we were unable to delete the file, is it because it's a non-empty + * directory? + */ + if (!file.isDirectory()) { + final StringBuffer message = new StringBuffer( + "Can't delete directory '" + file.getPath() + "'\n"); + file.listFiles(new FileFilter() { + public boolean accept(File pathname) { + message.append(" contains file '" + pathname + "'\n"); + return true; + } + }); + throw new IOException(message.toString().trim()); + } else { + throw new IOException("Unable to delete file '" + file.getPath() + + "'"); + } + } + +}