Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests/bpf: Add --json-summary option to test_progs

Currently, test_progs outputs all stdout/stderr as it runs, and when it
is done, prints a summary.

It is non-trivial for tooling to parse that output and extract meaningful
information from it.

This change adds a new option, `--json-summary`/`-J` that let the caller
specify a file where `test_progs{,-no_alu32}` can write a summary of the
run in a json format that can later be parsed by tooling.

Currently, it creates a summary section with successes/skipped/failures
followed by a list of failed tests and subtests.

A test contains the following fields:
- name: the name of the test
- number: the number of the test
- message: the log message that was printed by the test.
- failed: A boolean indicating whether the test failed or not. Currently
we only output failed tests, but in the future, successful tests could
be added.
- subtests: A list of subtests associated with this test.

A subtest contains the following fields:
- name: same as above
- number: sanme as above
- message: the log message that was printed by the subtest.
- failed: same as above but for the subtest

An example run and json content below:
```
$ sudo ./test_progs -a $(grep -v '^#' ./DENYLIST.aarch64 | awk '{print
$1","}' | tr -d '\n') -j -J /tmp/test_progs.json
$ jq < /tmp/test_progs.json | head -n 30
{
"success": 29,
"success_subtest": 23,
"skipped": 3,
"failed": 28,
"results": [
{
"name": "bpf_cookie",
"number": 10,
"message": "test_bpf_cookie:PASS:skel_open 0 nsec\n",
"failed": true,
"subtests": [
{
"name": "multi_kprobe_link_api",
"number": 2,
"message": "kprobe_multi_link_api_subtest:PASS:load_kallsyms 0 nsec\nlibbpf: extern 'bpf_testmod_fentry_test1' (strong): not resolved\nlibbpf: failed to load object 'kprobe_multi'\nlibbpf: failed to load BPF skeleton 'kprobe_multi': -3\nkprobe_multi_link_api_subtest:FAIL:fentry_raw_skel_load unexpected error: -3\n",
"failed": true
},
{
"name": "multi_kprobe_attach_api",
"number": 3,
"message": "libbpf: extern 'bpf_testmod_fentry_test1' (strong): not resolved\nlibbpf: failed to load object 'kprobe_multi'\nlibbpf: failed to load BPF skeleton 'kprobe_multi': -3\nkprobe_multi_attach_api_subtest:FAIL:fentry_raw_skel_load unexpected error: -3\n",
"failed": true
},
{
"name": "lsm",
"number": 8,
"message": "lsm_subtest:PASS:lsm.link_create 0 nsec\nlsm_subtest:FAIL:stack_mprotect unexpected stack_mprotect: actual 0 != expected -1\n",
"failed": true
}
```

The file can then be used to print a summary of the test run and list of
failing tests/subtests:

```
$ jq -r < /tmp/test_progs.json '"Success: \(.success)/\(.success_subtest), Skipped: \(.skipped), Failed: \(.failed)"'

Success: 29/23, Skipped: 3, Failed: 28
$ jq -r < /tmp/test_progs.json '.results | map([
if .failed then "#\(.number) \(.name)" else empty end,
(
. as {name: $tname, number: $tnum} | .subtests | map(
if .failed then "#\($tnum)/\(.number) \($tname)/\(.name)" else empty end
)
)
]) | flatten | .[]' | head -n 20
#10 bpf_cookie
#10/2 bpf_cookie/multi_kprobe_link_api
#10/3 bpf_cookie/multi_kprobe_attach_api
#10/8 bpf_cookie/lsm
#15 bpf_mod_race
#15/1 bpf_mod_race/ksym (used_btfs UAF)
#15/2 bpf_mod_race/kfunc (kfunc_btf_tab UAF)
#36 cgroup_hierarchical_stats
#61 deny_namespace
#61/1 deny_namespace/unpriv_userns_create_no_bpf
#73 fexit_stress
#83 get_func_ip_test
#99 kfunc_dynptr_param
#99/1 kfunc_dynptr_param/dynptr_data_null
#99/4 kfunc_dynptr_param/dynptr_data_null
#100 kprobe_multi_bench_attach
#100/1 kprobe_multi_bench_attach/kernel
#100/2 kprobe_multi_bench_attach/modules
#101 kprobe_multi_test
#101/1 kprobe_multi_test/skel_api
```

Signed-off-by: Manu Bretelle <chantr4@gmail.com>
Signed-off-by: Andrii Nakryiko <andrii@kernel.org>
Link: https://lore.kernel.org/bpf/20230317163256.3809328-1-chantr4@gmail.com

authored by

Manu Bretelle and committed by
Andrii Nakryiko
2be7aa76 6cae5a71

+82 -6
+3 -1
tools/testing/selftests/bpf/Makefile
··· 234 234 CGROUP_HELPERS := $(OUTPUT)/cgroup_helpers.o 235 235 TESTING_HELPERS := $(OUTPUT)/testing_helpers.o 236 236 TRACE_HELPERS := $(OUTPUT)/trace_helpers.o 237 + JSON_WRITER := $(OUTPUT)/json_writer.o 237 238 CAP_HELPERS := $(OUTPUT)/cap_helpers.o 238 239 239 240 $(OUTPUT)/test_dev_cgroup: $(CGROUP_HELPERS) $(TESTING_HELPERS) ··· 560 559 TRUNNER_EXTRA_SOURCES := test_progs.c cgroup_helpers.c trace_helpers.c \ 561 560 network_helpers.c testing_helpers.c \ 562 561 btf_helpers.c flow_dissector_load.h \ 563 - cap_helpers.c test_loader.c xsk.c disasm.c 562 + cap_helpers.c test_loader.c xsk.c disasm.c \ 563 + json_writer.c 564 564 TRUNNER_EXTRA_FILES := $(OUTPUT)/urandom_read $(OUTPUT)/bpf_testmod.ko \ 565 565 $(OUTPUT)/liburandom_read.so \ 566 566 $(OUTPUT)/xdp_synproxy \
+78 -5
tools/testing/selftests/bpf/test_progs.c
··· 18 18 #include <sys/socket.h> 19 19 #include <sys/un.h> 20 20 #include <bpf/btf.h> 21 + #include "json_writer.h" 21 22 22 23 static bool verbose(void) 23 24 { ··· 270 269 fprintf(env.stdout, "\n"); 271 270 } 272 271 272 + static void jsonw_write_log_message(json_writer_t *w, char *log_buf, size_t log_cnt) 273 + { 274 + /* open_memstream (from stdio_hijack_init) ensures that log_bug is terminated by a 275 + * null byte. Yet in parallel mode, log_buf will be NULL if there is no message. 276 + */ 277 + if (log_cnt) { 278 + jsonw_string_field(w, "message", log_buf); 279 + } else { 280 + jsonw_string_field(w, "message", ""); 281 + } 282 + } 283 + 273 284 static void dump_test_log(const struct prog_test_def *test, 274 285 const struct test_state *test_state, 275 286 bool skip_ok_subtests, 276 - bool par_exec_result) 287 + bool par_exec_result, 288 + json_writer_t *w) 277 289 { 278 290 bool test_failed = test_state->error_cnt > 0; 279 291 bool force_log = test_state->force_log; ··· 310 296 if (test_state->log_cnt && print_test) 311 297 print_test_log(test_state->log_buf, test_state->log_cnt); 312 298 299 + if (w && print_test) { 300 + jsonw_start_object(w); 301 + jsonw_string_field(w, "name", test->test_name); 302 + jsonw_uint_field(w, "number", test->test_num); 303 + jsonw_write_log_message(w, test_state->log_buf, test_state->log_cnt); 304 + jsonw_bool_field(w, "failed", test_failed); 305 + jsonw_name(w, "subtests"); 306 + jsonw_start_array(w); 307 + } 308 + 313 309 for (i = 0; i < test_state->subtest_num; i++) { 314 310 subtest_state = &test_state->subtest_states[i]; 315 311 subtest_failed = subtest_state->error_cnt; ··· 338 314 test->test_name, subtest_state->name, 339 315 test_result(subtest_state->error_cnt, 340 316 subtest_state->skipped)); 317 + 318 + if (w && print_subtest) { 319 + jsonw_start_object(w); 320 + jsonw_string_field(w, "name", subtest_state->name); 321 + jsonw_uint_field(w, "number", i+1); 322 + jsonw_write_log_message(w, subtest_state->log_buf, subtest_state->log_cnt); 323 + jsonw_bool_field(w, "failed", subtest_failed); 324 + jsonw_end_object(w); 325 + } 326 + } 327 + 328 + if (w && print_test) { 329 + jsonw_end_array(w); 330 + jsonw_end_object(w); 341 331 } 342 332 343 333 print_test_result(test, test_state); ··· 753 715 ARG_TEST_NAME_GLOB_DENYLIST = 'd', 754 716 ARG_NUM_WORKERS = 'j', 755 717 ARG_DEBUG = -1, 718 + ARG_JSON_SUMMARY = 'J' 756 719 }; 757 720 758 721 static const struct argp_option opts[] = { ··· 779 740 "Number of workers to run in parallel, default to number of cpus." }, 780 741 { "debug", ARG_DEBUG, NULL, 0, 781 742 "print extra debug information for test_progs." }, 743 + { "json-summary", ARG_JSON_SUMMARY, "FILE", 0, "Write report in json format to this file."}, 782 744 {}, 783 745 }; 784 746 ··· 909 869 break; 910 870 case ARG_DEBUG: 911 871 env->debug = true; 872 + break; 873 + case ARG_JSON_SUMMARY: 874 + env->json = fopen(arg, "w"); 875 + if (env->json == NULL) { 876 + perror("Failed to open json summary file"); 877 + return -errno; 878 + } 912 879 break; 913 880 case ARGP_KEY_ARG: 914 881 argp_usage(state); ··· 1064 1017 stdio_restore(); 1065 1018 if (env.test) { 1066 1019 env.test_state->error_cnt++; 1067 - dump_test_log(env.test, env.test_state, true, false); 1020 + dump_test_log(env.test, env.test_state, true, false, NULL); 1068 1021 } 1069 1022 if (env.worker_id != -1) 1070 1023 fprintf(stderr, "[%d]: ", env.worker_id); ··· 1171 1124 1172 1125 stdio_restore(); 1173 1126 1174 - dump_test_log(test, state, false, false); 1127 + dump_test_log(test, state, false, false, NULL); 1175 1128 } 1176 1129 1177 1130 struct dispatch_data { ··· 1330 1283 } while (false); 1331 1284 1332 1285 pthread_mutex_lock(&stdout_output_lock); 1333 - dump_test_log(test, state, false, true); 1286 + dump_test_log(test, state, false, true, NULL); 1334 1287 pthread_mutex_unlock(&stdout_output_lock); 1335 1288 } /* while (true) */ 1336 1289 error: ··· 1355 1308 { 1356 1309 int i; 1357 1310 int succ_cnt = 0, fail_cnt = 0, sub_succ_cnt = 0, skip_cnt = 0; 1311 + json_writer_t *w = NULL; 1358 1312 1359 1313 for (i = 0; i < prog_test_cnt; i++) { 1360 1314 struct test_state *state = &test_states[i]; ··· 1370 1322 fail_cnt++; 1371 1323 else 1372 1324 succ_cnt++; 1325 + } 1326 + 1327 + if (env->json) { 1328 + w = jsonw_new(env->json); 1329 + if (!w) 1330 + fprintf(env->stderr, "Failed to create new JSON stream."); 1331 + } 1332 + 1333 + if (w) { 1334 + jsonw_start_object(w); 1335 + jsonw_uint_field(w, "success", succ_cnt); 1336 + jsonw_uint_field(w, "success_subtest", sub_succ_cnt); 1337 + jsonw_uint_field(w, "skipped", skip_cnt); 1338 + jsonw_uint_field(w, "failed", fail_cnt); 1339 + jsonw_name(w, "results"); 1340 + jsonw_start_array(w); 1373 1341 } 1374 1342 1375 1343 /* ··· 1404 1340 if (!state->tested || !state->error_cnt) 1405 1341 continue; 1406 1342 1407 - dump_test_log(test, state, true, true); 1343 + dump_test_log(test, state, true, true, w); 1408 1344 } 1409 1345 } 1346 + 1347 + if (w) { 1348 + jsonw_end_array(w); 1349 + jsonw_end_object(w); 1350 + jsonw_destroy(&w); 1351 + } 1352 + 1353 + if (env->json) 1354 + fclose(env->json); 1410 1355 1411 1356 printf("Summary: %d/%d PASSED, %d SKIPPED, %d FAILED\n", 1412 1357 succ_cnt, sub_succ_cnt, skip_cnt, fail_cnt);
+1
tools/testing/selftests/bpf/test_progs.h
··· 114 114 FILE *stdout; 115 115 FILE *stderr; 116 116 int nr_cpus; 117 + FILE *json; 117 118 118 119 int succ_cnt; /* successful tests */ 119 120 int sub_succ_cnt; /* successful sub-tests */