• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements. See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership. The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the  "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18 /*
19  * $Id$
20  */
21 
22 /*
23  *
24  * XSLTestHarness.java
25  *
26  */
27 package org.apache.qetest.xsl;
28 
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.IOException;
32 import java.util.Hashtable;
33 import java.util.Properties;
34 import java.util.StringTokenizer;
35 import java.util.Set;
36 import java.util.Iterator;
37 
38 import org.apache.qetest.FileBasedTest;
39 import org.apache.qetest.Logger;
40 import org.apache.qetest.QetestUtils;
41 import org.apache.qetest.Reporter;
42 
43 //-------------------------------------------------------------------------
44 
45 /**
46  * Utility to run multiple FileBasedTest objects in a row.
47  * <p>Generally run from the command line and passed a list
48  * of tests to execute, the XSLTestHarness will run each test in
49  * order, saving the results of each test for reporting later.</p>
50  * <p>User must have supplied minimal legal properties in the input
51  * Properties file: outputDir, inputDir, logFile, and tests.</p>
52  * @todo update to accept per-test.properties and pass'em thru
53  * @todo update to check for similarly named tests (in different pkgs)
54  * @todo update TestReporter et al to better cover case when
55  *        user doesn't call testCaseClose (where do results go?)
56  * @todo report on memory usage, etc.
57  * @author Shane_Curcuru@lotus.com
58  * @version $Id$
59  */
60 public class XSLTestHarness
61 {
62 
63 /**
64  * Convenience method to print out usage information.
65  * @return String denoting usage suitable for printing
66  */
usage()67     public String usage()
68     {
69         FileBasedTest tmp = new FileBasedTest();
70         return ("XSLTestHarness - execute multiple Tests in sequence and log results:\n"
71                 + "    Usage: java XSLTestHarness [-load] properties.prop\n"
72                 + "    Reads in all options from a Properties file:\n"
73                 + "    " + OPT_TESTS + "=semicolon;delimited;list;of FQCNs tests to run\n"
74                 + "    Most other options (in prop file only) are identical to FileBasedTest:\n"
75                 + tmp.usage()
76                 );
77     }
78 
79     /**
80      * Various property names we're expecting.
81      * <ul>
82      * <li>tests=TestOne;TestTwo;TestThree - semicolon-delimited list of
83      * TestClassNames to execute, in order; assumes all are in the
84      * org.apache.qetest. as base package currently (subject to change)</li>
85      * <li>logFile=LogFileName - name of output XML file to store harness
86      * log data in (passed to Reporter; constant in FileBasedTest.java)</li>
87      * <li>inputDir=path\\to\\tests - where the tests should find data</li>
88      * <li>outputDir=path\\to\\output - where the tests should send output and results</li>
89      * <li>goldDir=path\\to\\golds - where the tests should find gold files</li>
90      * <li>loggingLevel=50 - how much output tests should produce</li>
91      * <li>resultsViewer=Filename.xsl - reference to results processing stylesheet file</li>
92      * <li>Any other options are passed as-is to individual tests</li>
93      * </ul>
94      * <p>Currently each test has it's own logFile in the outputDir,
95      * named after the test.</p>
96      */
97 
98     /**
99      * Parameter: semicolong delimited list of FQCN's of test names.
100      * <p>Default: none - this parameter is required.  If the name
101      * is not package-complete, the harness may attempt to 'guess'
102      * the correct package underneath org.apache.qetest.</p>
103      */
104     public static final String OPT_TESTS = "tests";
105 
106     /** Delimiter for OPT_TESTS.  */
107     public static final String TESTS_DELIMITER = ";";
108 
109     /**
110      * We prepend the default package if any test name does not
111      * have a '.' in it.
112      * This is part of our 'guess' at the appropriate packagename.
113      * <b>WARNING!</b> Subject to change!
114      */
115     public static final String DEFAULT_PACKAGE = "org.apache.qetest.";
116 
117     /** Separator character for package.ClassName.  */
118     public static final String DOT = ".";
119 
120     /** Default extension for logFiles.  */
121     public static final String LOG_EXTENSION = ".xml";
122 
123     /**
124      * Generic Properties block for storing initialization info.
125      * All startup options get stored in here for later use, both by
126      * the test itself and by any Reporters we use.
127      */
128     protected Properties harnessProps;
129 
130     /** Our Reporter, who we tell all our secrets to.  */
131     protected Reporter reporter;
132 
133 
134     /**
135      * Setup any options and construct a list of tests to execute.
136      * <p>Accesses our class variables harnessProps and debug.
137      * Must not use Reporter, since it hasn't been created yet.</p>
138      * @param args array of command line arguments
139      * @return array of testClassNames to execute; null if error
140      */
doTestHarnessInit(String args[])141     protected String[] doTestHarnessInit(String args[])
142     {
143         // Harness loads all info from one properties file
144         // semi-HACK: accept and ignore -load as first arg only
145         String propFileName = null;
146         if ("-load".equalsIgnoreCase(args[0]))
147         {
148             propFileName = args[1];
149         }
150         else
151         {
152             propFileName = args[0];
153         }
154         try
155         {
156             // Load named file into our properties block
157             FileInputStream fIS = new FileInputStream(propFileName);
158             harnessProps = new Properties();
159             harnessProps.load(fIS);
160         }
161         catch (IOException ioe)
162         {
163             System.err.println("ERROR! loading properties file failed: " + propFileName);
164             ioe.printStackTrace();
165             return null;
166         }
167 
168         // Grab the list of tests, which is specific only to the harness
169         // String testNames = harnessProps.getProperty(OPT_TESTS);
170         StringBuffer testNamesStrBuff = new StringBuffer();
171         Set<String> strPropNames = harnessProps.stringPropertyNames();
172         Iterator<String> strPropIter = strPropNames.iterator();
173         while (strPropIter.hasNext()) {
174            String propName = strPropIter.next();
175            if (propName.endsWith(DOT + OPT_TESTS)) {
176               String propValue = harnessProps.getProperty(propName);
177               testNamesStrBuff.append(propValue + TESTS_DELIMITER);
178            }
179         }
180 
181         String testNames = testNamesStrBuff.toString();
182         if (testNames.length() > 0) {
183            testNames = testNames.substring(0, testNames.length());
184         }
185 
186         if ((testNames == null) || (testNames.length() == 0))
187         {
188             System.err.println("ERROR! No tests(1) were supplied in the properties file!");
189             return null;
190         }
191 
192         // Split up the list of names
193         StringTokenizer st = new StringTokenizer(testNames, TESTS_DELIMITER);
194         int testCount = st.countTokens();
195         if (testCount == 0)
196         {
197             System.err.println("ERROR! No tests(2) were supplied in the properties file!");
198             return null;
199         }
200         String tests[] = new String[testCount];
201         for (int i = 0; st.hasMoreTokens(); i++)
202         {
203             String s = st.nextToken();
204             if (s.startsWith("org"))
205             {
206                 // Assume user specified complete package.ClassName
207                 tests[i] = s;
208             }
209             else
210             {
211                 // Use QetestUtils to find the correct name.
212                 tests[i] = QetestUtils.testClassnameForName(s, QetestUtils.defaultPackages, null);
213             }
214         }
215         // Munge the inputDir and goldDir to use platform path
216         //  separators if needed
217 
218         String tempS = harnessProps.getProperty(FileBasedTest.OPT_INPUTDIR);
219         tempS = swapPathDelimiters(tempS);
220         File tempF = new File(tempS);
221         if (tempF.exists())
222         {
223             harnessProps.put(FileBasedTest.OPT_INPUTDIR, tempS);
224         }
225         else
226         {
227             System.err.println("ERROR! " + FileBasedTest.OPT_INPUTDIR + " property does not exist! " + tempS);
228             return null;
229         }
230         tempS = harnessProps.getProperty(FileBasedTest.OPT_GOLDDIR);
231         tempS = swapPathDelimiters(tempS);
232         tempF = new File(tempS);
233         if (tempF.exists())
234         {
235             harnessProps.put(FileBasedTest.OPT_GOLDDIR, tempS);
236         }
237         else
238         {
239             System.err.println("WARNING! " + FileBasedTest.OPT_GOLDDIR + " property does not exist! " + tempS);
240         }
241 
242         // Also swap around path on outputDir, logFile
243         tempS = harnessProps.getProperty(FileBasedTest.OPT_OUTPUTDIR);
244         tempS = swapPathDelimiters(tempS);
245         tempF = new File(tempS);
246         if (tempF.exists())
247         {
248             harnessProps.put(FileBasedTest.OPT_OUTPUTDIR, tempS);
249         }
250         else
251         {
252             System.err.println("WARNING! " + FileBasedTest.OPT_OUTPUTDIR + " property does not exist! " + tempS);
253         }
254 
255         tempS = harnessProps.getProperty(Logger.OPT_LOGFILE);
256         tempS = swapPathDelimiters(tempS);
257         harnessProps.put(Logger.OPT_LOGFILE, tempS);
258         return tests;
259     }
260 
261     /**
262      * Update a path to use system-dependent delimiter.
263      *
264      * Allow user to specify a system-dependent path in the
265      * properties file we're loaded from, but then let another
266      * user run the same files on another environment.
267      *
268      * I'm drawing a complete blank today on the classic way to
269      * do this, so don't be disappointed if you look at the code
270      * and it's goofy.
271      */
swapPathDelimiters(String s)272     protected String swapPathDelimiters(String s)
273     {
274         if (null == s)
275             return null;
276         // If we're not on Windows, swap an apparent Windows-based
277         //  backslash separator with a forward slash separator
278         // This is because I'm lazy and checkin .properties files
279         //  with Windows based paths, but want unix-based people
280         //  to be able to run the tests as-is
281         if (File.separatorChar != '\\')
282             return s.replace('\\', File.separatorChar);
283         else
284             return s;
285     }
286 
287     /**
288      * Go run the available tests!
289      * <p>This is sort-of the equivalent of runTest() in a Test
290      * object.  Each test is run in order, and is the equivalent
291      * of a testCase for the Harness.  The Harness records a master
292      * log file, and each test puts its results in it's own log file.</p>
293      */
runHarness(String testList[])294     protected boolean runHarness(String testList[])
295     {
296         // Report that we've begun testing
297         // Note that we're hackishly re-using the 'test' metaphor
298         //      on a grand scale here, where each of the harness'
299         //      testCases corresponds to one entire Test
300         reporter.testFileInit("Harness", "Harness executing " + testList.length + " tests");
301         logHarnessProps();
302 
303         // Note 'passCount' is poorly named: a test may fail but
304         //  may still return true from runTest. You really have to
305         //  look at the result files to see real test status
306         int passCount = 0;
307         int nonPassCount = 0;
308         // Run each test in order!
309         for (int testIdx = 0; testIdx < testList.length; testIdx++)
310         {
311             boolean testStat = false;
312             try
313             {
314                 // This method logs out status to our log file, as well
315                 //      as initializing and running the test
316                 testStat = runOneTest(testList[testIdx], harnessProps);
317             }
318             catch (Throwable t)
319             {
320                 // Catch everything, log it, and move on
321                 reporter.checkErr("Test " + testList[testIdx] + " threw: " + t.toString());
322                 reporter.logThrowable(reporter.ERRORMSG, t, "Test "
323                                       + testList[testIdx] + " threw: " + t.toString());
324             }
325             finally
326             {
327                 if (testStat)
328                     passCount++;
329                 else
330                     nonPassCount++;
331             }
332         }
333         // Below line is not a 'check': each runOneTest call logs it's own status
334         // Only for information; remember that the runTest status is not the pass/fail of the test!
335         reporter.logCriticalMsg("All tests complete, testStatOK:" + passCount + " testStatNOTOK:" + nonPassCount);
336 
337         // Have the reporter write out a summary file for us
338         reporter.writeResultsStatus(true);
339 
340         // Close reporter and return true only if all tests passed
341         // Note the passCount/nonPassCount are misnomers, since they
342         //  really only report if a test aborted, not passed
343         reporter.testFileClose();
344         if ((passCount < 0) && (nonPassCount == 0))
345             return true;
346         else
347             return false;
348     }
349 
350 
351     /**
352      * Run a single FileBasedTest and report it's results.
353      * <p>Uses our class field reporter to dump our results to, also
354      * creates a separate reporter for the test to use.</p>
355      * <p>See the code for the specific initialization we custom-craft for
356      * each individual test.  Basically we clone our harnessProps, update the
357      * logFile and outputDir per test, and create a testReporter, then use these
358      * to initialize the test before we call runTest on it.</p>
359      * @param testName FQCN of the test to execute; must be instanceof FileBasedTest
360      * @param hProps property block to use as initializer
361      * @return the pass/fail return from runTest(), which is not necessarily
362      *         the same as what we're going to log as the test's result
363      */
runOneTest(String testName, Properties hProps)364     protected boolean runOneTest(String testName, Properties hProps)
365     {
366         // Report on what we're about to do
367         reporter.testCaseInit("runOneTest:" + testName);
368 
369         // Validate our basic arguments
370         if ((testName == null) || (testName.length() == 0) || (hProps == null))
371         {
372             reporter.checkErr("runOneTest called with bad arguments!");
373             reporter.testCaseClose();
374             return false;
375         }
376 
377         // Calculate just the ClassName of the test for later use as the logFile name
378         String bareClassName = null;
379         StringTokenizer st = new StringTokenizer(testName, ".");
380         for (bareClassName = st.nextToken(); st.hasMoreTokens(); bareClassName = st.nextToken())
381         { /* empty loop body */
382         }
383         st = null; // no longer needed
384 
385         // Validate that the output directory exists for the test to put it's results in
386         String testOutDir = hProps.getProperty(FileBasedTest.OPT_OUTPUTDIR);
387         if ((testOutDir == null) || (testOutDir.length() == 0))
388         {
389             // Default to current dir plus the bareClassName if not set
390             testOutDir = new String("." + File.separator + bareClassName);
391         }
392         else
393         {
394             // Append the bareClassName so different tests don't clobber each other
395             testOutDir += File.separator + bareClassName;
396         }
397         File oDir = new File(testOutDir);
398         if (!oDir.exists())
399         {
400             if (!oDir.mkdirs())
401             {
402                 // Report this but keep going anyway
403                 reporter.logErrorMsg("Could not create testOutDir: " + testOutDir);
404             }
405         }
406         // no longer needed
407         oDir = null;
408 
409         // Validate we can instantiate the test object itself
410         reporter.logTraceMsg("About to newInstance(" + testName + ")");
411         FileBasedTest test = null;
412         try
413         {
414             Class testClass = Class.forName(testName);
415             test = (FileBasedTest)testClass.newInstance();
416         }
417         catch (Exception e1)
418         {
419             reporter.checkErr("Could not create test, threw: " + e1.toString());
420             reporter.logThrowable(reporter.ERRORMSG, e1, "Could not create test, threw");
421             reporter.testCaseClose();
422             return false;
423         }
424 
425         // Create a properties block for the test and pre-fill it with custom info
426         //      Start with the harness' properties, and then replace certain values
427         Properties testProps = (Properties)hProps.clone();
428         testProps.put(FileBasedTest.OPT_OUTPUTDIR, testOutDir);
429         testProps.put(Logger.OPT_LOGFILE, testOutDir + LOG_EXTENSION);
430 
431         // Disable the ConsoleReporter for the *individual* tests, it's too confusing
432         testProps.put("noDefaultReporter", "true");
433         reporter.logHashtable(reporter.INFOMSG, testProps, "testProps before test creation");
434 
435         // Initialize the test with the properties we created
436         test.setProperties(testProps);
437         boolean testInit = test.initializeFromProperties(testProps);
438         reporter.logInfoMsg("Test(" + testName + ").initializeFromProperties() = " + testInit);
439 
440         // -----------------
441         // Execute the test!
442         // -----------------
443         boolean runTestStat = test.runTest(testProps);
444 
445         // Report where the test stored it's results - future use
446         //  by multiViewResults.xsl or some other rolledup report
447         // Note we should really handle the filenames here better,
448         //  especially for relative vs. absolute issues
449         Hashtable h = new Hashtable(2);
450         h.put("result", reporter.resultToString(test.getReporter().getCurrentFileResult()));
451         h.put("fileRef", (String)testProps.get(Logger.OPT_LOGFILE));
452         reporter.logElement(reporter.WARNINGMSG, "resultsfile", h, test.getTestDescription());
453         h = null; // no longer needed
454 
455         // Call worker method to actually calculate the result and call check*()
456         logTestResult(bareClassName, test.getReporter().getCurrentFileResult(),
457                       runTestStat, test.getAbortTest());
458 
459         // Cleanup local variables and garbage collect, in case tests don't
460         //      release all resources or something
461         testProps = null;
462         test = null;
463         logMemory();    // Side effect: System.gc()
464 
465         reporter.testCaseClose();
466         return runTestStat;
467     }
468 
469 
470     /**
471      * Convenience method to report the result of a single test.
472      * <p>Depending on the test's return value, it's currentFileResult,
473      * and if it was ever aborted, we call check to our reporter to log it.</p>
474      * @param testName basic name of the test
475      * @param testResult result of whole test file
476      * @param testStat return value from test.runTest()
477      * @param testAborted if the test was aborted at all
478      */
logTestResult(String testName, int testResult, boolean testStat, boolean testAborted)479     protected void logTestResult(String testName, int testResult, boolean testStat, boolean testAborted)
480     {
481         // Report the 'rolled-up' results of the test, combining each of the above data
482         switch (testResult)
483         {
484             case Logger.INCP_RESULT:
485                 // There is no 'checkIncomplete' method, so simply avoid calling check at all
486                 reporter.logErrorMsg(testName + ".runTest() returned INCP_RESULT!");
487                 break;
488             case Logger.PASS_RESULT:
489                 // Only report a pass if it returned true and didn't abort
490                 if (testStat && (!testAborted))
491                 {
492                     reporter.checkPass(testName + ".runTest()");
493                 }
494                 else
495                 {
496                     // Assume something went wrong and call it an ERRR
497                     reporter.checkErr(testName + ".runTest()");
498                 }
499                 break;
500             case Logger.AMBG_RESULT:
501                 reporter.checkAmbiguous(testName + ".runTest()");
502                 break;
503             case Logger.FAIL_RESULT:
504                 reporter.checkFail(testName + ".runTest()");
505                 break;
506             case Logger.ERRR_RESULT:
507                 reporter.checkErr(testName + ".runTest()");
508                 break;
509             default:
510                 // Assume something went wrong
511                 //  (always 'err' on the safe side, ha, ha)
512                 reporter.checkErr(testName + ".runTest()");
513                 break;
514         }
515     }
516 
517 
518     /**
519      * Convenience method to log out any version or system info.
520      * <p>Logs System.getProperties(), the harnessProps block, plus
521      * info about the classpath.</p>
522      */
logHarnessProps()523     protected void logHarnessProps()
524     {
525         reporter.logHashtable(reporter.WARNINGMSG, System.getProperties(), "System.getProperties");
526         reporter.logHashtable(reporter.WARNINGMSG, harnessProps, "harnessProps");
527         // Since we're running a bunch of tests, also check which version
528         //      of various jars we're running against
529         logClasspathInfo(System.getProperty("java.class.path"));
530     }
531 
532 
533     /**
534      * Convenience method to log out misc info about your classpath.
535      * @param classpath presumably the java.class.path to search for jars
536      */
logClasspathInfo(String classpath)537     protected void logClasspathInfo(String classpath)
538     {
539         StringTokenizer st = new StringTokenizer(classpath, File.pathSeparator);
540         for (int i = 0; st.hasMoreTokens(); i++)
541         {
542             logClasspathItem(st.nextToken());
543         }
544     }
545 
546 
547     /**
548      * Convenience method to log out misc info about a single classpath entry.
549      * <p>Implicitly looks for specific jars, namely xalan.jar, xerces.jar, etc.</p>
550      * @param filename classpath entry to report about
551      */
logClasspathItem(String filename)552     protected void logClasspathItem(String filename)
553     {
554         // Make sure the comparison names are all lower case
555         // This allows us to do case-insensitive compares, but
556         //      actually use the case-sensitive filename for lookups
557         String filenameLC = filename.toLowerCase();
558         String checknames[] = { "xalan.jar", "xerces.jar", "testxsl.jar", "minitest.jar"};
559 
560         for (int i = 0; i < checknames.length; i++)
561         {
562             if (filenameLC.indexOf(checknames[i]) > -1)
563             {
564                 File f = new File(filename);
565                 if (f.exists())
566                 {
567                     Hashtable h = new Hashtable(4);
568                     h.put("jarname", checknames[i]);
569                     h.put("length", String.valueOf(f.length()));
570                     h.put("lastModified", String.valueOf(f.lastModified()));
571                     h.put("path", f.getAbsolutePath());
572                     reporter.logElement(Reporter.INFOMSG, "classpathitem", h, null);
573                 }
574             }
575         }
576     }
577 
578 
579     /**
580      * Cheap-o memory logger - just reports Runtime.totalMemory/freeMemory.
581      */
logMemory()582     protected void logMemory()
583     {
584         Runtime r = Runtime.getRuntime();
585         r.gc();
586         reporter.logPerfMsg("UMem", r.freeMemory(), "freeMemory");
587         reporter.logPerfMsg("UMem", r.totalMemory(), "totalMemory");
588     }
589 
590 
591     /**
592      * Run the test harness to execute the specified tests.
593      */
doMain(String args[])594     public void doMain(String args[])
595     {
596         // Must have at least one arg to continue
597         if ((args == null) || (args.length == 0))
598         {
599             System.err.println("ERROR in usage: must have at least one argument");
600             System.err.println(usage());
601             return;
602         }
603 
604         // Initialize ourselves and a list of tests to execute
605         // Side effects: sets harnessProps, debug
606         String tests[] = doTestHarnessInit(args);
607         if (tests == null)
608         {
609             System.err.println("ERROR in usage: Problem during initialization - no tests!");
610             System.err.println(usage());
611             return;
612         }
613 
614         // Use a separate copy of our properties to init our Reporter
615         Properties reporterProps = (Properties)harnessProps.clone();
616 
617         // Ensure we have an XMLFileLogger if we have a logName
618         String logF = reporterProps.getProperty(Logger.OPT_LOGFILE);
619 
620         if ((logF != null) && (!logF.equals("")))
621         {
622             // We should ensure there's an XMLFileReporter
623             String r = reporterProps.getProperty(Reporter.OPT_LOGGERS);
624 
625             if (r == null)
626             {
627                 reporterProps.put(Reporter.OPT_LOGGERS,
628                               "org.apache.qetest.XMLFileLogger");
629             }
630             else if (r.indexOf("XMLFileLogger") <= 0)
631             {
632                 reporterProps.put(Reporter.OPT_LOGGERS,
633                               r + Reporter.LOGGER_SEPARATOR
634                               + "org.apache.qetest.XMLFileLogger");
635             }
636         }
637 
638         // Ensure we have a ConsoleLogger unless asked not to
639         // @todo improve and document this feature
640         String noDefault = reporterProps.getProperty("noDefaultReporter");
641         if (noDefault == null)
642         {
643             // We should ensure there's an XMLFileReporter
644             String r = reporterProps.getProperty(Reporter.OPT_LOGGERS);
645 
646             if (r == null)
647             {
648                 reporterProps.put(Reporter.OPT_LOGGERS,
649                               "org.apache.qetest.ConsoleLogger");
650             }
651             else if (r.indexOf("ConsoleLogger") <= 0)
652             {
653                 reporterProps.put(Reporter.OPT_LOGGERS,
654                               r + Reporter.LOGGER_SEPARATOR
655                               + "org.apache.qetest.ConsoleLogger");
656             }
657         }
658 
659         // A Reporter will auto-initialize from the values
660         //  in the properties block
661         reporter = new Reporter(reporterProps);
662         reporter.addDefaultLogger();  // add default logger if needed
663 
664         // Call worker method to actually run all the tests
665         // Worker method manages all it's own reporting, including
666         //  calling testFileInit/testFileClose
667         boolean notUsed = runHarness(tests);
668 
669         // Tell user if a logFile should have been saved
670         String logFile = reporterProps.getProperty(Logger.OPT_LOGFILE);
671         if (logFile != null)
672         {
673             System.out.println("");
674             System.out.println("Hey! A summary-harness logFile was written to: " + logFile);
675         }
676     }
677 
678 
679     /**
680      * Main method to run the harness from the command line.
681      */
main(String[] args)682     public static void main (String[] args)
683     {
684         XSLTestHarness app = new XSLTestHarness();
685         app.doMain(args);
686     }
687 }    // end of class XSLTestHarness
688 
689