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<TestDescription>() { 390 public int compare(TestDescription td1, TestDescription td2) { 391 return td1.getRootRelativeURL().compareTo(td2.getRootRelativeURL()); 392 } 393 }); 394 395 Vector<TestTree.Node> v = new Vector<>(); 396 for (int i = 0; i < files.length; i++) { 397 TestTree.Node n = read0(finder, files[i], testTree, allFiles); 398 if (n != null) 399 v.addElement(n); 400 } 401 TestTree.Node[] nodes = new TestTree.Node[v.size()]; 402 v.copyInto(nodes); 403 404 return testTree.new Node(file.getName(), tests, nodes); 405 } 406 407 //------------------------------------------------------------------------------------------ 408 409 /** 410 * Write an int to a data output stream using a variable length encoding. 411 * The int is broken into groups of seven bits, and these are written out 412 * in big-endian order. Leading zeroes are suppressed and all but the last 413 * byte have the top bit set. 414 * @see BinaryTestFinder#readInt 415 */ 416 private static void writeInt(DataOutputStream out, int v) throws IOException { 417 if (v < 0) 418 throw new IllegalArgumentException(); 419 420 boolean leadZero = true; 421 for (int i = 28; i > 0; i -= 7) { 422 int b = (v >> i) & 0x7f; 423 leadZero = leadZero && (b == 0); 424 if (!leadZero) 425 out.writeByte(0x80 | b); 426 } 427 out.writeByte(v & 0x7f); 428 } 429 430 //------------------------------------------------------------------------------------------ 431 432 private static final TestDescription[] noTests = { }; 433 private PrintStream log = System.out; 434 private boolean strictFinder = false; 435 private int numFinderErrors = 0; 436 437 //------------------------------------------------------------------------------------------ 438 439 /** 440 * StringTable is an array of strings. Other parts of the encoding can 441 * choose to write strings as references (indexes) into the string table. 442 * Strings in the table are use-counted so that only frequently used 443 * strings are output. 444 * @see BinaryTestFinder.StringTable 445 */ 446 static class StringTable { 447 /** 448 * Add a new string to the table; if it has already been added, 449 * increase its use count. 450 */ 451 void add(String s) { 452 Entry e = map.get(s); 453 if (e == null) { 454 e = new Entry(); 455 map.put(s, e); 456 } 457 e.useCount++; 458 } 459 460 /** 461 * Add all the strings used in a test description to the table. 462 */ 463 void add(TestDescription test) { 464 for (Iterator<String> i = test.getParameterKeys(); i.hasNext(); ) { 465 String key = (i.next()); 466 String param = test.getParameter(key); 467 add(key); 468 add(param); 469 } 470 } 471 472 /** 473 * Return the number of strings in the table. 474 */ 475 int getSize() { 476 return map.size(); 477 } 478 479 /** 480 * Return the number of sstrings that were written to the output file. 481 * Not all strings are written out: only frequently used ones are. 482 */ 483 int getWrittenSize() { 484 return writtenSize; 485 } 486 487 /** 488 * Get the index of a string in the table. 489 */ 490 int getIndex(String s) { 491 Entry e = map.get(s); 492 if (e == null) 493 throw new IllegalArgumentException(); 494 return e.index; 495 } 496 497 /** 498 * Write the contents of the table to an entry called "strings" 499 * in a zip file. 500 */ 501 ZipEntry write(ZipOutputStream zos) throws IOException 502 { 503 ZipEntry entry = new ZipEntry("strings"); 504 zos.putNextEntry(entry); 505 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 506 write(dos); 507 dos.flush(); 508 zos.closeEntry(); 509 return entry; 510 } 511 512 /** 513 * Write the contents of the table to a stream 514 */ 515 void write(DataOutputStream o) throws IOException { 516 Vector<String> v = new Vector<>(map.size()); 517 v.addElement(""); 518 int nextIndex = 1; 519 for (Iterator<Map.Entry<String, Entry>> iter = map.entrySet().iterator(); iter.hasNext(); ) { 520 Map.Entry<String, Entry> e = iter.next(); 521 String key = e.getKey(); 522 Entry entry = e.getValue(); 523 if (entry.isFrequent()) { 524 entry.index = nextIndex++; 525 v.addElement(key); 526 } 527 } 528 529 writeInt(o, v.size()); 530 for (int i = 0; i < v.size(); i++) 531 o.writeUTF(v.elementAt(i)); 532 533 writtenSize = nextIndex; 534 } 535 536 /** 537 * Write a reference to a string to a stream. The string must have 538 * previously been added into nthe string table, and the string table 539 * written out. 540 * If the string is a frequent one, a pointer to its position in the 541 * previously written stream will be generated. If it is not a frequent 542 * string, zero will be written, followed by the value of the string itself. 543 */ 544 void writeRef(String s, DataOutputStream o) throws IOException { 545 Entry e = map.get(s); 546 if (e == null) 547 throw new IllegalArgumentException(); 548 549 if (e.isFrequent()) 550 writeInt(o, e.index); 551 else { 552 writeInt(o, 0); 553 o.writeUTF(s); 554 } 555 } 556 557 private Map<String, Entry> map = new TreeMap<>(); 558 private int writtenSize; 559 560 /** 561 * Data for each string in the string table. 562 */ 563 static class Entry { 564 /** 565 * How many times the string has been added to the string table. 566 */ 567 int useCount = 0; 568 569 /** 570 * The position of the string in the table when the table 571 * was written. 572 */ 573 int index = 0; 574 575 /** 576 * Determine if the string is frequent enough in the table to 577 * be written out. 578 */ 579 boolean isFrequent() { 580 return (useCount > 1); 581 } 582 } 583 } 584 585 //------------------------------------------------------------------------------------------ 586 587 /** 588 * TestTable is a table of test descriptions, whose written form is 589 * based on references into a string table. 590 * @see BinaryTestFinder.TestTable 591 */ 592 static class TestTable 593 { 594 /** 595 * Create a new TestTable. 596 */ 597 TestTable(StringTable stringTable) { 598 this.stringTable = stringTable; 599 } 600 601 /** 602 * Add a test description to the test table. The strings used by the 603 * test description are automatically added to the testTable's stringTable. 604 */ 605 void add(TestDescription td) { 606 tests.addElement(td); 607 testMap.put(td, new Entry()); 608 stringTable.add(td); 609 } 610 611 /** 612 * Get the number of tests in this test table. 613 */ 614 int getSize() { 615 return tests.size(); 616 } 617 618 /** 619 * Get the index for a test description, based on its position when the 620 * test table was written out. This index is the byte offset in the 621 * written stream. 622 */ 623 int getIndex(TestDescription td) { 624 Entry e = testMap.get(td); 625 if (e == null) 626 throw new IllegalArgumentException(); 627 return e.index; 628 } 629 630 /** 631 * Write the contents of the table to an entry called "tests" 632 * in a zip file. 633 */ 634 ZipEntry write(ZipOutputStream zos) throws IOException 635 { 636 ZipEntry entry = new ZipEntry("tests"); 637 zos.putNextEntry(entry); 638 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 639 write(dos); 640 dos.flush(); 641 zos.closeEntry(); 642 return entry; 643 } 644 645 /** 646 * Write the contents of the table to a stream. The position of each test 647 * description in the stream is recorded, so that a random acess stream 648 * can randomly access the individual test descriptions. The table is 649 * written as a count, followed by that many encoded test descriptions. 650 * Each test description is written as a count followed by that many 651 * name-value pairs of string references. 652 */ 653 void write(DataOutputStream o) throws IOException { 654 writeInt(o, tests.size()); 655 for (int i = 0; i < tests.size(); i++) { 656 TestDescription td = tests.elementAt(i); 657 Entry e = testMap.get(td); 658 e.index = o.size(); 659 write(td, o); 660 } 661 } 662 663 /** 664 * Write a single test description to a stream. It is written as a count, 665 * followed by that many name-value pairs of string references. 666 */ 667 private void write(TestDescription td, DataOutputStream o) throws IOException { 668 // should consider using load/save here 669 writeInt(o, td.getParameterCount()); 670 for (Iterator<String> i = td.getParameterKeys(); i.hasNext(); ) { 671 String key = (i.next()); 672 String value = td.getParameter(key); 673 stringTable.writeRef(key, o); 674 stringTable.writeRef(value, o); 675 } 676 } 677 678 private Map<TestDescription, Entry> testMap = new HashMap<>(); 679 private Vector<TestDescription> tests = new Vector<>(); 680 private StringTable stringTable; 681 682 /** 683 * Data for each test description in the table. 684 */ 685 class Entry { 686 /** 687 * The byte offset of the test description in the stream when 688 * last written out. 689 */ 690 int index = -1; 691 } 692 } 693 694 //------------------------------------------------------------------------------------------ 695 696 /** 697 * TestTree is a tree of tests, whose written form is based on 698 * references into a TestTable. There is a very strong correspondence 699 * between a node and the results of reading a file from a test finder, 700 * which yields a set of test descriptions and a set of additional files 701 * to be read. 702 * @see BinaryTestFinder.TestTable 703 */ 704 static class TestTree 705 { 706 /** 707 * Create an test tree. The root node of the tree should be set later. 708 */ 709 TestTree(TestTable testTable) { 710 this.testTable = testTable; 711 } 712 713 /** 714 * Set the root node of the tree. 715 */ 716 void setRoot(Node root) { 717 this.root = root; 718 } 719 720 /** 721 * Get the number of nodes in this tree. 722 */ 723 int getSize() { 724 return (root == null ? 0 : root.getSize()); 725 } 726 727 /** 728 * Write the contents of the tree to an entry called "tree" 729 * in a zip file. 730 */ 731 ZipEntry write(ZipOutputStream zos) throws IOException 732 { 733 ZipEntry entry = new ZipEntry("tree"); 734 zos.putNextEntry(entry); 735 DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos)); 736 write(dos); 737 dos.flush(); 738 zos.closeEntry(); 739 return entry; 740 } 741 742 /** 743 * Write the contents of the tree to a stream. Each node of the tree 744 * is written as 3 parts: 745 * <ul> 746 * <li>the name of the node 747 * <li>the number of test descriptions in this node, followed by that 748 * many references into the test table. 749 * <li>the number of child nodes, followed by that many nodes, written 750 * recursively. 751 * </ul> 752 */ 753 void write(DataOutputStream o) throws IOException { 754 root.write(o); 755 } 756 757 private Node root; 758 private TestTable testTable; 759 760 /** 761 * A node within the test tree. Each node has a name, a set of test 762 * descriptions, and a set of child nodes. 763 */ 764 class Node 765 { 766 /** 767 * Create a node. The individual test descriptions are added to 768 * the tree's test table. 769 */ 770 Node(String name, TestDescription[] tests, Node[] children) { 771 this.name = name; 772 this.tests = tests; 773 this.children = children; 774 775 for (int i = 0; i < tests.length; i++) 776 testTable.add(tests[i]); 777 } 778 779 /** 780 * Get the number of nodes at this point in the tree: count one 781 * for this node and add the size of all its children. 782 */ 783 int getSize() { 784 int n = 1; 785 if (children != null) { 786 for (int i = 0; i < children.length; i++) 787 n += children[i].getSize(); 788 } 789 return n; 790 } 791 792 /** 793 * Merge the contents of this node with another to produce 794 * a new node. 795 * @param other The node to be merged with this one. 796 * @return a new Node, containing the merge of this one 797 * and the specified node. 798 */ 799 Node merge(Node other) { 800 if (!other.name.equals(name)) 801 throw new IllegalArgumentException(name + ":" + other.name); 802 803 TreeMap<String, Node> mergedChildrenMap = new TreeMap<>(); 804 for (int i = 0; i < children.length; i++) { 805 Node child = children[i]; 806 mergedChildrenMap.put(child.name, child); 807 } 808 for (int i = 0; i < other.children.length; i++) { 809 Node otherChild = other.children[i]; 810 Node c = mergedChildrenMap.get(otherChild.name); 811 mergedChildrenMap.put(otherChild.name, 812 (c == null ? otherChild : otherChild.merge(c))); 813 } 814 Node[] mergedChildren = 815 mergedChildrenMap.values().toArray(new Node[mergedChildrenMap.size()]); 816 817 TestDescription[] mergedTests; 818 if (tests.length + other.tests.length == 0) 819 mergedTests = noTests; 820 else { 821 mergedTests = new TestDescription[tests.length + other.tests.length]; 822 System.arraycopy(tests, 0, mergedTests, 0, tests.length); 823 System.arraycopy(other.tests, 0, mergedTests, tests.length, other.tests.length); 824 } 825 826 return new Node(name, mergedTests, mergedChildren); 827 } 828 829 /** 830 * Write the contents of a node to a stream. First the name 831 * is written, then the number of test descriptions, followed 832 * by that many references to the test table, then the number 833 * of child nodes, followed by that many child nodes in place. 834 */ 835 void write(DataOutputStream o) throws IOException { 836 o.writeUTF(name); 837 writeInt(o, tests.length); 838 for (int i = 0; i < tests.length; i++) 839 writeInt(o, testTable.getIndex(tests[i])); 840 writeInt(o, children.length); 841 for (int i = 0; i < children.length; i++) 842 children[i].write(o); 843 } 844 845 private String name; 846 private TestDescription[] tests; 847 private Node[] children; 848 } 849 } 850 851 }