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 package org.apache.qetest; 23 24 import java.io.File; 25 import java.io.FilenameFilter; 26 import java.lang.reflect.Constructor; 27 import java.util.Enumeration; 28 import java.util.Properties; 29 import java.util.Vector; 30 31 /** 32 * Generic Test driver for FileTestlets. 33 * 34 * <p>This driver provides basic services for iterating over a tree 35 * of test files and executing a specified testlet on each test that 36 * is selected by a set of specified filters. It automatically handles 37 * iteration and optional recursion down the tree, and by default 38 * assumes there are three 'matching' trees for inputs, golds, and 39 * creates a tree for outputs.</p> 40 * 41 * <p>Key methods are separated into worker methods so subclasses can 42 * override just the parts of the algorithm they need to change.</p> 43 * 44 * <p>//@todo move and refactor XSLProcessorTestBase to 45 * be more generic and reduce dependencies; also reduce dependency 46 * on internal variables and instead always use lookups into 47 * our testProps object.</p> 48 * 49 * @author shane_curcuru@us.ibm.com 50 * @version $Id$ 51 */ 52 public class FileTestletDriver extends FileBasedTest /// extends XSLProcessorTestBase 53 { 54 55 //----------------------------------------------------- 56 //-------- Constants for common input params -------- 57 //----------------------------------------------------- 58 59 /** 60 * Parameter: Run a specific list of files, instead of 61 * iterating over directories. 62 * <p>Default: null, do normal iteration.</p> 63 */ 64 public static final String OPT_FILELIST = "fileList"; 65 66 /** 67 * Parameter: FQCN or simple classname of Testlet to use. 68 * <p>User may pass in either a FQCN or just a base classname, 69 * and we will attempt to look it up in any of the most common 70 * Xalan-testing packages. See QetestUtils.testClassForName().</p> 71 * <p>Default: null, use StylesheetTestlet.</p> 72 */ 73 public static final String OPT_TESTLET = "testlet"; 74 75 /** Classname of Testlet to use. */ 76 protected String testlet = null; 77 78 /** 79 * Parameter: FQCN or simple classname of FilenameFilter for 80 * directories under testDir we will process. 81 * If fileList is not set, we simply go to our inputDir, and 82 * then use this filter to iterate through directories returned. 83 * <p>Default: null, use ConformanceDirRules.</p> 84 */ 85 public static final String OPT_DIRFILTER = "dirFilter"; 86 87 /** Classname of FilenameFilter to use for dirs. */ 88 protected String dirFilter = null; 89 90 /** 91 * Parameter: FQCN or simple classname of FilenameFilter for 92 * files within subdirs we will process. 93 * If fileList is not set, we simply go through all directories 94 * specified by directoryFilter, and then use this filter to 95 * find all stylesheet test files in that directory to test. 96 * Note that this does <b>not</b> handle embedded tests, where 97 * the XML document has an xml-stylesheet PI that defines the 98 * stylesheet to use to process it. 99 * <p>Default: null, use ConformanceFileRules.</p> 100 */ 101 public static final String OPT_FILEFILTER = "fileFilter"; 102 103 /** Classname of FilenameFilter to use for files. */ 104 protected String fileFilter = null; 105 106 /** Unique runId for each specific invocation of this test driver. */ 107 protected String runId = null; 108 109 /** Convenience constant: .gold extension for gold files. */ 110 public static final String GLD_EXTENSION = ".gld"; 111 112 /** Convenience constant: .out extension for output result file. */ 113 public static final String OUT_EXTENSION = ".out"; 114 115 116 /** Just initialize test name, comment; numTestCases is not used. */ FileTestletDriver()117 public FileTestletDriver() 118 { 119 testName = "FileTestletDriver"; 120 testComment = "Test driver for File-based Testlets"; 121 } 122 123 124 /** 125 * Initialize this test - fill in parameters. 126 * Simply fills in convenience variables from user parameters. 127 * 128 * @param p unused 129 * @return true 130 */ doTestFileInit(Properties p)131 public boolean doTestFileInit(Properties p) 132 { 133 // Copy any of our parameters from testProps to 134 // our local convenience variables 135 testlet = testProps.getProperty(OPT_TESTLET, testlet); 136 dirFilter = testProps.getProperty(OPT_DIRFILTER, dirFilter); 137 fileFilter = testProps.getProperty(OPT_FILEFILTER, fileFilter); 138 139 // Grab a unique runid for logging out with our tests 140 // Used in results reporting stylesheets to differentiate 141 // between different test runs 142 runId = QetestUtils.createRunId(testProps.getProperty("runId")); 143 testProps.put("runId", runId); // put back in the properties 144 // for later use 145 return true; 146 } 147 148 149 /** 150 * Run through the directory given to us and run tests found 151 * in subdirs; or run through our fileList. 152 * 153 * This method logs some basic runtime data (like the actual 154 * testlet and ProcessorWrapper implementations used) and 155 * then decides to either run a user-specified fileList or to 156 * use our dirFilter to iterate over the inputDir. 157 * 158 * @param p Properties block of options to use - unused 159 * @return true if OK, false if we should abort 160 */ runTestCases(Properties p)161 public boolean runTestCases(Properties p) 162 { 163 // First log out any other runtime information, like the 164 // actual current testlet and filters 165 try 166 { 167 // Note that each of these calls actually force the 168 // creation of an actual object of each type: this is 169 // required since we may default the types or our call 170 // to QetestUtils.testClassForName() may return a 171 // different classname than the user actually specified 172 // Care should be taken that the construction of objects 173 // here does not affect our testing later on 174 Properties runtimeProps = new Properties(); 175 // ... and add a few extra things ourselves 176 runtimeProps.put("actual.testlet", getTestlet()); 177 runtimeProps.put("actual.dirFilter", getDirFilter()); 178 runtimeProps.put("actual.fileFilter", getFileFilter()); 179 reporter.logHashtable(Logger.CRITICALMSG, runtimeProps, 180 "actual.runtime information"); 181 } 182 catch (Exception e) 183 { 184 // This is not necessarily an error 185 reporter.logThrowable(Logger.WARNINGMSG, e, "Logging actual.runtime threw"); 186 } 187 188 // Now either run a list of specific tests the user specified, 189 // or do the default of iterating over a set of directories 190 String fileList = testProps.getProperty(OPT_FILELIST); 191 if (null != fileList) 192 { 193 // Process the specific list of tests the user supplied 194 String desc = "User-supplied fileList: " + fileList; // provide default value 195 // Use static worker class to process the list 196 Vector datalets = FileDataletManager.readFileList(reporter, fileList, desc, testProps); 197 198 // Actually process the specified files in a testCase 199 processFileList(datalets, desc); 200 } 201 else 202 { 203 // Do the default, which is to iterate over the inputDir 204 // Note that this calls the testCaseInit/testCaseClose 205 // logging methods itself 206 processInputDir(); 207 } 208 return true; 209 } 210 211 212 /** 213 * Do the default: test all files found in subdirs 214 * of our inputDir, using FilenameFilters for dirs and files. 215 * Parameters: none, uses our internal members inputDir, 216 * outputDir, goldDir, etc. Will attempt to use a default 217 * inputDir if the specified one doesn't exist. 218 * 219 * This is a special case of recurseSubDir, since we report 220 * differently from the top level. 221 */ processInputDir()222 public void processInputDir() 223 { 224 // Ensure the inputDir is there - we must have a valid location for input files 225 File topInputDir = new File(inputDir); 226 227 if (!topInputDir.exists()) 228 { 229 // Try a default inputDir 230 String oldInputDir = inputDir; // cache for potential error message 231 topInputDir = new File((inputDir = getDefaultInputDir())); 232 if (!topInputDir.exists()) 233 { 234 // No inputDir, can't do any tests! 235 // Note we put this in a fake testCase, since this 236 // is likely the only thing our test reports 237 reporter.testCaseInit("processInputDir - mock testcase"); 238 reporter.checkErr("topInputDir(" + oldInputDir 239 + ", or " + inputDir + ") does not exist, aborting!"); 240 reporter.testCaseClose(); 241 return; 242 } 243 } 244 245 FileDatalet topDirs = new FileDatalet(topInputDir.getPath(), outputDir, goldDir); 246 247 // Optionally process this topDirs, and always recurse at 248 // least one level below it 249 recurseSubDir(topDirs, getProcessTopDir(), true); 250 } 251 252 253 /** 254 * Optionally process all the files in this dir and optionally 255 * recurse downwards using our dirFilter. 256 * 257 * This is a pre-order traversal; we process files in this 258 * dir first and then optionally recurse. 259 * 260 * @param base FileDatalet representing the input, output, 261 * gold directory triplet we should use 262 * @param process if we should call processSubDir on this dir 263 * @param recurse if we should recurse below this directory, 264 * or just stop here after processSubDir() 265 */ recurseSubDir(FileDatalet base, boolean process, boolean recurse)266 public void recurseSubDir(FileDatalet base, boolean process, boolean recurse) 267 { 268 // Process this directory first: pre-order traversal 269 if (process) 270 processSubDir(base); 271 272 if (!recurse) 273 return; 274 275 // If we should recurse, do so now 276 File inputDir = new File(base.getInput()); 277 FilenameFilter filter = getDirFilter(); 278 reporter.logTraceMsg("recurseSubDir(" + inputDir.getPath() 279 + ") looking for subdirs with: " + filter); 280 281 // Use our filter to get a list of directories to process 282 String subdirs[] = inputDir.list(filter); 283 284 // Validate that we have some valid directories to process 285 if ((null == subdirs) || (subdirs.length <= 0)) 286 { 287 reporter.logWarningMsg("recurseSubDir(" + inputDir.getPath() 288 + ") no valid subdirs found!"); 289 return; 290 } 291 292 // For every subdirectory, check if we should run tests in it 293 for (int i = 0; i < subdirs.length; i++) 294 { 295 File subTestDir = new File(inputDir, subdirs[i]); 296 297 if ((null == subTestDir) || (!subTestDir.exists())) 298 { 299 // Just log it and continue; presumably we'll find 300 // other directories to test 301 reporter.logWarningMsg("subTestDir(" + subTestDir.getPath() 302 + ") does not exist, skipping!"); 303 continue; 304 } 305 FileDatalet subdir = new FileDatalet(base, subdirs[i]); 306 307 // Process each other directory, and optionally continue 308 // to recurse downwards 309 recurseSubDir(subdir, true, getRecurseDirs()); 310 } // end of for... 311 } 312 313 314 /** 315 * Process a single subdirectory and run our testlet over 316 * every file found by our fileFilter therein. 317 * 318 * @param base FileDatalet representing the input, output, 319 * gold directory triplet we should use 320 */ processSubDir(FileDatalet base)321 public void processSubDir(FileDatalet base) 322 { 323 // Validate that each of the specified dirs exists 324 // Ask it to be strict in ensuring output, gold are created 325 if (!base.validate(true)) 326 { 327 // Just log it and continue; presumably we'll find 328 // other directories to test 329 reporter.logWarningMsg("processSubDir(" + base.getInput() 330 + ", " + base.getOutput() 331 + ", " + base.getGold() 332 + ") some dir does not exist, skipping!"); 333 return; 334 } 335 336 File subInputDir = new File(base.getInput()); 337 // Call worker method to process the individual directory 338 // and get a list of .xsl files to test 339 Vector files = getFilesFromDir(subInputDir, getFileFilter()); 340 341 if ((null == files) || (0 == files.size())) 342 { 343 reporter.logStatusMsg("processSubDir(" + base.getInput() 344 + ") no files found(1), skipping!"); 345 return; 346 } 347 348 // 'Transform' the list of individual test files into a 349 // list of Datalets with all fields filled in 350 //@todo should getFilesFromDir and buildDatalets be combined? 351 Vector datalets = buildDatalets(files, base); 352 353 if ((null == datalets) || (0 == datalets.size())) 354 { 355 reporter.logWarningMsg("processSubDir(" + base.getInput() 356 + ") no tests found(2), skipping!"); 357 return; 358 } 359 360 // Now process the list of files found in this dir 361 processFileList(datalets, "Testing subdir: " + base.getInput()); 362 } 363 364 365 /** 366 * Run a list of stylesheet tests through a Testlet. 367 * The file names are assumed to be fully specified, and we assume 368 * the corresponding directories exist. 369 * Each fileList is turned into a testcase. 370 * 371 * @param vector of Datalet objects to pass in 372 * @param desc String to use as testCase description 373 */ processFileList(Vector datalets, String desc)374 public void processFileList(Vector datalets, String desc) 375 { 376 // Validate arguments 377 if ((null == datalets) || (0 == datalets.size())) 378 { 379 // Bad arguments, report it as an error 380 // Note: normally, this should never happen, since 381 // this class normally validates these arguments 382 // before calling us 383 reporter.checkErr("processFileList: Testlet or datalets are null/blank, nothing to test!"); 384 return; 385 } 386 387 // Put each fileList into a testCase 388 reporter.testCaseInit(desc); 389 390 // Now just go through the list and process each set 391 int numDatalets = datalets.size(); 392 reporter.logInfoMsg("processFileList() with " + numDatalets 393 + " potential tests"); 394 // Iterate over every datalet and test it 395 for (int ctr = 0; ctr < numDatalets; ctr++) 396 { 397 try 398 { 399 // Create a Testlet to execute a test with this 400 // next datalet - the Testlet will log all info 401 // about the test, including calling check*() 402 getTestlet().execute((Datalet)datalets.elementAt(ctr)); 403 } 404 catch (Throwable t) 405 { 406 // Log any exceptions as fails and keep going 407 reporter.logThrowable(Logger.ERRORMSG, t, "Datalet threw"); 408 reporter.checkErr("Datalet num " + ctr + " threw: " + t.toString()); 409 } 410 } // of while... 411 reporter.testCaseClose(); 412 } 413 414 415 /** 416 * Use the supplied filter on given directory to return a list 417 * of tests to be run. 418 * 419 * The real logic is in the filter, which can be specified as 420 * an option or by overriding getDefaultFileFilter(). 421 * 422 * @param dir directory to scan 423 * @param filter to use on this directory; if null, uses default 424 * @return Vector of local path\filenames of tests to run; 425 * the tests themselves will exist; null if error 426 */ getFilesFromDir(File dir, FilenameFilter filter)427 public Vector getFilesFromDir(File dir, FilenameFilter filter) 428 { 429 // Validate arguments 430 if ((null == dir) || (!dir.exists())) 431 { 432 // Bad arguments, report it as an error 433 // Note: normally, this should never happen, since 434 // this class normally validates these arguments 435 // before calling us 436 reporter.logWarningMsg("getFilesFromDir(" + dir.toString() + ") dir null or does not exist"); 437 return null; 438 } 439 // Get the list of 'normal' test files 440 String[] files = dir.list(filter); 441 Vector v = new Vector(files.length); 442 for (int i = 0; i < files.length; i++) 443 { 444 v.addElement(files[i]); 445 } 446 reporter.logTraceMsg("getFilesFromDir(" + dir.toString() + ") found " + v.size() + " total files to test"); 447 return v; 448 } 449 450 451 /** 452 * Transform a vector of individual test names into a Vector 453 * of filled-in datalets to be tested 454 * 455 * This basically just calculates local path\filenames across 456 * the three presumably-parallel directory trees of 457 * inputDir, outputDir and goldDir. 458 * It then stuffs each of these values plus some 459 * generic info like our testProps into each datalet it creates. 460 * 461 * @param files Vector of local path\filenames to be tested 462 * @param base FileDatalet denoting directories 463 * input, output, gold 464 * @return Vector of FileDatalets that are fully filled in, 465 * i.e. output, gold, etc are filled in respectively 466 * to input 467 */ buildDatalets(Vector files, FileDatalet base)468 public Vector buildDatalets(Vector files, FileDatalet base) 469 { 470 // Validate arguments 471 if ((null == files) || (files.size() < 1)) 472 { 473 // Bad arguments, report it as an error 474 // Note: normally, this should never happen, since 475 // this class normally validates these arguments 476 // before calling us 477 reporter.logWarningMsg("buildDatalets null or empty file vector"); 478 return null; 479 } 480 Vector v = new Vector(files.size()); 481 482 // For every file in the vector, construct the matching 483 // out, gold, and xml/xsl files 484 for (Enumeration elements = files.elements(); 485 elements.hasMoreElements(); /* no increment portion */ ) 486 { 487 String file = null; 488 try 489 { 490 file = (String)elements.nextElement(); 491 } 492 catch (ClassCastException cce) 493 { 494 // Just skip this entry 495 reporter.logWarningMsg("Bad file element found, skipping: " + cce.toString()); 496 continue; 497 } 498 v.addElement(buildDatalet(base, file)); 499 } 500 return v; 501 } 502 503 /** 504 * Construct a FileDatalet with corresponding output, gold files. 505 * 506 * This basically just calls worker methods to construct and 507 * set options on a datalet to return. 508 * 509 * @param base FileDatalet denoting directories 510 * input, output, gold 511 * @param name bare name of the input file 512 * @return FileDatalet that is fully filled in, 513 * i.e. output, gold, etc are filled in respectively 514 * to input and any options are set 515 */ buildDatalet(FileDatalet base, String name)516 protected FileDatalet buildDatalet(FileDatalet base, String name) 517 { 518 // Worker method to construct paths 519 FileDatalet d = buildDataletPaths(base, name); 520 // Worker method to set any other options, etc. 521 setDataletOptions(d); 522 return d; 523 } 524 525 /** 526 * Construct a FileDatalet with corresponding output, gold files. 527 * 528 * This worker method just has the logic to construct the 529 * corresponding output and gold filenames; feel free to subclass. 530 * 531 * This class simply appends .out and .gld to the end of the 532 * existing names: foo.xml: foo.xml.out, foo.xml.gld. 533 * 534 * @param base FileDatalet denoting directories 535 * input, output, gold 536 * @param name bare name of the input file 537 * @return FileDatalet that is fully filled in, 538 * i.e. output, gold, etc are filled in respectively 539 * to input 540 */ buildDataletPaths(FileDatalet base, String name)541 protected FileDatalet buildDataletPaths(FileDatalet base, String name) 542 { 543 return new FileDatalet(base.getInput() + File.separator + name, 544 base.getOutput() + File.separator + name + OUT_EXTENSION, 545 base.getGold() + File.separator + name + GLD_EXTENSION); 546 } 547 548 /** 549 * Fillin FileDatalet.setOptions and any other processing. 550 * 551 * This is designed to be overriden so subclasses can put any 552 * special items in the datalet's options or do other 553 * preprocessing of the datalet. 554 * 555 * @param base FileDatalet to apply options, etc. to 556 */ setDataletOptions(FileDatalet base)557 protected void setDataletOptions(FileDatalet base) 558 { 559 base.setDescription(base.getInput()); 560 // Optimization: put in a copy of our fileChecker, so 561 // that each testlet doesn't have to create it's own 562 // fileCheckers should not store state, so this 563 // shouldn't affect the testing at all 564 base.setOptions(testProps); 565 // Note: set our options in the datalet first, then 566 // put the fileChecker directly into their options 567 base.getOptions().put("fileCheckerImpl", fileChecker); 568 } 569 570 /** If we should process the top level directory (default:false). */ getProcessTopDir()571 protected boolean getProcessTopDir() 572 { return false; } 573 574 /** If we should always recurse lower level directories (default:false). */ getRecurseDirs()575 protected boolean getRecurseDirs() 576 { return false; } 577 578 /** Default FilenameFilter FQCN for directories. */ getDefaultDirFilter()579 protected String getDefaultDirFilter() 580 { return "org.apache.qetest.DirFilter"; } 581 582 /** Default FilenameFilter FQCN for files. */ getDefaultFileFilter()583 protected String getDefaultFileFilter() 584 { return "org.apache.qetest.FilePatternFilter"; } 585 586 /** Default Testlet FQCN for executing stylesheet tests. */ getDefaultTestlet()587 protected String getDefaultTestlet() 588 { return "org.apache.qetest.FileTestlet"; } 589 590 /** Default list of packages to search for classes. */ getDefaultPackages()591 protected String[] getDefaultPackages() 592 { return QetestUtils.defaultPackages; } 593 594 /** Cached Testlet Class; used for life of this test. */ 595 protected Class cachedTestletClazz = null; 596 597 /** 598 * Convenience method to get a Testlet to use. 599 * Attempts to return one as specified by our testlet parameter, 600 * otherwise returns a default StylesheetTestlet. 601 * 602 * @return Testlet for use in this test; null if error 603 */ getTestlet()604 public Testlet getTestlet() 605 { 606 // Find a Testlet class to use if we haven't already 607 if (null == cachedTestletClazz) 608 { 609 cachedTestletClazz = QetestUtils.testClassForName(testlet, 610 getDefaultPackages(), 611 getDefaultTestlet()); 612 } 613 try 614 { 615 // Create it and set our reporter into it 616 Testlet t = (Testlet)cachedTestletClazz.newInstance(); 617 t.setLogger((Logger)reporter); 618 return (Testlet)t; 619 } 620 catch (Exception e) 621 { 622 // Ooops, none found! This should be very rare, since 623 // we know the defaultTestlet should be found 624 return null; 625 } 626 } 627 628 629 /** 630 * Convenience method to get a default filter for directories. 631 * Uses category member variable if set. 632 * 633 * @return FilenameFilter using DirFilter(category, null). 634 */ getDirFilter()635 public FilenameFilter getDirFilter() 636 { 637 // Find a FilenameFilter class to use 638 Class clazz = QetestUtils.testClassForName(dirFilter, 639 getDefaultPackages(), 640 getDefaultDirFilter()); 641 try 642 { 643 // Create it, optionally with a category 644 String category = testProps.getProperty(OPT_CATEGORY); 645 if ((null != category) && (category.length() > 1)) // Arbitrary check for non-null, non-blank string 646 { 647 Class[] parameterTypes = 648 { 649 java.lang.String.class, 650 java.lang.String.class 651 }; 652 Constructor ctor = clazz.getConstructor(parameterTypes); 653 654 Object[] ctorArgs = 655 { 656 category, 657 null 658 }; 659 return (FilenameFilter)ctor.newInstance(ctorArgs); 660 } 661 else 662 { 663 return (FilenameFilter)clazz.newInstance(); 664 } 665 } 666 catch (Exception e) 667 { 668 // Ooops, none found! 669 return null; 670 } 671 } 672 673 674 /** 675 * Convenience method to get a default filter for files. 676 * Uses excludes member variable if set. 677 * 678 * @return FilenameFilter using FileExtensionFilter(null, excludes). 679 */ getFileFilter()680 public FilenameFilter getFileFilter() 681 { 682 // Find a FilenameFilter class to use 683 Class clazz = QetestUtils.testClassForName(fileFilter, 684 getDefaultPackages(), 685 getDefaultFileFilter()); 686 try 687 { 688 // Create it, optionally with excludes 689 String excludes = testProps.getProperty(OPT_EXCLUDES); 690 if ((null != excludes) && (excludes.length() > 1)) // Arbitrary check for non-null, non-blank string 691 { 692 Class[] parameterTypes = 693 { 694 java.lang.String.class, 695 java.lang.String.class 696 }; 697 Constructor ctor = clazz.getConstructor(parameterTypes); 698 699 Object[] ctorArgs = 700 { 701 null, 702 excludes 703 }; 704 return (FilenameFilter)ctor.newInstance(ctorArgs); 705 } 706 else 707 { 708 return (FilenameFilter)clazz.newInstance(); 709 } 710 } 711 catch (Exception e) 712 { 713 // Ooops, none found! 714 return null; 715 } 716 } 717 718 719 /** 720 * Convenience method to get a default inputDir when none or 721 * a bad one was given. 722 * @return String pathname of default inputDir "tests\conf". 723 */ getDefaultInputDir()724 public String getDefaultInputDir() 725 { 726 return "tests" + File.separator + "conf"; 727 } 728 729 730 /** 731 * Convenience method to print out usage information - update if needed. 732 * @return String denoting usage of this test class 733 */ usage()734 public String usage() 735 { 736 return ("Additional options supported by FileTestletDriver:\n" 737 + " -" + OPT_FILELIST 738 + " <name of listfile of tests to run>\n" 739 + " -" + OPT_DIRFILTER 740 + " <classname of FilenameFilter for dirs>\n" 741 + " -" + OPT_FILEFILTER 742 + " <classname of FilenameFilter for files>\n" 743 + " -" + OPT_TESTLET 744 + " <classname of Testlet to execute tests with>\n" 745 + super.usage()); // Grab our parent classes usage as well 746 } 747 748 749 /** 750 * Main method to run test from the command line - can be left alone. 751 * @param args command line argument array 752 */ main(String[] args)753 public static void main(String[] args) 754 { 755 FileTestletDriver app = new FileTestletDriver(); 756 app.doMain(args); 757 } 758 } 759