selftests/bpf: Compare btf_dump expected output in-process

test_btf_dump_case() writes the dumped types to a temporary file and
then runs a command against the expectation:

	system("awk '...' 'progs/x.c' | diff -u - '/tmp/x.output.XXXXXX'");

This creates three processes per test case: a shell, awk and diff. The
awk cannot be deferred to the failure path, because its output is the
expected text.

In a VM with a 9p root (used on BPF CI) process creation dominates the
cost. A bare fork+exec measures around 110ms, so the btf_dump test
cases spend most of their runtime forking.

Replace the awk pipeline with a simple marker parser. Buffer the dump
in memory, and use compare_text_to_expected(), which only runs diff(1)
on mismatch.

Measured with "time ./test_progs -t btf_dump" in the VM:

	real 2.751s -> 0.702s, sys 1.901s -> 0.193s

This also drops the temporary file setup.

Signed-off-by: Ihor Solodrai <ihor.solodrai@linux.dev>
Link: https://lore.kernel.org/r/20260828215207.3105313-4-ihor.solodrai@linux.dev
Signed-off-by: Alexei Starovoitov <ast@kernel.org>
This commit is contained in:
Ihor Solodrai 2026-08-28 14:52:04 -07:00 committed by Alexei Starovoitov
parent c0722dc328
commit 122255fb19

View File

@ -1,6 +1,7 @@
// SPDX-License-Identifier: GPL-2.0
#include <test_progs.h>
#include <bpf/btf.h>
#include "testing_helpers.h"
void btf_dump_printf(void *ctx, const char *fmt, va_list args)
{
@ -43,12 +44,75 @@ static int btf_dump_all_types(const struct btf *btf, void *ctx)
return err;
}
/*
* Expected output is embedded in the test case source, between
* START-EXPECTED-OUTPUT and END-EXPECTED-OUTPUT markers. A region is either
* plain C, where a declaration doubles as its own expectation, or C wrapped in
* a block comment, where the rendered form differs from the source. All
* regions of a file concatenate into one expectation, compared against one
* whole-file dump.
*
* Returns a malloc'd buffer for the caller to free, or NULL on failure.
*/
static char *read_expected_output(const char *path)
{
size_t out_sz = 0, line_cap = 0;
char *out = NULL, *line = NULL;
bool in_region = false;
FILE *f, *out_file;
f = fopen(path, "r");
if (!f)
return NULL;
out_file = open_memstream(&out, &out_sz);
if (!out_file) {
fclose(f);
return NULL;
}
while (getline(&line, &line_cap, f) > 0) {
const char *p;
if (strstr(line, "START-EXPECTED-OUTPUT")) {
in_region = true;
continue;
}
if (strstr(line, "END-EXPECTED-OUTPUT"))
in_region = false;
if (!in_region)
continue;
p = line + strspn(line, " \t");
/* opening or closing line of a commented out region */
if (!strncmp(p, "/*", 2) || !strncmp(p, "*/", 2))
continue;
/*
* Only a '*' directly after the indentation is a comment
* prefix. Without one the line is taken as it is, leading
* whitespace included.
*/
p = *p == '*' ? p + 1 : line;
fputs(p, out_file);
}
free(line);
fclose(f);
fclose(out_file);
return out;
}
static int test_btf_dump_case(int n, struct btf_dump_test_case *t)
{
char test_file[256], out_file[256], diff_cmd[1024];
char *dump = NULL, *expected = NULL;
struct btf *btf = NULL;
int err = 0, fd = -1;
FILE *f = NULL;
char test_file[256];
size_t dump_sz = 0;
int err = 0;
FILE *f;
snprintf(test_file, sizeof(test_file), "%s.bpf.o", t->file);
@ -72,21 +136,14 @@ static int test_btf_dump_case(int n, struct btf_dump_test_case *t)
ASSERT_EQ(ptr_sz, (size_t)8, "ptr_sz");
}
snprintf(out_file, sizeof(out_file), "/tmp/%s.output.XXXXXX", t->file);
fd = mkstemp(out_file);
if (!ASSERT_GE(fd, 0, "create_tmp")) {
err = fd;
goto done;
}
f = fdopen(fd, "w");
if (!ASSERT_OK_PTR(f, "open_tmp")) {
close(fd);
f = open_memstream(&dump, &dump_sz);
if (!ASSERT_OK_PTR(f, "open_memstream")) {
err = -errno;
goto done;
}
err = btf_dump_all_types(btf, f);
fclose(f);
close(fd);
if (!ASSERT_OK(err, "btf_dump"))
goto done;
@ -97,28 +154,23 @@ static int test_btf_dump_case(int n, struct btf_dump_test_case *t)
* without preserving the directory structure.
*/
snprintf(test_file, sizeof(test_file), "%s.c", t->file);
/*
* Diff test output and expected test output, contained between
* START-EXPECTED-OUTPUT and END-EXPECTED-OUTPUT lines in test case.
* For expected output lines, everything before '*' is stripped out.
* Also lines containing comment start and comment end markers are
* ignored.
*/
snprintf(diff_cmd, sizeof(diff_cmd),
"awk '/START-EXPECTED-OUTPUT/{out=1;next} "
"/END-EXPECTED-OUTPUT/{out=0} "
"/\\/\\*|\\*\\//{next} " /* ignore comment start/end lines */
"out {sub(/^[ \\t]*\\*/, \"\"); print}' '%s' | diff -u - '%s'",
test_file, out_file);
err = system(diff_cmd);
if (!ASSERT_OK(err, "diff")) {
fprintf(stdout, "output=%s, diff cmd:\n%s\n", out_file, diff_cmd);
expected = read_expected_output(test_file);
if (!ASSERT_OK_PTR(expected, "read_expected_output")) {
err = -errno;
goto done;
}
remove(out_file);
/*
* The mismatch has already been reported, so this only has to
* register the failure. ASSERT_OK() would append a stale errno to it.
*/
err = compare_text_to_expected(dump, expected);
ASSERT_EQ(err, 0, "compare_text_to_expected");
done:
free(expected);
free(dump);
btf__free(btf);
return err;
}