diff --git a/WHATSNEW b/WHATSNEW index 9d9b40f81..94eaf1798 100644 --- a/WHATSNEW +++ b/WHATSNEW @@ -61,6 +61,9 @@ Other changes: overwrite attribute that is consistent with 's attribute names. + * You can now specify a list of methods to run in a JUnit test case. + Bugzilla Report 34748. + Changes from Ant 1.8.0 TO Ant 1.8.1 =================================== diff --git a/build.xml b/build.xml index c9e7f6bdc..fb5770317 100644 --- a/build.xml +++ b/build.xml @@ -350,7 +350,7 @@ classname="org.apache.xalan.trace.TraceListenerEx3" classpathref="classpath" ignoresystemclasses="true"/> java-tmp-dir Name of the test class. Yes + + methods + Comma-separated list of names of test case methods to execute. + Since 1.8.2 +

The methods attribute can be useful in the following scenarios:

+
    +
  • A test method has failed and you want to re-run the test method + to test a fix or re-run the test under the Java debugger without + having to wait for the other (possibly long running) test methods + to complete.
  • +
  • One or more test methods are running slower than expected and you + want to re-run them under a Java profiler (without the overhead + of running the profiler whilst other test methods are being + executed).
  • +
+

If the methods attribute is used but no test method + is specified, then no test method from the suite will be executed.

+ + No; default is to run all test methods in the suite. + fork Run the tests in a separate VM. diff --git a/lib/libraries.properties b/lib/libraries.properties index 135d22c07..3e948f6fc 100644 --- a/lib/libraries.properties +++ b/lib/libraries.properties @@ -45,7 +45,7 @@ jasper-compiler.version=4.1.36 jasper-runtime.version=${jasper-compiler.version} jdepend.version=2.9.1 jruby.version=0.9.8 -junit.version=3.8.2 +junit.version=4.8.1 jsch.version=0.1.42 jython.version=2.1 #log4j 1.2.15 requires JMS and a few other Sun jars that are not in the m2 repo diff --git a/lib/optional/README b/lib/optional/README index 7120e2161..3310d4afc 100644 --- a/lib/optional/README +++ b/lib/optional/README @@ -1,3 +1,3 @@ -The file junit-3.8.2.jar is version 3.8.2 of JUnit, see the file LICENSE.junit.html +The file junit-4.8.1.jar is version 4.8.1 of JUnit, see the file LICENSE.junit.html for the terms of distribution. For more information about JUnit or the latest release, see . diff --git a/lib/optional/junit-3.8.2.jar b/lib/optional/junit-3.8.2.jar deleted file mode 100644 index c8f711d05..000000000 Binary files a/lib/optional/junit-3.8.2.jar and /dev/null differ diff --git a/lib/optional/junit-4.8.1.jar b/lib/optional/junit-4.8.1.jar new file mode 100644 index 000000000..524cd65ce Binary files /dev/null and b/lib/optional/junit-4.8.1.jar differ diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/Constants.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/Constants.java index 971aef027..12876a0a9 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/Constants.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/Constants.java @@ -23,6 +23,7 @@ package org.apache.tools.ant.taskdefs.optional.junit; */ public class Constants { + static final String METHOD_NAMES = "methods="; static final String HALT_ON_ERROR = "haltOnError="; static final String HALT_ON_FAILURE = "haltOnFailure="; static final String FILTERTRACE = "filtertrace="; diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnit4TestMethodAdapter.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnit4TestMethodAdapter.java new file mode 100644 index 000000000..58f31c5c0 --- /dev/null +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnit4TestMethodAdapter.java @@ -0,0 +1,227 @@ +/* + * 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.util.Iterator; +import java.util.List; +import junit.framework.JUnit4TestAdapterCache; +import junit.framework.Test; +import junit.framework.TestResult; +import org.junit.runner.Description; +import org.junit.runner.Request; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; +import org.junit.runner.notification.Failure; +import org.junit.runner.notification.RunListener; +import org.junit.runner.notification.RunNotifier; + +/** + * Adapter between JUnit 3.8.x API and JUnit 4.x API for execution of tests + * and listening of events (test start, test finish, test failure). + * The constructor is passed a JUnit 4 test class and a list of name of methods + * in it that should be executed. Method {@link #run run(TestResult)} executes + * the given JUnit-4-style test methods and notifies the given {@code TestResult} + * object using its old (JUnit 3.8.x style) API. + * + * @author Marian Petras + */ +public class JUnit4TestMethodAdapter implements Test { + + private final Class testClass; + private final String[] methodNames; + private final Runner runner; + private final Cache cache; + + /** + * Creates a new adapter for the given class and a method within the class. + * + * @param testClass test class containing the method to be executed + * @param methodNames names of the test methods that are to be executed + * @exception java.lang.IllegalArgumentException + * if any of the arguments is {@code null} + * or if any of the given method names is {@code null} or empty + */ + public JUnit4TestMethodAdapter(final Class testClass, + final String[] methodNames) { + if (testClass == null) { + throw new IllegalArgumentException("testClass is "); + } + if (methodNames == null) { + throw new IllegalArgumentException("methodNames is "); + } + for (int i = 0; i < methodNames.length; i++) { + if (methodNames[i] == null) { + throw new IllegalArgumentException("method name #" + i + " is "); + } + if (methodNames[i].length() == 0) { + throw new IllegalArgumentException("method name #" + i + " is empty"); + } + } + this.testClass = testClass; + this.methodNames = methodNames; + this.cache = Cache.instance; + + // Warning: If 'testClass' is an old-style (pre-JUnit-4) class, + // then all its test methods will be executed by the returned runner! + Request request; + if (methodNames.length == 1) { + request = Request.method(testClass, methodNames[0]); + } else { + request = Request.aClass(testClass).filterWith( + new MultipleMethodsFilter(testClass, methodNames)); + } + runner = request.getRunner(); + } + + public int countTestCases() { + return runner.testCount(); + } + + public Description getDescription() { + return runner.getDescription(); + } + + public List/**/ getTests() { + return cache.asTestList(getDescription()); + } + + public Class getTestClass() { + return testClass; + } + + public void run(final TestResult result) { + runner.run(cache.getNotifier(result)); + } + + public String toString() { + String testClassName = testClass.getName(); + StringBuilder buf = new StringBuilder(testClassName.length() + + 12 * methodNames.length) + .append(':'); + if (methodNames.length != 0) { + buf.append(methodNames[0]); + for (int i = 1; i < methodNames.length; i++) { + buf.append(',') + .append(methodNames[i]); + } + } + return buf.toString(); + } + + private static final class MultipleMethodsFilter extends Filter { + + private final Description methodsListDescription; + private final Class testClass; + private final String[] methodNames; + + private MultipleMethodsFilter(Class testClass, String[] methodNames) { + if (testClass == null) { + throw new IllegalArgumentException("testClass is "); + } + if (methodNames == null) { + throw new IllegalArgumentException("methodNames is "); + } + methodsListDescription = Description.createSuiteDescription(testClass); + for (int i = 0; i < methodNames.length; i++) { + methodsListDescription.addChild( + Description.createTestDescription(testClass, methodNames[i])); + } + this.testClass = testClass; + this.methodNames = methodNames; + } + + public boolean shouldRun(Description description) { + if (methodNames.length == 0) { + return false; + } + if (description.isTest()) { + Iterator/**/ it = methodsListDescription.getChildren().iterator(); + while (it.hasNext()) { + Description methodDescription = (Description) it.next(); + if (methodDescription.equals(description)) { + return true; + } + } + } else { + Iterator/**/ it = description.getChildren().iterator(); + while (it.hasNext()) { + Description each = (Description) it.next(); + if (shouldRun(each)) { + return true; + } + } + } + return false; + } + + public String describe() { + StringBuilder buf = new StringBuilder(40); + if (methodNames.length == 0) { + buf.append("No methods"); + } else { + buf.append(methodNames.length == 1 ? "Method" : "Methods"); + buf.append(' '); + buf.append(methodNames[0]); + for (int i = 1; i < methodNames.length; i++) { + buf.append(',').append(methodNames[i]); + } + } + buf.append('(').append(testClass.getName()).append(')'); + return buf.toString(); + } + + } + + /** + * Effectively a copy of {@code JUnit4TestAdapterCache}, except that its + * method {@code getNotifier()} does not require an argument + * of type {@code JUnit4TestAdapter}. + */ + private static final class Cache extends JUnit4TestAdapterCache { + + private static final Cache instance = new Cache(); + + public static JUnit4TestAdapterCache getDefault() { + return instance; + } + + public RunNotifier getNotifier(final TestResult result) { + RunNotifier notifier = new RunNotifier(); + notifier.addListener(new RunListener() { + public void testFailure(Failure failure) throws Exception { + result.addError(asTest(failure.getDescription()), + failure.getException()); + } + + public void testFinished(Description description) + throws Exception { + result.endTest(asTest(description)); + } + + public void testStarted(Description description) + throws Exception { + result.startTest(asTest(description)); + } + }); + return notifier; + } + + } + +} diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java index 32e260652..b9a6977d2 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTask.java @@ -728,6 +728,7 @@ public class JUnitTask extends Task { new SplitClassLoader(myLoader, path, getProject(), new String[] { "BriefJUnitResultFormatter", + "JUnit4TestMethodAdapter", "JUnitResultFormatter", "JUnitTaskMirrorImpl", "JUnitTestRunner", @@ -751,6 +752,8 @@ public class JUnitTask extends Task { * @since Ant 1.2 */ public void execute() throws BuildException { + checkMethodLists(); + setupJUnitDelegate(); List testLists = new ArrayList(); @@ -851,6 +854,9 @@ public class JUnitTask extends Task { while (iter.hasNext()) { test = (JUnitTest) iter.next(); printDual(writer, logWriter, test.getName()); + if (test.getMethods() != null) { + printDual(writer, logWriter, ":" + test.getMethodsString().replace(',', '+')); + } if (test.getTodir() == null) { printDual(writer, logWriter, "," + getProject().resolveFile(".")); @@ -922,6 +928,9 @@ public class JUnitTask extends Task { cmd.setClassname("org.apache.tools.ant.taskdefs.optional.junit.JUnitTestRunner"); if (casesFile == null) { cmd.createArgument().setValue(test.getName()); + if (test.getMethods() != null) { + cmd.createArgument().setValue(Constants.METHOD_NAMES + test.getMethodsString()); + } } else { log("Running multiple tests in the same VM", Project.MSG_VERBOSE); cmd.createArgument().setValue(Constants.TESTSFILE + casesFile); @@ -1322,7 +1331,7 @@ public class JUnitTask extends Task { if (classLoader != null) { classLoader.setThreadContextLoader(); } - runner = delegate.newJUnitTestRunner(test, test.getHaltonerror(), + runner = delegate.newJUnitTestRunner(test, test.getMethods(), test.getHaltonerror(), test.getFiltertrace(), test.getHaltonfailure(), false, true, classLoader); @@ -1407,6 +1416,29 @@ public class JUnitTask extends Task { return Enumerations.fromCompound(enums); } + /** + * Verifies all test elements having the methods + * attribute specified and having the if-condition resolved + * to true, that the value of the methods attribute is valid. + * @exception BuildException if some of the tests matching the described + * conditions has invalid value of the + * methods attribute + * @since 1.8.2 + */ + private void checkMethodLists() throws BuildException { + if (tests.isEmpty()) { + return; + } + + Enumeration testsEnum = tests.elements(); + while (testsEnum.hasMoreElements()) { + JUnitTest test = (JUnitTest) testsEnum.nextElement(); + if (test.hasMethodsSpecified() && test.shouldRun(getProject())) { + test.resolveMethods(); + } + } + } + /** * return an enumeration listing each test, then each batchtest * @return enumeration diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirror.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirror.java index defb09f78..67451f8d4 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirror.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirror.java @@ -55,6 +55,7 @@ public interface JUnitTaskMirror { /** * Create a new test runner for a test. * @param test the test to run. + * @param methods names of the test methods to be run. * @param haltOnError if true halt the tests if an error occurs. * @param filterTrace if true filter the stack traces. * @param haltOnFailure if true halt the test if a failure occurs. @@ -63,7 +64,7 @@ public interface JUnitTaskMirror { * @param classLoader the classloader to use to create the runner. * @return the test runner. */ - JUnitTestRunnerMirror newJUnitTestRunner(JUnitTest test, boolean haltOnError, + JUnitTestRunnerMirror newJUnitTestRunner(JUnitTest test, String[] methods, boolean haltOnError, boolean filterTrace, boolean haltOnFailure, boolean showOutput, boolean logTestListenerEvents, AntClassLoader classLoader); diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirrorImpl.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirrorImpl.java index 815728bf6..36ceae086 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirrorImpl.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTaskMirrorImpl.java @@ -60,9 +60,10 @@ public final class JUnitTaskMirrorImpl implements JUnitTaskMirror { /** {@inheritDoc}. */ public JUnitTaskMirror.JUnitTestRunnerMirror newJUnitTestRunner(JUnitTest test, + String[] methods, boolean haltOnError, boolean filterTrace, boolean haltOnFailure, boolean showOutput, boolean logTestListenerEvents, AntClassLoader classLoader) { - return new JUnitTestRunner(test, haltOnError, filterTrace, haltOnFailure, + return new JUnitTestRunner(test, methods, haltOnError, filterTrace, haltOnFailure, showOutput, logTestListenerEvents, classLoader); } diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTest.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTest.java index 960a254b3..8079b9598 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTest.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTest.java @@ -22,6 +22,7 @@ import java.util.Enumeration; import java.util.Hashtable; import java.util.Properties; import java.util.Vector; +import org.apache.tools.ant.BuildException; import org.apache.tools.ant.Project; import org.apache.tools.ant.PropertyHelper; @@ -41,6 +42,19 @@ public class JUnitTest extends BaseTest implements Cloneable { /** the name of the test case */ private String name = null; + /** + * whether the list of test methods has been specified + * @see #setMethods(java.lang.String) + * @see #setMethods(java.lang.String[]) + */ + private boolean methodsSpecified = false; + + /** comma-separated list of names of test methods to execute */ + private String methodsList = null; + + /** the names of test methods to execute */ + private String[] methods = null; + /** the name of the result file */ private String outfile = null; @@ -73,11 +87,53 @@ public class JUnitTest extends BaseTest implements Cloneable { * @param filtertrace if true filter stack traces. */ public JUnitTest(String name, boolean haltOnError, boolean haltOnFailure, - boolean filtertrace) { + boolean filtertrace) { + this(name, haltOnError, haltOnFailure, filtertrace, null); + } + + /** + * Constructor with options. + * @param name the name of the test. + * @param haltOnError if true halt the tests if there is an error. + * @param haltOnFailure if true halt the tests if there is a failure. + * @param filtertrace if true filter stack traces. + * @param methods if true run only test methods that failed during the + * previous run of the test suite + * @since 1.8.2 + */ + public JUnitTest(String name, boolean haltOnError, boolean haltOnFailure, + boolean filtertrace, String[] methods) { this.name = name; this.haltOnError = haltOnError; this.haltOnFail = haltOnFailure; this.filtertrace = filtertrace; + this.methods = methods; + this.methodsSpecified = (methods != null); + } + + /** + * Sets names of individual test methods to be executed. + * @param value comma-separated list of names of individual test methods + * to be executed, + * or null if all test methods should be executed + * @since 1.8.2 + */ + public void setMethods(String value) { + methodsList = value; + methodsSpecified = (value != null); + methods = null; + } + + /** + * Sets names of individual test methods to be executed. + * @param value non-empty array of names of test methods to be executed + * @see #setMethods(String) + * @since 1.8.2 + */ + void setMethods(String[] value) { + methods = value; + methodsSpecified = (value != null); + methodsList = null; } /** @@ -96,6 +152,189 @@ public class JUnitTest extends BaseTest implements Cloneable { outfile = value; } + /** + * Informs whether a list of test methods has been specified in this test. + * @return true if test methods to be executed have been + * specified, false otherwise + * @see #setMethods(java.lang.String) + * @see #setMethods(java.lang.String[]) + * @since 1.8.2 + */ + boolean hasMethodsSpecified() { + return methodsSpecified; + } + + /** + * Get names of individual test methods to be executed. + * + * @return array of names of the individual test methods to be executed, + * or null if all test methods in the suite + * defined by the test class will be executed + * @since 1.8.2 + */ + String[] getMethods() { + if (methodsSpecified && (methods == null)) { + resolveMethods(); + } + return methods; + } + + /** + * Gets a comma-separated list of names of methods that are to be executed + * by this test. + * @return the comma-separated list of test method names, or an empty + * string of no method is to be executed, or null + * if no method is specified + * @since 1.8.2 + */ + String getMethodsString() { + if ((methodsList == null) && methodsSpecified) { + if (methods.length == 0) { + methodsList = ""; + } else if (methods.length == 1) { + methodsList = methods[0]; + } else { + StringBuffer buf = new StringBuffer(methods.length * 16); + buf.append(methods[0]); + for (int i = 1; i < methods.length; i++) { + buf.append(',').append(methods[i]); + } + methodsList = buf.toString(); + } + } + return methodsList; + } + + /** + * Computes the value of the {@link #methods} field from the value + * of the {@link #methodsList} field, if it has not been computed yet. + * @exception BuildException if the value of the {@link #methodsList} field + * was invalid + * @since 1.8.2 + */ + void resolveMethods() { + if ((methods == null) && methodsSpecified) { + try { + methods = parseTestMethodNamesList(methodsList); + } catch (IllegalArgumentException ex) { + throw new BuildException( + "Invalid specification of test methods: \"" + + methodsList + + "\"; expected: comma-separated list of valid Java identifiers", + ex); + } + } + } + + /** + * Parses a comma-separated list of method names and check their validity. + * @param methodNames comma-separated list of method names to be parsed + * @return array of individual test method names + * @exception java.lang.IllegalArgumentException + * if the given string is null or if it is not + * a comma-separated list of valid Java identifiers; + * an empty string is acceptable and is handled as an empty + * list + * @since 1.8.2 + */ + public static String[] parseTestMethodNamesList(String methodNames) + throws IllegalArgumentException { + if (methodNames == null) { + throw new IllegalArgumentException("methodNames is "); + } + + methodNames = methodNames.trim(); + + int length = methodNames.length(); + if (length == 0) { + return new String[0]; + } + + /* strip the trailing comma, if any */ + if (methodNames.charAt(length - 1) == ',') { + methodNames = methodNames.substring(0, length - 1).trim(); + length = methodNames.length(); + if (length == 0) { + throw new IllegalArgumentException("Empty method name"); + } + } + + final char[] chars = methodNames.toCharArray(); + /* easy detection of one particular case of illegal string: */ + if (chars[0] == ',') { + throw new IllegalArgumentException("Empty method name"); + } + /* count number of method names: */ + int wordCount = 1; + for (int i = 1; i < chars.length; i++) { + if (chars[i] == ',') { + wordCount++; + } + } + /* prepare the resulting array: */ + String[] result = new String[wordCount]; + /* parse the string: */ + final int stateBeforeWord = 1; + final int stateInsideWord = 2; + final int stateAfterWord = 3; + // + int state = stateBeforeWord; + int wordStartIndex = -1; + int wordIndex = 0; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + switch (state) { + case stateBeforeWord: + if (c == ',') { + throw new IllegalArgumentException("Empty method name"); + } else if (c == ' ') { + // remain in the same state + } else if (Character.isJavaIdentifierStart(c)) { + wordStartIndex = i; + state = stateInsideWord; + } else { + throw new IllegalArgumentException("Illegal start of method name: " + c); + } + break; + case stateInsideWord: + if (c == ',') { + result[wordIndex++] = new String(methodNames.substring(wordStartIndex, i)); + state = stateBeforeWord; + } else if (c == ' ') { + result[wordIndex++] = new String(methodNames.substring(wordStartIndex, i)); + state = stateAfterWord; + } else if (Character.isJavaIdentifierPart(c)) { + // remain in the same state + } else { + throw new IllegalArgumentException("Illegal character in method name: " + c); + } + break; + case stateAfterWord: + if (c == ',') { + state = stateBeforeWord; + } else if (c == ' ') { + // remain in the same state + } else { + throw new IllegalArgumentException("Space in method name"); + } + break; + default: + // this should never happen + } + } + switch (state) { + case stateBeforeWord: + case stateAfterWord: + break; + case stateInsideWord: + result[wordIndex++] = new String(methodNames.substring(wordStartIndex, chars.length)); + break; + default: + // this should never happen + } + return result; + } + /** * Get the name of the test class. * @return the name of the test. diff --git a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java index e56a7bd23..58ced0030 100644 --- a/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java +++ b/src/main/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunner.java @@ -163,6 +163,9 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR */ private static String crashFile = null; + /** Names of test methods to execute */ + private String[] methods = null; + /** * Constructor for fork=true or when the user hasn't specified a * classpath. @@ -205,7 +208,26 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR public JUnitTestRunner(JUnitTest test, boolean haltOnError, boolean filtertrace, boolean haltOnFailure, boolean showOutput, boolean logTestListenerEvents) { - this(test, haltOnError, filtertrace, haltOnFailure, showOutput, + this(test, null, haltOnError, filtertrace, haltOnFailure, showOutput, + logTestListenerEvents, null); + } + + /** + * Constructor for fork=true or when the user hasn't specified a + * classpath. + * @param test the test to run. + * @param methods names of methods of the test to be executed. + * @param haltOnError whether to stop the run if an error is found. + * @param filtertrace whether to filter junit.*.* stack frames out of exceptions + * @param haltOnFailure whether to stop the run if failure is found. + * @param showOutput whether to send output to System.out/.err as well as formatters. + * @param logTestListenerEvents whether to print TestListener events. + * @since 1.8.2 + */ + public JUnitTestRunner(JUnitTest test, String[] methods, boolean haltOnError, + boolean filtertrace, boolean haltOnFailure, + boolean showOutput, boolean logTestListenerEvents) { + this(test, methods, haltOnError, filtertrace, haltOnFailure, showOutput, logTestListenerEvents, null); } @@ -254,12 +276,26 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR boolean filtertrace, boolean haltOnFailure, boolean showOutput, boolean logTestListenerEvents, ClassLoader loader) { + this(test, null, haltOnError, filtertrace, haltOnFailure, showOutput, + logTestListenerEvents, loader); + } + + + /** + * Constructor to use when the user has specified a classpath. + * @since 1.8.2 + */ + public JUnitTestRunner(JUnitTest test, String[] methods, boolean haltOnError, + boolean filtertrace, boolean haltOnFailure, + boolean showOutput, boolean logTestListenerEvents, + ClassLoader loader) { JUnitTestRunner.filtertrace = filtertrace; this.junitTest = test; this.haltOnError = haltOnError; this.haltOnFailure = haltOnFailure; this.showOutput = showOutput; this.logTestListenerEvents = logTestListenerEvents; + this.methods = methods; this.loader = loader; } @@ -340,9 +376,12 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR loader); } + final boolean testMethodsSpecified = (methods != null); + // check for a static suite method first, even when using // JUnit 4 Method suiteMethod = null; + if (!testMethodsSpecified) { try { // check if there is a suite method suiteMethod = testClass.getMethod("suite", new Class[0]); @@ -350,6 +389,7 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR // no appropriate suite method found. We don't report any // error here since it might be perfectly normal. } + } if (suiteMethod != null) { // if there is a suite method available, then try @@ -359,7 +399,23 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR } else { Class junit4TestAdapterClass = null; - + boolean useSingleMethodAdapter = false; + + if (junit.framework.TestCase.class.isAssignableFrom(testClass)) { + // Do not use JUnit 4 API for running JUnit 3.x + // tests - it is not able to run individual test + // methods. + // + // Technical details: + // org.junit.runner.Request.method(Class, String).getRunner() + // would return a runner which always executes all + // test methods. The reason is that the Runner would be + // an instance of class + // org.junit.internal.runners.OldTestClassRunner + // that does not implement interface Filterable - so it + // is unable to filter out test methods not matching + // the requested name. + } else { // Check for JDK 5 first. Will *not* help on JDK 1.4 // if only junit-4.0.jar in CP because in that case // linkage of whole task will already have failed! But @@ -373,29 +429,69 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR if (loader == null) { junit4TestAdapterClass = Class.forName(JUNIT_4_TEST_ADAPTER); + if (testMethodsSpecified) { + /* + * We cannot try to load the JUnit4TestAdapter + * before trying to load JUnit4TestMethodAdapter + * because it might fail with + * NoClassDefFoundException, instead of plain + * ClassNotFoundException. + */ + junit4TestAdapterClass = Class.forName( + "org.apache.tools.ant.taskdefs.optional.junit.JUnit4TestMethodAdapter"); + useSingleMethodAdapter = true; + } } else { junit4TestAdapterClass = Class.forName(JUNIT_4_TEST_ADAPTER, true, loader); + if (testMethodsSpecified) { + junit4TestAdapterClass = + Class.forName( + "org.apache.tools.ant.taskdefs.optional.junit.JUnit4TestMethodAdapter", + true, loader); + useSingleMethodAdapter = true; + } } } catch (ClassNotFoundException e) { // OK, fall back to JUnit 3. } + } junit4 = junit4TestAdapterClass != null; if (junit4) { // Let's use it! + Class[] formalParams; + Object[] actualParams; + if (useSingleMethodAdapter) { + formalParams = new Class[] {Class.class, String[].class}; + actualParams = new Object[] {testClass, methods}; + } else { + formalParams = new Class[] {Class.class}; + actualParams = new Object[] {testClass}; + } suite = (Test) junit4TestAdapterClass - .getConstructor(new Class[] {Class.class}). - newInstance(new Object[] {testClass}); + .getConstructor(formalParams). + newInstance(actualParams); } else { // Use JUnit 3. // try to extract a test suite automatically this // will generate warnings if the class is no // suitable Test - suite = new TestSuite(testClass); + if (!testMethodsSpecified) { + suite = new TestSuite(testClass); + } else if (methods.length == 1) { + suite = TestSuite.createTest(testClass, methods[0]); + } else { + TestSuite testSuite = new TestSuite(testClass.getName()); + for (int i = 0; i < methods.length; i++) { + testSuite.addTest( + TestSuite.createTest(testClass, methods[i])); + } + suite = testSuite; + } } } @@ -670,11 +766,16 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR * logtestlistenereventslog TestListener events to * System.out.false * + * methodsComma-separated list of names of individual + * test methods to execute. + * null + * * * @param args the command line arguments. * @throws IOException on error. */ public static void main(String[] args) throws IOException { + String[] methods = null; boolean haltError = false; boolean haltFail = false; boolean stackfilter = true; @@ -696,7 +797,15 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR } for (int i = 1; i < args.length; i++) { - if (args[i].startsWith(Constants.HALT_ON_ERROR)) { + if (args[i].startsWith(Constants.METHOD_NAMES)) { + try { + String methodsList = args[i].substring(Constants.METHOD_NAMES.length()); + methods = JUnitTest.parseTestMethodNamesList(methodsList); + } catch (IllegalArgumentException ex) { + System.err.println("Invalid specification of test method names: " + args[i]); + System.exit(ERRORS); + } + } else if (args[i].startsWith(Constants.HALT_ON_ERROR)) { haltError = Project.toBoolean(args[i].substring(Constants.HALT_ON_ERROR.length())); } else if (args[i].startsWith(Constants.HALT_ON_FAILURE)) { haltFail = Project.toBoolean(args[i].substring(Constants.HALT_ON_FAILURE.length())); @@ -744,18 +853,30 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.FileReader(args[0])); String testCaseName; + String[] testMethodNames; int code = 0; boolean errorOccurred = false; boolean failureOccurred = false; String line = null; while ((line = reader.readLine()) != null) { StringTokenizer st = new StringTokenizer(line, ","); - testCaseName = st.nextToken(); + String testListSpec = st.nextToken(); + int colonIndex = testListSpec.indexOf(':'); + if (colonIndex == -1) { + testCaseName = testListSpec; + testMethodNames = null; + } else { + testCaseName = testListSpec.substring(0, colonIndex); + testMethodNames = JUnitTest.parseTestMethodNamesList( + testListSpec + .substring(colonIndex + 1) + .replace('+', ',')); + } JUnitTest t = new JUnitTest(testCaseName); t.setTodir(new File(st.nextToken())); t.setOutfile(st.nextToken()); t.setProperties(props); - code = launch(t, haltError, stackfilter, haltFail, + code = launch(t, testMethodNames, haltError, stackfilter, haltFail, showOut, outputToFormat, logTestListenerEvents); errorOccurred = (code == ERRORS); @@ -783,7 +904,7 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR JUnitTest t = new JUnitTest(args[0]); t.setProperties(props); returnCode = launch( - t, haltError, stackfilter, haltFail, + t, methods, haltError, stackfilter, haltFail, showOut, outputToFormat, logTestListenerEvents); } @@ -917,12 +1038,12 @@ public class JUnitTestRunner implements TestListener, JUnitTaskMirror.JUnitTestR /** * @since Ant 1.6.2 */ - private static int launch(JUnitTest t, boolean haltError, + private static int launch(JUnitTest t, String[] methods, boolean haltError, boolean stackfilter, boolean haltFail, boolean showOut, boolean outputToFormat, boolean logTestListenerEvents) { JUnitTestRunner runner = - new JUnitTestRunner(t, haltError, stackfilter, haltFail, showOut, + new JUnitTestRunner(t, methods, haltError, stackfilter, haltFail, showOut, logTestListenerEvents, null); runner.forked = true; runner.outputToFormatters = outputToFormat; diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/BatchTestTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/BatchTestTest.java new file mode 100644 index 000000000..33745ef69 --- /dev/null +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/BatchTestTest.java @@ -0,0 +1,142 @@ +/* + * 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 junit.framework.ComparisonFailure; +import junit.framework.TestCase; + +/** + * + * @author Marian Petras + */ +public class BatchTestTest extends TestCase { + + public BatchTestTest(String testName) { + super(testName); + } + + + public void testParseTestMethodNamesList() { + try { + JUnitTest.parseTestMethodNamesList(null); + fail("IllegalArgumentException expected when the param is "); + } catch (IllegalArgumentException ex) { + //this is an expected exception + } + + assertEquals(new String[0], JUnitTest.parseTestMethodNamesList("")); + assertEquals(new String[0], JUnitTest.parseTestMethodNamesList(" ")); + assertEquals(new String[0], JUnitTest.parseTestMethodNamesList(" ")); + + checkParseCausesIAE(","); + checkParseCausesIAE(" ,"); + checkParseCausesIAE(", "); + checkParseCausesIAE(" , "); + checkParseCausesIAE(",a"); + checkParseCausesIAE(" ,a"); + checkParseCausesIAE(" ,a"); + checkParseCausesIAE(" , a"); + checkParseCausesIAE(" ,a "); + checkParseCausesIAE(" ,a ,"); + checkParseCausesIAE("ab,,cd"); + checkParseCausesIAE("ab, ,cd"); + checkParseCausesIAE("ab, ,cd"); + checkParseCausesIAE("ab, ,cd,"); + checkParseCausesIAE(",ab, ,cd,"); + + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc ")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList(" abc")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList(" abc ")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc ")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc,")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc, ")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc ,")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList("abc , ")); + assertEquals(new String[] {"abc"}, JUnitTest.parseTestMethodNamesList(" abc ,")); + + /* legal Java identifiers: */ + assertEquals(new String[] {"a"}, JUnitTest.parseTestMethodNamesList("a")); + assertEquals(new String[] {"a1"}, JUnitTest.parseTestMethodNamesList("a1")); + assertEquals(new String[] {"a$"}, JUnitTest.parseTestMethodNamesList("a$")); + assertEquals(new String[] {"a$1"}, JUnitTest.parseTestMethodNamesList("a$1")); + assertEquals(new String[] {"_bc"}, JUnitTest.parseTestMethodNamesList("_bc")); + assertEquals(new String[] {"___"}, JUnitTest.parseTestMethodNamesList("___")); + + /* illegal Java identifiers: */ + checkParseCausesIAE("1"); + checkParseCausesIAE("1a"); + checkParseCausesIAE("1ab"); + checkParseCausesIAE("1abc"); + checkParseCausesIAE("1abc d"); + checkParseCausesIAE("1abc de"); + checkParseCausesIAE("1abc def"); + checkParseCausesIAE("1abc def,"); + checkParseCausesIAE(",1abc def"); + + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc,def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc,def,")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc,def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc, def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc, def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc ,def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc ,def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc , def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList("abc , def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc,def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc,def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc, def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc, def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc ,def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc ,def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc , def")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc , def ")); + assertEquals(new String[] {"abc", "def"}, JUnitTest.parseTestMethodNamesList(" abc , def ,")); + } + + private static void checkParseCausesIAE(String param) { + try { + JUnitTest.parseTestMethodNamesList(param); + fail("IllegalArgumentException expected when the param is \"" + param + '"'); + } catch (IllegalArgumentException ex) { + //this is an expected exception + } + } + + private static void assertEquals(String[] expected, String[] actual) { + assertEquals(null, expected, actual); + } + + private static void assertEquals(String message, + String[] expected, + String[] actual) { + if ((expected == null) && (actual == null)) { + return; + } + if (expected.length != actual.length) { + throw new ComparisonFailure(message, + expected.toString(), + actual.toString()); + } + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + +} diff --git a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunnerTest.java b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunnerTest.java index d44ebdf47..2b685b7e1 100644 --- a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunnerTest.java +++ b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junit/JUnitTestRunnerTest.java @@ -33,6 +33,22 @@ public class JUnitTestRunnerTest extends TestCase { super(name); } + // check that a valid method name generates no errors + public void testValidMethod(){ + TestRunner runner = createRunnerForTestMethod(ValidMethodTestCase.class,"testA"); + runner.run(); + assertEquals(runner.getFormatter().getError(), JUnitTestRunner.SUCCESS, runner.getRetCode()); + } + + // check that having an invalid method name generates an error + public void testInvalidMethod(){ + TestRunner runner = createRunnerForTestMethod(InvalidMethodTestCase.class,"testInvalid"); + runner.run(); + String error = runner.getFormatter().getError(); + // might be FAILURES or ERRORS depending on JUnit version? + assertTrue(error, runner.getRetCode() != JUnitTestRunner.SUCCESS); + } + // check that having no suite generates no errors public void testNoSuite(){ TestRunner runner = createRunner(NoSuiteTestCase.class); @@ -87,14 +103,22 @@ public class JUnitTestRunnerTest extends TestCase { } protected TestRunner createRunner(Class clazz){ - return new TestRunner(new JUnitTest(clazz.getName()), true, true, true); + return new TestRunner(new JUnitTest(clazz.getName()), null, + true, true, true); } + protected TestRunner createRunnerForTestMethod(Class clazz, String method){ + return new TestRunner(new JUnitTest(clazz.getName()), new String[] {method}, + true, true, true); + } + // the test runner that wrap the dummy formatter that interests us private final static class TestRunner extends JUnitTestRunner { private ResultFormatter formatter = new ResultFormatter(); - TestRunner(JUnitTest test, boolean haltonerror, boolean filtertrace, boolean haltonfailure){ - super(test, haltonerror, filtertrace, haltonfailure, TestRunner.class.getClassLoader()); + TestRunner(JUnitTest test, String[] methods, boolean haltonerror, + boolean filtertrace, boolean haltonfailure){ + super(test, methods, haltonerror, filtertrace, haltonfailure, + false, false, TestRunner.class.getClassLoader()); // use the classloader that loaded this class otherwise // it will not be able to run inner classes if this test // is ran in non-forked mode. @@ -133,6 +157,24 @@ public class JUnitTestRunnerTest extends TestCase { public static class NoTestCase { } + public static class InvalidMethodTestCase extends TestCase { + public InvalidMethodTestCase(String name){ super(name); } + public void testA(){ + throw new NullPointerException("thrown on purpose"); + } + } + + public static class ValidMethodTestCase extends TestCase { + public ValidMethodTestCase(String name){ super(name); } + public void testA(){ + // expected to be executed + } + public void testB(){ + // should not be executed + throw new NullPointerException("thrown on purpose"); + } + } + public static class InvalidTestCase extends TestCase { public InvalidTestCase(String name){ super(name);