| /* |
| * 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.ant.antunit; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.Hashtable; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.tools.ant.BuildEvent; |
| import org.apache.tools.ant.BuildException; |
| import org.apache.tools.ant.BuildListener; |
| import org.apache.tools.ant.MagicNames; |
| import org.apache.tools.ant.Project; |
| import org.apache.tools.ant.ProjectComponent; |
| import org.apache.tools.ant.ProjectHelper; |
| import org.apache.tools.ant.Task; |
| import org.apache.tools.ant.taskdefs.Ant; |
| import org.apache.tools.ant.types.Mapper; |
| import org.apache.tools.ant.types.PropertySet; |
| import org.apache.tools.ant.types.ResourceCollection; |
| import org.apache.tools.ant.types.resources.FileResource; |
| import org.apache.tools.ant.types.resources.Union; |
| import org.apache.tools.ant.util.FileNameMapper; |
| import org.apache.tools.ant.util.regexp.RegexpMatcher; |
| import org.apache.tools.ant.util.regexp.RegexpMatcherFactory; |
| |
| /** |
| * Run every target whose name starts with "test" in a set of build files. |
| * |
| * <p>Run the "setUp" target before each of them if present, same for |
| * "tearDown" after each "test*" target (targets named just "test" are |
| * ignored). If a target throws an AssertionFailedException, the test |
| * has failed; any other exception is considered an error (although |
| * BuildException will be scanned recursively for nested |
| * AssertionFailedExceptions).</p> |
| */ |
| public class AntUnit extends Task { |
| |
| /** |
| * Message to print if an error or failure occured. |
| */ |
| public static final String ERROR_TESTS_FAILED = "Tests failed with "; |
| |
| /** |
| * Message if no tests have been specified. |
| */ |
| public static final String ERROR_NO_TESTS = |
| "You must specify build files to test."; |
| |
| /** |
| * Message if non-File resources have been specified. |
| */ |
| public static final String ERROR_NON_FILES = |
| "Only file system resources are supported."; |
| |
| /** |
| * The build files to process. |
| */ |
| private Union buildFiles; |
| |
| |
| private AntUnitExecutionNotifier notifier = new AntUnitExecutionNotifier() { |
| |
| public void fireEndTest(String targetName) { |
| AntUnit.this.fireEndTest(targetName); |
| } |
| |
| public void fireError(String targetName, Throwable t) { |
| AntUnit.this.fireError(targetName, t); |
| } |
| |
| public void fireFail(String targetName, AssertionFailedException ae) { |
| AntUnit.this.fireFail(targetName, ae); |
| } |
| |
| public void fireStartTest(String targetName) { |
| AntUnit.this.fireStartTest(targetName); |
| } |
| }; |
| |
| /** |
| * The object responsible for the execution of the unit test. |
| * scriptRunner is invoked to executes the targets and keep the |
| * reference to the project. scriptRunner is defined only when the |
| * antunit script is running. |
| */ |
| private AntUnitScriptRunner scriptRunner; |
| |
| /** |
| * listeners. |
| */ |
| private ArrayList listeners = new ArrayList(); |
| |
| /** |
| * propertysets. |
| */ |
| private ArrayList propertySets = new ArrayList(); |
| |
| /** |
| * Holds references to be inherited by the test project |
| */ |
| private ArrayList referenceSets = new ArrayList(); |
| |
| /** |
| * has a failure occured? |
| */ |
| private int failures = 0; |
| |
| /** |
| * has an error occured? |
| */ |
| private int errors = 0; |
| |
| /** |
| * stop testing if an error or failure occurs? |
| */ |
| private boolean failOnError = true; |
| |
| /** |
| * Name of a property to set in case of an error. |
| */ |
| private String errorProperty = null; |
| |
| /** |
| * Add build files to run as tests. |
| * @param rc the ResourceCollection to add. |
| */ |
| public void add(ResourceCollection rc) { |
| if (buildFiles == null) { |
| buildFiles = new Union(); |
| } |
| buildFiles.add(rc); |
| } |
| |
| /** |
| * Add a test listener. |
| * @param al the AntUnitListener to add. |
| */ |
| public void add(AntUnitListener al) { |
| listeners.add(al); |
| al.setParentTask(this); |
| } |
| |
| /** |
| * Add a PropertySet. |
| * @param ps the PropertySet to add. |
| */ |
| public void addPropertySet(PropertySet ps) { |
| propertySets.add(ps); |
| } |
| |
| /** |
| * Add a set of inherited references. |
| */ |
| public ReferenceSet createReferenceSet() { |
| ReferenceSet set = new ReferenceSet(); |
| set.setProject(getProject()); |
| referenceSets.add(set); |
| return set; |
| } |
| |
| /** |
| * Add an inherited reference |
| */ |
| public void addReference(Reference reference) { |
| //wrap in a singleton reference set. |
| createReferenceSet().addReference(reference); |
| } |
| |
| /** |
| * Set the name of a property to set if an error or failure occurs. |
| * @param s the name of the error property. |
| */ |
| public void setErrorProperty(String s) { |
| errorProperty = s; |
| } |
| |
| /** |
| * Set whether to stop testing if an error or failure occurs? |
| * @param failOnError default <code>true</code> |
| */ |
| public void setFailOnError(boolean failOnError) { |
| this.failOnError = failOnError; |
| } |
| |
| /** |
| * Execute the tests. |
| */ |
| public void execute() { |
| if (buildFiles == null) { |
| throw new BuildException(ERROR_NO_TESTS); |
| } |
| doResourceCollection(buildFiles); |
| if (failures > 0 || errors > 0) { |
| if (errorProperty != null) { |
| getProject().setNewProperty(errorProperty, "true"); |
| } |
| if (failOnError) { |
| throw new BuildException(ERROR_TESTS_FAILED |
| + failures + " failure" |
| + (failures != 1 ? "s" : "") |
| + " and " |
| + errors + " error" |
| + (errors != 1 ? "s" : "")); |
| } |
| } |
| } |
| |
| /** |
| * Processes a ResourceCollection. |
| */ |
| private void doResourceCollection(ResourceCollection rc) { |
| //should relax this restriction if/when Ant core allows non-files |
| if (!rc.isFilesystemOnly()) { |
| throw new BuildException(ERROR_NON_FILES); |
| } |
| |
| Iterator i = rc.iterator(); |
| while (i.hasNext()) { |
| FileResource r = (FileResource) i.next(); |
| if (r.isExists()) { |
| doFile(r.getFile()); |
| } else { |
| log("Skipping " + r + " since it doesn't exist", |
| Project.MSG_VERBOSE); |
| } |
| } |
| } |
| |
| /** |
| * Processes a single build file. |
| */ |
| private void doFile(final File f) { |
| log("Running tests in build file " + f, Project.MSG_DEBUG); |
| ProjectFactory prjFactory = new ProjectFactory() { |
| public Project createProject() { |
| return createProjectForFile(f); |
| } |
| }; |
| try { |
| scriptRunner = new AntUnitScriptRunner(prjFactory); |
| List testTargets = scriptRunner.getTestTartgets(); |
| scriptRunner.runSuite(testTargets, notifier); |
| } finally { |
| scriptRunner=null; |
| } |
| } |
| |
| |
| /** |
| * Redirect output to new project instance. |
| * @param outputToHandle the output to handle. |
| */ |
| public void handleOutput(String outputToHandle) { |
| if (scriptRunner!=null) { |
| scriptRunner.getCurrentProject().demuxOutput(outputToHandle, false); |
| } else { |
| super.handleOutput(outputToHandle); |
| } |
| } |
| |
| /** |
| * Redirect input to new project instance. |
| * @param buffer the buffer containing the input. |
| * @param offset the offset into <code>buffer</code>. |
| * @param length the length of the data. |
| */ |
| public int handleInput(byte[] buffer, int offset, int length) |
| throws IOException { |
| if (scriptRunner!=null) { |
| return scriptRunner.getCurrentProject().demuxInput(buffer, offset, length); |
| } |
| return super.handleInput(buffer, offset, length); |
| } |
| |
| /** |
| * Redirect flush to new project instance. |
| * @param toFlush the output String to flush. |
| */ |
| public void handleFlush(String toFlush) { |
| if (scriptRunner!=null) { |
| scriptRunner.getCurrentProject().demuxFlush(toFlush, false); |
| } else { |
| super.handleFlush(toFlush); |
| } |
| } |
| |
| /** |
| * Redirect error output to new project instance. |
| * @param errorOutputToHandle the error output to handle. |
| */ |
| public void handleErrorOutput(String errorOutputToHandle) { |
| if (scriptRunner!=null) { |
| scriptRunner.getCurrentProject().demuxOutput(errorOutputToHandle, true); |
| } else { |
| super.handleErrorOutput(errorOutputToHandle); |
| } |
| } |
| |
| /** |
| * Redirect error flush to new project instance. |
| * @param errorOutputToFlush the error output to flush. |
| */ |
| public void handleErrorFlush(String errorOutputToFlush) { |
| if (scriptRunner!=null) { |
| scriptRunner.getCurrentProject().demuxFlush(errorOutputToFlush, true); |
| } else { |
| super.handleErrorFlush(errorOutputToFlush); |
| } |
| } |
| |
| /** |
| * Creates a new project instance and configures it. |
| * @param f the File for which to create a Project. |
| */ |
| private Project createProjectForFile(File f) { |
| Project p = new Project(); |
| p.setDefaultInputStream(getProject().getDefaultInputStream()); |
| p.initProperties(); |
| p.setInputHandler(getProject().getInputHandler()); |
| getProject().initSubProject(p); |
| //pass through inherited properties |
| for (Iterator outer = propertySets.iterator(); outer.hasNext(); ) { |
| PropertySet set = (PropertySet) outer.next(); |
| Map props = set.getProperties(); |
| for (Iterator keys = props.keySet().iterator(); |
| keys.hasNext(); ) { |
| String key = keys.next().toString(); |
| if (MagicNames.PROJECT_BASEDIR.equals(key) |
| || MagicNames.ANT_FILE.equals(key)) { |
| continue; |
| } |
| Object value = props.get(key); |
| if (value != null && value instanceof String |
| && p.getProperty(key) == null) { |
| p.setNewProperty(key, (String) value); |
| } |
| } |
| } |
| |
| //pass through inherited references. this code is borrowed |
| //with significant modification from taskdefs.Ant in Ant core. |
| //unfortunately the only way we can share the code directly |
| //would be to extend Ant (which might not be a bad idea?) |
| for (int i = 0; i < referenceSets.size(); ++i) { |
| ReferenceSet set = (ReferenceSet) referenceSets.get(i); |
| set.copyReferencesInto(p); |
| } |
| |
| p.setUserProperty(MagicNames.ANT_FILE, f.getAbsolutePath()); |
| attachListeners(f, p); |
| |
| // read build file |
| ProjectHelper.configureProject(p, f); |
| |
| return p; |
| } |
| |
| /** |
| * Wraps all registered test listeners in BuildListeners and |
| * attaches them to the new project instance. |
| * @param buildFile a build file. |
| * @param p the Project to attach to. |
| */ |
| private void attachListeners(File buildFile, Project p) { |
| Iterator it = listeners.iterator(); |
| while (it.hasNext()) { |
| AntUnitListener al = (AntUnitListener) it.next(); |
| p.addBuildListener(new BuildToAntUnitListener(buildFile |
| .getAbsolutePath(), |
| al)); |
| al.setCurrentTestProject(p); |
| } |
| } |
| |
| /** |
| * invokes start on all registered test listeners. |
| * @param targetName the name of the target. |
| */ |
| private void fireStartTest(String targetName) { |
| Iterator it = listeners.iterator(); |
| while (it.hasNext()) { |
| AntUnitListener al = (AntUnitListener) it.next(); |
| al.startTest(targetName); |
| } |
| } |
| |
| /** |
| * invokes addFailure on all registered test listeners. |
| * @param targetName the name of the failed target. |
| * @param ae the associated AssertionFailedException. |
| */ |
| private void fireFail(String targetName, AssertionFailedException ae) { |
| failures++; |
| Iterator it = listeners.iterator(); |
| while (it.hasNext()) { |
| AntUnitListener al = (AntUnitListener) it.next(); |
| al.addFailure(targetName, ae); |
| } |
| } |
| |
| /** |
| * invokes addError on all registered test listeners. |
| * @param targetName the name of the failed target. |
| * @param t the associated Throwable. |
| */ |
| private void fireError(String targetName, Throwable t) { |
| errors++; |
| Iterator it = listeners.iterator(); |
| while (it.hasNext()) { |
| AntUnitListener al = (AntUnitListener) it.next(); |
| al.addError(targetName, t); |
| } |
| } |
| |
| /** |
| * invokes endTest on all registered test listeners. |
| * @param targetName the name of the current target. |
| */ |
| private void fireEndTest(String targetName) { |
| Iterator it = listeners.iterator(); |
| while (it.hasNext()) { |
| AntUnitListener al = (AntUnitListener) it.next(); |
| al.endTest(targetName); |
| } |
| } |
| |
| /** |
| * Defines a collection of inherited {@link Reference references}, |
| * with an optional nested {@link Mapper} that maps them to new |
| * reference IDs in the target project. |
| */ |
| public static class ReferenceSet extends ProjectComponent { |
| /** |
| * references inherited from parent project by antunit scripts |
| */ |
| private ArrayList references = new ArrayList(); |
| /** |
| * maps source reference ID to target reference ID |
| */ |
| private Mapper mapper; |
| |
| public void addReference(Reference reference) { |
| references.add(reference); |
| } |
| |
| public Mapper createMapper() { |
| if (mapper == null) { |
| return mapper = new Mapper(getProject()); |
| } else { |
| throw new BuildException("Only one mapper element is allowed" |
| + " per referenceSet", getLocation()); |
| } |
| } |
| |
| /** configure a single mapper to translate reference IDs */ |
| public void setMapper(String typeName, String from, String to) { |
| Mapper mapper = createMapper(); |
| Mapper.MapperType type = new Mapper.MapperType(); |
| type.setValue(typeName); |
| |
| mapper.setType(type); |
| mapper.setFrom(from); |
| mapper.setTo(to); |
| } |
| |
| /** |
| * Copy all identified references into the target project, |
| * applying any name mapping required by a nested mapper |
| * element. |
| */ |
| public void copyReferencesInto(Project newProject) { |
| FileNameMapper mapper = this.mapper == null |
| ? null : this.mapper.getImplementation(); |
| HashSet matches = new HashSet(); |
| Hashtable src = getProject().getReferences(); |
| |
| for (Iterator it = references.iterator(); it.hasNext(); ) { |
| Reference ref = (Reference) it.next(); |
| |
| matches.clear(); |
| ref.addMatchingReferences(src, matches); |
| |
| for (Iterator ids = matches.iterator(); ids.hasNext(); ) { |
| String refid = (String) ids.next(); |
| String toRefid = ref.getToRefid(); |
| |
| //transform the refid with the mapper if necessary |
| if (mapper != null && toRefid == null) { |
| String[] mapped = mapper.mapFileName(refid); |
| if (mapped != null) { |
| toRefid = mapped[0]; |
| } |
| } |
| if (toRefid == null) { |
| toRefid = refid; |
| } |
| |
| //clone the reference into the new project |
| copyReference(refid, newProject, toRefid); |
| } |
| } |
| } |
| |
| /** |
| * Try to clone and reconfigure the object referenced by |
| * oldkey in the parent project and add it to the new project |
| * with the key newkey. This protects the parent project from |
| * modification by the child project. |
| * |
| * <p>If we cannot clone it, copy the referenced object itself and |
| * keep our fingers crossed.</p> |
| * @param oldKey the reference id in the current project. |
| * @param newKey the reference id in the new project. |
| */ |
| private void copyReference(String oldKey, Project newProject, |
| String newKey) { |
| Object orig = getProject().getReference(oldKey); |
| if (orig == null) { |
| log("No object referenced by " + oldKey + ". Can't copy to " |
| + newKey, |
| Project.MSG_WARN); |
| return; |
| } |
| |
| Class c = orig.getClass(); |
| Object copy = orig; |
| try { |
| Method cloneM = c.getMethod("clone", new Class[0]); |
| if (cloneM != null) { |
| copy = cloneM.invoke(orig, new Object[0]); |
| log("Adding clone of reference " + oldKey, |
| Project.MSG_DEBUG); |
| } |
| } catch (Exception e) { |
| // not Clonable |
| } |
| |
| |
| if (copy instanceof ProjectComponent) { |
| ((ProjectComponent) copy).setProject(newProject); |
| } else { |
| try { |
| Method setProjectM = |
| c.getMethod("setProject", new Class[] {Project.class}); |
| if (setProjectM != null) { |
| setProjectM.invoke(copy, new Object[] {newProject}); |
| } |
| } catch (NoSuchMethodException e) { |
| // ignore this if the class being referenced does not have |
| // a set project method. |
| } catch (Exception e2) { |
| String msg = "Error setting new project instance for " |
| + "reference with id " + oldKey; |
| throw new BuildException(msg, e2, getLocation()); |
| } |
| } |
| newProject.addReference(newKey, copy); |
| } |
| |
| |
| } |
| |
| public static class Reference extends Ant.Reference { |
| |
| private String regex; |
| private RegexpMatcher matcher; |
| |
| public String getRegex() { |
| return regex; |
| } |
| |
| /** |
| * Set a regular expression to match references. |
| * @param regex |
| */ |
| public void setRegex(String regex) { |
| this.regex = regex; |
| RegexpMatcherFactory matchMaker = new RegexpMatcherFactory(); |
| matcher = matchMaker.newRegexpMatcher(); |
| matcher.setPattern(regex); |
| } |
| |
| /** |
| * Add to <code>dest</code> any reference IDs in |
| * <code>src</code> matching this reference descriptor |
| * @param src table of references to check |
| * @param dest set of reference IDs matching this reference pattern |
| */ |
| public void addMatchingReferences(Hashtable src, Collection dest) { |
| String id = getRefId(); |
| if (id != null) { |
| if (src.containsKey(id)) { |
| dest.add(id); |
| } |
| } else if (matcher != null) { |
| for (Iterator it = src.keySet().iterator(); it.hasNext(); ) { |
| String refid = (String)it.next(); |
| if (matcher.matches(refid)) { |
| dest.add(refid); |
| } |
| } |
| } else { |
| throw new BuildException("either the refid or regex attribute " |
| + "is required for reference elements"); |
| } |
| } |
| } |
| |
| /** |
| * Adapts AntUnitListener to BuildListener. |
| */ |
| private class BuildToAntUnitListener implements BuildListener { |
| private String buildFile; |
| private AntUnitListener a; |
| |
| BuildToAntUnitListener(String buildFile, AntUnitListener a) { |
| this.buildFile = buildFile; |
| this.a = a; |
| } |
| |
| public void buildStarted(BuildEvent event) { |
| a.startTestSuite(event.getProject(), buildFile); |
| } |
| public void buildFinished(BuildEvent event) { |
| a.endTestSuite(event.getProject(), buildFile); |
| } |
| public void targetStarted(BuildEvent event) { |
| } |
| public void targetFinished(BuildEvent event) { |
| } |
| public void taskStarted(BuildEvent event) {} |
| public void taskFinished(BuildEvent event) {} |
| public void messageLogged(BuildEvent event) {} |
| } |
| |
| } |