/* * 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$ */ package org.apache.qetest; import java.io.File; import java.io.FilenameFilter; import java.lang.reflect.Constructor; import java.util.Enumeration; import java.util.Properties; import java.util.Vector; /** * Generic Test driver for FileTestlets. * *
This driver provides basic services for iterating over a tree * of test files and executing a specified testlet on each test that * is selected by a set of specified filters. It automatically handles * iteration and optional recursion down the tree, and by default * assumes there are three 'matching' trees for inputs, golds, and * creates a tree for outputs.
* *Key methods are separated into worker methods so subclasses can * override just the parts of the algorithm they need to change.
* *//@todo move and refactor XSLProcessorTestBase to * be more generic and reduce dependencies; also reduce dependency * on internal variables and instead always use lookups into * our testProps object.
* * @author shane_curcuru@us.ibm.com * @version $Id$ */ public class FileTestletDriver extends FileBasedTest /// extends XSLProcessorTestBase { //----------------------------------------------------- //-------- Constants for common input params -------- //----------------------------------------------------- /** * Parameter: Run a specific list of files, instead of * iterating over directories. *Default: null, do normal iteration.
*/ public static final String OPT_FILELIST = "fileList"; /** * Parameter: FQCN or simple classname of Testlet to use. *User may pass in either a FQCN or just a base classname, * and we will attempt to look it up in any of the most common * Xalan-testing packages. See QetestUtils.testClassForName().
*Default: null, use StylesheetTestlet.
*/ public static final String OPT_TESTLET = "testlet"; /** Classname of Testlet to use. */ protected String testlet = null; /** * Parameter: FQCN or simple classname of FilenameFilter for * directories under testDir we will process. * If fileList is not set, we simply go to our inputDir, and * then use this filter to iterate through directories returned. *Default: null, use ConformanceDirRules.
*/ public static final String OPT_DIRFILTER = "dirFilter"; /** Classname of FilenameFilter to use for dirs. */ protected String dirFilter = null; /** * Parameter: FQCN or simple classname of FilenameFilter for * files within subdirs we will process. * If fileList is not set, we simply go through all directories * specified by directoryFilter, and then use this filter to * find all stylesheet test files in that directory to test. * Note that this does not handle embedded tests, where * the XML document has an xml-stylesheet PI that defines the * stylesheet to use to process it. *Default: null, use ConformanceFileRules.
*/ public static final String OPT_FILEFILTER = "fileFilter"; /** Classname of FilenameFilter to use for files. */ protected String fileFilter = null; /** Unique runId for each specific invocation of this test driver. */ protected String runId = null; /** Convenience constant: .gold extension for gold files. */ public static final String GLD_EXTENSION = ".gld"; /** Convenience constant: .out extension for output result file. */ public static final String OUT_EXTENSION = ".out"; /** Just initialize test name, comment; numTestCases is not used. */ public FileTestletDriver() { testName = "FileTestletDriver"; testComment = "Test driver for File-based Testlets"; } /** * Initialize this test - fill in parameters. * Simply fills in convenience variables from user parameters. * * @param p unused * @return true */ public boolean doTestFileInit(Properties p) { // Copy any of our parameters from testProps to // our local convenience variables testlet = testProps.getProperty(OPT_TESTLET, testlet); dirFilter = testProps.getProperty(OPT_DIRFILTER, dirFilter); fileFilter = testProps.getProperty(OPT_FILEFILTER, fileFilter); // Grab a unique runid for logging out with our tests // Used in results reporting stylesheets to differentiate // between different test runs runId = QetestUtils.createRunId(testProps.getProperty("runId")); testProps.put("runId", runId); // put back in the properties // for later use return true; } /** * Run through the directory given to us and run tests found * in subdirs; or run through our fileList. * * This method logs some basic runtime data (like the actual * testlet and ProcessorWrapper implementations used) and * then decides to either run a user-specified fileList or to * use our dirFilter to iterate over the inputDir. * * @param p Properties block of options to use - unused * @return true if OK, false if we should abort */ public boolean runTestCases(Properties p) { // First log out any other runtime information, like the // actual current testlet and filters try { // Note that each of these calls actually force the // creation of an actual object of each type: this is // required since we may default the types or our call // to QetestUtils.testClassForName() may return a // different classname than the user actually specified // Care should be taken that the construction of objects // here does not affect our testing later on Properties runtimeProps = new Properties(); // ... and add a few extra things ourselves runtimeProps.put("actual.testlet", getTestlet()); runtimeProps.put("actual.dirFilter", getDirFilter()); runtimeProps.put("actual.fileFilter", getFileFilter()); reporter.logHashtable(Logger.CRITICALMSG, runtimeProps, "actual.runtime information"); } catch (Exception e) { // This is not necessarily an error reporter.logThrowable(Logger.WARNINGMSG, e, "Logging actual.runtime threw"); } // Now either run a list of specific tests the user specified, // or do the default of iterating over a set of directories String fileList = testProps.getProperty(OPT_FILELIST); if (null != fileList) { // Process the specific list of tests the user supplied String desc = "User-supplied fileList: " + fileList; // provide default value // Use static worker class to process the list Vector datalets = FileDataletManager.readFileList(reporter, fileList, desc, testProps); // Actually process the specified files in a testCase processFileList(datalets, desc); } else { // Do the default, which is to iterate over the inputDir // Note that this calls the testCaseInit/testCaseClose // logging methods itself processInputDir(); } return true; } /** * Do the default: test all files found in subdirs * of our inputDir, using FilenameFilters for dirs and files. * Parameters: none, uses our internal members inputDir, * outputDir, goldDir, etc. Will attempt to use a default * inputDir if the specified one doesn't exist. * * This is a special case of recurseSubDir, since we report * differently from the top level. */ public void processInputDir() { // Ensure the inputDir is there - we must have a valid location for input files File topInputDir = new File(inputDir); if (!topInputDir.exists()) { // Try a default inputDir String oldInputDir = inputDir; // cache for potential error message topInputDir = new File((inputDir = getDefaultInputDir())); if (!topInputDir.exists()) { // No inputDir, can't do any tests! // Note we put this in a fake testCase, since this // is likely the only thing our test reports reporter.testCaseInit("processInputDir - mock testcase"); reporter.checkErr("topInputDir(" + oldInputDir + ", or " + inputDir + ") does not exist, aborting!"); reporter.testCaseClose(); return; } } FileDatalet topDirs = new FileDatalet(topInputDir.getPath(), outputDir, goldDir); // Optionally process this topDirs, and always recurse at // least one level below it recurseSubDir(topDirs, getProcessTopDir(), true); } /** * Optionally process all the files in this dir and optionally * recurse downwards using our dirFilter. * * This is a pre-order traversal; we process files in this * dir first and then optionally recurse. * * @param base FileDatalet representing the input, output, * gold directory triplet we should use * @param process if we should call processSubDir on this dir * @param recurse if we should recurse below this directory, * or just stop here after processSubDir() */ public void recurseSubDir(FileDatalet base, boolean process, boolean recurse) { // Process this directory first: pre-order traversal if (process) processSubDir(base); if (!recurse) return; // If we should recurse, do so now File inputDir = new File(base.getInput()); FilenameFilter filter = getDirFilter(); reporter.logTraceMsg("recurseSubDir(" + inputDir.getPath() + ") looking for subdirs with: " + filter); // Use our filter to get a list of directories to process String subdirs[] = inputDir.list(filter); // Validate that we have some valid directories to process if ((null == subdirs) || (subdirs.length <= 0)) { reporter.logWarningMsg("recurseSubDir(" + inputDir.getPath() + ") no valid subdirs found!"); return; } // For every subdirectory, check if we should run tests in it for (int i = 0; i < subdirs.length; i++) { File subTestDir = new File(inputDir, subdirs[i]); if ((null == subTestDir) || (!subTestDir.exists())) { // Just log it and continue; presumably we'll find // other directories to test reporter.logWarningMsg("subTestDir(" + subTestDir.getPath() + ") does not exist, skipping!"); continue; } FileDatalet subdir = new FileDatalet(base, subdirs[i]); // Process each other directory, and optionally continue // to recurse downwards recurseSubDir(subdir, true, getRecurseDirs()); } // end of for... } /** * Process a single subdirectory and run our testlet over * every file found by our fileFilter therein. * * @param base FileDatalet representing the input, output, * gold directory triplet we should use */ public void processSubDir(FileDatalet base) { // Validate that each of the specified dirs exists // Ask it to be strict in ensuring output, gold are created if (!base.validate(true)) { // Just log it and continue; presumably we'll find // other directories to test reporter.logWarningMsg("processSubDir(" + base.getInput() + ", " + base.getOutput() + ", " + base.getGold() + ") some dir does not exist, skipping!"); return; } File subInputDir = new File(base.getInput()); // Call worker method to process the individual directory // and get a list of .xsl files to test Vector files = getFilesFromDir(subInputDir, getFileFilter()); if ((null == files) || (0 == files.size())) { reporter.logStatusMsg("processSubDir(" + base.getInput() + ") no files found(1), skipping!"); return; } // 'Transform' the list of individual test files into a // list of Datalets with all fields filled in //@todo should getFilesFromDir and buildDatalets be combined? Vector datalets = buildDatalets(files, base); if ((null == datalets) || (0 == datalets.size())) { reporter.logWarningMsg("processSubDir(" + base.getInput() + ") no tests found(2), skipping!"); return; } // Now process the list of files found in this dir processFileList(datalets, "Testing subdir: " + base.getInput()); } /** * Run a list of stylesheet tests through a Testlet. * The file names are assumed to be fully specified, and we assume * the corresponding directories exist. * Each fileList is turned into a testcase. * * @param vector of Datalet objects to pass in * @param desc String to use as testCase description */ public void processFileList(Vector datalets, String desc) { // Validate arguments if ((null == datalets) || (0 == datalets.size())) { // Bad arguments, report it as an error // Note: normally, this should never happen, since // this class normally validates these arguments // before calling us reporter.checkErr("processFileList: Testlet or datalets are null/blank, nothing to test!"); return; } // Put each fileList into a testCase reporter.testCaseInit(desc); // Now just go through the list and process each set int numDatalets = datalets.size(); reporter.logInfoMsg("processFileList() with " + numDatalets + " potential tests"); // Iterate over every datalet and test it for (int ctr = 0; ctr < numDatalets; ctr++) { try { // Create a Testlet to execute a test with this // next datalet - the Testlet will log all info // about the test, including calling check*() getTestlet().execute((Datalet)datalets.elementAt(ctr)); } catch (Throwable t) { // Log any exceptions as fails and keep going reporter.logThrowable(Logger.ERRORMSG, t, "Datalet threw"); reporter.checkErr("Datalet num " + ctr + " threw: " + t.toString()); } } // of while... reporter.testCaseClose(); } /** * Use the supplied filter on given directory to return a list * of tests to be run. * * The real logic is in the filter, which can be specified as * an option or by overriding getDefaultFileFilter(). * * @param dir directory to scan * @param filter to use on this directory; if null, uses default * @return Vector of local path\filenames of tests to run; * the tests themselves will exist; null if error */ public Vector getFilesFromDir(File dir, FilenameFilter filter) { // Validate arguments if ((null == dir) || (!dir.exists())) { // Bad arguments, report it as an error // Note: normally, this should never happen, since // this class normally validates these arguments // before calling us reporter.logWarningMsg("getFilesFromDir(" + dir.toString() + ") dir null or does not exist"); return null; } // Get the list of 'normal' test files String[] files = dir.list(filter); Vector v = new Vector(files.length); for (int i = 0; i < files.length; i++) { v.addElement(files[i]); } reporter.logTraceMsg("getFilesFromDir(" + dir.toString() + ") found " + v.size() + " total files to test"); return v; } /** * Transform a vector of individual test names into a Vector * of filled-in datalets to be tested * * This basically just calculates local path\filenames across * the three presumably-parallel directory trees of * inputDir, outputDir and goldDir. * It then stuffs each of these values plus some * generic info like our testProps into each datalet it creates. * * @param files Vector of local path\filenames to be tested * @param base FileDatalet denoting directories * input, output, gold * @return Vector of FileDatalets that are fully filled in, * i.e. output, gold, etc are filled in respectively * to input */ public Vector buildDatalets(Vector files, FileDatalet base) { // Validate arguments if ((null == files) || (files.size() < 1)) { // Bad arguments, report it as an error // Note: normally, this should never happen, since // this class normally validates these arguments // before calling us reporter.logWarningMsg("buildDatalets null or empty file vector"); return null; } Vector v = new Vector(files.size()); // For every file in the vector, construct the matching // out, gold, and xml/xsl files for (Enumeration elements = files.elements(); elements.hasMoreElements(); /* no increment portion */ ) { String file = null; try { file = (String)elements.nextElement(); } catch (ClassCastException cce) { // Just skip this entry reporter.logWarningMsg("Bad file element found, skipping: " + cce.toString()); continue; } v.addElement(buildDatalet(base, file)); } return v; } /** * Construct a FileDatalet with corresponding output, gold files. * * This basically just calls worker methods to construct and * set options on a datalet to return. * * @param base FileDatalet denoting directories * input, output, gold * @param name bare name of the input file * @return FileDatalet that is fully filled in, * i.e. output, gold, etc are filled in respectively * to input and any options are set */ protected FileDatalet buildDatalet(FileDatalet base, String name) { // Worker method to construct paths FileDatalet d = buildDataletPaths(base, name); // Worker method to set any other options, etc. setDataletOptions(d); return d; } /** * Construct a FileDatalet with corresponding output, gold files. * * This worker method just has the logic to construct the * corresponding output and gold filenames; feel free to subclass. * * This class simply appends .out and .gld to the end of the * existing names: foo.xml: foo.xml.out, foo.xml.gld. * * @param base FileDatalet denoting directories * input, output, gold * @param name bare name of the input file * @return FileDatalet that is fully filled in, * i.e. output, gold, etc are filled in respectively * to input */ protected FileDatalet buildDataletPaths(FileDatalet base, String name) { return new FileDatalet(base.getInput() + File.separator + name, base.getOutput() + File.separator + name + OUT_EXTENSION, base.getGold() + File.separator + name + GLD_EXTENSION); } /** * Fillin FileDatalet.setOptions and any other processing. * * This is designed to be overriden so subclasses can put any * special items in the datalet's options or do other * preprocessing of the datalet. * * @param base FileDatalet to apply options, etc. to */ protected void setDataletOptions(FileDatalet base) { base.setDescription(base.getInput()); // Optimization: put in a copy of our fileChecker, so // that each testlet doesn't have to create it's own // fileCheckers should not store state, so this // shouldn't affect the testing at all base.setOptions(testProps); // Note: set our options in the datalet first, then // put the fileChecker directly into their options base.getOptions().put("fileCheckerImpl", fileChecker); } /** If we should process the top level directory (default:false). */ protected boolean getProcessTopDir() { return false; } /** If we should always recurse lower level directories (default:false). */ protected boolean getRecurseDirs() { return false; } /** Default FilenameFilter FQCN for directories. */ protected String getDefaultDirFilter() { return "org.apache.qetest.DirFilter"; } /** Default FilenameFilter FQCN for files. */ protected String getDefaultFileFilter() { return "org.apache.qetest.FilePatternFilter"; } /** Default Testlet FQCN for executing stylesheet tests. */ protected String getDefaultTestlet() { return "org.apache.qetest.FileTestlet"; } /** Default list of packages to search for classes. */ protected String[] getDefaultPackages() { return QetestUtils.defaultPackages; } /** Cached Testlet Class; used for life of this test. */ protected Class cachedTestletClazz = null; /** * Convenience method to get a Testlet to use. * Attempts to return one as specified by our testlet parameter, * otherwise returns a default StylesheetTestlet. * * @return Testlet for use in this test; null if error */ public Testlet getTestlet() { // Find a Testlet class to use if we haven't already if (null == cachedTestletClazz) { cachedTestletClazz = QetestUtils.testClassForName(testlet, getDefaultPackages(), getDefaultTestlet()); } try { // Create it and set our reporter into it Testlet t = (Testlet)cachedTestletClazz.newInstance(); t.setLogger((Logger)reporter); return (Testlet)t; } catch (Exception e) { // Ooops, none found! This should be very rare, since // we know the defaultTestlet should be found return null; } } /** * Convenience method to get a default filter for directories. * Uses category member variable if set. * * @return FilenameFilter using DirFilter(category, null). */ public FilenameFilter getDirFilter() { // Find a FilenameFilter class to use Class clazz = QetestUtils.testClassForName(dirFilter, getDefaultPackages(), getDefaultDirFilter()); try { // Create it, optionally with a category String category = testProps.getProperty(OPT_CATEGORY); if ((null != category) && (category.length() > 1)) // Arbitrary check for non-null, non-blank string { Class[] parameterTypes = { java.lang.String.class, java.lang.String.class }; Constructor ctor = clazz.getConstructor(parameterTypes); Object[] ctorArgs = { category, null }; return (FilenameFilter)ctor.newInstance(ctorArgs); } else { return (FilenameFilter)clazz.newInstance(); } } catch (Exception e) { // Ooops, none found! return null; } } /** * Convenience method to get a default filter for files. * Uses excludes member variable if set. * * @return FilenameFilter using FileExtensionFilter(null, excludes). */ public FilenameFilter getFileFilter() { // Find a FilenameFilter class to use Class clazz = QetestUtils.testClassForName(fileFilter, getDefaultPackages(), getDefaultFileFilter()); try { // Create it, optionally with excludes String excludes = testProps.getProperty(OPT_EXCLUDES); if ((null != excludes) && (excludes.length() > 1)) // Arbitrary check for non-null, non-blank string { Class[] parameterTypes = { java.lang.String.class, java.lang.String.class }; Constructor ctor = clazz.getConstructor(parameterTypes); Object[] ctorArgs = { null, excludes }; return (FilenameFilter)ctor.newInstance(ctorArgs); } else { return (FilenameFilter)clazz.newInstance(); } } catch (Exception e) { // Ooops, none found! return null; } } /** * Convenience method to get a default inputDir when none or * a bad one was given. * @return String pathname of default inputDir "tests\conf". */ public String getDefaultInputDir() { return "tests" + File.separator + "conf"; } /** * Convenience method to print out usage information - update if needed. * @return String denoting usage of this test class */ public String usage() { return ("Additional options supported by FileTestletDriver:\n" + " -" + OPT_FILELIST + "