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