1 /* 2 * $Id$ 3 * 4 * Copyright (c) 1996, 2011, 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.File; 30 import java.text.Collator; 31 import java.util.Arrays; 32 import java.util.Comparator; 33 import java.util.HashMap; 34 import java.util.Iterator; 35 import java.util.Locale; 36 import java.util.Map; 37 import java.util.Vector; 38 39 import com.sun.javatest.util.I18NResourceBundle; 40 import com.sun.javatest.util.StringArray; 41 42 /** 43 * Base implementation for test finders which search for test descriptions 44 * given a starting location. When creating instances of TestFinder for use, 45 * the creator should be sure to call the init() method before use. 46 */ 47 public abstract class TestFinder 48 { 49 /** 50 * This exception is to report serious problems that occur while 51 * finding tests. 52 */ 53 public static class Fault extends Exception 54 { 55 /** 56 * Create a Fault. 57 * @param i18n A resource bundle in which to find the detail message. 58 * @param msgKey The key for the detail message. 59 */ 60 public Fault(I18NResourceBundle i18n, String msgKey) { 61 super(i18n.getString(msgKey)); 62 } 63 64 /** 65 * Create a Fault. 66 * @param i18n A resource bundle in which to find the detail message. 67 * @param msgKey The key for the detail message. 68 * @param arg An argument to be formatted with the detail message by 69 * {@link java.text.MessageFormat#format} 70 */ 71 public Fault(I18NResourceBundle i18n, String msgKey, Object arg) { 72 super(i18n.getString(msgKey, arg)); 73 } 74 75 /** 76 * Create a Fault. 77 * @param i18n A resource bundle in which to find the detail message. 78 * @param msgKey The key for the detail message. 79 * @param args An array of arguments to be formatted with the detail message by 80 * {@link java.text.MessageFormat#format} 81 */ 82 public Fault(I18NResourceBundle i18n, String msgKey, Object[] args) { 83 super(i18n.getString(msgKey, args)); 84 } 85 } 86 87 /** 88 * This interface is used to report significant errors found while 89 * reading files, but which are not of themselves serious enough 90 * to stop reading further. More serious errors can be reported by 91 * throwing TestFinder.Fault. 92 * @see TestFinder#error 93 * @see TestFinder#localizedError 94 * @see TestFinder.Fault 95 */ 96 public static interface ErrorHandler { 97 /** 98 * Report an error found while reading a file. 99 * @param msg A detail string identifying the error 100 */ 101 void error(String msg); 102 } 103 104 105 /** 106 * Initialize the data required by the finder. 107 * Clients creating instances of test finders should call this before allowing use 108 * of the finder. Not doing so may result in unexpected results. 109 * @param args 110 * An array of strings specified as arguments in the environment. Null 111 * indicates no args. 112 * @param testSuiteRoot 113 * The root file that will be passed to test descriptions read 114 * by the finder. 115 * @param env 116 * The environment being used to run the test. May be null. 117 * @throws TestFinder.Fault if there is a problem interpreting any of args. 118 */ 119 public void init(String[] args, File testSuiteRoot, TestEnvironment env) throws Fault { 120 if (args != null) 121 decodeAllArgs(args); 122 123 setRoot(testSuiteRoot); 124 this.env = env; 125 } 126 127 /** 128 * Initialize the data required by the finder. 129 * Clients creating instances of test finders should call this before allowing use 130 * of the finder. Not doing so may result in unexpected results. 131 * @param args 132 * An array of strings specified as arguments in the environment. Null 133 * indicates no args. 134 * @param testSuiteRoot 135 * The root file that will be passed to test descriptions read 136 * by the finder. 137 * @param tests 138 * The tests to be read by the finder. (ignored) 139 * @param filters 140 * An optional array of filters to filter the tests read by the finder. 141 * @param env 142 * The environment being used to run the test. May be null. 143 * @throws TestFinder.Fault if there is a problem interpreting any of args. 144 * @see #init(String[],File,TestEnvironment) 145 * @deprecated Use one of the other init() methods. This functionality is no 146 * longer supported. Methods on TestResultTable should yield similar 147 * results. 148 */ 149 public void init(String[] args, File testSuiteRoot, 150 File[] tests, TestFilter[] filters, 151 TestEnvironment env) throws Fault { 152 init(args, testSuiteRoot, env); 153 } 154 155 /** 156 * Perform argument decoding, calling decodeArg for successive 157 * args until exhausted. 158 * @param args The arguments to be decoded 159 * @throws TestFinder.Fault if decodeArg throws the exception 160 * while decoding one of the arguments, or if decodeArg does 161 * not recognize an argument. 162 */ 163 protected void decodeAllArgs(String[] args) throws Fault { 164 for (int i = 0; i < args.length; ) { 165 int j = decodeArg(args, i); 166 if (j == 0) { 167 throw new Fault(i18n, "finder.badArg", args[i]); 168 } 169 i += j; 170 } 171 } 172 173 /** 174 * Decode the arg at a specified position in the arg array. 175 * If overridden by a subtype, the subtype should try and decode any 176 * args it recognizes, and then call super.decodeArg to give the 177 * superclass(es) a chance to recognize any arguments. 178 * @param args The array of arguments 179 * @param i The next argument to be decoded 180 * @return The number of elements consumed in the array; 181 * for example, for a simple option like "-v" the 182 * result should be 1; for an option with an argument 183 * like "-f file" the result should be 2, etc. 184 * @throws TestFinder.Fault If there is a problem with the value of the current 185 * arg, such as a bad value to an option, the Fault 186 * exception can be thrown. The exception should NOT be 187 * thrown if the current arg is unrecognized: in that case, 188 * an implementation should delegate the call to the 189 * supertype. 190 */ 191 protected int decodeArg(String[] args, int i) throws Fault { 192 return 0; 193 } 194 195 /** 196 * Set the test suite root file or directory. 197 * @param testSuiteRoot The path to be set as the root of the 198 * test suite in which files will be read. 199 * @throws IllegalStateException if already set 200 * @throws TestFinder.Fault if there is some test-finder-specific 201 * problem with the specified file. 202 * @see #getRoot 203 */ 204 protected void setRoot(File testSuiteRoot) throws IllegalStateException, Fault { 205 if (root != null) 206 throw new IllegalStateException("root already set"); 207 208 root = (testSuiteRoot.isAbsolute() ? 209 testSuiteRoot : new File(userDir, testSuiteRoot.getPath())); 210 rootDir = (root.isDirectory() ? 211 root : new File(root.getParent())); 212 } 213 214 /** 215 * Get the root file of the test suite, as passed in to the 216 * <code>init</code> method. 217 * @return the root file of the test suite 218 * @see #setRoot 219 */ 220 public File getRoot() { 221 return root; 222 } 223 224 /** 225 * Get the root directory of the test suite; this is either the 226 * root passed in to the init method or if that is a file, it is 227 * the directory containing the file. 228 * @return the root directory of the test suite 229 */ 230 public File getRootDir() { 231 return rootDir; 232 } 233 234 //-------------------------------------------------------------------------- 235 236 /** 237 * Incoming files and test descriptions are sorted by their name during 238 * processing, this method allows adjustment of the comparison method to 239 * be used during this sorting. Sorting can be disabled by calling this 240 * method with a <code>null</code> parameter. By default, this class will 241 * do US Locale sorting. 242 * 243 * @param c The comparison operator to be used. Null indicates no sorting (old behavior). 244 * @see #getComparator 245 * @see #foundTestDescription(TestDescription) 246 * @see #foundFile(File) 247 * @since 3.2 248 */ 249 public void setComparator(Comparator<String> c) { 250 comp = c; 251 252 } 253 254 /** 255 * Get the current comparator being used. 256 * 257 * @return The current comparator, may be null. 258 * @see #setComparator 259 * @since 3.2 260 */ 261 public Comparator<String> getComparator() { 262 return comp; 263 } 264 265 /** 266 * Get the default to be used when the user does not want to specify 267 * their own. The default is a US Locale Collator. 268 * @return The comparator which would be used if a custom one was not provided. 269 */ 270 protected static Comparator<String> getDefaultComparator() { 271 // this is the default 272 final Collator c = Collator.getInstance(Locale.US); 273 c.setStrength(Collator.PRIMARY); 274 return new Comparator<String>() { 275 @Override 276 public int compare(String s1, String s2) { 277 return c.compare(s1, s2); 278 } 279 }; 280 } 281 282 //-------------------------------------------------------------------------- 283 284 /** 285 * Get the registered error handler. 286 * @return The error handler currently receiving error messages. May 287 * be null. 288 * @see #setErrorHandler 289 */ 290 public ErrorHandler getErrorHandler() { 291 return errHandler; 292 } 293 294 /** 295 * Set an error handler to be informed of errors that may arise 296 * while reading tests. This is typically used to report errors 297 * that are not associated with any specific test, such as syntax 298 * errors outside of any test description, or problems accessing files. 299 * @param h The error handler that will be informed of non-fatal 300 * errors that occur while reading the test suite 301 * @see #getErrorHandler 302 */ 303 public void setErrorHandler(ErrorHandler h) { 304 errHandler = h; 305 } 306 307 /** 308 * Report an error to the error handler. 309 * @param i18n A resource bundle containing the localized error messages 310 * @param key The name of the entry in the resource bundle containing 311 * the appropriate error message. 312 * The message should not need any arguments. 313 */ 314 protected void error(I18NResourceBundle i18n, String key) { 315 localizedError(i18n.getString(key)); 316 } 317 318 /** 319 * Report an error to the error handler. 320 * @param i18n A resource bundle containing the localized error messages 321 * @param key The name of the entry in the resource bundle containing 322 * the appropriate error message. 323 * The message will be formatted with a single argument, using 324 * MessageFormat.format. 325 * @param arg The argument to be formatted in the message found in the 326 * resource bundle 327 */ 328 protected void error(I18NResourceBundle i18n, String key, Object arg) { 329 localizedError(i18n.getString(key, arg)); 330 } 331 332 /** 333 * Report an error to the error handler. 334 * @param i18n A resource bundle containing the localized error messages 335 * @param key The name of the entry in the resource bundle containing 336 * the appropriate error message. 337 * The message will be formatted with an array of arguments, using 338 * MessageFormat.format. 339 * @param args The arguments to be formatted in the message found in the 340 * resource bundle 341 */ 342 protected void error(I18NResourceBundle i18n, String key, Object[] args) { 343 localizedError(i18n.getString(key, args)); 344 } 345 346 /** 347 * Report a message to the error handler, without additional processing. 348 * @param msg The message to be reported 349 * @see #error 350 */ 351 protected void localizedError(String msg) { 352 errorMessages.add(msg); 353 if (errHandler != null) 354 errHandler.error(msg); 355 } 356 357 /** 358 * Get an count of the number of errors found by this test finder, 359 * as recorded by calls to the error handler via error and localizedError. 360 * The count may be reset using the clearErrors method. 361 * @return the number of errors found by the test finder 362 * @see #getErrors 363 * @see #clearErrors 364 */ 365 public synchronized int getErrorCount() { 366 return errorMessages.size(); 367 } 368 369 /** 370 * Get the errors recorded by the test finder, as recorded by calls 371 * to the error handler via error and localizedError. Errors reported 372 * by the error methods will be given localized. If there are no errors.\, 373 * an empty array (not null) will be returned. 374 * @return the errors found by the test finder 375 */ 376 public synchronized String[] getErrors() { 377 String[] errs = new String[errorMessages.size()]; 378 errorMessages.copyInto(errs); 379 return errs; 380 } 381 382 /** 383 * Clear outstanding errors found by the test finder, so that until 384 * a new error is reported, getErrorCount will return 0 and getErrors 385 * will return an empty array. 386 */ 387 public synchronized void clearErrors() { 388 errorMessages.setSize(0); 389 } 390 391 392 //-------------------------------------------------------------------------- 393 394 /** 395 * Determine whether a location corresponds to a directory (folder) or 396 * an actual file. If the finder implementation chooses, the locations 397 * used in read() and scan() may be real or virtual. This method will be 398 * queried to determine if a location is a container or something that 399 * should be scanned for tests. If it is both... 400 * @since 4.0 401 * @param path The location in question. 402 */ 403 public boolean isFolder(File path) { 404 if (!path.isAbsolute()) { 405 File f = new File(getRoot(), path.getPath()); 406 return f.isDirectory(); 407 } 408 else 409 return path.isDirectory(); 410 } 411 412 /** 413 * Determine when the last time this path was modified. This is used 414 * to decide whether to rescan that location or not. The default implementation 415 * defers the choice to the java. 416 * @since 4.0 417 * @param f The location in question. 418 */ 419 public long lastModified(File f) { 420 if (f.isAbsolute()) 421 return f.lastModified(); 422 else { 423 File real = new File(getRoot(), f.getPath()); 424 return real.lastModified(); 425 } 426 } 427 428 /** 429 * Read a file, looking for test descriptions and other files that might 430 * need to be read. If the file is relative, it will be evaluated relative 431 * to getRootDir. Depending on the test finder, the file may be either 432 * a plain file or a directory. 433 * @param file The file to be read. 434 */ 435 public synchronized void read(File file) { 436 if (tests != null) 437 tests.setSize(0); 438 439 if (files != null) 440 files.setSize(0); 441 442 testsInFile.clear(); 443 444 scan(file.isAbsolute() ? file : new File(rootDir, file.getPath())); 445 //scan(file); 446 } 447 448 /** 449 * Scan a file, looking for test descriptions and other files that might 450 * need to be scanned. The implementation depends on the type of test 451 * finder. 452 * @param file The file to scan 453 */ 454 protected abstract void scan(File file); 455 456 /** 457 * Handle a test description entry read from a file. 458 * By default, the name-value pair is inserted into the entries 459 * dictionary; however, the method can be overridden by a subtype 460 * to adjust the name or value before putting it into the dictionary, 461 * or even to ignore/fault the pair. 462 * @param entries The dictionary of the entries being read 463 * @param name The name of the entry that has been read 464 * @param value The value of the entry that has been read 465 */ 466 protected void processEntry(Map<String, String> entries, String name, String value) { 467 // uniquefy the keys as they go into the entries table 468 name = name.intern(); 469 470 if (name.equalsIgnoreCase("keywords")) { 471 // canonicalize keywords in their own special table 472 String keywordCacheValue = keywordCache.get(value); 473 if (keywordCacheValue == null) { 474 String lv = value.toLowerCase(); 475 String[] lvs = StringArray.split(lv); 476 Arrays.sort(lvs); 477 keywordCacheValue = StringArray.join(lvs).intern(); 478 keywordCache.put(value, keywordCacheValue); 479 } 480 value = keywordCacheValue; 481 } 482 else 483 value = value.intern(); 484 485 entries.put(name, value); 486 } 487 488 // cache for canonicalized lists of keywords 489 private Map<String, String> keywordCache = new HashMap<>(); 490 491 /** 492 * "normalize" the test description entries read from a file. 493 * By default, this is a no-op; however, the method can be overridden 494 * by a subtype to supply default values for missing entries, etc. 495 * @param entries A set of tag values read from a test description in a file 496 * @return A normalized set of entries 497 */ 498 protected Map<String, String> normalize(Map<String, String> entries) { 499 return entries; 500 } 501 502 503 //-------------------------------------------------------------------------- 504 505 /** 506 * Report that data for a test description has been found. 507 * @param entries The data for the test description 508 * @param file The file being read 509 * @param line The line number within the file (used for error messages) 510 */ 511 protected void foundTestDescription(Map<String, String> entries, File file, int line) { 512 entries = normalize(entries); 513 514 if (debug) { 515 System.err.println("Found TestDescription"); 516 517 System.err.println("--------values----------------------------"); 518 for (Iterator<String> i = entries.keySet().iterator() ; i.hasNext() ;) { 519 Object key = i.next(); 520 System.err.println(">> " + key + ": " + entries.get(key) ); 521 } 522 System.err.println("------------------------------------------"); 523 } 524 525 String id = entries.get("id"); 526 if (id == null) 527 id = ""; 528 529 // make sure test has unique id within file 530 Integer prevLine = testsInFile.get(id); 531 if (prevLine != null) { 532 int i = 1; 533 String newId; 534 while (testsInFile.get(newId = (id + "__" + i)) != null) 535 i++; 536 537 error(i18n, "finder.nonUniqueId", 538 new Object[] { file, 539 (id.equals("") ? "(unset)" : id), 540 new Integer(line), 541 prevLine, 542 newId } 543 ); 544 545 id = newId; 546 entries.put("id", id); 547 } 548 549 testsInFile.put(id, new Integer(line)); 550 551 // create the test description 552 TestDescription td = new TestDescription(root, file, entries); 553 554 if (errHandler != null) { 555 // more checks: check that the path does not include white space, 556 // because the exclude list parser does not handle paths with whitespace 557 String rru = td.getRootRelativeURL(); 558 if (rru.indexOf(' ') != -1) { 559 error(i18n, "finder.spaceInId", td.getRootRelativeURL()); 560 } 561 } 562 563 foundTestDescription(td); 564 } 565 566 /** 567 * Report that a test description has been found. 568 * @param td The data for the test description. May never be null. 569 * @see #foundTestDescription(java.util.Map, java.io.File, int) 570 */ 571 protected void foundTestDescription(TestDescription td) { 572 if (debug) { 573 System.err.println("Found TestDescription" + td.getName()); 574 } 575 576 if (tests == null) 577 tests = new Vector<>(); 578 579 int target = 0; 580 581 // binary insert 582 if (tests.size() == 0) { 583 target = 0; 584 } 585 else if (comp == null) { 586 target = tests.size(); // at end 587 } 588 else { 589 int left = 0, right = tests.size()-1, center = 0; 590 String name = td.getName(); 591 592 while (left < right) { 593 center = ((right+left)/2); 594 int cmp = comp.compare(name, tests.get(center).getName()); 595 if (cmp < 0) 596 right = center; 597 else if (cmp >= 0) 598 left = center+1; 599 } // while 600 601 if (comp.compare(name, tests.get(left).getName()) > 0) 602 target = left+1; 603 else 604 target = left; 605 606 /* old insertion sort 607 for (int i = 0; i < tests.size(); i++) { 608 if (comp.compare(td.getName(), 609 ((TestDescription)tests.elementAt(i)).getName()) > 0) { 610 target = i; 611 break; 612 } 613 else { } 614 } // for 615 */ 616 } 617 618 tests.insertElementAt(td, target); 619 } 620 621 /** 622 * Get the test descriptions that were found by the most recent call 623 * of read. 624 * @return the test descriptions that were found by the most recent call 625 * of read. 626 * @see #read 627 * @see #foundTestDescription 628 */ 629 public TestDescription[] getTests() { 630 if (tests == null) 631 return noTests; 632 else { 633 TestDescription[] tds = new TestDescription[tests.size()]; 634 tests.copyInto(tds); 635 return tds; 636 } 637 } 638 639 private static final TestDescription[] noTests = { }; 640 641 /** 642 * Report that another file that needs to be read has been found. 643 * @param newFile the file that has been found that needs to be read. 644 * @see #read 645 * @see #getFiles 646 */ 647 protected void foundFile(File newFile) { 648 if (files == null) 649 files = new Vector<>(); 650 651 int target = 0; 652 653 // binary insert 654 if (files.size() == 0) { 655 target = 0; 656 } 657 else if (comp == null) { 658 target = files.size(); // at end 659 } 660 else { 661 int left = 0, right = files.size()-1, center = 0; 662 String path = newFile.getPath(); 663 664 while (left < right) { 665 center = ((right+left)/2); 666 int cmp = comp.compare(path, files.get(center).getPath()); 667 if (cmp < 0) 668 right = center; 669 else if (cmp >= 0) 670 left = center+1; 671 } // while 672 673 if (comp.compare(path, files.get(left).getPath()) > 0) 674 target = left+1; 675 else 676 target = left; 677 } 678 679 // this is insertion sort to get locale sensitive sorting of 680 // the test suite content 681 /* 682 int target = files.size(); 683 684 if (comp != null) { 685 for (int i = 0; i < files.size(); i++) { 686 if (comp.compare(newFile.getPath(), 687 ((File)files.elementAt(i)).getPath()) < 0) { 688 target = i; 689 break; 690 } 691 else { } 692 } // for 693 } 694 else { 695 // just let it insert at the end 696 } 697 */ 698 699 files.insertElementAt(newFile, target); 700 } 701 702 /** 703 * Get the files that were found by the most recent call 704 * of read. 705 * @return the files that were found by the most recent call of read. 706 * @see #read 707 * @see #foundFile 708 */ 709 public File[] getFiles() { 710 if (files == null) 711 return new File[0]; 712 else { 713 File[] fs = new File[files.size()]; 714 files.copyInto(fs); 715 return fs; 716 } 717 } 718 719 720 //----------member variables------------------------------------------------ 721 private File root; 722 private File rootDir; 723 724 /** 725 * The environment passed in when the test finder was initialized. 726 * It is not used by the basic test finder code, but may be used 727 * by individual test finders to modify test descriptions as they are 728 * read. 729 * @deprecated This feature was available in earlier versions of 730 * JT Harness but does not interact well with JT Harness 3.0's GUI features. 731 * Use with discretion, if at all. 732 */ 733 protected TestEnvironment env; 734 private ErrorHandler errHandler; 735 private Comparator<String> comp = getDefaultComparator(); 736 737 private Vector<File> files; 738 private Vector<TestDescription> tests; 739 740 private Map<String, Integer> testsInFile = new HashMap<>(); 741 742 private Vector<String> errorMessages = new Vector<>(); 743 744 /** 745 * A boolean to enable trace output while debugging test finders. 746 */ 747 protected static boolean debug = Boolean.getBoolean("debug." + TestFinder.class.getName()); 748 private static final File userDir = new File(System.getProperty("user.dir")); 749 private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(TestFinder.class); 750 751 }