NIHVIVO-222 First step at coding the Java-based SeleniumRunner.

This commit is contained in:
jeb228 2010-05-11 14:06:28 +00:00
parent a8cfc10b2e
commit 72ee86f0a7
8 changed files with 769 additions and 0 deletions

View file

@ -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;
/**
* <p>
* A harness that runs a system-level command.
* </p>
* <p>
* No provision is made for standard input.
* </p>
* <p>
* 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.
* </p>
*
* @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<String, String> environmentAdditions = new HashMap<String, String>();
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<String> 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();
}
}
}

View file

@ -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);
}
}

View file

@ -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<String> command) {
log("Subprocess started: " + command);
}
public void subProcessStdout(String string) {
logRawText(string);
}
public void subProcessErrout(String string) {
logRawText(string);
}
public void subProcessStop(List<String> 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());
}
}

View file

@ -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.");
}
}

View file

@ -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<File> suites = new ArrayList<File>();
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 <parameters_file> "
+ "[\"interactive\"]");
System.exit(1);
}
}

View file

@ -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<File> suiteParentDirectories;
private Collection<File> 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<File> checkSuiteParentDirectories(Properties props) {
String value = getRequiredProperty(props, PROP_SUITE_DIRECTORIES);
List<File> dirs = new ArrayList<File>();
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<File> getSuiteParentDirectories() {
return suiteParentDirectories;
}
public void setSelectedSuites(Collection<File> selectedSuites) {
this.selectedSuites = selectedSuites;
}
public Collection<File> getSelectedSuites() {
return new ArrayList<File>(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<File> 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;
}
}
}));
}
}

View file

@ -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.");
}
}

View file

@ -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()
+ "'");
}
}
}