From 9bf0a7543072b66835b2417ce932fc34da4e45ee Mon Sep 17 00:00:00 2001 From: acolyer Date: Wed, 4 Aug 2004 11:59:41 +0000 Subject: [PATCH] Added support to ease programmatic testing of compiler (AjcTestCase class and accompanying support). --- org.aspectj.ajdt.core/.classpath | 27 +- .../src/org/aspectj/tools/ajc/Main.java | 9 +- .../testsrc/org/aspectj/tools/ajc/Ajc.java | 396 +++++++++++ .../org/aspectj/tools/ajc/AjcTestCase.java | 646 ++++++++++++++++++ .../aspectj/tools/ajc/AjcTestCaseTest.java | 51 ++ .../aspectj/tools/ajc/CompilationResult.java | 202 ++++++ .../org/aspectj/tools/ajc/MainTest.java | 13 +- 7 files changed, 1326 insertions(+), 18 deletions(-) create mode 100644 org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/Ajc.java create mode 100644 org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCase.java create mode 100644 org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCaseTest.java create mode 100644 org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/CompilationResult.java diff --git a/org.aspectj.ajdt.core/.classpath b/org.aspectj.ajdt.core/.classpath index 0f9073da2..32c74b39c 100644 --- a/org.aspectj.ajdt.core/.classpath +++ b/org.aspectj.ajdt.core/.classpath @@ -1,16 +1,17 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/org.aspectj.ajdt.core/src/org/aspectj/tools/ajc/Main.java b/org.aspectj.ajdt.core/src/org/aspectj/tools/ajc/Main.java index 1ec80de03..fa790ae7c 100644 --- a/org.aspectj.ajdt.core/src/org/aspectj/tools/ajc/Main.java +++ b/org.aspectj.ajdt.core/src/org/aspectj/tools/ajc/Main.java @@ -192,6 +192,11 @@ public class Main { return ourHandler; } + // for unit testing... + void setController(CommandController controller) { + this.controller = controller; + } + /** * Run without throwing exceptions but optionally using System.exit(..). * This sets up a message handler which emits messages immediately, @@ -284,7 +289,7 @@ public class Main { boolean passed = command.runCommand(args, holder); if (report(passed, holder) && controller.incremental()) { // final boolean onCommandLine = controller.commandLineIncremental(); - while (controller.doRepeatCommand()) { + while (controller.doRepeatCommand(command)) { holder.clearMessages(); if (controller.buildFresh()) { continue outer; @@ -646,7 +651,7 @@ public class Main { } /** @return false if we should quit, true to do another command */ - boolean doRepeatCommand() { + boolean doRepeatCommand(ICommand command) { if (!running) { return false; } diff --git a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/Ajc.java b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/Ajc.java new file mode 100644 index 000000000..5df4feae5 --- /dev/null +++ b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/Ajc.java @@ -0,0 +1,396 @@ +/* ******************************************************************* + * 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.FilenameFilter; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import junit.framework.AssertionFailedError; + +import org.aspectj.bridge.ICommand; +import org.aspectj.bridge.IMessage; +import org.aspectj.bridge.IMessageHandler; +import org.aspectj.bridge.MessageHandler; +import org.aspectj.util.FileUtil; + +/** + * The Ajc class is intended for use as part of a unit-test suite, it drives + * the AspectJ compiler and lets you check the compilation results. Compilations + * run in a sandbox that is created in C:\temp\ajcSandbox or /tmp/ajcSandbox + * depending on your platform. + *

+ * The expected usage of Ajc is through the TestCase superclass, + * AjcTestCase, which provides helper methods that conveniently + * drive the base functions exposed by this class. + *

+ * @see org.aspectj.tools.ajc.AjcTestCase + */ +public class Ajc { + + private static final String SANDBOX_NAME = "ajcSandbox"; + private static final String TESTER_PATH = + "../testing-client/bin" + File.pathSeparator + "../runtime/bin"; + private CompilationResult result; + private File sandbox; + private File baseDir; + private Main main; + private String[] ajcArgs; + private int incrementalStage = 10; + private boolean shouldEmptySandbox = true; + private AjcCommandController controller; + + /** + * Constructs a new Ajc instance, with a new AspectJ compiler + * inside. + */ + public Ajc() { + main = new Main(); + controller = new AjcCommandController(); + main.setController(controller); + } + + /** + * By default, each call to compile creates a new + * sandbox (C:\temp\ajcSandbox\ajtTestxxx.tmp, or /tmp/ajcSandbox/ajcTestxxx.tmp + * depending on your platform). To write a test that performs multiple + * (non-incremental) compiles, building on the results of previous + * compilations, set 'should empty sandbox' to false after the first + * compile, which will cause subsequent compiles in the test to use the + * same directory and contents. + */ + public void setShouldEmptySandbox(boolean empty) { + this.shouldEmptySandbox = empty; + } + + /** + * Call the compiler with the given arguments (args are exactly the same + * as you would pass to ajc on the command-line). The results of the + * compile are returned in a CompilationResult, which + * provides for easy testing of results. + *

The compilation happens in a sandbox (C:\temp\ajcSandbox\ajTestxxx.tmp or + * /tmp/ajcSandbox/ajcTestxxx.tmp depending on platform). Compiler arguments are + * adapted to the sandbox as follows.

+ *

For every file or directory listed in an argument (source file, or component + * of inpath, aspectpath, sourceroots, classpath,...), if the file is specified + * using an absolute path then it is left unchanged, but if the file is specified + * using a relative path, and a base directory (see setBaseDir) has been provided, + * then files/directories are copied from the base directory to the sandbox, and the + * compiler arguments adjusted to reflect their new location. + *

+ *

For example, given a baseDir of "tests/pr12345" and a compile command: + * "ajc src/A.java src/B.java", the files in + *

+	 *    tests/pr12345/
+	 *                  src/
+	 *                      A.java
+	 *                      B.java
+	 * 
+ * are copied to: + *
+	 *     ajcSandbox/ajcTestxxx.tmp/
+	 *                               src/
+	 *                                   A.java
+	 *                                   B.java
+	 * 
+ *

+ * If no classpath is specified (no -classpath in the arguments) the classpath will + * be set to include the sandbox directory, testing-client/bin (for the Tester class), + * and runtime/bin (for the AspectJ runtime). If a classpath is specified, + * then any relative directories in it will be made relative to the sandbox, and + * the testing-client and runtime bin directories are also added. + *

+ *

+ * If no output directory is specified (no -d in the arguments), the output directory + * is set to the sandbox. If a directory is specified, and the path is relative, it + * will be made relative to the sandbox. + *

+ * + *

+ * @param args The compiler arguments. + * @return a CompilationResult object with all the messages produced by + * the compiler, a description of the ajc command that was issued, + * and the standard output and error of the compile (excluding messages + * which are provided separately) + * @throws IOException + * @see org.aspectj.tools.ajc.CompilationResult + */ + public CompilationResult compile(String[] args) throws IOException { + incrementalStage = 10; + return compile(args,false); + } + + private CompilationResult compile(String[] args,boolean isIncremental) throws IOException { + result = null; + ajcArgs = args; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + PrintStream pout = new PrintStream(out); + ByteArrayOutputStream err = new ByteArrayOutputStream(); + PrintStream perr = new PrintStream(err); + PrintStream systemOut = System.out; + PrintStream systemErr = System.err; + System.setOut(pout); + System.setErr(perr); + + List fails = new ArrayList(); + List errors = new ArrayList(); + List warnings = new ArrayList(); + List infos = new ArrayList(); + + try { + if (!isIncremental && shouldEmptySandbox) { + createEmptySandbox(); + } + args = adjustToSandbox(args,!isIncremental); + MessageHandler holder = new MessageHandler(); + main.setHolder(holder); + if (isIncremental) { + controller.doIncremental(holder); + } else { + main.runMain(args,false); + } + addMessagesTo(infos,holder.getMessages(IMessage.INFO,false)); + addMessagesTo(warnings,holder.getWarnings()); + addMessagesTo(errors,holder.getErrors()); + addMessagesTo(fails,holder.getMessages(IMessage.FAIL,true)); + String stdOut = new String(out.toByteArray()); + String stdErr = new String(err.toByteArray()); + result = new CompilationResult(args,stdOut,stdErr,infos,errors,warnings,fails); + } finally { + System.setOut(systemOut); + System.setErr(systemErr); + } + return result; + } + + /** + * After compiling for the first time with compile(), if the -incremental option was specified + * you can do as many subsequent incremental compiles as you like by calling this method. + *

+ * Throws an IllegalStateException if you try and call this method without first doing a compile + * that specified the -incremental option. + *

+ * @return A CompilationResult giving the results of the most recent increment. + * @throws IOException + */ + public CompilationResult doIncrementalCompile() throws IOException { + if ((ajcArgs == null) || !isIncremental(ajcArgs)) { + throw new IllegalStateException("Can't do incremental compile unless -incremental specified and first compile has taken place"); + } + incrementalStage += 10; + return compile(ajcArgs,true); + } + + /** + * Return the result of the last compile or incremental compile. This is the same as the + * return value from the compile() or doIncrementalCompile() methods. + */ + public CompilationResult getLastCompilationResult() { + return result; + } + + /** + * Get the sandbox directory used for the compilation. + */ + public File getSandboxDirectory() { + if (sandbox == null) {createEmptySandbox();} + return sandbox; + } + + /** + * Set the base directory relative to which all relative paths specified in the arguments to a compile will be + * interpreted. + */ + public void setBaseDir(File dir) { + if ((dir != null) && !dir.isDirectory()) throw new IllegalArgumentException(dir.getPath() + " is not a directory"); + baseDir = dir; + } + + private void addMessagesTo(List aList, IMessage[] messages) { + for (int i = 0; i < messages.length; i++) { + aList.add(messages[i]); + } + } + + private boolean isIncremental(String[] args) { + for (int i = 0; i < args.length; i++) { + if (args[i].trim().equals("-incremental")) return true; + } + return false; + } + + private void createEmptySandbox() { + String os = System.getProperty("os.name"); + File tempDir = null; + // AMC - I did this rather than use the JDK default as I hate having to go look + // in c:\documents and settings\......... for the results of a failed test. + if (os.startsWith("Windows")) { + tempDir = new File("C:\\temp"); + } else { + tempDir = new File("/tmp"); + } + File sandboxRoot = new File(tempDir,SANDBOX_NAME); + if (!sandboxRoot.exists()) { + sandboxRoot.mkdir(); + } + + FileUtil.deleteContents(sandboxRoot); + + try { + sandbox = File.createTempFile("ajcTest",".tmp",sandboxRoot); + sandbox.delete(); + sandbox.mkdir(); + } catch (IOException ioEx) { + throw new AssertionFailedError("Unable to create sandbox directory for test"); + } + } + + /** + * Make every relative file name and dir be absolute under sandbox + * Add TESTER_PATH to classpath + */ + private String[] adjustToSandbox(String[] args,boolean doCopy) throws IOException { + String[] newArgs = new String[args.length]; + boolean hasClasspath = false; + boolean hasOutdir = false; + for (int i = 0; i < args.length; i++) { + newArgs[i] = args[i]; + if (FileUtil.hasSourceSuffix(args[i])) { + File f = new File(args[i]); + //newArgs[i] = new File(baseDir,args[i]).getAbsolutePath(); // might be quicker? + newArgs[i] = adjustFileOrDir(f,doCopy).getAbsolutePath(); + } else { + if ((args[i].equals("-aspectpath") || + args[i].equals("-inpath") || + args[i].equals("-injars") || + args[i].equals("-classpath") || + args[i].equals("-sourceroots") || + args[i].equals("-Xlintfile") || + args[i].equals("-extdirs") || + args[i].equals("-d")) && + args.length > (i+1)) { + newArgs[i] = args[i]; + StringBuffer buff = new StringBuffer(); + boolean copyThisTime = doCopy; + if (args[i].equals("-d")) { + copyThisTime = false; + hasOutdir = true; + } + StringTokenizer strTok = new StringTokenizer(args[++i],File.pathSeparator); + while (strTok.hasMoreTokens()) { + File f = new File(strTok.nextToken()); + buff.append(adjustFileOrDir(f,copyThisTime).getAbsolutePath()); + if (strTok.hasMoreTokens()) buff.append(File.pathSeparator); + } + newArgs[i] = buff.toString(); + if (args[i-1].equals("-classpath")) { + hasClasspath = true; + newArgs[i] = newArgs[i] + File.pathSeparator + TESTER_PATH; + } + } else { + // could be resource file + File f = new File(args[i]); + if (f.exists()) { + newArgs[i] = adjustFileOrDir(f,doCopy).getAbsolutePath(); + } + } + } + } + if (!hasClasspath) { + String[] oldArgs = newArgs; + newArgs = new String[oldArgs.length + 2]; + System.arraycopy(oldArgs,0,newArgs,0,oldArgs.length); + newArgs[oldArgs.length] = "-classpath"; + newArgs[oldArgs.length + 1] = TESTER_PATH + File.pathSeparator + getSandboxDirectory().getAbsolutePath(); + } + if (!hasOutdir) { + String[] oldArgs = newArgs; + newArgs = new String[oldArgs.length + 2]; + System.arraycopy(oldArgs,0,newArgs,0,oldArgs.length); + newArgs[oldArgs.length] = "-d"; + newArgs[oldArgs.length + 1] = getSandboxDirectory().getPath(); + } + return newArgs; + } + + private File adjustFileOrDir(File from,boolean doCopy) throws IOException { + File to = from; + File ret = from; + if (!from.isAbsolute()) { + ret = new File(sandbox,from.getPath()); + File fromParent = from.getParentFile(); + String relativeToPath = (fromParent != null) ? (fromParent.getPath() + File.separator) : ""; + if (baseDir != null) { + from = new File(baseDir,from.getPath()); + } + if (!from.exists()) return ret; + if (doCopy) { + // harness requires that any files with the same name, and a different extension, + // get copied too (e.g. .out, .err, .event files) + if (from.isFile()) { + final String prefix = from.getName().substring(0,from.getName().lastIndexOf('.')); + String[] toCopy = from.getParentFile().list(new FilenameFilter() { + public boolean accept(File dir, String name) { + if (name.indexOf('.') == -1) return false; + String toMatch = name.substring(0,name.lastIndexOf('.')); + return (toMatch.equals(prefix)); + } + }); + for (int i = 0; i < toCopy.length; i++) { + String toPath = relativeToPath + toCopy[i]; + to = new File(sandbox,toPath); + FileUtil.copyFile(new File(from.getParentFile(),toCopy[i]), + to); + } + } else { + FileUtil.copyFile(from,ret); + } + } + } + return ret; + } +} + +/* + * So that we can drive incremental compilation easily from a unit + * test. + */ +class AjcCommandController extends Main.CommandController { + + private ICommand command; + + /* (non-Javadoc) + * @see org.aspectj.tools.ajc.Main.CommandController#doRepeatCommand() + */ + boolean doRepeatCommand(ICommand command) { + this.command = command; + return false; // ensure that control returns to caller + } + + /* (non-Javadoc) + * @see org.aspectj.tools.ajc.Main.CommandController#running() + */ + public boolean running() { + return false; // so that we can come back for more... + } + + public void doIncremental(IMessageHandler handler) { + if (command == null) throw new IllegalArgumentException("Can't repeat command until it has executed at least once!"); + command.repeatCommand(handler); + } +} diff --git a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCase.java b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCase.java new file mode 100644 index 000000000..acfc9ac0f --- /dev/null +++ b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCase.java @@ -0,0 +1,646 @@ +/* ******************************************************************* + * 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 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 { + + /** + * The Ajc (compiler) instance used for thet test. Created afresh + * during the test setup. + */ + protected Ajc ajc; + + private 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"; + + /** + * 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 contains text. + */ + 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. + *

+ */ + public Message(int line, String srcFile, String text, ISourceLocation[] seeAlso) { + this.line = line; + StringBuffer srcFileName = new StringBuffer(); + if (srcFile != null) { + char[] chars = srcFile.toCharArray(); + for (int i = 0; i < chars.length; i++) { + if ((chars[i] == '\\') || (chars[i] == '/')) { + srcFileName.append(File.separator); + } else { + srcFileName.append(chars[i]); + } + } + this.sourceFileName = srcFileName.toString(); + } + this.text = text; + this.seeAlsos = seeAlso; + } + + /** + * Create a message spec that will match any compiler message where + * the message text includes 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 CompilationResults. + */ + 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); + + boolean ignoreInfos = true; + public List fails; + public List infos; + public List warnings; + public List errors; + + /** + * 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) { + 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); + } + + /** + * 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); + } + + /** + * 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); + } + } + + /** + * 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 extraFails = copyAll(result.getFailMessages()); + List extraInfos = copyAll(result.getInfoMessages()); + List extraWarnings = copyAll(result.getWarningMessages()); + List extraErrors = copyAll(result.getErrorMessages()); + 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); + } + + boolean infosEmpty = expected.isIgnoringInfoMessages() ? true: (missingInfos.isEmpty() && extraInfos.isEmpty()); + if ( !(missingFails.isEmpty() && missingWarnings.isEmpty() && missingErrors.isEmpty() && + extraFails.isEmpty() && extraWarnings.isEmpty() && extraErrors.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); + if (!expected.isIgnoringInfoMessages()) { + addExtra(failureReport,"info",extraInfos); + } + addExtra(failureReport,"warning",extraWarnings); + addExtra(failureReport,"error",extraErrors); + addExtra(failureReport,"fail",extraFails); + 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(); + 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); + return ajc.compile(args); + } catch(IOException ioEx ) { + fail("IOException thrown during compilation: " + ioEx); + } + return null; + } + + /** + * 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); + } + + /** + * 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); + } + + /** + * 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) { + RunResult result = 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 = new URLClassLoader(urls,null); + //System.out.println(cLoader.getParent()); + 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); + } + Class toRun = cLoader.loadClass(className); + Method mainMethod = toRun.getMethod("main",new Class[] {String[].class}); + System.setOut(new PrintStream(baosOut)); + System.setErr(new PrintStream(baosErr)); + mainMethod.invoke(null,new Object[] {args}); + result = 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 result; + } + + 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(); + } +} diff --git a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCaseTest.java b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCaseTest.java new file mode 100644 index 000000000..79a06dfce --- /dev/null +++ b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/AjcTestCaseTest.java @@ -0,0 +1,51 @@ +/* ******************************************************************* + * 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.File; + +import org.aspectj.util.FileUtil; + +/** + * @author colyer + * Exercise the features of the AjcTestCase class and check they do as + * expected + */ +public class AjcTestCaseTest extends AjcTestCase { + + public void testCompile() { + File baseDir = new File("../tests/base/test106"); + String[] args = new String[] {"Driver.java","pkg/Obj.java"}; + CompilationResult result = ajc(baseDir,args); + assertNoMessages(result); + RunResult rresult = run("Driver",new String[0],null); + System.out.println(rresult.getStdOut()); + } + + public void testIncrementalCompile() throws Exception { + File baseDir = new File("../tests/incrementalju/initialTests/classAdded"); + String[] args = new String[] {"-sourceroots","src","-d",".","-incremental"}; + CompilationResult result = ajc(baseDir,args); + assertNoMessages(result); + RunResult rr = run("main.Main",new String[0],null); + // prepare for increment + FileUtil.copyFile(new File(baseDir,"src.20/main/Main.java"), + new File(ajc.getSandboxDirectory(),"src/main/Main.java")); + assertFalse("main.Target does not exist",new File(ajc.getSandboxDirectory(),"main/Target.class").exists()); + result = ajc.doIncrementalCompile(); + assertNoMessages(result); + assertTrue("main.Target created",new File(ajc.getSandboxDirectory(),"main/Target.class").exists()); + rr = run("main.Main",new String[0],null); + System.out.println(rr.getStdOut()); + } + +} diff --git a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/CompilationResult.java b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/CompilationResult.java new file mode 100644 index 000000000..31e2afdfc --- /dev/null +++ b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/CompilationResult.java @@ -0,0 +1,202 @@ +/* ******************************************************************* + * 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.util.Collections; +import java.util.Iterator; +import java.util.List; + +/** + * Utility class that makes the results of a compiler run available. + *

+ * Instances of this class are returned by the Ajc.compile() and + * doIncrementalCompile() methods (and the AjcTestCase.ajc() wrapper). + *

+ *

+ * This class provides a useful toString() method that is very helpful when + * debugging or creating messages for assert statements. + *

+ *

Note that the stdOut and stdErr captured from the compiler run do + * not contain any rendered messages - these are in the messages lists + * instead. Therefore for many compiler runs, they will be empty. + *

+ */ +public class CompilationResult { + + private String[] args; + private String stdOut; + private String stdErr; + private List /*IMessage*/ infoMessages; + private List /*IMessage*/ errorMessages; + private List /*IMessage*/ warningMessages; + private List /*IMessage*/ failMessages; + + /** + * Build a compilation result - called by the Ajc.compile and + * Ajc.doIncrementalCompile methods. Should be no need for you + * to construct an instance yourself. + */ + protected CompilationResult( + String[] args, + String stdOut, + String stdErr, + List infoMessages, + List errorMessages, + List warningMessages, + List failMessages) { + this.args = args; + this.stdOut = stdOut; + this.stdErr = stdErr; + this.infoMessages = (infoMessages == null) ? Collections.EMPTY_LIST : infoMessages; + this.errorMessages = (errorMessages == null) ? Collections.EMPTY_LIST : errorMessages; + this.warningMessages = (warningMessages == null) ? Collections.EMPTY_LIST : warningMessages; + this.failMessages = (failMessages == null) ? Collections.EMPTY_LIST : failMessages; + } + + /** + * The arguments that were passed to the compiler. + */ + public String[] getArgs() { return args; } + /** + * The standard output written by the compiler, excluding any messages. + */ + public String getStandardOutput() { return stdOut; } + /** + * The standard error written by the compiler, excluding any messages. + */ + public String getStandardError() { return stdErr; } + + /** + * True if the compiler issued any messages of any kind. + */ + public boolean hasMessages() { return (hasInfoMessages() || hasErrorMessages() || hasWarningMessages() || hasFailMessages()); } + /** + * True if the compiler issued one or more informational messages. + */ + public boolean hasInfoMessages() { return !infoMessages.isEmpty(); } + /** + * True if the compiler issued one or more error messages. + */ + public boolean hasErrorMessages() { return !errorMessages.isEmpty(); } + /** + * True if the compiler issued one or more warning messages. + */ + public boolean hasWarningMessages() { return !warningMessages.isEmpty(); } + /** + * True if the compiler issued one or more fail or abort messages. + */ + public boolean hasFailMessages() { return !failMessages.isEmpty(); } + + /** + * The informational messages produced by the compiler. The list + * entries are the IMessage objects created during the + * compile - so that you can programmatically test source locations + * etc. etc.. It may often be easier to use the assertMessages + * helper methods defined in the AjcTestCase class to test for messages + * though. + * @see org.aspectj.tools.ajc.AjcTestCase + */ + public List /*IMessage*/ getInfoMessages() { return infoMessages; } + /** + * The error messages produced by the compiler. The list + * entries are the IMessage objects created during the + * compile - so that you can programmatically test source locations + * etc. etc.. It may often be easier to use the assertMessages + * helper methods defined in the AjcTestCase class to test for messages + * though. + * @see org.aspectj.tools.ajc.AjcTestCase + */ + public List /*IMessage*/ getErrorMessages() { return errorMessages; } + /** + * The warning messages produced by the compiler. The list + * entries are the IMessage objects created during the + * compile - so that you can programmatically test source locations + * etc. etc.. It may often be easier to use the assertMessages + * helper methods defined in the AjcTestCase class to test for messages + * though. + * @see org.aspectj.tools.ajc.AjcTestCase + */ + public List /*IMessage*/ getWarningMessages() { return warningMessages; } + /** + * The fail or abort messages produced by the compiler. The list + * entries are the IMessage objects created during the + * compile - so that you can programmatically test source locations + * etc. etc.. It may often be easier to use the assertMessages + * helper methods defined in the AjcTestCase class to test for messages + * though. + * @see org.aspectj.tools.ajc.AjcTestCase + */ + public List /*IMessage*/ getFailMessages() { return failMessages; } + + /** + * Returns string containing message count summary, list of messages + * by type, and the actual ajc compilation command that was issued. + */ + public String toString() { + StringBuffer buff = new StringBuffer(); + buff.append("AspectJ Compilation Result:\n"); + int totalMessages = infoMessages.size() + warningMessages.size() + errorMessages.size() + failMessages.size(); + buff.append(totalMessages); + buff.append(" messages"); + if (totalMessages > 0) { + buff.append(" ("); + buff.append(infoMessages.size()); + buff.append(" info, "); + buff.append(warningMessages.size()); + buff.append(" warning, "); + buff.append(errorMessages.size()); + buff.append(" error, "); + buff.append(failMessages.size()); + buff.append(" fail)"); + } + buff.append("\n"); + int msgNo = 1; + for (Iterator iter = failMessages.iterator(); iter.hasNext();) { + buff.append("[fail "); + buff.append(msgNo++); + buff.append("] "); + buff.append(iter.next().toString()); + buff.append("\n"); + } + msgNo = 1; + for (Iterator iter = errorMessages.iterator(); iter.hasNext();) { + buff.append("[error "); + buff.append(msgNo++); + buff.append("] "); + buff.append(iter.next().toString()); + buff.append("\n"); + } + msgNo = 1; + for (Iterator iter = warningMessages.iterator(); iter.hasNext();) { + buff.append("[warning "); + buff.append(msgNo++); + buff.append("] "); + buff.append(iter.next().toString()); + buff.append("\n"); + } + msgNo = 1; + for (Iterator iter = infoMessages.iterator(); iter.hasNext();) { + buff.append("[info "); + buff.append(msgNo++); + buff.append("] "); + buff.append(iter.next().toString()); + buff.append("\n"); + } + buff.append("\ncommand was: 'ajc"); + for (int i = 0; i < args.length; i++) { + buff.append(' '); + buff.append(args[i]); + } + buff.append("'\n"); + return buff.toString(); + } +} diff --git a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/MainTest.java b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/MainTest.java index 13d7216dd..aa2110c03 100644 --- a/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/MainTest.java +++ b/org.aspectj.ajdt.core/testsrc/org/aspectj/tools/ajc/MainTest.java @@ -13,13 +13,12 @@ package org.aspectj.tools.ajc; import java.util.ArrayList; - -import junit.framework.TestCase; +import java.util.ResourceBundle; /** * */ -public class MainTest extends TestCase { +public class MainTest extends AjcTestCase { public void testMainbare() { ArrayList list = new ArrayList(); @@ -32,4 +31,12 @@ public class MainTest extends TestCase { // assertTrue(-1 != ((String)o).indexOf("-aspectpath")); // assertTrue(-1 != ((String)o).indexOf("-incremental")); } + + public void testDashX() { + String xoptionText = ResourceBundle.getBundle("org.aspectj.ajdt.ajc.messages").getString("xoption.usage"); + xoptionText = xoptionText.substring("{compiler.name}".length()); + CompilationResult result = ajc(null,new String[] {"-X"}); + assertMessages(result,"Expecting xoptions usage message", + new MessageSpec(null,null,null,newMessageList(new Message(xoptionText)))); + } } -- 2.39.5