You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@river.apache.org by pe...@apache.org on 2014/10/26 14:17:31 UTC
svn commit: r1634322 [3/41] - in /river/jtsk/skunk/qa_refactor/trunk:
qa/src/com/sun/jini/qa/harness/
qa/src/com/sun/jini/test/impl/end2end/e2etest/
qa/src/com/sun/jini/test/impl/joinmanager/
qa/src/com/sun/jini/test/impl/mahalo/ qa/src/com/sun/jini/te...
Modified: river/jtsk/skunk/qa_refactor/trunk/qa/src/com/sun/jini/qa/harness/MasterHarness.java
URL: http://svn.apache.org/viewvc/river/jtsk/skunk/qa_refactor/trunk/qa/src/com/sun/jini/qa/harness/MasterHarness.java?rev=1634322&r1=1634321&r2=1634322&view=diff
==============================================================================
--- river/jtsk/skunk/qa_refactor/trunk/qa/src/com/sun/jini/qa/harness/MasterHarness.java (original)
+++ river/jtsk/skunk/qa_refactor/trunk/qa/src/com/sun/jini/qa/harness/MasterHarness.java Sun Oct 26 13:17:28 2014
@@ -1,1426 +1,1438 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.sun.jini.qa.harness;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.IOException;
-import java.io.ObjectOutputStream;
-import java.io.OutputStream;
-import java.io.PrintStream;
-import java.io.FileOutputStream;
-import java.io.FileNotFoundException;
-import java.net.ServerSocket;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.MalformedURLException;
-import java.util.ArrayList;
-import java.util.Date;
-import java.text.MessageFormat;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.MissingResourceException;
-import java.util.Properties;
-import java.util.ResourceBundle;
-import java.util.StringTokenizer;
-import java.util.TreeMap;
-import java.util.TreeSet;
-import java.util.jar.JarFile;
-import java.util.zip.ZipEntry;
-import java.lang.reflect.Field;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.SocketAddress;
-
-//Should there be an 'AbortTestRequest' ?
-
-/**
- * An implementation of the master component of a distributed test harness.
- * Provides the command line interface for starting a test run, execs and
- * manages the VM which contains the master test, and coordinates the
- * activities of all participating slave harness.
- * <p>
- * Command line options accessed by this class:
- * <ul>
- * <li><code>-tests testlist</code> where <code>testlist</code> is a
- * comma-separated list of test descriptor names to run. If omitted, all
- * tests matching other filters (such as categories) are run.
- *
- * <li> <code>-xtests testlist</code> where <code>testlist</code> is the
- * comma separated list of tests to exclude from testing
- *
- * <li> <code>-xcategories catlist</code> where <code>catlist</code> is
- * the comma set of categories to exclude from testing. Any test
- * which is a member of any category in this list is excluded from
- * testing.
- * <li> <code>-env envfile</code> where <code>envfile</code> is the name
- * of a properties file containing environment definitions for
- * running tests (i.e. which jdk/jsk versions to use when running tests).
- * If the file is specified as a relative path, it is resolved relative
- * to the current working directory.
- * <li> <code>-categories categorylist</code> is the comma-separated
- * set of categories to test
- * <li> <code>-help</code> prints a usage message
- * </ul>
- * <p>
- * Property values accessed by this class:
- * <ul>
- * <li><code>com.sun.jini.qa.harness.verifier</code> is the name of the
- * configuration verifier class to load. Before a test is run,
- * this class is loaded (if defined) and its <code>canRun</code>
- * method called to determine whether to run the test.
- * <li><code>com.sun.jini.qa.harness.runCommandAfterEachTest</code> is a
- * platform dependent command line to be executed at the conclusion of
- * each test.
- * </ul>
- * <p>
- * Note that there is no actual distinction between command-line options
- * and property values. That is, a property file could contain the entry
- * 'xtests=foo,bar' to exclude tests foo and bar. Likewise, the command line
- * could include the option "-com.sun.jini.qa.harness.runCommandAfterEachTest ls"
- * which would cause the Unix 'ls' command to be run after each test.
- * <p>
- * Tests are always run in a separate VM spawned by this class. The
- * <code>QAConfig</code> object is serialized and written to the
- * test VM's input stream.
- * <p>
- * Test status information must be passed back from the test VM
- * to the harness VM. To do this, the wrapper writes a specially formatted
- * string as the last line of output to <code>System.err</code>.
- * <code>MasterHarness</code> tracks the last line written to this stream,
- * and uses the information in it to update the test results summary tables.
- * Because the <code>message</code> portion of the status could be
- * a multi-line string, the wrapper converts any '\n' or '\r' characters
- * in the message to the tokens '<<n>>' or '<<r>>' respectively.
- * <code>MasterHarness</code> restores these line terminator characters
- * as part of reconstructing the test results.
- *</table> <p>
- */
-class MasterHarness {
-
- /**
- * token which identifies the beginning of a test result message being
- * sent from a test running in another VM
- */
- private static final String STATUS_TOKEN = "!STATUS!";
-
- /** the keep-alive port number */
- public final static int KEEPALIVE_PORT=10004;
-
- /** the output stream for this VM's output */
- private volatile PrintStream outStream = System.out;
-
- /** the pipe for collecting stderr output from the test vm */
- private Pipe errPipe;
-
- /** the pipe for collecting stdout output from the test vm */
- private Pipe outPipe;
-
- /** the max recursion depth when parsing test sets */
- private final static int MAXTESTCOUNT = 100;
-
- /** the command line arguments */
- private final String[] args;
-
- /** the list of test categories to run. If null, all categories are run. */
- private List categories;
-
- /** depth counter to detect infinite recursion in <code>addTest</code> */
- private int testTries = 0;
-
- /**
- * the object which forwards input to a test subprocess. This object
- * is reused for all test subprocesses.
- */
- private InputForwarder forwarder;
-
- /** the set of tests to run */
- private final TestList testList;
-
- /** the list of categories to exclude from testing */
- private List xCategories;
-
- /** the list of tests to exclude from testing */
- private List xTestNames;
-
- /** the configuration object for this test run */
- private final QAConfig config;
-
- /**
- * boolean controlling whether to read from stdin
- */
- boolean doInputBind;
-
- /** a reference to the keep-alive thread */
- private final Thread keepaliveThread;
-
- /** the category map, cached if non-null */
- private HashMap categoryMap = null;
-
- /** the test name to test description properties map */
- private HashMap testMap = null;
-
- /**
- * Construct the <code>MasterHarness</code>. Creates an instance
- * of <code>QAConfig</code>, starts the keep-alive thread, connects
- * to any participating slave harnesses, and builds the test list.
- *
- * @param args <code>String</code> array containing the command line
- * arguments
- */
- MasterHarness(String[] args) throws TestException {
- this.args = args;
- if (args.length == 0 || args[0].equals("-h") || args[0].equals("-help"))
- {
- usage();
- System.exit(1);
- }
- File f = new File(args[0]);
- // args[0] must be absolute, or it will be resolved against
- // the kit installation directory (which is undefined)
- if (! f.isAbsolute()) {
- f = f.getAbsoluteFile();
- args[0] = f.toString();
- }
- if (! f.exists()) {
- usage();
- System.exit(1);
- }
- config = new QAConfig(args);
- keepaliveThread = new Thread(new KeepAlivePort(), "Keepalive");
- keepaliveThread.setDaemon(false);
- keepaliveThread.start();
- try {
- Thread.sleep(1000);
- } catch (InterruptedException ignore) {
- }
- SlaveHarness.connect();
- // check for installed provider. Failure here should kill slaves.
- try {
- Class policyClass =
- Class.forName("com.sun.jini.qa.harness.MergedPolicyProvider");
- if (policyClass.getClassLoader().getParent() != null) {
- outStream.println("MergedPolicyprovider must be "
- + "installed in an extensions ClassLoader");
- System.exit(1);
- }
- } catch (Exception e) {
- outStream.println("failed to find MergedPolicyProvider");
- System.exit(1);
- }
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("CONFIGURATION FILE:");
- outStream.println("");
- outStream.println(" " + args[0]);
- testList = new TestList(config, System.currentTimeMillis());
- loadTestDescriptions();
- buildTestList();
- displayConfigInfo(); // seems like this should be in QAConfig
- }
-
- /**
- * A <code>Runnable</code> which opens the keep-alive server socket
- * and accepts connections for the life of this VM.
- */
- private class KeepAlivePort implements Runnable {
-
- public void run() {
- ServerSocket socket = null;
- ArrayList socketList = new ArrayList(); // keep references
- try {
- SocketAddress add = new InetSocketAddress(KEEPALIVE_PORT);
- socket = new ServerSocket();
-// if (!socket.getReuseAddress()) socket.setReuseAddress(true);
- socket.bind(add);
- while (true) {
- socketList.add(socket.accept());
- }
- } catch (Exception e) {
- outStream.println("Problem with KEEPALIVE_PORT:" + KEEPALIVE_PORT );
- outStream.println("Unexpected exception:");
- e.printStackTrace(outStream);
- } finally {
- try {
- socket.close();
- } catch (IOException ex) {/*Ignore*/}
- System.exit(1);
- }
- }
- }
-
- /**
- * Delete files which have been registered for deletion. Registered
- * files are deleted on the master host, and a request to delete
- * registered files is sent to all slave hosts.
- *
- * @throws TestException if an error occurs when communicating
- * with a slave
- */
- private void deleteRegisteredFiles() throws TestException {
- config.deleteRegisteredFiles();
- SlaveHarness.broadcastRequest(new DeleteRegisteredFilesRequest());
- }
-
- /**
- * Build the list of tests.
- */
- private void buildTestList() throws TestException {
- String testString = config.getStringConfigVal("tests", null);
- categories = getCategories();
- xTestNames = buildList(config.getStringConfigVal("xtests", null));
- addFromURL(xTestNames, config.getStringConfigVal("excludeList", null));
- xCategories = buildList(config.getStringConfigVal("xcategories", null));
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("SETTING UP THE TEST LIST:");
- outStream.println("");
- // If there are no arguments assume the user wants all the tests.
- if (testString == null) {
- addByCategory();
- } else {
- addTests(testString);
- }
- }
-
- /**
- * Attempt to add the test specified by <code>testName</code> to the current
- * set of tests. A test is obtained by calling the configurations
- * <code>getTestDescription</code> factory method. The test is added if:
- * <ul>
- * <li>the named test can be found and it's <code>TestDescription</code>
- * is valid
- * <li>the test is not in the excluded test list
- * <li>none of the test categories are in the excluded category list
- * <li>the test has a category which is included in the requested categories
- * (if any)
- * <li>the test passes test suite specific validity checks
- * <li>the test is not a duplicate
- * </ul>
- * @param testName the name of the test to add
- */
- private void addTest(String testName) throws TestException {
- String reason = null;
- TestDescription td = null;
- Properties p = (Properties) testMap.get(testName);
- // for td's not in the jar, or if -categories was not included
- if (!testName.endsWith(".td")) {
- testName += ".td";
- }
- if (p == null) {
- p = config.loadProperties(testName);
- }
- if (p == null) {
- throw new TestException("no properties for " + testName);
- }
- try {
- td = config.getTestDescription(testName, p);
- } catch (TestException e) {
- e.printStackTrace();
- reason = e.getMessage();
- }
-
- /* perform built-in checks in the test description */
- if (reason == null) {
- reason = td.checkValidity();
- }
-
- /* if ok, perform command line checks */
- if (reason == null) {
- reason = checkValidity(td);
- }
-
- /* complain and bail if anything went wrong */
- if (reason != null) {
- outStream.println(" Skipping test: " + testName);
- outStream.println(" Reason: " + reason);
- return;
- }
- outStream.println(" Adding test: " + testName);
- String[] configTags = config.getConfigTags();
- for (int i = 0; i < configTags.length; i++) {
- testList.add(new TestRun(td, configTags[i]));
- }
- }
-
- /**
- * Run all of the tests in the test list and displays their results.
- *
- * @return true if all started tests complete and pass; false if either
- * one or more of the started tests fail or does not complete
- */
- boolean runTests() throws TestException {
- deleteRegisteredFiles();
- doInputBind = config.getBooleanConfigVal(
- "com.sun.jini.qa.harness.bindInput", true);
- boolean genHtml = config.getBooleanConfigVal(
- "com.sun.jini.qa.harness.generateHtml", false);
- outStream.println("-----------------------------------------");
- outStream.println("STARTING TO RUN THE TESTS");
- outStream.println("");
- outStream.println("");
-
- // main test loop
- while (testList.hasMore()) {
- long testRunStartTime = System.currentTimeMillis();
- TestResult testResult = null;
- File logFile = null;
- TestRun testRun = testList.next();
- TestDescription td = testRun.td;
- String configTag = testRun.configTag;
- config.doConfigurationSetup(configTag, td);
- String verifierNames = config.getStringConfigVal(
- "com.sun.jini.qa.harness.verifier", null);
- if (!checkVerifiers(td, verifierNames)) {
- testResult = new TestResult(
- testRun, false, Test.SKIP,
- "verifiers are: " + verifierNames);
- testList.add(testRun, testResult);
- outStream.println(testResult.toString());
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("");
- continue;
- }
- if (genHtml) {
- logFile = getLogFile(td);
- setOutputStream(logFile);
- }
- String running = ((testRun.isRerun) ? "RE-RUNNING " : "Running ");
- outStream.println(running + td.getName());
- outStream.println("Time is " + new Date());
- TestRunner runner = new TestRunner(testRun);
- int interval = getTimeout(); // ask the subclass
- if (interval > 0) {
- Thread testThread = new Thread(runner, "TestRunner");
- Timeout.TimeoutHandler handler =
- new Timeout.ThreadTimeoutHandler(testThread);
- Timeout timeout = new Timeout(handler, interval);
- testThread.start();
- timeout.start();
- try {
- testThread.join();
- } catch (InterruptedException e) { //shouldn't happen
- outStream.println("testThread.join() interrupted..." +
- "should not happen");
- }
- if(testThread.interrupted()) {
- outStream.println("Test was interrupted");
- }
- if (timeout.timedOut()) {
- outStream.println("Timed out");
- SlaveTest.broadcast(new TeardownRequest());
- } else {
- timeout.cancel();
- }
- } else {
- runner.run();
- }
- deleteRegisteredFiles(); // do here in case test vm died
- SlaveTest.waitForSlaveDeath(60); // give them a minute
- testResult = runner.getTestResult();
- testResult.setLogFile(logFile);
- testResult.setElapsedTime(
- System.currentTimeMillis() - testRunStartTime);
- testList.add(testRun, testResult);
- outStream.println(testResult.toString());
- runCommandAfterEachTest();
- if (genHtml) {
- outStream.close(); // close log file
- setOutputStream(System.out); // return to System.out
- } else {
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("");
- }
- } // end main while loop
- outStream.println("SUMMARY =================================");
- outStream.println("");
- TestList.TestResultIterator iter = testList.createTestResultIterator();
- boolean removePassResults = config.getBooleanConfigVal(
- "com.sun.jini.qa.harness.generateHtml.removePassResults",false);
- while (iter.hasMore()) {
- TestResult[] results = iter.next();
- for (int i = 0; i < results.length; i++) {
- if (i == 0) {
- outStream.println(results[i].toString());
- } else {
- outStream.println("RE-RUN " + i);
- outStream.println(results[i].toString());
- }
- // remove passed test result is required
- if (removePassResults && results[i].state) {
- if (results[i].logFile != null &&
- ! results[i].logFile.delete())
- {
- outStream.println("Error: could not remove "
- + results[i].logFile);
- }
- // set logFile to null so that further report generation
- // won't try to create links to the log files
- results[i].logFile = null;
- }
- }
- outStream.println("-----------------------------------------");
- }
- outStream.println("");
- outStream.println("# of tests started = " +
- testList.getNumStarted());
- outStream.println("# of tests completed = "
- + testList.getNumCompleted());
- if (testList.getNumSkipped() > 0) {
- outStream.println("# of tests skipped = " +
- testList.getNumSkipped());
- }
- outStream.println("# of tests passed = " + testList.getNumPassed());
- outStream.println("# of tests failed = " + testList.getNumFailed());
- if (testList.getNumRerun() > 0) {
- outStream.println("# of tests rerun = " +
- testList.getNumRerun());
- }
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("");
- testList.setFinishTime(System.currentTimeMillis());
- outStream.println(" Date finished:");
- outStream.println(" "
- + (new Date(testList.getFinishTime())).toString());
- outStream.println(" Time elapsed:");
- outStream.println(" " +
- ((testList.getDurationTime())/1000) + " seconds");
- outStream.println("");
-
- if (genHtml) {
- try {
- HtmlReport htmlReport = new HtmlReport(config, testList);
- htmlReport.generate();
- } catch (IOException ioe) {
- outStream.println("Exception trying to generate html:");
- ioe.printStackTrace(outStream);
- }
- setOutputStream(System.out); // return to System.out
- }
-
- boolean genXml = config.getBooleanConfigVal(
- "com.sun.jini.qa.harness.generateXml", false);
- if (genXml) {
- try {
- XmlReport xmlReport = new XmlReport(config, testList);
- xmlReport.generate();
- } catch (Exception e) {
- outStream.println("Exception trying to generate xml:");
- e.printStackTrace(outStream);
- }
- }
- return (testList.getNumFailed() == 0 &&
- testList.getNumStarted() == testList.getNumCompleted());
- } //end runTests
-
- /**
- * Set the output stream used by the harness VM to
- * the given stream.
- */
- private void setOutputStream(PrintStream out) {
- if (outStream != null) {
- outStream.flush();
- }
- outStream = out;
- }
-
- /**
- * Set the output stream used by the harness VM to
- * a FileOutputStream writing to the given file.
- * If the FileOutputStream cannot be created,
- * the VM will exit.
- */
- private void setOutputStream(File logFile) {
- try {
- setOutputStream(new PrintStream(new FileOutputStream(logFile)));
- } catch (FileNotFoundException fnfe) {
- outStream.println("Cannot create " + logFile + ". Exiting...");
- System.exit(2);
- }
- }
-
- /**
- * Establish a file object given a test description. The
- * file name is the test name with '/' replaced with '_'.
- * The file path is the same file path as that given in the
- * <code>com.sun.jini.qa.harness.generateHtml.resultLog</code> property.
- */
- private File getLogFile(TestDescription td) {
- String resultLog = config.getStringConfigVal(
- "com.sun.jini.qa.harness.generateHtml.resultLog","index.html");
- File resultDir = (new File(resultLog)).getParentFile();
- if (resultDir != null && !resultDir.exists()) {
- resultDir.mkdirs();
- }
- String testName = td.getName().replace('/','_').replace('\\','_');
- File logFile = new File(resultDir, testName + ".txt");
- int counter = 1;
- while (logFile.exists()) {
- logFile = new File(resultDir, testName + "-" + counter + ".txt");
- counter++;
- }
- return logFile;
- }
-
- /**
- * A <code>Runnable</code> used to execute a test.
- */
- private class TestRunner implements Runnable {
-
- private TestRun testRun;
- private TestResult testResult;
-
- /**
- * Construct the <code>TestRunner</code>.
- *
- * @param testRun the test to run
- */
- public TestRunner(TestRun testRun) {
- this.testRun = testRun;
- }
-
- /**
- * Run the test in another VM.
- * The <code>TestResult</code> is saved for later retrieval.
- */
- public void run() {
- testResult = runTestOtherVM(testRun);
- }
-
- /**
- * Return the <code>TestResult</code> returned when the test
- * was executed by this <code>TestRunner</code>.
- *
- * @return the test result
- */
- public TestResult getTestResult() {
- return testResult;
- }
- }
-
- /**
- * Returns the categories that were input for the current test run either
- * on the command line, or in the configuration file.
- *
- * @return <code>String</code> array containing the categories that were
- * input for the current test run either on the command line, or
- * in the configuration file.
- */
- private String[] getRequestedCategories() {
- if (categories == null) {
- return null;
- }
- return ((String[])categories.toArray(new String[categories.size()]));
- }
-
- /**
- * Convert a string of comma-separated tokens to lower case and return
- * the tokens as a <code>List</code>.
- *
- * @param flatString the string of comma-separated tokens
- *
- * @return a <code>List</code> of tokens. If <code>flatString</code> is
- * <code>null</code> or contains no tokens, a zero length
- * <code>List</code> is returned.
- */
- private List buildList(String flatString) {
- ArrayList list = new ArrayList();
- if (flatString != null) {
- StringTokenizer st = new StringTokenizer(flatString, ",");
- while (st.hasMoreTokens()) {
- list.add(st.nextToken().toLowerCase());
- }
- }
- return list;
- }
-
- private void addFromURL(List list, String urlString) throws TestException {
- if (urlString == null) {
- return;
- }
- URL url = null;
- try {
- url = new URL(urlString);
- } catch (MalformedURLException e1) {
- try {
- url = new URL("file:" + urlString);
- } catch (MalformedURLException e2) {
- throw new TestException("neither " + urlString
- + " nor file:" + urlString
- + " are value URLs");
- }
- }
- try {
- URLConnection conn = url.openConnection();
- BufferedReader reader =
- new BufferedReader(new InputStreamReader(conn.getInputStream()));
- String line;
- while ((line = reader.readLine()) != null) {
- if (line.trim().length() == 0) {
- continue;
- }
- list.add(line.trim().toLowerCase());
- }
- } catch (IOException e) {
- throw new TestException("problem reading exclusion list at " + url, e);
- }
- }
-
- /**
- * Add all of the tests identified in <code>testSet</code> to the
- * set of tests to run. The list is a comma-separated set of
- * test names or test set names. A test set is preceded by the
- * prefix "set:". The config is searched for a match to the test
- * set name, and if a match is found, the value of the match is
- * included in the list of tests. This evaluation is performed recursively,
- * so that sets may refer to other sets. Tests are not added to the
- * run set unless they meet the criteria defined in <@link addTest>.
- *
- * @param testSet the comma-separated set of tests to run
- */
- private void addTests(String testSet) throws TestException {
- // to make sure we are not in some kind of property loop
- if (testTries > MAXTESTCOUNT) {
- return;
- }
- testTries++;
- StringTokenizer st = new StringTokenizer(testSet, ",");
- while (st.hasMoreTokens()) {
- String nextTest = st.nextToken();
-
- /* if it starts with "set:" it's a subset */
- if (nextTest.substring(0,4).equalsIgnoreCase("set:")) {
- String subset = nextTest.substring(4);
- String nextList = config.getStringConfigVal(subset, null);
- if (nextList != null) {
- addTests(nextList);
- }
- } else {
- addTest(nextTest);
- }
- }
- testTries--;
- }
-
- /**
- * Perform first-level validity checks on a test. This method returns
- * a 'reason' string for the failures it detects. In order for a
- * test to be considered valid, it:
- * <ul>
- * <li>must not have been excluded by name
- * <li>must not have been excluded by category
- * <li>must have a category which matches the requested categories
- * to run (if any)
- * <li>must not be a duplicate
- * </ul>
- * If a specialization of this class overrides this method, a
- * call to <code>super.checkValidity(td)</code> should be
- * performed.
- *
- * @param td the test description of the test to validate
- *
- * @return <code>null</code> if validation passes, or a 'reason'
- * string describing the failure
- */
- private String checkValidity(TestDescription td) {
-
- /* Determine if the test has been excluded by name */
- String testName = td.getName();
- if (xTestNames.contains(testName.toLowerCase())) {
- return "excluded by name";
- }
-
- /* Determine if the test belongs to any excluded category */
- String[] cats = td.getCategories();
- for (int i = 0; i < cats.length; i++) {
- if (xCategories.contains(cats[i].toLowerCase())) {
- return "excluded by category";
- }
- }
-
- /* Determine if test's categories are valid */
- if (categories != null) { // if null, categories are ignored
- boolean good = false;
- for (int i = 0; i < cats.length; i++) {
- if (categories.contains(cats[i])) {
- good = true;
- break;
- }
- }
- if (!good) {
- StringBuffer buf = new StringBuffer();
- buf.append("category mismatch");
- buf.append(" Categories selected for this run: ");
- for (int j = 0; j < categories.size(); j++) {
- buf.append(categories.get(j) + " ");
- }
- buf.append("\n");
- buf.append(" Categories this test applies to: ");
- String[] testCategories = td.getCategories();
- for (int j = 0; j < testCategories.length; j++) {
- buf.append(testCategories[j] + " ");
- }
- buf.append("\n");
- return buf.toString();
- }
- }
-
- /* check for undesired duplicates */
- if (testList.contains(td)) {
- return "duplicate test";
- }
- return null;
- }
-
- /**
- * Method called after each test to run a command in a separate process.
- * As an example, this method is useful to execute a diagnostic
- * command script to determine system resources after each test.
- * <p>
- * The command to run is specified by the property
- * <code>com.sun.jini.qa.harness.runCommandAfterEachTest.</code> If this
- * property is null or not specified, then no command is run.
- * <p>
- * The method returns once the command finishes. Any exception thrown
- * by trying to run the command is ignored.
- */
- private void runCommandAfterEachTest() {
- String command = config.getStringConfigVal(
- "com.sun.jini.qa.harness.runCommandAfterEachTest",
- null);
- try {
- if (command != null) {
- Process process = Runtime.getRuntime().exec(command);
- process.waitFor();
- }
- } catch (Exception ignore) {}
- }
-
- /**
- * Run a test in its own VM. All slaves are sent a request to start
- * their slave tests. The command line to execute locally is obtained
- * from the <code>TestDescription</code> and passed to
- * <code>Runtime.exec</code>. The <code>QAConfig</code> instance
- * is written to the processes <code>System.in</code> stream. Pipes
- * are created to pass any output from the process to the output stream.
- * When the process exits, the test status info is extracted from
- * the last line of the process <code>System.err</code> stream
- * and returned.
- *
- * @param testRun the test to run
- *
- * @return the <code>TestResult</code> returned by the tests
- * <code>run</code> method
- */
- private TestResult runTestOtherVM(TestRun testRun) {
- boolean discardOKOutput =
- config.getBooleanConfigVal("com.sun.jini.qa.harness.discardOKOutput",
- false);
- TestResult testResult = null;
- Process proc = null;
- Throwable unexpectedException = null;
- PrintStream printStream = outStream;
- ByteArrayOutputStream stream = null;
- try {
- // slaves should be ready to accept requests on return
- SlaveHarness.broadcastRequest(new SlaveTestRequest(config));
- String[] cmdArray =
-// testRun.td.getCommandLine(config.getSystemProps("master"));
- testRun.td.getCommandLine(null);
- StringBuffer sb = new StringBuffer();
- for (int i = 0; i < cmdArray.length; i++) {
- if (cmdArray[i].indexOf(' ') >= 0) {
- sb.append("'").append(cmdArray[i]).append("' ");
- } else {
- sb.append(cmdArray[i]).append(" ");
- }
- }
- printStream.println(
- "Starting test in separate process with command:");
- printStream.println(sb.toString());
- File workingDir = testRun.td.getWorkingDir();
- proc = Runtime.getRuntime().exec(cmdArray, null, workingDir);
- printStream = outStream;
- if (discardOKOutput) {
- stream = new ByteArrayOutputStream();
- printStream = new PrintStream(stream);
- }
- SlaveHarness.setLogStreams(printStream);
- TestResultFilter filter = bindOutput(proc, printStream);
- config.setTestTotal(testList.getTestTotal());
- config.setTestIndex(testList.getTestNumber());
- ObjectOutputStream os =
- new ObjectOutputStream(proc.getOutputStream());
- os.writeObject(config);
- os.flush();
-// bindInput(proc);
- proc.waitFor();
-// bindInput(null);
- outPipe.waitTillEmpty(5000);//XXX do I need to detect timeout?
- errPipe.waitTillEmpty(5000);
- testResult = filter.getTestResult(testRun);
- if (testResult == null) {
- testResult = new TestResult(testRun,
- false,
- Test.TEST,
- "Test VM terminated without "
- + "returning test status");
- }
- } catch (InterruptedException e) {
- testResult = new TestResult(testRun,
- false,
- Test.TEST,
- "test process was interrupted");
- try {
- Class procClass = proc.getClass();
- Field field = procClass.getDeclaredField("pid");
- field.setAccessible(true);
- int pid = field.getInt(proc);
- printStream.println();
- printStream.println("Attempting to dump threads of " +
- "test VM process " + pid);
- Process p = Runtime.getRuntime().exec(
- "/usr/bin/kill -QUIT " + pid);
- p.waitFor();
- Thread.sleep(5000); //allow time for the thread dump to happen
-
- SlaveTest.broadcast(new SlaveThreadDumpRequest());
- ObjectOutputStream os =
- new ObjectOutputStream(proc.getOutputStream());
- os.writeObject(new MasterThreadDumpRequest());
- os.flush();
- Thread.sleep(5000);
-// outPipe.waitTillEmpty(10000);
-// errPipe.waitTillEmpty(10000);
- } catch (Exception e2) {
- printStream.println();
- printStream.println("Attempt to dump threads of test VM failed");
- }
- } catch (Throwable e) {
- unexpectedException = e;
- testResult = new TestResult(testRun,
- false,
- Test.TEST,
- "runTestOtherVM failed: " + e);
- } finally {
- if (discardOKOutput && !testResult.state) {
- printStream.flush(); // don't know if this is necessary
- outStream.print(stream.toString());
- }
- SlaveHarness.setLogStreams(outStream);
- if (unexpectedException != null) {
- outStream.println("Unexpected exception:");
- unexpectedException.printStackTrace(outStream);
- }
- //output time stamp
- outStream.println();
- StringBuffer buf = new StringBuffer();
- (new MessageFormat("{0,time}")).
- format(new Object[] {new Date()}, buf, null);
- outStream.println("TIME: " + buf);
- outStream.println();
- if (proc != null) {
- try {
- proc.destroy(); // just in case, silently ignore errors
- Thread.sleep(5000); // give it time to die
- int exitValue = proc.exitValue();
- outStream.println("Test process was destroyed "
- + "and returned code " + exitValue);
- } catch (IllegalThreadStateException itse) {
- outStream.println("Test process was destroyed "
- + "but has not yet terminated");
- itse.printStackTrace();
- } catch (Exception e) {
- }
- }
- }
- return testResult;
- }
-
- /**
- * Attach <code>System.in</code> to the processes output stream.
- * This is an optional feature which can be turned on if a test
- * requires keyboard input.
- *
- * @param proc the process to attach <code>stdin</code> to.
- * <code>proc</code> may be <code>null</code>, in which case
- * any input received by <code>System.in</code> is discarded.
- *
- */
- private void bindInput(Process proc) {
- if (doInputBind) {
- if (forwarder == null) {
- forwarder = new InputForwarder();
- Thread forwarderThread =
- new Thread(forwarder, "Forwarder");
- forwarderThread.start();
- }
- if (proc != null) {
- forwarder.setOutputStream(proc.getOutputStream());
- } else {
- forwarder.setOutputStream(null);
- }
- }
- }
-
- /**
- * Utility class to forward bytes from <code>System.in</code> to
- * an output stream. An instance of this class is designed to be reused for
- * different output streams.
- */
- private class InputForwarder implements Runnable {
-
- OutputStream out;
- final Object outLock = new Object();
-
- /**
- * Set the output stream to forward bytes to.
- *
- * @param out the output stream, which may be <code>null</code>
- */
- public void setOutputStream(OutputStream out) {
- synchronized (outLock) {
- this.out = out;
- }
- }
-
- /**
- * Forward characters from <code>System.in</code> to the processes input
- * stream. Since the read may not be interruptible, there is no reliable
- * way to terminate this thread when a test VM exits. Therefore the
- * output stream will change over time via calls to
- * <code>setOutputStream</code>.
- */
- public void run() {
- boolean doit = true;
- while (doit) {
- try {
- int charval = System.in.read();
- synchronized (outLock) {
- if (charval == -1) {
- doit = false;
- } else if (out != null) {
- out.write(charval);
- out.flush();
- }
- }
- } catch (IOException e) {
- outStream.println("I/O exception in forwarder:");
- e.printStackTrace(outStream);
- }
- }
- }
- }
-
- /**
- * Attach stdout and stderr to the subprocess.
- * The <code>TestResultFilter</code> used to filter the
- * <code>System.err</code> stream is returned so that
- * the status encoded in the last line can be retrieved.
- *
- * @param proc the process to attach the IO streams to
- *
- * @return the <code>TestResultFilter</code> used to scan
- * <code>System.err</code>
- * @throws IOException if the process I/O streams cannot be obtained
- */
- private TestResultFilter bindOutput(Process proc,
- PrintStream stream) throws IOException
- {
- TestResultFilter f = new TestResultFilter();
- outPipe =
- new Pipe("test-out", proc.getInputStream(), stream, null, null);
- outPipe.start();
- errPipe =
- new Pipe("test-err", proc.getErrorStream(), stream, f, null);
- errPipe.start();
- return f;
- }
-
- /**
- * A <code>Pipe.Filter</code> which examines the input stream for
- * the token string <code>STATUS_TOKEN</code>. Once this token is
- * detected, the input stream is absorbed by this filter and used
- * to construct a <code>TestResult</code> object.
- */
- private static class TestResultFilter implements Pipe.Filter {
-
- byte[] inputBuffer = new byte[STATUS_TOKEN.length()];
- byte[] statusBytes = STATUS_TOKEN.getBytes();
- StringBuffer statusBuffer = new StringBuffer();
- int count;
- boolean searching = true;
-
- /**
- * Search for the identifying token. The token characters, and any
- * following characters, are not returned by this filter.
- *
- * @param b the input byte to filter
- * @return a byte array containing the filter results
- */
- public byte[] filterInput(byte b) {
- byte[] returnBuffer = new byte[0];
- if (searching) {
- inputBuffer[count++] = b;
- for (int i = 0; i < count; i++) {
- if (inputBuffer[i] != statusBytes[i]) {
- returnBuffer = new byte[count];
- System.arraycopy(inputBuffer,
- 0,
- returnBuffer,
- 0,
- count);
- count = 0;
- break;
- }
- }
- if (count == statusBytes.length) {
- searching = false;
- }
- } else {
- String s = new String(new byte[]{b});
- statusBuffer.append(s);
- }
- return returnBuffer;
- }
-
- /**
- * Get the <code>TestResult</code> encoded in last line of the output stream
- * by the test wrapper. The format of the data is:
- * <ul>
- * <li>the identifying token: STATUS_TOKEN
- * <li>a character: 'P' if the test passed, 'F' if the test failed
- * <li>a character representation of the integer failure type
- * <li>a string containing the status message
- * </ul>
- *
- * @param testRun the test run
- * @return the <code>TestResult</code> object corresponding to the encoded
- * info. <code>null</code> is returned if the identifying token
- * was never detected, or if the encoded test result info is missing.
- */
- private TestResult getTestResult(TestRun testRun) {
- String s = statusBuffer.toString();
- if (s.length() < 2) {
- return null;
- }
- boolean passed = (s.charAt(0) == 'P');
- int type = s.charAt(1) - '0';
- return new TestResult(testRun, passed, type, s.substring(2));
- }
- }
-
- /**
- * Generate the test result string to be sent from the test VM
- * to the harness VM.
- *
- * @param state boolean indicating whether the test passed
- * @param type the failure type
- * @param message the test summary message
- */
- static String genMessage(boolean state, int type, String message) {
- String s = STATUS_TOKEN + (state ? "P" : "F")
- + Integer.toString(type)
- + message;
- return s;
- }
-
- /**
- * Call all registered <code>ConfigurationVerifiers</code> for
- * the given test. If any verifier returns <code>false</code>, this
- * method returns false. Otherwise this method returns true.
- *
- * @param td the <code>TestDescription</code> of the current test
- * @param verifierNames the list of verifier class names
- * @throws TestException which wraps any exception thrown while
- * instantiating or calling the verifiers
- */
- private boolean checkVerifiers(TestDescription td, String verifierNames)
- throws TestException
- {
- String[] verifiers = config.parseString(verifierNames, ", \t");
- if (verifiers != null) {
- for (int i = 0; i < verifiers.length; i++) {
- try {
- Class c = Class.forName(verifiers[i],
- true,
- config.getTestLoader());
- ConfigurationVerifier v =
- (ConfigurationVerifier) c.newInstance();
- if (!v.canRun(td, config)) {
- return false;
- }
- } catch (Exception e) {
- throw new TestException("Exception invoking "
- + "configuration filter",
- e);
- }
- }
- }
- return true;
- }
-
- /**
- * Add categories of tests. Test full test/category list in
- * <code>${com.sun.jini.qa.home}/lib/testlist.txt</code> is
- * read and filtered by the set of category names supplied on
- * the command line. The input file is assumed to be sorted
- * by category, then by test name. Duplicate test names which
- * appear in more than one requested category are discarded.
- * Test names in testlist.txt are expressed using dot notation,
- * and are converted to the local file system path syntax.
- */
- private void addByCategory() throws TestException {
- String[] rqstdCats = getRequestedCategories();
- if (rqstdCats == null || rqstdCats.length == 0) {
- outStream.println("No tests or categories specified");
- System.exit(1);
- }
- // used to avoid dups due to multiple category membership
- HashSet testsToRun = new HashSet();
- for (int i = 0; i < rqstdCats.length; i++) {
- ArrayList l = (ArrayList) categoryMap.get(rqstdCats[i]);
- if (l == null) {
- outStream.println("no tests in category " + rqstdCats[i]);
- continue;
- }
- for (int j = 0; j < l.size(); j++) {
- String testName = (String) l.get(j);
- if (testsToRun.contains(testName)) {
- continue;
- }
- testsToRun.add(testName);
- addTest(testName);
- }
- }
- }
-
- private void loadTestDescriptions() throws TestException {
- if (categoryMap != null) {
- return;
- }
- categoryMap = new HashMap();
- testMap = new HashMap();
- // inhibit category processing if none requested
- if (config.getStringConfigVal("categories", null) == null
- && config.getStringConfigVal("category", null) == null)
- {
- return;
- }
- String testJar = config.getStringConfigVal("testJar", null);
- if (testJar == null) {
- throw new TestException("testjar is not defined");
- }
- JarFile jarFile;
- try {
- jarFile = new JarFile(testJar);
- } catch (IOException e) {
- throw new TestException("cannot access test jar file " + testJar,
- e);
- }
- Enumeration en = jarFile.entries();
- while (en.hasMoreElements()) {
- ZipEntry entry = (ZipEntry) en.nextElement();
- if (entry.getName().endsWith(".td")) {
- Properties p = config.loadProperties(entry.getName());
- if (p.getProperty("testClass") == null) {
- outStream.println(
- "Test description file has no testClass property: "
- + entry.getName());
- continue;
- }
- testMap.put(entry.getName(), p);
- String cats = p.getProperty("testCategories");
- if (cats != null) {
- String[] categories = config.parseString(cats, ", \t");
- for (int i = 0; i < categories.length; i++) {
- ArrayList l =
- (ArrayList) categoryMap.get(categories[i]);
- if (l == null) {
- l = new ArrayList();
- categoryMap.put(categories[i], l);
- }
- l.add(entry.getName());
- }
- }
- }
- }
- }
-
- /**
- * This method prints to standard output, a usage message for the QA test
- * harness.
- */
- private void usage() {
- outStream.print("Usage: QARunner configFilename ");
- outStream.print("[-categories cat1,cat2]");
- outStream.println("[-tests test1,test2]");
- outStream.print("[-xcategories] cat1,cat2]");
- outStream.println("[-xtests test1,test2]");
- outStream.println("Descriptions:");
- outStream.println("configFilename");
- outStream.println("\tThe name of the properties file");
- outStream.println("\tfor this run of tests");
- outStream.println("-categories");
- outStream.println("\tA comma separated list ");
- outStream.println("\tof the categories this product is being ");
- outStream.println("\ttested in. ");
- outStream.println("-tests");
- outStream.println("\tA comma separated list of the individual");
- outStream.println("\ttests to run. These tests are only run if");
- outStream.println("\tthey are appropriate for the categories");
- outStream.println("\tthe product is in");
- outStream.println("-xcategories (optional)");
- outStream.println("\tA comma separated list of the categories");
- outStream.println("\tto exclude from the current test run. Any");
- outStream.println("\ttest belonging to one or more of these");
- outStream.println("\tcategories will not be run.");
- outStream.println("-tests (optional)");
- outStream.println("\tA comma separated list of the tests");
- outStream.println("\tto exclude from the current test run");
- outStream.println("One of -tests or -categories must be specified");
- }//end usage
-
- /**
- * Prints to standard output configuration information for this
- * test run. This method is somewhat out-of-date.
- */
- private void displayConfigInfo() {
- String installDir = "com.sun.jini.qa.home";//XXX note 'qa'
- String jskHome = "com.sun.jini.jsk.home";
-
- String[] categories = getRequestedCategories();
- StringBuffer categoryString = new StringBuffer("No Categories");
- if (categories != null) {
- categoryString = new StringBuffer();
- for (int i = 0; i < categories.length; i++) {
- categoryString.append(categories[i] + " ");
- }
- }
- outStream.println("");
- outStream.println("-----------------------------------------");
- outStream.println("GENERAL HARNESS CONFIGURATION INFORMATION:");
- outStream.println("");
- outStream.println(" Date started:");
- outStream.println(" " + (new Date()).toString());
- outStream.println(" Installation directory of the JSK:");
- outStream.println(" " + jskHome + "="
- + config.getStringConfigVal(jskHome, null));
- outStream.println(" Installation directory of the harness:");
- outStream.println(" " + installDir + "="
- + config.getStringConfigVal(installDir, null));
- outStream.println(" Categories being tested:");
- outStream.println(" categories=" + categoryString);
- Properties properties = System.getProperties();
- outStream.println("-----------------------------------------");
- outStream.println("ENVIRONMENT PROPERTIES:");
- outStream.println("");
- outStream.println(" JVM information:");
- outStream.println(" " + properties.getProperty("java.vm.name","unknown")
- + ", " + properties.getProperty("java.vm.version")
- + ", " + properties.getProperty("sun.arch.data.model","32")
- + " bit VM mode");
- outStream.println(" " + properties.getProperty("java.vm.vendor",""));
- outStream.println(" OS information:");
- outStream.println(" " + properties.getProperty("os.name","unknown")
- + ", " + properties.getProperty("os.version")
- + ", " + properties.getProperty("os.arch"));
- outStream.println("");
-
- }
-
- /**
- * Get the categories to run. If no categories are specified,
- * then <code>null</code> is returned to signify that all categories
- * are to be run.
- *
- * @return the list of categories of tests to run, or <code>null</code>
- * for all categories.
- */
- private List getCategories() {
- ArrayList categories = null;
- String cats = config.getStringConfigVal("categories", null);
- if (cats == null)
- cats = config.getStringConfigVal("category", null);
- if (cats != null) {
- categories = new ArrayList();
- StringTokenizer st = new StringTokenizer(cats, ",");
- while (st.hasMoreTokens()) {
- categories.add(st.nextToken().toLowerCase());
- }
- }
- return categories;
- }
-
- /**
- * Return the timeout value to use. The value is specified by
- * the parameter named <code>com.sun.jini.qa.harness.timeout</code>,
- and is iterpreted
- * as a value in seconds. If undefined, a default value of
- * zero (no timeout) is returned.
- *
- * @return the timeout value for a test
- */
- private int getTimeout() {
- int seconds = config.getIntConfigVal("com.sun.jini.qa.harness.timeout",
- 0);
- return seconds * 1000; // convert to milliseconds
- }
-
- private static class SlaveThreadDumpRequest implements SlaveRequest {
-
- public Object doSlaveRequest(SlaveTest slaveTest) throws Exception {
- AdminManager manager = slaveTest.getAdminManager();
- Iterator it = manager.iterator();
- while (it.hasNext()) {
- Object admin = it.next();
- if (admin instanceof NonActivatableGroupAdmin) {
- if (((NonActivatableGroupAdmin) admin).forceThreadDump()) {
- try {
- Thread.sleep(5000); // give it time to flush
- } catch (InterruptedException e) {
- }
- }
- }
- }
- return null;
- }
- }
-
- private static class MasterThreadDumpRequest
- implements MasterTest.MasterTestRequest
- {
-
- public void doRequest(TestEnvironment test) throws Exception {
- if (test instanceof QATestEnvironment) {
- ((QATestEnvironment) test).forceThreadDump(); // delay done by test
- }
- }
- }
-}
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.sun.jini.qa.harness;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.FileOutputStream;
+import java.io.FileNotFoundException;
+import java.net.ServerSocket;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.MalformedURLException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.text.MessageFormat;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.MissingResourceException;
+import java.util.Properties;
+import java.util.ResourceBundle;
+import java.util.StringTokenizer;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.jar.JarFile;
+import java.util.zip.ZipEntry;
+import java.lang.reflect.Field;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+
+//Should there be an 'AbortTestRequest' ?
+
+/**
+ * An implementation of the master component of a distributed test harness.
+ * Provides the command line interface for starting a test run, execs and
+ * manages the VM which contains the master test, and coordinates the
+ * activities of all participating slave harness.
+ * <p>
+ * Command line options accessed by this class:
+ * <ul>
+ * <li><code>-tests testlist</code> where <code>testlist</code> is a
+ * comma-separated list of test descriptor names to run. If omitted, all
+ * tests matching other filters (such as categories) are run.
+ *
+ * <li> <code>-xtests testlist</code> where <code>testlist</code> is the
+ * comma separated list of tests to exclude from testing
+ *
+ * <li> <code>-xcategories catlist</code> where <code>catlist</code> is
+ * the comma set of categories to exclude from testing. Any test
+ * which is a member of any category in this list is excluded from
+ * testing.
+ * <li> <code>-env envfile</code> where <code>envfile</code> is the name
+ * of a properties file containing environment definitions for
+ * running tests (i.e. which jdk/jsk versions to use when running tests).
+ * If the file is specified as a relative path, it is resolved relative
+ * to the current working directory.
+ * <li> <code>-categories categorylist</code> is the comma-separated
+ * set of categories to test
+ * <li> <code>-help</code> prints a usage message
+ * </ul>
+ * <p>
+ * Property values accessed by this class:
+ * <ul>
+ * <li><code>com.sun.jini.qa.harness.verifier</code> is the name of the
+ * configuration verifier class to load. Before a test is run,
+ * this class is loaded (if defined) and its <code>canRun</code>
+ * method called to determine whether to run the test.
+ * <li><code>com.sun.jini.qa.harness.runCommandAfterEachTest</code> is a
+ * platform dependent command line to be executed at the conclusion of
+ * each test.
+ * </ul>
+ * <p>
+ * Note that there is no actual distinction between command-line options
+ * and property values. That is, a property file could contain the entry
+ * 'xtests=foo,bar' to exclude tests foo and bar. Likewise, the command line
+ * could include the option "-com.sun.jini.qa.harness.runCommandAfterEachTest ls"
+ * which would cause the Unix 'ls' command to be run after each test.
+ * <p>
+ * Tests are always run in a separate VM spawned by this class. The
+ * <code>QAConfig</code> object is serialized and written to the
+ * test VM's input stream.
+ * <p>
+ * Test status information must be passed back from the test VM
+ * to the harness VM. To do this, the wrapper writes a specially formatted
+ * string as the last line of output to <code>System.err</code>.
+ * <code>MasterHarness</code> tracks the last line written to this stream,
+ * and uses the information in it to update the test results summary tables.
+ * Because the <code>message</code> portion of the status could be
+ * a multi-line string, the wrapper converts any '\n' or '\r' characters
+ * in the message to the tokens '<<n>>' or '<<r>>' respectively.
+ * <code>MasterHarness</code> restores these line terminator characters
+ * as part of reconstructing the test results.
+ *</table> <p>
+ */
+class MasterHarness {
+
+ /**
+ * token which identifies the beginning of a test result message being
+ * sent from a test running in another VM
+ */
+ private static final String STATUS_TOKEN = "!STATUS!";
+
+ /** the keep-alive port number */
+ public final static int KEEPALIVE_PORT=10004;
+
+ /** the output stream for this VM's output */
+ private volatile PrintStream outStream = System.out;
+
+ /** the pipe for collecting stderr output from the test vm */
+ private Pipe errPipe;
+
+ /** the pipe for collecting stdout output from the test vm */
+ private Pipe outPipe;
+
+ /** the max recursion depth when parsing test sets */
+ private final static int MAXTESTCOUNT = 100;
+
+ /** the command line arguments */
+ private final String[] args;
+
+ /** the list of test categories to run. If null, all categories are run. */
+ private List categories;
+
+ /** depth counter to detect infinite recursion in <code>addTest</code> */
+ private int testTries = 0;
+
+ /**
+ * the object which forwards input to a test subprocess. This object
+ * is reused for all test subprocesses.
+ */
+ private InputForwarder forwarder;
+
+ /** the set of tests to run */
+ private final TestList testList;
+
+ /** the list of categories to exclude from testing */
+ private List xCategories;
+
+ /** the list of tests to exclude from testing */
+ private List xTestNames;
+
+ /** the configuration object for this test run */
+ private final QAConfig config;
+
+ /**
+ * boolean controlling whether to read from stdin
+ */
+ boolean doInputBind;
+
+ /** a reference to the keep-alive thread */
+ private final Thread keepaliveThread;
+
+ /** the category map, cached if non-null */
+ private HashMap categoryMap = null;
+
+ /** the test name to test description properties map */
+ private HashMap testMap = null;
+
+ /**
+ * Construct the <code>MasterHarness</code>. Creates an instance
+ * of <code>QAConfig</code>, starts the keep-alive thread, connects
+ * to any participating slave harnesses, and builds the test list.
+ *
+ * @param args <code>String</code> array containing the command line
+ * arguments
+ */
+ MasterHarness(String[] args) throws TestException {
+ this.args = args;
+ if (args.length == 0 || args[0].equals("-h") || args[0].equals("-help"))
+ {
+ usage();
+ System.exit(1);
+ }
+ File f = new File(args[0]);
+ // args[0] must be absolute, or it will be resolved against
+ // the kit installation directory (which is undefined)
+ if (! f.isAbsolute()) {
+ f = f.getAbsoluteFile();
+ args[0] = f.toString();
+ }
+ if (! f.exists()) {
+ usage();
+ System.exit(1);
+ }
+ config = new QAConfig(args);
+ keepaliveThread = new Thread(new KeepAlivePort(), "Keepalive");
+ keepaliveThread.setDaemon(false);
+ keepaliveThread.start();
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignore) {
+ Thread.currentThread().interrupt();
+ }
+ SlaveHarness.connect();
+ // check for installed provider. Failure here should kill slaves.
+ try {
+ Class policyClass =
+ Class.forName("com.sun.jini.qa.harness.MergedPolicyProvider");
+ if (policyClass.getClassLoader().getParent() != null) {
+ outStream.println("MergedPolicyprovider must be "
+ + "installed in an extensions ClassLoader");
+ System.exit(1);
+ }
+ } catch (Exception e) {
+ outStream.println("failed to find MergedPolicyProvider");
+ System.exit(1);
+ }
+ outStream.println("");
+ outStream.println("-----------------------------------------");
+ outStream.println("CONFIGURATION FILE:");
+ outStream.println("");
+ outStream.println(" " + args[0]);
+ testList = new TestList(config, System.currentTimeMillis());
+ loadTestDescriptions();
+ buildTestList();
+ displayConfigInfo(); // seems like this should be in QAConfig
+ }
+
+ /**
+ * A <code>Runnable</code> which opens the keep-alive server socket
+ * and accepts connections for the life of this VM.
+ */
+ private class KeepAlivePort implements Runnable {
+
+ public void run() {
+ ServerSocket socket = null;
+ ArrayList socketList = new ArrayList(); // keep references
+ try {
+ SocketAddress add = new InetSocketAddress(KEEPALIVE_PORT);
+ socket = new ServerSocket();
+// if (!socket.getReuseAddress()) socket.setReuseAddress(true);
+ socket.bind(add);
+ while (true) {
+ socketList.add(socket.accept());
+ }
+ } catch (Exception e) {
+ outStream.println("Problem with KEEPALIVE_PORT:" + KEEPALIVE_PORT );
+ outStream.println("Unexpected exception:");
+ e.printStackTrace(outStream);
+ } finally {
+ try {
+ socket.close();
+ } catch (IOException ex) {/*Ignore*/}
+ System.exit(1);
+ }
+ }
+ }
+
+ /**
+ * Delete files which have been registered for deletion. Registered
+ * files are deleted on the master host, and a request to delete
+ * registered files is sent to all slave hosts.
+ *
+ * @throws TestException if an error occurs when communicating
+ * with a slave
+ */
+ private void deleteRegisteredFiles() throws TestException {
+ config.deleteRegisteredFiles();
+ SlaveHarness.broadcastRequest(new DeleteRegisteredFilesRequest());
+ }
+
+ /**
+ * Build the list of tests.
+ */
+ private void buildTestList() throws TestException {
+ String testString = config.getStringConfigVal("tests", null);
+ categories = getCategories();
+ xTestNames = buildList(config.getStringConfigVal("xtests", null));
+ addFromURL(xTestNames, config.getStringConfigVal("excludeList", null));
+ xCategories = buildList(config.getStringConfigVal("xcategories", null));
+ outStream.println("");
+ outStream.println("-----------------------------------------");
+ outStream.println("SETTING UP THE TEST LIST:");
+ outStream.println("");
+ // If there are no arguments assume the user wants all the tests.
+ if (testString == null) {
+ addByCategory();
+ } else {
+ addTests(testString);
+ }
+ }
+
+ /**
+ * Attempt to add the test specified by <code>testName</code> to the current
+ * set of tests. A test is obtained by calling the configurations
+ * <code>getTestDescription</code> factory method. The test is added if:
+ * <ul>
+ * <li>the named test can be found and it's <code>TestDescription</code>
+ * is valid
+ * <li>the test is not in the excluded test list
+ * <li>none of the test categories are in the excluded category list
+ * <li>the test has a category which is included in the requested categories
+ * (if any)
+ * <li>the test passes test suite specific validity checks
+ * <li>the test is not a duplicate
+ * </ul>
+ * @param testName the name of the test to add
+ */
+ private void addTest(String testName) throws TestException {
+ String reason = null;
+ TestDescription td = null;
+ Properties p = (Properties) testMap.get(testName);
+ // for td's not in the jar, or if -categories was not included
+ if (!testName.endsWith(".td")) {
+ testName += ".td";
+ }
+ if (p == null) {
+ p = config.loadProperties(testName);
+ }
+ if (p == null) {
+ throw new TestException("no properties for " + testName);
+ }
+ try {
+ td = config.getTestDescription(testName, p);
+ } catch (TestException e) {
+ e.printStackTrace();
+ reason = e.getMessage();
+ }
+
+ /* perform built-in checks in the test description */
+ if (reason == null) {
+ reason = td.checkValidity();
+ }
+
+ /* if ok, perform command line checks */
+ if (reason == null) {
+ reason = checkValidity(td);
+ }
+
+ /* complain and bail if anything went wrong */
+ if (reason != null) {
+ outStream.println(" Skipping test: " + testName);
+ outStream.println(" Reason: " + reason);
+ return;
+ }
+ outStream.println(" Adding test: " + testName);
+ String[] configTags = config.getConfigTags();
+ for (int i = 0; i < configTags.length; i++) {
+ testList.add(new TestRun(td, configTags[i]));
+ }
+ }
+
+ /**
+ * Run all of the tests in the test list and displays their results.
+ *
+ * @return true if all started tests complete and pass; false if either
+ * one or more of the started tests fail or does not complete
+ */
+ boolean runTests() throws TestException {
+ deleteRegisteredFiles();
+ doInputBind = config.getBooleanConfigVal(
+ "com.sun.jini.qa.harness.bindInput", true);
+ boolean genHtml = config.getBooleanConfigVal(
+ "com.sun.jini.qa.harness.generateHtml", false);
+ outStream.println("-----------------------------------------");
+ outStream.println("STARTING TO RUN THE TESTS");
+ outStream.println("");
+ outStream.println("");
+
+ // main test loop
+ while (testList.hasMore()) {
+ long testRunStartTime = System.currentTimeMillis();
+ TestResult testResult = null;
+ File logFile = null;
+ TestRun testRun = testList.next();
+ TestDescription td = testRun.td;
+ String configTag = testRun.configTag;
+ config.doConfigurationSetup(configTag, td);
+ String verifierNames = config.getStringConfigVal(
+ "com.sun.jini.qa.harness.verifier", null);
+ if (!checkVerifiers(td, verifierNames)) {
+ testResult = new TestResult(
+ testRun, false, Test.SKIP,
+ "verifiers are: " + verifierNames);
+ testList.add(testRun, testResult);
+ outStream.println(testResult.toString());
+ outStream.println("");
+ outStream.println("-----------------------------------------");
+ outStream.println("");
+ continue;
+ }
+ if (genHtml) {
+ logFile = getLogFile(td);
+ setOutputStream(logFile);
+ }
+ String running = ((testRun.isRerun) ? "RE-RUNNING " : "Running ");
+ outStream.println(running + td.getName());
+ outStream.println("Time is " + new Date());
+ TestRunner runner = new TestRunner(testRun);
+ int interval = getTimeout(); // ask the subclass
+ if (interval > 0) {
+ Thread testThread = new Thread(runner, "TestRunner");
+ Timeout.TimeoutHandler handler =
+ new Timeout.ThreadTimeoutHandler(testThread);
+ Timeout timeout = new Timeout(handler, interval);
+ testThread.start();
+ timeout.start();
+ try {
+ testThread.join();
+ } catch (InterruptedException e) { //shouldn't happen
+ Thread.currentThread().interrupt();
+ outStream.println("testThread.join() interrupted..." +
+ "should not happen");
+ }
+ if(testThread.interrupted()) {
+ outStream.println("Test was interrupted");
+ }
+ if (timeout.timedOut()) {
+ outStream.println("Timed out");
+ SlaveTest.broadcast(new TeardownRequest());
+ } else {
+ timeout.cancel();
+ }
+ } else {
+ runner.run();
+ }
+ deleteRegisteredFiles(); // do here in case test vm died
+ SlaveTest.waitForSlaveDeath(60); // give them a minute
+ testResult = runner.getTestResult();
+ testResult.setLogFile(logFile);
+ testResult.setElapsedTime(
+ System.currentTimeMillis() - testRunStartTime);
+ testList.add(testRun, testResult);
+ outStream.println(testResult.toString());
+ runCommandAfterEachTest();
+ if (genHtml) {
+ outStream.close(); // close log file
+ setOutputStream(System.out); // return to System.out
+ } else {
+ outStream.println("");
+ outStream.println("-----------------------------------------");
+ outStream.println("");
+ }
+ } // end main while loop
+ outStream.println("SUMMARY =================================");
+ outStream.println("");
+ TestList.TestResultIterator iter = testList.createTestResultIterator();
+ boolean removePassResults = config.getBooleanConfigVal(
+ "com.sun.jini.qa.harness.generateHtml.removePassResults",false);
+ while (iter.hasMore()) {
+ TestResult[] results = iter.next();
+ for (int i = 0; i < results.length; i++) {
+ if (i == 0) {
+ outStream.println(results[i].toString());
+ } else {
+ outStream.println("RE-RUN " + i);
+ outStream.println(results[i].toString());
+ }
+ // remove passed test result is required
+ if (removePassResults && results[i].state) {
+ if (results[i].logFile != null &&
+ ! results[i].logFile.delete())
+ {
+ outStream.println("Error: could not remove "
+ + results[i].logFile);
+ }
+ // set logFile to null so that further report generation
+ // won't try to create links to the log files
+ results[i].logFile = null;
+ }
+ }
+ outStream.println("-----------------------------------------");
+ }
+ outStream.println("");
+ outStream.println("# of tests started = " +
+ testList.getNumStarted());
+ outStream.println("# of tests completed = "
+ + testList.getNumCompleted());
+ if (testList.getNumSkipped() > 0) {
+ outStream.println("# of tests skipped = " +
+ testList.getNumSkipped());
+ }
+ outStream.println("# of tests passed = " + testList.getNumPassed());
+ outStream.println("# of tests failed = " + testList.getNumFailed());
+ if (testList.getNumRerun() > 0) {
+ outStream.println("# of tests rerun = " +
+ testList.getNumRerun());
+ }
+ outStream.println("");
+ outStream.println("-----------------------------------------");
+ outStream.println("");
+ testList.setFinishTime(System.currentTimeMillis());
+ outStream.println(" Date finished:");
+ outStream.println(" "
+ + (new Date(testList.getFinishTime())).toString());
+ outStream.println(" Time elapsed:");
+ outStream.println(" " +
+ ((testList.getDurationTime())/1000) + " seconds");
+ outStream.println("");
+
+ if (genHtml) {
+ try {
+ HtmlReport htmlReport = new HtmlReport(config, testList);
+ htmlReport.generate();
+ } catch (IOException ioe) {
+ outStream.println("Exception trying to generate html:");
+ ioe.printStackTrace(outStream);
+ }
+ setOutputStream(System.out); // return to System.out
+ }
+
+ boolean genXml = config.getBooleanConfigVal(
+ "com.sun.jini.qa.harness.generateXml", false);
+ if (genXml) {
+ try {
+ XmlReport xmlReport = new XmlReport(config, testList);
+ xmlReport.generate();
+ } catch (Exception e) {
+ outStream.println("Exception trying to generate xml:");
+ e.printStackTrace(outStream);
+ }
+ }
+ return (testList.getNumFailed() == 0 &&
+ testList.getNumStarted() == testList.getNumCompleted());
+ } //end runTests
+
+ /**
+ * Set the output stream used by the harness VM to
+ * the given stream.
+ */
+ private void setOutputStream(PrintStream out) {
+ if (outStream != null) {
+ outStream.flush();
+ }
+ outStream = out;
+ }
+
+ /**
+ * Set the output stream used by the harness VM to
+ * a FileOutputStream writing to the given file.
+ * If the FileOutputStream cannot be created,
+ * the VM will exit.
+ */
+ private void setOutputStream(File logFile) {
+ try {
+ setOutputStream(new PrintStream(new FileOutputStream(logFile)));
+ } catch (FileNotFoundException fnfe) {
+ outStream.println("Cannot create " + logFile + ". Exiting...");
+ System.exit(2);
+ }
+ }
+
+ /**
+ * Establish a file object given a test description. The
+ * file name is the test name with '/' replaced with '_'.
+ * The file path is the same file path as that given in the
+ * <code>com.sun.jini.qa.harness.generateHtml.resultLog</code> property.
+ */
+ private File getLogFile(TestDescription td) {
+ String resultLog = config.getStringConfigVal(
+ "com.sun.jini.qa.harness.generateHtml.resultLog","index.html");
+ File resultDir = (new File(resultLog)).getParentFile();
+ if (resultDir != null && !resultDir.exists()) {
+ resultDir.mkdirs();
+ }
+ String testName = td.getName().replace('/','_').replace('\\','_');
+ File logFile = new File(resultDir, testName + ".txt");
+ int counter = 1;
+ while (logFile.exists()) {
+ logFile = new File(resultDir, testName + "-" + counter + ".txt");
+ counter++;
+ }
+ return logFile;
+ }
+
+ /**
+ * A <code>Runnable</code> used to execute a test.
+ */
+ private class TestRunner implements Runnable {
+
+ private TestRun testRun;
+ private TestResult testResult;
+
+ /**
+ * Construct the <code>TestRunner</code>.
+ *
+ * @param testRun the test to run
+ */
+ public TestRunner(TestRun testRun) {
+ this.testRun = testRun;
+ }
+
+ /**
+ * Run the test in another VM.
+ * The <code>TestResult</code> is saved for later retrieval.
+ */
+ public void run() {
+ testResult = runTestOtherVM(testRun);
+ }
+
+ /**
+ * Return the <code>TestResult</code> returned when the test
+ * was executed by this <code>TestRunner</code>.
+ *
+ * @return the test result
+ */
+ public TestResult getTestResult() {
+ return testResult;
+ }
+ }
+
+ /**
+ * Returns the categories that were input for the current test run either
+ * on the command line, or in the configuration file.
+ *
+ * @return <code>String</code> array containing the categories that were
+ * input for the current test run either on the command line, or
+ * in the configuration file.
+ */
+ private String[] getRequestedCategories() {
+ if (categories == null) {
+ return null;
+ }
+ return ((String[])categories.toArray(new String[categories.size()]));
+ }
+
+ /**
+ * Convert a string of comma-separated tokens to lower case and return
+ * the tokens as a <code>List</code>.
+ *
+ * @param flatString the string of comma-separated tokens
+ *
+ * @return a <code>List</code> of tokens. If <code>flatString</code> is
+ * <code>null</code> or contains no tokens, a zero length
+ * <code>List</code> is returned.
+ */
+ private List buildList(String flatString) {
+ ArrayList list = new ArrayList();
+ if (flatString != null) {
+ StringTokenizer st = new StringTokenizer(flatString, ",");
+ while (st.hasMoreTokens()) {
+ list.add(st.nextToken().toLowerCase());
+ }
+ }
+ return list;
+ }
+
+ private void addFromURL(List list, String urlString) throws TestException {
+ if (urlString == null) {
+ return;
+ }
+ URL url = null;
+ try {
+ url = new URL(urlString);
+ } catch (MalformedURLException e1) {
+ try {
+ url = new URL("file:" + urlString);
+ } catch (MalformedURLException e2) {
+ throw new TestException("neither " + urlString
+ + " nor file:" + urlString
+ + " are value URLs");
+ }
+ }
+ try {
+ URLConnection conn = url.openConnection();
+ BufferedReader reader =
+ new BufferedReader(new InputStreamReader(conn.getInputStream()));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ if (line.trim().length() == 0) {
+ continue;
+ }
+ list.add(line.trim().toLowerCase());
+ }
+ } catch (IOException e) {
+ throw new TestException("problem reading exclusion list at " + url, e);
+ }
+ }
+
+ /**
+ * Add all of the tests identified in <code>testSet</code> to the
+ * set of tests to run. The list is a comma-separated set of
+ * test names or test set names. A test set is preceded by the
+ * prefix "set:". The config is searched for a match to the test
+ * set name, and if a match is found, the value of the match is
+ * included in the list of tests. This evaluation is performed recursively,
+ * so that sets may refer to other sets. Tests are not added to the
+ * run set unless they meet the criteria defined in <@link addTest>.
+ *
+ * @param testSet the comma-separated set of tests to run
+ */
+ private void addTests(String testSet) throws TestException {
+ // to make sure we are not in some kind of property loop
+ if (testTries > MAXTESTCOUNT) {
+ return;
+ }
+ testTries++;
+ StringTokenizer st = new StringTokenizer(testSet, ",");
+ while (st.hasMoreTokens()) {
+ String nextTest = st.nextToken();
+
+ /* if it starts with "set:" it's a subset */
+ if (nextTest.substring(0,4).equalsIgnoreCase("set:")) {
+ String subset = nextTest.substring(4);
+ String nextList = config.getStringConfigVal(subset, null);
+ if (nextList != null) {
+ addTests(nextList);
+ }
+ } else {
+ addTest(nextTest);
+ }
+ }
+ testTries--;
+ }
+
+ /**
+ * Perform first-level validity checks on a test. This method returns
+ * a 'reason' string for the failures it detects. In order for a
+ * test to be considered valid, it:
+ * <ul>
+ * <li>must not have been excluded by name
+ * <li>must not have been excluded by category
+ * <li>must have a category which matches the requested categories
+ * to run (if any)
+ * <li>must not be a duplicate
+ * </ul>
+ * If a specialization of this class overrides this method, a
+ * call to <code>super.checkValidity(td)</code> should be
+ * performed.
+ *
+ * @param td the test description of the test to validate
+ *
+ * @return <code>null</code> if validation passes, or a 'reason'
+ * string describing the failure
+ */
+ private String checkValidity(TestDescription td) {
+
+ /* Determine if the test has been excluded by name */
+ String testName = td.getName();
+ if (xTestNames.contains(testName.toLowerCase())) {
+ return "excluded by name";
+ }
+
+ /* Determine if the test belongs to any excluded category */
+ String[] cats = td.getCategories();
+ for (int i = 0; i < cats.length; i++) {
+ if (xCategories.contains(cats[i].toLowerCase())) {
+ return "excluded by category";
+ }
+ }
+
+ /* Determine if test's categories are valid */
+ if (categories != null) { // if null, categories are ignored
+ boolean good = false;
+ for (int i = 0; i < cats.length; i++) {
+ if (categories.contains(cats[i])) {
+ good = true;
+ break;
+ }
+ }
+ if (!good) {
+ StringBuffer buf = new StringBuffer();
+ buf.append("category mismatch");
+ buf.append(" Categories selected for this run: ");
+ for (int j = 0; j < categories.size(); j++) {
+ buf.append(categories.get(j) + " ");
+ }
+ buf.append("\n");
+ buf.append(" Categories this test applies to: ");
+ String[] testCategories = td.getCategories();
+ for (int j = 0; j < testCategories.length; j++) {
+ buf.append(testCategories[j] + " ");
+ }
+ buf.append("\n");
+ return buf.toString();
+ }
+ }
+
+ /* check for undesired duplicates */
+ if (testList.contains(td)) {
+ return "duplicate test";
+ }
+ return null;
+ }
+
+ /**
+ * Method called after each test to run a command in a separate process.
+ * As an example, this method is useful to execute a diagnostic
+ * command script to determine system resources after each test.
+ * <p>
+ * The command to run is specified by the property
+ * <code>com.sun.jini.qa.harness.runCommandAfterEachTest.</code> If this
+ * property is null or not specified, then no command is run.
+ * <p>
+ * The method returns once the command finishes. Any exception thrown
+ * by trying to run the command is ignored.
+ */
+ private void runCommandAfterEachTest() {
+ String command = config.getStringConfigVal(
+ "com.sun.jini.qa.harness.runCommandAfterEachTest",
+ null);
+ try {
+ if (command != null) {
+ Process process = Runtime.getRuntime().exec(command);
+ process.waitFor();
+ }
+ } catch (Exception ignore) {}
+ }
+
+ /**
+ * Run a test in its own VM. All slaves are sent a request to start
+ * their slave tests. The command line to execute locally is obtained
+ * from the <code>TestDescription</code> and passed to
+ * <code>Runtime.exec</code>. The <code>QAConfig</code> instance
+ * is written to the processes <code>System.in</code> stream. Pipes
+ * are created to pass any output from the process to the output stream.
+ * When the process exits, the test status info is extracted from
+ * the last line of the process <code>System.err</code> stream
+ * and returned.
+ *
+ * @param testRun the test to run
+ *
+ * @return the <code>TestResult</code> returned by the tests
+ * <code>run</code> method
+ */
+ private TestResult runTestOtherVM(TestRun testRun) {
+ boolean discardOKOutput =
+ config.getBooleanConfigVal("com.sun.jini.qa.harness.discardOKOutput",
+ false);
+ TestResult testResult = null;
+ Process proc = null;
+ Throwable unexpectedException = null;
+ PrintStream printStream = outStream;
+ ByteArrayOutputStream stream = null;
+ try {
+ // slaves should be ready to accept requests on return
+ SlaveHarness.broadcastRequest(new SlaveTestRequest(config));
+ String[] cmdArray =
+// testRun.td.getCommandLine(config.getSystemProps("master"));
+ testRun.td.getCommandLine(null);
+ StringBuffer sb = new StringBuffer();
+ for (int i = 0; i < cmdArray.length; i++) {
+ if (cmdArray[i].indexOf(' ') >= 0) {
+ sb.append("'").append(cmdArray[i]).append("' ");
+ } else {
+ sb.append(cmdArray[i]).append(" ");
+ }
+ }
+ printStream.println(
+ "Starting test in separate process with command:");
+ printStream.println(sb.toString());
+ File workingDir = testRun.td.getWorkingDir();
+ proc = Runtime.getRuntime().exec(cmdArray, null, workingDir);
+ printStream = outStream;
+ if (discardOKOutput) {
+ stream = new ByteArrayOutputStream();
+ printStream = new PrintStream(stream);
+ }
+ SlaveHarness.setLogStreams(printStream);
+ TestResultFilter filter = bindOutput(proc, printStream);
+ config.setTestTotal(testList.getTestTotal());
+ config.setTestIndex(testList.getTestNumber());
+ ObjectOutputStream os =
+ new ObjectOutputStream(proc.getOutputStream());
+ os.writeObject(config);
+ os.flush();
+// bindInput(proc);
+ proc.waitFor();
+// bindInput(null);
+ outPipe.waitTillEmpty(5000);//XXX do I need to detect timeout?
+ errPipe.waitTillEmpty(5000);
+ testResult = filter.getTestResult(testRun);
+ if (testResult == null) {
+ testResult = new TestResult(testRun,
+ false,
+ Test.TEST,
+ "Test VM terminated without "
+ + "returning test status");
+ }
+ } catch (InterruptedException e) {
+ testResult = new TestResult(testRun,
+ false,
+ Test.TEST,
+ "test process was interrupted");
+ try {
+ Class procClass = proc.getClass();
+ Field field = procClass.getDeclaredField("pid");
+ field.setAccessible(true);
+ int pid = field.getInt(proc);
+ printStream.println();
+ printStream.println("Attempting to dump threads of " +
+ "test VM process " + pid);
+ Process p = Runtime.getRuntime().exec(
+ "/usr/bin/kill -QUIT " + pid);
+ p.waitFor();
+ Thread.sleep(5000); //allow time for the thread dump to happen
+
+ SlaveTest.broadcast(new SlaveThreadDumpRequest());
+ ObjectOutputStream os =
+ new ObjectOutputStream(proc.getOutputStream());
+ os.writeObject(new MasterThreadDumpRequest());
+ os.flush();
+ Thread.sleep(5000);
+// outPipe.waitTillEmpty(10000);
+// errPipe.waitTillEmpty(10000);
+ } catch (Exception e2) {
+ printStream.println();
+ printStream.println("Attempt to dump threads of test VM failed");
+ } finally {
+ Thread.currentThread().interrupt();
+ }
+ } catch (TestException e) {
+ unexpectedException = e;
+ testResult = new TestResult(testRun,
+ false,
+ Test.TEST,
+ "runTestOtherVM failed: " + e);
+ } catch (IOException e) {
+ unexpectedException = e;
+ testResult = new TestResult(testRun,
+ false,
+ Test.TEST,
+ "runTestOtherVM failed: " + e);
+ } finally {
+ if (discardOKOutput && !testResult.state) {
+ printStream.flush(); // don't know if this is necessary
+ outStream.print(stream.toString());
+ }
+ SlaveHarness.setLogStreams(outStream);
+ if (unexpectedException != null) {
+ outStream.println("Unexpected exception:");
+ unexpectedException.printStackTrace(outStream);
+ }
+ //output time stamp
+ outStream.println();
+ StringBuffer buf = new StringBuffer();
+ (new MessageFormat("{0,time}")).
+ format(new Object[] {new Date()}, buf, null);
+ outStream.println("TIME: " + buf);
+ outStream.println();
+ if (proc != null) {
+ try {
+ proc.destroy(); // just in case, silently ignore errors
+ Thread.sleep(5000); // give it time to die
+ int exitValue = proc.exitValue();
+ outStream.println("Test process was destroyed "
+ + "and returned code " + exitValue);
+ } catch (IllegalThreadStateException itse) {
+ outStream.println("Test process was destroyed "
+ + "but has not yet terminated");
+ itse.printStackTrace();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+ return testResult;
+ }
+
+ /**
+ * Attach <code>System.in</code> to the processes output stream.
+ * This is an optional feature which can be turned on if a test
+ * requires keyboard input.
+ *
+ * @param proc the process to attach <code>stdin</code> to.
+ * <code>proc</code> may be <code>null</code>, in which case
+ * any input received by <code>System.in</code> is discarded.
+ *
+ */
+ private void bindInput(Process proc) {
+ if (doInputBind) {
+ if (forwarder == null) {
+ forwarder = new InputForwarder();
+ Thread forwarderThread =
+ new Thread(forwarder, "Forwarder");
+ forwarderThread.start();
+ }
+ if (proc != null) {
+ forwarder.setOutputStream(proc.getOutputStream());
+ } else {
+ forwarder.setOutputStream(null);
+ }
+ }
+ }
+
+ /**
+ * Utility class to forward bytes from <code>System.in</code> to
+ * an output stream. An instance of this class is designed to be reused for
+ * different output streams.
+ */
+ private class InputForwarder implements Runnable {
+
+ OutputStream out;
+ final Object outLock = new Object();
+
+ /**
+ * Set the output stream to forward bytes to.
+ *
+ * @param out the output stream, which may be <code>null</code>
+ */
+ public void setOutputStream(OutputStream out) {
+ synchronized (outLock) {
+ this.out = out;
+ }
+ }
+
+ /**
+ * Forward characters from <code>System.in</code> to the processes input
+ * stream. Since the read may not be interruptible, there is no reliable
+ * way to terminate this thread when a test VM exits. Therefore the
+ * output stream will change over time via calls to
+ * <code>setOutputStream</code>.
+ */
+ public void run() {
+ boolean doit = true;
+ while (doit) {
+ try {
+ int charval = System.in.read();
+ synchronized (outLock) {
+ if (charval == -1) {
+ doit = false;
+ } else if (out != null) {
+ out.write(charval);
+ out.flush();
+ }
+ }
+ } catch (IOException e) {
+ outStream.println("I/O exception in forwarder:");
+ e.printStackTrace(outStream);
+ }
+ }
+ }
+ }
+
+ /**
+ * Attach stdout and stderr to the subprocess.
+ * The <code>TestResultFilter</code> used to filter the
+ * <code>System.err</code> stream is returned so that
+ * the status encoded in the last line can be retrieved.
+ *
[... 382 lines stripped ...]