AjcTaskTest.java

/* *******************************************************************
 * Copyright (c) 1999-2001 Xerox Corporation
 *               2002 Palo Alto Research Center, Incorporated (PARC)
 *               2003 Contributors
 *               2005 Contributors
 * All rights reserved.
 * This program and the accompanying materials are made available
 * under the terms of the Eclipse Public License v 2.0
 * which accompanies this distribution and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt
 *
 * Contributors:
 *     Xerox/PARC     initial implementation
 *     IBM            ongoing maintenance
 * ******************************************************************/

package org.aspectj.tools.ant.taskdefs;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.tools.ant.BuildEvent;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.selectors.FilenameSelector;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.tools.ant.taskdefs.AjcTask.CompilerArg;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

import junit.framework.TestCase;

import static org.junit.Assert.assertNotEquals;

/**
 * AjcTask test cases. Please put new ones with others between "-------" comments.
 * <p>
 * Some API tests, but mostly functional tests driving the task execute using data in ../taskdefs/testdata. This will
 * re-run in forked mode for any nonfailing compile if aspectjtools-dist is built into
 * ../aj-build/dist/tools/lib/aspectjtools.jar.
 */
public class AjcTaskTest extends TestCase {

  private static final Class NO_EXCEPTION = null;
  private static final String NOFILE = "NOFILE";

  private static final File tempDir;
  private static final String aspectjtoolsJar;
  private static final String testdataDir;
  private static final StringBuffer MESSAGES = new StringBuffer();
  /**
   * accept writable .class files
   */
  private static final FileFilter PICK_CLASS_FILES;

  static {
    tempDir = new File("IncrementalAjcTaskTest-temp");
    String toolsPath = "../aj-build/dist/tools/lib/aspectjtools.jar";
    File toolsjar = new File(toolsPath);
    if (toolsjar.canRead())
      aspectjtoolsJar = toolsjar.getAbsolutePath();
    else {
      aspectjtoolsJar = null;
      System.out.println("AjcTaskTest not forking - build aspectjtools-dist to get " + toolsPath);
    }
    File dir = new File("../taskdefs/testdata");
    testdataDir = dir.canRead() && dir.isDirectory() ? dir.getAbsolutePath() : null;
    PICK_CLASS_FILES = file -> (null != file) && file.isFile() && file.canWrite() && file.getPath().endsWith(".class");
  }

  /**
   * Check that aspectjtools are found on the classpath, reporting any errors to System.err.
   * <p>
   * Run multiple times with different classpaths. This should find variants
   * <ul>
   *   <li>aspectjtools.jar,</li>
   *   <li>aspectj-tools.jar,</li>
   *   <li>aspectj-tools-1.1.jar,</li>
   *   <li>aspectjtools-1.0.6.jar</li>
   * </ul>
   * but not
   * <ul>
   *   <li>aspectjrt.jar,</li>
   *   <li>aspectj/tools.jar.</li>
   * </ul>
   * Use testing aspect to stub out {@code System.getProperty("java.class.path")}.
   *
   * @param args a string array; the first element is the expected path, if any
   */
  public static void main(String[] args) {
    java.io.File toolsjar = AjcTask.findAspectjtoolsJar();
    if ((null == args) || (0 == args.length))
      if (null != toolsjar)
        System.err.println("FAIL - not expected: " + toolsjar);
      else if ("-help".equals(args[0]))
        System.out.println("java " + AjcTaskTest.class.getName() + " <expectedPathToAspectjtoolsJar>");
      else if (null == toolsjar)
        System.err.println("FAIL - expected: " + args[0]);
      else {
        String path = toolsjar.getAbsolutePath();
        if (!path.equals(args[0]))
          System.err.println("FAIL - expected: " + args[0] + " actual: " + path);
      }
  }

  public static void collectMessage(String s) {
    MESSAGES.append(s);
  }

  private static void deleteTempDir() {
    if ((null != tempDir) && tempDir.exists()) {
      FileUtil.deleteContents(tempDir);
      tempDir.delete();
      // when tempDir not used...
      if (null != testdataDir) {
        File dataDir = new File(testdataDir);
        if (dataDir.canRead())
          FileUtil.deleteContents(dataDir, PICK_CLASS_FILES, false);
      }
    }
  }

  private static File getTempDir() {
    return tempDir;
  }

  public AjcTaskTest(String name) {
    super(name);
  }

  @Override
  public void tearDown() {
    deleteTempDir();
    MESSAGES.setLength(0);
  }

  private void checkRun(AjcTask task, String exceptionString) {
    try {
      task.execute();
      assertNull(exceptionString);
    }
    catch (BuildException e) {
      if (null == exceptionString)
        fail("unexpected " + e.getMessage());
      else {
        String m = e.getMessage();
        if (null == m)
          fail("not " + exceptionString);
        else if (!m.contains(exceptionString))
          assertEquals(exceptionString, e.getMessage());
      }
    }
  }

  private void checkContains(String[] cmd, String option, boolean contains) {
    for (String s : cmd) {
      if (option.equals(s)) {
        if (contains)
          return;
        else
          fail("not expecting " + option + " in " + Arrays.asList(cmd));
      }
    }
    if (contains)
      fail("expecting " + option + " in " + Arrays.asList(cmd));
  }

  protected AjcTask getTask(String input) {
    return getTask(input, getTempDir());
  }

  protected AjcTask getTask(String input, File destDir) {
    AjcTask task = new AjcTask();
    Project p = new Project();
    task.setProject(p);
    if (null != destDir)
      task.setDestdir(destDir);
    if (NOFILE.equals(input))
      ;// add nothing
    else if (input.endsWith(".lst")) {
      if (input.contains(","))
        throw new IllegalArgumentException("lists not supported: " + input);
      else if (null == testdataDir)
        throw new Error("testdata not found - run in ../taskdefs");
      else {
        String path = testdataDir + File.separator + input;
        task.setArgfiles(new Path(task.getProject(), path));
      }
    }
    else if ((input.endsWith(".java") || input.endsWith(".aj"))) {
      FilenameSelector fns = new FilenameSelector();
      fns.setName(input);
      task.addFilename(fns);
    }
    else {
      String path = testdataDir + File.separator + input;
      task.setSourceRoots(new Path(task.getProject(), path));
    }
    task.setClasspath(new Path(p, "../lib/test/aspectjrt.jar"));
    return task;
  }

  /**
   * used in testMessageHolderClassName
   */
  public static class InfoHolder extends MessageHandler {
    public InfoHolder() {}

    @Override
    public boolean handleMessage(IMessage message) {
      if (0 == IMessage.INFO.compareTo(message.getKind()))
        AjcTaskTest.collectMessage(message.getMessage());
      return true;
    }
  }

  /**
   * used in testMessageHolderClassName
   */
  public static class Holder extends MessageHandler {
    public Holder() {}

    @Override
    public boolean handleMessage(IMessage message) {
      IMessage.Kind kind = message.getKind();
      if (IMessage.ERROR.isSameOrLessThan(kind)) {
        String m = kind.toString();
        AjcTaskTest.collectMessage(m.substring(0, 1));
      }
      return true;
    }
  }

  // ------------------------------------------------------
  // Start of test cases
  // ------------------------------------------------------

  public void testNullDestDir() {
    AjcTask task = getTask(NOFILE, null);
    String[] cmd = task.makeCommand();

    for (String s : cmd)
      assertNotEquals("-d", s);
  }

  public void testOutputRequirement() {
    AjcTask task = getTask("default.lst");
    checkRun(task, null);

    // copyInJars now just emits warning b/c unused
    task = getTask("default.lst", null);
    task.setCopyInjars(true);
    checkRun(task, null);

    // sourceRootCopyFilter requires destDir
    task = getTask("default.lst", null);
    task.setSourceRootCopyFilter("**/*.java");
    checkRun(task, "sourceRoot");
  }

  public void testSourceRootCopyFilter() {
    // sourceRootCopyFilter works..
    File destDir = getTempDir();
    assertTrue("unable to create " + destDir, destDir.canRead() || destDir.mkdirs());
    AjcTask task = getTask("sourceroot", destDir);
    task.setSourceRootCopyFilter("doNotCopy,**/*.txt");
    File file = new File(destDir, "Default.java").getAbsoluteFile();
    assertFalse(file + ".canRead() prematurely", file.canRead());
    checkRun(task, null);

    // got expected resources
    assertTrue(file + ".canRead() failed", file.canRead());
    File pack = new File(destDir, "pack");
    file = new File(pack, "Pack.java").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());
    file = new File(pack, "includeme").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());

    // didn't get unexpected resources
    file = new File(pack, "something.txt");
    assertFalse(file + ".canRead() passed", file.canRead());
    file = new File(destDir, "doNotCopy");
    assertFalse(file + ".canRead() passed", file.canRead());
    file = new File(destDir, "skipTxtFiles.txt");
    assertFalse(file + ".canRead() passed", file.canRead());
  }

  public void testInpathDirCopyFilter() {
    // inpathDirCopyFilter works with output directory
    File destDir = getTempDir();
    assertTrue("unable to create " + destDir, destDir.canRead() || destDir.mkdirs());
    AjcTask task = getTask(NOFILE, destDir);
    Project p = task.getProject();
    Path indirs = new Path(p);
    File dir = new File(testdataDir, "inpathDirs").getAbsoluteFile();
    indirs.addExisting(new Path(p, new File(dir, "inpathDirOne").getAbsolutePath()));
    indirs.addExisting(new Path(p, new File(dir, "inpathDirTwo").getAbsolutePath()));
    task.setInpath(indirs);
    task.setInpathDirCopyFilter("doNotCopy,**/*.txt");

    File file = new File(destDir, "Default.java").getAbsoluteFile();
    assertFalse(file + ".canRead() prematurely", file.canRead());
    checkRun(task, null);

    // got expected resources
    File pack = new File(destDir, "pack");
    file = new File(pack, "includeme").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());
    file = new File(pack, "Pack.class").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());
    file = new File(destDir, "copyMe.htm").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());
    file = new File(destDir, "Default.class").getAbsoluteFile();
    assertTrue(file + ".canRead() failed", file.canRead());

    // didn't get unexpected resources
    file = new File(pack, "something.txt");
    assertFalse(file + ".canRead() passed", file.canRead());
    file = new File(destDir, "doNotCopy");
    assertFalse(file + ".canRead() passed", file.canRead());
    file = new File(destDir, "skipTxtFiles.txt");
    assertFalse(file + ".canRead() passed", file.canRead());
  }

  public void testInpathDirCopyFilterWithJar() throws IOException {
    checkInpathCopy("testInpathDirCopyFilterWithJar-out.jar");
  }

  // test resource copying for oddball jar files that don't end in .jar
  public void testInpathDirCopyFilterWithOddjar() throws IOException {
    checkInpathCopy("testInpathDirCopyFilterWithJar-outJarFile");
  }

  private void checkInpathCopy(String outjarFileStr) throws IOException {
    // inpathDirCopyFilter works with output jar
    File destDir = getTempDir();
    assertTrue("unable to create " + destDir, destDir.canRead() || destDir.mkdirs());
    AjcTask task = getTask(NOFILE, null);
    File destJar = new File(destDir, outjarFileStr);
    task.setOutjar(destJar);
    Project p = task.getProject();
    Path indirs = new Path(p);
    File dir = new File(testdataDir, "inpathDirs").getAbsoluteFile();
    indirs.addExisting(new Path(p, new File(dir, "inpathDirOne").getAbsolutePath()));
    indirs.addExisting(new Path(p, new File(dir, "inpathDirTwo").getAbsolutePath()));
    task.setInpath(indirs);
    task.setInpathDirCopyFilter("doNotCopy,**/*.txt,**/*.class");

    checkRun(task, null);

    try (JarFile jarFile = new JarFile(destJar)) {
      String[] expected = { "copyMe.htm", "pack/includeme", "pack/Pack.class", "Default.class" };
      String[] unexpected = { "doNotCopy", "skipTxtFiles.txt", "pack/something.txt" };
      for (String value : expected) {
        JarEntry entry = jarFile.getJarEntry(value);
        assertNotNull(value + " not found", entry);
      }
      for (String s : unexpected) {
        JarEntry entry = jarFile.getJarEntry(s);
        assertNull(s + " found", entry);
      }
    }
  }

  public void testInpathDirCopyFilterError() {
    // inpathDirCopyFilter fails with no output directory or jar iff specified
    AjcTask task = getTask(NOFILE, null);
    Project p = task.getProject();
    Path indirs = new Path(p);
    File dir = new File(testdataDir, "inpathDirs").getAbsoluteFile();
    indirs.addExisting(new Path(p, new File(dir, "inpathDirOne").getAbsolutePath()));
    indirs.addExisting(new Path(p, new File(dir, "inpathDirTwo").getAbsolutePath()));
    task.setInpath(indirs);
    task.setInpathDirCopyFilter("doNotCopy,**/*.txt,**/*.class");

    // expecting error
    checkRun(task, "inpathDirCopyFilter");
  }

  // this test method submitted by patch from Andrew Huff (IBM)
  // verifies that the log attribute of AjcTask writes output to the given log file
  public void testLoggingMode() {
    AjcTask task = getTask("default.lst");
    task.setFailonerror(false);
    File logFile = new File("testLogFile1.txt");
    String s = logFile.getAbsolutePath();
    logFile.delete();
    long initialLength = logFile.length();
    task.setLog(logFile);
    runTest(task, null, MessageHolderChecker.INFOS);
    long newLength = logFile.length();
    assertTrue(newLength > initialLength);
    logFile.delete();
  }


  public void testCommandEditor() {
    String className = VerboseCommandEditor.class.getName();
    System.setProperty(AjcTask.COMMAND_EDITOR_NAME, className);
    assertEquals(className, System.getProperty(AjcTask.COMMAND_EDITOR_NAME));
    AjcTask task = getTask(NOFILE);
    task.setCommandEditor(new VerboseCommandEditor());
    String[] cmd = task.makeCommand();
    assertEquals(VerboseCommandEditor.VERBOSE, cmd[0]);

    task = getTask(NOFILE);
    task.setCommandEditorClass(VerboseCommandEditor.class.getName());
    cmd = task.makeCommand();
    assertEquals(VerboseCommandEditor.VERBOSE, cmd[0]);
  }

  //    public void testStaticCommandEditor() {
  //        // XXX need to test COMMAND_EDITOR, but can't require property when run
  //    }

  public void testLimitTo() {
    int numArgs = 100;
    String arg = "123456789";
    String[] args = new String[numArgs];
    for (int i = 0; i < args.length; i++)
      args[i] = arg;
    // no limit
    int max = numArgs * (arg.length() + 1);
    Location location = new Location("AjcTaskTest.java");
    String[] newArgs = AjcTask.GuardedCommand.limitTo(args, max, location);
    assertSame("same", args, newArgs);

    // limited - read file and verify arguments
    max--;
    newArgs = AjcTask.GuardedCommand.limitTo(args, max, location);
    assertNotSame("not same", args, newArgs);
    assertNotNull("not null", newArgs);
    String label = "newArgs " + Arrays.asList(newArgs);
    assertEquals("size 2" + label, 2, newArgs.length);
    assertEquals("-argfile", newArgs[0]);
    File file = new File(newArgs[1]);
    assertTrue("readable newArgs[1]" + label, file.canRead());
    FileReader fin = null;
    try {
      fin = new FileReader(file);
      BufferedReader reader = new BufferedReader(fin);
      String line;
      int i = 0;
      while (null != (line = reader.readLine()))
        assertEquals(i + ": ", args[i++], line);
      assertEquals("num entries", i, args.length);
    }
    catch (IOException e) {
      fail("IOException " + e.getMessage());
    }
    finally {
      if (null != fin) {
        try {
          fin.close();
        }
        catch (IOException e) {
          // ignore
        }
      }
      file.delete();
    }
  }

  public void testFindAspectjtoolsJar() {
    File toolsJar = AjcTask.findAspectjtoolsJar();
    if (null != toolsJar)
      assertNull("tools jar found?: " + toolsJar, toolsJar);
    // not found when unit testing b/c not on system classpath
    // so just checking for exceptions.
    // XXX need aspect to stub out System.getProperty(..)
  }


  public void testMessageHolderClassName() {
    AjcTask task = getTask("compileError.lst");
    task.setFailonerror(false);
    MESSAGES.setLength(0);
    runTest(task, null, MessageHolderChecker.ONE_ERROR, Holder.class.getName());
    String result = MESSAGES.toString();
    MESSAGES.setLength(0);
    // The test program produces three errors with the current 1.8 compiler, this may change by 1.8 release and so
    // this will need reverting back to "e"
    assertEquals("messages", "eee", result);
  }

  // TODO skipped test - works locally but not on build machine?
  public void skip_testMessageHolderClassWithDoneSignal() {
    AjcTask task = getTask("default.lst");
    task.setFailonerror(false);
    String DONE = "This is a unique message, not confused with others.";
    task.setXDoneSignal(DONE);
    MESSAGES.setLength(0);
    runTest(task, null, MessageHolderChecker.INFOS, InfoHolder.class.getName());
    final String result = MESSAGES.toString();
    String temp = result;
    MESSAGES.setLength(0);
    if (!temp.endsWith(DONE)) {
      if (temp.length() > 20)
        temp = "..." + temp.substring(temp.length() - 20);
      fail(DONE + " is not suffix of \"" + temp + "\"");
    }
    // exactly one such message
    temp = result;
    temp = temp.substring(0, temp.length() - DONE.length());
    if (temp.endsWith(DONE)) {
      temp = result;
      if (temp.length() > 20)
        temp = "..." + temp.substring(temp.length() - 20);
      fail(DONE + " signalled twice: \"" + temp + "\"");
    }
  }

  public void testDefaultListForkedNoTools() {
    AjcTask task = getTask("default.lst");
    task.setFork(true);
    boolean passed = false;
    try {
      runTest(task, BuildException.class, MessageHolderChecker.NONE);
      passed = true;
    }
    finally {
      if (!passed) {
        String m = "AjcTaskTest.testDefaultListForkedNoTools()" + " fails if aspectjtools.jar is on the classpath";
        System.err.println(m);
      }
    }
  }

  public void testDefaultListForkedIncremental() {
    AjcTask task = getTask("default.lst");
    task.setFork(true);
    task.setIncremental(true);
    runTest(task, BuildException.class, MessageHolderChecker.NONE);
  }

  /**
   * failonerror should default to true, unlike other booleans
   */
  public void testCompileErrorFailOnErrorDefault() {
    AjcTask task = getTask("compileError.lst");
    final PrintStream serr = System.err;
    try {
      System.setErr(new PrintStream(new java.io.ByteArrayOutputStream()));
      // Current 1.8 compiler produces 3 errors for this test program, may need reverting to ONE_ERROR by release
      runTest(task, BuildException.class, MessageHolderChecker.THREE_ERRORS);
    }
    finally {
      System.setErr(serr);
    }
  }

  public void testCompileErrorListDefaultHolder() {
    AjcTask task = getTask("compileError.lst");
    final PrintStream serr = System.err;
    try {
      System.setErr(new PrintStream(new java.io.ByteArrayOutputStream()));
      task.execute();
      fail("expected BuildException from failed compile by default");
    }
    catch (BuildException t) {
      // ok
    }
    finally {
      System.setErr(serr);
      deleteTempDir();
    }
  }

  public void testDefaultList() {
    AjcTask task = getTask("default.lst");
    runTest(task, NO_EXCEPTION, MessageHolderChecker.INFOS);
  }

  public void testCompileErrorList() {
    AjcTask task = getTask("compileError.lst");
    task.setFailonerror(false);
    // Prior to the 1.8 compiler there is one error here, 'syntax error on here'
    // With 1.8 there are 3 errors about completing the method header, ending the class body, ending the method - this may
    // change by 1.8 final... this might need reverting back to ONE_ERROR
    runTest(task, NO_EXCEPTION, MessageHolderChecker.THREE_ERRORS);
  }

  public void testShowWeaveInfo() {
    AjcTask task = getTask("showweaveinfo.lst");
    task.setShowWeaveInfo(true);
    MessageHandler mh = new MessageHandler(false);
    mh.dontIgnore(IMessage.WEAVEINFO);
    MessageHolderChecker mhc = new MessageHolderChecker(0, 0, 0, 0, MessageHolderChecker.IGNORE);
    mhc.weaveinfos = 2; // Expect 2 weaving messages
    runTest(task, NO_EXCEPTION, mhc);
    mhc.weaveinfos = MessageHolderChecker.IGNORE;
  }

  public void testCompileWarningList() {
    AjcTask task = getTask("compileWarning.lst");
    runTest(task, NO_EXCEPTION, MessageHolderChecker.ONE_WARNING);
  }

  public void testNoSuchFileList() {
    AjcTask task = getTask("NoSuchFile.lst");
    task.setFailonerror(false);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.ONE_ERROR);
  }

  public void testVersions() {
    String[] inputs = AjcTask.TARGET_INPUTS;
    for (String value : inputs) {
    	System.out.println("Value is "+value);
      AjcTask task = getTask(NOFILE);
      task.setTarget(value);
      String[] cmd = task.makeCommand();
      checkContains(cmd, "-target", true);
      checkContains(cmd, value, true);
    }

    inputs = AjcTask.SOURCE_INPUTS;
    for (String s : inputs) {
      AjcTask task = getTask(NOFILE);
      task.setSource(s);
      String[] cmd = task.makeCommand();
      checkContains(cmd, "-source", true);
      checkContains(cmd, s, true);
    }

    inputs = AjcTask.COMPLIANCE_INPUTS;
    for (String input : inputs) {
      AjcTask task = getTask(NOFILE);
      task.setCompliance(input);
      String[] cmd = task.makeCommand();
      checkContains(cmd, input, true);
    }
  }

  public void testClasspath() {
    AjcTask task = getTask(NOFILE);
    String[] cmd = task.makeCommand();
    checkContains(cmd, "-bootclasspath", false);
    String classpath = null;
    for (int i = 0; i < cmd.length; i++) {
      if ("-classpath".equals(cmd[i])) {
        classpath = cmd[i + 1];
        break;
      }
    }
    assertTrue("expecting aspectj in classpath", (classpath.contains("aspectjrt.jar")));
  }

  CompilerArg createCompilerArg(String value) {
    CompilerArg c = new CompilerArg();
    c.setValue(value);
    return c;
  }

  public void testAddModulesJ9() {
    AjcTask task = getTask(NOFILE);
    task.createCompilerarg().setValue("--add-modules");
    task.createCompilerarg().setValue("java.xml.bind,java.io");
    String[] cmd = task.makeCommand();
    System.out.println(Arrays.toString(cmd));
    int addModulesPos = findOptionPosition(cmd, "--add-modules");
    assertNotSame(-1, addModulesPos);
    assertEquals("java.xml.bind,java.io", cmd[addModulesPos + 1]);
  }

  private int findOptionPosition(String[] cmd, String optionString) {
    for (int i = 0; i < cmd.length; i++) {
      if (cmd[i].equals(optionString))
        return i;
    }
    return -1;
  }

  // ---------------------------------------- sourcefile
  // XXX need to figure out how to specify files directly programmatically
  //    public void testDefaultFile() {
  //        AjcTask task = getTask("testdata/Default.java");
  //        runTest(task, NO_EXCEPTION, MessageHolderChecker.INFOS);
  //    }

  public void testNoFile() {
    AjcTask task = getTask(NOFILE);
    task.setFailonerror(false);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.ONE_ERROR);
  }

  public void testCompileErrorFile() {
    AjcTask task = getTask("compileError.lst");
    task.setFailonerror(false);
    // 1.8 compiler currently produces 3 errors for the test program, may need to revert to ONE_ERROR by 1.8 release
    runTest(task, NO_EXCEPTION, MessageHolderChecker.THREE_ERRORS);
  }

  public void testCompileWarningFile() {
    AjcTask task = getTask("compileWarning.lst");
    task.setFailonerror(false);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.ONE_WARNING);
  }

  public void testNoSuchFile() {
    AjcTask task = getTask("NoSuchFile.lst");
    task.setFailonerror(false);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.ONE_ERROR);
  }

  public void testDefaultFileComplete() {
    AjcTask task = getTask("default.lst");
    defaultSettings(task);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.INFOS);
  }

  private void defaultSettings(AjcTask task) {
    task.setDebugLevel("none");
    task.setDeprecation(true);
    task.setFailonerror(false);
    task.setNoExit(true); // ok to override Ant?
    task.setNoImportError(true);
    task.setNowarn(true);
    task.setXTerminateAfterCompilation(true);
    task.setPreserveAllLocals(true);
    task.setProceedOnError(true);
    task.setReferenceInfo(true);
    task.setSource("1.8");
    task.setTarget("1.1");
    task.setTime(true);
    task.setVerbose(true);
    task.setXlint("info");
  }

  public void testLogCommand() {
    final String DEFAULT = "default.lst";
    AjcTask task = getTask(DEFAULT);
    defaultSettings(task);
    task.setVerbose(false);
    task.setLogCommand(true);
    LogListener listener = new LogListener(Project.MSG_INFO);
    task.getProject().addBuildListener(listener);
    runTest(task, NO_EXCEPTION, MessageHolderChecker.INFOS);
    String[] results = listener.getResults();
    boolean matched = false;
    for (int i = 0; !matched && (i < results.length); i++) {
      String s = results[i];
      matched = (null != s) && (s.contains(DEFAULT));
    }
    if (!matched)
      fail(DEFAULT + " not found in " + Arrays.asList(results));
  }

  public void testXOptions() {
    String[] xopts = new String[] {
      "serializableAspects", "lazyTjp", "reweavable", "reweavable:compress", "noInline"
    };
    for (String xopt : xopts) {
      AjcTask task = getTask(NOFILE);
      task.setX(xopt);
      String[] cmd = task.makeCommand();
      checkContains(cmd, "-X" + xopt, true);
    }

  }

  public void testAptProc() {
    AjcTask task = getTask(NOFILE);
    task.setProc("none");
    checkContains(task.makeCommand(), "-proc:none", true);
    task.setProc("only");
    checkContains(task.makeCommand(), "-proc:only", true);
  }

  public void testAptProcessor() {
    AjcTask task = getTask(NOFILE);
    task.setProcessor("some.SomeClass");
    checkContains(task.makeCommand(), "-processor", true);
    checkContains(task.makeCommand(), "some.SomeClass", true);
  }

  public void testAptProcessorpath() {
    AjcTask task = getTask(NOFILE);
    task.setProcessorpath("some/path");
    checkContains(task.makeCommand(), "-processorpath", true);
    checkContains(task.makeCommand(), "some/path", true);
  }

  public void testAptGeneratedDirectory() {
    AjcTask task = getTask(NOFILE);
    task.setS("some/path");
    checkContains(task.makeCommand(), "-s", true);
    checkContains(task.makeCommand(), "some/path", true);
  }

  public void testOutxml() {
    File destDir = getTempDir();
    assertTrue("unable to create " + destDir, destDir.canRead() || destDir.mkdirs());
    AjcTask task = getTask("showweaveinfo.lst", destDir);
    task.setOutxml(true);
    checkRun(task, null);
    File outxmlFile = new File(destDir, "META-INF/aop-ajc.xml");
    assertTrue("META-INF/aop-ajc.xml missing", outxmlFile.exists());
  }

  public void testOutxmlFile() {
    String customName = "custom/aop.xml";
    File destDir = getTempDir();
    assertTrue("unable to create " + destDir, destDir.canRead() || destDir.mkdirs());
    AjcTask task = getTask("showweaveinfo.lst", destDir);
    task.setOutxmlfile(customName);
    checkRun(task, null);
    File outxmlFile = new File(destDir, customName);
    assertTrue(customName + " missing", outxmlFile.exists());
  }

  // ------------------------------------------------------
  // End of test cases
  // ------------------------------------------------------

  protected void runTest(
    AjcTask task, Class exceptionType, MessageHolderChecker checker, String messageHolderClass
  )
  {
    task.setMessageHolderClass(messageHolderClass);
    runTest(task, exceptionType, checker, (MessageHandler) null);
  }

  protected void runTest(
    AjcTask task, Class exceptionType, MessageHolderChecker checker
  )
  {
    MessageHandler holder = new MessageHandler();
    task.setMessageHolder(holder);
    runTest(task, exceptionType, checker, holder);
  }

  protected void runTest(
    AjcTask task, Class exceptionType, MessageHolderChecker checker, MessageHandler holder
  )
  {
    Throwable thrown = null;
    // re-run forked iff tools.jar and expect to pass
    boolean rerunForked =
      (null != aspectjtoolsJar)
        && (null == exceptionType)
        && ((null == checker) || !checker.expectFail());
    String label = "same-vm ";
    while (true) { // same vm, then perhaps forked
      try {
        task.execute();
      }
      catch (Throwable t) {
        thrown = t;
      }
      finally {
        deleteTempDir();
      }
      if (null == exceptionType) {
        if (null != thrown)
          fail(label + "thrown: " + render(thrown));
      }
      else if (null == thrown)
        fail(label + "expected " + exceptionType.getName());
      else if (!(exceptionType.isAssignableFrom(thrown.getClass())))
        fail(label + "expected " + exceptionType.getName() + " got " + render(thrown));
      if (null != holder) {
        if (null == checker)
          checker = MessageHolderChecker.NONE;
        checker.check(holder, label);
      }
      if (!rerunForked)
        break;
      else {
        label = "other-vm ";
        rerunForked = false;
        // can't reset without losing values...
        task.setFork(true);
        task.setFailonerror(true);
        task.setForkclasspath(new Path(task.getProject(), aspectjtoolsJar));
      }
    }
  }

  protected String render(Throwable thrown) {
    return LangUtil.renderException(thrown);
  }

  static class MessageHolderChecker { // XXX export to testing-utils
    /**
     * use as value to ignore results
     */
    static int IGNORE = Integer.MIN_VALUE;

    static MessageHolderChecker NONE = new MessageHolderChecker(0, 0, 0, 0, 0);
    /**
     * any number (0+) of info messages
     */
    static MessageHolderChecker INFOS = new MessageHolderChecker(0, 0, 0, 0, IGNORE);
    /**
     * one error, any number of info messages
     */
    static MessageHolderChecker ONE_ERROR = new MessageHolderChecker(0, 0, 1, 0, IGNORE);
    static MessageHolderChecker ONE_ERROR_ONE_ABORT = new MessageHolderChecker(1, 0, 1, 0, IGNORE);
    /**
     * one warning, any number of info messages
     */
    static MessageHolderChecker ONE_WARNING = new MessageHolderChecker(0, 0, 0, 1, IGNORE);
    static MessageHolderChecker THREE_ERRORS = new MessageHolderChecker(0, 0, 3, 0, IGNORE);


    int aborts, fails, errors, warnings, infos;
    int weaveinfos;

    public MessageHolderChecker(
      int aborts, int fails, int errors, int warnings, int infos
    )
    {
      this.aborts = aborts;
      this.fails = fails;
      this.errors = errors;
      this.warnings = warnings;
      this.infos = infos;
      this.weaveinfos = IGNORE;
    }

    public boolean expectFail() {
      return 0 < (aborts + fails + errors);
    }

    public void check(IMessageHolder holder, String label) {
      boolean failed = true;
      try {
        check(holder, aborts, IMessage.ABORT);
        check(holder, fails, IMessage.FAIL);
        check(holder, errors, IMessage.ERROR);
        check(holder, warnings, IMessage.WARNING);
        check(holder, infos, IMessage.INFO);
        check(holder, weaveinfos, IMessage.WEAVEINFO);
        failed = false;
      }
      finally {
        if (failed)
          MessageUtil.print(System.err, holder, label + "failed?");
      }
    }

    private void check(
      IMessageHolder holder, int num, IMessage.Kind kind
    )
    {
      if (num != IGNORE) {
        int actual = holder.numMessages(kind, false);

        // Filter out a warning which occurs, if the current release does not match the stored binary in lib/test.
        // If e.g. we run tests for a milestone release a.b.5.M1 and afterwards switch back to a.b.5-SNAPSHOT, we do not
        // want to update lib/test for a single commit, just to make this test pass. Hence, we ignore this warning here.
        if (kind.equals(IMessage.WARNING)) {
          for (IMessage message : holder.getMessages(kind, false)) {
            String warningMessage = message.getMessage();
            if (warningMessage.matches("bad version number found in .*aspectjrt.jar expected .* found .*"))
              actual--;
          }
        }

        if (num != actual) {
          if (actual > 0)
            MessageUtil.print(System.err, holder, kind + " expected " + num + " got " + actual);
          System.out.println("===\n" + Arrays.toString(holder.getMessages(kind, false)) + "\n===\n");
          assertEquals(kind.toString(), num, actual);
        }
      }
    }
  }

  private static class LogListener implements BuildListener {
    private final List<String> results = new ArrayList<>();
    private final int priority;

    private LogListener(int priority) {
      this.priority = priority;
    }

    @Override
    public void buildFinished(BuildEvent event) {}

    @Override
    public void buildStarted(BuildEvent event) {}

    @Override
    public void messageLogged(BuildEvent event) {
      if (priority == event.getPriority())
        results.add(event.getMessage());
    }

    @Override
    public void targetFinished(BuildEvent event) {}

    @Override
    public void targetStarted(BuildEvent event) {}

    @Override
    public void taskFinished(BuildEvent event) {}

    @Override
    public void taskStarted(BuildEvent event) {}

    String[] getResults() {
      return results.toArray(new String[0]);
    }
  }
}

class SnoopingCommandEditor implements ICommandEditor {
  private static final String[] NONE = new String[0];
  String[] lastCommand;

  @Override
  public String[] editCommand(String[] command) {
    lastCommand = (String[]) LangUtil.safeCopy(command, NONE);
    return command;
  }

  public String[] lastCommand() {
    return (String[]) LangUtil.safeCopy(lastCommand, NONE);
  }
}

class VerboseCommandEditor implements ICommandEditor {
  public static final String VERBOSE = "-verbose";

  @Override
  public String[] editCommand(String[] command) {
    for (String s : command) {
      if (VERBOSE.equals(s))
        return command;
    }

    String[] result = new String[1 + command.length];
    result[0] = VERBOSE;
    System.arraycopy(result, 1, command, 0, command.length);
    return result;
  }
}

class AppendingCommandEditor implements ICommandEditor {
  private static final String[] NONE = new String[0];
  public static ICommandEditor VERBOSE = new AppendingCommandEditor(new String[] { "-verbose" }, NONE);
  public static ICommandEditor INVALID = new AppendingCommandEditor(NONE, new String[] { "-invalidOption" });

  final String[] prefix;
  final String[] suffix;

  public AppendingCommandEditor(String[] prefix, String[] suffix) {
    this.prefix = prefix;
    this.suffix = suffix;
  }

  @Override
  public String[] editCommand(String[] command) {
    int len = command.length + prefix.length + suffix.length;
    String[] result = new String[len];
    System.arraycopy(result, 0, prefix, 0, prefix.length);
    System.arraycopy(result, prefix.length, command, 0, command.length);
    System.arraycopy(result, prefix.length + command.length, suffix, 0, suffix.length);
    return result;
  }
}