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::")); + } } +