blob: 2f37e73b3fb31dff80bcf258be4510c67518f18f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
/*
* $Id$
*/
/*
*
* XSLTestHarness.java
*
*/
package org.apache.qetest.xsl;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.qetest.FileBasedTest;
import org.apache.qetest.Logger;
import org.apache.qetest.QetestUtils;
import org.apache.qetest.Reporter;
//-------------------------------------------------------------------------
/**
* Utility to run multiple FileBasedTest objects in a row.
* <p>Generally run from the command line and passed a list
* of tests to execute, the XSLTestHarness will run each test in
* order, saving the results of each test for reporting later.</p>
* <p>User must have supplied minimal legal properties in the input
* Properties file: outputDir, inputDir, logFile, and tests.</p>
* @todo update to accept per-test.properties and pass'em thru
* @todo update to check for similarly named tests (in different pkgs)
* @todo update TestReporter et al to better cover case when
* user doesn't call testCaseClose (where do results go?)
* @todo report on memory usage, etc.
* @author Shane_Curcuru@lotus.com
* @version $Id$
*/
public class XSLTestHarness
{
/**
* Convenience method to print out usage information.
* @return String denoting usage suitable for printing
*/
public String usage()
{
FileBasedTest tmp = new FileBasedTest();
return ("XSLTestHarness - execute multiple Tests in sequence and log results:\n"
+ " Usage: java XSLTestHarness [-load] properties.prop\n"
+ " Reads in all options from a Properties file:\n"
+ " " + OPT_TESTS + "=semicolon;delimited;list;of FQCNs tests to run\n"
+ " Most other options (in prop file only) are identical to FileBasedTest:\n"
+ tmp.usage()
);
}
/**
* Various property names we're expecting.
* <ul>
* <li>tests=TestOne;TestTwo;TestThree - semicolon-delimited list of
* TestClassNames to execute, in order; assumes all are in the
* org.apache.qetest. as base package currently (subject to change)</li>
* <li>logFile=LogFileName - name of output XML file to store harness
* log data in (passed to Reporter; constant in FileBasedTest.java)</li>
* <li>inputDir=path\\to\\tests - where the tests should find data</li>
* <li>outputDir=path\\to\\output - where the tests should send output and results</li>
* <li>goldDir=path\\to\\golds - where the tests should find gold files</li>
* <li>loggingLevel=50 - how much output tests should produce</li>
* <li>resultsViewer=Filename.xsl - reference to results processing stylesheet file</li>
* <li>Any other options are passed as-is to individual tests</li>
* </ul>
* <p>Currently each test has it's own logFile in the outputDir,
* named after the test.</p>
*/
/**
* Parameter: semicolong delimited list of FQCN's of test names.
* <p>Default: none - this parameter is required. If the name
* is not package-complete, the harness may attempt to 'guess'
* the correct package underneath org.apache.qetest.</p>
*/
public static final String OPT_TESTS = "tests";
/** Delimiter for OPT_TESTS. */
public static final String TESTS_DELIMITER = ";";
/**
* We prepend the default package if any test name does not
* have a '.' in it.
* This is part of our 'guess' at the appropriate packagename.
* <b>WARNING!</b> Subject to change!
*/
public static final String DEFAULT_PACKAGE = "org.apache.qetest.";
/** Separator character for package.ClassName. */
public static final String DOT = ".";
/** Default extension for logFiles. */
public static final String LOG_EXTENSION = ".xml";
/**
* Generic Properties block for storing initialization info.
* All startup options get stored in here for later use, both by
* the test itself and by any Reporters we use.
*/
protected Properties harnessProps;
/** Our Reporter, who we tell all our secrets to. */
protected Reporter reporter;
/**
* Setup any options and construct a list of tests to execute.
* <p>Accesses our class variables harnessProps and debug.
* Must not use Reporter, since it hasn't been created yet.</p>
* @param args array of command line arguments
* @return array of testClassNames to execute; null if error
*/
protected String[] doTestHarnessInit(String args[])
{
// Harness loads all info from one properties file
// semi-HACK: accept and ignore -load as first arg only
String propFileName = null;
if ("-load".equalsIgnoreCase(args[0]))
{
propFileName = args[1];
}
else
{
propFileName = args[0];
}
try
{
// Load named file into our properties block
FileInputStream fIS = new FileInputStream(propFileName);
harnessProps = new Properties();
harnessProps.load(fIS);
}
catch (IOException ioe)
{
System.err.println("ERROR! loading properties file failed: " + propFileName);
ioe.printStackTrace();
return null;
}
// Grab the list of tests, which is specific only to the harness
String testNames = harnessProps.getProperty(OPT_TESTS);
if ((testNames == null) || (testNames.length() == 0))
{
System.err.println("ERROR! No tests(1) were supplied in the properties file!");
return null;
}
// Split up the list of names
StringTokenizer st = new StringTokenizer(testNames, TESTS_DELIMITER);
int testCount = st.countTokens();
if (testCount == 0)
{
System.err.println("ERROR! No tests(2) were supplied in the properties file!");
return null;
}
String tests[] = new String[testCount];
for (int i = 0; st.hasMoreTokens(); i++)
{
String s = st.nextToken();
if (s.startsWith("org"))
{
// Assume user specified complete package.ClassName
tests[i] = s;
}
else
{
// Use QetestUtils to find the correct name.
tests[i] = QetestUtils.testClassnameForName(s, QetestUtils.defaultPackages, null);
}
}
// Munge the inputDir and goldDir to use platform path
// separators if needed
String tempS = harnessProps.getProperty(FileBasedTest.OPT_INPUTDIR);
tempS = swapPathDelimiters(tempS);
File tempF = new File(tempS);
if (tempF.exists())
{
harnessProps.put(FileBasedTest.OPT_INPUTDIR, tempS);
}
else
{
System.err.println("ERROR! " + FileBasedTest.OPT_INPUTDIR + " property does not exist! " + tempS);
return null;
}
tempS = harnessProps.getProperty(FileBasedTest.OPT_GOLDDIR);
tempS = swapPathDelimiters(tempS);
tempF = new File(tempS);
if (tempF.exists())
{
harnessProps.put(FileBasedTest.OPT_GOLDDIR, tempS);
}
else
{
System.err.println("WARNING! " + FileBasedTest.OPT_GOLDDIR + " property does not exist! " + tempS);
}
// Also swap around path on outputDir, logFile
tempS = harnessProps.getProperty(FileBasedTest.OPT_OUTPUTDIR);
tempS = swapPathDelimiters(tempS);
tempF = new File(tempS);
if (tempF.exists())
{
harnessProps.put(FileBasedTest.OPT_OUTPUTDIR, tempS);
}
else
{
System.err.println("WARNING! " + FileBasedTest.OPT_OUTPUTDIR + " property does not exist! " + tempS);
}
tempS = harnessProps.getProperty(Logger.OPT_LOGFILE);
tempS = swapPathDelimiters(tempS);
harnessProps.put(Logger.OPT_LOGFILE, tempS);
return tests;
}
/**
* Update a path to use system-dependent delimiter.
*
* Allow user to specify a system-dependent path in the
* properties file we're loaded from, but then let another
* user run the same files on another environment.
*
* I'm drawing a complete blank today on the classic way to
* do this, so don't be disappointed if you look at the code
* and it's goofy.
*/
protected String swapPathDelimiters(String s)
{
if (null == s)
return null;
// If we're not on Windows, swap an apparent Windows-based
// backslash separator with a forward slash separator
// This is because I'm lazy and checkin .properties files
// with Windows based paths, but want unix-based people
// to be able to run the tests as-is
if (File.separatorChar != '\\')
return s.replace('\\', File.separatorChar);
else
return s;
}
/**
* Go run the available tests!
* <p>This is sort-of the equivalent of runTest() in a Test
* object. Each test is run in order, and is the equivalent
* of a testCase for the Harness. The Harness records a master
* log file, and each test puts its results in it's own log file.</p>
*/
protected boolean runHarness(String testList[])
{
// Report that we've begun testing
// Note that we're hackishly re-using the 'test' metaphor
// on a grand scale here, where each of the harness'
// testCases corresponds to one entire Test
reporter.testFileInit("Harness", "Harness executing " + testList.length + " tests");
logHarnessProps();
// Note 'passCount' is poorly named: a test may fail but
// may still return true from runTest. You really have to
// look at the result files to see real test status
int passCount = 0;
int nonPassCount = 0;
// Run each test in order!
for (int testIdx = 0; testIdx < testList.length; testIdx++)
{
boolean testStat = false;
try
{
// This method logs out status to our log file, as well
// as initializing and running the test
testStat = runOneTest(testList[testIdx], harnessProps);
}
catch (Throwable t)
{
// Catch everything, log it, and move on
reporter.checkErr("Test " + testList[testIdx] + " threw: " + t.toString());
reporter.logThrowable(reporter.ERRORMSG, t, "Test "
+ testList[testIdx] + " threw: " + t.toString());
}
finally
{
if (testStat)
passCount++;
else
nonPassCount++;
}
}
// Below line is not a 'check': each runOneTest call logs it's own status
// Only for information; remember that the runTest status is not the pass/fail of the test!
reporter.logCriticalMsg("All tests complete, testStatOK:" + passCount + " testStatNOTOK:" + nonPassCount);
// Have the reporter write out a summary file for us
reporter.writeResultsStatus(true);
// Close reporter and return true only if all tests passed
// Note the passCount/nonPassCount are misnomers, since they
// really only report if a test aborted, not passed
reporter.testFileClose();
if ((passCount < 0) && (nonPassCount == 0))
return true;
else
return false;
}
/**
* Run a single FileBasedTest and report it's results.
* <p>Uses our class field reporter to dump our results to, also
* creates a separate reporter for the test to use.</p>
* <p>See the code for the specific initialization we custom-craft for
* each individual test. Basically we clone our harnessProps, update the
* logFile and outputDir per test, and create a testReporter, then use these
* to initialize the test before we call runTest on it.</p>
* @param testName FQCN of the test to execute; must be instanceof FileBasedTest
* @param hProps property block to use as initializer
* @return the pass/fail return from runTest(), which is not necessarily
* the same as what we're going to log as the test's result
*/
protected boolean runOneTest(String testName, Properties hProps)
{
// Report on what we're about to do
reporter.testCaseInit("runOneTest:" + testName);
// Validate our basic arguments
if ((testName == null) || (testName.length() == 0) || (hProps == null))
{
reporter.checkErr("runOneTest called with bad arguments!");
reporter.testCaseClose();
return false;
}
// Calculate just the ClassName of the test for later use as the logFile name
String bareClassName = null;
StringTokenizer st = new StringTokenizer(testName, ".");
for (bareClassName = st.nextToken(); st.hasMoreTokens(); bareClassName = st.nextToken())
{ /* empty loop body */
}
st = null; // no longer needed
// Validate that the output directory exists for the test to put it's results in
String testOutDir = hProps.getProperty(FileBasedTest.OPT_OUTPUTDIR);
if ((testOutDir == null) || (testOutDir.length() == 0))
{
// Default to current dir plus the bareClassName if not set
testOutDir = new String("." + File.separator + bareClassName);
}
else
{
// Append the bareClassName so different tests don't clobber each other
testOutDir += File.separator + bareClassName;
}
File oDir = new File(testOutDir);
if (!oDir.exists())
{
if (!oDir.mkdirs())
{
// Report this but keep going anyway
reporter.logErrorMsg("Could not create testOutDir: " + testOutDir);
}
}
// no longer needed
oDir = null;
// Validate we can instantiate the test object itself
reporter.logTraceMsg("About to newInstance(" + testName + ")");
FileBasedTest test = null;
try
{
Class testClass = Class.forName(testName);
test = (FileBasedTest)testClass.newInstance();
}
catch (Exception e1)
{
reporter.checkErr("Could not create test, threw: " + e1.toString());
reporter.logThrowable(reporter.ERRORMSG, e1, "Could not create test, threw");
reporter.testCaseClose();
return false;
}
// Create a properties block for the test and pre-fill it with custom info
// Start with the harness' properties, and then replace certain values
Properties testProps = (Properties)hProps.clone();
testProps.put(FileBasedTest.OPT_OUTPUTDIR, testOutDir);
testProps.put(Logger.OPT_LOGFILE, testOutDir + LOG_EXTENSION);
// Disable the ConsoleReporter for the *individual* tests, it's too confusing
testProps.put("noDefaultReporter", "true");
reporter.logHashtable(reporter.INFOMSG, testProps, "testProps before test creation");
// Initialize the test with the properties we created
test.setProperties(testProps);
boolean testInit = test.initializeFromProperties(testProps);
reporter.logInfoMsg("Test(" + testName + ").initializeFromProperties() = " + testInit);
// -----------------
// Execute the test!
// -----------------
boolean runTestStat = test.runTest(testProps);
// Report where the test stored it's results - future use
// by multiViewResults.xsl or some other rolledup report
// Note we should really handle the filenames here better,
// especially for relative vs. absolute issues
Hashtable h = new Hashtable(2);
h.put("result", reporter.resultToString(test.getReporter().getCurrentFileResult()));
h.put("fileRef", (String)testProps.get(Logger.OPT_LOGFILE));
reporter.logElement(reporter.WARNINGMSG, "resultsfile", h, test.getTestDescription());
h = null; // no longer needed
// Call worker method to actually calculate the result and call check*()
logTestResult(bareClassName, test.getReporter().getCurrentFileResult(),
runTestStat, test.getAbortTest());
// Cleanup local variables and garbage collect, in case tests don't
// release all resources or something
testProps = null;
test = null;
logMemory(); // Side effect: System.gc()
reporter.testCaseClose();
return runTestStat;
}
/**
* Convenience method to report the result of a single test.
* <p>Depending on the test's return value, it's currentFileResult,
* and if it was ever aborted, we call check to our reporter to log it.</p>
* @param testName basic name of the test
* @param testResult result of whole test file
* @param testStat return value from test.runTest()
* @param testAborted if the test was aborted at all
*/
protected void logTestResult(String testName, int testResult, boolean testStat, boolean testAborted)
{
// Report the 'rolled-up' results of the test, combining each of the above data
switch (testResult)
{
case Logger.INCP_RESULT:
// There is no 'checkIncomplete' method, so simply avoid calling check at all
reporter.logErrorMsg(testName + ".runTest() returned INCP_RESULT!");
break;
case Logger.PASS_RESULT:
// Only report a pass if it returned true and didn't abort
if (testStat && (!testAborted))
{
reporter.checkPass(testName + ".runTest()");
}
else
{
// Assume something went wrong and call it an ERRR
reporter.checkErr(testName + ".runTest()");
}
break;
case Logger.AMBG_RESULT:
reporter.checkAmbiguous(testName + ".runTest()");
break;
case Logger.FAIL_RESULT:
reporter.checkFail(testName + ".runTest()");
break;
case Logger.ERRR_RESULT:
reporter.checkErr(testName + ".runTest()");
break;
default:
// Assume something went wrong
// (always 'err' on the safe side, ha, ha)
reporter.checkErr(testName + ".runTest()");
break;
}
}
/**
* Convenience method to log out any version or system info.
* <p>Logs System.getProperties(), the harnessProps block, plus
* info about the classpath.</p>
*/
protected void logHarnessProps()
{
reporter.logHashtable(reporter.WARNINGMSG, System.getProperties(), "System.getProperties");
reporter.logHashtable(reporter.WARNINGMSG, harnessProps, "harnessProps");
// Since we're running a bunch of tests, also check which version
// of various jars we're running against
logClasspathInfo(System.getProperty("java.class.path"));
}
/**
* Convenience method to log out misc info about your classpath.
* @param classpath presumably the java.class.path to search for jars
*/
protected void logClasspathInfo(String classpath)
{
StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
for (int i = 0; st.hasMoreTokens(); i++)
{
logClasspathItem(st.nextToken());
}
}
/**
* Convenience method to log out misc info about a single classpath entry.
* <p>Implicitly looks for specific jars, namely xalan.jar, xerces.jar, etc.</p>
* @param filename classpath entry to report about
*/
protected void logClasspathItem(String filename)
{
// Make sure the comparison names are all lower case
// This allows us to do case-insensitive compares, but
// actually use the case-sensitive filename for lookups
String filenameLC = filename.toLowerCase();
String checknames[] = { "xalan.jar", "xerces.jar", "testxsl.jar", "minitest.jar"};
for (int i = 0; i < checknames.length; i++)
{
if (filenameLC.indexOf(checknames[i]) > -1)
{
File f = new File(filename);
if (f.exists())
{
Hashtable h = new Hashtable(4);
h.put("jarname", checknames[i]);
h.put("length", String.valueOf(f.length()));
h.put("lastModified", String.valueOf(f.lastModified()));
h.put("path", f.getAbsolutePath());
reporter.logElement(Reporter.INFOMSG, "classpathitem", h, null);
}
}
}
}
/**
* Cheap-o memory logger - just reports Runtime.totalMemory/freeMemory.
*/
protected void logMemory()
{
Runtime r = Runtime.getRuntime();
r.gc();
reporter.logPerfMsg("UMem", r.freeMemory(), "freeMemory");
reporter.logPerfMsg("UMem", r.totalMemory(), "totalMemory");
}
/**
* Run the test harness to execute the specified tests.
*/
public void doMain(String args[])
{
// Must have at least one arg to continue
if ((args == null) || (args.length == 0))
{
System.err.println("ERROR in usage: must have at least one argument");
System.err.println(usage());
return;
}
// Initialize ourselves and a list of tests to execute
// Side effects: sets harnessProps, debug
String tests[] = doTestHarnessInit(args);
if (tests == null)
{
System.err.println("ERROR in usage: Problem during initialization - no tests!");
System.err.println(usage());
return;
}
// Use a separate copy of our properties to init our Reporter
Properties reporterProps = (Properties)harnessProps.clone();
// Ensure we have an XMLFileLogger if we have a logName
String logF = reporterProps.getProperty(Logger.OPT_LOGFILE);
if ((logF != null) && (!logF.equals("")))
{
// We should ensure there's an XMLFileReporter
String r = reporterProps.getProperty(Reporter.OPT_LOGGERS);
if (r == null)
{
reporterProps.put(Reporter.OPT_LOGGERS,
"org.apache.qetest.XMLFileLogger");
}
else if (r.indexOf("XMLFileLogger") <= 0)
{
reporterProps.put(Reporter.OPT_LOGGERS,
r + Reporter.LOGGER_SEPARATOR
+ "org.apache.qetest.XMLFileLogger");
}
}
// Ensure we have a ConsoleLogger unless asked not to
// @todo improve and document this feature
String noDefault = reporterProps.getProperty("noDefaultReporter");
if (noDefault == null)
{
// We should ensure there's an XMLFileReporter
String r = reporterProps.getProperty(Reporter.OPT_LOGGERS);
if (r == null)
{
reporterProps.put(Reporter.OPT_LOGGERS,
"org.apache.qetest.ConsoleLogger");
}
else if (r.indexOf("ConsoleLogger") <= 0)
{
reporterProps.put(Reporter.OPT_LOGGERS,
r + Reporter.LOGGER_SEPARATOR
+ "org.apache.qetest.ConsoleLogger");
}
}
// A Reporter will auto-initialize from the values
// in the properties block
reporter = new Reporter(reporterProps);
reporter.addDefaultLogger(); // add default logger if needed
// Call worker method to actually run all the tests
// Worker method manages all it's own reporting, including
// calling testFileInit/testFileClose
boolean notUsed = runHarness(tests);
// Tell user if a logFile should have been saved
String logFile = reporterProps.getProperty(Logger.OPT_LOGFILE);
if (logFile != null)
{
System.out.println("");
System.out.println("Hey! A summary-harness logFile was written to: " + logFile);
}
}
/**
* Main method to run the harness from the command line.
*/
public static void main (String[] args)
{
XSLTestHarness app = new XSLTestHarness();
app.doMain(args);
}
} // end of class XSLTestHarness