/* ******************************************************************* * 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: * * Usage: * * 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 }