/*
* $Id$
*
* Copyright (c) 2001, 2011, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.javatest.finder;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import com.sun.javatest.TestDescription;
import com.sun.javatest.TestFinder;
/**
* BinaryTestWriter creates the data file used by BinaryTestFinder.
* It uses a test finder to find all the tests in a test suite and writes
* them out in a compact compressed form. By default it uses the standard
* tag test finder, and writes the output in a file called
* testsuite.jtd in the root directory of the test suite.
*
* Options:
*
* - -finder finderClass finderArgs ... -end
*
- the test finder to be used to locate the tests; the default is the standard tag test finder
*
- -strictFinder
*
- Do not ignore errors from the source finder, exit with error code instead
*
- -o output-file
*
- specify the name of the output file; the default is testsuite.jtd in the root directory of the test suite.
*
- testsuite
*
- (Required.) The test suite root file.
*
- initial-files
*
- (Optional)Any initial starting points within the test suite: the default is the test suite root
*
*/
public class BinaryTestWriter
{
/**
* This exception is used to report bad command line arguments.
*/
public class BadArgs extends Exception {
/**
* Create a BadArgs exception.
* @param msg A detail message about an error that has been found.
*/
BadArgs(String msg) {
super(msg);
}
}
/**
* This exception is used to report problems that occur while running.
*/
public class Fault extends Exception {
/**
* Create a Fault exception.
* @param msg A detail message about a fault that has occurred.
*/
Fault(String msg) {
super(msg);
}
}
//------------------------------------------------------------------------------------------
/**
* Standard program entry point.
* @param args An array of strings, typically provided via the command line.
* The arguments should be of the form:
* [options] testsuite [tests]
* Options |
* -finder finderClass finderArgs ... -end
* | The name of a test finder class and any arguments it might take.
* The results of reading this test finder will be stored in the
* output file.
* |
-o output-file
* | The output file in which to write the results.
* |
*/
public static void main(String[] args) {
int result = 0;
try {
BinaryTestWriter m = new BinaryTestWriter();
result = m.run(args);
}
catch (BadArgs e) {
System.err.println("Bad Arguments: " + e.getMessage());
usage(System.err);
System.exit(1);
}
catch (Fault f) {
System.err.println("Error: " + f.getMessage());
System.exit(2);
}
catch (IOException e) {
System.err.println("Error: " + e);
System.exit(3);
}
System.exit(result);
}
/**
* Print out command-line help.
*/
private static void usage(PrintStream out) {
String prog = System.getProperty("program", "java " + BinaryTestWriter.class.getName());
out.println("Usage:");
out.println(" " + prog + " [options] test-suite [tests...]");
out.println("Options:");
out.println(" -finder finderClass finderArgs... -end");
out.println(" -o output-file");
out.println(" -strictFinder");
}
//------------------------------------------------------------------------------------------
/**
* Main work method.
* Reads all the arguments on the command line, makes sure a valid
* testFinder is available, and then calls methods to create the tree of tests
* and then write the binary file.
* @param args An array of strings, typically provided via the command line
* @return The disposition of the run, i.e. zero for a problem-free execution, non-zero
* if there was some sort of problem.
* @throws BinaryTestWriter.BadArgs
* if a problem is found in the arguments provided
* @throws BinaryTestWriter.Fault
* if a fault is found while running
* @throws IOException
* if a problem is found while trying to read a file
* or write the output file
* @see #main
*/
public int run(String[] args) throws BadArgs, Fault, IOException {
File testSuite = null;
String finder = "com.sun.javatest.finder.TagTestFinder";
String[] finderArgs = { };
File outFile = null;
File[] tests = null;
for (int i = 0; i < args.length; i++) {
if (args[i].equalsIgnoreCase("-finder") && (i + 1 < args.length)) {
finder = args[++i];
int j = ++i;
while ((i < args.length - 1) && !(args[i].equalsIgnoreCase("-end")))
++i;
finderArgs = new String[i - j];
System.arraycopy(args, j, finderArgs, 0, finderArgs.length);
}
else if (args[i].equalsIgnoreCase("-o") && (i + 1 < args.length)) {
outFile = new File(args[++i]);
}
else if (args[i].equalsIgnoreCase("-strictFinder")) {
strictFinder = true;
}
else if (args[i].startsWith("-") ) {
throw new BadArgs(args[i]);
}
else {
testSuite = new File(args[i++]);
if (i < args.length) {
tests = new File[args.length - i];
for (int j = 0; j < tests.length; j++)
tests[j] = new File(args[i + j]);
}
break;
}
}
if (testSuite == null)
throw new BadArgs("testsuite.html file not specified");
TestFinder testFinder = initializeTestFinder(finder, finderArgs, testSuite);
if (tests == null)
tests = new File[] { testFinder.getRoot() }; // equals testSuite, adjusted by finder as necessary .. e.g. for dirWalk, webWalk etc
if (outFile == null)
outFile = new File(testFinder.getRootDir(), "testsuite.jtd");
if (strictFinder) {
testFinder.setErrorHandler(new TestFinder.ErrorHandler() {
public void error(String msg) {
numFinderErrors++;
System.err.println("Finder reported error:\n" + msg);
System.err.println("");
}
}
);
}
StringTable stringTable = new StringTable();
TestTable testTable = new TestTable(stringTable);
TestTree testTree = new TestTree(testTable);
if (log != null)
log.println("Reading tests...");
// read the tests into internal data structures
read(testFinder, tests, testTree);
if (testTree.getSize() == 0)
throw new Fault("No tests found -- check arguments.");
// write out the data structure into a zip file
if (log != null)
log.println("Writing " + outFile);
try (FileOutputStream fos = new FileOutputStream(outFile);
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fos))) {
zos.setMethod(ZipOutputStream.DEFLATED);
zos.setLevel(9);
ZipEntry stringZipEntry = stringTable.write(zos);
ZipEntry testTableZipEntry = testTable.write(zos);
ZipEntry testTreeZipEntry = testTree.write(zos);
// report statistics
if (log != null) {
log.println("strings: " + stringTable.getSize() + " entries, " + zipStats(stringZipEntry));
log.println("tests: " + testTable.getSize() + " tests, " + zipStats(testTableZipEntry));
log.println("tree: " + testTree.getSize() + " nodes, " + zipStats(testTreeZipEntry));
}
if (strictFinder && numFinderErrors > 0) {
System.err.println("*** Source finder reported " + numFinderErrors + " errors during execution. ***");
return 4;
}
else {
return 0;
}
}
}
/**
* Creates and initializes an instance of a test finder
*
* @param finder The class name of the required test finder
* @param args any args to pass to the TestFinder's init method.
* @param ts The testsuite root file
* @return The newly created TestFinder.
*/
private TestFinder initializeTestFinder(String finder, String[] args, File ts) throws Fault {
TestFinder testFinder;
if (ts == null)
throw new NullPointerException();
try {
Class> c = Class.forName(finder);
testFinder = (TestFinder) (c.newInstance());
testFinder.init(args, ts, null);
}
catch (ClassNotFoundException e) {
throw new Fault("Error: Can't find class for test finder specified: " + finder);
}
catch (InstantiationException e) {
throw new Fault("Error: Can't create new instance of test finder: " + e);
}
catch (IllegalAccessException e) {
throw new Fault("Error: Can't access test finder: " + e);
}
catch (TestFinder.Fault e) {
throw new Fault("Error: Can't initialize test-finder: " + e.getMessage());
}
return testFinder;
}
/**
* Gets and returns the test suite file. Adds testsuite.html or
* tests/testsuite.html to the end of the path if necessary.
*/
private File getTestSuiteFile(String file) throws Fault {
File tsa = new File(file);
if (tsa.isFile())
return tsa;
else {
File tsb = new File(tsa, "testsuite.html");
if (tsb.exists())
return tsb;
else {
File tsc = new File(tsa, "tests/testsuite.html");
if (tsc.exists())
return tsc;
else
throw new Fault("Bad input. " + file + " is not a JCK");
}
}
}
/**
* Create a string containing statistics about a zip file entry.
*/
private String zipStats(ZipEntry e) {
long size = e.getSize();
long csize = e.getCompressedSize();
return size + " bytes (" + csize + " compressed, " + (csize * 100 / size) + "%)";
}
//------------------------------------------------------------------------------------------
/**
* Read all the tests from a test suite and store them in a test tree
*/
void read(TestFinder finder, File[] files, TestTree testTree) throws Fault
{
if (files.length < 1)
throw new IllegalArgumentException();
File rootDir = finder.getRootDir();
Set allFiles = new HashSet<>();
TestTree.Node r = null;
for (int i = 0; i < files.length; i++) {
File f = files[i];
if (!f.isAbsolute())
f = new File(rootDir, f.getPath());
TestTree.Node n = read0(finder, f, testTree, allFiles);
if (n == null)
continue;
while (!f.equals(rootDir)) {
f = f.getParentFile();
n = testTree.new Node(f.getName(), noTests, new TestTree.Node[] { n });
}
r = (r == null ? n : r.merge(n));
}
if (r == null)
throw new Fault("No tests found");
testTree.setRoot(r);
}
/**
* Read the tests from a file in test suite
*/
private TestTree.Node read0(TestFinder finder, File file, TestTree testTree, Set allFiles)
{
// keep track of which files we have read, and ignore duplicates
if (allFiles.contains(file))
return null;
else
allFiles.add(file);
finder.read(file);
TestDescription[] tests = finder.getTests();
File[] files = finder.getFiles();
if (tests.length == 0 && files.length == 0)
return null;
Arrays.sort(files);
Arrays.sort(tests, new Comparator() {
public int compare(TestDescription td1, TestDescription td2) {
return td1.getRootRelativeURL().compareTo(td2.getRootRelativeURL());
}
});
Vector v = new Vector<>();
for (int i = 0; i < files.length; i++) {
TestTree.Node n = read0(finder, files[i], testTree, allFiles);
if (n != null)
v.addElement(n);
}
TestTree.Node[] nodes = new TestTree.Node[v.size()];
v.copyInto(nodes);
return testTree.new Node(file.getName(), tests, nodes);
}
//------------------------------------------------------------------------------------------
/**
* Write an int to a data output stream using a variable length encoding.
* The int is broken into groups of seven bits, and these are written out
* in big-endian order. Leading zeroes are suppressed and all but the last
* byte have the top bit set.
* @see BinaryTestFinder#readInt
*/
private static void writeInt(DataOutputStream out, int v) throws IOException {
if (v < 0)
throw new IllegalArgumentException();
boolean leadZero = true;
for (int i = 28; i > 0; i -= 7) {
int b = (v >> i) & 0x7f;
leadZero = leadZero && (b == 0);
if (!leadZero)
out.writeByte(0x80 | b);
}
out.writeByte(v & 0x7f);
}
//------------------------------------------------------------------------------------------
private static final TestDescription[] noTests = { };
private PrintStream log = System.out;
private boolean strictFinder = false;
private int numFinderErrors = 0;
//------------------------------------------------------------------------------------------
/**
* StringTable is an array of strings. Other parts of the encoding can
* choose to write strings as references (indexes) into the string table.
* Strings in the table are use-counted so that only frequently used
* strings are output.
* @see BinaryTestFinder.StringTable
*/
static class StringTable {
/**
* Add a new string to the table; if it has already been added,
* increase its use count.
*/
void add(String s) {
Entry e = map.get(s);
if (e == null) {
e = new Entry();
map.put(s, e);
}
e.useCount++;
}
/**
* Add all the strings used in a test description to the table.
*/
void add(TestDescription test) {
for (Iterator i = test.getParameterKeys(); i.hasNext(); ) {
String key = (i.next());
String param = test.getParameter(key);
add(key);
add(param);
}
}
/**
* Return the number of strings in the table.
*/
int getSize() {
return map.size();
}
/**
* Return the number of sstrings that were written to the output file.
* Not all strings are written out: only frequently used ones are.
*/
int getWrittenSize() {
return writtenSize;
}
/**
* Get the index of a string in the table.
*/
int getIndex(String s) {
Entry e = map.get(s);
if (e == null)
throw new IllegalArgumentException();
return e.index;
}
/**
* Write the contents of the table to an entry called "strings"
* in a zip file.
*/
ZipEntry write(ZipOutputStream zos) throws IOException
{
ZipEntry entry = new ZipEntry("strings");
zos.putNextEntry(entry);
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos));
write(dos);
dos.flush();
zos.closeEntry();
return entry;
}
/**
* Write the contents of the table to a stream
*/
void write(DataOutputStream o) throws IOException {
Vector v = new Vector<>(map.size());
v.addElement("");
int nextIndex = 1;
for (Iterator> iter = map.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry e = iter.next();
String key = e.getKey();
Entry entry = e.getValue();
if (entry.isFrequent()) {
entry.index = nextIndex++;
v.addElement(key);
}
}
writeInt(o, v.size());
for (int i = 0; i < v.size(); i++)
o.writeUTF(v.elementAt(i));
writtenSize = nextIndex;
}
/**
* Write a reference to a string to a stream. The string must have
* previously been added into nthe string table, and the string table
* written out.
* If the string is a frequent one, a pointer to its position in the
* previously written stream will be generated. If it is not a frequent
* string, zero will be written, followed by the value of the string itself.
*/
void writeRef(String s, DataOutputStream o) throws IOException {
Entry e = map.get(s);
if (e == null)
throw new IllegalArgumentException();
if (e.isFrequent())
writeInt(o, e.index);
else {
writeInt(o, 0);
o.writeUTF(s);
}
}
private Map map = new TreeMap<>();
private int writtenSize;
/**
* Data for each string in the string table.
*/
static class Entry {
/**
* How many times the string has been added to the string table.
*/
int useCount = 0;
/**
* The position of the string in the table when the table
* was written.
*/
int index = 0;
/**
* Determine if the string is frequent enough in the table to
* be written out.
*/
boolean isFrequent() {
return (useCount > 1);
}
}
}
//------------------------------------------------------------------------------------------
/**
* TestTable is a table of test descriptions, whose written form is
* based on references into a string table.
* @see BinaryTestFinder.TestTable
*/
static class TestTable
{
/**
* Create a new TestTable.
*/
TestTable(StringTable stringTable) {
this.stringTable = stringTable;
}
/**
* Add a test description to the test table. The strings used by the
* test description are automatically added to the testTable's stringTable.
*/
void add(TestDescription td) {
tests.addElement(td);
testMap.put(td, new Entry());
stringTable.add(td);
}
/**
* Get the number of tests in this test table.
*/
int getSize() {
return tests.size();
}
/**
* Get the index for a test description, based on its position when the
* test table was written out. This index is the byte offset in the
* written stream.
*/
int getIndex(TestDescription td) {
Entry e = testMap.get(td);
if (e == null)
throw new IllegalArgumentException();
return e.index;
}
/**
* Write the contents of the table to an entry called "tests"
* in a zip file.
*/
ZipEntry write(ZipOutputStream zos) throws IOException
{
ZipEntry entry = new ZipEntry("tests");
zos.putNextEntry(entry);
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos));
write(dos);
dos.flush();
zos.closeEntry();
return entry;
}
/**
* Write the contents of the table to a stream. The position of each test
* description in the stream is recorded, so that a random acess stream
* can randomly access the individual test descriptions. The table is
* written as a count, followed by that many encoded test descriptions.
* Each test description is written as a count followed by that many
* name-value pairs of string references.
*/
void write(DataOutputStream o) throws IOException {
writeInt(o, tests.size());
for (int i = 0; i < tests.size(); i++) {
TestDescription td = tests.elementAt(i);
Entry e = testMap.get(td);
e.index = o.size();
write(td, o);
}
}
/**
* Write a single test description to a stream. It is written as a count,
* followed by that many name-value pairs of string references.
*/
private void write(TestDescription td, DataOutputStream o) throws IOException {
// should consider using load/save here
writeInt(o, td.getParameterCount());
for (Iterator i = td.getParameterKeys(); i.hasNext(); ) {
String key = (i.next());
String value = td.getParameter(key);
stringTable.writeRef(key, o);
stringTable.writeRef(value, o);
}
}
private Map testMap = new HashMap<>();
private Vector tests = new Vector<>();
private StringTable stringTable;
/**
* Data for each test description in the table.
*/
class Entry {
/**
* The byte offset of the test description in the stream when
* last written out.
*/
int index = -1;
}
}
//------------------------------------------------------------------------------------------
/**
* TestTree is a tree of tests, whose written form is based on
* references into a TestTable. There is a very strong correspondence
* between a node and the results of reading a file from a test finder,
* which yields a set of test descriptions and a set of additional files
* to be read.
* @see BinaryTestFinder.TestTable
*/
static class TestTree
{
/**
* Create an test tree. The root node of the tree should be set later.
*/
TestTree(TestTable testTable) {
this.testTable = testTable;
}
/**
* Set the root node of the tree.
*/
void setRoot(Node root) {
this.root = root;
}
/**
* Get the number of nodes in this tree.
*/
int getSize() {
return (root == null ? 0 : root.getSize());
}
/**
* Write the contents of the tree to an entry called "tree"
* in a zip file.
*/
ZipEntry write(ZipOutputStream zos) throws IOException
{
ZipEntry entry = new ZipEntry("tree");
zos.putNextEntry(entry);
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(zos));
write(dos);
dos.flush();
zos.closeEntry();
return entry;
}
/**
* Write the contents of the tree to a stream. Each node of the tree
* is written as 3 parts:
*
* - the name of the node
*
- the number of test descriptions in this node, followed by that
* many references into the test table.
*
- the number of child nodes, followed by that many nodes, written
* recursively.
*
*/
void write(DataOutputStream o) throws IOException {
root.write(o);
}
private Node root;
private TestTable testTable;
/**
* A node within the test tree. Each node has a name, a set of test
* descriptions, and a set of child nodes.
*/
class Node
{
/**
* Create a node. The individual test descriptions are added to
* the tree's test table.
*/
Node(String name, TestDescription[] tests, Node[] children) {
this.name = name;
this.tests = tests;
this.children = children;
for (int i = 0; i < tests.length; i++)
testTable.add(tests[i]);
}
/**
* Get the number of nodes at this point in the tree: count one
* for this node and add the size of all its children.
*/
int getSize() {
int n = 1;
if (children != null) {
for (int i = 0; i < children.length; i++)
n += children[i].getSize();
}
return n;
}
/**
* Merge the contents of this node with another to produce
* a new node.
* @param other The node to be merged with this one.
* @return a new Node, containing the merge of this one
* and the specified node.
*/
Node merge(Node other) {
if (!other.name.equals(name))
throw new IllegalArgumentException(name + ":" + other.name);
TreeMap mergedChildrenMap = new TreeMap<>();
for (int i = 0; i < children.length; i++) {
Node child = children[i];
mergedChildrenMap.put(child.name, child);
}
for (int i = 0; i < other.children.length; i++) {
Node otherChild = other.children[i];
Node c = mergedChildrenMap.get(otherChild.name);
mergedChildrenMap.put(otherChild.name,
(c == null ? otherChild : otherChild.merge(c)));
}
Node[] mergedChildren =
mergedChildrenMap.values().toArray(new Node[mergedChildrenMap.size()]);
TestDescription[] mergedTests;
if (tests.length + other.tests.length == 0)
mergedTests = noTests;
else {
mergedTests = new TestDescription[tests.length + other.tests.length];
System.arraycopy(tests, 0, mergedTests, 0, tests.length);
System.arraycopy(other.tests, 0, mergedTests, tests.length, other.tests.length);
}
return new Node(name, mergedTests, mergedChildren);
}
/**
* Write the contents of a node to a stream. First the name
* is written, then the number of test descriptions, followed
* by that many references to the test table, then the number
* of child nodes, followed by that many child nodes in place.
*/
void write(DataOutputStream o) throws IOException {
o.writeUTF(name);
writeInt(o, tests.length);
for (int i = 0; i < tests.length; i++)
writeInt(o, testTable.getIndex(tests[i]));
writeInt(o, children.length);
for (int i = 0; i < children.length; i++)
children[i].write(o);
}
private String name;
private TestDescription[] tests;
private Node[] children;
}
}
}