IncrementalCase.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:
 *     PARC     initial implementation
 * ******************************************************************/

package org.aspectj.ajdt.internal.compiler.batch;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.Properties;

import org.aspectj.bridge.ICommand;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.Message;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.bridge.ReflectionFactory;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

/**
 * Mostly stateless incremental test case.
 * Subclass to use from junit.
 */
public class IncrementalCase { // XXX NOT bound to junit - bridge tests?
    public static final String[] RA_String = new String[0]; // XXX
	boolean verbose = true;
	boolean ignoreWarnings = false;

	public static void main(String[] args) throws IOException {
    	IncrementalCase me = new IncrementalCase();
		MessageHandler h = new MessageHandler();
//		boolean result;
		StringBuilder sb = new StringBuilder();
		for (String arg : args) {
			sb.append("\n###### results for " + arg);
			sb.append("\n" + me.run(new File(arg), h) + ": " + h);
		}
		System.err.flush();
		System.out.flush();
		System.err.println(sb.toString());
	}

	/**
	 * Run an incremental compile case.
	 * For each i=1..9, copy files srcDir/*{i=1..9}0.java
	 * to the sandbox and compile.
	 * This only expects the changed files to be recompiled, but
	 * it also calls verifyCompile(..);
	 * @param handler all non-functional feedback here.
	 * Exceptions are logged as ABORT messages
	 */
	public boolean run(File srcBase, IMessageHandler handler)
		throws IOException {
		final String cname = ReflectionFactory.ECLIPSE;

		File targetBase =
			makeDir(getSandboxDir(), "IncrementalCaseSandbox", handler);
		if (null == targetBase) {
			return false;
		}
		File targetSrc = makeDir(targetBase, "src", handler);
		if (null == targetSrc) {
			return false;
		}
		File targetClasses = makeDir(targetBase, "classes", handler);
		if (null == targetClasses) {
			return false;
		}
		final List<File> files = new ArrayList<>();
		final FileFilter collector = new FileFilter() {
			@Override
			public boolean accept(File file) {
				return files.add(file);
			}
		};
		final ICommand compiler =
			ReflectionFactory.makeCommand(cname, handler);

		List recompiled = null;
		boolean result = true;

		final String toSuffix = ".java";
//		final String canonicalFrom = srcBase.getCanonicalPath();
		final Definition[] defs = getDefinitions(srcBase);
		if ((null == defs) || (defs.length < 9)) {
			throw new Error("did not get definitions");
		}
		MessageHandler compilerMessages = new MessageHandler();
        StringBuilder commandLine = new StringBuilder();
		for (int i = 1; result && (i < 10); i++) {
			String fromSuffix = "." + i + "0.java";
			// copy files, collecting as we go...
			files.clear();
			FileUtil.copyDir(
				srcBase,
				targetSrc,
				fromSuffix,
				toSuffix,
				collector);
			if (0 == files.size()) { // XXX detect incomplete?
				break;
			}
			List safeFiles = Collections.unmodifiableList(files);
			log("Compiling ", safeFiles, handler);
			if (1 == i) {
				ArrayList<String> argList = new ArrayList<>(getBaseArgs(targetSrc, targetClasses));
				File[] fra = (File[]) safeFiles.toArray(new File[0]);
				// sigh
				argList.addAll(
					Arrays.asList(FileUtil.getAbsolutePaths(fra)));
				String[] args = argList.toArray(new String[0]);
    		    commandLine.append(""+argList);
            	result = compiler.runCommand(args, compilerMessages);
			} else {
				if (null == recompiled) {
					recompiled = new ArrayList();
				} else {
					recompiled.clear();
				}
				compilerMessages.init();
	            commandLine.append("["+i+": " + recompiled + "] ");
    			result =
					compiler.repeatCommand(compilerMessages);
			}
			result =
				verifyCompile(
					i,
					result,
					srcBase,
					targetSrc,
					targetClasses,
					defs[i - 1],
					compilerMessages,
                    commandLine,
					handler);
		}
		return result;
	}

	// -------------------------------------- test case verification
	/**
	 * Verify that this incremental compile step worked.
	 * @param recompiled the List of Files the compiler recompiled - null the first pass
	 * @param files the (unmodifiable) List of File passed as sources to the compiler
     * @param recompiled the List sink for the Files actually recompiled
     */
    // XXX argh no parent/child relationship in this world...
	protected boolean verifyCompile(
		int iteration,
		boolean result,
		File srcDir,
		File sandboxSrcDir,
		File sandboxClassesDir,
		Definition def,
		IMessageHolder compilerMessages,
        StringBuilder commandLine,
		IMessageHandler handler) {
        log("verifyCompile -  iteration ", iteration, handler);
		log("verifyCompile -        def ", def, handler);
        log("verifyCompile -    command ", commandLine.toString(), handler);
		log("verifyCompile -   messages ", compilerMessages, handler);
        StringBuilder failures = new StringBuilder();
        if (def.expectFail == result) {
             failures.append("iteration " + iteration +
                " expected to " + (def.expectFail ? "fail\n" : "pass"));
        }
        if (0 < failures.length()) {
            fail(handler,
                "\nFailures in iteration " + iteration
                + "\n Command: " + commandLine
                + "\nMessages: " + compilerMessages
                + "\n     Def: " + def
                + "\nFailures: " + failures);
            return false;
        }
		IMessage[] messages = compilerMessages.getMessages(IMessage.ERROR, IMessageHolder.EQUAL);
		String[] expected =
			(null != def.errors ? def.errors : def.eclipseErrors);
		if (haveAll("errors", expected, messages, handler)) {
			if (!ignoreWarnings) {
				messages = compilerMessages.getMessages(IMessage.WARNING, IMessageHolder.EQUAL);
				expected =
					(null != def.warnings
						? def.warnings
						: def.eclipseWarnings);
				if (!haveAll("warnings", expected, messages, handler)) {
                    return false;
                }
			}
		}
        return true;
	}

	// -------------------------------------- test case setup
	/**
	 * Get the sandbox (parent) directory.
	 * This implementation uses the temporary directory
	 */
	protected File getSandboxDir() throws IOException { // XXX util
		File tempFile = File.createTempFile("IncrementalCase", ".txt");
		File tempDir = tempFile.getParentFile();
		tempFile.delete();
		return tempDir;
	}

	//XXX hack
	public File outputDir;

	/** @param srcDir ignored for now */
	protected List<String> getBaseArgs(File srcDir, File classesDir) {
		outputDir = classesDir;
		String[] input =
			new String[] {
				"-verbose",
//				"-classpath",
//				System.getProperty("sun.boot.class.path"),
				"-d",
				classesDir.getAbsolutePath()};
		return Collections.unmodifiableList(
				new ArrayList<>(Arrays.asList(input)));
	}

	protected File makeDir(
		File parent,
		String name,
		IMessageHandler handler) { // XXX util
		File result = new File(parent, name);
		if (!result.exists()) {
			result.mkdirs();
			if (!result.exists()) {
				fail(handler, "unable to create " + result);
				return null;
			}
		}
		return result;
	}

    // -------------------------------------- test case verification


    List<String> normalizeFilenames(String[] ra) { // XXX util
        List<String> result = new ArrayList<>();
        if (null != ra) {
			for (String s : ra) {
				result.add(normalizeFilename(s));
			}
            if (1 < ra.length) {
                Collections.sort(result);
            }
        }
        return result;
    }

    /** @param list the List of File */
    List<String> normalizeFilenames(List<File> list) { // XXX util
        List<String> result = new ArrayList<>();
        for (File file: list) {
//        for (Iterator<?> iter = list.iterator(); iter.hasNext();) {
			result.add(normalizeFilename(file.getPath()));
		}
        Collections.sort(result);
        return result;
    }

    String normalizeFilename(String s) {        // XXX error-prone
        final String suffix = ".java";
        int loc = s.lastIndexOf(suffix);
        if (-1 == loc) {
            return s; // punt
        }
        s = s.substring(0, loc + suffix.length()).replace('\\', '/');
        loc = s.lastIndexOf("/");
        return (-1 == loc ? s : s.substring(loc+1));
    }


	/** XXX duplicate message checking */
	boolean haveAll(
		String label,
		String[] expected,
		IMessage[] messages,
		IMessageHandler handler) {
		if (null == expected) {
			expected = new String[0];
		}
		boolean result = true;
		final int[] exp = new int[expected.length];
		StringBuilder sb = new StringBuilder();
		sb.append("[");
		for (int i = 0; i < exp.length; i++) {
			String s = expected[i];
			int loc = s.lastIndexOf(":");
			if (-1 != loc)
				s = s.substring(loc + 1);
			try {
				exp[i] = Integer.parseInt(s);
				sb.append(exp[i] + ((i < (exp.length - 1)) ? ", " : ""));
			} catch (NumberFormatException e) {
				info(handler, "bad " + label + ":" + expected[i]);
				// XXX worse than info...
				sb.append("bad" + ((i < (exp.length - 1)) ? ", " : "]"));
			}
		}
        sb.append("]");
		final String context =
			label
				+ "\n in context haveAll expected="
				+ Arrays.asList(expected)
				+ " exp="
				+ sb
				+ " actual="
				+ Arrays.asList(messages);
		info(handler, context);

		BitSet foundSet = new BitSet(10);
		for (final int expLine : exp) {
			boolean found = false;
			for (int j = 0; !found && (j < messages.length); j++) {
				ISourceLocation sl = messages[j].getSourceLocation();
				found = ((null != sl) && (expLine == sl.getLine()));
				if (found) {
					info(handler, "found " + label + " for: " + expLine);
					if (foundSet.get(j)) {
						info(
								handler,
								"duplicate " + label + " expected: " + expLine);
					}
					foundSet.set(j);
				}
			}
			if (!found) {
				String s =
						"expected "
								+ label
								+ " not found: "
								+ expLine
								+ context;
				fail(handler, s); // bad short-circuit
				if (!result) {
					result = false;
				}
			}

		}
		sb.setLength(0);
		for (int i = 0; i < messages.length; i++) {
			if (!foundSet.get(i)) {
				sb.append(
					"\n unexpected " + label + " found: " + messages[i]);
			}
		}
		if (0 == sb.length()) {
			return true;
		} else {
			fail(handler, sb.toString() + context);
			return false;
		}
	}
	// -------------------------------------- messages
	protected void log(String label, Object o, IMessageHandler handler) {
		if (verbose) {
            if (null != handler) {
    			message(IMessage.INFO, label + ": " + o, handler);
            } else {
                System.err.println("\nlog: " + label + ": " + o);
		    }
        }
	}
	protected void info(IMessageHandler handler, String mssg) {
		message(IMessage.INFO, mssg, handler);
	}
	protected void fail(IMessageHandler handler, String mssg) {
		message(IMessage.FAIL, mssg, handler);
	}

	/** this is the only client of the message handler - remplement to do other notification*/
	protected void message(
		IMessage.Kind kind,
		String mssg,
		IMessageHandler handler) {
		if (null != handler) {
			handler.handleMessage(
				new Message("\n### " + mssg, kind, null, null));
		}
	}

	/** @return Definition[9] read from srceBase/Definition.PATH */
	Definition[] getDefinitions(File srcBase) {
		File file = new File(srcBase, Definition.PATH);
		Properties props = new Properties();
		FileInputStream in = null;
		try {
			in = new FileInputStream(file);
			props.load(in);
		} catch (IOException e) {
			e.printStackTrace(System.err);
		} finally {
			if (null != in)
				try {
					in.close();
				} catch (IOException e) {
				}
		}
		Definition[] result = new Definition[9];
		for (int i = 0; i < 9; i++) { // XXX matches run
			result[i] = new Definition((1+i) + "0", props);
		}
		return result;
	}

	static class Definition {
		static final String PATH = "expected.txt";
		boolean expectFail;
		String prefix;
		String[] files;
		String[] recompiled;
		String[] errors;
		String[] warnings;
		String[] eclipseErrors;
		String[] eclipseWarnings;
		Definition(String prefix, Properties props) {
//			Enumeration keys = props.keys();
			this.prefix = prefix;
			files = get(props, prefix + ".files");
			recompiled = get(props, prefix + ".recompiled");
			errors = get(props, prefix + ".errors");
			warnings = get(props, prefix + ".warnings");
			eclipseErrors = get(props, prefix + ".eclipse.errors");
			eclipseWarnings = get(props, prefix + ".eclipse.warnings");
			expectFail =
				(((null != errors) && (0 < errors.length))
					|| ((null != eclipseErrors)
						&& (0 < eclipseErrors.length)));
		}
		String[] get(Properties props, String key) {
			String s = props.getProperty(key);
			if (null != s) {
				return LangUtil.split(s);
			}
			return null;
		}
		@Override
		public String toString() {
			return "Definition "
                + " expectFail="
                + expectFail
                + " prefix="
				+ prefix
				+ " files="
				+ safe(files)
                + " recompiled="
                + safe(recompiled)
				+ " errors="
				+ safe(errors)
				+ " warnings="
				+ safe(warnings)
				+ " eclipseErrors="
				+ safe(eclipseErrors)
				+ " eclipseWarnings="
				+ safe(eclipseWarnings);
		}
		String safe(String[] in) {
			return (null == in ? "" : "" + Arrays.asList(in));
		}
	}
}