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 }