FlatSuiteReader.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.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessage.Kind;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.bridge.Message;
import org.aspectj.bridge.MessageUtil;
import org.aspectj.bridge.SourceLocation;
import org.aspectj.testing.util.BridgeUtil;
import org.aspectj.testing.util.ObjectChecker;
import org.aspectj.testing.util.SFileReader;
import org.aspectj.testing.util.StandardObjectChecker;
import org.aspectj.testing.util.UtilLineReader;
import org.aspectj.util.FileUtil;
import org.aspectj.util.LangUtil;

/**
 * SFileReader.Maker implementation to read tests
 * XXX supports iterative but not yet incremental compiles
 */
public class FlatSuiteReader implements SFileReader.Maker {
	public static final String[] RA_String = new String[0];
	public static final FlatSuiteReader ME = new FlatSuiteReader();
	private static final SFileReader READER = new SFileReader(ME);

	static boolean isNumber(String s) { // XXX costly
		if ((null == s) || (0 == s.length())) {
			return false;
		}
		try {
			Integer.valueOf(s);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}

    /** if true, clean up records before returning from make */
    public boolean clean;

	private FlatSuiteReader() {
	}

	/**
	 * @see org.aspectj.testing.harness.bridge.SFileReader.Maker#getType()
	 */
	public Class getType() {
		return AjcTest.Spec.class;
	}

	/**
	 * This constructs an AjcTest.Spec assuming we are at the start of a
	 * test definition in reader and taking the parent directory of
	 * the reader as the base directory for the test suite root.
	 * @return the next AjcTest in reader, or null
	 * @see org.aspectj.testing.harness.bridge.SFileReader.Maker#make(UtilLineReader)
	 */
	public Object make(final UtilLineReader reader)
		throws AbortException, IOException {
		final AjcTest.Spec result = new AjcTest.Spec();
		boolean usingEclipse = false; // XXX
		/** handle read errors by throwing AbortException with context info */
		class R {
			public String read(String context) throws IOException {
				return read(context, true);
			}
			public String read(String context, boolean required)
				throws IOException {
				final boolean skipEmpties = false;
				String result = reader.nextLine(skipEmpties);
				if ((null != result) && (0 == result.length())) {
					result = null;
				}
				if ((null == result) && required) {
					String s = "expecting " + context + " at " + reader;
					throw new AbortException(s);
				}
				return result;
			}
		}

		final R r = new R();
		//final String baseDir = reader.getFile().getParent();
		String line;
		String[] words;
//		boolean isRequired = true;

		final int startLine = reader.getLineNumber() - 1;

		// description first - get from last line read
		// XXX permits exactly one blank line between test records?
		result.description = reader.lastLine();
		if (null == result.description) {
			throw new AbortException("expecting description at " + reader);
		}

		// next line is baseDir {option..}
		line = r.read("baseDir {option..}");
		words = LangUtil.split(line);
		if ((null == words) || (0 == words.length)) {
			throw new AbortException(
				"expecting dir {option..} at " + reader);
		}
		// XXX per-test (shared) root
		//final File sourceRoot =  new File(baseDir, words[0]);
		result.setTestDirOffset(words[0]);

		String[] compileOptions = new String[words.length - 1];
		System.arraycopy(words, 1, compileOptions, 0, words.length - 1);

		// next are 1..n source lines: source...
		CompilerRun.Spec lastCompileSpec = null;
		// save last source file as default for error/warning line
		File lastFile = null; // XXX per-compiler-run errors
		while (null != (line = r.read("source.."))) {
			words = LangUtil.split(line);
			if (0 == FileUtil.sourceSuffixLength(words[0])) { // XXX
				break;
			} else {
				lastCompileSpec = new CompilerRun.Spec();
				lastCompileSpec.testSrcDirOffset = null;
				// srcs are in test base for old
				lastCompileSpec.addOptions(compileOptions);
				lastCompileSpec.addPaths(words);
				lastFile = new File(words[words.length - 1]);
				result.addChild(lastCompileSpec);
			}
		}
		if (null == lastCompileSpec) {
			throw new AbortException("expected sources at " + reader);
		}

		List<Message> exp = new ArrayList<>();
		// !compile || noerrors || className {runOption..}
		String first = words[0];
		if ("!compile".equals(first)) {
			//result.className = words[0];
			//result.runOptions = new String[words.length-1];
			//System.arraycopy(words, 0, result.runOptions, 0, words.length-1);
		} else if ("noerrors".equals(first)) {
			// className is null, but no errors expected
			// so compile succeeds but run not attempted
			//result.errors = Main.RA_ErrorLine;
			// result.runOptions = Main.RA_String;
		} else if (isNumber(first) || (first.contains(":"))) {
			exp.addAll(makeMessages(IMessage.ERROR, words, 0, lastFile));
		} else {
			String[] args = new String[words.length - 1];
			System.arraycopy(words, 0, args, 0, args.length);
			JavaRun.Spec spec = new JavaRun.Spec();
			spec.className = first;
			spec.addOptions(args);
			//XXXrun.runDir = sourceRoot;
			result.addChild(spec);
		}

		// optional: warnings, eclipse.warnings, eclipse.errors
		// XXX unable to specify error in eclipse but not ajc
		boolean gotErrors = false;
		while (null
			!= (line =
				r.read(
					" errors, warnings, eclipse.warnings, eclipse.error",
					false))) {
			words = LangUtil.split(line);
			first = words[0];
			if ("eclipse.warnings:".equals(first)) {
				if (usingEclipse) {
					exp.addAll(
						makeMessages(
							IMessage.WARNING,
							words,
							0,
							lastFile));
				}
			} else if ("eclipse.errors:".equals(first)) {
				if (usingEclipse) {
					exp.addAll(
						makeMessages(IMessage.ERROR, words, 0, lastFile));
				}
			} else if ("warnings:".equals(first)) {
				exp.addAll(
					makeMessages(IMessage.WARNING, words, 0, lastFile));
			} else if (gotErrors) {
				exp.addAll(
					makeMessages(IMessage.WARNING, words, 0, lastFile));
			} else {
				exp.addAll(
					makeMessages(IMessage.ERROR, words, 0, lastFile));
				gotErrors = true;
			}
		}
		lastCompileSpec.addMessages(exp);

		int endLine = reader.getLineNumber();
		File sourceFile = reader.getFile();
		ISourceLocation sl =
			new SourceLocation(sourceFile, startLine, endLine, 0);
		result.setSourceLocation(sl);

        if (clean) {
            cleanup(result, reader);
        }
        return result;
	}

    /** post-process result
     * - use file name as keyword
     * - clip / for dir offsets
     * - extract purejava keyword variants
     * - extract bugID
     * - convert test options to force-options
     * - detect illegal xml characters
     */
    private void cleanup(AjcTest.Spec result, UtilLineReader lineReader) {
        LangUtil.throwIaxIfNull(result, "result");
        LangUtil.throwIaxIfNull(lineReader, "lineReader");

        File suiteFile = lineReader.getFile();
        String name = suiteFile.getName();
        if (!name.endsWith(".txt")) {
            throw new Error("unexpected name: " + name);
        }
        result.addKeyword("from-" + name.substring(0,name.length()-4));

        final String dir = result.testDirOffset;
        if (dir.endsWith("/")) {
            result.testDirOffset = dir.substring(0,dir.length()-1);
        }

        StringBuffer description = new StringBuffer(result.description);
        if (strip(description, "PUREJAVA")) {
            result.addKeyword("purejava");
        }
        if (strip(description, "PUREJAVE")) {
            result.addKeyword("purejava");
        }
        if (strip(description, "[purejava]")) {
            result.addKeyword("purejava");
        }
        String input = description.toString();
        int loc = input.indexOf("PR#");
        if (-1 != loc) {
            String prefix = input.substring(0, loc).trim();
            String pr = input.substring(loc+3, loc+6).trim();
            String suffix = input.substring(loc+6).trim();
            description.setLength(0);
            description.append((prefix + " " + suffix).trim());
            try {
                result.setBugId(Integer.parseInt(pr));
            } catch (NumberFormatException e) {
                throw new Error("unable to convert " + pr + " for " + result
                    + " at " + lineReader);
            }
        }
        input = description.toString();
        String error = null;
        if (input.contains("&")) {
            error = "char &";
        } else if (input.contains("<")) {
            error = "char <";
        } else if (input.contains(">")) {
            error = "char >";
        } else if (input.contains("\"")) {
            error = "char \"";
        }
        if (null != error) {
            throw new Error(error + " in " + input + " at " + lineReader);
        }
        result.description = input;

        ArrayList<String> newOptions = new ArrayList<>();
        Iterable<String> optionsCopy = result.getOptionsList();
        for (String option: optionsCopy) {
			if (option.startsWith("-")) {
                newOptions.add("!" + option.substring(1));
            } else {
                throw new Error("non-flag option? " + option);
            }
		}
        result.setOptionsArray((String[]) newOptions.toArray(new String[0]));
    }

    private boolean strip(StringBuffer sb, String infix) {
        String input = sb.toString();
        int loc = input.indexOf(infix);
        if (-1 != loc) {
            String prefix = input.substring(0, loc);
            String suffix = input.substring(loc+infix.length());
            input = (prefix.trim() + " " + suffix.trim()).trim();
            sb.setLength(0);
            sb.append(input);
            return true;
        }
        return false;
    }

	/**
	 * Generate list of expected messages of this kind.
	 * @param kind any non-null kind, but s.b. IMessage.WARNING or ERROR
	 * @param words
	 * @param start index in words where to start
	 * @param lastFile default file for source location if the input does not specify
	 * @return List
	 */
	private List<Message> makeMessages(// XXX weak - also support expected exceptions, etc.
	Kind kind, String[] words, int start, File lastFile) {
		List<Message> result = new ArrayList<>();
		for (int i = start; i < words.length; i++) {
			ISourceLocation sl =
				BridgeUtil.makeSourceLocation(words[i], lastFile);
			if (null == sl) { // XXX signalling during make
				// System.err.println(...);
				//MessageUtil.debug(handler, "not a source location: " + words[i]);
			} else {
				String text =
					(("" + sl.getLine()).equals(words[i]) ? "" : words[i]);
				result.add(new Message(text, kind, null, sl));
			}
		}
		return (0 == result.size() ? Collections.<Message>emptyList() : result);
	}

	/**
	 * Read suite spec from a flat .txt file.
	 * @throws AbortException on failure
	 * @return AjcTest.Suite.Spec with any AjcTest.Spec as children
	 */
	public AjcTest.Suite.Spec readSuite(File suiteFile) {
		LangUtil.throwIaxIfNull(suiteFile, "suiteFile");
		if (!suiteFile.isAbsolute()) {
			suiteFile = suiteFile.getAbsoluteFile();
		}
        final AjcTest.Suite.Spec result = new AjcTest.Suite.Spec();
        result.setSuiteDirFile(suiteFile.getParentFile());
		ObjectChecker collector = new StandardObjectChecker(IRunSpec.class) {
			public boolean doIsValid(Object o) {
                result.addChild((IRunSpec) o);
				return true;
			}
		};
		boolean abortOnError = true;
		try {
			READER.readNodes(
				suiteFile,
				collector,
				abortOnError,
				System.err);
		} catch (IOException e) {
			IMessage m = MessageUtil.fail("reading " + suiteFile, e);
			throw new AbortException(m);
		}

		return result;
	}
}