/* *******************************************************************
* 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 v1.0
* which accompanies this distribution and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Xerox/PARC initial implementation
* ******************************************************************/
package org.aspectj.testing.harness.bridge;
import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.testing.util.TestUtil;
import org.aspectj.testing.xml.IXmlWritable;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;
/**
* Calculate changes in a directory tree.
* This implements two different specification styles:
*
* - Specify files added, removed, updated, and/or a component
* to check any existing files
* - Specify expected directory. When complete this checks that
* any files in expected directory are matched in the actual.
* (.class files are dissassembled before comparison.)
*
*
* Usage:
*
* - Set up with any expected changes and/or an expected directory
* - Set up with any file checker
* - start(..) before changes.
* This issues messages for any removed files not found,
* which represent an error in the expected changes.
* - Do whatever operations will change the directory
* - end(..).
* This issues messages for any files not removed, added, or updated,
* and, if any checker was set, any checker messages for matching
* added or updated files
*
* When comparing directories, this ignores any paths containing "CVS".
*/
public class DirChanges {
public static final String DELAY_NAME = "dir-changes.delay";
private static final long DELAY;
static {
long delay = 10l;
try {
delay = Long.getLong(DELAY_NAME).longValue();
if ((delay > 40000) || (delay < 0)) {
delay = 10l;
}
} catch (Throwable t) {
// ignore
}
DELAY = delay;
}
private static final boolean EXISTS = true;
final Spec spec;
/** start time, in milliseconds - valid only from start(..)..end(..) */
long startTime;
/** base directory of actual files - valid only from start(..)..end(..) */
File baseDir;
/** if set, this is run against any resulting existing files
* specified in added/updated lists.
* This does not affect expected-directory comparison.
*/
IFileChecker fileChecker;
/** handler valid from start..end of start(..) and end(..) methods */
IMessageHandler handler;
/**
* Constructor for DirChanges.
*/
public DirChanges(Spec spec) {
LangUtil.throwIaxIfNull(spec, "spec");
this.spec = spec;
}
/**
* Inspect the base dir, and issue any messages for
* removed files not present.
* @param baseDir File for a readable directory
* @return true if this started without sending messages
* for removed files not present.
*/
public boolean start(IMessageHandler handler, File baseDir) {
FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
final IMessageHandler oldHandler = this.handler;
this.handler = handler;
this.baseDir = baseDir;
startTime = 0l;
final boolean doCompare = false;
boolean result
= exists("at start, did not expect added file to exist", !EXISTS, spec.added, doCompare);
result &= exists("at start, expected unchanged file to exist", EXISTS, spec.unchanged, doCompare);
result &= exists("at start, expected updated file to exist", EXISTS, spec.updated, doCompare);
result &= exists("at start, expected removed file to exist", EXISTS, spec.removed, doCompare);
startTime = System.currentTimeMillis();
// ensure tests don't complete in < 1 second, otherwise can confuse fast machines.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.handler = oldHandler;
return result;
}
/**
* Inspect the base dir, issue any messages for
* files not added, files not updated, and files not removed,
* and compare expected/actual files added or updated.
* This sleeps before checking until at least DELAY milliseconds after start.
* @throws IllegalStateException if called before start(..)
*/
public boolean end(IMessageHandler handler, File srcBaseDir) {
FileUtil.throwIaxUnlessCanReadDir(baseDir, "baseDir");
if (0l == startTime) {
throw new IllegalStateException("called before start");
}
final long targetTime = startTime + spec.delayInMilliseconds;
do {
long curTime = System.currentTimeMillis();
if (curTime >= targetTime) {
break;
}
try {
Thread.sleep(targetTime-curTime);
} catch (InterruptedException e) {
break;
}
} while (true);
final IMessageHandler oldHandler = this.handler;
this.handler = handler;
try {
// variant 1: check specified files
// deferring comparison to end...
final boolean doCompare = (null != fileChecker);
final boolean fastFail = spec.fastFail;
boolean result
= exists("at end, expected file was not added", EXISTS, spec.added, doCompare);
if (result || !fastFail) {
result &= exists("at end, expected file was not unchanged", EXISTS, spec.unchanged, doCompare, false);
}
if (result || !fastFail) {
result &= exists("at end, expected file was not updated", EXISTS, spec.updated, doCompare);
}
if (result || !fastFail) {
result &= exists("at end, file exists, was not removed", !EXISTS, spec.removed, doCompare);
}
// if (result || !fastFail) {
// // XXX validate that unchanged mod-time did not change
// }
// variant 1: compare expected directory
if (result || !fastFail) {
result &= compareDir(srcBaseDir);
}
return result;
} finally {
this.handler = oldHandler;
baseDir = null;
startTime = 0l;
}
}
/**
* Verify that all files in any specified expected directory
* have matching files in the base directory, putting any messages
* in the handler (only one if the spec indicates fast-fail).
* @param srcBaseDir the File for the base directory of the test sources
* (any expected dir is specified relative to this directory)
* @return true if the same, false otherwise
*/
private boolean compareDir(File srcBaseDir) {
if (null == spec.expDir) {
return true;
}
File expDir = new File(srcBaseDir, spec.expDir);
File actDir = baseDir;
//System.err.println("XXX comparing actDir=" + actDir + " expDir=" + expDir);
return TestUtil.sameDirectoryContents(handler, expDir, actDir, spec.fastFail);
}
/** @param comp FileMessageComparer (if any) given matching messages to compare */
protected void setFileComparer(IFileChecker comp) {
this.fileChecker = comp;
}
/**
* Signal fail if any files do {not} exist or do {not} have last-mod-time after startTime
* @param handler the IMessageHandler sink for messages
* @param label the String infix for the fail message
* @param exists if true, then file must exist and be modified after start time;
* if false, then the file must not exist or must be modified before start time.
* @param pathList the List of path (without any Spec.defaultSuffix) of File
* in Spec.baseDir to find (or not, if !exists)
*/
protected boolean exists(
String label,
boolean exists,
List pathList,
boolean doCompare) {
// boolean expectStartEarlier = true;
return exists(label, exists, pathList,doCompare, true);
}
protected boolean exists(
String label,
boolean exists,
List pathList,
boolean doCompare,
boolean expectStartEarlier) {
boolean result = true;
if (!LangUtil.isEmpty(pathList)) {
// final File expDir = ((!doCompare || (null == spec.expDir))
// ? null
// : new File(baseDir, spec.expDir));
for (Iterator iter = pathList.iterator(); iter.hasNext();) {
final String entry = (String) iter.next() ;
String path = entry ;
if (null != spec.defaultSuffix) {
if (".class".equals(spec.defaultSuffix)) {
path = path.replace('.', '/');
}
path = path + spec.defaultSuffix;
}
File actualFile = new File(baseDir, path);
if (exists != (actualFile.canRead() && actualFile.isFile()
&& (expectStartEarlier
? startTime <= actualFile.lastModified()
: startTime > actualFile.lastModified()
))) {
failMessage(handler, exists, label, path, actualFile);
if (result) {
result = false;
}
} else if (exists && doCompare && (null != fileChecker)) {
if (!fileChecker.checkFile(handler, path, actualFile) && result) {
result = false;
}
}
}
}
return result;
}
/**
* Generate fail message "{un}expected {label} file {path} in {baseDir}".
* @param handler the IMessageHandler sink
* @param label String message infix
* @param path the path to the file
*/
protected void failMessage(
IMessageHandler handler,
boolean exists,
String label,
String path,
File file) {
MessageUtil.fail(handler, label + " \"" + path + "\" in " + baseDir);
}
/** Check actual File found at a path, usually to diff expected/actual contents */
public static interface IFileChecker {
/**
* Check file found at path.
* Implementations should return false when adding fail (or worse)
* message to the handler, and true otherwise.
* @param handler IMessageHandler sink for messages, esp. fail.
* @param path String for normalized path portion of actualFile.getPath()
* @param actualFile File to file found
* @return false if check failed and messages added to handler
*/
boolean checkFile(IMessageHandler handler, String path, File actualFile);
}
// File-comparison code with a bit more generality -- too unweildy
// /**
// * Default FileChecker compares files literally, transforming any
// * with registered normalizers.
// */
// public static class FileChecker implements IFileChecker {
// final File baseExpectedDir;
// NormalizedCompareFiles fileComparer;
//
// public FileChecker(File baseExpectedDir) {
// this.baseExpectedDir = baseExpectedDir;
// fileComparer = new NormalizedCompareFiles();
// }
// public boolean checkFile(IMessageHandler handler, String path, File actualFile) {
// if (null == baseExpectedDir) {
// MessageUtil.error(handler, "null baseExpectedDir set on construction");
// } else if (!baseExpectedDir.canRead() || !baseExpectedDir.isDirectory()) {
// MessageUtil.error(handler, "bad baseExpectedDir: " + baseExpectedDir);
// } else {
// File expectedFile = new File(baseExpectedDir, path);
// if (!expectedFile.canRead()) {
// MessageUtil.fail(handler, "cannot read expected file: " + expectedFile);
// } else {
// return doCheckFile(handler, expectedFile, actualFile, path);
// }
// }
// return false;
// }
//
// protected boolean doCheckFile(
// IMessageHandler handler,
// File expectedFile,
// File actualFile,
// String path) {
// fileComparer.setHandler(handler);
// FileLine[] expected = fileComparer.diff();
// return false;
// }
// }
// /**
// * CompareFiles implementation that pre-processes input
// * to normalize it. Currently it reads all files except
// * .class files, which it disassembles first.
// */
// public static class NormalizedCompareFiles extends CompareFiles {
// private final static String[] NO_PATHS = new String[0];
// private static String normalPath(File file) { // XXX util
// if (null == file) {
// return "";
// }
// return file.getAbsolutePath().replace('\\', '/');
// }
//
// private String[] baseDirs;
// private IMessageHandler handler;
//
// public NormalizedCompareFiles() {
// }
//
// void init(IMessageHandler handler, File[] baseDirs) {
// this.handler = handler;
// if (null == baseDirs) {
// this.baseDirs = NO_PATHS;
// } else {
// this.baseDirs = new String[baseDirs.length];
// for (int i = 0; i < baseDirs.length; i++) {
// this.baseDirs[i] = normalPath(baseDirs[i]) + "/";
// }
// }
// }
//
// private String getClassName(File file) {
// String result = null;
// String path = normalPath(file);
// if (!path.endsWith(".class")) {
// MessageUtil.error(handler,
// "NormalizedCompareFiles expected "
// + file
// + " to end with .class");
// } else {
// path = path.substring(0, path.length()-6);
// for (int i = 0; i < baseDirs.length; i++) {
// if (path.startsWith(baseDirs[i])) {
// return path.substring(baseDirs[i].length()).replace('/', '.');
// }
// }
// MessageUtil.error(handler,
// "NormalizedCompareFiles expected "
// + file
// + " to start with one of "
// + LangUtil.arrayAsList(baseDirs));
// }
//
// return result;
// }
//
// /**
// * Read file as normalized lines, sending handler any messages
// * ERROR for input failures and FAIL for processing failures.
// * @return NOLINES on error or normalized lines from file otherwise
// */
// public FileLine[] getFileLines(File file) {
// FileLineator capture = new FileLineator();
// InputStream in = null;
// try {
// if (!file.getPath().endsWith(".class")) {
// in = new FileInputStream(file);
// FileUtil.copyStream(
// new BufferedReader(new InputStreamReader(in)),
// new PrintWriter(capture));
// } else {
// String name = getClassName(file);
// if (null == name) {
// return new FileLine[0];
// }
// String path = normalPath(file);
// path = path.substring(0, path.length()-name.length());
// // XXX sole dependency on bcweaver/bcel
// LazyClassGen.disassemble(path, name, capture);
// }
// } catch (IOException e) {
// MessageUtil.fail(handler,
// "NormalizedCompareFiles IOException reading " + file, e);
// return null;
// } finally {
// if (null != in) {
// try { in.close(); }
// catch (IOException e) {} // ignore
// }
// capture.flush();
// capture.close();
// }
// String missed = capture.getMissed();
// if (!LangUtil.isEmpty(missed)) {
// MessageUtil.fail(handler,
// "NormalizedCompareFiles missed input: "
// + missed);
// return null;
// } else {
// return capture.getFileLines();
// }
// }
//
//
// }
/**
* Specification for a set of File added, removed, or updated
* in a given directory, or for a directory base for a tree of expected files.
* If defaultSuffix is specified, entries may be added without it.
* Currently the directory tree
* only is used to verify files that are expected
* and found after the process completes.
*/
public static class Spec implements IXmlWritable {
/** XML element name */
public static final String XMLNAME = "dir-changes";
/** a symbolic name for the base directory */
String dirToken; // XXX default to class?
/** if set, then append to specified paths when seeking files */
String defaultSuffix;
/** relative path of dir with expected files for comparison */
String expDir;
long delayInMilliseconds = DELAY;
/** if true, fail on first mis-match */
boolean fastFail;
/** relative paths (String) of expected files added */
final ArrayList added;
/** relative paths (String) of expected files removed/deleted */
final ArrayList removed;
/** relative paths (String) of expected files updated/changed */
final ArrayList updated;
/** relative paths (String) of expected files NOT
* added, removed, or changed
* XXX unchanged unimplemented
*/
final ArrayList unchanged;
public Spec() {
added = new ArrayList<>();
removed = new ArrayList<>();
updated = new ArrayList<>();
unchanged = new ArrayList<>();
}
/**
* @param dirToken the symbol name of the base directory (classes, run)
*/
public void setDirToken(String dirToken) {
this.dirToken = dirToken;
}
/**
* Set the directory containing the expected files.
* @param expectedDirRelativePath path relative to the test base
* of the directory containing expected results for the output dir.
*/
public void setExpDir(String expectedDirRelativePath) {
expDir = expectedDirRelativePath;
}
public void setDelay(String delay) {
if (null != delay) {
// let NumberFormatException propogate up
delayInMilliseconds = Long.parseLong(delay);
if (delayInMilliseconds < 0l) {
delayInMilliseconds = 0l;
}
}
}
/**
* @param clipSuffix the String suffix, if any, to clip automatically
*/
public void setDefaultSuffix(String defaultSuffix) {
this.defaultSuffix = defaultSuffix;
}
public void setAdded(String items) {
XMLWriter.addFlattenedItems(added, items);
}
public void setRemoved(String items) {
XMLWriter.addFlattenedItems(removed, items);
}
public void setUpdated(String items) {
XMLWriter.addFlattenedItems(updated, items);
}
public void setUnchanged(String items) {
XMLWriter.addFlattenedItems(unchanged, items);
}
public void setFastfail(boolean fastFail) {
this.fastFail = fastFail;
}
/** @return true if some list was specified */
private boolean hasFileList() {
return (!LangUtil.isEmpty(added)
|| !LangUtil.isEmpty(removed)
|| !LangUtil.isEmpty(updated)
|| !LangUtil.isEmpty(unchanged)
);
}
/**
* Emit specification in XML form if not empty.
* This writes nothing if there is no expected dir
* and there are no added, removed, or changed.
* fastFail is written only if true, since the default is false.
*/
public void writeXml(XMLWriter out) {
if (!hasFileList() && LangUtil.isEmpty(expDir)) {
return;
}
// XXX need to permit defaults here...
out.startElement(XMLNAME, false);
if (!LangUtil.isEmpty(dirToken)) {
out.printAttribute("dirToken", dirToken.trim());
}
if (!LangUtil.isEmpty(defaultSuffix)) {
out.printAttribute("defaultSuffix", defaultSuffix.trim());
}
if (!LangUtil.isEmpty(expDir)) {
out.printAttribute("expDir", expDir.trim());
}
if (!LangUtil.isEmpty(added)) {
out.printAttribute("added", XMLWriter.flattenList(added));
}
if (!LangUtil.isEmpty(removed)) {
out.printAttribute("removed", XMLWriter.flattenList(removed));
}
if (!LangUtil.isEmpty(updated)) {
out.printAttribute("updated", XMLWriter.flattenList(updated));
}
if (!LangUtil.isEmpty(unchanged)) {
out.printAttribute("unchanged", XMLWriter.flattenList(unchanged));
}
if (fastFail) {
out.printAttribute("fastFail", "true");
}
out.endElement(XMLNAME);
}
/**
* Write list as elements to XMLWriter.
* @param out XMLWriter output sink
* @param dirChanges List of DirChanges.Spec to write
*/
public static void writeXml(XMLWriter out, List dirChanges) {
if (LangUtil.isEmpty(dirChanges)) {
return;
}
LangUtil.throwIaxIfNull(out, "out");
for (Iterator iter = dirChanges.iterator(); iter.hasNext();) {
DirChanges.Spec spec = iter.next();
if (null == spec) {
continue;
}
spec.writeXml(out);
}
}
} // class Spec
}