From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=74.125.82.69 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1792703284; b=AGI3VxayAlkLg0+VokdiYRJ2h7oJ9UF/uRAP9ES2CEnOHiFNqeKQItPPMON7E4hwHSqX IAADu18TwdEFJviw+HG6CLJe6e7WgAw07ZBzS7VcKkRElU6ZoD88bhDyQxcA/NX/XFGNU t6pSrCNnA7OUMEVzPXKVqoOp1ZC78A3ja8rlM7uPYLjFRJXkMDH7v/O/xyRugwW0Beqyv 3s7Nq+G73tdT682+ypQrj7980wEaqEM2kQaKjiuxL4IyBn91C0Eo5nf6J7BfwRJ6uknxr 2lFa5/zI/RtCGAUbkycQhol3SpWFttuCUf6UyjK0yyiloH4/vD/FoD2RgZWeHZAFHTQ== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1792703284; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Forwarded-Encrypted:X-Gm-Message-State:X-Received:Date:In-Reply-To: Mime-Version:References:X-Mailer:Message-ID:Subject:From:To:Cc: Content-Type; bh=e+8D5UHsu6jAB+yBeV8IX+0MZErNYtFjNo3jz+Hu1mU=; b=Ho9nF5nkHdRnF02RScLjb6YdultBlK7BK5vuAsoC1PszLnVhqKEyQgS3ksVoPhFsnQKJ Q5lusTTMf26RTg4s9hEgTnD0iwo3UylqpVag//usIHNWRK7/vUxsWhI18zlzVSHE0Ztu6 tWlpIUATPhQBMy/nw1Z030fIXg68xEQYzGs28zgW0kXaCGOB6/6r5E1P0NwUjD4RihNzP yugDeT3/XHyNvtZH3qfD9y0gD4hANWxRMJlAzI29GkElr6+s2zVtj+aKvhXTmx075Nz+q XYJv5gUBIpGGJzIhXgs7bIiox8gy19jQrLTlb1JjAUKIRzzlgge6Z1BcTKhCCWw/Hlg== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20251104 header.b=AVCWfVUo; arc=none smtp.remote-ip=74.125.82.69 Received: from mail-dl1-f69.google.com (mail-dl1-f69.google.com [74.125.82.69]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F0080606A325 for ; Thu, 22 Oct 2026 21:08:03 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org F0080606A325 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=google.com header.i=@google.com header.a=rsa-sha256 header.s=20251104 header.b=AVCWfVUo Received: by mail-dl1-f69.google.com with SMTP id a92af1059eb24-186c1aae818so567639c88.0 for ; Thu, 22 Oct 2026 14:08:03 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1792703283; x=1793308083; darn=vger.kernel.org; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:from:to:cc:subject:date:message-id:reply-to :content-type; bh=e+8D5UHsu6jAB+yBeV8IX+0MZErNYtFjNo3jz+Hu1mU=; b=AVCWfVUoHbrtHd6qG0uXz8jv9KZwVyo/28Wzw7Bt1ct5lfIZFSo57RXl9V87Gprgyd eD8ltgkII4F1/sZuWrXkBeAA2E5o0dVsHilKvOCfI0rxYA2KbMCeRKG+qcvhur9ng46v kHntGpu9U1/B/7YRxlqlZcHY7Y+VLPTTqD+6CDPVcRGci0axtk0hLNfShFoVGCyHjQG3 d2+xvHcplY6+pLJusLVeQJPvR1/JxmdZOxnZyqGquPtUPr4fbHhIO8SXahuVum0+F0iM mjH0nqezxPA1waqVEtD44PlF/VaAYNkFaHnd4zRfviyvUl4s9e1mRsnVn+k/b3kxCp0I IURw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792703283; x=1793308083; h=content-type:cc:to:from:subject:message-id:references:mime-version :in-reply-to:date:x-gm-message-state:from:to:cc:subject:date :message-id:reply-to:content-type; bh=e+8D5UHsu6jAB+yBeV8IX+0MZErNYtFjNo3jz+Hu1mU=; b=MzWQG22O3ISSwYhVa+3pOcrTkap9JKwb7OjawotW4NJUwehijUgWBJfkUs9JFPtXti t2AvK1PT0BH1iWk4s73vQY1h3VCgw0LAr1TdeAf2cFQggwhb0k0EMmxK5RDutEnprmbd b7XWAlT7oY5GlL++ihTV9eIUR47gpexPbN6stE8x244GCJEnBOqrSHKVKpA3JL+xWxLj az41eQMTJgTW2r4g0EtMUzZF83FN38F4RC53i3X2BmWdlS/rjqOsRX9xpyb0RcDip/w+ 2uSnC2znrrQWyE9GVHxuqYG1eCVV3ejCg1311u+HCgYmehCRQhnOqrNVHmR5SzOyqTLw zPgA== X-Forwarded-Encrypted: i=1; AKwUvBxkEmZy4nx33a1BtOH78d40RNmtZeDzmmHZNbshpvukElO1pjxVuVT51AAKAFBSV6PfQVrmt+k48JHDhmaPk+b8@vger.kernel.org X-Gm-Message-State: AFq9FYL+Ik9lkUbE+1CREm1fCos8wlPSRKlrrwDxH9FTO+7WVFrV2/se caaUpyYan6NZO5klN+07q/kwXKUMMh/rRuD2wCdXWQy6Z3cCfA/rOLetfuL5bNkFWpixsQxGurH M0/EiSd+YlQ== X-Received: from dlbvv9.prod.google.com ([2002:a05:7022:5f09:b0:15f:bc5b:ddbb]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:701b:42c9:10b0:144:c122:6cc6 with SMTP id a92af1059eb24-195c28e9422mr605964c88.32.1792703282772; Thu, 22 Oct 2026 14:08:02 -0700 (PDT) Date: Thu, 22 Oct 2026 14:07:52 -0700 In-Reply-To: <20261022210753.1851597-1-irogers@google.com> X-Mailing-List: linux-perf-users@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list Mime-Version: 1.0 References: <20261012234724.721224-1-irogers@google.com> <20261022210753.1851597-1-irogers@google.com> X-Mailer: git-send-email 2.56.0.307.g5fe7bd680c-goog Message-ID: <20261022210753.1851597-3-irogers@google.com> Subject: [PATCH v5 2/3] perf test: Improve JUnit XML schema compliance and completeness From: Ian Rogers 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 Content-Type: text/plain; charset="UTF-8" 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 ''. - Emit '' (populated with uname sysname, release, version, and machine when available) at the start of '' and '' and '' at the end, matching the element sequence required by JUnit.xsd. - Add the required 'type="failure"' attribute to '' elements and emit self-closing tags for passing '' elements and ''/'' 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[]'/'--junit[=]' in perf-test.txt. Signed-off-by: Ian Rogers Acked-by: Namhyung Kim --- 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 f85e9b561ac4..ca58f8d6298f 100644 --- a/tools/perf/Documentation/perf-test.txt +++ b/tools/perf/Documentation/perf-test.txt @@ -85,6 +85,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[]:: +--junit[=]:: + 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 b7bad9c42921..58dc75ba82cf 100644 --- a/tools/perf/tests/builtin-test.c +++ b/tools/perf/tests/builtin-test.c @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -529,23 +530,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(""); @@ -594,6 +631,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, + " \n", + escaped_class, escaped_test, elapsed); + goto out; + } + + strbuf_addf(&junit_xml_buf, + " \n", + escaped_class, escaped_test, elapsed); + if (result == TEST_TIMEOUT) { + if (err_output && *err_output) { + strbuf_addf(&junit_xml_buf, + " \n%s\n \n", + escaped_err); + } else { + strbuf_addstr(&junit_xml_buf, + " \n"); + } + } else if (result != TEST_SKIP) { + if (err_output && *err_output) { + strbuf_addf(&junit_xml_buf, + " \n%s\n \n", + escaped_err); + } else { + strbuf_addstr(&junit_xml_buf, + " \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, + " \n%s\n \n", + escaped_reason, escaped_err); + } else { + strbuf_addf(&junit_xml_buf, + " \n", + escaped_reason); + } + free(escaped_reason); + } + } + strbuf_addstr(&junit_xml_buf, " \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) @@ -664,41 +763,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, - " \n", - escaped_class, escaped_test, elapsed); - if (result == TEST_TIMEOUT) { - strbuf_addf(&junit_xml_buf, - " \n%s\n \n", - escaped_err); - } else if (result != TEST_OK && result != TEST_SKIP) { - strbuf_addf(&junit_xml_buf, - " \n%s\n \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, - " \n%s\n \n", - escaped_reason, escaped_err); - } else { - strbuf_addf(&junit_xml_buf, " \n", - escaped_reason); - } - free(escaped_reason); - } - strbuf_addstr(&junit_xml_buf, " \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; @@ -1566,7 +1633,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, "\n"); + fprintf(fp, "\n"); + fprintf(fp, + " \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, " \n"); + if (sysname) + fprintf(fp, " \n", sysname); + if (release) + fprintf(fp, " \n", release); + if (version) + fprintf(fp, " \n", version); + if (machine) + fprintf(fp, " \n", machine); + fprintf(fp, " \n"); + free(sysname); + free(release); + free(version); + free(machine); + } else { + fprintf(fp, " \n"); + } + fprintf(fp, "%s", junit_xml_buf.buf); + fprintf(fp, " \n"); + fprintf(fp, " \n"); + fprintf(fp, " \n"); + fprintf(fp, "\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); @@ -1589,42 +1735,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, "\n"); - fprintf(fp, "\n"); - fprintf(fp, - " \n", - total, - summary_tests_failed, - summary_tests_timed_out, - summary_tests_skipped); - fprintf(fp, "%s", junit_xml_buf.buf); - fprintf(fp, " \n"); - fprintf(fp, "\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); @@ -1634,8 +1747,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)); @@ -1724,23 +1842,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, - " \n", - escaped_class, escaped_test); - strbuf_addf(&junit_xml_buf, - " \n", - escaped_reason); - strbuf_addstr(&junit_xml_buf, " \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; @@ -1775,7 +1880,7 @@ static int __cmd_test(struct test_suite **suites, int argc, const char *argv[], for (size_t x = 0; x < num_tests; x++) finish_test(child_tests, x, num_tests, width); } - 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.307.g5fe7bd680c-goog