Ant JUnit Nice Output Formatter

I’ve seached for nice Apache Ant formatter which do not writes full stack trace in console/file and just writes what methods are passed, what methods are failed, and if failed - where and why they failed (and only trace about methods from test class). I haven’t found any. So I’ve written one.

For example:

[junit] ----------------------------------------------------------
[junit] Testsuite: com.undefined.MyTest
[junit] Ran [0.322] testMethodOne ... OK
[junit] Ran [0.023] testMethodOne... OK
[junit] Ran [0.333] testMethodTwo ... FAILED
[junit] Ran [0.343] testMethodThree ... FAILED
[junit]
[junit] Testcase: testMethodTwo(com.undefined.MyTest):	FAILED
[junit]     (AssertionFailedError): expected:<Bender> but was:<null>
[junit]     (MyTest) assertEqualsInfo: 887
[junit]     (MyTest) testGetUserInfoByUserId: 188

There is ant task for this:


<target name="easy-test" depends="test-clean, compile">
    <mkdir dir="out/junit"/>

    <junit printsummary="off" fork="on"
           failureproperty="test.failed" showoutput="off" dir="out"
           outputtoformatters="false" filtertrace="on" >

        <classpath>

            ....

        </classpath>

        <formatter classname="com.undefined.testing.OneLinerFormatter" usefile="false" />

        <batchtest fork="on" todir="out/junit" filtertrace="on">
            <fileset dir="src/java/test">
                <include name="**/*Test.java"/>
            </fileset>
        </batchtest>

    </junit>

    <fail if="test.failed">tests.failed=${test.failed}</fail>

</target>

And finally, the implementation:


package com.undefined.testing;

import java.io.BufferedReader;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.NumberFormat;
import java.util.Hashtable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import junit.framework.AssertionFailedError;
import junit.framework.Test;

import org.apache.tools.ant.taskdefs.optional.junit.JUnitResultFormatter;
import org.apache.tools.ant.taskdefs.optional.junit.JUnitTest;
import org.apache.tools.ant.taskdefs.optional.junit.JUnitVersionHelper;

import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.StringUtils;

/**
 * Prints a single lines of tests to a specified Writer.
 * Inspired by the BriefJUnitResultFormatter and
 * XMLJUnitResultFormatter.
 *
 * @see FormatterElement
 * @see BriefJUnitResultFormatter
 * @see XMLJUnitResultFormatter
 */

public class OneLinerFormatter implements JUnitResultFormatter {

    private final String TAB_STR = "    ";

    private final boolean showCausesLines = true;
    // (\w+\.)+(\w+)\((\w+).(?:\w+):(\d+)\)
    private final Pattern traceLinePattern = Pattern.compile("(\\w+\\.)+(\\w+)\\((\\w+).(?:\\w+):(\\d+)\\)");

    /**
     * Where to write the log to.
     */
    private OutputStream out;

    /**
     * Used for writing the results.
     */
    private PrintWriter output;

    /**
     * Used as part of formatting the results.
     */
    private StringWriter results;

    /**
     * Used for writing formatted results to.
     */
    private PrintWriter resultWriter;

    /**
     * Formatter for timings.
     */
    private NumberFormat numberFormat = NumberFormat.getInstance();

    /**
     * Output suite has written to System.out
     */
    private String systemOutput = null;

    /**
     * Output suite has written to System.err
     */
    private String systemError = null;

    /**
     * tests that failed.
     */
    private Hashtable failedTests = new Hashtable();
    /**
     * Timing helper.
     */
    private Hashtable testStarts = new Hashtable();

    /**
     * Constructor for OneLinerFormatter.
     */
    public OneLinerFormatter() {
        results = new StringWriter();
        resultWriter = new PrintWriter(results);
    }

    /**
     * Sets the stream the formatter is supposed to write its results to.
     * @param out the output stream to write to
     */
    public void setOutput(OutputStream out) {
        this.out = out;
        output = new PrintWriter(out);
    }

    /**
     * @see JUnitResultFormatter#setSystemOutput(String)
     */
    public void setSystemOutput(String out) {
        systemOutput = out;
    }

    /**
     * @see JUnitResultFormatter#setSystemError(String)
     */
    public void setSystemError(String err) {
        systemError = err;
    }

    /**
     * The whole testsuite started.
     * @param suite the test suite
     */
    public void startTestSuite(JUnitTest suite) {
        if (output == null) {
            return; // Quick return - no output do nothing.
        }
        StringBuffer sb = new StringBuffer(StringUtils.LINE_SEP);
        sb.append("----------------------------------------------------------");
        sb.append(StringUtils.LINE_SEP);
        sb.append("Testsuite: ");
        sb.append(suite.getName());
        sb.append(StringUtils.LINE_SEP);
        output.write(sb.toString());
        output.flush();
    }

    /**
     * The whole testsuite ended.
     * @param suite the test suite
     */
    public void endTestSuite(JUnitTest suite) {
        StringBuffer sb = new StringBuffer("Tests run: ");
        sb.append(suite.runCount());
        sb.append(", Failures: ");
        sb.append(suite.failureCount());
        sb.append(", Errors: ");
        sb.append(suite.errorCount());
        sb.append(", Time elapsed: ");
        sb.append(numberFormat.format(suite.getRunTime() / 1000.0));
        sb.append(" sec");
        sb.append(StringUtils.LINE_SEP);
        sb.append(StringUtils.LINE_SEP);

        // append the err and output streams to the log
        if (systemOutput != null && systemOutput.length() > 0) {
            sb.append("------------- Standard Output ---------------")
              .append(StringUtils.LINE_SEP)
              .append(systemOutput)
              .append("------------- ---------------- ---------------")
              .append(StringUtils.LINE_SEP);
        }

        if (systemError != null && systemError.length() > 0) {
            sb.append("------------- Standard Error -----------------")
              .append(StringUtils.LINE_SEP)
              .append(systemError)
              .append("------------- ---------------- ---------------")
              .append(StringUtils.LINE_SEP);
        }

        if (output != null) {
            try {
                output.write(sb.toString());
                resultWriter.close();
                output.write(results.toString());
                output.flush();
            } finally {
                if (out != System.out && out != System.err) {
                    FileUtils.close(out);
                }
            }
        }
    }

    /**
     * A test started.
     * @param test a test
     */
    public void startTest(Test test) {
        testStarts.put(test, new Long(System.currentTimeMillis()));
    }

    /**
     * A test ended.
     * @param test a test
     */
    public void endTest(Test test) {
        // Fix for bug #5637 - if a junit.extensions.TestSetup is
        // used and throws an exception during setUp then startTest
        // would never have been called
        if (!testStarts.containsKey(test)) {
            startTest(test);
        }

        boolean failed = failedTests.containsKey(test);

        Long l = (Long) testStarts.get(test);

        output.write("Ran [");
        output.write(((System.currentTimeMillis() - l.longValue()) / 1000.0) + "] ");
        output.write(getTestName(test) + " ... " + (failed ? "FAILED" : "OK"));
	    output.write(StringUtils.LINE_SEP);
	    output.flush();
    }

    /**
     * Interface TestListener for JUnit >= 3.4.
     *
     * <p>A Test failed.
     * @param test a test
     * @param t    the exception thrown by the test
     */
    public void addFailure(Test test, Throwable t) {
        formatError("\tFAILED", test, t);
    }

    /**
     * Interface TestListener for JUnit &gt; 3.4.
     *
     * <p>A Test failed.
     * @param test a test
     * @param t    the assertion failed by the test
     */
    public void addFailure(Test test, AssertionFailedError t) {
        addFailure(test, (Throwable) t);
    }

    /**
     * A test caused an error.
     * @param test  a test
     * @param error the error thrown by the test
     */
    public void addError(Test test, Throwable error) {
        formatError("\tCaused an ERROR", test, error);
    }

    /**
     * Get test name
     *
     * @param test a test
     * @return test name
     */
    protected String getTestName(Test test) {
        if (test == null) {
            return "null";
        } else {
            return /* JUnitVersionHelper.getTestCaseClassName(test) + ": " + */
                      JUnitVersionHelper.getTestCaseName(test);
        }
    }

    /**
     * Get test case full class name
     *
     * @param test a test
     * @return test full class name
     */
    protected String getTestCaseClassName(Test test) {
        if (test == null) {
            return "null";
        } else {
            return JUnitVersionHelper.getTestCaseClassName(test);
        }
    }

    /**
     * Format the test for printing..
     * @param test a test
     * @return the formatted testname
     */
    protected String formatTest(Test test) {
        if (test == null) {
            return "Null Test: ";
        } else {
            return "Testcase: " + test.toString() + ":";
        }
    }

    /**
     * Format an error and print it.
     * @param type the type of error
     * @param test the test that failed
     * @param error the exception that the test threw
     */
    protected synchronized void formatError(String type, Test test,
                                            Throwable error) {
        if (test != null) {
        	failedTests.put(test, test);
            endTest(test);
        }

        resultWriter.println(formatTest(test) + type);
        resultWriter.println(TAB_STR + "(" + error.getClass().getSimpleName() + "): " +
                    ((error.getMessage() != null) ? error.getMessage() : error));

        if (showCausesLines) {
            // resultWriter.append(StringUtils.LINE_SEP);
            resultWriter.println(filterErrorTrace(test, error));
        }

        resultWriter.println();

        /* String strace = JUnitTestRunner.getFilteredTrace(error);
           resultWriter.println(strace);
           resultWriter.println(); */
    }

    protected String filterErrorTrace(Test test, Throwable error) {
        String trace = StringUtils.getStackTrace(error);
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        StringReader sr = new StringReader(trace);
        BufferedReader br = new BufferedReader(sr);

        String line;
        try {
            while ((line = br.readLine()) != null) {
                if (line.indexOf(getTestCaseClassName(test)) != -1) {
                    Matcher matcher = traceLinePattern.matcher(line);
                    // pw.println(matcher + ": " + matcher.find());
                    if (matcher.find()) {
                        pw.print(TAB_STR);
                        pw.print("(" + matcher.group(3) + ")");
                        pw.print(matcher.group(2) + ": ");
                        pw.println(matcher.group(4));
                    } else {
                        pw.println(line);
                    }
                }
            }
        } catch (Exception e) {
            return trace; // return the trace unfiltered
        }

        return sw.toString();

    }

}
#ant #java #junit