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("&amp;");
         case '<' -> sb.append("&lt;");
@@ -190,9 +234,7 @@ public final class SurefireReportWriter {
         case '"' -> sb.append("&quot;");
         case '\'' -> sb.append("&apos;");
         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));

Reply via email to