org.xhtmlrenderer.test.ReferenceComparison Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of flying-saucer-core Show documentation
Show all versions of flying-saucer-core Show documentation
Flying Saucer is a CSS 2.1 renderer written in Java. This artifact contains the core rendering and layout code as well as Java2D output.
package org.xhtmlrenderer.test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xhtmlrenderer.layout.LayoutContext;
import org.xhtmlrenderer.render.Box;
import org.xhtmlrenderer.swing.BoxRenderer;
import org.xhtmlrenderer.util.IOUtil;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.nio.file.Files.newInputStream;
import static java.nio.file.Files.newOutputStream;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
/**
* ReferenceComparison runs a comparison of rendering a set of source XHTML files against a
*/
public class ReferenceComparison {
private static final Logger log = LoggerFactory.getLogger(ReferenceComparison.class);
private static final List EXTENSIONS = asList("htm", "html", "xht", "xhtml", "xml");
private static final String RENDER_SFX = ".render.txt";
private static final String LAYOUT_SFX = ".layout.txt";
private static final String PNG_SFX = ".png";
private final int width;
private final boolean isVerbose;
private static final String LINE_SEPARATOR = "\n";
public static void main(String[] args) throws IOException {
// TODO: check args
ReferenceComparison rc = new ReferenceComparison(1024, false);
File source = new File(args[0]);
File reference = new File(args[1]);
File failed = new File(args[2]);
rc.compareDirectory(source, reference, failed);
}
/**
* Initializes (does not launch) the reference comparison.
*
* @param width width at which pages should be rendered
*/
public ReferenceComparison(int width, boolean verbose) {
this.width = width;
this.isVerbose = verbose;
}
public void compareDirectory(File sourceDirectory, File referenceDir, File failedDirectory) throws IOException {
checkDirectories(sourceDirectory, referenceDir, failedDirectory);
log("Starting comparison using width " + width);
IOUtil.deleteAllFiles(failedDirectory);
boolean wasEnabled = enableLogging(false);
try {
CompareStatistics stats = new CompareStatistics();
for (File file : listSourceFiles(sourceDirectory)) {
try {
compareFile(file, referenceDir, failedDirectory, stats);
} catch (IOException e) {
stats.failedIOException(e);
}
}
stats.report();
} finally {
enableLogging(wasEnabled);
}
}
private boolean enableLogging(final boolean isEnabled) {
final String prop = "xr.util-logging.loggingEnabled";
final boolean orgVal = Boolean.parseBoolean(System.getProperty(prop));
System.setProperty(prop, Boolean.valueOf(isEnabled).toString());
return orgVal;
}
private void checkDirectories(File sourceDirectory, File referenceDir, File failedDirectory) {
if (!sourceDirectory.exists() || !sourceDirectory.isDirectory()) {
throw new IllegalArgumentException("Source dir. doesn't exist, or not a directory: " + sourceDirectory);
}
if (!referenceDir.exists() || !referenceDir.isDirectory()) {
throw new IllegalArgumentException("Reference dir. doesn't exist, or not a directory: " + referenceDir);
}
if (failedDirectory.exists() && !failedDirectory.isDirectory()) {
throw new IllegalArgumentException("Need directory for failed matches, not a directory: " + failedDirectory);
} else if (!failedDirectory.exists()) {
if (!failedDirectory.mkdirs()) {
throw new RuntimeException(
"Could not create directory path (.mkdirs failed without an exception) " +
failedDirectory.getAbsolutePath()
);
}
}
}
private boolean verbose() {
return isVerbose;
}
private Iterable listSourceFiles(File sourceDirectory) {
File[] files = sourceDirectory.listFiles((dir, name) -> EXTENSIONS.contains(name.substring(name.lastIndexOf('.') + 1)));
return files == null ? emptyList() : asList(files);
}
void compareFile(File source, File referenceDir, File failedDirectory, CompareStatistics stat) throws IOException {
log("Comparing " + source.getPath());
stat.checking(source);
BoxRenderer renderer = new BoxRenderer(source, width);
Box box;
try {
log("rendering");
box = renderer.render();
log("rendered");
} catch (Exception e) {
log.error("Failed to render file {}", source, e);
stat.failedToRender(e);
storeFailed(failedDirectory, source);
log("Could not render input file, skipping: " + source + " err: " + e.getMessage());
return;
}
LayoutContext layoutContext = renderer.getLayoutContext();
String inputFileName = source.getName();
String refRendered = trimTrailingLS(readReference(referenceDir, inputFileName, RENDER_SFX));
String rendered = trimTrailingLS(box.dump(layoutContext, "", Box.DUMP_RENDER));
if (!compareLines(refRendered, rendered, stat)) {
storeFailed(failedDirectory, new File(referenceDir, inputFileName), RENDER_SFX, rendered);
}
final String refLaidOut = trimTrailingLS(readReference(referenceDir, inputFileName, LAYOUT_SFX));
final String laidOut = trimTrailingLS(box.dump(layoutContext, "", Box.DUMP_LAYOUT));
if (!compareLines(refLaidOut, laidOut, stat)) {
storeFailed(failedDirectory, new File(referenceDir, inputFileName), LAYOUT_SFX, laidOut);
}
}
private String trimTrailingLS(String s) {
if (s.endsWith(LINE_SEPARATOR)) {
s = s.substring(0, s.length() - LINE_SEPARATOR.length());
}
return s;
}
private void storeFailed(File failedDirectory, File refFile, String suffix, String compareTo) {
copyToFailed(failedDirectory, refFile, "");
copyToFailed(failedDirectory, refFile, PNG_SFX);
copyToFailed(failedDirectory, refFile, suffix);
try (OutputStreamWriter fw = new OutputStreamWriter(newOutputStream(new File(failedDirectory, refFile.getName() + ".err" + suffix).toPath()), UTF_8)) {
try (BufferedWriter bw = new BufferedWriter(fw)) {
bw.write(compareTo);
bw.flush();
} catch (IOException e) {
throw new RuntimeException("unexpected IO exception on writing 'failed' info for test.", e);
}
// swallow
} catch (IOException e) {
throw new RuntimeException("unexpected IO exception on writing 'failed' info for test.", e);
}
// swallow
}
private void copyToFailed(File failedDirectory, File refFile, String suffix) {
File source = new File(failedDirectory, refFile.getName() + suffix);
if (!source.exists()) {
source = new File(refFile.getAbsoluteFile().getParentFile(), refFile.getName() + suffix);
try {
IOUtil.copyFile(source, failedDirectory);
} catch (IOException e) {
System.err.println("Failed to copy file (reference) " + source + " to failed directory, err " + e.getMessage());
}
}
}
private boolean compareLines(String refText, String text, CompareStatistics statistics) throws IOException {
log("running comparison");
try (LineNumberReader lnrRef = new LineNumberReader(new StringReader(refText))) {
try (LineNumberReader lnrOther = new LineNumberReader(new StringReader(text))) {
String lineRef;
String lineOther;
while ((lineRef = lnrRef.readLine()) != null) {
lineOther = lnrOther.readLine();
if (lineOther == null) {
statistics.failedRefIsLonger();
return false;
}
if (!lineRef.equals(lineOther)) {
statistics.failedDontMatch(lineRef, lineOther);
return false;
}
}
if (lnrOther.readLine() != null) {
statistics.failedOtherIsLonger();
return false;
}
}
}
return true;
}
private void storeFailed(File failedDirectory, File sourceFile) {
try {
IOUtil.copyFile(sourceFile, failedDirectory);
} catch (IOException e) {
System.err.println("Failed to copy file to failed directory: " + sourceFile + ", err: " + e.getMessage());
}
}
private String readReference(File referenceDir, String input, String sfx) throws IOException {
File f = new File(referenceDir, input + sfx);
try (BufferedReader rdr = new BufferedReader(new InputStreamReader(newInputStream(f.toPath()), UTF_8))) {
String line;
StringBuilder sb = new StringBuilder();
while ((line = rdr.readLine()) != null) {
sb.append(line);
sb.append(LINE_SEPARATOR);
}
return sb.toString();
}
}
private void log(final String msg) {
if (verbose()) {
System.out.println(msg);
}
}
private static class CompareStatistics {
private File currentFile;
private static final Result OK = new ResultOK();
private final Map files = new HashMap<>();
private void failedToRender(Exception e) {
files.put(currentFile, new RenderFailed(e));
}
private void failedRefIsLonger() {
files.put(currentFile, new RefIsLonger());
}
private void failedDontMatch(String lineRef, String lineOther) {
files.put(currentFile, new LineMismatch(lineRef, lineOther));
}
private void failedOtherIsLonger() {
files.put(currentFile, new OtherIsLonger());
}
private void failedIOException(IOException e) {
files.put(currentFile, new FailedIO(e));
}
private void checking(File source) {
currentFile = source;
files.put(currentFile, OK);
}
private void report() {
int failed = 0;
for (Map.Entry entry : files.entrySet()) {
Result result = entry.getValue();
if (result instanceof FailedResult) {
failed++;
System.out.println(result.describe(entry.getKey()));
}
}
System.out.println("Checked " + files.keySet().size() + " files, " + (failed > 0 ? failed + " failed." : "all OK."));
}
private record RenderFailed(Exception exception) implements Result {
public String describe(File file) {
return "FAIL: Render operation threw exception for %s, err %s".formatted(file.getName(), exception.getMessage());
}
}
private static class RefIsLonger implements FailedResult {
public String describe(File file) {
return "FAIL: reference is longer (more lines): %s".formatted(file.getName());
}
}
private record LineMismatch(String lineRef, String lineOther) implements FailedResult {
public String describe(File file) {
return "FAIL: line content doesn't match for %s%sref: %s%sother: %s".formatted(
file.getName(), LINE_SEPARATOR, lineRef, LINE_SEPARATOR, lineOther);
}
}
private static class OtherIsLonger implements FailedResult {
public String describe(File file) {
return "FAIL: new rendered output is longer (more lines): %s".formatted(file.getName());
}
}
private static class FailedIO implements FailedResult {
private final IOException exception;
public FailedIO(IOException e) {
this.exception = e;
}
public String describe(File file) {
return "FAIL: IOException when comparing: %s (err: %s".formatted(file, exception.getMessage());
}
}
private interface Result {
String describe(File file);
}
private interface FailedResult extends Result {
}
private static class ResultOK implements Result {
public String describe(File file) {
return "OK: %s".formatted(file.getName());
}
}
}
}