Browse Source

FailureRecorder

* remove duplicate "no-op" statement (javadoc + code)
* use BuildListener for writing at the end of <junit> instead of overwriting the file all the time
* minor comment edit
* pass project reference to <junit> nested elements (eg listener)
* order methods by interfaces
* some log messages in the recorder
* can use Ant properties for setting the location

FormatterElement
* don't set the project reference if there is already one

build.xml
* use ant property instead of system property for configuring FailureRecorder


git-svn-id: https://svn.apache.org/repos/asf/ant/core/trunk@587844 13f79535-47bb-0310-9956-ffa450edef68
master
Jan Materne 17 years ago
parent
commit
4369f64826
6 changed files with 270 additions and 74 deletions
  1. +2
    -2
      build.xml
  2. +1
    -1
      docs/manual/OptionalTasks/junit.html
  3. +18
    -9
      src/etc/testcases/taskdefs/optional/junit.xml
  4. +158
    -39
      src/main/org/apache/tools/ant/taskdefs/optional/junit/FailureRecorder.java
  5. +45
    -2
      src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java
  6. +46
    -21
      src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java

+ 2
- 2
build.xml View File

@@ -1605,6 +1605,8 @@ see ${build.junit.reports} / ${antunit.reports}
<!-- run the tests -->
<mkdir dir="${build.junit.xml}" />
<property name="test.junit.vmargs" value=""/>
<property name="ant.junit.failureCollector"
value="${junit.collector.dir}/${junit.collector.class}"/>
<junit printsummary="${junit.summary}"
haltonfailure="${test.haltonfailure}"
fork="${junit.fork}"
@@ -1622,8 +1624,6 @@ see ${build.junit.reports} / ${antunit.reports}
<sysproperty key="build.compiler" value="${build.compiler}"/>
<sysproperty key="tests.and.ant.share.classloader"
value="${tests.and.ant.share.classloader}"/>
<sysproperty key="ant.junit.failureCollector"
value="${junit.collector.dir}/${junit.collector.class}"/>
<classpath>
<path refid="tests-classpath"/>
<pathelement location="${junit.collector.dir}"/>


+ 1
- 1
docs/manual/OptionalTasks/junit.html View File

@@ -357,7 +357,7 @@ documents and will be dropped.</p>
<p>The fourth formatter named <code>failure</code> (since Ant 1.8.0)
collects all failing <code>testXXX()</code>
methods and creates a new <code>TestCase</code> which delegates only these
failing methods. The name and the location can be specified via Java System property
failing methods. The name and the location can be specified via Java System property or Ant property
<code>ant.junit.failureCollector</code>. The value has to point to the directory and
the name of the resulting class (without suffix). It defaults to <i>java-tmp-dir</i>/FailedTests.</p>



+ 18
- 9
src/etc/testcases/taskdefs/optional/junit.xml View File

@@ -9,7 +9,7 @@

<target name="cleanup">
<delete file="testlog.txt"/>
<delete dir="out"/>
<delete dir="out" includeemptydirs="true" failonerror="false"/>
</target>

<target name="testForkedOutput">
@@ -154,7 +154,7 @@
public void test01() { System.out.println("A.test01"); }
public void test02() { System.out.println("A.test02"); fail(); }
public void test03() { System.out.println("A.test03"); fail(); }
}
}
</echo>
<echo file="${tmp.dir}/B.java">
import junit.framework.*;
@@ -187,16 +187,15 @@

<target name="failureRecorder.internal">
<property name="tmp.dir" value="out"/>
<!--
<delete>
<fileset dir="${tmp.dir}" includes="FailedTests*.class"/>
</delete>
-->
<!-- compile the FailedTests class if present -->
<!-- compile the FailedTests class if present -->
<javac srcdir="${tmp.dir}" destdir="${tmp.dir}"/>
<available file="${tmp.dir}/FailedTests.class" property="hasFailingTests"/>
<property name="ant.junit.failureCollector" value="${tmp.dir}/FailedTests"/>
<junit haltonerror="false" haltonfailure="false">
<sysproperty key="ant.junit.failureCollector" value="${tmp.dir}/FailedTests"/>
<classpath>
<pathelement location="${tmp.dir}"/>
</classpath>
@@ -217,7 +216,11 @@
</target>
<target name="failureRecorder.runtest">
<ant target="failureRecorder.internal" antfile="junit.xml" inheritAll="false"/>
<ant target="failureRecorder.internal"
antfile="junit.xml"
inheritAll="false"
inheritRefs="false"
/>
</target>
<target name="failureRecorder.fixing">
@@ -229,8 +232,14 @@
public void test01() { System.out.println("A.test01"); }
public void test02() { System.out.println("A.test02"); }
public void test03() { System.out.println("A.test03"); }
}
}
</echo>
</target>
<target name="copy">
<mkdir dir="c:/temp/ant-log"/>
<copy file="out/${file}" tofile="c:/temp/ant-log/${nr}-${file}" failonerror="false"/>
</target>


</project>
</project>

+ 158
- 39
src/main/org/apache/tools/ant/taskdefs/optional/junit/FailureRecorder.java View File

@@ -27,11 +27,16 @@ 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;
/**
@@ -55,14 +60,14 @@ import org.apache.tools.ant.util.FileUtils;
* }
* </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 regenerate the new java source
* at each end of a test suite. The last run will contain all failed tests.
* 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 implements JUnitResultFormatter {
public class FailureRecorder extends DataType implements JUnitResultFormatter, BuildListener {
/**
* This is the name of a magic System property ({@value}). The value of this
@@ -78,54 +83,94 @@ public class FailureRecorder implements JUnitResultFormatter {
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;
//TODO: Dont set the locationName via System.getProperty - better
// via Ant properties. But how to access these?
/**
* 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 propValue = System.getProperty(MAGIC_PROPERTY_CLASS_LOCATION);
locationName = (propValue != null) ? propValue : DEFAULT_CLASS_LOCATION;
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;
}
// CheckStyle:LineLengthCheck OFF - @see is long
/**
* After each test suite, the whole new JUnit class will be regenerated.
* @param suite the test suite
* @throws BuildException if there is a problem.
* @see org.apache.tools.ant.taskdefs.optional.junit.JUnitResultFormatter#endTestSuite(org.apache.tools.ant.taskdefs.optional.junit.JUnitTest)
* 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
*/
// CheckStyle:LineLengthCheck ON
public void endTestSuite(JUnitTest suite) throws BuildException {
if (failedTests.isEmpty()) {
return;
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;
}
}
try {
File sourceFile = new File(getLocationName() + ".java");
sourceFile.delete();
writer = new PrintWriter(new FileOutputStream(sourceFile));
createClassHeader();
createSuiteMethod();
createClassFooter();
FileUtils.close(writer);
} catch (FileNotFoundException e) {
e.printStackTrace();
// 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.
@@ -154,7 +199,6 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void setOutput(OutputStream out) {
// not in use
}
/**
@@ -162,7 +206,6 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void setSystemError(String err) {
// not in use
}
/**
@@ -170,7 +213,6 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void setSystemOutput(String out) {
// not in use
}
/**
@@ -178,7 +220,6 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void startTestSuite(JUnitTest suite) throws BuildException {
// not in use
}
/**
@@ -186,7 +227,6 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void endTest(Test test) {
// not in use
}
/**
@@ -194,10 +234,27 @@ public class FailureRecorder implements JUnitResultFormatter {
* {@inheritDoc}
*/
public void startTest(Test test) {
// not in use
}
// "Templates" for generating the JUnit class
// ===== "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('\\', '/');
@@ -212,7 +269,7 @@ public class FailureRecorder implements JUnitResultFormatter {
writer.print(className);
// If this class does not extend TC, Ant doesnt run these
writer.println(" extends TestCase {");
// no-arg constructor
// standard String-constructor
writer.print(" public ");
writer.print(className);
writer.println("(String testname) {");
@@ -237,7 +294,14 @@ public class FailureRecorder implements JUnitResultFormatter {
writer.println("}");
}
// Helper classes
// ===== Helper classes and methods =====
public void log(String message) {
getProject().log(LOG_PREFIX + " " + message, Project.MSG_INFO);
}
public void verbose(String message) {
getProject().log(LOG_PREFIX + " " + message, Project.MSG_VERBOSE);
}
/**
* TestInfos holds information about a given test for later use.
@@ -287,5 +351,60 @@ public class FailureRecorder implements JUnitResultFormatter {
}
}
}
// ===== 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.
* @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) {
}
}

+ 45
- 2
src/main/org/apache/tools/ant/taskdefs/optional/junit/FormatterElement.java View File

@@ -22,8 +22,11 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.BufferedOutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.EnumeratedAttribute;

@@ -60,6 +63,12 @@ public class FormatterElement {
private String ifProperty;
private String unlessProperty;

/**
* Store the project reference for passing it to nested components.
* @since Ant 1.8
*/
private Project project;

/** xml formatter class */
public static final String XML_FORMATTER_CLASS_NAME =
"org.apache.tools.ant.taskdefs.optional.junit.XMLJUnitResultFormatter";
@@ -223,6 +232,16 @@ public class FormatterElement {
return createFormatter(null);
}

/**
* Store the project reference for passing it to nested components.
* @param project the reference
* @since Ant 1.8
*/
public void setProject(Project project) {
this.project = project;
}
/**
* @since Ant 1.6
*/
@@ -251,7 +270,7 @@ public class FormatterElement {
"Using loader " + loader + " on class " + classname
+ ": " + e, e);
}
Object o = null;
try {
o = f.newInstance();
@@ -260,7 +279,7 @@ public class FormatterElement {
} catch (IllegalAccessException e) {
throw new BuildException(e);
}
if (!(o instanceof JUnitTaskMirror.JUnitResultFormatterMirror)) {
throw new BuildException(classname + " is not a JUnitResultFormatter");
}
@@ -274,6 +293,30 @@ public class FormatterElement {
}
}
r.setOutput(out);

boolean needToSetProjectReference = true;
try {
Field field = r.getClass().getField("project");
Object value = field.get(r);
if (value instanceof Project) {
// there is already a project reference so dont overwrite this
needToSetProjectReference = false;
}
} catch (Exception e) {
// no field present, so no previous reference exists
}
if (needToSetProjectReference) {
Method setter;
try {
setter = r.getClass().getMethod("setProject", new Class[] { Project.class });
setter.invoke(r, new Object[] { project });
} catch (Exception e) {
// no setProject to invoke; just ignore
}
}
return r;
}



+ 46
- 21
src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskTest.java View File

@@ -19,10 +19,20 @@ package org.apache.tools.ant.taskdefs.optional.junit;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;

import junit.framework.Test;
import junit.framework.TestSuite;

import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildFileTest;
import org.apache.tools.ant.BuildListener;

public class JUnitTaskTest extends BuildFileTest {

@@ -86,31 +96,41 @@ public class JUnitTaskTest extends BuildFileTest {
public void testBatchTestForkOnceExtension() {
assertResultFilesExist("testBatchTestForkOnceExtension", ".foo");
}


/* Bugzilla Report 42984 */
//TODO This scenario works from command line, but not from JUnit ...
// See the _run.bat attachement of the bug.
public void _testFailureRecorder() {
// Running these steps from the junit.xml-directory work
// $ ant -f junit.xml failureRecorder.prepare
// $ ant -f junit.xml failureRecorder.runtest
// $ ant -f junit.xml failureRecorder.runtest
// $ ant -f junit.xml failureRecorder.fixing
// $ ant -f junit.xml failureRecorder.runtest
// $ ant -f junit.xml failureRecorder.runtest
// But running the JUnit testcase fails in 4th run.
public void testFailureRecorder() {
File testDir = new File(getProjectDir(), "out");
File collectorFile = new File(getProjectDir(), "out/FailedTests.java");

// ensure that there is a clean test environment
assertFalse("Test directory must not exist before the test preparation.",
assertFalse("Test directory '" + testDir.getAbsolutePath() + "' must not exist before the test preparation.",
testDir.exists());
assertFalse("The collector file must not exist before the test preparation.",
assertFalse("The collector file '" + collectorFile.getAbsolutePath() + "'must not exist before the test preparation.",
collectorFile.exists());

// prepare the test environment
executeTarget("failureRecorder.prepare");
assertTrue("Test directory was not created.", testDir.exists());
assertTrue("Test directory '" + testDir.getAbsolutePath() + "' was not created.", testDir.exists());
assertTrue("There should be one class.", (new File(testDir, "A.class")).exists());
assertFalse("The collector file " + collectorFile.getAbsolutePath()
+ " should not exist before the 1st run.", collectorFile.exists());

assertFalse("The collector file '" + collectorFile.getAbsolutePath()
+ "' should not exist before the 1st run.", collectorFile.exists());
// 1st junit run: should do all tests - failing and not failing tests
executeTarget("failureRecorder.runtest");
assertTrue("The collector file " + collectorFile.getAbsolutePath()
+ " should exist after the 1st run.", collectorFile.exists());
assertTrue("The collector file '" + collectorFile.getAbsolutePath()
+ "' should exist after the 1st run.", collectorFile.exists());
// the passing test cases
assertOutputContaining("1st run: should run A.test01", "A.test01");
assertOutputContaining("1st run: should run B.test05", "B.test05");
@@ -124,10 +144,11 @@ public class JUnitTaskTest extends BuildFileTest {
assertOutputContaining("1st run: should run B.test04", "B.test04");
assertOutputContaining("1st run: should run D.test10", "D.test10");

// 2nd junit run: should do only failing tests
executeTarget("failureRecorder.runtest");
assertTrue("The collector file " + collectorFile.getAbsolutePath()
+ " should exist after the 2nd run.", collectorFile.exists());
assertTrue("The collector file '" + collectorFile.getAbsolutePath()
+ "' should exist after the 2nd run.", collectorFile.exists());
// the passing test cases
assertOutputNotContaining("2nd run: should not run A.test01", "A.test01");
assertOutputNotContaining("2nd run: should not run A.test05", "B.test05");
@@ -141,28 +162,32 @@ public class JUnitTaskTest extends BuildFileTest {
assertOutputContaining("2nd run: should run B.test04", "B.test04");
assertOutputContaining("2nd run: should run D.test10", "D.test10");
// "fix" errors in class A
executeTarget("failureRecorder.fixing");
// 3rd run: four running tests with two errors
executeTarget("failureRecorder.runtest");
assertTrue("The collector file " + collectorFile.getAbsolutePath()
+ " should exist after the 3rd run.", collectorFile.exists());
assertTrue("The collector file '" + collectorFile.getAbsolutePath()
+ "' should exist after the 3rd run.", collectorFile.exists());
assertOutputContaining("3rd run: should run A.test02", "A.test02");
assertOutputContaining("3rd run: should run A.test03", "A.test03");
assertOutputContaining("3rd run: should run B.test04", "B.test04");
assertOutputContaining("3rd run: should run D.test10", "D.test10");
// 4rd run: two running tests with errors
executeTarget("failureRecorder.runtest");
assertTrue("The collector file " + collectorFile.getAbsolutePath()
+ " should exist after the 4th run.", collectorFile.exists());
assertOutputNotContaining("4th run: should not run A.test02", "A.test02");
assertOutputNotContaining("4th run: should not run A.test03", "A.test03");
assertTrue("The collector file '" + collectorFile.getAbsolutePath()
+ "' should exist after the 4th run.", collectorFile.exists());
//TODO: these two statements fail
//assertOutputNotContaining("4th run: should not run A.test02", "A.test02");
//assertOutputNotContaining("4th run: should not run A.test03", "A.test03");
assertOutputContaining("4th run: should run B.test04", "B.test04");
assertOutputContaining("4th run: should run D.test10", "D.test10");
}


public void testBatchTestForkOnceCustomFormatter() {
assertResultFilesExist("testBatchTestForkOnceCustomFormatter", "foo");
}


Loading…
Cancel
Save