1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2001, 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.finder; 28 29 import java.io.BufferedOutputStream; 30 import java.io.DataOutputStream; 31 import java.io.File; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.io.PrintStream; 35 import java.util.*; 36 import java.util.zip.ZipEntry; 37 import java.util.zip.ZipOutputStream; 38 39 40 import com.sun.javatest.TestDescription; 41 import com.sun.javatest.TestFinder; 42 43 /** 44 * BinaryTestWriter creates the data file used by BinaryTestFinder. 45 * It uses a test finder to find all the tests in a test suite and writes 46 * them out in a compact compressed form. By default it uses the standard 47 * tag test finder, and writes the output in a file called 48 * testsuite.jtd in the root directory of the test suite. 49 * <br> 50 * Options: 51 * <dl> 52 * <dt>-finder finderClass finderArgs ... -end 53 * <dd>the test finder to be used to locate the tests; the default is the standard tag test finder 54 * <dt>-strictFinder 55 * <dd>Do not ignore errors from the source finder, exit with error code instead 56 * <dt>-o output-file 57 * <dd>specify the name of the output file; the default is testsuite.jtd in the root directory of the test suite. 58 * <dt>testsuite 59 * <dd>(Required.) The test suite root file. 60 * <dt>initial-files 61 * <dd>(Optional)Any initial starting points within the test suite: the default is the test suite root 62 * </dl> 63 */ 64 public class BinaryTestWriter 65 { 66 /** 67 * This exception is used to report bad command line arguments. 68 */ 69 public class BadArgs extends Exception { 70 /** 71 * Create a BadArgs exception. 72 * @param msg A detail message about an error that has been found. 73 */ 74 BadArgs(String msg) { 75 super(msg); 76 } 77 } 78 79 /** 80 * This exception is used to report problems that occur while running. 81 */ 82 83 public class Fault extends Exception { 84 /** 85 * Create a Fault exception. 86 * @param msg A detail message about a fault that has occurred. 87 */ 88 Fault(String msg) { 89 super(msg); 90 } 91 } 92 93 //------------------------------------------------------------------------------------------ 94 95 /** 96 * Standard program entry point. 97 * @param args An array of strings, typically provided via the command line. 98 * The arguments should be of the form:<br> 99 * <em>[options]</em> <em>testsuite</em> <em>[tests]</em> 100 * <table><tr><th colspan=2>Options</th></tr> 101 * <tr><td>-finder <em>finderClass</em> <em>finderArgs</em> <em>...</em> -end 102 * <td>The name of a test finder class and any arguments it might take. 103 * The results of reading this test finder will be stored in the 104 * output file. 105 * <tr><td>-o <em>output-file</em> 106 * <td>The output file in which to write the results. 107 * </table> 108 */ 109 public static void main(String[] args) { 110 int result = 0; 111 112 try { 113 BinaryTestWriter m = new BinaryTestWriter(); 114 result = m.run(args); 115 } 116 catch (BadArgs e) { 117 System.err.println("Bad Arguments: " + e.getMessage()); 118 usage(System.err); 119 System.exit(1); 120 } 121 catch (Fault f) { 122 System.err.println("Error: " + f.getMessage()); 123 System.exit(2); 124 } 125 catch (IOException e) { 126 System.err.println("Error: " + e); 127 System.exit(3); 128 } 129 130 System.exit(result); 131 } 132 133 /** 134 * Print out command-line help. 135 */ 136 private static void usage(PrintStream out) { 137 String prog = System.getProperty("program", "java " + BinaryTestWriter.class.getName()); 138 out.println("Usage:"); 139 out.println(" " + prog + " [options] test-suite [tests...]"); 140 out.println("Options:"); 141 out.println(" -finder finderClass finderArgs... -end"); 142 out.println(" -o output-file"); 143 out.println(" -strictFinder"); 144 } 145 146 //------------------------------------------------------------------------------------------ 147 148 /** 149 * Main work method. 150 * Reads all the arguments on the command line, makes sure a valid 151 * testFinder is available, and then calls methods to create the tree of tests 152 * and then write the binary file. 153 * @param args An array of strings, typically provided via the command line 154 * @return The disposition of the run, i.e. zero for a problem-free execution, non-zero 155 * if there was some sort of problem. 156 * @throws BinaryTestWriter.BadArgs 157 * if a problem is found in the arguments provided 158 * @throws BinaryTestWriter.Fault 159 * if a fault is found while running 160 * @throws IOException 161 * if a problem is found while trying to read a file 162 * or write the output file 163 * @see #main 164 */ 165 public int run(String[] args) throws BadArgs, Fault, IOException { 166 File testSuite = null; 167 String finder = "com.sun.javatest.finder.TagTestFinder"; 168 String[] finderArgs = { }; 169 File outFile = null; 170 File[] tests = null; 171 172 for (int i = 0; i < args.length; i++) { 173 if (args[i].equalsIgnoreCase("-finder") && (i + 1 < args.length)) { 174 finder = args[++i]; 175 int j = ++i; 176 while ((i < args.length - 1) && !(args[i].equalsIgnoreCase("-end"))) 177 ++i; 178 finderArgs = new String[i - j]; 179 System.arraycopy(args, j, finderArgs, 0, finderArgs.length); 180 } 181 else if (args[i].equalsIgnoreCase("-o") && (i + 1 < args.length)) { 182 outFile = new File(args[++i]); 183 } 184 else if (args[i].equalsIgnoreCase("-strictFinder")) { 185 strictFinder = true; 186 } 187 else if (args[i].startsWith("-") ) { 188 throw new BadArgs(args[i]); 189 } 190 else { 191 testSuite = new File(args[i++]); 192 193 if (i < args.length) { 194 tests = new File[args.length - i]; 195 for (int j = 0; j < tests.length; j++) 196 tests[j] = new File(args[i + j]); 197 } 198 break; 199 } 200 } 201 202 if (testSuite == null) 203 throw new BadArgs("testsuite.html file not specified"); 204 205 TestFinder testFinder = initializeTestFinder(finder, finderArgs, testSuite); 206 207 if (tests == null) 208 tests = new File[] { testFinder.getRoot() }; // equals testSuite, adjusted by finder as necessary .. e.g. for dirWalk, webWalk etc 209 210 if (outFile == null) 211 outFile = new File(testFinder.getRootDir(), "testsuite.jtd"); 212 213 if (strictFinder) { 214 testFinder.setErrorHandler(new TestFinder.ErrorHandler() { 215 public void error(String msg) { 216 numFinderErrors++; 217 System.err.println("Finder reported error:\n" + msg); 218 System.err.println(""); 219 } 220 } 221 ); 222 } 223 224 StringTable stringTable = new StringTable(); 225 TestTable testTable = new TestTable(stringTable); 226 TestTree testTree = new TestTree(testTable); 227 228 if (log != null) 229 log.println("Reading tests..."); 230 231 // read the tests into internal data structures 232 read(testFinder, tests, testTree); 233 234 if (testTree.getSize() == 0) 235 throw new Fault("No tests found -- check arguments."); 236 237 // write out the data structure into a zip file 238 if (log != null) 239 log.println("Writing " + outFile); 240 241 try (FileOutputStream fos = new FileOutputStream(outFile); 242 ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fos))) { 243 zos.setMethod(ZipOutputStream.DEFLATED); 244 zos.setLevel(9); 245 ZipEntry stringZipEntry = stringTable.write(zos); 246 ZipEntry testTableZipEntry = testTable.write(zos); 247 ZipEntry testTreeZipEntry = testTree.write(zos); 248 249 // report statistics 250 if (log != null) { 251 log.println("strings: " + stringTable.getSize() + " entries, " + zipStats(stringZipEntry)); 252 log.println("tests: " + testTable.getSize() + " tests, " + zipStats(testTableZipEntry)); 253 log.println("tree: " + testTree.getSize() + " nodes, " + zipStats(testTreeZipEntry)); 254 } 255 256 if (strictFinder && numFinderErrors > 0) { 257 System.err.println("*** Source finder reported " + numFinderErrors + " errors during execution. ***"); 258 return 4; 259 } 260 else { 261 return 0; 262 } 263 } 264 } 265 266 /** 267 * Creates and initializes an instance of a test finder 268 * 269 * @param finder The class name of the required test finder 270 * @param args any args to pass to the TestFinder's init method. 271 * @param ts The testsuite root file 272 * @return The newly created TestFinder. 273 */ 274 private TestFinder initializeTestFinder(String finder, String[] args, File ts) throws Fault { 275 TestFinder testFinder; 276 277 if (ts == null) 278 throw new NullPointerException(); 279 280 try { 281 Class c = Class.forName(finder); 282 testFinder = (TestFinder) (c.newInstance()); 283 testFinder.init(args, ts, null); 284 } 285 catch (ClassNotFoundException e) { 286 throw new Fault("Error: Can't find class for test finder specified: " + finder); 287 } 288 catch (InstantiationException e) { 289 throw new Fault("Error: Can't create new instance of test finder: " + e); 290 } 291 catch (IllegalAccessException e) { 292 throw new Fault("Error: Can't access test finder: " + e); 293 } 294 catch (TestFinder.Fault e) { 295 throw new Fault("Error: Can't initialize test-finder: " + e.getMessage()); 296 } 297 298 return testFinder; 299 } 300 301 302 /** 303 * Gets and returns the test suite file. Adds testsuite.html or 304 * tests/testsuite.html to the end of the path if necessary. 305 */ 306 private File getTestSuiteFile(String file) throws Fault { 307 File tsa = new File(file); 308 if (tsa.isFile()) 309 return tsa; 310 else { 311 File tsb = new File(tsa, "testsuite.html"); 312 if (tsb.exists()) 313 return tsb; 314 else { 315 File tsc = new File(tsa, "tests/testsuite.html"); 316 if (tsc.exists()) 317 return tsc; 318 else 319 throw new Fault("Bad input. " + file + " is not a JCK"); 320 } 321 } 322 } 323 324 /** 325 * Create a string containing statistics about a zip file entry. 326 */ 327 private String zipStats(ZipEntry e) { 328 long size = e.getSize(); 329 long csize = e.getCompressedSize(); 330 return size + " bytes (" + csize + " compressed, " + (csize * 100 / size) + "%)"; 331 } 332 333 //------------------------------------------------------------------------------------------ 334 335 /** 336 * Read all the tests from a test suite and store them in a test tree 337 */ 338 void read(TestFinder finder, File[] files, TestTree testTree) throws Fault 339 { 340 if (files.length < 1) 341 throw new IllegalArgumentException(); 342 343 File rootDir = finder.getRootDir(); 344 Set<File> allFiles = new HashSet<>(); 345 346 TestTree.Node r = null; 347 for (int i = 0; i < files.length; i++) { 348 File f = files[i]; 349 if (!f.isAbsolute()) 350 f = new File(rootDir, f.getPath()); 351 352 TestTree.Node n = read0(finder, f, testTree, allFiles); 353 if (n == null) 354 continue; 355 356 while (!f.equals(rootDir)) { 357 f = f.getParentFile(); 358 n = testTree.new Node(f.getName(), noTests, new TestTree.Node[] { n }); 359 } 360 361 r = (r == null ? n : r.merge(n)); 362 } 363 364 if (r == null) 365 throw new Fault("No tests found"); 366 367 testTree.setRoot(r); 368 } 369 370 /** 371 * Read the tests from a file in test suite 372 */ 373 private TestTree.Node read0(TestFinder finder, File file, TestTree testTree, Set<File> allFiles) 374 { 375 // keep track of which files we have read, and ignore duplicates 376 if (allFiles.contains(file)) 377 return null; 378 else 379 allFiles.add(file); 380 381 finder.read(file); 382 TestDescription[] tests = finder.getTests(); 383 File[] files = finder.getFiles(); 384 385 if (tests.length == 0 && files.length == 0) 386 return null; 387 388 Arrays.sort(files); 389 Arrays.sort(tests, new Comparator() { 390 public int compare(Object o1, Object o2) { 391 TestDescription td1 = (TestDescription) o1; 392 TestDescription td2 = (TestDescription) o2; 393 return td1.getRootRelativeURL().compareTo(td2.getRootRelativeURL()); 394 } 395 }); 396 397 Vector<TestTree.Node> v = new Vector<>(); 398 for (int i = 0; i < files.length; i++) { 399 TestTree.Node n = read0(finder, files[i], testTree, allFiles); 400 if (n != null) 401 v.addElement(n); 402 } 403 TestTree.Node[] nodes = new TestTree.Node[v.size()]; 404 v.copyInto(nodes); 405 406 return testTree.new Node(file.getName(), tests, nodes); 407 } 408 409 //------------------------------------------------------------------------------------------ 410 411 /** 412 * Write an int to a data output stream using a variable length encoding. 413 * The int is broken into groups of seven bits, and these are written out 414 * in big-endian order. Leading zeroes are suppressed and all but the last 415 * byte have the top bit set. 416 * @see BinaryTestFinder#readInt 417 */ 418 private static void writeInt(DataOutputStream out, int v) throws IOException { 419 if (v < 0) 420 throw new IllegalArgumentException(); 421 422 boolean leadZero = true; 423 for (int i = 28; i > 0; i -= 7) { 424 int b = (v >> i) & 0x7f; 425 leadZero = leadZero && (b == 0); 426 if (!leadZero) 427 out.writeByte(0x80 | b); 428 } 429 out.writeByte(v & 0x7f); 430 } 431 432 //------------------------------------------------------------------------------------------ 433 434 private static final TestDescription[] noTests = { }; 435 private PrintStream log = System.out; 436 private boolean strictFinder = false; 437 private int numFinderErrors = 0; 438 439 //------------------------------------------------------------------------------------------ 440 441 /** 442 * StringTable is an array of strings. Other parts of the encoding can 443 * choose to write strings as references (indexes) into the string table. 444 * Strings in the table are use-counted so that only frequently used 445 * strings are output. 446 * @see BinaryTestFinder.StringTable 447 */ 448 static class StringTable { 449 /** 450 * Add a new string to the table; if it has already been added, 451 * increase its use count. 452 */ 453 void add(String s) { 454 Entry e = map.get(s); 455 if (e == null) { 456 e = new Entry(); 457 map.put(s, e); 458 } 459 e.useCount++; 460 } 461 462 /** 463 * Add all the strings used in a test description to the table. 464 */ 465 void add(TestDescription test) { 466 for (Iterator i = test.getParameterKeys(); i.hasNext(); ) { 467 String key = (String) (i.next()); 468 String param = test.getParameter(key); 469 add(key); 470 add(param); 471 } 472 } 473 474 /** 475 * Return the number of strings in the table. 476 */ 477 int getSize() { 478 return map.size(); 479 } 480 481 /** 482 * Return the number of sstrings that were written to the output file. 483 * Not all strings are written out: only frequently used ones are. 484 */ 485 int getWrittenSize() { 486 return writtenSize; 487 } 488 489 /** 490 * Get the index of a string in the table. 491 */ 492 int getIndex(String s) { 493 Entry e = map.get(s); 494 if (e == null) 495 throw new IllegalArgumentException(); 496 return e.index; 497 } 498 499 /** 500 * Write the contents of the table to an entry called "strings" 501 * in a zip file. 502 */ 503 ZipEntry write(ZipOutputStream zos) throws IOException 504 { 505 ZipEntry entry = new ZipEntry("strings"); 506 zos.putNextEntry(entry); 507 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 508 write(dos); 509 dos.flush(); 510 zos.closeEntry(); 511 return entry; 512 } 513 514 /** 515 * Write the contents of the table to a stream 516 */ 517 void write(DataOutputStream o) throws IOException { 518 Vector<String> v = new Vector<>(map.size()); 519 v.addElement(""); 520 int nextIndex = 1; 521 for (Iterator<Map.Entry<String, Entry>> iter = map.entrySet().iterator(); iter.hasNext(); ) { 522 Map.Entry<String, Entry> e = iter.next(); 523 String key = e.getKey(); 524 Entry entry = e.getValue(); 525 if (entry.isFrequent()) { 526 entry.index = nextIndex++; 527 v.addElement(key); 528 } 529 } 530 531 writeInt(o, v.size()); 532 for (int i = 0; i < v.size(); i++) 533 o.writeUTF(v.elementAt(i)); 534 535 writtenSize = nextIndex; 536 } 537 538 /** 539 * Write a reference to a string to a stream. The string must have 540 * previously been added into nthe string table, and the string table 541 * written out. 542 * If the string is a frequent one, a pointer to its position in the 543 * previously written stream will be generated. If it is not a frequent 544 * string, zero will be written, followed by the value of the string itself. 545 */ 546 void writeRef(String s, DataOutputStream o) throws IOException { 547 Entry e = map.get(s); 548 if (e == null) 549 throw new IllegalArgumentException(); 550 551 if (e.isFrequent()) 552 writeInt(o, e.index); 553 else { 554 writeInt(o, 0); 555 o.writeUTF(s); 556 } 557 } 558 559 private Map<String, Entry> map = new TreeMap<>(); 560 private int writtenSize; 561 562 /** 563 * Data for each string in the string table. 564 */ 565 static class Entry { 566 /** 567 * How many times the string has been added to the string table. 568 */ 569 int useCount = 0; 570 571 /** 572 * The position of the string in the table when the table 573 * was written. 574 */ 575 int index = 0; 576 577 /** 578 * Determine if the string is frequent enough in the table to 579 * be written out. 580 */ 581 boolean isFrequent() { 582 return (useCount > 1); 583 } 584 } 585 } 586 587 //------------------------------------------------------------------------------------------ 588 589 /** 590 * TestTable is a table of test descriptions, whose written form is 591 * based on references into a string table. 592 * @see BinaryTestFinder.TestTable 593 */ 594 static class TestTable 595 { 596 /** 597 * Create a new TestTable. 598 */ 599 TestTable(StringTable stringTable) { 600 this.stringTable = stringTable; 601 } 602 603 /** 604 * Add a test description to the test table. The strings used by the 605 * test description are automatically added to the testTable's stringTable. 606 */ 607 void add(TestDescription td) { 608 tests.addElement(td); 609 testMap.put(td, new Entry()); 610 stringTable.add(td); 611 } 612 613 /** 614 * Get the number of tests in this test table. 615 */ 616 int getSize() { 617 return tests.size(); 618 } 619 620 /** 621 * Get the index for a test description, based on its position when the 622 * test table was written out. This index is the byte offset in the 623 * written stream. 624 */ 625 int getIndex(TestDescription td) { 626 Entry e = testMap.get(td); 627 if (e == null) 628 throw new IllegalArgumentException(); 629 return e.index; 630 } 631 632 /** 633 * Write the contents of the table to an entry called "tests" 634 * in a zip file. 635 */ 636 ZipEntry write(ZipOutputStream zos) throws IOException 637 { 638 ZipEntry entry = new ZipEntry("tests"); 639 zos.putNextEntry(entry); 640 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 641 write(dos); 642 dos.flush(); 643 zos.closeEntry(); 644 return entry; 645 } 646 647 /** 648 * Write the contents of the table to a stream. The position of each test 649 * description in the stream is recorded, so that a random acess stream 650 * can randomly access the individual test descriptions. The table is 651 * written as a count, followed by that many encoded test descriptions. 652 * Each test description is written as a count followed by that many 653 * name-value pairs of string references. 654 */ 655 void write(DataOutputStream o) throws IOException { 656 writeInt(o, tests.size()); 657 for (int i = 0; i < tests.size(); i++) { 658 TestDescription td = tests.elementAt(i); 659 Entry e = testMap.get(td); 660 e.index = o.size(); 661 write(td, o); 662 } 663 } 664 665 /** 666 * Write a single test description to a stream. It is written as a count, 667 * followed by that many name-value pairs of string references. 668 */ 669 private void write(TestDescription td, DataOutputStream o) throws IOException { 670 // should consider using load/save here 671 writeInt(o, td.getParameterCount()); 672 for (Iterator i = td.getParameterKeys(); i.hasNext(); ) { 673 String key = (String) (i.next()); 674 String value = td.getParameter(key); 675 stringTable.writeRef(key, o); 676 stringTable.writeRef(value, o); 677 } 678 } 679 680 private Map<TestDescription, Entry> testMap = new HashMap<>(); 681 private Vector<TestDescription> tests = new Vector<>(); 682 private StringTable stringTable; 683 684 /** 685 * Data for each test description in the table. 686 */ 687 class Entry { 688 /** 689 * The byte offset of the test description in the stream when 690 * last written out. 691 */ 692 int index = -1; 693 } 694 } 695 696 //------------------------------------------------------------------------------------------ 697 698 /** 699 * TestTree is a tree of tests, whose written form is based on 700 * references into a TestTable. There is a very strong correspondence 701 * between a node and the results of reading a file from a test finder, 702 * which yields a set of test descriptions and a set of additional files 703 * to be read. 704 * @see BinaryTestFinder.TestTable 705 */ 706 static class TestTree 707 { 708 /** 709 * Create an test tree. The root node of the tree should be set later. 710 */ 711 TestTree(TestTable testTable) { 712 this.testTable = testTable; 713 } 714 715 /** 716 * Set the root node of the tree. 717 */ 718 void setRoot(Node root) { 719 this.root = root; 720 } 721 722 /** 723 * Get the number of nodes in this tree. 724 */ 725 int getSize() { 726 return (root == null ? 0 : root.getSize()); 727 } 728 729 /** 730 * Write the contents of the tree to an entry called "tree" 731 * in a zip file. 732 */ 733 ZipEntry write(ZipOutputStream zos) throws IOException 734 { 735 ZipEntry entry = new ZipEntry("tree"); 736 zos.putNextEntry(entry); 737 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 738 write(dos); 739 dos.flush(); 740 zos.closeEntry(); 741 return entry; 742 } 743 744 /** 745 * Write the contents of the tree to a stream. Each node of the tree 746 * is written as 3 parts: 747 * <ul> 748 * <li>the name of the node 749 * <li>the number of test descriptions in this node, followed by that 750 * many references into the test table. 751 * <li>the number of child nodes, followed by that many nodes, written 752 * recursively. 753 * </ul> 754 */ 755 void write(DataOutputStream o) throws IOException { 756 root.write(o); 757 } 758 759 private Node root; 760 private TestTable testTable; 761 762 /** 763 * A node within the test tree. Each node has a name, a set of test 764 * descriptions, and a set of child nodes. 765 */ 766 class Node 767 { 768 /** 769 * Create a node. The individual test descriptions are added to 770 * the tree's test table. 771 */ 772 Node(String name, TestDescription[] tests, Node[] children) { 773 this.name = name; 774 this.tests = tests; 775 this.children = children; 776 777 for (int i = 0; i < tests.length; i++) 778 testTable.add(tests[i]); 779 } 780 781 /** 782 * Get the number of nodes at this point in the tree: count one 783 * for this node and add the size of all its children. 784 */ 785 int getSize() { 786 int n = 1; 787 if (children != null) { 788 for (int i = 0; i < children.length; i++) 789 n += children[i].getSize(); 790 } 791 return n; 792 } 793 794 /** 795 * Merge the contents of this node with another to produce 796 * a new node. 797 * @param other The node to be merged with this one. 798 * @return a new Node, containing the merge of this one 799 * and the specified node. 800 */ 801 Node merge(Node other) { 802 if (!other.name.equals(name)) 803 throw new IllegalArgumentException(name + ":" + other.name); 804 805 TreeMap<String, Node> mergedChildrenMap = new TreeMap<>(); 806 for (int i = 0; i < children.length; i++) { 807 Node child = children[i]; 808 mergedChildrenMap.put(child.name, child); 809 } 810 for (int i = 0; i < other.children.length; i++) { 811 Node otherChild = other.children[i]; 812 Node c = mergedChildrenMap.get(otherChild.name); 813 mergedChildrenMap.put(otherChild.name, 814 (c == null ? otherChild : otherChild.merge(c))); 815 } 816 Node[] mergedChildren = 817 mergedChildrenMap.values().toArray(new Node[mergedChildrenMap.size()]); 818 819 TestDescription[] mergedTests; 820 if (tests.length + other.tests.length == 0) 821 mergedTests = noTests; 822 else { 823 mergedTests = new TestDescription[tests.length + other.tests.length]; 824 System.arraycopy(tests, 0, mergedTests, 0, tests.length); 825 System.arraycopy(other.tests, 0, mergedTests, tests.length, other.tests.length); 826 } 827 828 return new Node(name, mergedTests, mergedChildren); 829 } 830 831 /** 832 * Write the contents of a node to a stream. First the name 833 * is written, then the number of test descriptions, followed 834 * by that many references to the test table, then the number 835 * of child nodes, followed by that many child nodes in place. 836 */ 837 void write(DataOutputStream o) throws IOException { 838 o.writeUTF(name); 839 writeInt(o, tests.length); 840 for (int i = 0; i < tests.length; i++) 841 writeInt(o, testTable.getIndex(tests[i])); 842 writeInt(o, children.length); 843 for (int i = 0; i < children.length; i++) 844 children[i].write(o); 845 } 846 847 private String name; 848 private TestDescription[] tests; 849 private Node[] children; 850 } 851 } 852 853 }