From: Ian Rogers <irogers@google.com>
To: irogers@google.com, acme@kernel.org, namhyung@kernel.org
Cc: 9erthalion6@gmail.com, adrian.hunter@intel.com,
james.clark@linaro.org, jolsa@kernel.org, leo.yan@arm.com,
linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org,
mingo@redhat.com, peterz@infradead.org
Subject: [PATCH v2 2/2] perf test: Improve JUnit XML schema compliance and completeness
Date: Mon, 28 Sep 2026 18:18:36 -0700 [thread overview]
Message-ID: <20260929011837.4157334-2-irogers@google.com> (raw)
In-Reply-To: <20260929011837.4157334-1-irogers@google.com>
Improve the JUnit XML output generated by 'perf test -j' so that it
validates cleanly against both the strict Apache Ant/Windy Road JUnit
schema (JUnit.xsd) and the Jenkins xUnit/Maven Surefire schema
(junit-10.xsd):
- Add the required 'package', 'id', 'timestamp' (ISO 8601 UTC),
'hostname', and 'time' attributes to '<testsuite>'.
- Emit '<properties>' (populated with uname sysname, release, version,
and machine when available) at the start of '<testsuite>' and
'<system-out/>' and '<system-err/>' at the end, matching the element
sequence required by JUnit.xsd.
- Add the required 'type="failure"' attribute to '<failure>' elements
and emit self-closing tags for passing '<testcase>' elements and
'<failure>'/'<error>' elements with empty error output.
- Escape apostrophes (''') and validate UTF-8 multi-byte sequences
against valid XML 1.0 codepoints in xml_escape().
- Use the actual suite description and '%.2f' time format for user
override skipped tests ('-s'/'--skip').
- Document '-j[<file>]'/'--junit[=<file>]' in perf-test.txt.
Assisted-by: Antigravity:gemini-3.1-pro
Signed-off-by: Ian Rogers <irogers@google.com>
---
tools/perf/Documentation/perf-test.txt | 5 +
tools/perf/tests/builtin-test.c | 309 +++++++++++++++++--------
2 files changed, 212 insertions(+), 102 deletions(-)
diff --git a/tools/perf/Documentation/perf-test.txt b/tools/perf/Documentation/perf-test.txt
index 16d940e8d221..9b1f153ce881 100644
--- a/tools/perf/Documentation/perf-test.txt
+++ b/tools/perf/Documentation/perf-test.txt
@@ -55,6 +55,11 @@ OPTIONS
Do not fork child for each test, run all tests within single process, this
sets sequential mode and disables the per-test timeout.
+-j[<file>]::
+--junit[=<file>]::
+ Generate JUnit XML test report to the specified file, or 'test.xml' if
+ no file is specified.
+
--dso::
Specify a DSO for the "Symbols" test.
diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-test.c
index 4ecbaf757095..1e85eb1eba82 100644
--- a/tools/perf/tests/builtin-test.c
+++ b/tools/perf/tests/builtin-test.c
@@ -28,6 +28,7 @@
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
+#include <sys/utsname.h>
#include <sys/wait.h>
#include <unistd.h>
@@ -466,23 +467,59 @@ static int __printf(2, 3) strbuf_addf_safe(struct strbuf *sb, const char *fmt, .
static char *xml_escape(const char *str)
{
struct strbuf buf = STRBUF_INIT;
- const char *p;
+ const unsigned char *p;
char *res;
if (!str)
return strdup("");
- for (p = str; *p; p++) {
- if (*p == '&')
- strbuf_addstr(&buf, "&");
- else if (*p == '<')
- strbuf_addstr(&buf, "<");
- else if (*p == '>')
- strbuf_addstr(&buf, ">");
- else if (*p == '"')
- strbuf_addstr(&buf, """);
- else if ((unsigned char)*p >= 32 || *p == '\n' || *p == '\t')
- strbuf_addch(&buf, *p);
+ for (p = (const unsigned char *)str; *p; p++) {
+ unsigned char c = *p;
+ int err = 0;
+
+ if (c == '&') {
+ err = strbuf_addstr(&buf, "&");
+ } else if (c == '<') {
+ err = strbuf_addstr(&buf, "<");
+ } else if (c == '>') {
+ err = strbuf_addstr(&buf, ">");
+ } else if (c == '"') {
+ err = strbuf_addstr(&buf, """);
+ } else if (c == '\'') {
+ err = strbuf_addstr(&buf, "'");
+ } else if ((c >= 0x20 && c <= 0x7e) || c == '\n' || c == '\t') {
+ err = strbuf_addch(&buf, c);
+ } else if (c >= 0xc2 && c <= 0xdf && (p[1] & 0xc0) == 0x80) {
+ err = strbuf_add(&buf, p, 2);
+ p += 1;
+ } else if (c >= 0xe0 && c <= 0xef &&
+ (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80) {
+ unsigned int cp = ((c & 0x0f) << 12) |
+ ((p[1] & 0x3f) << 6) |
+ (p[2] & 0x3f);
+
+ if (cp >= 0x800 &&
+ (cp < 0xd800 || (cp >= 0xe000 && cp <= 0xfffd))) {
+ err = strbuf_add(&buf, p, 3);
+ p += 2;
+ }
+ } else if (c >= 0xf0 && c <= 0xf4 &&
+ (p[1] & 0xc0) == 0x80 && (p[2] & 0xc0) == 0x80 &&
+ (p[3] & 0xc0) == 0x80) {
+ unsigned int cp = ((c & 0x07) << 18) |
+ ((p[1] & 0x3f) << 12) |
+ ((p[2] & 0x3f) << 6) |
+ (p[3] & 0x3f);
+
+ if (cp >= 0x10000 && cp <= 0x10ffff) {
+ err = strbuf_add(&buf, p, 4);
+ p += 3;
+ }
+ }
+ if (err < 0) {
+ strbuf_release(&buf);
+ return NULL;
+ }
}
res = strbuf_detach(&buf, NULL);
return res ? res : strdup("");
@@ -531,6 +568,68 @@ static int get_max_desc_width(int width)
return width > term_width ? term_width : width;
}
+static void append_junit_testcase(const char *classname, const char *testname,
+ int result, const char *skip_msg,
+ const char *err_output, double elapsed)
+{
+ char *escaped_err = xml_escape(err_output);
+ char *escaped_class = xml_escape(classname);
+ char *escaped_test = xml_escape(testname);
+
+ if (!escaped_err || !escaped_class || !escaped_test)
+ goto out;
+
+ if (result == TEST_OK) {
+ strbuf_addf(&junit_xml_buf,
+ " <testcase classname=\"%s\" name=\"%s\" time=\"%.2f\"/>\n",
+ escaped_class, escaped_test, elapsed);
+ goto out;
+ }
+
+ strbuf_addf(&junit_xml_buf,
+ " <testcase classname=\"%s\" name=\"%s\" time=\"%.2f\">\n",
+ escaped_class, escaped_test, elapsed);
+ if (result == TEST_TIMEOUT) {
+ if (err_output && *err_output) {
+ strbuf_addf(&junit_xml_buf,
+ " <error message=\"TIMEOUT\" type=\"timeout\">\n%s\n </error>\n",
+ escaped_err);
+ } else {
+ strbuf_addstr(&junit_xml_buf,
+ " <error message=\"TIMEOUT\" type=\"timeout\"/>\n");
+ }
+ } else if (result != TEST_SKIP) {
+ if (err_output && *err_output) {
+ strbuf_addf(&junit_xml_buf,
+ " <failure message=\"FAILED\" type=\"failure\">\n%s\n </failure>\n",
+ escaped_err);
+ } else {
+ strbuf_addstr(&junit_xml_buf,
+ " <failure message=\"FAILED\" type=\"failure\"/>\n");
+ }
+ } else {
+ char *escaped_reason = xml_escape(skip_msg ? skip_msg : "Skip");
+
+ if (escaped_reason) {
+ if (err_output && *err_output) {
+ strbuf_addf(&junit_xml_buf,
+ " <skipped message=\"%s\">\n%s\n </skipped>\n",
+ escaped_reason, escaped_err);
+ } else {
+ strbuf_addf(&junit_xml_buf,
+ " <skipped message=\"%s\"/>\n",
+ escaped_reason);
+ }
+ free(escaped_reason);
+ }
+ }
+ strbuf_addstr(&junit_xml_buf, " </testcase>\n");
+out:
+ free(escaped_err);
+ free(escaped_class);
+ free(escaped_test);
+}
+
static int print_test_result(struct test_suite *t, int curr_suite, int curr_test_case,
int result, int width, int running,
const char *err_output, double elapsed)
@@ -601,41 +700,9 @@ static int print_test_result(struct test_suite *t, int curr_suite, int curr_test
}
if (junit_filename && result != TEST_RUNNING) {
- const char *classname = t->desc;
- const char *testname = test_description(t, curr_test_case);
- char *escaped_err = xml_escape(err_output);
- char *escaped_class = xml_escape(classname);
- char *escaped_test = xml_escape(testname);
-
- strbuf_addf(&junit_xml_buf,
- " <testcase classname=\"%s\" name=\"%s\" time=\"%.2f\">\n",
- escaped_class, escaped_test, elapsed);
- if (result == TEST_TIMEOUT) {
- strbuf_addf(&junit_xml_buf,
- " <error message=\"TIMEOUT\" type=\"timeout\">\n%s\n </error>\n",
- escaped_err);
- } else if (result != TEST_OK && result != TEST_SKIP) {
- strbuf_addf(&junit_xml_buf,
- " <failure message=\"FAILED\">\n%s\n </failure>\n",
- escaped_err);
- } else if (result == TEST_SKIP) {
- const char *reason = skip_reason(t, curr_test_case);
- char *escaped_reason = xml_escape(reason ? reason : "Skip");
-
- if (err_output && *err_output) {
- strbuf_addf(&junit_xml_buf,
- " <skipped message=\"%s\">\n%s\n </skipped>\n",
- escaped_reason, escaped_err);
- } else {
- strbuf_addf(&junit_xml_buf, " <skipped message=\"%s\"/>\n",
- escaped_reason);
- }
- free(escaped_reason);
- }
- strbuf_addstr(&junit_xml_buf, " </testcase>\n");
- free(escaped_err);
- free(escaped_class);
- free(escaped_test);
+ append_junit_testcase(t->desc, test_description(t, curr_test_case),
+ result, skip_reason(t, curr_test_case),
+ err_output, elapsed);
}
return 0;
@@ -1407,7 +1474,86 @@ static void cmd_test_sig_handler(int sig)
siglongjmp(cmd_test_jmp_buf, sig);
}
-static void print_tests_summary(void)
+static void write_junit_xml(const struct timespec *start_time, time_t start_realtime)
+{
+ unsigned int total = summary_tests_passed + summary_subtests_passed +
+ summary_tests_skipped + summary_tests_timed_out +
+ summary_tests_failed;
+ struct timespec end_time;
+ double elapsed;
+ struct tm tm;
+ char timestamp[32];
+ struct utsname uts;
+ bool have_uts;
+ char *escaped_host;
+ FILE *fp;
+ int fd;
+
+ fd = open(junit_filename, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0644);
+ if (fd < 0) {
+ pr_err("Failed to open %s for writing junit XML output: %s\n",
+ junit_filename, strerror(errno));
+ return;
+ }
+
+ fp = fdopen(fd, "w");
+ if (!fp) {
+ close(fd);
+ pr_err("Failed to associate stream with fd for %s: %s\n",
+ junit_filename, strerror(errno));
+ return;
+ }
+
+ have_uts = uname(&uts) == 0;
+ escaped_host = xml_escape(have_uts && uts.nodename[0] ? uts.nodename : "localhost");
+
+ clock_gettime(CLOCK_MONOTONIC, &end_time);
+ elapsed = (end_time.tv_sec - start_time->tv_sec) +
+ (end_time.tv_nsec - start_time->tv_nsec) / 1000000000.0;
+ if (!gmtime_r(&start_realtime, &tm) ||
+ !strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%S", &tm))
+ snprintf(timestamp, sizeof(timestamp), "1970-01-01T00:00:00");
+
+ fprintf(fp, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
+ fprintf(fp, "<testsuites>\n");
+ fprintf(fp,
+ " <testsuite name=\"perf-tests\" package=\"perf-tests\" id=\"0\" timestamp=\"%s\" hostname=\"%s\" tests=\"%u\" failures=\"%u\" errors=\"%u\" skipped=\"%u\" time=\"%.2f\">\n",
+ timestamp, escaped_host ?: "localhost", total, summary_tests_failed,
+ summary_tests_timed_out, summary_tests_skipped, elapsed);
+ free(escaped_host);
+ if (have_uts) {
+ char *sysname = xml_escape(uts.sysname);
+ char *release = xml_escape(uts.release);
+ char *version = xml_escape(uts.version);
+ char *machine = xml_escape(uts.machine);
+
+ fprintf(fp, " <properties>\n");
+ if (sysname)
+ fprintf(fp, " <property name=\"sysname\" value=\"%s\"/>\n", sysname);
+ if (release)
+ fprintf(fp, " <property name=\"release\" value=\"%s\"/>\n", release);
+ if (version)
+ fprintf(fp, " <property name=\"version\" value=\"%s\"/>\n", version);
+ if (machine)
+ fprintf(fp, " <property name=\"machine\" value=\"%s\"/>\n", machine);
+ fprintf(fp, " </properties>\n");
+ free(sysname);
+ free(release);
+ free(version);
+ free(machine);
+ } else {
+ fprintf(fp, " <properties/>\n");
+ }
+ fprintf(fp, "%s", junit_xml_buf.buf);
+ fprintf(fp, " <system-out/>\n");
+ fprintf(fp, " <system-err/>\n");
+ fprintf(fp, " </testsuite>\n");
+ fprintf(fp, "</testsuites>\n");
+ fclose(fp);
+ pr_info("Wrote junit XML output to %s\n", junit_filename);
+}
+
+static void print_tests_summary(const struct timespec *start_time, time_t start_realtime)
{
pr_info("\n=== Test Summary ===\n");
pr_info("Passed main tests : %u\n", summary_tests_passed);
@@ -1430,42 +1576,9 @@ static void print_tests_summary(void)
color_fprintf(debug_file(), PERF_COLOR_GREEN, "Failed tests : 0\n");
}
- if (junit_filename) {
- int fd;
- FILE *fp;
-
- fd = open(junit_filename, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0644);
- if (fd >= 0) {
- fp = fdopen(fd, "w");
- if (fp) {
- unsigned int total = summary_tests_passed +
- summary_subtests_passed +
- summary_tests_skipped +
- summary_tests_timed_out +
- summary_tests_failed;
- fprintf(fp, "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
- fprintf(fp, "<testsuites>\n");
- fprintf(fp,
- " <testsuite name=\"perf-tests\" tests=\"%u\" failures=\"%u\" errors=\"%u\" skipped=\"%u\">\n",
- total,
- summary_tests_failed,
- summary_tests_timed_out,
- summary_tests_skipped);
- fprintf(fp, "%s", junit_xml_buf.buf);
- fprintf(fp, " </testsuite>\n");
- fprintf(fp, "</testsuites>\n");
- fclose(fp);
- pr_info("Wrote junit XML output to %s\n", junit_filename);
- } else {
- close(fd);
- pr_err("Failed to associate stream with fd for %s: %s\n",
- junit_filename, strerror(errno));
- }
- } else {
- pr_err("Failed to open %s for writing junit XML output: %s\n",
- junit_filename, strerror(errno));
- }
- }
+ if (junit_filename)
+ write_junit_xml(start_time, start_realtime);
+
strbuf_release(&junit_xml_buf);
strbuf_release(&summary_timed_out_tests_buf);
strbuf_release(&summary_failed_tests_buf);
@@ -1475,8 +1588,13 @@ static int __cmd_test(struct test_suite **suites, int argc, const char *argv[],
struct intlist *skiplist)
{
static int width = 0;
+ struct timespec suite_start_time;
+ time_t suite_start_realtime;
int err = 0;
+ clock_gettime(CLOCK_MONOTONIC, &suite_start_time);
+ suite_start_realtime = time(NULL);
+
for (struct test_suite **t = suites; *t; t++) {
int i, len = strlen(test_description(*t, -1));
@@ -1574,23 +1692,10 @@ static int __cmd_test(struct test_suite **suites, int argc, const char *argv[],
" Skip (user override)\n");
summary_tests_skipped++;
if (junit_filename) {
- char *escaped_class =
- xml_escape((const char *)
- test_description(*t, -1));
- char *escaped_test = xml_escape("override");
- char *escaped_reason =
- xml_escape("user override");
-
- strbuf_addf(&junit_xml_buf,
- " <testcase classname=\"%s\" name=\"%s\" time=\"0.000\">\n",
- escaped_class, escaped_test);
- strbuf_addf(&junit_xml_buf,
- " <skipped message=\"%s\"/>\n",
- escaped_reason);
- strbuf_addstr(&junit_xml_buf, " </testcase>\n");
- free(escaped_reason);
- free(escaped_test);
- free(escaped_class);
+ const char *desc = test_description(*t, -1);
+
+ append_junit_testcase(desc, desc, TEST_SKIP,
+ "user override", NULL, 0.0);
}
}
continue;
@@ -1631,7 +1736,7 @@ static int __cmd_test(struct test_suite **suites, int argc, const char *argv[],
setitimer(ITIMER_REAL, &timer, NULL);
signal(SIGALRM, SIG_DFL);
}
- print_tests_summary();
+ print_tests_summary(&suite_start_time, suite_start_realtime);
free(global_pfds);
free(global_pfd_indices);
global_pfds = NULL;
--
2.56.0.rc1.315.gc6ed9934b7-goog
next prev parent reply other threads:[~2026-09-29 1:18 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-28 21:01 [PATCH v1 1/2] perf test: Add per-test timeout support Ian Rogers
2026-09-28 21:01 ` [PATCH v1 2/2] perf test: Improve JUnit XML schema compliance and completeness Ian Rogers
2026-09-29 1:18 ` [PATCH v2 1/2] perf test: Add per-test timeout support Ian Rogers
2026-09-29 1:18 ` Ian Rogers [this message]
2026-09-29 2:53 ` [PATCH v3 " Ian Rogers
2026-09-29 2:53 ` [PATCH v3 2/2] perf test: Improve JUnit XML schema compliance and completeness Ian Rogers
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260929011837.4157334-2-irogers@google.com \
--to=irogers@google.com \
--cc=9erthalion6@gmail.com \
--cc=acme@kernel.org \
--cc=adrian.hunter@intel.com \
--cc=james.clark@linaro.org \
--cc=jolsa@kernel.org \
--cc=leo.yan@arm.com \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-perf-users@vger.kernel.org \
--cc=mingo@redhat.com \
--cc=namhyung@kernel.org \
--cc=peterz@infradead.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®