IncCompilerRun.java
/* *******************************************************************
* Copyright (c) 1999-2001 Xerox Corporation,
* 2002 Palo Alto Research Center, Incorporated (PARC).
* 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
* ******************************************************************/
package org.aspectj.testing.harness.bridge;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.aspectj.bridge.ICommand;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.ajde.CompileCommand;
import org.aspectj.testing.run.IRunIterator;
import org.aspectj.testing.run.IRunStatus;
import org.aspectj.testing.run.WrappedRunIterator;
import org.aspectj.testing.util.StructureModelUtil;
import org.aspectj.testing.util.StructureModelUtil.ModelIncorrectException;
import org.aspectj.testing.xml.AjcSpecXmlReader;
import org.aspectj.testing.xml.IXmlWritable;
import org.aspectj.testing.xml.SoftMessage;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
/**
* An incremental compiler run takes an existing compiler commmand
* from the sandbox, updates the staging directory, and recompiles.
* The staging directory is updated by prefix/suffix rules applied
* to files found below Sandbox.testBaseSrcDir.
* Files with suffix .{tag}.java are owned by this run
* and are copied to the staging directory
* unless they are prefixed "delete.", in which case the
* corresponding file is deleted. Any "owned" file is passed to
* the compiler as the list of changed files.
* The files entry contains the expected files recompiled. XXX underinclusive
* XXX prefer messages for expected files?
* XXX later: also support specified paths, etc.
*/
public class IncCompilerRun implements IAjcRun {
final Spec spec; // nonfinal later to make re-runnable
Sandbox sandbox;
/**
* @param handler must not be null, but may be reused in the same thread
*/
public IncCompilerRun(Spec spec) {
LangUtil.throwIaxIfNull(spec, "spec");
this.spec = spec;
}
/**
* Initialize this from the sandbox, using compiler and changedFiles.
* @param sandbox the Sandbox setup for this test, including copying
* any changed files, etc.
* @see org.aspectj.testing.harness.bridge.AjcTest.IAjcRun#setup(File, File)
* @throws AbortException containing IOException or IllegalArgumentException
* if the staging operations fail
*/
public boolean setupAjcRun(Sandbox sandbox, Validator validator) {
LangUtil.throwIaxIfNull(validator, "validator");
if (!validator.nullcheck(sandbox, "sandbox")
|| !validator.nullcheck(spec, "spec")
|| !validator.nullcheck(spec.tag, "fileSuffix")) {
return false;
}
File srcDir = sandbox.getTestBaseSrcDir(this);
File destDir = sandbox.stagingDir;
if (!validator.canReadDir(srcDir, "testBaseSrcDir")
|| !validator.canReadDir(destDir, "stagingDir")) {
return false;
}
this.sandbox = sandbox;
return doStaging(validator);
}
/**
* Handle copying and deleting of files per tag.
* This returns false unless
* (1) tag is "same", or
* (2) some file was copied or deleted successfully
* and there were no failures copying or deleting files.
* @return true if staging completed successfully
*/
boolean doStaging(final Validator validator) {
if ("same".equals(spec.tag)) {
return true;
}
boolean result = false;
try {
final String toSuffix = ".java";
final String fromSuffix = "." + spec.tag + toSuffix;
// copy our tagged generation of files to the staging directory,
// deleting any with ChangedFilesCollector.DELETE_SUFFIX
// sigh - delay until after last last-mod-time
intHolder holder = new intHolder();
List<File> copied = new ArrayList<>();
doStaging(validator,".java",holder,copied);
doStaging(validator,".jar",holder,copied);
doStaging(validator,".class",holder,copied);
doStaging(validator,".properties",holder,copied); // arbitrary resource extension
doStaging(validator,".xml",holder,copied); // arbitrary resource extension
if ((0 == holder.numCopies) && (0 == holder.numDeletes)) {
validator.fail("no files changed??");
} else {
result = (0 == holder.numFails);
}
if (0 < copied.size()) {
File[] files = copied.toArray(new File[0]);
FileUtil.sleepPastFinalModifiedTime(files);
}
} catch (NullPointerException npe) {
validator.fail("staging - input", npe);
} catch (IOException e) {
validator.fail("staging - operations", e);
}
return result;
}
private void doStaging(final Validator validator, final String toSuffix,
final intHolder holder,final List copied)
throws IOException
{
final String fromSuffix = "." + spec.tag + toSuffix;
final String clip = ".delete" + toSuffix;
FileFilter deleteOrCount = new FileFilter() {
/** do copy unless file should be deleted */
public boolean accept(File file) {
boolean doCopy = true;
String path = file.getAbsolutePath();
if (!path.endsWith(clip)) {
holder.numCopies++;
validator.info("copying file: " + path);
copied.add(file);
} else {
doCopy = false;
path = path.substring(0, path.length()-clip.length()) + toSuffix;
File toDelete = new File(path);
if (toDelete.delete()) {
validator.info("deleted file: " + path);
holder.numDeletes++;
} else {
validator.fail("unable to delete file: " + path);
holder.numFails++;
}
}
return doCopy;
}
};
File srcDir = sandbox.getTestBaseSrcDir(this);
File destDir = sandbox.stagingDir;
FileUtil.copyDir(srcDir, destDir, fromSuffix, toSuffix, deleteOrCount);
}
private static class intHolder {
int numCopies;
int numDeletes;
int numFails;
}
/**
* @see org.aspectj.testing.run.IRun#run(IRunStatus)
*/
public boolean run(IRunStatus status) {
ICommand compiler = sandbox.getCommand(this);
if (null == compiler) {
MessageUtil.abort(status, "null compiler");
}
// // This is a list of expected classes (in File-normal form
// // relative to base class/src dir, without .class suffix
// // -- like "org/aspectj/tools/ajc/Main")
// // A preliminary list is generated in doStaging.
// ArrayList expectedClasses = doStaging(status);
// if (null == expectedClasses) {
// return false;
// }
//
// // now add any (additional) expected-class entries listed in the spec
// // normalize to a similar file path (and do info messages for redundancies).
//
// List alsoChanged = spec.getPathsAsFile(sandbox.stagingDir);
// for (Iterator iter = alsoChanged.iterator(); iter.hasNext();) {
// File f = (File) iter.next();
//
// if (expectedClasses.contains(f)) {
// // XXX remove old comment changed.contains() works b/c getPathsAsFile producing both File
// // normalizes the paths, and File.equals(..) compares these lexically
// String s = "specification of changed file redundant with tagged file: ";
// MessageUtil.info(status, s + f);
// } else {
// expectedClasses.add(f);
// }
// }
//
// // now can create handler, use it for reporting
// List errors = spec.getMessages(IMessage.ERROR);
// List warnings = spec.getMessages(IMessage.WARNING);
// AjcMessageHandler handler = new AjcMessageHandler(errors, warnings, expectedClasses);
// same DirChanges handling for JavaRun, CompilerRun, IncCompilerRun
// XXX around advice or template method/class
DirChanges dirChanges = null;
if (!LangUtil.isEmpty(spec.dirChanges)) {
LangUtil.throwIaxIfFalse(1 == spec.dirChanges.size(), "expecting only 1 dirChanges");
dirChanges = new DirChanges(spec.dirChanges.get(0));
if (!dirChanges.start(status, sandbox.classesDir)) {
return false; // setup failed
}
}
AjcMessageHandler handler = new AjcMessageHandler(spec.getMessages());
boolean handlerResult = false;
boolean commandResult = false;
boolean result = false;
boolean report = false;
try {
handler.init();
if (spec.fresh) {
if (compiler instanceof CompileCommand) { // urk
((CompileCommand) compiler).buildNextFresh();
} else {
MessageUtil.info(handler, "fresh not supported by compiler: " + compiler);
}
}
// final long startTime = System.currentTimeMillis();
commandResult = compiler.repeatCommand(handler);
if (!spec.checkModel.equals("")) {
StructureModelUtil.checkModel(spec.checkModel);
}
// XXX disabled LangUtil.throwIaxIfNotAllAssignable(actualRecompiled, File.class, "recompiled");
report = true;
// handler does not verify sandbox...
handlerResult = handler.passed();
if (!handlerResult) {
result = false;
} else {
result = (commandResult == handler.expectingCommandTrue());
if (! result) {
String m = commandResult
? "incremental compile command did not return false as expected"
: "incremental compile command returned false unexpectedly";
MessageUtil.fail(status, m);
} else if (null != dirChanges) {
result = dirChanges.end(status, sandbox.testBaseDir);
}
}
} catch (ModelIncorrectException e) {
MessageUtil.fail(status,e.getMessage());
} finally {
if (!result || spec.runtime.isVerbose()) { // more debugging context in case of failure
MessageUtil.info(handler, "spec: " + spec.toLongString());
MessageUtil.info(handler, "sandbox: " + sandbox);
String[] classes = FileUtil.listFiles(sandbox.classesDir);
MessageUtil.info(handler, "sandbox.classes: " + Arrays.asList(classes));
}
// XXX weak - actual messages not reported in real-time, no fast-fail
if (report) {
handler.report(status);
}
}
return result;
}
// private boolean hasFile(ArrayList changed, File f) {
// return changed.contains(f); // d
// }
public String toString() {
return "" + spec;
// return "IncCompilerRun(" + spec + ")"; // XXX
}
/**
* initializer/factory for IncCompilerRun.
*/
public static class Spec extends AbstractRunSpec {
public static final String XMLNAME = "inc-compile";
protected boolean fresh;
protected ArrayList<String> classesAdded;
protected ArrayList<String> classesRemoved;
protected ArrayList<String> classesUpdated;
protected String checkModel;
/**
* skip description, skip sourceLocation,
* do keywords, skip options, do paths as classes, do comment,
* skip staging (always true), skip badInput (irrelevant)
* do dirChanges, do messages but skip children.
*/
// private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
// "", "", null, "", "classes", null, "", "", false, false, true);
//
/** identifies files this run owns, so {name}.{tag}.java maps to {name}.java */
String tag;
public Spec() {
super(XMLNAME);
setStaging(true);
classesAdded = new ArrayList<>();
classesRemoved = new ArrayList<>();
classesUpdated = new ArrayList<>();
checkModel="";
}
protected void initClone(Spec spec)
throws CloneNotSupportedException {
super.initClone(spec);
spec.fresh = fresh;
spec.tag = tag;
spec.classesAdded.clear();
spec.classesAdded.addAll(classesAdded);
spec.classesRemoved.clear();
spec.classesRemoved.addAll(classesRemoved);
spec.classesUpdated.clear();
spec.classesUpdated.addAll(classesUpdated);
}
public Object clone() throws CloneNotSupportedException {
Spec result = new Spec();
initClone(result);
return result;
}
public void setFresh(boolean fresh) {
this.fresh = fresh;
}
public void setTag(String input) {
tag = input;
}
public void setCheckModel(String thingsToCheck) {
this.checkModel=thingsToCheck;
}
public String toString() {
return "IncCompile.Spec(" + tag + ", " + super.toString() + ",["+checkModel+"])";
}
/** override to set dirToken to Sandbox.CLASSES and default suffix to ".class" */
public void addDirChanges(DirChanges.Spec spec) { // XXX copy/paste of CompilerRun.Spec...
if (null == spec) {
return;
}
spec.setDirToken(Sandbox.CLASSES_DIR);
spec.setDefaultSuffix(".class");
super.addDirChanges(spec);
}
/** @return a IncCompilerRun with this as spec if setup completes successfully. */
public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
IncCompilerRun run = new IncCompilerRun(this);
if (run.setupAjcRun(sandbox, validator)) {
// XXX need name
return new WrappedRunIterator(this, run);
}
return null;
}
/**
* Write this out as a compile element as defined in
* AjcSpecXmlReader.DOCTYPE.
* @see AjcSpecXmlReader#DOCTYPE
* @see IXmlWritable#writeXml(XMLWriter)
*/
public void writeXml(XMLWriter out) {
String attr = XMLWriter.makeAttribute("tag", tag);
out.startElement(xmlElementName, attr, false);
if (fresh) {
out.printAttribute("fresh", "true");
}
super.writeAttributes(out);
out.endAttributes();
if (!LangUtil.isEmpty(dirChanges)) {
DirChanges.Spec.writeXml(out, dirChanges);
}
SoftMessage.writeXml(out, getMessages());
out.endElement(xmlElementName);
}
public void setClassesAdded(String items) {
addItems(classesAdded, items);
}
public void setClassesUpdated(String items) {
addItems(classesUpdated, items);
}
public void setClassesRemoved(String items) {
addItems(classesRemoved, items);
}
private void addItems(List<String> list, String items) {
if (null != items) {
String[] classes = XMLWriter.unflattenList(items);
if (!LangUtil.isEmpty(classes)) {
for (String aClass : classes) {
if (!LangUtil.isEmpty(aClass)) {
list.add(aClass);
}
}
}
}
}
} // class IncCompilerRun.Spec
}
// // XXX replaced with method-local class - revisit if useful
//
// /**
// * This class collects the list of all changed files and
// * deletes the corresponding file for those prefixed "delete."
// */
// static class ChangedFilesCollector implements FileFilter {
// static final String DELETE_SUFFIX = ".delete.java";
// static final String REPLACE_SUFFIX = ".java";
// final ArrayList changed;
// final Validator validator;
// /** need this to generate paths by clipping */
// final File destDir;
//
// /** @param changed the sink for all files changed (full paths) */
// public ChangedFilesCollector(ArrayList changed, File destDir, Validator validator) {
// LangUtil.throwIaxIfNull(validator, "ChangedFilesCollector - handler");
// this.changed = changed;
// this.validator = validator;
// this.destDir = destDir;
// }
//
// /**
// * This converts the input File to normal String path form
// * (without any source suffix) and adds it to the list changed.
// * If the name of the file is suffixed ".delete..", then
// * delete the corresponding file, and return false (no copy).
// * Return true otherwise (copy file).
// * @see java.io.FileFilter#accept(File)
// */
// public boolean accept(File file) {
// final String aname = file.getAbsolutePath();
// String name = file.getName();
// boolean doCopy = true;
// boolean failed = false;
// if (name.endsWith(DELETE_SUFFIX)) {
// name = name.substring(0,name.length()-DELETE_SUFFIX.length());
// file = file.getParentFile();
// file = new File(file, name + REPLACE_SUFFIX);
// if (!file.canWrite()) {
// validator.fail("file to delete is not writable: " + file);
// failed = true;
// } else if (!file.delete()) {
// validator.fail("unable to delete file: " + file);
// failed = true;
// }
// doCopy = false;
// }
// if (!failed && doCopy) {
// int clip = FileUtil.sourceSuffixLength(file);
// if (-1 != clip) {
// name.substring(0, name.length()-clip);
// }
// if (null != destDir) {
// String path = destDir.getPath();
// if (!LangUtil.isEmpty(path)) {
// // XXX incomplete
// if (name.startsWith(path)) {
// } else {
// int loc = name.lastIndexOf(path);
// if (-1 == loc) { // sigh
//
// } else {
//
// }
// }
// }
// }
// name = FileUtil.weakNormalize(name);
// changed.add(file);
// }
// return doCopy;
// }
// };