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 }