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