mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
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 ('&apos;') 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, "&amp;");
-		else if (*p == '<')
-			strbuf_addstr(&buf, "&lt;");
-		else if (*p == '>')
-			strbuf_addstr(&buf, "&gt;");
-		else if (*p == '"')
-			strbuf_addstr(&buf, "&quot;");
-		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, "&amp;");
+		} else if (c == '<') {
+			err = strbuf_addstr(&buf, "&lt;");
+		} else if (c == '>') {
+			err = strbuf_addstr(&buf, "&gt;");
+		} else if (c == '"') {
+			err = strbuf_addstr(&buf, "&quot;");
+		} else if (c == '\'') {
+			err = strbuf_addstr(&buf, "&apos;");
+		} 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


  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®