/* ******************************************************************* * Copyright (c) 2004 IBM Corporation * All rights reserved. * This program and the accompanying materials are made available * under the terms of the Common Public License v1.0 * which accompanies this distribution and is available at * http://www.eclipse.org/legal/cpl-v10.html * * Contributors: * Adrian Colyer, * ******************************************************************/ package org.aspectj.tools.ajc; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.StringTokenizer; import org.aspectj.bridge.IMessage; import org.aspectj.bridge.ISourceLocation; import org.aspectj.testing.util.TestUtil; import org.aspectj.weaver.loadtime.WeavingURLClassLoader; import junit.framework.TestCase; /** * A TestCase class that acts as the superclass for all test cases wishing * to drive the ajc compiler. *
* This class provides a number of utility methods that make programmatic * testing of the compiler easy. See AjcTestCaseTest for a couple of simple * tests written using this class. *
** See the XMLBasedAjcTestCase subclass for TestCase class that can be * used to drive compiler tests based on an ajcTests.xml format test * specification file.
* @see org.aspectj.tools.ajc.AjcTestCase.Message * @see org.aspectj.tools.ajc.AjcTestCase.MessageSpec * @see org.aspectj.tools.ajc.AjcTestCase.RunResult * @see org.aspectj.tools.ajc.AjcTestCaseTest * @see org.aspectj.testing.XMLBasedAjcTestCase */ public class AjcTestCase extends TestCase { private RunResult lastRunResult; /** * The Ajc (compiler) instance used for thet test. Created afresh * during the test setup. */ protected Ajc ajc; public static final String DEFAULT_CLASSPATH_ENTRIES = File.pathSeparator + ".." + File.separator + "runtime" + File.separator + "bin" + File.pathSeparator + ".." + File.separator + "testing-client" + File.separator + "bin" + File.pathSeparator + ".." + File.separator + "bridge" + File.separator + "bin" + File.pathSeparator + ".." + File.separator + "util" + File.separator + "bin" + File.pathSeparator + ".." + File.separator + "aspectj5rt" + File.separator + "bin" + File.pathSeparator+ ".."+File.separator+"lib"+File.separator+"junit"+File.separator+"junit.jar" + File.pathSeparator+ ".."+File.separator+"loadtime"+File.separator+"bin" + File.pathSeparator+ ".."+File.separator+"weaver"+File.separator+"bin" + File.pathSeparator+ ".."+File.separator+"weaver5"+File.separator+"bin" // When the build machine executes the tests, it is using code built into jars rather than code build into // bin directories. This means for the necessary types to be found we have to put these jars on the classpath: + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"bridge.jar" + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"util.jar" + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"loadtime.jar" + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"weaver.jar" + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"weaver5.jar" + File.pathSeparator+".."+File.separator+"aj-build"+File.separator+"jars"+File.separator+"asm.jar" + File.pathSeparator+".."+File.separator+"lib" +File.separator+"test"+File.separator+"testing-client.jar" // hmmm, this next one should perhaps point to an aj-build jar... + File.pathSeparator+".."+File.separator+"lib" +File.separator+"test"+File.separator+"aspectjrt.jar" ; /** * Helper class that represents the specification of an individual * message expected to be produced during a compilation run. ** Message objects are combined in a MessageSpec which can then be * passed to the various assertMessage methods.
* @see org.aspectj.tools.ajc.AjcTestCase.MessageSpec */ public static class Message { private int line = -1; private String text; private String sourceFileName; private ISourceLocation[] seeAlsos; /** * Create a message that will match any compiler message on * the given line. */ public Message(int line) { this.line = line; } /** * Create a message that will match any compiler message on * the given line, where the message text containstext
.
*/
public Message(int line, String text) {
this.line = line;
this.text = text;
}
/**
* Create a message that will match any compiler message on
* the given line, where the message text contains text
.
*
* If srcFile is non-null, the source file location of the message must
* end with srcFile
.
*
* If seeAlso
is non-null, each source location in seeAlso
* must be matched by an extraSourceLocation in the message.
*
text
.
*/
public Message(String text) {
this.text = text;
}
/**
* Return true if this message spec matches the given compiler message.
*/
public boolean matches(IMessage message) {
ISourceLocation loc = message.getSourceLocation();
if ((loc == null) && ((line != -1) || (sourceFileName != null))) {
return false;
}
if (line != -1) {
if (loc.getLine() != line) {
return false;
}
}
if (sourceFileName != null) {
if (!loc.getSourceFile().getPath().endsWith(sourceFileName)) {
return false;
}
}
if (text != null) {
if (message.getMessage().indexOf(text) == -1) {
return false;
}
}
if (seeAlsos != null) {
List extraLocations = message.getExtraSourceLocations();
if (extraLocations.size() != seeAlsos.length) {
return false;
}
for (int i = 0; i < seeAlsos.length; i++) {
if (!hasAMatch(extraLocations,seeAlsos[i])) {
return false;
}
}
}
return true;
}
private boolean hasAMatch(List srcLocations,ISourceLocation sLoc) {
for (Iterator iter = srcLocations.iterator(); iter.hasNext();) {
ISourceLocation thisLoc = (ISourceLocation) iter.next();
if (thisLoc.getLine() == sLoc.getLine()) {
if (thisLoc.getSourceFile().getPath().equals(sLoc.getSourceFile().getPath())) {
return true;
}
}
}
return false;
}
/**
* Returns a string indicating what this Message
will match.
*/
public String toString() {
StringBuffer buff = new StringBuffer();
buff.append("message ");
if (sourceFileName != null) {
buff.append("in file ");
buff.append(sourceFileName);
buff.append(" ");
}
if (line != -1) {
buff.append("on line ");
buff.append(line);
buff.append(" ");
}
if (text != null) {
buff.append("containing text '");
buff.append(text);
buff.append("' ");
}
if (seeAlsos != null) {
buff.append("\n\twith see alsos:");
for (int i = 0; i < seeAlsos.length; i++) {
buff.append("\t\t");
buff.append(seeAlsos[i].getSourceFile().getPath());
buff.append(":");
buff.append(seeAlsos[i].getLine());
}
}
return buff.toString();
}
}
/**
* Helper class that represents the specification of a set of
* messages expected to be produced from a compiler run.
*
* Instances of MessageSpec are passed to the assertMessage methods
* to validate CompilationResult
s.
*/
public static class MessageSpec {
/**
* Convenience constant that matches a CompilationResult with
* any number of information messages, but no others.
*/
public static final MessageSpec EMPTY_MESSAGE_SET =
new MessageSpec(
null,
Collections.EMPTY_LIST,
Collections.EMPTY_LIST,
Collections.EMPTY_LIST,
Collections.EMPTY_LIST);
boolean ignoreInfos = true;
public List fails;
public List infos;
public List warnings;
public List errors;
public List weaves;
/**
* Set to true to enable or disable comparison of information messages.
*/
public void setInfoComparison(boolean enabled) {
this.ignoreInfos = !enabled;
}
/**
* True if information messages are not being included in matching.
*/
public boolean isIgnoringInfoMessages() {
return ignoreInfos;
}
/**
* Create a message specification to test a CompilationResult for a
* given set of info, warning, error, and fail messages.
* @param infos The set of info messages to test for. Specifying a non-null value
* for this parameter enables info message comparison.
* @param warnings The set of warning messages to test for - can pass null to indicate
* empty set.
* @param errors The set of error messages to test for - can pass null to indicate
* empty set.
* @param fails The set of fail or abort messages to test for - can pass null to indicate
* empty set.
*/
public MessageSpec(List infos, List warnings, List errors, List fails, List weaves) {
if (infos != null) {
this.infos = infos;
ignoreInfos = false;
} else {
this.infos = Collections.EMPTY_LIST;
}
this.warnings = ((warnings == null) ? Collections.EMPTY_LIST : warnings);
this.errors = ((errors == null) ? Collections.EMPTY_LIST : errors);
this.fails = ((fails == null) ? Collections.EMPTY_LIST : fails);
this.weaves = ((weaves == null) ? Collections.EMPTY_LIST : weaves);
}
/**
* Create a message specification to test a CompilationResult for a given
* set of info, warning, and error messages. The presence of any fail or
* abort messages in a CompilationResult will be a test failure.
*/
public MessageSpec(List infos, List warnings, List errors) {
this(infos,warnings,errors,null,null);
}
/**
* Create a message specification to test a CompilationResult for a given
* set of warning, and error messages. The presence of any fail or
* abort messages in a CompilationResult will be a test failure. Informational
* messages will be ignored.
*/
public MessageSpec(List warnings, List errors) {
this(null,warnings,errors,null,null);
}
}
public static class EmptyMessageSpec extends MessageSpec {
public EmptyMessageSpec() {
super(null,null);
}
}
/**
* Helper class representing the results of running a test program built
* by the compiler. Provides access to the standard out and error of the
* program, and the actual command that was executed.
*/
public static class RunResult {
private String command;
private String stdOut;
private String stdErr;
protected RunResult(String command, String stdOut, String stdErr) {
this.command = command;
this.stdOut = stdOut;
this.stdErr = stdErr;
}
/**
* Return the command that was executed, e.g. "java Driver".
*/
public String getCommand() { return command; }
/**
* The standard output from the run.
*/
public String getStdOut() {return stdOut;}
/**
* The standard error from the run.
*/
public String getStdErr() {return stdErr;}
/**
* Returns the command that was executed to produce this result.
*/
public String toString() { return command; }
}
/**
* Assert that no (non-informational) messages where produced during a compiler run.
*/
public void assertNoMessages(CompilationResult result) {
assertNoMessages(result,"Not expecting any compiler messages to be produced");
}
/**
* Assert that no (non-informational) messages where produced during a compiler run.
*/
public void assertNoMessages(CompilationResult result, String message) {
assertMessages(result, message,MessageSpec.EMPTY_MESSAGE_SET);
}
/**
* Assert that messages in accordance with the expected
message specification
* where produced during a compiler run.
*/
public void assertMessages(CompilationResult result, MessageSpec expected) {
assertMessages(result, "Compilation results did not meet expected messages specification",expected);
}
/**
* Assert that messages in accordance with the expected
message specification
* where produced during a compiler run.
*/
public void assertMessages(CompilationResult result, String message, MessageSpec expected) {
if (result == null) fail("Attempt to compare null compilation results against expected.");
List missingFails = copyAll(expected.fails);
List missingInfos = copyAll(expected.infos);
List missingWarnings = copyAll(expected.warnings);
List missingErrors = copyAll(expected.errors);
List missingWeaves = copyAll(expected.weaves);
List extraFails = copyAll(result.getFailMessages());
List extraInfos = copyAll(result.getInfoMessages());
List extraWarnings = copyAll(result.getWarningMessages());
List extraErrors = copyAll(result.getErrorMessages());
List extraWeaves = copyAll(result.getWeaveMessages());
compare(expected.fails,result.getFailMessages(),missingFails,extraFails);
compare(expected.warnings,result.getWarningMessages(),missingWarnings,extraWarnings);
compare(expected.errors,result.getErrorMessages(),missingErrors,extraErrors);
if (!expected.isIgnoringInfoMessages()) {
compare(expected.infos,result.getInfoMessages(),missingInfos,extraInfos);
}
compare(expected.weaves,result.getWeaveMessages(),missingWeaves,extraWeaves);
boolean infosEmpty = expected.isIgnoringInfoMessages() ? true: (missingInfos.isEmpty() && extraInfos.isEmpty());
if ( !(missingFails.isEmpty() && missingWarnings.isEmpty() && missingErrors.isEmpty() && missingWeaves.isEmpty() &&
extraFails.isEmpty() && extraWarnings.isEmpty() && extraErrors.isEmpty() && extraWeaves.isEmpty() && infosEmpty)) {
StringBuffer failureReport = new StringBuffer(message);
failureReport.append("\n");
if (!expected.isIgnoringInfoMessages()) {
addMissing(failureReport,"info",missingInfos);
}
addMissing(failureReport,"warning",missingWarnings);
addMissing(failureReport,"error",missingErrors);
addMissing(failureReport,"fail",missingFails);
addMissing(failureReport,"weaveInfo",missingWeaves);
if (!expected.isIgnoringInfoMessages()) {
addExtra(failureReport,"info",extraInfos);
}
addExtra(failureReport,"warning",extraWarnings);
addExtra(failureReport,"error",extraErrors);
addExtra(failureReport,"fail",extraFails);
addExtra(failureReport,"weaveInfo",extraWeaves);
failureReport.append("\ncommand was: ajc");
String[] args = result.getArgs();
for (int i = 0; i < args.length; i++) {
failureReport.append(" ");
failureReport.append(args[i]);
}
String report = failureReport.toString();
System.err.println(failureReport);
fail(message + "\n" + report);
}
}
/**
* Helper method to build a new message list for passing to a MessageSpec.
*/
protected List newMessageList(Message m1) {
List ret = new ArrayList();
ret.add(m1);
return ret;
}
/**
* Helper method to build a new message list for passing to a MessageSpec.
*/
protected List newMessageList(Message m1, Message m2) {
List ret = new ArrayList();
ret.add(m1);
ret.add(m2);
return ret;
}
/**
* Helper method to build a new message list for passing to a MessageSpec.
*/
protected List newMessageList(Message m1, Message m2, Message m3) {
List ret = new ArrayList();
ret.add(m1);
ret.add(m2);
ret.add(m3);
return ret;
}
/**
* Helper method to build a new message list for passing to a MessageSpec.
*/
protected List newMessageList(Message[] messages) {
List ret = new ArrayList();
for (int i = 0; i < messages.length; i++) {
ret.add(messages[i]);
}
return ret;
}
/**
* Perform a compilation and return the result.
* @param baseDir the base directory relative to which all relative paths and
* directories in the arguments will be interpreted.
* @param args the compiler arguments, as you would specify on the command-line.
* See the Ajc class for a description of the argument processing done in
* order to run the compilation in a sandbox.
* @see org.aspectj.tools.ajc.Ajc
*/
public CompilationResult ajc(File baseDir, String[] args) {
try {
ajc.setBaseDir(baseDir);
args = fixupArgs(args);
return ajc.compile(args);
} catch(IOException ioEx ) {
fail("IOException thrown during compilation: " + ioEx);
}
return null;
}
public File getSandboxDirectory() {
return ajc.getSandboxDirectory();
}
/**
* Indicate whether or not the sandbox should be emptied before the next compile.
* @see org.aspectj.tools.ajc.Ajc#setShouldEmptySandbox(boolean)
*/
public void setShouldEmptySandbox(boolean empty) {
ajc.setShouldEmptySandbox(empty);
}
public RunResult getLastRunResult() {
return lastRunResult;
}
public void testNothingForAntJUnit() {}
/**
* Run the given class (main method), and return the result in a RunResult. The program runs with
* a classpath containing the sandbox directory, runtime, testing-client, bridge, and
* util projects (all used by the Tester class), and any jars in the sandbox.
*/
public RunResult run(String className){
return run(className,new String[0],null);
}
public RunResult run(String className, String[] args, String classpath) {
return run(className,args,null,false);
}
/**
* Run the given class, and return the result in a RunResult. The program runs with
* a classpath containing the sandbox directory, runtime, testing-client, bridge, and
* util projects (all used by the Tester class), and any jars in the sandbox.
* @param args the arguments to pass to the program.
* @param classpath the execution classpath, the sandbox directory, runtime, testing-client,
* bridge, and util projects will all be appended to the classpath, as will any jars in
* the sandbox.
*/
public RunResult run(String className, String[] args, String classpath, boolean useLTW) {
lastRunResult = null;
StringBuffer cp = new StringBuffer();
if (classpath != null) {
cp.append(classpath);
cp.append(File.pathSeparator);
}
cp.append(ajc.getSandboxDirectory().getAbsolutePath());
cp.append(DEFAULT_CLASSPATH_ENTRIES);
getAnyJars(ajc.getSandboxDirectory(),cp);
classpath = cp.toString();
StringBuffer command = new StringBuffer("java -classpath ");
command.append(classpath);
command.append(" ");
command.append(className);
for (int i = 0; i < args.length; i++) {
command.append(" ");
command.append(args[i]);
}
PrintStream systemOut = System.out;
PrintStream systemErr = System.err;
ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
ByteArrayOutputStream baosErr = new ByteArrayOutputStream();
StringTokenizer strTok = new StringTokenizer(classpath,File.pathSeparator);
URL[] urls = new URL[strTok.countTokens()];
try {
for (int i = 0; i < urls.length; i++) {
urls[i] = new File(strTok.nextToken()).getCanonicalFile().toURL();
}
} catch (Exception malEx) {
fail("Bad classpath specification: " + classpath);
}
URLClassLoader cLoader;
if (useLTW) {
ClassLoader parent = getClass().getClassLoader();
cLoader = new WeavingURLClassLoader(urls,parent);
}
else {
cLoader = new URLClassLoader(urls,null);
}
try {
try {
Class testerClass = cLoader.loadClass("org.aspectj.testing.Tester");
Method setBaseDir = testerClass.getDeclaredMethod("setBASEDIR",new Class[] {File.class});
setBaseDir.invoke(null,new Object[] {ajc.getSandboxDirectory()});
} catch (Exception ex) {
fail ("Unable to prepare org.aspectj.testing.Tester for test run: " + ex);
}
System.setOut(new PrintStream(baosOut));
System.setErr(new PrintStream(baosErr));
Class toRun = cLoader.loadClass(className);
Method mainMethod = toRun.getMethod("main",new Class[] {String[].class});
mainMethod.invoke(null,new Object[] {args});
lastRunResult = new RunResult(command.toString(),new String(baosOut.toByteArray()),new String(baosErr.toByteArray()));
} catch(ClassNotFoundException cnf) {
fail("Can't find class: " + className);
} catch(NoSuchMethodException nsm) {
fail(className + " does not have a main method");
} catch (IllegalAccessException illEx) {
fail("main method in class " + className + " is not public");
} catch (InvocationTargetException invTgt) {
// the main method threw an exception...
fail("Exception thrown by " + className + ".main(String[]) :" + invTgt.getTargetException());
} finally {
System.setOut(systemOut);
System.setErr(systemErr);
}
return lastRunResult;
}
/**
* Any central pre-processing of args.
* This supplies aspectjrt.jar if available and classpath not set.
* @param args the String[] args to fix up
* @return the String[] args to use
*/
protected String[] fixupArgs(String[] args) {
if (null == args) {
return null;
}
int cpIndex = -1;
boolean hasruntime = false;
for (int i = 0; i < args.length-1; i++) {
args[i] = adaptToPlatform(args[i]);
if ("-classpath".equals(args[i])) {
cpIndex = i;
String next = args[i+1];
hasruntime = ((null != next)
&& (-1 != next.indexOf("aspectjrt.jar")));
}
}
if (-1 == cpIndex) {
String[] newargs = new String[args.length + 2];
newargs[0] = "-classpath";
newargs[1] = TestUtil.aspectjrtPath().getPath();
System.arraycopy(args, 0, newargs, 2, args.length);
args = newargs;
} else {
if (!hasruntime) {
cpIndex++;
String[] newargs = new String[args.length];
System.arraycopy(args, 0, newargs, 0, args.length);
newargs[cpIndex] = args[cpIndex] + File.pathSeparator
+ TestUtil.aspectjrtPath().getPath();
args = newargs;
}
}
return args;
}
private String adaptToPlatform(String s) {
String ret = s.replace(';',File.pathSeparatorChar);
//ret = ret.replace(':',File.pathSeparatorChar);
return ret;
}
private List copyAll(List in) {
if (in == Collections.EMPTY_LIST) return in;
List out = new ArrayList();
for (Iterator iter = in.iterator(); iter.hasNext();) {
out.add(iter.next());
}
return out;
}
/**
* Compare the set of expected messages against the set of actual messages,
* leaving in missingElements the set of messages that were expected but did not
* occur, and in extraElements the set of messages that occured but were not
* excpected
* @param expected the expected messages
* @param actual the actual messages
* @param missingElements the missing messages, when passed in must contain all of the expected messages
* @param extraElements the additional messages, when passed in must contain all of the actual messages
*/
private void compare(List expected, List actual, List missingElements, List extraElements) {
for (Iterator expectedIter = expected.iterator(); expectedIter.hasNext();) {
Message expectedMessage = (Message) expectedIter.next();
for (Iterator actualIter = actual.iterator(); actualIter.hasNext();) {
IMessage actualMessage = (IMessage) actualIter.next();
if (expectedMessage.matches(actualMessage)) {
missingElements.remove(expectedMessage);
extraElements.remove(actualMessage);
}
}
}
}
private void addMissing(StringBuffer buff,String type, List messages) {
if (!messages.isEmpty()) {
buff.append("Missing expected ");
buff.append(type);
buff.append(" messages:\n");
for (Iterator iter = messages.iterator(); iter.hasNext();) {
buff.append("\t");
buff.append(iter.next().toString());
buff.append("\n");
}
}
}
private void addExtra(StringBuffer buff, String type, List messages) {
if (!messages.isEmpty()) {
buff.append("Unexpected ");
buff.append(type);
buff.append(" messages:\n");
for (Iterator iter = messages.iterator(); iter.hasNext();) {
buff.append("\t");
buff.append(iter.next().toString());
buff.append("\n");
}
}
}
// add any jars in the directory to the classpath
private void getAnyJars(File dir,StringBuffer buff) {
File[] files = dir.listFiles();
for (int i = 0; i < files.length; i++) {
if (files[i].getName().endsWith(".jar")) {
buff.append(File.pathSeparator);
buff.append(files[i].getAbsolutePath());
} else if (files[i].isDirectory()) {
getAnyJars(files[i],buff);
}
}
}
/* (non-Javadoc)
* @see junit.framework.TestCase#setUp()
*/
protected void setUp() throws Exception {
super.setUp();
ajc = new Ajc();
}
/* (non-Javadoc)
* @see junit.framework.TestCase#tearDown()
*/
protected void tearDown() throws Exception {
super.tearDown();
//ajc = null;
}
}