AjcTest.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.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
//import org.aspectj.bridge.*;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.ISourceLocation;
import org.aspectj.testing.run.IRunIterator;
import org.aspectj.testing.run.IRunStatus;
import org.aspectj.testing.run.Runner;
import org.aspectj.testing.xml.XMLWriter;
import org.aspectj.util.LangUtil;
/**
* An AjcTest has child subruns (compile, [inc-compile|run]*).
* XXX title keys shared between all instances
* (add Thread to key to restrict access?)
*/
public class AjcTest extends RunSpecIterator {
/** Unwrap an AjcTest.Spec from an IRunStatus around an AjcTest */
public static Spec unwrapSpec(IRunStatus status) {
if (null != status) {
Object id = status.getIdentifier();
if (id instanceof Runner.IteratorWrapper) {
IRunIterator iter = ((Runner.IteratorWrapper) id).iterator;
if (iter instanceof AjcTest) {
return (Spec) ((AjcTest) iter).spec;
}
}
}
return null;
}
/** Unwrap initial CompilerRun.Spec from an AjcTest.Spec */
public static CompilerRun.Spec unwrapCompilerRunSpec(Spec spec) {
if (null != spec) {
List kids = spec.getChildren();
if (0 < kids.size()) {
Object o = kids.get(0);
if (o instanceof CompilerRun.Spec) {
return (CompilerRun.Spec) o;
}
}
}
return null;
}
/** The spec creates the sandbox, so we use it throughout */
public AjcTest(Spec spec, Sandbox sandbox, Validator validator) {
super(spec, sandbox, validator, true);
}
/**
* Clear the command from the sandbox, to avoid memory leaks.
* @see org.aspectj.testing.harness.bridge.RunSpecIterator#iterationCompleted()
*/
public void iterationCompleted() {
super.iterationCompleted();
sandbox.clearCommand(this);
}
/**
* Specification for an ajc test.
* Keyword directives are global/parent options passed, e.g., as
* <pre>-ajctest[Require|Skip]Keywords=keyword{,keyword}..</pre>.
* See VALID_SUFFIXES for complete list.
*/
public static class Spec extends AbstractRunSpec {
public static final String XMLNAME = "ajc-test";
/**
* do description as title, do sourceLocation,
* do keywords, do options, skip paths, do comment,
* skip staging, skip badInput,
* skip dirChanges, do messages and do children
* (though we do children directly).
*/
private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
"title", null, null, null, "", null, "", "", true, false, false);
private static final String OPTION_PREFIX = "-ajctest";
private static final String[] VALID_OPTIONS = new String[] { OPTION_PREFIX };
private static final String TITLE_LIST = "TitleList=";
private static final String TITLE_FAIL_LIST = "TitleFailList=";
private static final String TITLE_CONTAINS= "TitleContains=";
private static final String REQUIRE_KEYWORDS = "RequireKeywords=";
private static final String SKIP_KEYWORDS = "SkipKeywords=";
private static final String PICK_PR = "PR=";
private static final List<String> VALID_SUFFIXES
= Collections.unmodifiableList(Arrays.asList(new String[]
{ TITLE_LIST, TITLE_FAIL_LIST, TITLE_CONTAINS,
REQUIRE_KEYWORDS, SKIP_KEYWORDS, PICK_PR }));
/** Map String titlesName to List (String) of titles to accept */
private static final Map<String,List<String>> TITLES = new HashMap<>();
private static List<String> getTitles(String titlesName) {
return getTitles(titlesName, false);
}
private static List<String> getTitles(String titlesName, boolean fail) {
if (LangUtil.isEmpty(titlesName)) {
return Collections.emptyList();
}
List<String> result = (List<String>) TITLES.get(titlesName);
if (null == result) {
result = makeTitlesList(titlesName, fail);
TITLES.put(titlesName, result);
}
return result;
}
/**
* Make titles list per titlesKey, either a path to a file
* containing "[PASS|FAIL] {title}(..)" entries,
* or a comma-delimited list of titles.
* @param titlesKey a String, either a path to a file
* containing "[PASS|FAIL] {title}(..)" entries,
* or a comma-delimited list of titles.
* @param fail if true, only read titles prefixed "FAIL" from files
* @return the unmodifiable List of titles (maybe empty, never null)
*/
private static List<String> makeTitlesList(String titlesKey, boolean fail) {
File file = new File(titlesKey);
return file.canRead()
? readTitlesFile(file, fail)
: parseTitlesList(titlesKey);
}
/**
* Parse list of titles from comma-delmited list
* titlesList, trimming each entry and permitting
* comma to be escaped with '\'.
* @param titlesList a comma-delimited String of titles
* @return the unmodifiable List of titles (maybe empty, never null)
*/
private static List<String> parseTitlesList(String titlesList) {
List<String> result = new ArrayList<>();
String last = null;
StringTokenizer st = new StringTokenizer(titlesList, ",");
while (st.hasMoreTokens()) {
String next = st.nextToken().trim();
if (next.endsWith("\\")) {
next = next.substring(0, next.length()-1);
if (null == last) {
last = next;
} else {
last += next;
}
next = null;
} else if (null != last) {
next = (last + next).trim();
last = null;
} else {
next = next.trim();
}
if (!LangUtil.isEmpty(next)) {
result.add(next);
}
}
if (null != last) {
String m = "unterminated entry \"" + last; // XXX messages
System.err.println(m + "\" in " + titlesList);
result.add(last.trim());
}
return Collections.unmodifiableList(result);
}
/**
* Read titles from a test result file, accepting
* only those prefixed with [PASS|FAIL] and
* excluding the "[PASS|FAIL] Suite.Spec(.." entry.
* @param titlesFile the File containing a
* list of titles from test results,
* with some lines of the form
* <code>[PASS|FAIL] {title}()<code> (excluding
* <code>[PASS|FAIL] Suite.Spec(...<code>.
* @param titlesFile the File path to the file containing titles
* @param fail if true, only select titles prefixed "FAIL"
* @return the unmodifiable List of titles (maybe empty, never null)
*/
private static List<String> readTitlesFile(File titlesFile, boolean fail) {
List<String> result = new ArrayList<>();
Reader reader = null;
try {
reader = new FileReader(titlesFile);
BufferedReader lines = new BufferedReader(reader);
String line;
while (null != (line = lines.readLine())) {
if ((line.startsWith("FAIL ")
|| (!fail && line.startsWith("PASS ")))
&& (!line.substring(5).startsWith("Suite.Spec("))) {
String title = line.substring(5);
int loc = title.lastIndexOf("(");
if (-1 != loc) {
title = title.substring(0, loc);
}
result.add(title);
}
}
} catch (IOException e) {
System.err.println("ignoring titles in " + titlesFile); // XXX messages
e.printStackTrace(System.err);
} finally {
if (null != reader) {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}
return Collections.unmodifiableList(result);
}
/** base directory of the test suite - set before making run */
private File suiteDir;
/** path offset from suite directory to base of test directory */
String testDirOffset; // XXX revert to private after fixes
/** id of bug - if 0, then no bug associated with this test */
private int bugId;
public Spec() {
super(XMLNAME);
setXMLNames(NAMES);
}
protected void initClone(Spec spec)
throws CloneNotSupportedException {
super.initClone(spec);
spec.bugId = bugId;
spec.suiteDir = suiteDir;
spec.testDirOffset = testDirOffset;
}
public Object clone() throws CloneNotSupportedException {
Spec result = new Spec();
initClone(result);
return result;
}
public void setSuiteDir(File suiteDir) {
this.suiteDir = suiteDir;
}
public File getSuiteDir() {
return suiteDir;
}
/** @param bugId 100..999999 */
public void setBugId(int bugId) {
LangUtil.throwIaxIfFalse((bugId > 10) && (bugId < 1000000), "bad bug id: " + bugId);
this.bugId = bugId;
}
public int getBugId() {
return bugId;
}
public void setTestDirOffset(String testDirOffset) {
if (!LangUtil.isEmpty(testDirOffset)) {
this.testDirOffset = testDirOffset;
}
}
public String getTestDirOffset() {
return (null == testDirOffset ? "" : testDirOffset);
}
/**
* @param sandbox ignored
* @see org.aspectj.testing.harness.bridge.AbstractRunSpec#makeAjcRun(Sandbox, Validator)
*/
public IRunIterator makeRunIterator(Sandbox sandbox, Validator validator) {
LangUtil.throwIaxIfNull(validator, "validator");
// if no one set suiteDir, see if we have a source location
if (null == suiteDir) {
ISourceLocation loc = getSourceLocation();
if (!validator.nullcheck(loc, "suite file location")
|| !validator.nullcheck(loc.getSourceFile(), "suite file")) {
return null;
}
File locDir = loc.getSourceFile().getParentFile();
if (!validator.canReadDir(locDir, "source location dir")) {
return null;
}
suiteDir = locDir;
}
// we make a new sandbox with more state for our subruns, keep that,
// in order to defer initialization to nextRun()
File testBaseDir;
String testDirOffset = getTestDirOffset();
if (LangUtil.isEmpty(testDirOffset)) {
testBaseDir = suiteDir;
} else {
testBaseDir = new File(suiteDir, testDirOffset);
if (!validator.canReadDir(testBaseDir, "testBaseDir")) {
return null;
}
}
Sandbox childSandbox = null;
try {
childSandbox = new Sandbox(testBaseDir, validator);
validator.registerSandbox(childSandbox);
} catch (IllegalArgumentException e) {
validator.fail(e.getMessage());
return null;
}
return new AjcTest(this, childSandbox, validator);
}
/** @see IXmlWritable#writeXml(XMLWriter) */
public void writeXml(XMLWriter out) {
out.println("");
String value = (null == testDirOffset? "" : testDirOffset);
String attr = XMLWriter.makeAttribute("dir", value);
if (0 != bugId) {
attr += " " + XMLWriter.makeAttribute("pr", ""+bugId);
}
out.startElement(xmlElementName, attr, false);
super.writeAttributes(out);
out.endAttributes();
super.writeChildren(out);
out.endElement(xmlElementName);
}
/**
* AjcTest overrides this to skip if
* <ul>
* <li>the spec has a keyword the parent wants to skip</li>
* <li>the spec does not have a required keyword</li>
* <li>the spec does not have a required bugId</li>
* <li>the spec does not have a required title (description)n</li>
* </ul>
* When skipping, this issues a messages as to why skipped.
* Skip combinations are not guaranteed to work correctly. XXX
* @return false if this wants to be skipped, true otherwise
* @throws Error if selected option is not of the form
* <pre>-ajctest[Require|Skip]Keywords=keyword{,keyword}..</pre>.
*/
protected boolean doAdoptParentValues(RT parentRuntime, IMessageHandler handler) {
if (!super.doAdoptParentValues(parentRuntime, handler)) {
return false;
}
runtime.copy(parentRuntime);
String[] globalOptions = runtime.extractOptions(VALID_OPTIONS, true);
for (String globalOption : globalOptions) {
String option = globalOption;
if (!option.startsWith(OPTION_PREFIX)) {
throw new Error("only expecting " + OPTION_PREFIX + "..: " + option);
}
option = option.substring(OPTION_PREFIX.length());
boolean keywordMustExist = false;
List<String> permittedTitles = null;
List<String> permittedTitleStrings = null;
String havePr = null;
if (option.startsWith(REQUIRE_KEYWORDS)) {
option = option.substring(REQUIRE_KEYWORDS.length());
keywordMustExist = true;
} else if (option.startsWith(SKIP_KEYWORDS)) {
option = option.substring(SKIP_KEYWORDS.length());
} else if (option.startsWith(TITLE_LIST)) {
option = option.substring(TITLE_LIST.length());
permittedTitles = getTitles(option);
} else if (option.startsWith(TITLE_FAIL_LIST)) {
option = option.substring(TITLE_FAIL_LIST.length());
permittedTitles = getTitles(option, true);
} else if (option.startsWith(TITLE_CONTAINS)) {
option = option.substring(TITLE_CONTAINS.length());
permittedTitleStrings = getTitles(option);
} else if (option.startsWith(PICK_PR)) {
if (0 == bugId) {
skipMessage(handler, "bugId required, but no bugId for this test");
return false;
} else {
havePr = "" + bugId;
}
option = option.substring(PICK_PR.length());
} else {
throw new Error("unrecognized suffix: " + globalOption
+ " (expecting: " + OPTION_PREFIX + VALID_SUFFIXES + "...)");
}
if (null != permittedTitleStrings) {
boolean gotHit = false;
for (Iterator<String> iter = permittedTitleStrings.iterator();
!gotHit && iter.hasNext();
) {
String substring = (String) iter.next();
if (this.description.contains(substring)) {
gotHit = true;
}
}
if (!gotHit) {
String reason = "title "
+ this.description
+ " does not contain any of "
+ option;
skipMessage(handler, reason);
return false;
}
} else if (null != permittedTitles) {
if (!permittedTitles.contains(this.description)) {
String reason = "titlesList "
+ option
+ " did not contain "
+ this.description;
skipMessage(handler, reason);
return false;
}
} else {
// all other options handled as comma-delimited lists
List<String> specs = LangUtil.commaSplit(option);
// XXX also throw Error on empty specs...
for (String spec : specs) {
if (null != havePr) {
if (havePr.equals(spec)) { // String.equals()
havePr = null;
}
} else if (keywordMustExist != keywords.contains(spec)) {
String reason = "keyword " + spec
+ " was " + (keywordMustExist ? "not found" : "found");
skipMessage(handler, reason);
return false;
}
}
if (null != havePr) {
skipMessage(handler, "bugId required, but not matched for this test");
return false;
}
}
}
return true;
}
} // AjcTest.Spec
/**
* A suite of AjcTest has children for each AjcTest
* and flows all options down as globals
*/
public static class Suite extends RunSpecIterator {
final Spec spec;
/**
* Count the number of AjcTest in this suite.
* @param spec
* @return
*/
public static int countTests(Suite.Spec spec) {
return spec.children.size();
}
public static AjcTest.Spec[] getTests(Suite.Spec spec) {
if (null == spec) {
return new AjcTest.Spec[0];
}
return (AjcTest.Spec[]) spec.children.toArray(new AjcTest.Spec[0]);
}
public Suite(Spec spec, Sandbox sandbox, Validator validator) {
super(spec, sandbox, validator, false);
this.spec = spec;
}
/**
* While being called to make the sandbox for the child,
* set up the child's suite dir based on ours.
* @param child must be instanceof AjcTest.Spec
* @see org.aspectj.testing.harness.bridge.RunSpecIterator#makeSandbox(IRunSpec, Validator)
* @return super.makeSandbox(child, validator)
*/
protected Sandbox makeSandbox(
IRunSpec child,
Validator validator) {
if (!(child instanceof AjcTest.Spec)) {
validator.fail("only expecting AjcTest children");
return null;
}
if (!validator.canReadDir(spec.suiteDir, "spec.suiteDir")) {
return null;
}
((AjcTest.Spec) child).setSuiteDir(spec.suiteDir);
return super.makeSandbox(child, validator);
}
/**
* A suite spec contains AjcTest children.
* The suite dir or source location should be set
* if the tests do not each have a source location
* with a source file in the suite dir.
* XXX whether to write out suiteDir in XML?
*/
public static class Spec extends AbstractRunSpec {
public static final String XMLNAME = "suite";
/**
* do description, do sourceLocation,
* do keywords, do options, skip paths, do comment,
* skip staging, skip badInput,
* skip dirChanges, skip messages and do children
* (though we do children directly).
*/
// private static final XMLNames NAMES = new XMLNames(XMLNames.DEFAULT,
// null, null, null, null, "", null, "", "", true, true, false);
File suiteDir;
public Spec() {
super(XMLNAME, false); // do not skip this even if children skip
}
public Object clone() throws CloneNotSupportedException {
Spec spec = new Spec();
super.initClone(spec);
spec.suiteDir = suiteDir;
return spec;
}
/** @param suiteDirPath the String path to the base suite dir */
public void setSuiteDir(String suiteDirPath) {
if (!LangUtil.isEmpty(suiteDirPath)) {
this.suiteDir = new File(suiteDirPath);
}
}
/** @param suiteDirFile the File for the base suite dir */
public void setSuiteDirFile(File suiteDir) {
this.suiteDir = suiteDir;
}
/** @return suiteDir from any set or source location if set */
public File getSuiteDirFile() {
if (null == suiteDir) {
ISourceLocation loc = getSourceLocation();
if (null != loc) {
File sourceFile = loc.getSourceFile();
if (null != sourceFile) {
suiteDir = sourceFile.getParentFile();
}
}
}
return suiteDir;
}
/**
* @return
* @see org.aspectj.testing.harness.bridge.AbstractRunSpec#makeRunIterator(Sandbox, Validator)
*/
public IRunIterator makeRunIterator(
Sandbox sandbox,
Validator validator) {
return new Suite(this, sandbox, validator);
}
public String toString() {
// removed nKids as misleading, since children.size() may change
//int nKids = children.size();
//return "Suite.Spec(" + suiteDir + ", " + nKids + " tests)";
return "Suite.Spec(" + suiteDir + ")";
}
}
}
}