/* * ALMA - Atacama Large Millimiter Array * (c) European Southern Observatory, 2002 * Copyright by ESO (in the framework of the ALMA collaboration), * All rights reserved * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, * MA 02111-1307 USA */ package alma.acs.testsupport.tat; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import junit.runner.Version; import org.junit.internal.JUnitSystem; import org.junit.internal.TextListener; import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.RunListener; /** * Replacement for a standard JUnit runner such as JUnit3's <code>junit.textui.TestRunner</code>, * or JUnit4's {@link JUnitCore}, which helps avoid some issues with using JUnit embedded in ALMA TAT scripts. * <p> * Note that the intent is not to get rid of the output-based testing that TAT offers, * but to complement it with result-based testing as JUnit proposes it, * and to make the latter easier in a TAT environment. * Both approaches have their merit, so chose one depending on what you want to test. * <p> * TATJUnitRunner internally calls {@link JUnitCore#run(Class...)}, controlling the {@link RunListener} output * as well as <code>System.out</code> and <code>System.err</code>. * <p> * This class fixes the following issues * <ol> * <li>If the tests succeed, <code>JUnitCore</code> produces too much output, * leading to unnecessary maintenance work.<br> * <code>TATJUnitRunner</code> produces only very simple fixed output for successful tests, * which means it suppresses * <ul> * <li><code>JUnitCore</code> output * <li>any other output to <code>System.out</code> * <li>any other output to <code>System.err</code> * </ul> * Instead, it prints 2 fixed lines, as required by TAT / NRI for gathering test statistics. * Both <code>JUnitCore</code> and <code>TATJUnitRunner</code> * return an exit code <code>0</code> to indicate a successful run. * <li>If a test fails (JUnit3: either "failure" or "error"), * <code>TestRunner</code> reports this in the execution summary, * without giving enough details on the problem, e.g. the assertion * that caused the failure.<br> * <code>TATJUnitRunner</code> in this case becomes rather verbose * and dumps on <code>System.err</code> * <ul> * <li>the detailed test runner output. * <li>any output to <code>System.out</code> during test execution * <li>any output to <code>System.err</code> during test execution * </ul> * Both <code>JUnitCore</code> and <code>TATJUnitRunner</code> * return an exit code <code>1</code> for a failure. * </ol> * For either failure or success, the following line is printed as the first line to stdout, * so that TAT or other tools can analyze how many tests have run, and how many succeeded: <br> * <code>TEST_RUNNER_REPORT success/total: int/int</code> <br> * where <code>int</code> is replaced by the respective integer number. * * @author hsommer */ public class TATJUnitRunner { private PrintStream oldSysOut; private PrintStream oldSysErr; private File sysOutFile; private PrintStream newSysOut; private File sysErrFile; private PrintStream newSysErr; private File resultFile; private PrintStream resultOut; private JUnitCore delegate = new JUnitCore(); /** * true if the class under test has the {@link KeepTestOutput} annotation. */ private boolean keepTestOutput = false; private void createStreams(Class<?> testClass) throws IOException { sysOutFile = createTmpFile("sysout", testClass); newSysOut = new PrintStream(new FileOutputStream(sysOutFile, true)); sysErrFile = createTmpFile("syserr", testClass); newSysErr = new PrintStream(new FileOutputStream(sysErrFile, true)); resultFile = createTmpFile("result", testClass); resultOut = new PrintStream(new FileOutputStream(resultFile, true)); } private void redirectSysStreams() throws IOException { oldSysOut = System.out; System.setOut(newSysOut); oldSysErr = System.err; System.setErr(newSysErr); } private void restoreSysStreams() { System.setOut(oldSysOut); System.setErr(oldSysErr); } private void closeStreams() { newSysOut.close(); newSysErr.close(); resultOut.close(); } /** * Dumps the content of the specified file to System.err and, if all goes well, deletes that file. * @param source * @param sectionHeader */ private void dumpAndDeleteCapturedFile(File source, String sectionHeader) { byte buffer[] = new byte[2048]; int status = -1; boolean error = false; try { FileInputStream sourceStream = new FileInputStream(source); System.err.println(sectionHeader); System.err.println("<<<<<<<<<<<<<<<<<<<"); do { try { status = sourceStream.read(buffer); } catch (IOException ex) { error = true; System.err.print("Error reading file: " + ex.getMessage()); } if (status > 0) System.err.write(buffer, 0, status); } while (status > 0); System.err.println(">>>>>>>>>>>>>>>>>>>"); System.err.println(); try { sourceStream.close(); } catch (IOException ex) { error = true; System.err.print("Couldn't close file: " + ex.getMessage()); } } catch (FileNotFoundException ex) { error = true; System.err.print("Error opening file: " + ex.getMessage()); } if (!error && !keepTestOutput) { source.delete(); } } /** * Modified version of {@link JUnitCore#runMain(JUnitSystem, String...)}. * We restrict ourselves to running only one test class (or suite) at a time. * This method is not thread-safe! * @param testClass * @return the result * @throws IOException */ public Result runMain(Class<?> testClass) throws IOException { Result result = null; try { keepTestOutput = testClass.isAnnotationPresent(KeepTestOutput.class); createStreams(testClass); redirectSysStreams(); JUnitSystem system = new JUnitSystem() { public void exit(int code) { System.exit(code); } public PrintStream out() { return resultOut; } }; system.out().println("JUnit version " + Version.id()); RunListener listener= new TextListener(system); delegate.addListener(listener); // or should we listen directly? result = delegate.run(testClass); } finally { restoreSysStreams(); closeStreams(); } // in either case print a summary which automated tools can analyze (SPR ALMASW2005121) int total = result.getRunCount(); int success = total - result.getFailureCount(); String runnerReport = "TEST_RUNNER_REPORT success/total: " + success + "/" + total; System.out.println(runnerReport); if (result.wasSuccessful()) { System.out.println("JUnit test run succeeded"); if (!keepTestOutput) { sysOutFile.delete(); sysErrFile.delete(); resultFile.delete(); } } else { System.err.println("JUnitRunner: Errors and/or failures during test execution!\n"); // Dump the JUnit test runner output, the captured stdout, and the captured stderr of the text execution to System.err. dumpAndDeleteCapturedFile(resultFile, "Test execution trace:"); dumpAndDeleteCapturedFile(sysOutFile, "System.out during test execution:"); dumpAndDeleteCapturedFile(sysErrFile, "System.err during test execution:"); } return result; } /** * @param prefix One of stdout, stderr, result * @param testClass * @return * @throws IOException */ private File createTmpFile(String prefix, Class<?> testClass) throws IOException { File tmpFile = File.createTempFile(prefix + "-" + testClass.getSimpleName() + "-", ".log", new File(System.getProperty("user.dir"))); return tmpFile; } /** * @throws FileNotFoundException Hack, even other IOException will be wrapped by FileNotFoundException just for temporary backward compatibility. * @deprecated This method is used for compatibility with the old JUnit-3 based version of this class. It will be removed after ACS 11.2. */ public static void run(Class testClass) throws FileNotFoundException { TATJUnitRunner inst = new TATJUnitRunner(); try { inst.runMain(testClass); } catch (IOException ex) { ex.printStackTrace(); throw new FileNotFoundException(ex.toString()); } } public static void main(String[] args) { Result result = null; try { if (args.length < 1 || args[0] == null || args[0].length() == 0) { System.err.println("usage: JUnit4Runner testclassname"); } else { TATJUnitRunner inst = new TATJUnitRunner(); String testClassName = args[0]; Class<?> testClass = Class.forName(testClassName); // or should we first redirect the streams, in case of static initializers? result = inst.runMain(testClass); } } catch (Throwable thr) { thr.printStackTrace(); System.exit(1); } if (result.wasSuccessful()) { System.exit(0); } else { System.exit(1); } } /** * Test classes can use this annotation to cause TATJUnitRunner to keep the * tmp files with the stdout, stderr and test output. * This has no influence on what output TAT sees, but allows checking the test output * which is normally not available any more after the test succeeds. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public static @interface KeepTestOutput { } }