From nobody Mon Jun 8 04:19:58 2026 Received: from mail-dl1-f73.google.com (mail-dl1-f73.google.com [74.125.82.73]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id C7B413B5F48 for ; Tue, 2 Jun 2026 07:32:09 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=74.125.82.73 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780385537; cv=none; b=RNkNzRXRReC2AWHGxoI9vl5NqEGLnimnGQsj5+IULvIerASkpv8mNO6cDKACKYxgegD9Qwi6n1Yf6fc4Xcy6thzJg5w8PVy3F2c5JtzTYXRFYuRmK1QLTuwOK2nxEGmGnJCEU1eJwC/XBNDgHcMUnGfAMt7NLlSPpaHMVAq25bA= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1780385537; c=relaxed/simple; bh=hS27iNrAnq8kOwOklEU4uPP0EYrhwwZRTFiHsUrV8ag=; h=Date:In-Reply-To:Mime-Version:References:Message-ID:Subject:From: To:Cc:Content-Type; b=YM6sH1w7QaSNvH0F4Ku6/q7ch6hV39Z0rdBsqCy7cAU8SSuiVdbw1Vf8IT/TD7a8V3xqrj1uwdERqtq17x0UohwAYNLFfE9Jd6iq66pHSvtecW0zPnbuenQ4zuFKOydQWTTH/20YCSVz/Bk0NCoFqjPBxFxnjjB0Vkq6nhmGnyg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=reject dis=none) header.from=google.com; spf=pass smtp.mailfrom=flex--irogers.bounces.google.com; dkim=pass (2048-bit key) header.d=google.com header.i=@google.com header.b=T2cnhwzF; arc=none smtp.client-ip=74.125.82.73 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) header.d=google.com header.i=@google.com header.b="T2cnhwzF" Received: by mail-dl1-f73.google.com with SMTP id a92af1059eb24-135fe34cb98so673579c88.0 for ; Tue, 02 Jun 2026 00:32:09 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20251104; t=1780385529; x=1780990329; darn=vger.kernel.org; h=cc:to:from:subject:message-id:references:mime-version:in-reply-to :date:from:to:cc:subject:date:message-id:reply-to; bh=CfSPcJEKzW3wWuMYsIN3KAD+VhUJvMgxEDPRBvQxMwE=; b=T2cnhwzFPDQN9JdNXDzMues75Lb4+RHSSK7t6FdRlkshZOsvYLZe6jNd3cl/PaxPST mQoBHz4zlOBkG6sXp6qFOAO+A5mhSyxlWmJK7U5m8EMXxQh2TtALAdNGoBOBkCH5vIfL JG4R75V/P4VDrw1mbQOXR0sy45J9/dASynZvARDeD5kn76K7FxP2Ot98/tVR/R3CppkP YFVWbT9yVCvkPYn2XxUX5ektu0CrDr+tWbXxwABCML0aVHY6Cb39gfNFGhV0Q8LJl93p 8h2Mg2WLkMfIPBiOJB8xZ+Vr4o4pXRdspumnhx6ZJ5P/CLzFbX1xi+OrkV1dvi5gv7Dn roNw== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20251104; t=1780385529; x=1780990329; h=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; bh=CfSPcJEKzW3wWuMYsIN3KAD+VhUJvMgxEDPRBvQxMwE=; b=ZoiN2wOEgMCLGLLATTBw/NkKaqCFno5Q68MiWoXdUeShK/rjcxS8kn7GhG1F0iJqyF Y3ejU6EVZrXVWQpiKYrP3D9HqjA1Pv68kRopsfD+B+QCiMLmzDeh0gHgtPDcdr0zQri3 WEk0PNvkCBnqvHRvrPaVBREPfeGoKi3GLsP4tIS6M5b4NZJczZH8YSYusl0a5ZGyUiBV BnG9BgQSbJp4yjB8iPnpc38BgIisjnoD3ZOQVnuy4iG+4JftXokFRaonGsmCynj43WTH 9bTGpiRDLAyQe7X3MszBXBxBlrXwz2PvTJIZKOiZsN9E/HjnV5QwY+ECD0KIj+Wnz3NU oMgA== X-Forwarded-Encrypted: i=1; AFNElJ8c9az2ZW3lww3vsUIODpqD7r/nSQpH+bCekHBIlCWXKqiUdjELnxzlarCyh3meOqaykGwoRhBt4vPF6jw=@vger.kernel.org X-Gm-Message-State: AOJu0YyWERq+Zui/CHN2nV12PiHjO2oN+O9RAhCAJjxIVExRsQNzv1Oe TtYE1zSIoHE+SmTol9cedS+/0089dzs2ybzqpSywsglfr4EBXT17yvH7Blj0vnSN08x+R9D20JL KHlU2IcMB2g== X-Received: from dlbpt1.prod.google.com ([2002:a05:7022:e801:b0:12a:c4ed:5eeb]) (user=irogers job=prod-delivery.src-stubby-dispatcher) by 2002:a05:7022:e03:b0:12d:c4cf:cd7 with SMTP id a92af1059eb24-137d3bf60acmr6672812c88.8.1780385528607; Tue, 02 Jun 2026 00:32:08 -0700 (PDT) Date: Tue, 2 Jun 2026 00:31:29 -0700 In-Reply-To: <20260602073132.2653307-1-irogers@google.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: Mime-Version: 1.0 References: <20260601061401.1541457-1-irogers@google.com> <20260602073132.2653307-1-irogers@google.com> X-Mailer: git-send-email 2.54.0.929.g9b7fa37559-goog Message-ID: <20260602073132.2653307-15-irogers@google.com> Subject: [PATCH v7] perf test: Add -j/--junit option for JUnit XML test reports From: Ian Rogers To: irogers@google.com, acme@kernel.org, namhyung@kernel.org Cc: adrian.hunter@intel.com, alexander.shishkin@linux.intel.com, james.clark@linaro.org, jolsa@kernel.org, linux-kernel@vger.kernel.org, linux-perf-users@vger.kernel.org, mingo@redhat.com, peterz@infradead.org Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset="utf-8" Add a -j/--junit command line option to generate standard JUnit XML format test reports. The generated file defaults to 'test.xml' if no filename is specified, but allows users to override the path (e.g. -jmytest.xml). The XML report captures individual test suite and subtest execution latency, alongside XML-escaped failure logs and skip reasons, while preserving the full multi-process concurrency speed of parallel test execution. Assisted-by: Gemini-CLI:Google Gemini 3 Signed-off-by: Ian Rogers --- tools/perf/tests/builtin-test.c | 149 ++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 6 deletions(-) diff --git a/tools/perf/tests/builtin-test.c b/tools/perf/tests/builtin-tes= t.c index 486f563ec04f..6a2bce394325 100644 --- a/tools/perf/tests/builtin-test.c +++ b/tools/perf/tests/builtin-test.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "builtin.h" #include "config.h" #include "hist.h" @@ -39,6 +40,9 @@ =20 #include "tests-scripts.h" =20 +static const char *junit_filename; +static struct strbuf junit_xml_buf =3D STRBUF_INIT; + /* * Command line option to not fork the test running in the same process and * making them easier to debug. @@ -307,6 +311,8 @@ struct child_test { struct strbuf err_output; int result; bool done; + struct timespec start_time; + struct timespec end_time; }; =20 static jmp_buf run_test_jmp_buf; @@ -372,8 +378,34 @@ static struct strbuf summary_failed_tests_buf =3D STRB= UF_INIT; static int strbuf_addstr_safe(struct strbuf *sb, const char *s); static int __printf(2, 3) strbuf_addf_safe(struct strbuf *sb, const char *= fmt, ...); =20 +static char *xml_escape(const char *str) +{ + struct strbuf buf =3D STRBUF_INIT; + const char *p; + char *res; + + if (!str) + return strdup(""); + + for (p =3D str; *p; p++) { + if (*p =3D=3D '&') + strbuf_addstr(&buf, "&"); + else if (*p =3D=3D '<') + strbuf_addstr(&buf, "<"); + else if (*p =3D=3D '>') + strbuf_addstr(&buf, ">"); + else if (*p =3D=3D '"') + strbuf_addstr(&buf, """); + else if ((unsigned char)*p >=3D 32 || *p =3D=3D '\n' || *p =3D=3D '\t') + strbuf_addch(&buf, *p); + } + res =3D strbuf_detach(&buf, NULL); + return res ? res : strdup(""); +} + static int print_test_result(struct test_suite *t, int curr_suite, int cur= r_test_case, - int result, int width, int running) + int result, int width, int running, + const char *err_output, double elapsed) { if (test_suite__num_test_cases(t) > 1) { char prefix[32]; @@ -421,6 +453,34 @@ static int print_test_result(struct test_suite *t, int= curr_suite, int curr_test break; } =20 + if (junit_filename && result !=3D TEST_RUNNING) { + const char *classname =3D t->desc; + const char *testname =3D test_description(t, curr_test_case); + char *escaped_err =3D xml_escape(err_output); + char *escaped_class =3D xml_escape(classname); + char *escaped_test =3D xml_escape(testname); + + strbuf_addf(&junit_xml_buf, + " \n", + escaped_class, escaped_test, elapsed); + if (result !=3D TEST_OK && result !=3D TEST_SKIP) { + strbuf_addf(&junit_xml_buf, + " \n%s\n \n", + escaped_err); + } else if (result =3D=3D TEST_SKIP) { + const char *reason =3D skip_reason(t, curr_test_case); + char *escaped_reason =3D xml_escape(reason ? reason : "Skip"); + + 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); + } + return 0; } =20 @@ -623,6 +683,8 @@ static void finish_test(struct child_test **child_tests= , int running_test, int c struct strbuf err_output =3D STRBUF_INIT; int last_running =3D -1; int ret; + struct timespec end_time; + double elapsed; =20 if (child_test =3D=3D NULL) { /* Test wasn't started. */ @@ -676,7 +738,7 @@ static void finish_test(struct child_test **child_tests= , int running_test, int c fprintf(debug_file(), PERF_COLOR_DELETE_LINE); } print_test_result(t, curr_suite, curr_test_case, TEST_RUNNING, - width, running); + width, running, NULL, 0.0); last_running =3D running; } } @@ -736,9 +798,14 @@ static void finish_test(struct child_test **child_test= s, int running_test, int c else if (verbose =3D=3D 1 && ret =3D=3D TEST_FAIL) print_test_failure_snippet(stderr, err_output.buf); =20 + clock_gettime(CLOCK_MONOTONIC, &end_time); + elapsed =3D (end_time.tv_sec - child_test->start_time.tv_sec) + + (end_time.tv_nsec - child_test->start_time.tv_nsec) / 1000000000.0; + + print_test_result(t, curr_suite, curr_test_case, ret, width, /*running=3D= */0, + err_output.buf, elapsed); strbuf_release(&err_output); strbuf_release(&child_test->err_output); - print_test_result(t, curr_suite, curr_test_case, ret, width, /*running=3D= */0); if (err > 0) close(err); zfree(&child_tests[running_test]); @@ -906,7 +973,7 @@ static int finish_tests_parallel(struct child_test **ch= ild_tests, size_t num_tes } print_test_result(next_child->test, next_child->suite_num, next_child->test_case_num, TEST_RUNNING, width, - running_count); + running_count, NULL, 0.0); } last_running =3D running_count; } @@ -941,12 +1008,14 @@ static int finish_tests_parallel(struct child_test *= *child_tests, size_t num_tes } child->result =3D finish_command(&child->process); child->process.pid =3D 0; + clock_gettime(CLOCK_MONOTONIC, &child->end_time); child->done =3D true; } } =20 while (next_to_print < num_tests) { struct child_test *child =3D child_tests[next_to_print]; + double elapsed; =20 if (!child) { next_to_print++; @@ -984,8 +1053,12 @@ static int finish_tests_parallel(struct child_test **= child_tests, size_t num_tes else if (verbose =3D=3D 1 && child->result =3D=3D TEST_FAIL) print_test_failure_snippet(stderr, child->err_output.buf); =20 + elapsed =3D (child->end_time.tv_sec - child->start_time.tv_sec) + + (child->end_time.tv_nsec - + child->start_time.tv_nsec) / 1000000000.0; + print_test_result(child->test, child->suite_num, child->test_case_num, - child->result, width, 0); + child->result, width, 0, child->err_output.buf, elapsed); strbuf_release(&child->err_output); child_tests[next_to_print] =3D NULL; zfree(&child); @@ -1008,11 +1081,18 @@ static int start_test(struct test_suite *test, int = curr_suite, int curr_test_cas *child =3D NULL; if (dont_fork) { if (pass =3D=3D 1) { + struct timespec start_time, end_time; + double elapsed; + + clock_gettime(CLOCK_MONOTONIC, &start_time); pr_debug("--- start ---\n"); err =3D test_function(test, curr_test_case)(test, curr_test_case); pr_debug("---- end ----\n"); + clock_gettime(CLOCK_MONOTONIC, &end_time); + elapsed =3D (end_time.tv_sec - start_time.tv_sec) + + (end_time.tv_nsec - start_time.tv_nsec) / 1000000000.0; print_test_result(test, curr_suite, curr_test_case, err, width, - /*running=3D*/0); + /*running=3D*/0, NULL, elapsed); } return 0; } @@ -1042,6 +1122,7 @@ static int start_test(struct test_suite *test, int cu= rr_suite, int curr_test_cas (*child)->process.err =3D -1; } (*child)->process.no_exec_cmd =3D run_test_child; + clock_gettime(CLOCK_MONOTONIC, &(*child)->start_time); if (sequential || pass =3D=3D 2) { err =3D start_command(&(*child)->process); if (err) @@ -1077,6 +1158,41 @@ static void print_tests_summary(void) } else { color_fprintf(stderr, PERF_COLOR_GREEN, "Failed tests : 0\n"); } + + if (junit_filename) { + int fd; + FILE *fp; + + fd =3D open(junit_filename, O_CREAT | O_TRUNC | O_WRONLY | O_NOFOLLOW, 0= 644); + if (fd >=3D 0) { + fp =3D fdopen(fd, "w"); + if (fp) { + unsigned int total =3D summary_tests_passed + + summary_subtests_passed + + summary_tests_skipped + + summary_tests_failed; + fprintf(fp, "\n"); + fprintf(fp, "\n"); + fprintf(fp, + " \n", + total, summary_tests_failed, + 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)); + } + } + strbuf_release(&junit_xml_buf); strbuf_release(&summary_failed_tests_buf); } =20 @@ -1163,6 +1279,25 @@ static int __cmd_test(struct test_suite **suites, in= t argc, const char *argv[], color_fprintf(stderr, PERF_COLOR_YELLOW, " Skip (user override)\n"); summary_tests_skipped++; + if (junit_filename) { + char *escaped_class =3D + xml_escape((const char *) + test_description(*t, -1)); + char *escaped_test =3D xml_escape("override"); + char *escaped_reason =3D + 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); + } } continue; } @@ -1342,6 +1477,8 @@ int cmd_test(int argc, const char **argv) "objdump binary to use for disassembly and annotations"), OPT_UINTEGER(0, "failure-snippet-lines", &failure_snippet_lines, "Number of lines to include in failure snippet, default 10"), + OPT_STRING_OPTARG('j', "junit", &junit_filename, "file", + "Generate junit XML output, default test.xml", "test.xml"), OPT_END() }; const char * const test_subcommands[] =3D { "list", NULL }; --=20 2.54.0.929.g9b7fa37559-goog