1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2012, 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.BufferedInputStream;
  30 import java.io.File;
  31 import java.io.FileInputStream;
  32 import java.io.FileNotFoundException;
  33 import java.io.InputStream;
  34 import java.io.IOException;
  35 import java.lang.ref.WeakReference;
  36 import java.lang.reflect.InvocationTargetException;
  37 import java.net.MalformedURLException;
  38 import java.net.URL;
  39 import java.net.URLClassLoader;
  40 import java.util.HashMap;
  41 import java.util.Iterator;
  42 import java.util.Map;
  43 import java.util.Properties;
  44 import java.util.Vector;
  45 import java.util.logging.Level;
  46 import java.util.logging.Logger;
  47 import java.util.logging.Handler;
  48 import java.util.logging.LogRecord;
  49 import com.sun.javatest.finder.BinaryTestFinder;
  50 import com.sun.javatest.finder.HTMLTestFinder;
  51 import com.sun.javatest.finder.TestFinderDecorator;
  52 import com.sun.javatest.interview.LegacyParameters;
  53 import com.sun.javatest.lib.KeywordScript;
  54 import com.sun.javatest.logging.WorkDirLogHandler;
  55 import com.sun.javatest.logging.ObservedFile;
  56 import com.sun.javatest.services.ServiceManager;
  57 import com.sun.javatest.services.ServiceReader;
  58 import com.sun.javatest.services.PropertyServiceReader;
  59 import com.sun.javatest.util.BackupPolicy;
  60 import com.sun.javatest.util.I18NResourceBundle;
  61 import com.sun.javatest.util.StringArray;
  62 
  63 import java.lang.reflect.Method;
  64 import java.lang.reflect.Modifier;
  65 
  66 /**
  67  * A class providing information about and access to the tests in a test suite.
  68  * The primary methods to access and run the tests are
  69  * <ul>
  70  * <li>{@link TestSuite#createTestFinder createTestFinder }
  71  * <li>{@link TestSuite#createTestFilter createTestFilter }
  72  * <li>{@link TestSuite#createScript createScript }
  73  * </ul>
  74  */
  75 public class TestSuite
  76 {
  77     /**
  78      * An exception used to report errors while using a TestSUite object.
  79      */
  80     public static class Fault extends Exception
  81     {
  82         /**
  83          * Create a Fault.
  84          * @param i18n A resource bundle in which to find the detail message.
  85          * @param s The key for the detail message.
  86          */
  87         public Fault(I18NResourceBundle i18n, String s) {
  88             super(i18n.getString(s));
  89         }
  90 
  91         /**
  92          * Create a Fault.
  93          * @param i18n A resource bundle in which to find the detail message.
  94          * @param s The key for the detail message.
  95          * @param o An argument to be formatted with the detail message by
  96          * {@link java.text.MessageFormat#format}
  97          */
  98         public Fault(I18NResourceBundle i18n, String s, Object o) {
  99             super(i18n.getString(s, o));
 100         }
 101 
 102         /**
 103          * Create a Fault.
 104          * @param i18n A resource bundle in which to find the detail message.
 105          * @param s The key for the detail message.
 106          * @param o An array of arguments to be formatted with the detail message by
 107          * {@link java.text.MessageFormat#format}
 108          */
 109         public Fault(I18NResourceBundle i18n, String s, Object[] o) {
 110             super(i18n.getString(s, o));
 111         }
 112     }
 113 
 114     /**
 115      * An exception that is used to report that a given file is not a test suite.
 116      */
 117     public static class NotTestSuiteFault extends Fault
 118     {
 119         /**
 120          * Create a Fault.
 121          * @param i18n A resource bundle in which to find the detail message.
 122          * @param s The key for the detail message.
 123          * @param f The file in question, to be formatted with the detail message by
 124          * {@link java.text.MessageFormat#format}
 125          */
 126         public NotTestSuiteFault(I18NResourceBundle i18n, String s, File f) {
 127             super(i18n, s, f.getPath());
 128         }
 129     }
 130 
 131     public static class DuplicateLogNameFault extends Fault
 132     {
 133         /**
 134          * Create a Fault.
 135          * @param i18n A resource bundle in which to find the detail message.
 136          * @param key The internal name of the log.
 137          * {@link java.text.MessageFormat#format}
 138          */
 139         public DuplicateLogNameFault(I18NResourceBundle i18n, String s, String key) {
 140             super(i18n, s, key);
 141         }
 142     }
 143 
 144     public static class NoSuchLogFault extends Fault
 145     {
 146         /**
 147          * Create a Fault.
 148          * @param i18n A resource bundle in which to find the detail message.
 149          * @param key The internal name of the log.
 150          * {@link java.text.MessageFormat#format}
 151          */
 152         public NoSuchLogFault(I18NResourceBundle i18n, String s, String key) {
 153             super(i18n, s, key);
 154         }
 155     }
 156 
 157     /**
 158      * Check if a file is the root of a valid test suite. A valid test suite is identified
 159      * either by the root directory of the test suite, or by the file testsuite.html within
 160      * that directory. The directory must contain either a test suite properties file
 161      * (testsuite.jtt) or, for backwards compatibility, a file named testsuite.html.
 162      * @param root The file to be checked.
 163      * @return true if and only if <em>root</em> is the root of a valid test suite.
 164      */
 165     public static boolean isTestSuite(File root) {
 166         //System.err.println("TestSuite.isTestSuite: " + root);
 167         File dir;
 168         if (root.isDirectory())
 169             dir = root;
 170         else {
 171             if (root.getName().equalsIgnoreCase(TESTSUITE_HTML))
 172                 dir = root.getParentFile();
 173             else
 174                 return false;
 175         }
 176         File jtt = new File(dir, TESTSUITE_JTT);
 177         File parentDir = dir.getParentFile();
 178         File parent_jtt = (parentDir == null ? null : new File(parentDir, TESTSUITE_JTT));
 179         File html = new File(dir, TESTSUITE_HTML);
 180         return (isReadableFile(jtt)
 181                 || isReadableFile(html) && (parent_jtt == null || !parent_jtt.exists()));
 182     }
 183 
 184     /**
 185      * Open a test suite.
 186      * @param root A file identifying the root of the test suite.
 187      * @return A TestSuite object for the test suite in question. The actual type of the result
 188      * will depend on the test suite properties found in the root directory of the test suite.
 189      * @throws FileNotFoundException if <em>root</em> does not exist.
 190      * @throws TestSuite.NotTestSuiteFault if <em>root</em> does not identify a valid test suite.
 191      * @throws TestSuite.Fault if any other problems occur while trying to open the test suite.
 192      * @see #isTestSuite
 193      */
 194     public static TestSuite open(File root) throws FileNotFoundException, Fault, NotTestSuiteFault {
 195         if (!root.exists())
 196             throw new FileNotFoundException(root.getPath());
 197 
 198         File canonRoot;
 199         try {
 200             canonRoot = root.getCanonicalFile();
 201         }
 202         catch (IOException e) {
 203             throw new Fault(i18n, "ts.cantCanonicalize",
 204                             new Object[] {root.getPath(), e.toString()});
 205         }
 206 
 207         File canonRootDir;
 208         if (canonRoot.isDirectory())
 209             canonRootDir = canonRoot;
 210         else {
 211             if (canonRoot.getName().equalsIgnoreCase(TESTSUITE_HTML))
 212                 canonRootDir = canonRoot.getParentFile();
 213             else
 214                 throw new NotTestSuiteFault(i18n, "ts.notTestSuiteFile", canonRoot);
 215         }
 216 
 217         File f = new File(canonRootDir, TESTSUITE_JTT);
 218         if (isReadableFile(f)) {
 219             try {
 220                 Properties p = new Properties();
 221                 InputStream in = new BufferedInputStream(new FileInputStream(f));
 222                 p.load(in);
 223                 in.close();
 224                 return open(canonRoot, p);
 225             }
 226             catch (IOException e) {
 227                 throw new Fault(i18n, "ts.cantReadTestSuiteFile", e.toString());
 228             }
 229         }
 230         else {
 231             // check for old style test suite
 232             File ts_html = new File(canonRootDir, TESTSUITE_HTML);
 233             File parentDir = canonRootDir.getParentFile();
 234             File parent_jtt = (parentDir == null ? null : new File(parentDir, TESTSUITE_JTT));
 235             if (isReadableFile(ts_html) && (parent_jtt == null || !parent_jtt.exists()))
 236                 return open(canonRoot, new HashMap());
 237             else
 238                 throw new NotTestSuiteFault(i18n, "ts.notTestSuiteFile", canonRoot);
 239         }
 240     }
 241 
 242     /**
 243      * Open a test suite.
 244      * @param root A file identifying the root of the test suite.
 245      * @param tsInfo Test Suite properties read from the test suite properties file.
 246      * @return A TestSuite object for the test suite in question.
 247      * @throws TestSuite.Fault if any problems occur while opening the test suite
 248      */
 249     private static TestSuite open(File root, Map tsInfo) throws Fault {
 250         synchronized (dirMap) {
 251             TestSuite ts;
 252 
 253             // if this test suite has already been opened, return that
 254             WeakReference<TestSuite> ref = dirMap.get(root);
 255             if (ref != null) {
 256                 ts = ref.get();
 257                 if (ts != null) {
 258                     return ts;
 259                 }
 260             }
 261 
 262             // otherwise, open it for real
 263             ts = open0(root, tsInfo);
 264 
 265             // save reference in case opened again
 266             dirMap.put(root, new WeakReference<>(ts));
 267             return ts;
 268         }
 269     }
 270 
 271     private static TestSuite open0(File root, Map tsInfo) throws Fault {
 272         String[] classPath = StringArray.split((String) (tsInfo.get("classpath")));
 273 
 274         ClassLoader cl;
 275         if (classPath.length == 0)
 276             cl = null;
 277         else {
 278             try {
 279                 File rootDir = (root.isDirectory() ? root : root.getParentFile());
 280                 URL[] p = new URL[classPath.length];
 281                 for (int i = 0; i < classPath.length; i++) {
 282                     String cpi = classPath[i];
 283                     if (cpi.toLowerCase().startsWith("http:"))
 284                         p[i] = new URL(cpi);
 285                     else {
 286                         File f = new File(cpi);
 287                         if (!f.isAbsolute())
 288                             f = new File(rootDir, cpi);
 289                         p[i] = f.toURI().toURL();
 290                     }
 291                 }
 292                 cl = new URLClassLoader(p, TestSuite.class.getClassLoader());
 293             }
 294             catch (MalformedURLException e) {
 295                 throw new Fault(i18n, "ts.badClassPath",
 296                                 new Object[] {root, e.getMessage()});
 297             }
 298         }
 299 
 300         String[] tsClassAndArgs = StringArray.split((String) (tsInfo.get("testsuite")));
 301 
 302         TestSuite testSuite;
 303         if (tsClassAndArgs.length == 0)
 304             testSuite = new TestSuite(root, tsInfo, cl);
 305         else {
 306             String className = tsClassAndArgs[0];
 307 
 308             try {
 309                 Class c = loadClass(className, cl);
 310                 Class[] tsArgTypes = {File.class, Map.class, ClassLoader.class};
 311                 Object[] tsArgs = {root, tsInfo, cl};
 312                 testSuite = (TestSuite)(newInstance(c, tsArgTypes, tsArgs));
 313             }
 314             catch (ClassCastException e) {
 315                 throw new Fault(i18n, "ts.notASubtype",
 316                                 new Object[] {className, "testsuite", TestSuite.class.getName()});
 317             }
 318             catch (UnsupportedClassVersionError uce){
 319                 throw new Fault(i18n, "ts.compiledRecentVersion",
 320                         new Object[] {System.getProperty("java.version"), root.getPath()});
 321             }
 322 
 323             String[] args = new String[tsClassAndArgs.length - 1];
 324             System.arraycopy(tsClassAndArgs, 1, args, 0, args.length);
 325             testSuite.init(args);
 326         }
 327 
 328         // initialize test finder
 329         testSuite.setTestFinder(new TestFinderDecorator(testSuite.createTestFinder()));
 330 
 331         return testSuite;
 332     }
 333 
 334     /**
 335      * Disposed of the shared TestSuite object for this test suite.  Use
 336      * the value from <code>TestSuite.getRoot()</code> as the value for
 337      * canonRoot.  Using this is only desired when disposal of the shared
 338      * TestSuite object is not desired - traditionally, it is not disposed
 339      * and is reused if the test suite is reopened.
 340      * @param canonRoot Canonical root of the test suite.
 341      * @see TestSuite#getRoot
 342      * @return The object which is about to be discarded.  Null if it was not
 343      *         not cached here.
 344      */
 345     /*
 346     public static TestSuite close(File canonRoot) {
 347         WeakReference ref = (WeakReference)(dirMap.remove(canonRoot));
 348         if (ref != null) {
 349             TestSuite ts = (TestSuite)(ref.get());
 350             if (ts != null) {
 351                 return ts;
 352             }
 353         }
 354 
 355         return null;
 356     }
 357      */
 358 
 359     /**
 360      * Create a TestSuite object.
 361      * @param root The root file for this test suite.
 362      * @param tsInfo Test suite properties, typically read from the test suite properties file
 363      * in the root directory of the test suite.
 364      * @param cl A class loader to be used to load additional classes as required,
 365      * typically using a class path defined in the test suite properties file.
 366      * @throws TestSuite.Fault if a problem occurs while creating this test suite.
 367      */
 368     public TestSuite(File root, Map tsInfo, ClassLoader cl) throws Fault {
 369         this.root = root;
 370         this.tsInfo = tsInfo;
 371         this.loader = cl;
 372 
 373         String kw = (tsInfo == null ? null : (String) (tsInfo.get("keywords")));
 374         keywords = (kw == null ? null : StringArray.split(kw));
 375     }
 376 
 377 
 378     /**
 379      * Create a TestSuite object, with no additional test suite properties and no
 380      * class loader.
 381      * @param root The root file for this test suite.
 382      */
 383     public TestSuite(File root) {
 384         this.root = root;
 385     }
 386 
 387     /**
 388      * Initialize this test suite, with args typically read from a .jtt file.
 389      * The default implementation does not recognize any arguments and always
 390      * throws an exception.
 391      * @param args an array of strings to initialize this test suite object
 392      * @throws TestSuite.Fault if there are any problems initializing the
 393      * test suite from the specified arguments.
 394      */
 395     protected void init(String[] args) throws Fault {
 396         if (args.length > 0)
 397             throw new Fault(i18n, "ts.badArgs", args[0]);
 398         // should be a decodeArgs loop
 399     }
 400 
 401     /**
 402      * Get the path for the root file of this test suite.
 403      * @return the path for the root file of this test suite.
 404      */
 405     public String getPath() {
 406         return root.getPath();
 407     }
 408 
 409     /**
 410      * Get the root file of this test suite.
 411      * @return the root file of this test suite.
 412      */
 413     public File getRoot() {
 414         return root;
 415     }
 416 
 417     /**
 418      * Get the root directory of this test suite. If the root file is itself a directory,
 419      * the result will be that directory; otherwise, the result will be the parent directory
 420      * of the root file.
 421      * @return the root directory of this test suite.
 422      */
 423     public File getRootDir() {
 424         return (root.isDirectory() ? root : new File(root.getParent()));
 425     }
 426 
 427     /**
 428      * Get the directory in the test suite that contains the tests.
 429      * By default, the following are checked:
 430      * <ol>
 431      * <li>The <code>tests</code> property in the test suite properties file.
 432      * If this entry is found, it must either identify an absolute filename, or
 433      * a directory relative to the test suite root directory, using '/' to
 434      * separate the components of the path.
 435      * <li>If the file <em>root</em><code>/tests/testsuite.html</code> exists,
 436      * the result is the directory <em>root</em><code>/tests</code>. This is
 437      * for compatibility with standard TCK layout.
 438      * <li>Otherwise, the result is the root directory of the test suite.
 439      * </ol>
 440      * @return the directory that contains the tests
 441      */
 442     public File getTestsDir() {
 443         String t = (String) (tsInfo == null ? null : tsInfo.get("tests"));
 444         if (t == null || t.length() == 0) {
 445             File rootDir = getRootDir();
 446             File testsDir = new File(rootDir, "tests");
 447             if (testsDir.isDirectory()) {
 448                 // if the tests directory exists, and there is no overriding
 449                 // testsuite.jtt entry, assume the tests dir is "tests/".
 450                 return testsDir;
 451             }
 452             // default
 453             return rootDir;
 454         }
 455         else {
 456             File f = new File(t);
 457             if (f.isAbsolute())
 458                 return f;
 459             else
 460                 return new File(getRootDir(), t.replace('/', File.separatorChar));
 461         }
 462     }
 463 
 464     /**
 465      * A notification method that is called when a test suite run is starting.
 466      * The method may be used to do any test suite specific initialization.
 467      * If overriding this method, be sure to call the superclass' method.  It is
 468      * fairly typical to register as a harness observer inside this method.  Note
 469      * that if an exception occurs during this method, it will be caught by the
 470      * harness and reported as a Harness.Observer.error().  It is recommended
 471      * that any implementations of this method register as an observer immediately
 472      * so that they can catch this error and do any cleanup to abort the
 473      * test suite startup sequence (check if services were started and close them
 474      * down, etc).
 475      * @param harness The harness that will be used to run the tests.
 476      * @throws TestSuite.Fault if an error occurred while doing test suite-specific
 477      * initialization that should cause the test run to be aborted.
 478      */
 479     public void starting(Harness harness) throws Fault {
 480         if (getServiceManager() != null) {
 481             serviceManager.setHarness(harness);
 482         }
 483     }
 484 
 485     /**
 486      * Create a test suite specific filter to be used to filter the tests
 487      * to be selected for a test run.
 488      * The method should return null if no test suite specific filtering is required.
 489      * The default is to return null.
 490      * @param filterEnv Configuration data that may be used by the filter.
 491      * @return a test suite filter, or null if no test suite specific filter is
 492      * required for this test suite.
 493      */
 494     public TestFilter createTestFilter(TestEnvironment filterEnv) {
 495         return null;
 496     }
 497 
 498     /**
 499      * Get a shared test finder to read the tests in this test suite.
 500      * @return a test finder to read the tests in this test suite
 501      * @see #createTestFinder
 502      * @see #setTestFinder
 503      */
 504     public TestFinder getTestFinder() {
 505         return finder;
 506     }
 507 
 508     /**
 509      * Set the shared test finder used to read the tests in this test suite.
 510      * Only one test finder may be set; attempts to change the test finder will
 511      * cause IllegalStateException to be thrown.
 512      * This method is normally called by TestSuite.open to initialize the
 513      * finder to the result of calling createTestFinder.
 514      * @param tf the test finder to be used
 515      * @throws IllegalStateException if the test finder has previously
 516      * been set to a different value
 517      * @see #getTestFinder
 518      */
 519     protected void setTestFinder(TestFinder tf) {
 520         if (tf == null)
 521             throw new NullPointerException();
 522 
 523         if (finder != null && finder != tf)
 524             throw new IllegalStateException();
 525 
 526         finder = tf;
 527     }
 528 
 529     /**
 530      * Create a test finder to be used to access the tests in this test suite.
 531      * The default implementation looks for a <code>finder</code> entry in the
 532      * test suite properties file, which should identify the class to be used
 533      * and any arguments it may require. The class will be loaded via the class
 534      * loader specified when the test suite was opened, if one was given;
 535      * otherwise, the system class loader will be used.
 536      *
 537      * The default implementation attempts to use a file <tt>testsuite.jtd</tt>
 538      * in the tests directory.  If found, a BinaryTestFinder will be created
 539      * using this file.  If it is not found, then it searches for a property
 540      * named <tt>finder</tt> in the test suite properties and will attempt to
 541      * instantiate that.  If no entry is found or it is blank, an
 542      * HTMLTestFinder is used, using whatever a basic settings HTMLTestFinder
 543      * initializes to.
 544      * @return a test finder to be used to read the tests in the test suite
 545      * @throws TestSuite.Fault if there is a problem creating the test finder
 546      * @see #getTestFinder
 547      * @see #setTestFinder
 548      * @see #getTestsDir
 549      */
 550     protected TestFinder createTestFinder() throws Fault {
 551         File testsDir = getTestsDir();
 552 
 553         // no BTF file; look for a finder=class args... entry
 554         String[] finderCmd = StringArray.split((String) (tsInfo.get("finder")));
 555         String finderClassName;
 556         String[] finderArgs = new String[0];
 557 
 558         if (finderCmd == null || finderCmd.length == 0) {
 559             //finderCmd = new String[] {HTMLTestFinder.class.getName()};
 560             finderCmd = null;   // ensure null for later use
 561             finderClassName = HTMLTestFinder.class.getName();
 562         }
 563         else {
 564             finderClassName = finderCmd[0];
 565 
 566             if (finderCmd.length > 1) {
 567                 finderArgs = new String[finderCmd.length - 1];
 568                 System.arraycopy(finderCmd, 1, finderArgs, 0, finderArgs.length);
 569             }
 570             else {
 571                 // finderArgs should remain empty array
 572             }
 573         }
 574 
 575         // first, try looking for testsuite.jtd
 576         String jtd = (String) (tsInfo.get("testsuite.jtd"));
 577         File jtdFile = (jtd == null ? new File(testsDir, "testsuite.jtd") : new File(root, jtd));
 578         if (jtdFile.exists()) {
 579             try {
 580                 // found a file for BinaryTestFinder
 581                 // only pass the finder class if it was not defaulted to HTMLTestFinder
 582                 return createBinaryTestFinder((finderCmd == null ? null : finderClassName),
 583                         finderArgs, testsDir, jtdFile);
 584             }
 585             catch (TestFinder.Fault e) {
 586                 // ignore, try to continue with normal finder
 587             }
 588             catch (Fault f) {
 589                 // ignore, try to continue with normal finder
 590             }
 591         }
 592 
 593         try {
 594             Class c = loadClass(finderClassName);
 595             TestFinder tf = (TestFinder) (newInstance(c));
 596             // called old deprecated entry till we know no-one cares
 597             //tf.init(finderArgs, testsRoot, null, null, tsInfo/*pass in env?*/);
 598             // this likely kills ExpandTestFinder, finally
 599             tf.init(finderArgs, testsDir, null, null, null/*pass in env?*/);
 600             return tf;
 601         }
 602         catch (ClassCastException e) {
 603             throw new Fault(i18n, "ts.notASubtype",
 604                             new Object[] {finderClassName, "finder", TestFinder.class.getName()});
 605         }
 606         catch (TestFinder.Fault e) {
 607             throw new Fault(i18n, "ts.errorInitFinder",
 608                             new Object[] {finderClassName, e.getMessage()});
 609         }
 610     }
 611 
 612     /**
 613      * In the case where a JTD file is found, attempt to load a binary test finder.
 614      * The default implementation attempts to use the finder property in the
 615      * test suite properties if it is a BinaryTestFinder subclass.
 616      *
 617      * @param finderClassName Finder class name to attempt to use as a BTF.  Null if
 618      *      the default BTF class should be used.
 619      * @param finderArgs Arguments to finder given from the test suite property.
 620      * @param testsDir Reference location to pass to finder.
 621      * @param jtdFile Location of the JTD file to give to the BTF.
 622      * @return The binary test finder which was created.
 623      * @throws com.sun.javatest.TestSuite.Fault
 624      * @throws com.sun.javatest.TestFinder.Fault
 625      * @see com.sun.javatest.TestFinder
 626      * @see com.sun.javatest.finder.BinaryTestFinder
 627      */
 628     protected TestFinder createBinaryTestFinder(String finderClassName,
 629             String finderArgs[], File testsDir, File jtdFile) throws Fault, TestFinder.Fault {
 630         try {
 631             TestFinder tf = null;
 632 
 633             if (finderClassName != null) {
 634                 Class c = loadClass(finderClassName);
 635                 tf = (TestFinder) (newInstance(c));
 636             }
 637 
 638             if (tf instanceof BinaryTestFinder) {
 639                 tf.init(finderArgs, testsDir, null, null, null);
 640                 return tf;
 641             }
 642             else {
 643                 return new BinaryTestFinder(testsDir, jtdFile);
 644             }
 645         }
 646         catch (ClassCastException e) {
 647             throw new Fault(i18n, "ts.notASubtype",
 648                             new Object[] {finderClassName, "finder", TestFinder.class.getName()});
 649         }
 650         catch (TestFinder.Fault e) {
 651             throw new Fault(i18n, "ts.errorInitFinder",
 652                             new Object[] {finderClassName, e.getMessage()});
 653         }
 654 
 655     }
 656 
 657     /**
 658      * Create and initialize a TestRunner that can be used to run
 659      * a series of tests.
 660      * The default implementation returns a TestRunner that
 661      * creates a number of test execution threads which each
 662      * create and run a script for each test obtained from
 663      * the test runners iterator.
 664      * @return a TestRunner that can be used to run a series of tests
 665      */
 666     public TestRunner createTestRunner() {
 667         return new DefaultTestRunner();
 668     }
 669 
 670     /**
 671      * Create and initialize a Script that can be used to run a test.
 672      * The default implementation looks for a <code>script</code> entry in the configuration
 673      * data provided, and if not found, looks for a <code>script</code> entry in the
 674      * test suite properties. The script entry should define the script class
 675      * to use and any arguments it may require. The class will be loaded via the class
 676      * loader specified when the test suite was opened, if one was given;
 677      * otherwise, the system class loader will be used. Individual test suites will
 678      * typically use a more direct means to create an appropriate script object.
 679      * The parameters for this method are normally passed through to the script
 680      * that is created.
 681      *
 682      * Note that the name of this method is "create", it is not recommended
 683      * that the value returned ever be re-used or cached for subsequent requests
 684      * to this method.
 685      * @param td The test description for the test to be executed.
 686      * @param exclTestCases Any test cases within the test that should not be executed.
 687      * @param scriptEnv Configuration data to be given to the test as necessary.
 688      * @param workDir A work directory in which to store the results of the test.
 689      * @param backupPolicy A policy object used to control how to backup any files that
 690      * might be overwritten.
 691      * @return a script to be used to execute the given test
 692      * @throws TestSuite.Fault if any errors occur while creating the script
 693      */
 694     public Script createScript(TestDescription td, String[] exclTestCases, TestEnvironment scriptEnv,
 695                                WorkDirectory workDir,
 696                                BackupPolicy backupPolicy) throws Fault {
 697         if (scriptClass == null) {
 698             String[] script = envLookup(scriptEnv, "script");
 699             if (script.length == 0)
 700                 script = StringArray.split((String) tsInfo.get("script"));
 701             if (script.length > 0) {
 702                 scriptClass = loadClass(script[0]);
 703                 if (!Script.class.isAssignableFrom(scriptClass)) {
 704                     throw new Fault(i18n, "ts.notASubtype",
 705                                     new Object[] {script[0], "script", Script.class.getName()});
 706                 }
 707                 scriptArgs = new String[script.length - 1];
 708                 System.arraycopy(script, 1, scriptArgs, 0, scriptArgs.length);
 709             }
 710             else {
 711                 // for backwards compatibility,
 712                 // see if KeywordScript is a reasonable default
 713                 boolean keywordScriptOK = false;
 714                 for (Iterator i = scriptEnv.keys().iterator(); i.hasNext() && !keywordScriptOK; ) {
 715                     String key = (String)(i.next());
 716                     keywordScriptOK = key.startsWith("script.");
 717                 }
 718                 if (keywordScriptOK) {
 719                     scriptClass = KeywordScript.class;
 720                     scriptArgs = new String[] { };
 721                 }
 722                 else {
 723                     throw new Fault(i18n, "ts.noScript");
 724                 }
 725             }
 726         }
 727 
 728         Script s = (Script)(newInstance(scriptClass));
 729         s.initArgs(scriptArgs);
 730         s.initTestDescription(td);
 731         s.initExcludedTestCases(exclTestCases);
 732         s.initTestEnvironment(scriptEnv);
 733         s.initWorkDir(workDir);
 734         s.initBackupPolicy(backupPolicy);
 735         s.initClassLoader(loader);
 736         return s;
 737     }
 738 
 739     /**
 740      * Create a configuration interview that can be used to collection the configuration
 741      * data for a test run.
 742      * <p>The default implementation returns a {@link LegacyParameters default}
 743      * interview suitable for use with test suites built with earlier versions
 744      * of the JT Harness: it provides questions equivalent to the fields in
 745      * the GUI Parameter Editor or command-line -params option. As such, much of the
 746      * necessary configuration data is provided indirectly via environment (.jte) files
 747      * which must be created and updated separately.
 748      * <p>Individual test suites should provide their own interview, with questions
 749      * customized to the configuration data they require.
 750      *
 751      * Note that the name of this method is "create", the harness may instantiate
 752      * multiple copies for temporary use, resetting data or transferring data.
 753      * Do not override this method with an implementation which caches the
 754      * return value.
 755      * @return A configuration interview to collect the configuration data for a test run.
 756      * @throws TestSuite.Fault if a problem occurs while creating the interview
 757      */
 758     public InterviewParameters createInterview()
 759         throws Fault
 760     {
 761         String[] classNameAndArgs = StringArray.split((String) (tsInfo.get("interview")));
 762         if (classNameAndArgs == null || classNameAndArgs.length == 0) {
 763             try {
 764                 return new LegacyParameters(this);
 765             }
 766             catch (InterviewParameters.Fault e) {
 767                 throw new Fault(i18n, "ts.errorInitDefaultInterview",
 768                                 e.getMessage());
 769             }
 770         }
 771 
 772 
 773         String className = classNameAndArgs[0];
 774         String[] args = new String[classNameAndArgs.length - 1];
 775         System.arraycopy(classNameAndArgs, 1, args, 0, args.length);
 776 
 777         try {
 778             Class c = loadClass(className);
 779             InterviewParameters p = (InterviewParameters) (newInstance(c));
 780             p.init(args);
 781             p.setTestSuite(this);
 782             return p;
 783         }
 784         catch (ClassCastException e) {
 785             throw new Fault(i18n, "ts.notASubtype",
 786                             new Object[] {className, "interview", InterviewParameters.class.getName()});
 787         }
 788         catch (InterviewParameters.Fault e) {
 789             //e.printStackTrace();
 790             throw new Fault(i18n, "ts.errorInitInterview",
 791                             new Object[] {className, e.getMessage()});
 792         }
 793 
 794     }
 795 
 796     /**
 797      * Create a configuration interview based on specified map of template values
 798      * @return A configuration interview to collect the configuration data for a test run.
 799      * @throws TestSuite.Fault if a problem occurs while creating the interview
 800      */
 801     public InterviewParameters loadInterviewFromTemplate(Map<String, String> templateInfo, InterviewParameters newInterview)
 802         throws Fault
 803     {
 804         newInterview.storeTemplateProperties(templateInfo);
 805         newInterview.propagateTemplateForAll();
 806         return newInterview;
 807     }
 808 
 809     /**
 810      * Create a configuration interview based on specified template file
 811      * @return A configuration interview to collect the configuration data for a test run.
 812      *         null if specified file is not template
 813      * @throws TestSuite.Fault if a problem occurs while creating the interview
 814      *         IOException if a problem occurs while reading a template file
 815      */
 816     public InterviewParameters loadInterviewFromTemplate(File template,
 817                                                          InterviewParameters ip)
 818         throws Fault, IOException
 819     {
 820         try (InputStream in = new BufferedInputStream(new FileInputStream(template))) {
 821             Map<String, String> stringProps = com.sun.javatest.util.Properties.load(in);
 822             String tm = stringProps.get(InterviewParameters.IS_TEMPLATE);
 823             if (InterviewParameters.TRUE.equals(tm)) {
 824                 stringProps.put(InterviewParameters.TEMPLATE_PATH,
 825                          template.getAbsolutePath());
 826                 ip.setTemplatePath(template.getAbsolutePath());
 827                 return loadInterviewFromTemplate(stringProps, ip);
 828             } else {
 829                 // XXX should probably return ip
 830                 //     or throw Fault
 831                 return null;
 832             }
 833         }
 834     }
 835 
 836 
 837     /**
 838      * Get a string containing a unique ID identifying this test suite,
 839      * or null if not available.  The default is taken from the "id" entry
 840      * in the .jtt file.
 841      * @return a unique ID identifying the test suite, or null if not specified.
 842      * @see #getName
 843      */
 844     public String getID() {
 845         return (tsInfo == null ? null : (String) (tsInfo.get("id")));
 846     }
 847 
 848     /**
 849      * Get a string identifying this test suite, or null if not available.
 850      * The default is taken from the "name" entry in the .jtt file.
 851      * This string is for presentation to the user, and may be localized
 852      * if appropriate.
 853      * @return a string identifying the test suite, or null if not specified.
 854      * @see #getID
 855      */
 856     public String getName() {
 857         return (tsInfo == null ? null : (String) (tsInfo.get("name")));
 858     }
 859 
 860     /**
 861      * Get the estimated number of tests in the test suite.
 862      * The default is to use the value of the "testCount" property from the
 863      * testsuite.jtt file.
 864      *
 865      * @return The estimated number of tests, or -1 if this number is not available.
 866      */
 867     public int getEstimatedTestCount() {
 868         try {
 869             if (tsInfo != null) {
 870                 String s = (String) (tsInfo.get("testCount"));
 871                 if (s != null)
 872                     return Integer.parseInt(s);
 873             }
 874         }
 875         catch (NumberFormatException e) {
 876             // ignore
 877         }
 878         return -1; // unknown
 879     }
 880 
 881     /**
 882      * Get the file name of the initial exclude list associated with the test suite.
 883      * The default is to use the value of the "initial.jtx" property from the
 884      * testsuite.jtt file. If the value is a relative filename, it will be made absolute
 885      * by evaluating it relative to the test suite root directory.
 886      * @return the name of the default exclude list, or null if none specified.
 887      */
 888     public File getInitialExcludeList() {
 889         String s = (tsInfo == null ? null : (String) (tsInfo.get("initial.jtx")));
 890         if (s == null)
 891             return null;
 892 
 893         File f = new File(s.replace('/', File.separatorChar));
 894         if (!f.isAbsolute())
 895             f = new File(getRootDir(), f.getPath());
 896         return f;
 897     }
 898 
 899     /**
 900      * Check if the test suite has an initial exclude list.
 901      * The default is to use getInitialExcludeList, and if that returns
 902      * a non-null result, check whether that file exists or not.
 903      * @return true if the test suite has an initial exclude list,
 904      * and false otherwise
 905      */
 906     public boolean hasInitialExcludeList() {
 907         File f = getInitialExcludeList();
 908         return (f == null ? false : f.exists());
 909     }
 910 
 911     /**
 912      * Get the URL for the latest exclude list associated with the test suite.
 913      * The default is to use the value of the "latest.jtx" property from the
 914      * testsuite.jtt file., which (if present) must be a fully qualified URL
 915      * identifying the latest exclude list for this test suite.
 916      * @return the name of the latest exclude list, or null if none specified.
 917      */
 918     public URL getLatestExcludeList() {
 919         try {
 920             String s = (tsInfo == null ? null : (String) (tsInfo.get("latest.jtx")));
 921             return (s == null ? null : new URL(s));
 922         }
 923         catch (MalformedURLException e) {
 924             // ignore
 925             return null;
 926         }
 927     }
 928 
 929     /**
 930      * Check if the test suite has a latest exclude list.
 931      * The default is to use getLatestExcludeList, and to
 932      * check whether that return a non-null result. The URL is not
 933      * itself checked for validity.
 934      * @return true if the test suite has a latest exclude list,
 935      * and false otherwise
 936      */
 937     public boolean hasLatestExcludeList() {
 938         URL u = getLatestExcludeList();
 939         return (u != null);
 940     }
 941 
 942     /**
 943      * Get the names of any helpsets containing related documents for this
 944      * test suite. The names should identify JavaHelp helpset files, as
 945      * used by javax.help.HelpSet.findHelpSet(ClassLoader, String).
 946      * Thus the names should identify resources of helpsets on the classpath.
 947      * This means you will typically need to put the directory or jar file
 948      * containing the help set on the classpath as well.
 949      * By default, the names will be looked up under the name "additionalDocs"
 950      * in the testsuite.jtt file.
 951      * @return an array of names identifying helpsets that contain related
 952      * documents for this testsuite. The result may be null if there are no
 953      * such documents.
 954      */
 955     public String[] getAdditionalDocNames() {
 956         return (tsInfo == null
 957                 ? null
 958                 : StringArray.split((String) (tsInfo.get("additionalDocs"))));
 959     }
 960 
 961     /**
 962      * Get the set of valid keywords for this test suite.
 963      * By default, the keywords will be looked up under the name "keywords"
 964      * in the testsuite.jtt file.
 965      * @return the set of valid keywords for this test suite, or null
 966      * if not known.
 967      */
 968     public String[] getKeywords() {
 969         return keywords;
 970     }
 971 
 972     /**
 973      * Get a list of associated files for a specified test description.
 974      * Normally, this will include the file containing the test description,
 975      * and any source files used by the test.  By default, the source files
 976      * are determined from the test description's "source" entry.
 977      * @see TestDescription#getSourceURLs()
 978      * @param td The test description for which the associated files are required
 979      * @return a list of associated files for this test description
 980      */
 981     public URL[] getFilesForTest(TestDescription td) {
 982         return td.getSourceURLs();
 983     }
 984 
 985     /**
 986      * This method should be overridden in subclasses
 987      * @param path String, which determines path to currently selected test's folder.
 988      * This is root relative path. This shouldn't be null, for the
 989      * root folder use "".
 990      * @return array of files with documentation for test's folder, determined by path.
 991      * null means there no documentation for this folder
 992      */
 993     public URL[] getDocsForFolder(String path) {
 994         return null;
 995     }
 996 
 997     /**
 998      * This method should be overridden in subclasses
 999      * @param td TestDescription for currently selected test case. This shouldn't be null.
1000      * @return array of files with documentation for test case, determined td.
1001      * null means there no documentation for this test case
1002      */
1003     public URL[] getDocsForTest(TestDescription td) {
1004         return null;
1005     }
1006 
1007     /**
1008      * Get A URL identifying a logo for this test suite, or null if none available.
1009      * @return a URL for a logo for the testsuite, or null if not available
1010      */
1011     public URL getLogo() {
1012         try {
1013             String s = (tsInfo == null ? null : (String) (tsInfo.get("logo")));
1014             return (s == null ? null : new URL(getRootDir().toURL(), s));
1015         }
1016         catch (MalformedURLException e) {
1017             // ignore
1018             return null;
1019         }
1020     }
1021 
1022     private static String[] envLookup(TestEnvironment env, String name) throws Fault {
1023         try {
1024             return env.lookup(name);
1025         }
1026         catch (TestEnvironment.Fault e) {
1027             throw new Fault(i18n, "ts.cantFindNameInEnv",
1028                             new Object[] {name, e.getMessage()});
1029         }
1030     }
1031 
1032     /**
1033      * Create a new instance of a class, translating any exceptions that may arise
1034      * into Fault.
1035      * @param c the class to be instantiated
1036      * @return an instance of the specified class
1037      * @throws TestSuite.Fault if any errors arise while trying to instantiate
1038      * the class.
1039      */
1040     protected static Object newInstance(Class c) throws Fault {
1041         try {
1042             return c.newInstance();
1043         }
1044         catch (InstantiationException e) {
1045             throw new Fault(i18n, "ts.cantInstantiate",
1046                             new Object[] { c.getName(), e });
1047         }
1048         catch (IllegalAccessException e) {
1049             throw new Fault(i18n, "ts.illegalAccess",
1050                             new Object[] { c.getName(), e });
1051         }
1052     }
1053 
1054 
1055     /**
1056      * Create a new instance of a class using a non-default constructor,
1057      * translating any exceptions that may arise into Fault.
1058      * @param c the class to be instantiated
1059      * @param argTypes the types of the argument to be passed to the constructor,
1060      * (thus implying the constructor to be used.)
1061      * @param args the arguments to be passed to the constructor
1062      * @return an instance of the specified class
1063      * @throws TestSuite.Fault if any errors arise while trying to instantiate
1064      * the class.
1065      */
1066     protected static Object newInstance(Class<?> c, Class[] argTypes, Object[] args)
1067         throws Fault
1068     {
1069         try {
1070             return c.getConstructor(argTypes).newInstance(args);
1071         }
1072         catch (IllegalAccessException e) {
1073             throw new Fault(i18n, "ts.illegalAccess",
1074                             new Object[] { c.getName(), e });
1075         }
1076         catch (InstantiationException e) {
1077             throw new Fault(i18n, "ts.cantInstantiate",
1078                             new Object[] { c.getName(), e });
1079         }
1080         catch (InvocationTargetException e) {
1081             Throwable te = e.getTargetException();
1082             if (te instanceof Fault)
1083                 throw (Fault) te;
1084             else
1085                 throw new Fault(i18n, "ts.cantInit", new Object[] { c.getName(), te });
1086         }
1087         catch (NoSuchMethodException e) {
1088             // don't recurse past the use of a single arg constructor
1089             if (argTypes.length > 1 && Boolean.getBoolean(FIND_LEGACY_CONSTRUCTOR)) {
1090                 return newInstance(c, new Class[] {File.class}, new Object[] {args[0]});
1091             }
1092 
1093             throw new Fault(i18n, "ts.cantFindConstructor",
1094                             new Object[] { c.getName(), e });
1095         }
1096     }
1097 
1098     /**
1099      * Load a class using the class loader provided when this test suite was created.
1100      * @param className the name of the class to be loaded
1101      * @return the class that was loaded
1102      * @throws TestSuite.Fault if there was a problem loading the specified class
1103      */
1104     public Class loadClass(String className) throws Fault {
1105         return loadClass(className, loader);
1106     }
1107 
1108     /**
1109      * Load a class using a specified loader, translating any errors that may arise
1110      * into Fault.
1111      * @param className the name of the class to be loaded
1112      * @param cl the class loader to use to load the specified class
1113      * @return the class that was loaded
1114      * @throws TestSuite.Fault if there was a problem loading the specified class
1115      */
1116     protected static Class loadClass(String className, ClassLoader cl) throws Fault {
1117         try {
1118             if (cl == null)
1119                 return Class.forName(className);
1120             else
1121                 return cl.loadClass(className);
1122         }
1123         catch (ClassNotFoundException e) {
1124             throw new Fault(i18n, "ts.classNotFound",
1125                             new Object[] { className, e });
1126         }
1127         catch (IllegalArgumentException e) {
1128             throw new Fault(i18n, "ts.badClassName",
1129                             new Object[] { className });
1130         }
1131     }
1132 
1133     /**
1134      * Get the class loader specified when this test suite object was created.
1135      * @return the class loader specified when this test suite object was created
1136      */
1137     public ClassLoader getClassLoader() {
1138         return loader;
1139     }
1140 
1141     public ServiceManager getServiceManager() {
1142         if (!needServices()) {
1143             return null;
1144         }
1145 
1146         if (serviceManager == null) {
1147             serviceManager = new ServiceManager(this);
1148         }
1149 
1150         return serviceManager;
1151     }
1152 
1153     /**
1154      * Checks if serviceReader is active and file with service description does
1155      * exist.
1156      * @return true, if it's needed to start services, false otherwise.
1157      */
1158     public boolean needServices() {
1159         ServiceReader sr = getServiceReader();
1160         if (sr == null) {
1161             return false;
1162         }
1163 
1164         /*
1165          * Since jt4.5 the ServiceReader has been extended with a new method.
1166          * To preserve ability to use new javatest with old test suites
1167          * the extra check is performed: check if the newly introduced method
1168          * is abstract or not.
1169          */
1170         boolean isLegacy = false;
1171         try {
1172             Method m = sr.getClass().getMethod("getServiceDescriptorFileName", new Class[0]);
1173             if (Modifier.isAbstract(m.getModifiers())) {
1174                 isLegacy = true;
1175             }
1176         } catch (NoSuchMethodException e) {
1177              isLegacy = true;
1178         }
1179         File descrFile = isLegacy ?
1180             new File(getRootDir(), File.separator + "lib" + File.separator + "services.xml") :
1181             new File(getRootDir(), sr.getServiceDescriptorFileName());
1182 
1183         return descrFile.exists();
1184     }
1185     /**
1186      * Returns a test suite specific ServiceReader, used to read Service
1187      * definitions.
1188      *
1189      * @return ServiceReader instance. Default is PropertyServiceReader
1190      */
1191     public ServiceReader getServiceReader() {
1192         if (serviceReader != null) {
1193             return serviceReader;
1194         }
1195 
1196         String servInfo = (String)tsInfo.get("serviceReader");
1197         if (servInfo != null) {
1198             String[] args = servInfo.split(" ");
1199             try {
1200                 Class c = loadClass(args[0]);
1201                 serviceReader = (ServiceReader) (newInstance(c));
1202                 if (args.length > 1) {
1203                     // problem with java1.5, which has no Arrays.copyOfRange();
1204                     String[] copy = new String[args.length - 1];
1205                     for (int i = 1; i < args.length; i++) {
1206                         copy[i-1] = args[i];
1207                     }
1208 
1209                     serviceReader.init(this, copy);
1210                 }
1211                 else {
1212                     serviceReader.init(this, null);
1213                 }
1214             }
1215             catch (TestSuite.Fault e) {
1216             }
1217         }
1218         else {
1219             serviceReader = new PropertyServiceReader();
1220             serviceReader.init(this, null);
1221         }
1222 
1223         return serviceReader;
1224     }
1225 
1226     /**
1227      * Get a map containing the test suite data in the .jtt file.
1228      * @return a map containing the test suite data in the .jtt file
1229      */
1230     protected Map getTestSuiteInfo() {
1231         return tsInfo;
1232     }
1233 
1234     /**
1235      * Get an entry from the data in the .jtt file.
1236      * @param name The name of the entry to get from the info in the .jtt file
1237      * @return the value of the specified entry, or null if not found.
1238      */
1239     public String getTestSuiteInfo(String name) {
1240         if (tsInfo == null)
1241             return null;
1242         else
1243             return (String) (tsInfo.get(name));
1244     }
1245 
1246     /**
1247      * Get the requested behavior for dealing with conflicts between
1248      * which tests are in the test suite vs those in the work directory.
1249      * @see #DELETE_NONTEST_RESULTS
1250      * @see #REFRESH_ON_RUN
1251      * @see #CLEAR_CHANGED_TEST
1252      */
1253     public boolean getTestRefreshBehavior(int event) {
1254         switch (event) {
1255         case DELETE_NONTEST_RESULTS:
1256             return Boolean.valueOf(getTestSuiteInfo("deleteNonExistTests")).booleanValue();
1257         case REFRESH_ON_RUN:
1258             return Boolean.valueOf( getTestSuiteInfo("refreshTestsOnRun") ).booleanValue();
1259         case CLEAR_CHANGED_TEST:
1260             return Boolean.valueOf( getTestSuiteInfo("clearChangedTests")).booleanValue();
1261         default:
1262             return false;
1263         }
1264     }
1265 
1266 
1267     /**
1268      * Returns notification logger associated with
1269      * given working directory or common logger if null was specified
1270      * @param wd - working directory or null
1271      */
1272     public Logger getNotificationLog(WorkDirectory wd) {
1273         return notifLogger;
1274     }
1275 
1276     public ObservedFile getObservedFile(WorkDirectory wd) {
1277         return getObservedFile(wd.getLogFileName());
1278     }
1279 
1280 
1281     public ObservedFile getObservedFile(String path) {
1282         String cPath = new File(path).getAbsolutePath();
1283         if (observedFiles.containsKey(cPath)) {
1284             return (ObservedFile) observedFiles.get(cPath);
1285         }
1286         return null;
1287     }
1288 
1289     void setLogFilePath(WorkDirectory wd) {
1290         ObservedFile f = new ObservedFile(wd.getLogFileName());
1291         if (f.length() != 0) {
1292             f.backup();
1293         }
1294         // return to current
1295         f = new ObservedFile(wd.getLogFileName());
1296 
1297         if(observedFiles == null) {
1298             observedFiles = new HashMap<>();
1299         }
1300         if (!observedFiles.containsKey(f.getAbsolutePath())) {
1301             observedFiles.put(f.getAbsolutePath(), f);
1302         }
1303 
1304     }
1305 
1306 
1307     /**
1308      * Creates general purpose logger with given key and ResourceBundleName registered for given WorkDirectory.
1309      * @param wd WorkDirectory logger should be registered for; may be <code>null</code> if no WorkDirectory
1310      * currently available (the log will be registered for the first WD created for this TestSuite
1311      * @param b name of ResorceBundle used for this logger; may be <code>null</code> if not required
1312      * @param key key for this log
1313      * @return general purpose logger with given key registered for given WorkDirectory or TestSuite (if WD is null)
1314      * @throws TestSuite.DuplicateLogNameFault if log with this key has been registered in the system already
1315      * @see #getLog
1316      */
1317 
1318     public Logger createLog(WorkDirectory wd, String b, String key) throws DuplicateLogNameFault {
1319 
1320         if (key == null || "".equals(key)) {
1321             throw new IllegalArgumentException("Log name can not be empty");
1322         }
1323 
1324         String logName = wd.getLogFileName();
1325 
1326         if (gpls == null)
1327             gpls = new Vector<GeneralPurposeLogger>();
1328 
1329         for (int i = 0; i < gpls.size(); i++)  {
1330             GeneralPurposeLogger gpl = gpls.get(i);
1331             if (gpl.getName().equals(key) && gpl.getLogFileName().equals(logName))
1332                 throw new DuplicateLogNameFault(i18n, "ts.logger.duplicatelognamefault", key);
1333         }
1334 
1335         GeneralPurposeLogger gpl = new GeneralPurposeLogger( key, wd, b, this);
1336         gpls.add(gpl);
1337         return gpl;
1338     }
1339 
1340     /**
1341      * Returns general purpose logger with given key registered for given WorkDirectory.
1342      * The log should be created first.
1343      * @param wd WorkDirectory desired logger is registered for
1344      * @param key key for this log
1345      * @return general purpose logger with given key registered for given WorkDirectory
1346      * @throws TestSuite.NoSuchLogFault if desired log not registered in the system
1347      * @throws NullPointerException if <code>wd</code> is null
1348      * @see #createLog
1349      */
1350     public Logger getLog(WorkDirectory wd, String key) throws NoSuchLogFault {
1351         if (gpls == null)
1352             throw new NoSuchLogFault(i18n, "ts.logger.nologscreated", key);
1353 
1354         if (wd == null)
1355             throw new NullPointerException(i18n.getString("ts.logger.nullwd"));
1356 
1357         String logFile = wd.getLogFileName();
1358 
1359         for (int i = 0; i < gpls.size(); i++) {
1360             GeneralPurposeLogger logger = gpls.get(i);
1361             if (logger.getLogFileName().equals(logFile) && logger.getName().equals(key))
1362                 return logger;
1363         }
1364         throw new NoSuchLogFault(i18n, "ts.logger.nosuchlogfault", key);
1365     }
1366 
1367     /**
1368      * Cleans the log file in given WorkDirectory
1369      * @param wd WorkDirectory desired logger is registered for
1370      * @throws IOException if log file's content can't be erased
1371      */
1372     public void eraseLog(WorkDirectory wd) throws IOException {
1373         if (wd == null)
1374             throw new NullPointerException(i18n.getString("ts.logger.nullwd"));
1375 
1376         if (gpls != null)
1377             for (int i = 0; i < gpls.size(); i++) {
1378                 GeneralPurposeLogger gpl = gpls.get(i);
1379                 if (gpl.getLogFileName().equals(wd.getLogFileName())) {
1380                     Handler[] h = gpl.getHandlers();
1381                     if (h[0] instanceof WorkDirLogHandler) {
1382                         ((WorkDirLogHandler)h[0]).eraseLogFile();
1383                         return;
1384                     }
1385                 }
1386             }
1387     }
1388 
1389     private static boolean isReadableFile(File f) {
1390         return (f.exists() && f.isFile() && f.canRead());
1391     }
1392 
1393     /**
1394      * Should tests which no longer exist in the test suite be
1395      * deleted from a work directory when it is opened?
1396      */
1397     public static final int DELETE_NONTEST_RESULTS = 0;
1398 
1399     /*
1400      * Should the content of the test suite be refreshed as the
1401      * tests run?  So the test description should be updated from the
1402      * finder just before the test runs.
1403      */
1404     public static final int REFRESH_ON_RUN = 1;
1405 
1406     /**
1407      * Should a test be reset to not run if it is found that the
1408      * test has changed in the test suite (test description does
1409      * not match the one in the existing result).
1410      */
1411     public static final int CLEAR_CHANGED_TEST = 2;
1412 
1413     private static class NotificationLogger extends Logger {
1414         private NotificationLogger(String resourceBundleName) {
1415             super(notificationLogName, resourceBundleName);
1416             setLevel(Level.CONFIG);
1417             // needs to be reimplemented - this initializes Swing, which is not
1418             // allowed inside the core harness
1419             // should be implemented so that the GUI attaches to the logging system
1420             // on startup
1421             //addHandler(new ErrorDialogHandler());
1422         }
1423 
1424         public synchronized void log(LogRecord record) {
1425             record.setLoggerName(this.getName());
1426             if (record.getThrown() != null) {
1427                 record.setLevel(Level.INFO);
1428             }
1429             super.log(record);
1430         }
1431 
1432 
1433         // overwrite to make sure exception is handled
1434         public void throwing(String sourceClass, String sourceMethod, Throwable thrown) {
1435             LogRecord lr = new LogRecord(Level.INFO, "THROW");
1436             lr.setSourceClassName(sourceClass);
1437             lr.setSourceMethodName(sourceMethod);
1438             lr.setThrown(thrown);
1439             log(lr);
1440         }
1441 
1442     }
1443 
1444     private static class GeneralPurposeLogger extends Logger {
1445         private GeneralPurposeLogger(String name, WorkDirectory wd, String resourceBundleName, TestSuite ts) {
1446             super(name, resourceBundleName);
1447             this.logFileName = wd.getLogFileName();
1448 
1449             if (wd != null) {
1450                 if (!handlersMap.containsKey(wd.getLogFileName())) {
1451                     WorkDirLogHandler wdlh = new WorkDirLogHandler(ts.getObservedFile(wd));
1452                     handlersMap.put(wd.getLogFileName(), wdlh);
1453                 }
1454 
1455                 addHandler(handlersMap.get(wd.getLogFileName()));
1456             }
1457             setLevel(Level.ALL);
1458         }
1459 
1460         public void log(LogRecord record) {
1461             Handler targets[] = getHandlers();
1462             if (targets != null) {
1463                 for (int i = 0; i < targets.length; i++) {
1464                     if (targets[i] instanceof WorkDirLogHandler) {
1465                         ((WorkDirLogHandler)targets[i]).publish(record, getName());
1466                     } else {
1467                         targets[i].publish(record);
1468                     }
1469                 }
1470             }
1471         }
1472 
1473         private String getLogFileName() {
1474             return logFileName;
1475         }
1476 
1477         private String logFileName;
1478     }
1479 
1480 
1481     private static final String TESTSUITE_HTML = "testsuite.html";
1482     private static final String TESTSUITE_JTT  = "testsuite.jtt";
1483     private static final String FIND_LEGACY_CONSTRUCTOR = "com.sun.javatest.ts.findLegacyCtor";
1484 
1485     private File root;
1486     private Map tsInfo;
1487     private ClassLoader loader;
1488     private TestFinder finder;
1489 
1490     // the following are used by the default impl of createScript
1491     private Class scriptClass;
1492     private String[] scriptArgs;
1493 
1494     private String[] keywords;
1495 
1496     private ServiceReader serviceReader;
1497     private ServiceManager serviceManager;
1498 
1499     private static Map<File, WeakReference<TestSuite>> dirMap = new HashMap<>(2);
1500 
1501     private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestSuite.class);
1502     private static String notificationLogName = i18n.getString("notification.logname");
1503 
1504     static Map<String, WorkDirLogHandler> handlersMap = new HashMap<>();
1505     private static Vector<GeneralPurposeLogger> gpls;
1506     private static Map<String, File> observedFiles;
1507 
1508     private final NotificationLogger notifLogger = new NotificationLogger(null);
1509 
1510     public static final String TM_CONTEXT_NAME = "tmcontext";
1511 
1512 }