This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new 1057c2c23a [IT] fix invalid characters in surefire report output
(#8092)
1057c2c23a is described below
commit 1057c2c23a706ba34d2b52d69eeacdd08210d340
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Tue Aug 25 15:23:32 2026 +0200
[IT] fix invalid characters in surefire report output (#8092)
---
.../surefirereport/SurefireReportWriter.java | 56 +++++++++++++++++++---
.../surefirereport/SurefireReportWriterTest.java | 43 +++++++++++++++++
2 files changed, 92 insertions(+), 7 deletions(-)
diff --git
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
index dea912edff..44ccb43dc5 100644
---
a/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
+++
b/plugins/transforms/surefirereport/src/main/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriter.java
@@ -26,11 +26,15 @@ import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Locale;
+import java.util.regex.Pattern;
import
org.apache.hop.pipeline.transforms.surefirereport.SurefireTestCase.Status;
/** Writes Maven Surefire 3.0-compatible XML reports. */
public final class SurefireReportWriter {
+ /** CSI escape sequences: what a colourising CLI writes around its output. */
+ private static final Pattern ANSI_ESCAPE = Pattern.compile("\u001b\\[[0-?]*[
-/]*[@-~]");
+
private static final String XSI =
"http://www.w3.org/2001/XMLSchema-instance";
private static final String XSD =
"https://maven.apache.org/surefire/maven-surefire-plugin/xsd/surefire-test-report-3.0.xsd";
@@ -163,7 +167,7 @@ public final class SurefireReportWriter {
static void writeCdata(BufferedWriter out, String text) throws IOException {
// Split CDATA if the text contains the terminator sequence.
- String remaining = text == null ? "" : text;
+ String remaining = sanitize(text);
int idx;
out.write("<![CDATA[");
while ((idx = remaining.indexOf("]]>")) >= 0) {
@@ -176,13 +180,53 @@ public final class SurefireReportWriter {
out.newLine();
}
+ /**
+ * Removes everything XML 1.0 cannot carry, in a CDATA section as much as in
an attribute. Tests
+ * that shell out to a colourising CLI (dbt does) leave ANSI escape
sequences in the captured log;
+ * their ESC (0x1b) makes the whole report unparsable, so Jenkins reports
nothing at all for the
+ * project. Escape sequences go whole rather than only their ESC, so their
printable tail ("[0m")
+ * does not litter the report.
+ */
+ static String sanitize(String text) {
+ if (text == null || text.isEmpty()) {
+ return "";
+ }
+ String stripped = ANSI_ESCAPE.matcher(text).replaceAll("");
+ StringBuilder sb = new StringBuilder(stripped.length());
+ for (int i = 0; i < stripped.length(); i++) {
+ char ch = stripped.charAt(i);
+ if (Character.isHighSurrogate(ch)
+ && i + 1 < stripped.length()
+ && Character.isLowSurrogate(stripped.charAt(i + 1))) {
+ // A valid pair: an emoji in a log is fine, only lone halves are not.
+ sb.append(ch).append(stripped.charAt(i + 1));
+ i++;
+ } else if (isLegalXmlCharacter(ch)) {
+ sb.append(ch);
+ }
+ }
+ return sb.toString();
+ }
+
+ private static boolean isLegalXmlCharacter(char ch) {
+ if (ch == '\t' || ch == '\n' || ch == '\r') {
+ return true;
+ }
+ if (ch < 0x20 || (ch >= 0x7f && ch <= 0x9f)) {
+ return false;
+ }
+ // Unpaired surrogates and the non-characters are not valid XML content
either.
+ return !Character.isSurrogate(ch) && ch != '\ufffe' && ch != '\uffff';
+ }
+
static String escapeAttribute(String value) {
if (value == null) {
return "";
}
- StringBuilder sb = new StringBuilder(value.length() + 16);
- for (int i = 0; i < value.length(); i++) {
- char ch = value.charAt(i);
+ String sanitized = sanitize(value);
+ StringBuilder sb = new StringBuilder(sanitized.length() + 16);
+ for (int i = 0; i < sanitized.length(); i++) {
+ char ch = sanitized.charAt(i);
switch (ch) {
case '&' -> sb.append("&");
case '<' -> sb.append("<");
@@ -190,9 +234,7 @@ public final class SurefireReportWriter {
case '"' -> sb.append(""");
case '\'' -> sb.append("'");
default -> {
- if (ch < 0x20 && ch != '\t' && ch != '\n' && ch != '\r') {
- // skip illegal XML 1.0 control characters
- } else {
+ if (isLegalXmlCharacter(ch)) {
sb.append(ch);
}
}
diff --git
a/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
index f480a1de8b..849dad36b1 100644
---
a/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
+++
b/plugins/transforms/surefirereport/src/test/java/org/apache/hop/pipeline/transforms/surefirereport/SurefireReportWriterTest.java
@@ -26,9 +26,11 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import javax.xml.parsers.DocumentBuilderFactory;
import
org.apache.hop.pipeline.transforms.surefirereport.SurefireTestCase.Status;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
+import org.w3c.dom.Document;
class SurefireReportWriterTest {
@@ -79,6 +81,47 @@ class SurefireReportWriterTest {
assertTrue(xml.contains(" world"));
}
+ @Test
+ void colouredLogStaysParsableXml() throws Exception {
+ Path report = tempDir.resolve("surefile_dbt.xml");
+ // What dbt (and any other colourising CLI) leaves in a captured log.
+ String colouredLog = "\u001b[0m15:52:04 \u001b[32mRunning with
dbt=1.8.0\u001b[0m\nDone.\n";
+ List<SurefireTestCase> cases = new ArrayList<>();
+ cases.add(
+ new SurefireTestCase(
+ "main-0001-dbt",
+ 1.0,
+ Status.FAIL,
+ colouredLog,
+ "\u001b[31merror\u001b[0m",
+ "boom \u001b[1mhighlighted\u001b[0m",
+ "AssertionError"));
+
+ SurefireReportWriter.write(report, "dbt", cases);
+
+ String xml = Files.readString(report, StandardCharsets.UTF_8);
+ assertFalse(xml.contains("\u001b"), "no escape character may reach the
report");
+ // The escape sequences go whole, so their printable tail does not litter
the log either.
+ assertFalse(xml.contains("[0m"), xml);
+ assertTrue(xml.contains("Running with dbt=1.8.0"));
+ assertTrue(xml.contains("message=\"boom highlighted\""));
+
+ // The report has to be readable by an XML parser, that is the whole point.
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ Document doc = factory.newDocumentBuilder().parse(report.toFile());
+ assertEquals(1, doc.getElementsByTagName("testcase").getLength());
+ }
+
+ @Test
+ void sanitizeDropsIllegalCharactersAndKeepsTheRest() {
+ assertEquals("ab", SurefireReportWriter.sanitize("a\u0000b\u0008"));
+ assertEquals("a\tb\nc\rd", SurefireReportWriter.sanitize("a\tb\nc\rd"));
+ assertEquals("", SurefireReportWriter.sanitize("\ud83d"));
+ assertEquals("\ud83d\ude00",
SurefireReportWriter.sanitize("\ud83d\ude00"));
+ assertEquals("", SurefireReportWriter.sanitize(null));
+ }
+
@Test
void formatTimeUsesIntegerWhenWhole() {
assertEquals("6", SurefireReportWriter.formatTime(6.0));