blob: dc80abffed266bde49510100e02ae2ed7fefdcc3 [file] [log] [blame]
/*
* 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.tools.ant.taskdefs.optional.junit;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.DataType;
import org.apache.tools.ant.util.FileUtils;
/**
* <p>Collects all failing test <i>cases</i> and creates a new JUnit test class containing
* a suite() method which calls these failed tests.</p>
* <p>Having classes <i>A</i> ... <i>D</i> with each several testcases you could earn a new
* test class like
* <pre>
* // generated on: 2007.08.06 09:42:34,555
* import junit.framework.*;
* public class FailedTests extends TestCase {
* public FailedTests(String testname) {
* super(testname);
* }
* public static Test suite() {
* TestSuite suite = new TestSuite();
* suite.addTest( new B("test04") );
* suite.addTest( new org.D("test10") );
* return suite;
* }
* }
* </pre>
*
* Because each running test case gets its own formatter, we collect
* the failing test cases in a static list. Because we dont have a finalizer
* method in the formatters "lifecycle", we register this formatter as
* BuildListener and generate the new java source on taskFinished event.
*
* @since Ant 1.8.0
*/
public class FailureRecorder extends DataType implements JUnitResultFormatter, BuildListener {
/**
* This is the name of a magic System property ({@value}). The value of this
* <b>System</b> property should point to the location where to store the
* generated class (without suffix).
* Default location and name is defined in DEFAULT_CLASS_LOCATION.
* @see #DEFAULT_CLASS_LOCATION
*/
public static final String MAGIC_PROPERTY_CLASS_LOCATION
= "ant.junit.failureCollector";
/** Default location and name for the generated JUnit class file. {@value} */
public static final String DEFAULT_CLASS_LOCATION
= System.getProperty("java.io.tmpdir") + "FailedTests";
/** Prefix for logging. {@value} */
private static final String LOG_PREFIX = " [junit]";
/** Class names of failed tests without duplicates. */
private static SortedSet/*<TestInfos>*/ failedTests = new TreeSet();
/** A writer for writing the generated source to. */
private PrintWriter writer;
/**
* Location and name of the generated JUnit class.
* Lazy instantiated via getLocationName().
*/
private static String locationName;
/**
* Returns the (lazy evaluated) location for the collector class.
* Order for evaluation: System property > Ant property > default value
* @return location for the collector class
* @see #MAGIC_PROPERTY_CLASS_LOCATION
* @see #DEFAULT_CLASS_LOCATION
*/
private String getLocationName() {
if (locationName == null) {
String syspropValue = System.getProperty(MAGIC_PROPERTY_CLASS_LOCATION);
String antpropValue = getProject().getProperty(MAGIC_PROPERTY_CLASS_LOCATION);
if (syspropValue != null) {
locationName = syspropValue;
verbose("System property '" + MAGIC_PROPERTY_CLASS_LOCATION + "' set, so use "
+ "its value '" + syspropValue + "' as location for collector class.");
} else if (antpropValue != null) {
locationName = antpropValue;
verbose("Ant property '" + MAGIC_PROPERTY_CLASS_LOCATION + "' set, so use "
+ "its value '" + antpropValue + "' as location for collector class.");
} else {
locationName = DEFAULT_CLASS_LOCATION;
verbose("System property '" + MAGIC_PROPERTY_CLASS_LOCATION + "' not set, so use "
+ "value as location for collector class: '"
+ DEFAULT_CLASS_LOCATION + "'");
}
File locationFile = new File(locationName);
if (!locationFile.isAbsolute()) {
File f = new File(getProject().getBaseDir(), locationName);
locationName = f.getAbsolutePath();
verbose("Location file is relative (" + locationFile + ")"
+ " use absolute path instead (" + locationName + ")");
}
}
return locationName;
}
/**
* This method is called by the Ant runtime by reflection. We use the project reference for
* registration of this class as BuildListener.
*
* @param project
* project reference
*/
public void setProject(Project project) {
// store project reference for logging
super.setProject(project);
// check if already registered
boolean alreadyRegistered = false;
Vector allListeners = project.getBuildListeners();
for (int i = 0; i < allListeners.size(); i++) {
Object listener = allListeners.get(i);
if (listener instanceof FailureRecorder) {
alreadyRegistered = true;
continue;
}
}
// register if needed
if (!alreadyRegistered) {
verbose("Register FailureRecorder (@" + this.hashCode() + ") as BuildListener");
project.addBuildListener(this);
}
}
// ===== JUnitResultFormatter =====
/**
* Not used
* {@inheritDoc}
*/
public void endTestSuite(JUnitTest suite) throws BuildException {
}
/**
* Add the failed test to the list.
* @param test the test that errored.
* @param throwable the reason it errored.
* @see junit.framework.TestListener#addError(junit.framework.Test, java.lang.Throwable)
*/
public void addError(Test test, Throwable throwable) {
failedTests.add(new TestInfos(test));
}
// CheckStyle:LineLengthCheck OFF - @see is long
/**
* Add the failed test to the list.
* @param test the test that failed.
* @param error the assertion that failed.
* @see junit.framework.TestListener#addFailure(junit.framework.Test, junit.framework.AssertionFailedError)
*/
// CheckStyle:LineLengthCheck ON
public void addFailure(Test test, AssertionFailedError error) {
failedTests.add(new TestInfos(test));
}
/**
* Not used
* {@inheritDoc}
*/
public void setOutput(OutputStream out) {
}
/**
* Not used
* {@inheritDoc}
*/
public void setSystemError(String err) {
}
/**
* Not used
* {@inheritDoc}
*/
public void setSystemOutput(String out) {
}
/**
* Not used
* {@inheritDoc}
*/
public void startTestSuite(JUnitTest suite) throws BuildException {
}
/**
* Not used
* {@inheritDoc}
*/
public void endTest(Test test) {
}
/**
* Not used
* {@inheritDoc}
*/
public void startTest(Test test) {
}
// ===== "Templates" for generating the JUnit class =====
private void writeJavaClass() {
try {
File sourceFile = new File((getLocationName() + ".java"));
verbose("Write collector class to '" + sourceFile.getAbsolutePath() + "'");
sourceFile.delete();
writer = new PrintWriter(new FileOutputStream(sourceFile));
createClassHeader();
createSuiteMethod();
createClassFooter();
FileUtils.close(writer);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
private void createClassHeader() {
String className = getLocationName().replace('\\', '/');
if (className.indexOf('/') > -1) {
className = className.substring(className.lastIndexOf('/') + 1);
}
SimpleDateFormat sdf = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss,SSS");
writer.print("// generated on: ");
writer.println(sdf.format(new Date()));
writer.println("import junit.framework.*;");
writer.print("public class ");
writer.print(className);
// If this class does not extend TC, Ant doesnt run these
writer.println(" extends TestCase {");
// standard String-constructor
writer.print(" public ");
writer.print(className);
writer.println("(String testname) {");
writer.println(" super(testname);");
writer.println(" }");
}
private void createSuiteMethod() {
writer.println(" public static Test suite() {");
writer.println(" TestSuite suite = new TestSuite();");
for (Iterator iter = failedTests.iterator(); iter.hasNext();) {
TestInfos testInfos = (TestInfos) iter.next();
writer.print(" suite.addTest(");
writer.print(testInfos);
writer.println(");");
}
writer.println(" return suite;");
writer.println(" }");
}
private void createClassFooter() {
writer.println("}");
}
// ===== Helper classes and methods =====
/**
* Logging facade in INFO-mode.
* @param message Log-message
*/
public void log(String message) {
getProject().log(LOG_PREFIX + " " + message, Project.MSG_INFO);
}
/**
* Logging facade in VERBOSE-mode.
* @param message Log-message
*/
public void verbose(String message) {
getProject().log(LOG_PREFIX + " " + message, Project.MSG_VERBOSE);
}
/**
* TestInfos holds information about a given test for later use.
*/
public class TestInfos implements Comparable {
/** The class name of the test. */
private String className;
/** The method name of the testcase. */
private String methodName;
/**
* This constructor extracts the needed information from the given test.
* @param test Test to analyze
*/
public TestInfos(Test test) {
className = test.getClass().getName();
methodName = test.toString();
methodName = methodName.substring(0, methodName.indexOf('('));
}
/**
* This String-Representation can directly be used for instantiation of
* the JUnit testcase.
* @return the string representation.
* @see java.lang.Object#toString()
* @see FailureRecorder#createSuiteMethod()
*/
public String toString() {
return "new " + className + "(\"" + methodName + "\")";
}
/**
* The SortedMap needs comparable elements.
* @param other the object to compare to.
* @return the result of the comparison.
* @see java.lang.Comparable#compareTo(T)
* @see SortedSet#comparator()
*/
public int compareTo(Object other) {
if (other instanceof TestInfos) {
TestInfos otherInfos = (TestInfos) other;
return toString().compareTo(otherInfos.toString());
} else {
return -1;
}
}
}
// ===== BuildListener =====
/**
* Not used
* {@inheritDoc}
*/
public void buildFinished(BuildEvent event) {
}
/**
* Not used
* {@inheritDoc}
*/
public void buildStarted(BuildEvent event) {
}
/**
* Not used
* {@inheritDoc}
*/
public void messageLogged(BuildEvent event) {
}
/**
* Not used
* {@inheritDoc}
*/
public void targetFinished(BuildEvent event) {
}
/**
* Not used
* {@inheritDoc}
*/
public void targetStarted(BuildEvent event) {
}
/**
* The task outside of this JUnitResultFormatter is the <junit> task. So all tests passed
* and we could create the new java class.
* @param event not used
* @see org.apache.tools.ant.BuildListener#taskFinished(org.apache.tools.ant.BuildEvent)
*/
public void taskFinished(BuildEvent event) {
if (!failedTests.isEmpty()) {
writeJavaClass();
}
}
/**
* Not used
* {@inheritDoc}
*/
public void taskStarted(BuildEvent event) {
}
}