This is an automated email from the ASF dual-hosted git repository.

bodewig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ant.git

commit 623d5663093d918451f1c02701a16306ba996283
Author: Stefan Bodewig <bode...@apache.org>
AuthorDate: Sat Jan 16 18:22:53 2021 +0100

    encode characters illegal in XML in junitlauncher's report
    
    also avoid double-encoding of sysout/err
    
    Bugzilla Reports 63436 and 65030
---
 WHATSNEW                                           |  8 ++
 .../junitlauncher/LegacyXmlResultFormatter.java    | 60 +++++++++------
 .../apache/tools/ant/util/DOMElementWriter.java    | 14 ++++
 .../LegacyXmlResultFormatterTest.java              | 86 ++++++++++++++++++++++
 4 files changed, 145 insertions(+), 23 deletions(-)

diff --git a/WHATSNEW b/WHATSNEW
index bc08e16..bf2406a 100644
--- a/WHATSNEW
+++ b/WHATSNEW
@@ -11,6 +11,14 @@ Fixed bugs:
    the # character. This has now been fixed.
    Bugzilla Reports 64912, 64790
 
+ * made sure LegacyXmlResultFormatter encodes characters illegal in
+   XML the same way JUnit5's built-in formatter would.
+   Bugzilla Report 65030
+
+ * LegacyXmlResultFormatter no longer double-encodes <>& in system-err
+   and system-out
+   Bugzilla Report 63436
+
 Other changes:
 --------------
 
diff --git 
a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
 
b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
index c0d4bee..35df278 100644
--- 
a/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
+++ 
b/src/main/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatter.java
@@ -199,16 +199,16 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
             // write the testsuite element
             writer.writeStartElement(ELEM_TESTSUITE);
             final String testsuiteName = determineTestSuiteName();
-            writer.writeAttribute(ATTR_NAME, testsuiteName);
+            writeAttribute(writer, ATTR_NAME, testsuiteName);
             // time taken for the tests execution
-            writer.writeAttribute(ATTR_TIME, String.valueOf((testPlanEndedAt - 
testPlanStartedAt) / ONE_SECOND));
+            writeAttribute(writer, ATTR_TIME, String.valueOf((testPlanEndedAt 
- testPlanStartedAt) / ONE_SECOND));
             // add the timestamp of report generation
             final String timestamp = DateUtils.format(new Date(), 
DateUtils.ISO8601_DATETIME_PATTERN);
-            writer.writeAttribute(ATTR_TIMESTAMP, timestamp);
-            writer.writeAttribute(ATTR_NUM_TESTS, 
String.valueOf(numTestsRun.longValue()));
-            writer.writeAttribute(ATTR_NUM_FAILURES, 
String.valueOf(numTestsFailed.longValue()));
-            writer.writeAttribute(ATTR_NUM_SKIPPED, 
String.valueOf(numTestsSkipped.longValue()));
-            writer.writeAttribute(ATTR_NUM_ABORTED, 
String.valueOf(numTestsAborted.longValue()));
+            writeAttribute(writer, ATTR_TIMESTAMP, timestamp);
+            writeAttribute(writer, ATTR_NUM_TESTS, 
String.valueOf(numTestsRun.longValue()));
+            writeAttribute(writer, ATTR_NUM_FAILURES, 
String.valueOf(numTestsFailed.longValue()));
+            writeAttribute(writer, ATTR_NUM_SKIPPED, 
String.valueOf(numTestsSkipped.longValue()));
+            writeAttribute(writer, ATTR_NUM_ABORTED, 
String.valueOf(numTestsAborted.longValue()));
 
             // write the properties
             writeProperties(writer);
@@ -228,8 +228,8 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
             writer.writeStartElement(ELEM_PROPERTIES);
             for (final String prop : properties.stringPropertyNames()) {
                 writer.writeStartElement(ELEM_PROPERTY);
-                writer.writeAttribute(ATTR_NAME, prop);
-                writer.writeAttribute(ATTR_VALUE, 
properties.getProperty(prop));
+                writeAttribute(writer, ATTR_NAME, prop);
+                writeAttribute(writer, ATTR_VALUE, 
properties.getProperty(prop));
                 writer.writeEndElement();
             }
             writer.writeEndElement();
@@ -257,11 +257,11 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
                 }
                 final String classname = 
(parentClassSource.get()).getClassName();
                 writer.writeStartElement(ELEM_TESTCASE);
-                writer.writeAttribute(ATTR_CLASSNAME, classname);
-                writer.writeAttribute(ATTR_NAME, useLegacyReportingName ? 
testId.getLegacyReportingName()
+                writeAttribute(writer, ATTR_CLASSNAME, classname);
+                writeAttribute(writer, ATTR_NAME, useLegacyReportingName ? 
testId.getLegacyReportingName()
                         : testId.getDisplayName());
                 final Stats stats = entry.getValue();
-                writer.writeAttribute(ATTR_TIME, String.valueOf((stats.endedAt 
- stats.startedAt) / ONE_SECOND));
+                writeAttribute(writer, ATTR_TIME, 
String.valueOf((stats.endedAt - stats.startedAt) / ONE_SECOND));
                 // skipped element if the test was skipped
                 writeSkipped(writer, testId);
                 // failed element if the test failed
@@ -280,7 +280,7 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
             writer.writeStartElement(ELEM_SKIPPED);
             final Optional<String> reason = skipped.get(testIdentifier);
             if (reason.isPresent()) {
-                writer.writeAttribute(ATTR_MESSAGE, reason.get());
+                writeAttribute(writer, ATTR_MESSAGE, reason.get());
             }
             writer.writeEndElement();
         }
@@ -295,9 +295,9 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
                 final Throwable t = cause.get();
                 final String message = t.getMessage();
                 if (message != null && !message.trim().isEmpty()) {
-                    writer.writeAttribute(ATTR_MESSAGE, message);
+                    writeAttribute(writer, ATTR_MESSAGE, message);
                 }
-                writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+                writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
                 // write out the stacktrace
                 writer.writeCData(StringUtils.getStackTrace(t));
             }
@@ -314,9 +314,9 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
                 final Throwable t = cause.get();
                 final String message = t.getMessage();
                 if (message != null && !message.trim().isEmpty()) {
-                    writer.writeAttribute(ATTR_MESSAGE, message);
+                    writeAttribute(writer, ATTR_MESSAGE, message);
                 }
-                writer.writeAttribute(ATTR_TYPE, t.getClass().getName());
+                writeAttribute(writer, ATTR_TYPE, t.getClass().getName());
                 // write out the stacktrace
                 writer.writeCData(StringUtils.getStackTrace(t));
             }
@@ -349,15 +349,29 @@ class LegacyXmlResultFormatter extends 
AbstractJUnitResultFormatter implements T
             final char[] chars = new char[1024];
             int numRead = -1;
             while ((numRead = reader.read(chars)) != -1) {
-                // although it's called a DOMElementWriter, the encode method 
is just a
-                // straight forward XML util method which doesn't concern 
about whether
-                // DOM, SAX, StAX semantics.
-                // TODO: Perhaps make it a static method
-                final String encoded = new DOMElementWriter().encode(new 
String(chars, 0, numRead));
-                writer.writeCharacters(encoded);
+                writer.writeCharacters(encode(new String(chars, 0, numRead)));
             }
         }
 
+        private void writeAttribute(final XMLStreamWriter writer, final String 
name, final String value)
+            throws XMLStreamException {
+            writer.writeAttribute(name, encode(value));
+        }
+
+        private String encode(final String s) {
+            boolean changed = false;
+            final StringBuilder sb = new StringBuilder();
+            for (char c : s.toCharArray()) {
+                if (!DOMElementWriter.isLegalXmlCharacter(c)) {
+                    changed = true;
+                    sb.append("&#").append((int) c).append(';');
+                } else {
+                    sb.append(c);
+                }
+            }
+            return changed ? sb.toString() : s;
+        }
+
         private String determineTestSuiteName() {
             // this is really a hack to try and match the expectations of the 
XML report in JUnit4.x
             // world. In JUnit5, the TestPlan doesn't have a name and a 
TestPlan (for which this is a
diff --git a/src/main/org/apache/tools/ant/util/DOMElementWriter.java 
b/src/main/org/apache/tools/ant/util/DOMElementWriter.java
index 4aafeb6..fe2c11a 100644
--- a/src/main/org/apache/tools/ant/util/DOMElementWriter.java
+++ b/src/main/org/apache/tools/ant/util/DOMElementWriter.java
@@ -588,6 +588,20 @@ public class DOMElementWriter {
      * @since 1.10, Ant 1.5
      */
     public boolean isLegalCharacter(final char c) {
+        return isLegalXmlCharacter(c);
+    }
+
+    /**
+     * Is the given character allowed inside an XML document?
+     *
+     * <p>See XML 1.0 2.2 <a
+     * href="https://www.w3.org/TR/1998/REC-xml-19980210#charsets";>
+     * https://www.w3.org/TR/1998/REC-xml-19980210#charsets</a>.</p>
+     * @param c the character to test.
+     * @return true if the character is allowed.
+     * @since 1.10.10
+     */
+    public static boolean isLegalXmlCharacter(final char c) {
         // CheckStyle:MagicNumber OFF
         if (c == 0x9 || c == 0xA || c == 0xD) {
             return true;
diff --git 
a/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatterTest.java
 
b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatterTest.java
new file mode 100644
index 0000000..cd0f7f9
--- /dev/null
+++ 
b/src/tests/junit/org/apache/tools/ant/taskdefs/optional/junitlauncher/LegacyXmlResultFormatterTest.java
@@ -0,0 +1,86 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You 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
+ *
+ *      https://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 org.apache.tools.ant.taskdefs.optional.junitlauncher;
+
+import org.apache.tools.ant.Project;
+import org.junit.Test;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Optional;
+import java.util.Properties;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.Assert.assertThat;
+
+public class LegacyXmlResultFormatterTest {
+
+    private static final String KEY = "key";
+    private static final String ORIG = "<\u0000&>foo";
+    private static final String ENCODED = "&lt;&amp;#0;&amp;&gt;foo";
+
+    private final LegacyXmlResultFormatter f = new LegacyXmlResultFormatter();
+
+    @Test
+    public void encodesAttributesProperly() throws Exception {
+        final TestPlan plan = startTest(true);
+        final String result = finishTest(plan);
+        assertThat(result, containsString("=\"" + ENCODED + "\""));
+    }
+
+    @Test
+    public void encodesSysOutProperly() throws Exception {
+        final TestPlan plan = startTest(false);
+        f.sysOutAvailable(ORIG.getBytes(StandardCharsets.UTF_8));
+        final String result = finishTest(plan);
+        assertThat(result, containsString(ENCODED));
+    }
+
+    private TestPlan startTest(final boolean withProperties) {
+        f.setContext(new TestExecutionContext() {
+            @Override
+            public Properties getProperties() {
+                final Properties p = new Properties();
+                if (withProperties) {
+                    p.setProperty(KEY, ORIG);
+                }
+                return p;
+            }
+
+            @Override
+            public Optional<Project> getProject() {
+                return Optional.empty();
+            }
+        });
+        final TestPlan testPlan = TestPlan.from(Collections.emptySet());
+        f.testPlanExecutionStarted(testPlan);
+        return testPlan;
+    }
+
+    private String finishTest(final TestPlan testPlan) throws IOException {
+        try (final ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
+            f.setDestination(bos);
+            f.testPlanExecutionFinished(testPlan);
+            return new String(bos.toByteArray(), StandardCharsets.UTF_8);
+        }
+    }
+}

Reply via email to