// Copyright (C) 2008 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.caja.util;
import com.google.caja.SomethingWidgyHappenedError;
import com.google.caja.lexer.CharProducer;
import com.google.caja.lexer.FetchedData;
import com.google.caja.lexer.FilePosition;
import com.google.caja.lexer.GuessContentType;
import com.google.caja.lexer.HtmlLexer;
import com.google.caja.lexer.HtmlTokenType;
import com.google.caja.lexer.InputSource;
import com.google.caja.lexer.JsLexer;
import com.google.caja.lexer.JsTokenQueue;
import com.google.caja.lexer.JsTokenType;
import com.google.caja.lexer.ParseException;
import com.google.caja.lexer.Token;
import com.google.caja.lexer.TokenConsumer;
import com.google.caja.lexer.TokenQueue;
import com.google.caja.parser.ParseTreeNode;
import com.google.caja.parser.html.DomParser;
import com.google.caja.parser.html.Nodes;
import com.google.caja.parser.js.Block;
import com.google.caja.parser.js.Expression;
import com.google.caja.parser.js.Parser;
import com.google.caja.render.JsMinimalPrinter;
import com.google.caja.render.JsPrettyPrinter;
import com.google.caja.reporting.Message;
import com.google.caja.reporting.MessageContext;
import com.google.caja.reporting.MessageLevel;
import com.google.caja.reporting.MessagePart;
import com.google.caja.reporting.MessageQueue;
import com.google.caja.reporting.MessageTypeInt;
import com.google.caja.reporting.RenderContext;
import com.google.caja.tools.TestSummary;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.StringReader;
import java.net.URI;
import java.util.regex.Pattern;
import junit.framework.TestCase;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
public abstract class CajaTestCase extends TestCase {
protected InputSource is;
protected MessageContext mc;
protected MessageQueue mq;
protected CajaTestCase(String name) { super(name); }
protected CajaTestCase() { super(); }
/**
* For random tests we choose a seed by using a system property so that
* failing random tests can be repeated.
*/
protected static final long SEED = Long.parseLong(
System.getProperty("junit.seed", "" + System.currentTimeMillis()));
private static boolean dumpedSeed;
@Override
protected void setUp() throws Exception {
synchronized (CajaTestCase.class) {
if (!dumpedSeed) {
dumpedSeed = true;
// Make sure it shows up in the junit test runner trace.
System.err.println("junit.seed=" + SEED);
}
}
this.is = new InputSource(URI.create("http://example.org/" + getName()));
this.mc = new MessageContext();
mc.addInputSource(is);
this.mq = TestUtil.createTestMessageQueue(this.mc);
}
@Override
protected void tearDown() throws Exception {
this.is = null;
this.mc = null;
this.mq = null;
}
protected CharProducer fromString(String... content) {
return fromString(Join.join("\n", content), is);
}
protected CharProducer fromString(String content, InputSource is) {
this.mc.addInputSource(is);
return CharProducer.Factory.create(new StringReader(content), is);
}
protected CharProducer fromString(String content, FilePosition pos) {
this.mc.addInputSource(is);
return CharProducer.Factory.create(new StringReader(content), pos);
}
protected CharProducer fromResource(String resourcePath)
throws IOException {
URI resource = TestUtil.getResource(getClass(), resourcePath);
if (resource == null) { throw new FileNotFoundException(resourcePath); }
return fromResource(resourcePath, new InputSource(resource));
}
protected CharProducer fromResource(String resourcePath, InputSource is)
throws IOException {
return dataFromResource(resourcePath, is).getTextualContent();
}
protected FetchedData dataFromResource(String resourcePath, InputSource is)
throws IOException {
ContentType guess = GuessContentType.guess(null, resourcePath, null);
mc.addInputSource(is);
return FetchedData.fromStream(
TestUtil.getResourceAsStream(getClass(), resourcePath),
guess != null ? guess.mimeType : "", "UTF-8", is);
}
protected static String plain(CharProducer cp) {
return cp.toString(cp.getOffset(), cp.getLimit());
}
protected Block js(CharProducer cp) throws ParseException {
return js(cp, false);
}
protected Expression jsExpr(CharProducer cp) throws ParseException {
return jsExpr(cp, false);
}
protected Block js(CharProducer cp, boolean quasi) throws ParseException {
return js(cp, JsTokenQueue.NO_COMMENT, quasi);
}
protected Block js(
CharProducer cp, Criterion<Token<JsTokenType>> filt, boolean quasi)
throws ParseException {
JsLexer lexer = new JsLexer(cp);
JsTokenQueue tq = new JsTokenQueue(lexer, sourceOf(cp), filt);
Parser p = new Parser(tq, mq, quasi);
Block b = p.parse();
tq.expectEmpty();
return b;
}
protected Expression jsExpr(CharProducer cp, boolean quasi)
throws ParseException {
JsLexer lexer = new JsLexer(cp);
JsTokenQueue tq = new JsTokenQueue(
lexer, sourceOf(cp), JsTokenQueue.NO_COMMENT);
Parser p = new Parser(tq, mq, quasi);
Expression e = p.parseExpression(true);
tq.expectEmpty();
return e;
}
protected Block quasi(CharProducer cp) throws ParseException {
return js(cp, true);
}
// TODO(kpreid): This appears to be the only remaining user of our HTML
// parsing infrastructure, and it is used for JsHtmlSanitizerTest. Replace
// it with a less customized HTML parser.
protected Element xml(CharProducer cp) throws ParseException {
return (Element) parseMarkup(cp, true, true);
}
protected DocumentFragment xmlFragment(CharProducer cp)
throws ParseException {
return (DocumentFragment) parseMarkup(cp, true, false);
}
protected DocumentFragment html(CharProducer cp) throws ParseException {
Node root = parseMarkup(cp, false, true);
DocumentFragment fragment = root.getOwnerDocument()
.createDocumentFragment();
fragment.appendChild(root);
Nodes.setFilePositionFor(fragment, Nodes.getFilePositionFor(root));
return fragment;
}
protected DocumentFragment htmlFragment(CharProducer cp)
throws ParseException {
return (DocumentFragment) parseMarkup(cp, false, false);
}
private Node parseMarkup(CharProducer cp, boolean asXml, boolean asDoc)
throws ParseException {
InputSource is = sourceOf(cp);
HtmlLexer lexer = new HtmlLexer(cp);
lexer.setTreatedAsXml(asXml);
TokenQueue<HtmlTokenType> tq = new TokenQueue<HtmlTokenType>(
lexer, is, DomParser.SKIP_COMMENTS);
DomParser p = new DomParser(tq, asXml, mq);
Node t = asDoc ? p.parseDocument() : p.parseFragment();
tq.expectEmpty();
return t;
}
protected Element markup(CharProducer cp) throws ParseException {
return new DomParser(new HtmlLexer(cp), false, sourceOf(cp), mq)
.parseDocument();
}
protected DocumentFragment markupFragment(CharProducer cp)
throws ParseException {
return new DomParser(new HtmlLexer(cp), false, sourceOf(cp), mq)
.parseFragment();
}
public static String render(MessageQueue mq) {
StringBuilder sb = new StringBuilder();
for (Message m : mq.getMessages()) {
try {
m.format(new MessageContext(), sb);
} catch (IOException e) {
sb.append(e.toString());
}
sb.append("\n");
}
return sb.toString();
}
public static String render(ParseTreeNode node) {
if (node == null) {
return null;
}
StringBuilder sb = new StringBuilder();
TokenConsumer tc = node.makeRenderer(sb, null);
node.render(new RenderContext(tc));
tc.noMoreTokens();
return sb.toString();
}
/**
* Returns a source code string for the given program without surrounding
* curly braces.
*/
public static String renderProgram(Block program) {
StringBuilder sb = new StringBuilder();
TokenConsumer tc = program.makeRenderer(sb, null);
program.renderBody(new RenderContext(tc));
tc.noMoreTokens();
return sb.toString();
}
protected static String formatShort(FilePosition p) {
StringBuilder sb = new StringBuilder();
try {
p.formatShort(sb);
} catch (IOException ex) {
throw new SomethingWidgyHappenedError(
"IOException from StringBuilder", ex);
}
return sb.toString();
}
protected static String minify(ParseTreeNode node) {
if (node == null) {
return null;
}
// Make sure it's a JS node.
StringBuilder sb = new StringBuilder();
if (!(node.makeRenderer(sb, null) instanceof JsPrettyPrinter)) {
throw new ClassCastException(node.getClass().getName());
}
TokenConsumer tc = new JsMinimalPrinter(sb);
node.render(new RenderContext(tc));
tc.noMoreTokens();
return sb.toString();
}
/**
* Ensures that a given node is cloneable by calling {@code clone()} on it and
* checking sanity of the result. Tests for specific {@code ParseTreeNode}
* subsystems should invoke this on a substantial set of example trees to
* guard against problems creeping into the {@code clone()} implementations.
*
* @param node a {@code ParseTreeNode}.
*/
protected void assertCloneable(ParseTreeNode node) {
assertDeepEquals(node, node.clone());
}
/**
* Ensures that two {@code ParseTreeNode} trees are deeply equal in the
* topology and types of nodes in each tree, and in the {@code getValue()} and
* {@code getFilePosition()} of each respective node.
*
* @param a a {@code ParseTreeNode}.
* @param b a {@code ParseTreeNode}.
*/
protected void assertDeepEquals(ParseTreeNode a, ParseTreeNode b) {
assertEquals(a.getValue(), b.getValue());
assertEquals(a.getFilePosition(), b.getFilePosition());
assertEquals(a.children().size(), b.children().size());
for (int i = 0; i < a.children().size(); ++i) {
assertDeepEquals(a.children().get(i), b.children().get(i));
}
}
/**
* Ensures that two {@code Arrays} are of the same length and corresponding
* objects are equal
*
* @param a a {@code Array of objects}.
* @param b a {@code Array of objects}.
*/
protected static void assertArrayEquals(Object[] a, Object[] b) {
assertEquals(a.length, b.length);
for (int i=0; i < a.length; i++) {
// So that eclipse diff works more friendly
if (a[i] instanceof String && b[i] instanceof String) {
assertEquals((String)a[i], (String)b[i]);
} else {
assertEquals(a[i], b[i]);
}
}
}
protected static void assertContains(String haystack, String needle) {
assertTrue("Expected result to contain <" + needle + ">",
haystack != null && haystack.contains(needle));
}
protected static void assertNotContains(String haystack, String needle) {
assertTrue("Expected result to not contain <" + needle + ">",
haystack != null && !haystack.contains(needle));
}
protected void assertMessagesLessSevereThan(MessageLevel level) {
for (Message msg : mq.getMessages()) {
if (level.compareTo(msg.getMessageLevel()) <= 0) {
fail(msg.format(mc));
}
}
}
protected void assertNoErrors() {
assertMessagesLessSevereThan(MessageLevel.ERROR);
}
protected void assertNoWarnings() {
assertMessagesLessSevereThan(MessageLevel.WARNING);
}
protected void assertMessage(
MessageTypeInt type, MessageLevel level, MessagePart... expectedParts) {
assertMessage(false, type, level, expectedParts);
}
protected void assertMessage(
boolean consume, final MessageTypeInt type, final MessageLevel level,
final MessagePart... expectedParts) {
assertMessage(
consume,
new Function<Message, Integer>() {
public Integer apply(Message msg) {
int score = 0;
if (msg.getMessageType() == type) { ++score; }
if (msg.getMessageLevel() == level) { ++score; }
score -= partsMissing(msg, expectedParts);
return score == 2 ? Integer.MAX_VALUE : score;
}
},
"type " + type + " and level " + level);
}
protected void assertMessage(
boolean consume, Function<Message, Integer> scorer, String description) {
Message closest = null;
int closestScore = Integer.MIN_VALUE;
for (Message msg : mq.getMessages()) {
final int score = scorer.apply(msg);
if (score == Integer.MAX_VALUE) {
if (consume) {
mq.getMessages().remove(msg);
}
return;
}
if (score > closestScore) {
closest = msg;
closestScore = score;
}
}
if (closest == null) {
fail("No message found like " + description);
} else {
fail("Failed to find message. Closest match was " + closest.format(mc)
+ " with parts " + closest.getMessageParts());
}
}
protected void assertNoMessage(MessageTypeInt type) {
for (Message msg : mq.getMessages()) {
if (msg.getMessageType() == type) { fail(msg.format(mc)); }
}
}
protected static void assertSerializable(Object o) throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(out);
oos.writeObject(o);
oos.close();
assertTrue(out.toByteArray().length > 0);
}
private static int partsMissing(Message msg, MessagePart... parts) {
int missing = 0;
outerLoop:
for (MessagePart expectedPart : parts) {
for (MessagePart candidate : msg.getMessageParts()) {
if (candidate.equals(expectedPart)) { continue outerLoop; }
if (candidate instanceof FilePosition
&& expectedPart instanceof FilePosition) {
FilePosition a = (FilePosition) candidate;
FilePosition b = (FilePosition) expectedPart;
// Ignore startCharInFile for purposes of testing to make tests more
// robust against changes.
if (a.source().equals(b.source())
&& a.startLineNo() == b.startLineNo()
&& a.startCharInLine() == b.startCharInLine()
&& a.endLineNo() == b.endLineNo()
&& a.endCharInLine() == b.endCharInLine()) {
continue outerLoop;
}
}
}
++missing;
}
return missing;
}
private static InputSource sourceOf(CharProducer cp) {
return cp.getSourceBreaks(0).source();
}
protected boolean isKnownFailure() {
return TestSummary.isFailureAnOption(getClass(), getName(),
new Function<String, Void>() {
@Override
public Void apply(String msg) {
System.err.println(msg);
return null;
}
});
}
@Override
protected void runTest() throws Throwable {
if (!isMethodInFilter()) {
System.err.println("Skipping " + getName());
return;
}
// In Eclipse, to suppress known test failures,
// (1) right click on the test in the package explorer and choose properties
// (2) Choose the Run/Debug Settings tab
// (3) Choose your favorite launch configuration and click edit.
// If there is none, make one by running the test.
// (4) In the "Environment" tab, add a property
// test.suppressKnownFailures=true
if ("true".equals("test.suppressKnownFailures") &&
isKnownFailure()) {
try {
super.runTest();
} catch (Throwable th) {
System.err.println("Suppressing known failure of " + getName());
th.printStackTrace();
}
return;
}
super.runTest();
}
protected boolean isMethodInFilter() {
return isMethodInFilter(getName());
}
public static boolean isMethodInFilter(String name) {
// Support running only selected test methods via Java system properties.
// This can be used in conjunction with TestFlag.FILTER.
String filterGlob = TestFlag.FILTER_METHOD.getString(null);
if (filterGlob != null) {
Pattern methodFilter = Pattern.compile(
globToPattern(filterGlob), Pattern.DOTALL);
return methodFilter.matcher(name).matches();
} else {
return true;
}
}
private static String globToPattern(String glob) {
StringBuilder sb = new StringBuilder();
int pos = 0;
for (int i = 0, n = glob.length(); i < n; ++i) {
char ch = glob.charAt(i);
if (ch == '*') {
sb.append(Pattern.quote(glob.substring(pos, i)));
pos = i + 1;
// ** matches across package boundaries
if (pos < n && '*' == glob.charAt(pos)) {
++pos;
sb.append(".*");
} else {
sb.append("[^.]*");
}
} else if (ch == '?') {
sb.append(Pattern.quote(glob.substring(pos, i))).append('.');
pos = i + 1;
}
}
sb.append(Pattern.quote(glob.substring(pos)));
return sb.toString();
}
}