1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2011, Oracle and/or its affiliates. All rights reserved.
   5  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   6  *
   7  * This code is free software; you can redistribute it and/or modify it
   8  * under the terms of the GNU General Public License version 2 only, as
   9  * published by the Free Software Foundation.  Oracle designates this
  10  * particular file as subject to the "Classpath" exception as provided
  11  * by Oracle in the LICENSE file that accompanied this code.
  12  *
  13  * This code is distributed in the hope that it will be useful, but WITHOUT
  14  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  15  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  16  * version 2 for more details (a copy is included in the LICENSE file that
  17  * accompanied this code).
  18  *
  19  * You should have received a copy of the GNU General Public License version
  20  * 2 along with this work; if not, write to the Free Software Foundation,
  21  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  22  *
  23  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  24  * or visit www.oracle.com if you need additional information or have any
  25  * questions.
  26  */
  27 package com.sun.javatest;
  28 
  29 import java.io.File;
  30 import java.text.Collator;
  31 import java.util.Arrays;
  32 import java.util.Comparator;
  33 import java.util.HashMap;
  34 import java.util.Iterator;
  35 import java.util.Locale;
  36 import java.util.Map;
  37 import java.util.Vector;
  38 
  39 import com.sun.javatest.util.I18NResourceBundle;
  40 import com.sun.javatest.util.StringArray;
  41 
  42 /**
  43  * Base implementation for test finders which search for test descriptions
  44  * given a starting location.  When creating instances of TestFinder for use,
  45  * the creator should be sure to call the init() method before use.
  46  */
  47 public abstract class TestFinder
  48 {
  49     /**
  50      * This exception is to report serious problems that occur while
  51      * finding tests.
  52      */
  53     public static class Fault extends Exception
  54     {
  55         /**
  56          * Create a Fault.
  57          * @param i18n A resource bundle in which to find the detail message.
  58          * @param msgKey The key for the detail message.
  59          */
  60         public Fault(I18NResourceBundle i18n, String msgKey) {
  61             super(i18n.getString(msgKey));
  62         }
  63 
  64         /**
  65          * Create a Fault.
  66          * @param i18n A resource bundle in which to find the detail message.
  67          * @param msgKey The key for the detail message.
  68          * @param arg An argument to be formatted with the detail message by
  69          * {@link java.text.MessageFormat#format}
  70          */
  71         public Fault(I18NResourceBundle i18n, String msgKey, Object arg) {
  72             super(i18n.getString(msgKey, arg));
  73         }
  74 
  75         /**
  76          * Create a Fault.
  77          * @param i18n A resource bundle in which to find the detail message.
  78          * @param msgKey The key for the detail message.
  79          * @param args An array of arguments to be formatted with the detail message by
  80          * {@link java.text.MessageFormat#format}
  81          */
  82         public Fault(I18NResourceBundle i18n, String msgKey, Object[] args) {
  83             super(i18n.getString(msgKey, args));
  84         }
  85     }
  86 
  87     /**
  88      * This interface is used to report significant errors found while
  89      * reading files, but which are not of themselves serious enough
  90      * to stop reading further. More serious errors can be reported by
  91      * throwing TestFinder.Fault.
  92      * @see TestFinder#error
  93      * @see TestFinder#localizedError
  94      * @see TestFinder.Fault
  95      */
  96     public static interface ErrorHandler {
  97         /**
  98          * Report an error found while reading a file.
  99          * @param msg A detail string identifying the error
 100          */
 101         void error(String msg);
 102     }
 103 
 104 
 105     /**
 106      * Initialize the data required by the finder.
 107      * Clients creating instances of test finders should call this before allowing use
 108      * of the finder.  Not doing so may result in unexpected results.
 109      * @param args
 110      *          An array of strings specified as arguments in the environment.  Null
 111      *          indicates no args.
 112      * @param testSuiteRoot
 113      *          The root file that will be passed to test descriptions read
 114      *          by the finder.
 115      * @param env
 116      *          The environment being used to run the test.  May be null.
 117      * @throws TestFinder.Fault if  there is a problem interpreting any of args.
 118      */
 119     public void init(String[] args, File testSuiteRoot, TestEnvironment env) throws Fault {
 120         if (args != null)
 121             decodeAllArgs(args);
 122 
 123         setRoot(testSuiteRoot);
 124         this.env = env;
 125     }
 126 
 127     /**
 128      * Initialize the data required by the finder.
 129      * Clients creating instances of test finders should call this before allowing use
 130      * of the finder.  Not doing so may result in unexpected results.
 131      * @param args
 132      *          An array of strings specified as arguments in the environment.  Null
 133      *          indicates no args.
 134      * @param testSuiteRoot
 135      *          The root file that will be passed to test descriptions read
 136      *          by the finder.
 137      * @param tests
 138      *          The tests to be read by the finder. (ignored)
 139      * @param filters
 140      *          An optional array of filters to filter the tests read by the finder.
 141      * @param env
 142      *          The environment being used to run the test.  May be null.
 143      * @throws TestFinder.Fault if there is a problem interpreting any of args.
 144      * @see #init(String[],File,TestEnvironment)
 145      * @deprecated Use one of the other init() methods.  This functionality is no
 146      *    longer supported.  Methods on TestResultTable should yield similar
 147      *    results.
 148      */
 149     public void init(String[] args, File testSuiteRoot,
 150                      File[] tests, TestFilter[] filters,
 151                      TestEnvironment env) throws Fault {
 152         init(args, testSuiteRoot, env);
 153     }
 154 
 155     /**
 156      * Perform argument decoding, calling decodeArg for successive
 157      * args until exhausted.
 158      * @param args The arguments to be decoded
 159      * @throws TestFinder.Fault if decodeArg throws the exception
 160      * while decoding one of the arguments, or if decodeArg does
 161      * not recognize an argument.
 162      */
 163     protected void decodeAllArgs(String[] args) throws Fault {
 164         for (int i = 0; i < args.length; ) {
 165             int j  = decodeArg(args, i);
 166             if (j == 0) {
 167                 throw new Fault(i18n, "finder.badArg", args[i]);
 168             }
 169             i += j;
 170         }
 171     }
 172 
 173     /**
 174      * Decode the arg at a specified position in the arg array.
 175      * If overridden by a subtype, the subtype should try and decode any
 176      * args it recognizes, and then call super.decodeArg to give the
 177      * superclass(es) a chance to recognize any arguments.
 178      * @param args      The array of arguments
 179      * @param i         The next argument to be decoded
 180      * @return          The number of elements consumed in the array;
 181      *                  for example, for a simple option like "-v" the
 182      *                  result should be 1; for an option with an argument
 183      *                  like "-f file" the result should be 2, etc.
 184      * @throws TestFinder.Fault If there is a problem with the value of the current
 185      *                  arg, such as a bad value to an option, the Fault
 186      *                  exception can be thrown. The exception should NOT be
 187      *                  thrown if the current arg is unrecognized: in that case,
 188      *                  an implementation should delegate the call to the
 189      *                  supertype.
 190      */
 191     protected int decodeArg(String[] args, int i) throws Fault {
 192         return 0;
 193     }
 194 
 195     /**
 196      * Set the test suite root file or directory.
 197      * @param testSuiteRoot The path to be set as the root of the
 198      * test suite in which files will be read.
 199      * @throws IllegalStateException if already set
 200      * @throws TestFinder.Fault if there is some test-finder-specific
 201      * problem with the specified file.
 202      * @see #getRoot
 203      */
 204     protected void setRoot(File testSuiteRoot) throws IllegalStateException, Fault {
 205         if (root != null)
 206             throw new IllegalStateException("root already set");
 207 
 208         root = (testSuiteRoot.isAbsolute() ?
 209                 testSuiteRoot : new File(userDir, testSuiteRoot.getPath()));
 210         rootDir = (root.isDirectory() ?
 211                    root : new File(root.getParent()));
 212     }
 213 
 214     /**
 215      * Get the root file of the test suite, as passed in to the
 216      * <code>init</code> method.
 217      * @return the root file of the test suite
 218      * @see #setRoot
 219      */
 220     public File getRoot() {
 221         return root;
 222     }
 223 
 224     /**
 225      * Get the root directory of the test suite; this is either the
 226      * root passed in to the init method or if that is a file, it is
 227      * the directory containing the file.
 228      * @return the root directory of the test suite
 229      */
 230     public File getRootDir() {
 231         return rootDir;
 232     }
 233 
 234     //--------------------------------------------------------------------------
 235 
 236     /**
 237      * Incoming files and test descriptions are sorted by their name during
 238      * processing, this method allows adjustment of the comparison method to
 239      * be used during this sorting.  Sorting can be disabled by calling this
 240      * method with a <code>null</code> parameter.  By default, this class will
 241      * do US Locale sorting.
 242      *
 243      * @param c The comparison operator to be used.  Null indicates no sorting (old behavior).
 244      * @see #getComparator
 245      * @see #foundTestDescription(TestDescription)
 246      * @see #foundFile(File)
 247      * @since 3.2
 248      */
 249     public void setComparator(Comparator<String> c) {
 250         comp = c;
 251 
 252     }
 253 
 254     /**
 255      * Get the current comparator being used.
 256      *
 257      * @return The current comparator, may be null.
 258      * @see #setComparator
 259      * @since 3.2
 260      */
 261     public Comparator<String> getComparator() {
 262         return comp;
 263     }
 264 
 265     /**
 266      * Get the default to be used when the user does not want to specify
 267      * their own.  The default is a US Locale Collator.
 268      * @return The comparator which would be used if a custom one was not provided.
 269      */
 270     protected static Comparator<String> getDefaultComparator() {
 271         // this is the default
 272         final Collator c = Collator.getInstance(Locale.US);
 273         c.setStrength(Collator.PRIMARY);
 274         return new Comparator<String>() {
 275             @Override
 276             public int compare(String s1, String s2) {
 277                 return c.compare(s1, s2);
 278             }
 279         };
 280     }
 281 
 282     //--------------------------------------------------------------------------
 283 
 284     /**
 285      * Get the registered error handler.
 286      * @return The error handler currently receiving error messages.  May
 287      *          be null.
 288      * @see #setErrorHandler
 289      */
 290     public ErrorHandler getErrorHandler() {
 291         return errHandler;
 292     }
 293 
 294     /**
 295      * Set an error handler to be informed of errors that may arise
 296      * while reading tests. This is typically used to report errors
 297      * that are not associated with any specific test, such as syntax
 298      * errors outside of any test description, or problems accessing files.
 299      * @param h The error handler that will be informed of non-fatal
 300      *          errors that occur while reading the test suite
 301      * @see #getErrorHandler
 302      */
 303     public void setErrorHandler(ErrorHandler h) {
 304         errHandler = h;
 305     }
 306 
 307     /**
 308      * Report an error to the error handler.
 309      * @param i18n A resource bundle containing the localized error messages
 310      * @param key  The name of the entry in the resource bundle containing
 311      * the appropriate error message.
 312      * The message should not need any arguments.
 313      */
 314     protected void error(I18NResourceBundle i18n, String key) {
 315         localizedError(i18n.getString(key));
 316     }
 317 
 318     /**
 319      * Report an error to the error handler.
 320      * @param i18n A resource bundle containing the localized error messages
 321      * @param key  The name of the entry in the resource bundle containing
 322      * the appropriate error message.
 323      * The message will be formatted with a single argument, using
 324      * MessageFormat.format.
 325      * @param arg The argument to be formatted in the message found in the
 326      * resource bundle
 327      */
 328     protected void error(I18NResourceBundle i18n, String key, Object arg) {
 329         localizedError(i18n.getString(key, arg));
 330     }
 331 
 332     /**
 333      * Report an error to the error handler.
 334      * @param i18n A resource bundle containing the localized error messages
 335      * @param key  The name of the entry in the resource bundle containing
 336      * the appropriate error message.
 337      * The message will be formatted with an array of arguments, using
 338      * MessageFormat.format.
 339      * @param args The arguments to be formatted in the message found in the
 340      * resource bundle
 341      */
 342     protected void error(I18NResourceBundle i18n, String key, Object[] args) {
 343         localizedError(i18n.getString(key, args));
 344     }
 345 
 346     /**
 347      * Report a message to the error handler, without additional processing.
 348      * @param msg The message to be reported
 349      * @see #error
 350      */
 351     protected void localizedError(String msg) {
 352         errorMessages.add(msg);
 353         if (errHandler != null)
 354             errHandler.error(msg);
 355     }
 356 
 357     /**
 358      * Get an count of the number of errors found by this test finder,
 359      * as recorded by calls to the error handler via error and localizedError.
 360      * The count may be reset using the clearErrors method.
 361      * @return the number of errors found by the test finder
 362      * @see #getErrors
 363      * @see #clearErrors
 364      */
 365     public synchronized int getErrorCount() {
 366         return errorMessages.size();
 367     }
 368 
 369     /**
 370      * Get the errors recorded by the test finder, as recorded by calls
 371      * to the error handler via error and localizedError. Errors reported
 372      * by the error methods will be given localized. If there are no errors.\,
 373      * an empty array (not null) will be returned.
 374      * @return the errors found by the test finder
 375      */
 376     public synchronized String[] getErrors() {
 377         String[] errs = new String[errorMessages.size()];
 378         errorMessages.copyInto(errs);
 379         return errs;
 380     }
 381 
 382     /**
 383      * Clear outstanding errors found by the test finder, so that until
 384      * a new error is reported, getErrorCount will return 0 and getErrors
 385      * will return an empty array.
 386      */
 387     public synchronized void clearErrors() {
 388         errorMessages.setSize(0);
 389     }
 390 
 391 
 392     //--------------------------------------------------------------------------
 393 
 394     /**
 395      * Determine whether a location corresponds to a directory (folder) or
 396      * an actual file.  If the finder implementation chooses, the locations
 397      * used in read() and scan() may be real or virtual.  This method will be
 398      * queried to determine if a location is a container or something that
 399      * should be scanned for tests.  If it is both...
 400      * @since 4.0
 401      * @param path The location in question.
 402      */
 403     public boolean isFolder(File path) {
 404         if (!path.isAbsolute()) {
 405             File f = new File(getRoot(), path.getPath());
 406             return f.isDirectory();
 407         }
 408         else
 409             return path.isDirectory();
 410     }
 411 
 412     /**
 413      * Determine when the last time this path was modified.  This is used
 414      * to decide whether to rescan that location or not.  The default implementation
 415      * defers the choice to the java.
 416      * @since 4.0
 417      * @param f The location in question.
 418      */
 419     public long lastModified(File f) {
 420         if (f.isAbsolute())
 421             return f.lastModified();
 422         else {
 423             File real = new File(getRoot(), f.getPath());
 424             return real.lastModified();
 425         }
 426     }
 427 
 428     /**
 429      * Read a file, looking for test descriptions and other files that might
 430      * need to be read.  If the file is relative, it will be evaluated relative
 431      * to getRootDir. Depending on the test finder, the file may be either
 432      * a plain file or a directory.
 433      * @param file The file to be read.
 434      */
 435     public synchronized void read(File file) {
 436         if (tests != null)
 437             tests.setSize(0);
 438 
 439         if (files != null)
 440             files.setSize(0);
 441 
 442         testsInFile.clear();
 443 
 444         scan(file.isAbsolute() ? file : new File(rootDir, file.getPath()));
 445         //scan(file);
 446     }
 447 
 448     /**
 449      * Scan a file, looking for test descriptions and other files that might
 450      * need to be scanned.  The implementation depends on the type of test
 451      * finder.
 452      * @param file The file to scan
 453      */
 454     protected abstract void scan(File file);
 455 
 456     /**
 457      * Handle a test description entry read from a file.
 458      * By default, the name-value pair is inserted into the entries
 459      * dictionary; however, the method can be overridden by a subtype
 460      * to adjust the name or value before putting it into the dictionary,
 461      * or even to ignore/fault the pair.
 462      * @param entries   The dictionary of the entries being read
 463      * @param name      The name of the entry that has been read
 464      * @param value     The value of the entry that has been read
 465      */
 466     protected void processEntry(Map<String, String> entries, String name, String value) {
 467         // uniquefy the keys as they go into the entries table
 468         name = name.intern();
 469 
 470         if (name.equalsIgnoreCase("keywords")) {
 471             // canonicalize keywords in their own special table
 472             String keywordCacheValue = keywordCache.get(value);
 473             if (keywordCacheValue == null) {
 474                 String lv = value.toLowerCase();
 475                 String[] lvs = StringArray.split(lv);
 476                 Arrays.sort(lvs);
 477                 keywordCacheValue = StringArray.join(lvs).intern();
 478                 keywordCache.put(value, keywordCacheValue);
 479             }
 480             value = keywordCacheValue;
 481         }
 482         else
 483             value = value.intern();
 484 
 485         entries.put(name, value);
 486     }
 487 
 488     // cache for canonicalized lists of keywords
 489     private Map<String, String> keywordCache = new HashMap<>();
 490 
 491     /**
 492      * "normalize" the test description entries read from a file.
 493      * By default, this is a no-op;  however, the method can be overridden
 494      * by a subtype to supply default values for missing entries, etc.
 495      * @param entries  A set of tag values read from a test description in a file
 496      * @return       A normalized set of entries
 497      */
 498     protected Map<String, String> normalize(Map<String, String> entries) {
 499         return entries;
 500     }
 501 
 502 
 503     //--------------------------------------------------------------------------
 504 
 505     /**
 506      * Report that data for a test description has been found.
 507      * @param entries The data for the test description
 508      * @param file   The file being read
 509      * @param line   The line number within the file (used for error messages)
 510      */
 511     protected void foundTestDescription(Map<String, String> entries, File file, int line) {
 512         entries = normalize(entries);
 513 
 514         if (debug) {
 515             System.err.println("Found TestDescription");
 516 
 517             System.err.println("--------values----------------------------");
 518             for (Iterator<String> i = entries.keySet().iterator() ; i.hasNext() ;) {
 519                 Object key = i.next();
 520                 System.err.println(">> " + key + ": " + entries.get(key) );
 521             }
 522             System.err.println("------------------------------------------");
 523         }
 524 
 525         String id = entries.get("id");
 526         if (id == null)
 527             id = "";
 528 
 529         // make sure test has unique id within file
 530         Integer prevLine = testsInFile.get(id);
 531         if (prevLine != null) {
 532             int i = 1;
 533             String newId;
 534             while (testsInFile.get(newId = (id + "__" + i)) != null)
 535                 i++;
 536 
 537             error(i18n, "finder.nonUniqueId",
 538                   new Object[] { file,
 539                                      (id.equals("") ? "(unset)" : id),
 540                                      new Integer(line),
 541                                      prevLine,
 542                                      newId }
 543                   );
 544 
 545             id = newId;
 546             entries.put("id", id);
 547         }
 548 
 549         testsInFile.put(id, new Integer(line));
 550 
 551         // create the test description
 552         TestDescription td = new TestDescription(root, file, entries);
 553 
 554         if (errHandler != null) {
 555             // more checks: check that the path does not include white space,
 556             // because the exclude list parser does not handle paths with whitespace
 557             String rru = td.getRootRelativeURL();
 558             if (rru.indexOf(' ') != -1) {
 559                 error(i18n, "finder.spaceInId", td.getRootRelativeURL());
 560             }
 561         }
 562 
 563         foundTestDescription(td);
 564     }
 565 
 566     /**
 567      * Report that a test description has been found.
 568      * @param td     The data for the test description.  May never be null.
 569      * @see #foundTestDescription(java.util.Map, java.io.File, int)
 570      */
 571     protected void foundTestDescription(TestDescription td) {
 572         if (debug) {
 573             System.err.println("Found TestDescription" + td.getName());
 574         }
 575 
 576         if (tests == null)
 577             tests = new Vector<>();
 578 
 579         int target = 0;
 580 
 581         // binary insert
 582         if (tests.size() == 0) {
 583             target = 0;
 584         }
 585         else if (comp == null) {
 586             target = tests.size();  // at end
 587         }
 588         else {
 589             int left = 0, right = tests.size()-1, center = 0;
 590             String name = td.getName();
 591 
 592             while (left < right) {
 593                 center = ((right+left)/2);
 594                 int cmp = comp.compare(name, tests.get(center).getName());
 595                 if (cmp < 0)
 596                     right = center;
 597                 else if (cmp >= 0)
 598                     left = center+1;
 599             }   // while
 600 
 601             if (comp.compare(name, tests.get(left).getName()) > 0)
 602                 target = left+1;
 603             else
 604                 target = left;
 605 
 606             /* old insertion sort
 607             for (int i = 0; i < tests.size(); i++) {
 608                 if (comp.compare(td.getName(),
 609                                 ((TestDescription)tests.elementAt(i)).getName()) > 0) {
 610                     target = i;
 611                     break;
 612                 }
 613                 else { }
 614             }   // for
 615             */
 616         }
 617 
 618         tests.insertElementAt(td, target);
 619     }
 620 
 621     /**
 622      * Get the test descriptions that were found by the most recent call
 623      * of read.
 624      * @return the test descriptions that were found by the most recent call
 625      * of read.
 626      * @see #read
 627      * @see #foundTestDescription
 628      */
 629     public TestDescription[] getTests() {
 630         if (tests == null)
 631             return noTests;
 632         else {
 633             TestDescription[] tds = new TestDescription[tests.size()];
 634             tests.copyInto(tds);
 635             return tds;
 636         }
 637     }
 638 
 639     private static final TestDescription[] noTests = { };
 640 
 641     /**
 642      * Report that another file that needs to be read has been found.
 643      * @param newFile the file that has been found that needs to be read.
 644      * @see #read
 645      * @see #getFiles
 646      */
 647     protected void foundFile(File newFile) {
 648         if (files == null)
 649             files = new Vector<>();
 650 
 651         int target = 0;
 652 
 653         // binary insert
 654         if (files.size() == 0) {
 655             target = 0;
 656         }
 657         else if (comp == null) {
 658             target = files.size();  // at end
 659         }
 660         else {
 661             int left = 0, right = files.size()-1, center = 0;
 662             String path = newFile.getPath();
 663 
 664             while (left < right) {
 665                 center = ((right+left)/2);
 666                 int cmp = comp.compare(path, files.get(center).getPath());
 667                 if (cmp < 0)
 668                     right = center;
 669                 else if (cmp >= 0)
 670                     left = center+1;
 671             }   // while
 672 
 673             if (comp.compare(path, files.get(left).getPath()) > 0)
 674                 target = left+1;
 675             else
 676                 target = left;
 677         }
 678 
 679         // this is insertion sort to get locale sensitive sorting of
 680         // the test suite content
 681         /*
 682         int target = files.size();
 683 
 684         if (comp != null) {
 685             for (int i = 0; i < files.size(); i++) {
 686                 if (comp.compare(newFile.getPath(),
 687                                 ((File)files.elementAt(i)).getPath()) < 0) {
 688                     target = i;
 689                     break;
 690                 }
 691                 else { }
 692             }   // for
 693         }
 694         else {
 695             // just let it insert at the end
 696         }
 697         */
 698 
 699         files.insertElementAt(newFile, target);
 700     }
 701 
 702     /**
 703      * Get the files that were found by the most recent call
 704      * of read.
 705      * @return the files that were found by the most recent call of read.
 706      * @see #read
 707      * @see #foundFile
 708      */
 709     public File[] getFiles() {
 710         if (files == null)
 711             return new File[0];
 712         else {
 713             File[] fs = new File[files.size()];
 714             files.copyInto(fs);
 715             return fs;
 716         }
 717     }
 718 
 719 
 720     //----------member variables------------------------------------------------
 721     private File root;
 722     private File rootDir;
 723 
 724     /**
 725      * The environment passed in when the test finder was initialized.
 726      * It is not used by the basic test finder code, but may be used
 727      * by individual test finders to modify test descriptions as they are
 728      * read.
 729      * @deprecated This feature was available in earlier versions of
 730      * JT Harness but does not interact well with JT Harness 3.0's GUI features.
 731      * Use with discretion, if at all.
 732      */
 733     protected TestEnvironment env;
 734     private ErrorHandler errHandler;
 735     private Comparator<String> comp = getDefaultComparator();
 736 
 737     private Vector<File> files;
 738     private Vector<TestDescription> tests;
 739 
 740     private Map<String, Integer> testsInFile = new HashMap<>();
 741 
 742     private Vector<String> errorMessages = new Vector<>();
 743 
 744     /**
 745      * A boolean to enable trace output while debugging test finders.
 746      */
 747     protected static boolean debug = Boolean.getBoolean("debug." + TestFinder.class.getName());
 748     private static final File userDir = new File(System.getProperty("user.dir"));
 749     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestFinder.class);
 750 
 751 }