/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.rendering.test.cts;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.junit.ComparisonFailure;
import org.junit.Test;
import org.xwiki.component.manager.ComponentManager;
import org.xwiki.rendering.parser.ParseException;
import org.xwiki.rendering.parser.StreamParser;
import org.xwiki.rendering.renderer.PrintRenderer;
import org.xwiki.rendering.renderer.PrintRendererFactory;
import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
import org.xwiki.rendering.syntax.SyntaxFactory;
import org.xwiki.velocity.internal.log.SLF4JLogChute;
import org.xwiki.xml.XMLUtils;
/**
* A generic JUnit Test used by {@link CompatibilityTestSuite} to run a single CTS test.
*
* @version $Id: b6c5992fa3d0c83833a1cc399a5e7e910ef3d5e8 $
* @since 4.1M1
*/
public class RenderingTest
{
/**
* The Syntax id corresponding to the syntax in which the CTS tests are written in.
*/
private static final String CTS_SYNTAX_ID = org.xwiki.rendering.syntax.Syntax.XDOMXML_CURRENT.toIdString();
/**
* The Velocity Engine we use to evaluate the test data. We do this to allow Velocity scripts to be added to test
* data.
*/
private static final VelocityEngine VELOCITY_ENGINE = new VelocityEngine();
static {
// Make velocity use SLF4J as logger
VELOCITY_ENGINE.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM, new SLF4JLogChute());
}
/**
* Symbols to start a special syntax block. For example: <code>${{{regex:...}}}</code> or
* <code>${{{velocity:...}}}</code>
*/
private static final String SPECIAL_SYNTAX_START = "${{{";
/**
* Symbols to close a special syntax block. For example: <code>${{{regex:...}}}</code> or
* <code>${{{velocity:...}}}</code>
*/
private static final String SPECIAL_SYNTAX_END = "}}}";
/**
* @see RenderingTest
*/
private TestData testData;
/**
* @see RenderingTest
*/
private ComponentManager componentManager;
/**
* @see RenderingTest
*/
private org.xwiki.rendering.syntax.Syntax metadataSyntax;
/**
* @param testData the data for a single test
* @param metadataSyntaxId the Syntax id of the syntax used as Metadata in the generated XDOM for parsers
* @param componentManager see {@link #getComponentManager()}
*/
public RenderingTest(TestData testData, String metadataSyntaxId, ComponentManager componentManager)
{
this.testData = testData;
this.componentManager = componentManager;
this.metadataSyntax = parseSyntax(metadataSyntaxId);
}
/**
* Executes a single test.
*
* @throws Exception if an error happened during the test
*/
@Test
public void execute() throws Exception
{
if (this.testData.isSyntaxInputTest) {
executeInputTest();
} else {
executeOutputTest();
}
}
/**
* Executes the test as an input test. This means:
* <ul>
* <li>Parse the Syntax input</li>
* <li>Render the generated XDOM using the CTS Renderer</li>
* <li>Compare result with the CTS Output</li>
* </ul>
*
* @throws Exception if an error happens, for example if a Parser or Renderer cannot be found
*/
private void executeInputTest() throws Exception
{
executeTest(this.testData.syntaxData, this.testData.syntaxId, this.testData.ctsData, CTS_SYNTAX_ID);
}
/**
* Executes the test as an output test. This means:
* <ul>
* <li>Parse the CTS input</li>
* <li>Render the generated XDOM using the Syntax Renderer</li>
* <li>Compare result with the Syntax Output</li>
* </ul>
*
* @throws Exception if an error happens, for example if a Parser or Renderer cannot be found
*/
private void executeOutputTest() throws Exception
{
executeTest(this.testData.ctsData, CTS_SYNTAX_ID, this.testData.syntaxData, this.testData.syntaxId);
}
/**
* Executes a test in a generic manner.
*
* @param inputData the input data to parse
* @param inputSyntaxId the syntax in which the input data is written in
* @param expectedOutputData the output data to compare to
* @param outputSyntaxId the syntax in which the output data is written in
* @throws Exception if an error happens, for example if a Parser or Renderer cannot be found
*/
private void executeTest(String inputData, String inputSyntaxId, String expectedOutputData, String outputSyntaxId)
throws Exception
{
String evaluatedInputData = evaluateContent(inputData);
String evaluatedOutputData = evaluateContent(expectedOutputData);
String result = convert(evaluatedInputData, inputSyntaxId, outputSyntaxId);
try {
if (isXMLSyntax(outputSyntaxId)) {
assertExpectedResult(
XMLUtils.formatXMLContent(normalizeXMLContent(evaluatedOutputData, outputSyntaxId)),
XMLUtils.formatXMLContent(result));
} else {
assertExpectedResult(evaluatedOutputData, result);
}
} catch (ParseException e) {
throw new RuntimeException(String.format("Failed to compare expected result with [%s]", result), e);
}
}
/**
* @param syntaxId the syntax to check
* @return true if the passed syntax id represents an XML syntax
*/
private boolean isXMLSyntax(String syntaxId)
{
return syntaxId.startsWith("xdom+xml") || syntaxId.startsWith("docbook");
}
/**
* @param source the source content
* @param sourceSyntaxId the source syntax
* @param targetSyntaxId the target syntax
* @return the target content
* @throws Exception when failing to convert
*/
private String convert(String source, String sourceSyntaxId, String targetSyntaxId) throws Exception
{
PrintRendererFactory rendererFactory =
getComponentManager().getInstance(PrintRendererFactory.class, targetSyntaxId);
PrintRenderer renderer = rendererFactory.createRenderer(new DefaultWikiPrinter());
StreamParser parser = getComponentManager().getInstance(StreamParser.class, sourceSyntaxId);
parser.parse(new StringReader(source), renderer);
return renderer.getPrinter().toString();
}
/**
* Normalize the expected XML output by reading and rendering the passed content. We do this so that we can easily
* compare the expected result with the result of the test and not have to care about license comments, whitespaces,
* newlines, etc.
*
* @param content the XML content to normalize
* @param syntaxId the syntax in which the XML content is written in
* @return the normalized content
* @throws Exception if the XML parser or Renderer cannot be found
*/
private String normalizeXMLContent(String content, String syntaxId) throws Exception
{
return convert(content, syntaxId, syntaxId);
}
/**
* Run Velocity when the <code>${{velocity:...}}}</code> syntax is used. The {@code $syntax} variable is replaced by
* the test Syntax object.
*
* @param content the content to evaluate
* @return the evaluated content
*/
private String evaluateContent(String content)
{
StringBuilder builder = new StringBuilder();
String fullSpecialSyntaxStart = String.format("%svelocity:", SPECIAL_SYNTAX_START);
int pos = content.indexOf(fullSpecialSyntaxStart);
if (pos > -1) {
builder.append(content.substring(0, pos));
// Find end of velocity definition
int pos2 = content.indexOf(SPECIAL_SYNTAX_END, pos + fullSpecialSyntaxStart.length());
if (pos2 == -1) {
throw new RuntimeException("Invalid velocity declaration: missing closing part " + SPECIAL_SYNTAX_END);
}
VelocityContext context = new VelocityContext();
context.put("syntax", this.metadataSyntax);
StringWriter writer = new StringWriter();
VELOCITY_ENGINE.evaluate(context, writer, "Rendering CTS",
content.substring(pos + fullSpecialSyntaxStart.length(), pos2));
builder.append(writer.toString());
builder.append(evaluateContent(content.substring(pos2 + SPECIAL_SYNTAX_END.length())));
} else {
builder.append(content);
}
return builder.toString();
}
/**
* Compare the passed expected string with the passed result. We support regexes for comparison using the format:
* ${{{regex:...}}}. For example:
*
* <pre>
* <code>
* beginDocument
* beginMacroMarkerStandalone [useravatar] [username=XWiki.UserNotExisting]
* beginGroup [[class]=[xwikirenderingerror]]
* onWord [Failed to execute the [useravatar] macro]
* endGroup [[class]=[xwikirenderingerror]]
* beginGroup [[class]=[xwikirenderingerrordescription hidden]]
* onVerbatim [org.xwiki.rendering.macro.MacroExecutionException: User [XWiki.UserNotExisting]${{{regex:.*}}}]
* endGroup [[class]=[xwikirenderingerrordescription hidden]]
* endMacroMarkerStandalone [useravatar] [username=XWiki.UserNotExisting]
* endDocument
* </code>
* </pre>
*
* @param expected the content to compare to
* @param result the result from the test
*/
private void assertExpectedResult(String expected, String result)
{
String escapedExpected = escapeRegexContent(expected);
Pattern pattern = Pattern.compile(escapedExpected, Pattern.DOTALL);
Matcher matcher = pattern.matcher(result);
if (!matcher.matches()) {
throw new ComparisonFailure("", expected, result);
}
}
/**
* Escape the passed content by locating regex syntaxes inside and regex-escaping the text so that the whole content
* can be matched using a Regex Matcher.
*
* @param content the content to escape
* @return the escaped content
*/
private String escapeRegexContent(String content)
{
StringBuilder builder = new StringBuilder();
String fullSpecialSyntaxStart = String.format("%sregex:", SPECIAL_SYNTAX_START);
int pos = content.indexOf(fullSpecialSyntaxStart);
if (pos > -1) {
builder.append(Pattern.quote(content.substring(0, pos)));
// Find end of regex definition
int pos2 = content.indexOf(SPECIAL_SYNTAX_END, pos + fullSpecialSyntaxStart.length());
if (pos2 == -1) {
throw new RuntimeException("Invalid regex declaration: missing closing part " + SPECIAL_SYNTAX_END);
}
builder.append(content.substring(pos + fullSpecialSyntaxStart.length(), pos2));
builder.append(escapeRegexContent(content.substring(pos2 + SPECIAL_SYNTAX_END.length())));
} else {
builder.append(Pattern.quote(content));
}
return builder.toString();
}
/**
* @return the component manager used to find Parser and Renderers
*/
private ComponentManager getComponentManager()
{
return this.componentManager;
}
/**
* Create a Syntax object from a Syntax id string.
*
* @param syntaxId the id of the Syntax to create
* @return the Syntax object
*/
private org.xwiki.rendering.syntax.Syntax parseSyntax(String syntaxId)
{
try {
SyntaxFactory factory = getComponentManager().getInstance(SyntaxFactory.class);
return factory.createSyntaxFromIdString(syntaxId);
} catch (Exception e) {
throw new RuntimeException(String.format("Failed to parse Syntax [%s]", syntaxId), e);
}
}
}