/* * * 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 org.apache.qpid.test.framework.listeners; import junit.framework.AssertionFailedError; import junit.framework.Test; import junit.framework.TestCase; import org.apache.log4j.Logger; import org.apache.qpid.junit.extensions.ShutdownHookable; import org.apache.qpid.junit.extensions.listeners.TKTestListener; import java.io.IOException; import java.io.PrintWriter; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; /** * Listens for test results for a named test and outputs these in the standard JUnit XML format to the specified * writer. * *

The API for this listener accepts notifications about different aspects of a tests results through different * methods, so some assumption needs to be made as to which test result a notification refers to. For example * {@link #startTest} will be called, then possibly {@link #timing} will be called, even though the test instance is * passed in both cases, it is not enough to distinguish a particular run of the test, as the test case instance may * be being shared between multiple threads, or being run a repeated number of times, and can therfore be re-used * between calls. The listeners make the assumption that, for every test, a unique thread will call {@link #startTest} * and {@link #endTest} to delimit each test. All calls to set test parameters, timings, state and so on, will occur * between the start and end and will be given with the same thread id as the start and end, so the thread id provides * a unqiue value to identify a particular test run against. * *

*
CRC Card
Responsibilities Collaborations *
Listen to test lifecycle notifications. *
Listen to test errors and failures. *
Listen to test timings. *
Listen to test memory usages. *
Listen to parameterized test parameters. *
Responsibilities *
* * @todo Merge this class with CSV test listener, making the collection of results common to both, and only factoring * out the results printing code into sub-classes. Provide a simple XML results formatter with the same format as * the ant XML formatter, and a more structured one for outputing results with timings and summaries from * performance tests. */ public class XMLTestListener implements TKTestListener, ShutdownHookable { /** Used for debugging. */ private static final Logger log = Logger.getLogger(XMLTestListener.class); /** The results file writer. */ protected Writer writer; /** Holds the results for individual tests. */ // protected Map results = new LinkedHashMap(); // protected List results = new ArrayList(); /** * Map for holding results on a per thread basis as they come in. A ThreadLocal is not used as sometimes an * explicit thread id must be used, where notifications come from different threads than the ones that called * the test method. */ Map threadLocalResults = Collections.synchronizedMap(new LinkedHashMap()); /** * Holds results for tests that have ended. Transferring these results here from the per-thread results map, means * that the thread id is freed for the thread to generate more results. */ List results = new ArrayList(); /** Holds the overall error count. */ protected int errors = 0; /** Holds the overall failure count. */ protected int failures = 0; /** Holds the overall tests run count. */ protected int runs = 0; /** Holds the name of the class that tests are being run for. */ String testClassName; /** * Creates a new XML results output listener that writes to the specified location. * * @param writer The location to write results to. * @param testClassName The name of the test class to include in the test results. */ public XMLTestListener(Writer writer, String testClassName) { log.debug("public XMLTestListener(Writer writer, String testClassName = " + testClassName + "): called"); this.writer = writer; this.testClassName = testClassName; } /** * Resets the test results to the default state of time zero, memory usage zero, parameter zero, test passed. * * @param test The test to resest any results for. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void reset(Test test, Long threadId) { log.debug("public void reset(Test test = " + test + ", Long threadId = " + threadId + "): called"); XMLTestListener.Result r = (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); r.error = null; r.failure = null; } /** * Notification that a test started. * * @param test The test that started. */ public void startTest(Test test) { log.debug("public void startTest(Test test = " + test + "): called"); Result newResult = new Result(test.getClass().getName(), ((TestCase) test).getName()); // Initialize the thread local test results. threadLocalResults.put(Thread.currentThread().getId(), newResult); runs++; } /** * Should be called every time a test completes with the run time of that test. * * @param test The name of the test. * @param nanos The run time of the test in nanoseconds. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void timing(Test test, long nanos, Long threadId) { } /** * Optionally called every time a test completes with the second timing test. * * @param test The name of the test. * @param nanos The second timing information of the test in nanoseconds. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void timing2(Test test, Long nanos, Long threadId) { } /** * Should be called every time a test completed with the amount of memory used before and after the test was run. * * @param test The test which memory was measured for. * @param memStart The total JVM memory used before the test was run. * @param memEnd The total JVM memory used after the test was run. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void memoryUsed(Test test, long memStart, long memEnd, Long threadId) { } /** * Should be called every time a parameterized test completed with the int value of its test parameter. * * @param test The test which memory was measured for. * @param parameter The int parameter value. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void parameterValue(Test test, int parameter, Long threadId) { } /** * Should be called every time a test completes with the current number of test threads running. * * @param test The test for which the measurement is being generated. * @param threads The number of tests being run concurrently. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void concurrencyLevel(Test test, int threads, Long threadId) { } /** * Notifies listeners of the tests read/set properties. * * @param properties The tests read/set properties. */ public void properties(Properties properties) { } /** * Notification that a test ended. * * @param test The test that ended. */ public void endTest(Test test) { log.debug("public void endTest(Test test = " + test + "): called"); // Move complete test results into the completed tests list. Result r = threadLocalResults.get(Thread.currentThread().getId()); results.add(r); // Clear all the test results for the thread. threadLocalResults.remove(Thread.currentThread().getId()); } /** * Called when a test completes. Success, failure and errors. This method should be used when registering an * end test from a different thread than the one that started the test. * * @param test The test which completed. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void endTest(Test test, Long threadId) { log.debug("public void endTest(Test test = " + test + ", Long threadId = " + threadId + "): called"); // Move complete test results into the completed tests list. Result r = (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); results.add(r); // Clear all the test results for the thread. threadLocalResults.remove(Thread.currentThread().getId()); } /** * An error occurred. * * @param test The test in which the error occurred. * @param t The throwable that resulted from the error. */ public void addError(Test test, Throwable t) { log.debug("public void addError(Test test = " + test + ", Throwable t = " + t + "): called"); Result r = threadLocalResults.get(Thread.currentThread().getId()); r.error = t; errors++; } /** * A failure occurred. * * @param test The test in which the failure occurred. * @param t The JUnit assertions that led to the failure. */ public void addFailure(Test test, AssertionFailedError t) { log.debug("public void addFailure(Test test = " + test + ", AssertionFailedError t = " + t + "): called"); Result r = threadLocalResults.get(Thread.currentThread().getId()); r.failure = t; failures++; } /** * Called when a test completes to mark it as a test fail. This method should be used when registering a * failure from a different thread than the one that started the test. * * @param test The test which failed. * @param e The assertion that failed the test. * @param threadId Optional thread id if not calling from thread that started the test method. May be null. */ public void addFailure(Test test, AssertionFailedError e, Long threadId) { log.debug("public void addFailure(Test test, AssertionFailedError e, Long threadId): called"); Result r = (threadId == null) ? threadLocalResults.get(Thread.currentThread().getId()) : threadLocalResults.get(threadId); r.failure = e; failures++; } /** * Notifies listeners of the start of a complete run of tests. */ public void startBatch() { log.debug("public void startBatch(): called"); // Reset all results counts. threadLocalResults = Collections.synchronizedMap(new HashMap()); errors = 0; failures = 0; runs = 0; // Write out the file header. try { writer.write("\n"); } catch (IOException e) { throw new RuntimeException("Unable to write the test results.", e); } } /** * Notifies listeners of the end of a complete run of tests. * * @param parameters The optional test parameters to log out with the batch results. */ public void endBatch(Properties parameters) { log.debug("public void endBatch(Properties parameters = " + parameters + "): called"); // Write out the results. try { // writer.write("\n"); writer.write("\n"); for (Result result : results) { writer.write(" \n"); if (result.error != null) { writer.write(" "); result.error.printStackTrace(new PrintWriter(writer)); writer.write(" "); } else if (result.failure != null) { writer.write(" "); result.failure.printStackTrace(new PrintWriter(writer)); writer.write(" "); } writer.write(" \n"); } writer.write("\n"); writer.flush(); } catch (IOException e) { throw new RuntimeException("Unable to write the test results.", e); } } /** * Supplies the shutdown hook. * * @return The shut down hook. */ public Thread getShutdownHook() { return new Thread(new Runnable() { public void run() { log.debug("XMLTestListener::ShutdownHook: called"); } }); } /** * Used to capture the results of a particular test run. */ protected static class Result { /** Holds the name of the test class. */ public String testClass; /** Holds the name of the test method. */ public String testName; /** Holds the exception that caused error in this test. */ public Throwable error; /** Holds the assertion exception that caused failure in this test. */ public AssertionFailedError failure; /** * Creates a placeholder for the results of a test. * * @param testClass The test class. * @param testName The name of the test that was run. */ public Result(String testClass, String testName) { this.testClass = testClass; this.testName = testName; } } }