diff --git a/NOTICE.md b/NOTICE.md
index c3e7638eb..1dc5c988b 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -123,6 +123,12 @@ freemarker
* Homepage: https://freemarker.apache.org/
* More information in folder: doc/3rd-party-licenses/freemarker
+flexmark-java
+
+ * License: BSD 2-Clause License
+ * Homepage: https://github.com/vsch/flexmark-java
+ * More information in folder: doc/3rd-party-licenses/flexmark-java
+
google-api-services-compute
* License: Apache License 2.0
diff --git a/config/testreport/css/default.css b/config/testreport/css/default.css
index 7018250c8..29ebac71f 100644
--- a/config/testreport/css/default.css
+++ b/config/testreport/css/default.css
@@ -407,6 +407,11 @@ template {
--web-vital-color-poor: rgb(200,120,120);
--load-bar-color: #8aa4c1;
+
+ --markdown-code-background-color: #f0f0f0;
+ --markdown-pre-background-color: #f5f5f5;
+ --markdown-table-header-background-color: #f0f0f0;
+ --markdown-blockquote-color: #666;
}
/*
@@ -1375,6 +1380,106 @@ table * {
list-style-position: inside;
}
+/*
+ * ----------------------------------------------------
+ * Markdown-rendered comment content
+ * ----------------------------------------------------
+ */
+.markdown {
+ color: var(--description-color);
+}
+ .markdown h1, .markdown h2, .markdown h3, .markdown h4,
+ .markdown h5, .markdown h6 {
+ margin: 0.8em 0 0.4em 0;
+ color: var(--section-headline-color);
+ border-bottom: none;
+ }
+ .markdown h1 {
+ font-size: 1.4rem;
+ border-bottom: 2px solid var(--section-headline-color);
+ }
+ .markdown h2 {
+ font-size: 1.25rem;
+ }
+ .markdown h3 {
+ font-size: 1.1rem;
+ border-bottom: 1px solid var(--section-headline-color);
+ }
+ .markdown h4 {
+ font-size: 1.05rem;
+ border-bottom: 1px dotted var(--section-headline-color);
+ }
+ .markdown h5 {
+ font-size: 1.02rem;
+ font-weight: normal;
+ }
+ .markdown h6 {
+ font-size: 1.00rem;
+ font-weight: normal;
+ font-style: italic;
+ }
+
+ .markdown p {
+ margin: 0.5em 0;
+ }
+ .markdown ul, .markdown ol {
+ margin: 0.5em 0 0.5em 1.5em;
+ padding: 0;
+ }
+ .markdown li {
+ margin: 0.2em 0;
+ }
+ .markdown code {
+ background-color: var(--markdown-code-background-color);
+ padding: 0.15em 0.4em;
+ border-radius: 3px;
+ font-size: 0.9em;
+ }
+ .markdown pre {
+ background-color: var(--markdown-pre-background-color);
+ border: 1px solid var(--main-border-color);
+ border-radius: 4px;
+ padding: 0.75em 1em;
+ overflow-x: auto;
+ }
+ .markdown pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+ }
+ .markdown table {
+ border-collapse: collapse;
+ margin: 0.5em 0;
+ }
+ .markdown th, .markdown td {
+ border: 1px solid var(--main-border-color);
+ padding: 0.3em 0.6em;
+ text-align: left;
+ }
+ .markdown th {
+ background-color: var(--markdown-table-header-background-color);
+ font-weight: bold;
+ padding: 0.5em 0.75em;
+ text-align: center;
+ }
+ .markdown blockquote {
+ border-left: 3px solid var(--main-border-color);
+ margin: 0.5em 0;
+ padding: 0.25em 1em;
+ color: var(--markdown-blockquote-color);
+ }
+ .markdown hr {
+ border: none;
+ border-top: 1px solid var(--main-border-color);
+ margin: 1em 0;
+ }
+ .markdown strong {
+ font-weight: bold;
+ }
+ .markdown em {
+ font-style: italic;
+ }
+
.subsection
{
diff --git a/doc/3rd-party-licenses/flexmark-java/LICENSE b/doc/3rd-party-licenses/flexmark-java/LICENSE
new file mode 100644
index 000000000..8b1cbd658
--- /dev/null
+++ b/doc/3rd-party-licenses/flexmark-java/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2015-2016, Atlassian Pty Ltd
+All rights reserved.
+
+Copyright (c) 2016-2018, Vladimir Schneider,
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/doc/3rd-party-licenses/flexmark-java/NOTICE b/doc/3rd-party-licenses/flexmark-java/NOTICE
new file mode 100644
index 000000000..84ba76f71
--- /dev/null
+++ b/doc/3rd-party-licenses/flexmark-java/NOTICE
@@ -0,0 +1,7 @@
+flexmark-java
+https://github.com/vsch/flexmark-java
+
+Copyright (c) 2015-2016, Atlassian Pty Ltd
+Copyright (c) 2016-2018, Vladimir Schneider
+
+Licensed under the BSD 2-Clause License.
diff --git a/doc/feature-doc/markdown-comments.md b/doc/feature-doc/markdown-comments.md
new file mode 100644
index 000000000..617fca2c6
--- /dev/null
+++ b/doc/feature-doc/markdown-comments.md
@@ -0,0 +1,54 @@
+# Markdown Comments in Reports
+
+XLT reports support optional Markdown formatting for test comments. When a comment
+value is prefixed with `::markdown::`, the content after the prefix is converted from
+Markdown to HTML and embedded in the report inside a styled `
`.
+
+## Usage
+
+Set the comment property in your test configuration:
+
+```properties
+# Plain text (unchanged, raw HTML also accepted)
+com.xceptance.xlt.loadtests.comment = This is a plain comment
+
+# Markdown-formatted comment
+com.xceptance.xlt.loadtests.comment = ::markdown::## Test Run Notes\n\n- **Environment**: staging\n- **Build**: 1.2.3\n- **Duration**: 2 hours
+```
+
+Multiple numbered comments are also supported:
+
+```properties
+com.xceptance.xlt.loadtests.comment.1 = ::markdown::## Summary\nAll scenarios passed.
+com.xceptance.xlt.loadtests.comment.2 = ::markdown::## Known Issues\n- Slow DNS on agent 3
+```
+
+## Behavior
+
+| Input | Output |
+|-------|--------|
+| No prefix | Passed through as-is (raw HTML allowed) |
+| `::markdown::` + content | Markdown → HTML, wrapped in `
` |
+| `::markdown::` only (no content) | Returned unchanged |
+| `null` | Returned as `null` |
+
+The `::markdown::` prefix is **case-insensitive** — `::Markdown::`, `::MARKDOWN::`, etc. all work.
+
+## Supported Markdown Features
+
+Powered by [flexmark-java](https://github.com/vsch/flexmark-java) with these extensions:
+
+- **Headings** (`# H1` through `###### H6`)
+- **Bold** / *Italic* / ~~Strikethrough~~
+- **Lists** (ordered and unordered)
+- **Tables** (GitHub-flavored)
+- **Code** (inline and fenced code blocks)
+- **Blockquotes**
+- **Links** (including auto-linking of URLs)
+- **Horizontal rules**
+
+## Styling
+
+The rendered Markdown is wrapped in `
` and styled via the report's
+`default.css`. The styles use the report's existing CSS custom properties for consistent
+appearance. Headings, tables, code blocks, blockquotes, and lists are all covered.
diff --git a/pom.xml b/pom.xml
index bb5c6e5e2..d7d27d06b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -536,6 +536,22 @@
parboiled-java
1.4.1
+
+
+ com.vladsch.flexmark
+ flexmark
+ 0.64.8
+
+
+ com.vladsch.flexmark
+ flexmark-ext-tables
+ 0.64.8
+
+
+ com.vladsch.flexmark
+ flexmark-ext-autolink
+ 0.64.8
+
org.glassfish.tyrus
diff --git a/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java b/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java
index d024f8cb2..8266cfabc 100644
--- a/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java
+++ b/src/main/java/com/xceptance/xlt/report/providers/ConfigurationReportProvider.java
@@ -19,6 +19,7 @@
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
+import java.util.Arrays;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
@@ -32,6 +33,11 @@
import org.apache.commons.vfs2.FileSystemManager;
import org.apache.commons.vfs2.VFS;
+import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
+import com.vladsch.flexmark.ext.tables.TablesExtension;
+import com.vladsch.flexmark.html.HtmlRenderer;
+import com.vladsch.flexmark.parser.Parser;
+import com.vladsch.flexmark.util.data.MutableDataSet;
import com.xceptance.common.util.ProductInformation;
import com.xceptance.common.util.RegExUtils;
import com.xceptance.xlt.api.engine.Data;
@@ -58,6 +64,24 @@ public class ConfigurationReportProvider extends AbstractReportProvider
private static final String MASK_PROPERTIES_REGEX_DEFAULT = "(?i)password";
+ private static final String MARKDOWN_PREFIX = "::markdown::";
+
+ /** Flexmark parser with tables and autolink extensions. */
+ private static final Parser MARKDOWN_PARSER;
+
+ /** Flexmark HTML renderer. */
+ private static final HtmlRenderer MARKDOWN_RENDERER;
+
+ static
+ {
+ // we want to support tables and autolinks
+ final MutableDataSet options = new MutableDataSet();
+ options.set(Parser.EXTENSIONS, Arrays.asList(TablesExtension.create(), AutolinkExtension.create()));
+
+ MARKDOWN_PARSER = Parser.builder(options).build();
+ MARKDOWN_RENDERER = HtmlRenderer.builder(options).build();
+ }
+
/**
* {@inheritDoc}
*/
@@ -111,7 +135,7 @@ public Object createReportFragment()
for (final Map.Entry entry : sortedLoadtestProps.entrySet())
{
- report.comments.add(entry.getValue());
+ report.comments.add(processComment(entry.getValue()));
}
// add project name
@@ -224,6 +248,35 @@ public boolean wantsDataRecords()
return false;
}
+ /**
+ * Processes a comment string. If the comment starts with the marker
+ * {@value #MARKDOWN_PREFIX} (case-insensitive), the remainder is treated
+ * as Markdown and converted to HTML, wrapped in a div with class "markdown".
+ * Otherwise the raw string is returned unchanged.
+ *
+ * @param comment
+ * the comment string to process
+ * @return the processed comment
+ */
+ static String processComment(final String comment)
+ {
+ if (comment == null)
+ {
+ return null;
+ }
+
+ final String cleanedString = comment.strip();
+
+ if (cleanedString.length() >= MARKDOWN_PREFIX.length() && cleanedString.substring(0, MARKDOWN_PREFIX.length()).equalsIgnoreCase(MARKDOWN_PREFIX))
+ {
+ final String markdown = cleanedString.substring(MARKDOWN_PREFIX.length());
+ final String html = MARKDOWN_RENDERER.render(MARKDOWN_PARSER.parse(markdown));
+ return "" + html + "
";
+ }
+
+ return comment;
+ }
+
/**
* Returns the custom JVM arguments stored in the file "jvmargs.cfg". If no such file can be found, the returned
* list is empty.
diff --git a/src/test/java/com/xceptance/xlt/report/providers/ConfigurationReportProviderTest.java b/src/test/java/com/xceptance/xlt/report/providers/ConfigurationReportProviderTest.java
index e4614972a..58e3cadd8 100644
--- a/src/test/java/com/xceptance/xlt/report/providers/ConfigurationReportProviderTest.java
+++ b/src/test/java/com/xceptance/xlt/report/providers/ConfigurationReportProviderTest.java
@@ -55,4 +55,61 @@ public void testSecretPropertiesAreMaskedInTheOutput() throws IOException
FileUtils.deleteDirectoryRelaxed(testDir.toFile());
}
}
+
+ // --- processComment tests ---
+
+ @Test
+ public void testProcessComment_plainString()
+ {
+ Assert.assertEquals("Hello World", ConfigurationReportProvider.processComment("Hello World"));
+ }
+
+ @Test
+ public void testProcessComment_rawHtml()
+ {
+ Assert.assertEquals("Bold", ConfigurationReportProvider.processComment("Bold"));
+ }
+
+ @Test
+ public void testProcessComment_markdown()
+ {
+ final String result = ConfigurationReportProvider.processComment("::markdown::**bold** text");
+ Assert.assertTrue("Should start with markdown div", result.startsWith(""));
+ Assert.assertTrue("Should end with closing div", result.endsWith("
"));
+ Assert.assertTrue("Should contain ", result.contains("bold"));
+ }
+
+ @Test
+ public void testProcessComment_caseInsensitive()
+ {
+ final String result = ConfigurationReportProvider.processComment("::Markdown::**bold** text");
+ Assert.assertTrue("Should start with markdown div", result.startsWith(""));
+ Assert.assertTrue("Should contain
", result.contains("bold"));
+
+ final String result2 = ConfigurationReportProvider.processComment("::MARKDOWN::**bold** text");
+ Assert.assertTrue("Upper case should also work", result2.startsWith(""));
+ }
+
+ @Test
+ public void testProcessComment_markdownTable()
+ {
+ final String table = "::markdown::| A | B |\n|---|---|\n| 1 | 2 |";
+ final String result = ConfigurationReportProvider.processComment(table);
+ Assert.assertTrue("Should start with markdown div", result.startsWith("
"));
+ Assert.assertTrue("Should contain table element", result.contains("
"));
+ }
+
+ @Test
+ public void testProcessComment_null()
+ {
+ Assert.assertNull(ConfigurationReportProvider.processComment(null));
+ }
+
+ @Test
+ public void testProcessComment_prefixOnly()
+ {
+ // prefix only with no content should be returned unchanged
+ Assert.assertEquals("", ConfigurationReportProvider.processComment("::markdown::"));
+ }
}
+