Skip to content

Commit

Permalink
Fixes GH-123: XML report must always produce a valid XML! Will reopen…
Browse files Browse the repository at this point in the history
… if the issue happens again. For now, guard against invalid strings.
  • Loading branch information
dweiss committed Sep 19, 2012
1 parent fa0b72a commit 8e02845
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 6 deletions.
@@ -1,6 +1,10 @@
package com.carrotsearch.ant.tasks.junit4.listeners.antxml;

import java.io.*;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
Expand All @@ -11,6 +15,7 @@
import org.apache.tools.ant.filters.TokenFilter;
import org.junit.runner.Description;
import org.simpleframework.xml.core.Persister;
import org.simpleframework.xml.transform.RegistryMatcher;

import com.carrotsearch.ant.tasks.junit4.JUnit4;
import com.carrotsearch.ant.tasks.junit4.events.aggregated.AggregatedSuiteResultEvent;
Expand Down Expand Up @@ -104,7 +109,9 @@ public void onSuiteResult(AggregatedSuiteResultEvent e) {

try {
File reportFile = new File(dir, "TEST-" + displayName + ".xml");
Persister persister = new Persister();
RegistryMatcher rm = new RegistryMatcher();
rm.bind(String.class, new XmlStringTransformer());
Persister persister = new Persister(rm);
persister.write(buildModel(e), reportFile);
} catch (Exception x) {
junit4.log("Could not serialize report for suite "
Expand Down
@@ -0,0 +1,53 @@
package com.carrotsearch.ant.tasks.junit4.listeners.antxml;

import org.simpleframework.xml.transform.Transform;

final class XmlStringTransformer implements Transform<String> {
private final StringBuilder buffer = new StringBuilder();

@Override
public String read(String value) throws Exception {
return value;
}

@Override
public String write(String value) throws Exception {
if (!isMappableXmlText(value)) {
return remap(value);
}
return value;
}

private String remap(CharSequence value) {
buffer.setLength(0);
final int length = value.length();
for (int i = 0; i < length; i = Character.offsetByCodePoints(value, i, 1)) {
int cp = Character.codePointAt(value, i);
if ((cp >= 0x20 && cp <= 0x00D7FF) ||
(cp < 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D)) ||
(cp >= 0xE000 && cp <= 0x00FFFD) ||
(cp >= 0x10000 && cp <= 0x10FFFF)) {
buffer.appendCodePoint(cp);
} else {
buffer.append(/* Replacement char. */ "\ufffd");
}
}
return buffer.toString();
}

private static boolean isMappableXmlText(CharSequence value) {
final int length = value.length();
for (int i = 0; i < length; i = Character.offsetByCodePoints(value, i, 1)) {
int cp = Character.codePointAt(value, i);
if ((cp >= 0x20 && cp <= 0x00D7FF) ||
(cp < 0x20 && (cp == 0x09 || cp == 0x0A || cp == 0x0D)) ||
(cp >= 0xE000 && cp <= 0x00FFFD) ||
(cp >= 0x10000 && cp <= 0x10FFFF)) {
// Ok, mappable XML character.
} else {
return false;
}
}
return true;
}
}
@@ -0,0 +1,76 @@
package com.carrotsearch.ant.tasks.junit4;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Random;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.Test;
import org.simpleframework.xml.Attribute;
import org.simpleframework.xml.Element;
import org.simpleframework.xml.Root;
import org.simpleframework.xml.core.Persister;
import org.simpleframework.xml.transform.RegistryMatcher;
import org.xml.sax.SAXParseException;

import com.carrotsearch.ant.tasks.junit4.listeners.antxml.XmlStringTransformerAccess;
import com.carrotsearch.randomizedtesting.RandomizedTest;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import com.google.common.base.Charsets;

public class TestXmlStringsRoundtrip extends RandomizedTest {
@Test
@Repeat(iterations = 100)
public void testRoundTrip() throws Exception {
char[] chars = new char[randomIntBetween(0, 1024)];
Random random = getRandom();
for (int i = 0; i < chars.length; i++) {
chars[i] = (char) random.nextInt();
}

check(chars);
}

@Test
public void testBoundary() throws Exception {
check(new char [] {'a', 0x0000, 'z'});
check(new char [] {'a', 0x0001, 'z'});
}

@Root
public static class Model {
@Attribute
public String attribute;

@Element(name = "system-out", data = true, required = true)
public String contents = "";

public Model() {}

public Model(String s) {
attribute = contents = s;
}
}

private void check(char[] chars) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();

RegistryMatcher rm = new RegistryMatcher();
rm.bind(String.class, XmlStringTransformerAccess.getInstance());
Persister persister = new Persister(rm);
persister.write(new Model(new String(chars)), baos);

DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
try {
docBuilder.parse(new ByteArrayInputStream(baos.toByteArray()));
} catch (SAXParseException e) {
System.out.println("Input: " + Arrays.toString(chars));
System.out.println("XML: " + new String(baos.toByteArray(), Charsets.UTF_8));
throw e;
}
}
}
@@ -1,19 +1,30 @@
package com.carrotsearch.ant.tasks.junit4.it;


import java.io.File;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.junit.Assert;
import org.junit.Test;


public class TestAntXmlReport extends JUnit4XmlTestBase {

@Test
public void antxml() {
public void antxml() throws Exception {
super.executeTarget("antxml");

// Simple check for existence.
Assert.assertTrue(
new File(getProject().getBaseDir(), "ant-xmls/TEST-com.carrotsearch.ant.tasks.junit4.tests.TestBeforeClassError.xml").length() > 0);

// Attempt to read and parse.
File basedir = new File(getProject().getBaseDir(), "ant-xmls");
for (File f : basedir.listFiles()) {
if (f.isFile() && f.getName().endsWith(".xml")) {
DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
docBuilder.parse(f);
}
}
}
}
@@ -0,0 +1,7 @@
package com.carrotsearch.ant.tasks.junit4.listeners.antxml;

public class XmlStringTransformerAccess {
public static XmlStringTransformer getInstance() {
return new XmlStringTransformer();
}
}
@@ -0,0 +1,13 @@
package com.carrotsearch.ant.tasks.junit4.tests;

import org.junit.Test;

public class TestInvalidUtfCharacter {
@Test
public void emitInvalidCharacter() throws Exception {
String msg = "Invalid char: >\u0002< >\u0000<";
System.out.println(msg);
System.out.flush();
throw new Exception(msg);
}
}
1 change: 1 addition & 0 deletions junit4-ant/src/test/resources/junit4.xml
Expand Up @@ -266,6 +266,7 @@
<include name="**/tests/TestIgnoredSuite.class" />
<include name="**/tests/TestBeforeClassError.class" />
<include name="**/tests/TestHierarchicalSuiteDescription.class" />
<include name="**/tests/TestInvalidUtfCharacter.class" />
</fileset>
</junit4:junit4>
</target>
Expand Down

0 comments on commit 8e02845

Please sign in to comment.