/*
* $Id$
*
* Copyright (c) 1996, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javatest;
import java.io.File;
import java.text.Collator;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Vector;
import com.sun.javatest.util.I18NResourceBundle;
import com.sun.javatest.util.StringArray;
/**
* Base implementation for test finders which search for test descriptions
* given a starting location. When creating instances of TestFinder for use,
* the creator should be sure to call the init() method before use.
*/
public abstract class TestFinder
{
/**
* This exception is to report serious problems that occur while
* finding tests.
*/
public static class Fault extends Exception
{
/**
* Create a Fault.
* @param i18n A resource bundle in which to find the detail message.
* @param msgKey The key for the detail message.
*/
public Fault(I18NResourceBundle i18n, String msgKey) {
super(i18n.getString(msgKey));
}
/**
* Create a Fault.
* @param i18n A resource bundle in which to find the detail message.
* @param msgKey The key for the detail message.
* @param arg An argument to be formatted with the detail message by
* {@link java.text.MessageFormat#format}
*/
public Fault(I18NResourceBundle i18n, String msgKey, Object arg) {
super(i18n.getString(msgKey, arg));
}
/**
* Create a Fault.
* @param i18n A resource bundle in which to find the detail message.
* @param msgKey The key for the detail message.
* @param args An array of arguments to be formatted with the detail message by
* {@link java.text.MessageFormat#format}
*/
public Fault(I18NResourceBundle i18n, String msgKey, Object[] args) {
super(i18n.getString(msgKey, args));
}
}
/**
* This interface is used to report significant errors found while
* reading files, but which are not of themselves serious enough
* to stop reading further. More serious errors can be reported by
* throwing TestFinder.Fault.
* @see TestFinder#error
* @see TestFinder#localizedError
* @see TestFinder.Fault
*/
public static interface ErrorHandler {
/**
* Report an error found while reading a file.
* @param msg A detail string identifying the error
*/
void error(String msg);
}
/**
* Initialize the data required by the finder.
* Clients creating instances of test finders should call this before allowing use
* of the finder. Not doing so may result in unexpected results.
* @param args
* An array of strings specified as arguments in the environment. Null
* indicates no args.
* @param testSuiteRoot
* The root file that will be passed to test descriptions read
* by the finder.
* @param env
* The environment being used to run the test. May be null.
* @throws TestFinder.Fault if there is a problem interpreting any of args.
*/
public void init(String[] args, File testSuiteRoot, TestEnvironment env) throws Fault {
if (args != null)
decodeAllArgs(args);
setRoot(testSuiteRoot);
this.env = env;
}
/**
* Initialize the data required by the finder.
* Clients creating instances of test finders should call this before allowing use
* of the finder. Not doing so may result in unexpected results.
* @param args
* An array of strings specified as arguments in the environment. Null
* indicates no args.
* @param testSuiteRoot
* The root file that will be passed to test descriptions read
* by the finder.
* @param tests
* The tests to be read by the finder. (ignored)
* @param filters
* An optional array of filters to filter the tests read by the finder.
* @param env
* The environment being used to run the test. May be null.
* @throws TestFinder.Fault if there is a problem interpreting any of args.
* @see #init(String[],File,TestEnvironment)
* @deprecated Use one of the other init() methods. This functionality is no
* longer supported. Methods on TestResultTable should yield similar
* results.
*/
public void init(String[] args, File testSuiteRoot,
File[] tests, TestFilter[] filters,
TestEnvironment env) throws Fault {
init(args, testSuiteRoot, env);
}
/**
* Perform argument decoding, calling decodeArg for successive
* args until exhausted.
* @param args The arguments to be decoded
* @throws TestFinder.Fault if decodeArg throws the exception
* while decoding one of the arguments, or if decodeArg does
* not recognize an argument.
*/
protected void decodeAllArgs(String[] args) throws Fault {
for (int i = 0; i < args.length; ) {
int j = decodeArg(args, i);
if (j == 0) {
throw new Fault(i18n, "finder.badArg", args[i]);
}
i += j;
}
}
/**
* Decode the arg at a specified position in the arg array.
* If overridden by a subtype, the subtype should try and decode any
* args it recognizes, and then call super.decodeArg to give the
* superclass(es) a chance to recognize any arguments.
* @param args The array of arguments
* @param i The next argument to be decoded
* @return The number of elements consumed in the array;
* for example, for a simple option like "-v" the
* result should be 1; for an option with an argument
* like "-f file" the result should be 2, etc.
* @throws TestFinder.Fault If there is a problem with the value of the current
* arg, such as a bad value to an option, the Fault
* exception can be thrown. The exception should NOT be
* thrown if the current arg is unrecognized: in that case,
* an implementation should delegate the call to the
* supertype.
*/
protected int decodeArg(String[] args, int i) throws Fault {
return 0;
}
/**
* Set the test suite root file or directory.
* @param testSuiteRoot The path to be set as the root of the
* test suite in which files will be read.
* @throws IllegalStateException if already set
* @throws TestFinder.Fault if there is some test-finder-specific
* problem with the specified file.
* @see #getRoot
*/
protected void setRoot(File testSuiteRoot) throws IllegalStateException, Fault {
if (root != null)
throw new IllegalStateException("root already set");
root = (testSuiteRoot.isAbsolute() ?
testSuiteRoot : new File(userDir, testSuiteRoot.getPath()));
rootDir = (root.isDirectory() ?
root : new File(root.getParent()));
}
/**
* Get the root file of the test suite, as passed in to the
* init
method.
* @return the root file of the test suite
* @see #setRoot
*/
public File getRoot() {
return root;
}
/**
* Get the root directory of the test suite; this is either the
* root passed in to the init method or if that is a file, it is
* the directory containing the file.
* @return the root directory of the test suite
*/
public File getRootDir() {
return rootDir;
}
//--------------------------------------------------------------------------
/**
* Incoming files and test descriptions are sorted by their name during
* processing, this method allows adjustment of the comparison method to
* be used during this sorting. Sorting can be disabled by calling this
* method with a null
parameter. By default, this class will
* do US Locale sorting.
*
* @param c The comparison operator to be used. Null indicates no sorting (old behavior).
* @see #getComparator
* @see #foundTestDescription(TestDescription)
* @see #foundFile(File)
* @since 3.2
*/
public void setComparator(Comparator c) {
comp = c;
}
/**
* Get the current comparator being used.
*
* @return The current comparator, may be null.
* @see #setComparator
* @since 3.2
*/
public Comparator getComparator() {
return comp;
}
/**
* Get the default to be used when the user does not want to specify
* their own. The default is a US Locale Collator.
* @return The comparator which would be used if a custom one was not provided.
*/
protected static Comparator getDefaultComparator() {
// this is the default
final Collator c = Collator.getInstance(Locale.US);
c.setStrength(Collator.PRIMARY);
return new Comparator() {
@Override
public int compare(String s1, String s2) {
return c.compare(s1, s2);
}
};
}
//--------------------------------------------------------------------------
/**
* Get the registered error handler.
* @return The error handler currently receiving error messages. May
* be null.
* @see #setErrorHandler
*/
public ErrorHandler getErrorHandler() {
return errHandler;
}
/**
* Set an error handler to be informed of errors that may arise
* while reading tests. This is typically used to report errors
* that are not associated with any specific test, such as syntax
* errors outside of any test description, or problems accessing files.
* @param h The error handler that will be informed of non-fatal
* errors that occur while reading the test suite
* @see #getErrorHandler
*/
public void setErrorHandler(ErrorHandler h) {
errHandler = h;
}
/**
* Report an error to the error handler.
* @param i18n A resource bundle containing the localized error messages
* @param key The name of the entry in the resource bundle containing
* the appropriate error message.
* The message should not need any arguments.
*/
protected void error(I18NResourceBundle i18n, String key) {
localizedError(i18n.getString(key));
}
/**
* Report an error to the error handler.
* @param i18n A resource bundle containing the localized error messages
* @param key The name of the entry in the resource bundle containing
* the appropriate error message.
* The message will be formatted with a single argument, using
* MessageFormat.format.
* @param arg The argument to be formatted in the message found in the
* resource bundle
*/
protected void error(I18NResourceBundle i18n, String key, Object arg) {
localizedError(i18n.getString(key, arg));
}
/**
* Report an error to the error handler.
* @param i18n A resource bundle containing the localized error messages
* @param key The name of the entry in the resource bundle containing
* the appropriate error message.
* The message will be formatted with an array of arguments, using
* MessageFormat.format.
* @param args The arguments to be formatted in the message found in the
* resource bundle
*/
protected void error(I18NResourceBundle i18n, String key, Object[] args) {
localizedError(i18n.getString(key, args));
}
/**
* Report a message to the error handler, without additional processing.
* @param msg The message to be reported
* @see #error
*/
protected void localizedError(String msg) {
errorMessages.add(msg);
if (errHandler != null)
errHandler.error(msg);
}
/**
* Get an count of the number of errors found by this test finder,
* as recorded by calls to the error handler via error and localizedError.
* The count may be reset using the clearErrors method.
* @return the number of errors found by the test finder
* @see #getErrors
* @see #clearErrors
*/
public synchronized int getErrorCount() {
return errorMessages.size();
}
/**
* Get the errors recorded by the test finder, as recorded by calls
* to the error handler via error and localizedError. Errors reported
* by the error methods will be given localized. If there are no errors.\,
* an empty array (not null) will be returned.
* @return the errors found by the test finder
*/
public synchronized String[] getErrors() {
String[] errs = new String[errorMessages.size()];
errorMessages.copyInto(errs);
return errs;
}
/**
* Clear outstanding errors found by the test finder, so that until
* a new error is reported, getErrorCount will return 0 and getErrors
* will return an empty array.
*/
public synchronized void clearErrors() {
errorMessages.setSize(0);
}
//--------------------------------------------------------------------------
/**
* Determine whether a location corresponds to a directory (folder) or
* an actual file. If the finder implementation chooses, the locations
* used in read() and scan() may be real or virtual. This method will be
* queried to determine if a location is a container or something that
* should be scanned for tests. If it is both...
* @since 4.0
* @param path The location in question.
*/
public boolean isFolder(File path) {
if (!path.isAbsolute()) {
File f = new File(getRoot(), path.getPath());
return f.isDirectory();
}
else
return path.isDirectory();
}
/**
* Determine when the last time this path was modified. This is used
* to decide whether to rescan that location or not. The default implementation
* defers the choice to the java.
* @since 4.0
* @param f The location in question.
*/
public long lastModified(File f) {
if (f.isAbsolute())
return f.lastModified();
else {
File real = new File(getRoot(), f.getPath());
return real.lastModified();
}
}
/**
* Read a file, looking for test descriptions and other files that might
* need to be read. If the file is relative, it will be evaluated relative
* to getRootDir. Depending on the test finder, the file may be either
* a plain file or a directory.
* @param file The file to be read.
*/
public synchronized void read(File file) {
if (tests != null)
tests.setSize(0);
if (files != null)
files.setSize(0);
testsInFile.clear();
scan(file.isAbsolute() ? file : new File(rootDir, file.getPath()));
//scan(file);
}
/**
* Scan a file, looking for test descriptions and other files that might
* need to be scanned. The implementation depends on the type of test
* finder.
* @param file The file to scan
*/
protected abstract void scan(File file);
/**
* Handle a test description entry read from a file.
* By default, the name-value pair is inserted into the entries
* dictionary; however, the method can be overridden by a subtype
* to adjust the name or value before putting it into the dictionary,
* or even to ignore/fault the pair.
* @param entries The dictionary of the entries being read
* @param name The name of the entry that has been read
* @param value The value of the entry that has been read
*/
protected void processEntry(Map entries, String name, String value) {
// uniquefy the keys as they go into the entries table
name = name.intern();
if (name.equalsIgnoreCase("keywords")) {
// canonicalize keywords in their own special table
String keywordCacheValue = keywordCache.get(value);
if (keywordCacheValue == null) {
String lv = value.toLowerCase();
String[] lvs = StringArray.split(lv);
Arrays.sort(lvs);
keywordCacheValue = StringArray.join(lvs).intern();
keywordCache.put(value, keywordCacheValue);
}
value = keywordCacheValue;
}
else
value = value.intern();
entries.put(name, value);
}
// cache for canonicalized lists of keywords
private Map keywordCache = new HashMap<>();
/**
* "normalize" the test description entries read from a file.
* By default, this is a no-op; however, the method can be overridden
* by a subtype to supply default values for missing entries, etc.
* @param entries A set of tag values read from a test description in a file
* @return A normalized set of entries
*/
protected Map normalize(Map entries) {
return entries;
}
//--------------------------------------------------------------------------
/**
* Report that data for a test description has been found.
* @param entries The data for the test description
* @param file The file being read
* @param line The line number within the file (used for error messages)
*/
protected void foundTestDescription(Map entries, File file, int line) {
entries = normalize(entries);
if (debug) {
System.err.println("Found TestDescription");
System.err.println("--------values----------------------------");
for (Iterator i = entries.keySet().iterator() ; i.hasNext() ;) {
Object key = i.next();
System.err.println(">> " + key + ": " + entries.get(key) );
}
System.err.println("------------------------------------------");
}
String id = entries.get("id");
if (id == null)
id = "";
// make sure test has unique id within file
Integer prevLine = testsInFile.get(id);
if (prevLine != null) {
int i = 1;
String newId;
while (testsInFile.get(newId = (id + "__" + i)) != null)
i++;
error(i18n, "finder.nonUniqueId",
new Object[] { file,
(id.equals("") ? "(unset)" : id),
new Integer(line),
prevLine,
newId }
);
id = newId;
entries.put("id", id);
}
testsInFile.put(id, new Integer(line));
// create the test description
TestDescription td = new TestDescription(root, file, entries);
if (errHandler != null) {
// more checks: check that the path does not include white space,
// because the exclude list parser does not handle paths with whitespace
String rru = td.getRootRelativeURL();
if (rru.indexOf(' ') != -1) {
error(i18n, "finder.spaceInId", td.getRootRelativeURL());
}
}
foundTestDescription(td);
}
/**
* Report that a test description has been found.
* @param td The data for the test description. May never be null.
* @see #foundTestDescription(java.util.Map, java.io.File, int)
*/
protected void foundTestDescription(TestDescription td) {
if (debug) {
System.err.println("Found TestDescription" + td.getName());
}
if (tests == null)
tests = new Vector<>();
int target = 0;
// binary insert
if (tests.size() == 0) {
target = 0;
}
else if (comp == null) {
target = tests.size(); // at end
}
else {
int left = 0, right = tests.size()-1, center = 0;
String name = td.getName();
while (left < right) {
center = ((right+left)/2);
int cmp = comp.compare(name, tests.get(center).getName());
if (cmp < 0)
right = center;
else if (cmp >= 0)
left = center+1;
} // while
if (comp.compare(name, tests.get(left).getName()) > 0)
target = left+1;
else
target = left;
/* old insertion sort
for (int i = 0; i < tests.size(); i++) {
if (comp.compare(td.getName(),
((TestDescription)tests.elementAt(i)).getName()) > 0) {
target = i;
break;
}
else { }
} // for
*/
}
tests.insertElementAt(td, target);
}
/**
* Get the test descriptions that were found by the most recent call
* of read.
* @return the test descriptions that were found by the most recent call
* of read.
* @see #read
* @see #foundTestDescription
*/
public TestDescription[] getTests() {
if (tests == null)
return noTests;
else {
TestDescription[] tds = new TestDescription[tests.size()];
tests.copyInto(tds);
return tds;
}
}
private static final TestDescription[] noTests = { };
/**
* Report that another file that needs to be read has been found.
* @param newFile the file that has been found that needs to be read.
* @see #read
* @see #getFiles
*/
protected void foundFile(File newFile) {
if (files == null)
files = new Vector<>();
int target = 0;
// binary insert
if (files.size() == 0) {
target = 0;
}
else if (comp == null) {
target = files.size(); // at end
}
else {
int left = 0, right = files.size()-1, center = 0;
String path = newFile.getPath();
while (left < right) {
center = ((right+left)/2);
int cmp = comp.compare(path, files.get(center).getPath());
if (cmp < 0)
right = center;
else if (cmp >= 0)
left = center+1;
} // while
if (comp.compare(path, files.get(left).getPath()) > 0)
target = left+1;
else
target = left;
}
// this is insertion sort to get locale sensitive sorting of
// the test suite content
/*
int target = files.size();
if (comp != null) {
for (int i = 0; i < files.size(); i++) {
if (comp.compare(newFile.getPath(),
((File)files.elementAt(i)).getPath()) < 0) {
target = i;
break;
}
else { }
} // for
}
else {
// just let it insert at the end
}
*/
files.insertElementAt(newFile, target);
}
/**
* Get the files that were found by the most recent call
* of read.
* @return the files that were found by the most recent call of read.
* @see #read
* @see #foundFile
*/
public File[] getFiles() {
if (files == null)
return new File[0];
else {
File[] fs = new File[files.size()];
files.copyInto(fs);
return fs;
}
}
//----------member variables------------------------------------------------
private File root;
private File rootDir;
/**
* The environment passed in when the test finder was initialized.
* It is not used by the basic test finder code, but may be used
* by individual test finders to modify test descriptions as they are
* read.
* @deprecated This feature was available in earlier versions of
* JT Harness but does not interact well with JT Harness 3.0's GUI features.
* Use with discretion, if at all.
*/
protected TestEnvironment env;
private ErrorHandler errHandler;
private Comparator comp = getDefaultComparator();
private Vector files;
private Vector tests;
private Map testsInFile = new HashMap<>();
private Vector errorMessages = new Vector<>();
/**
* A boolean to enable trace output while debugging test finders.
*/
protected static boolean debug = Boolean.getBoolean("debug." + TestFinder.class.getName());
private static final File userDir = new File(System.getProperty("user.dir"));
private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestFinder.class);
}