From: Lennart Poettering Date: Tue, 30 Jun 2026 16:56:49 +0000 (+0200) Subject: test: add test case for show_menu() X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=bd13921c782c5e37a6e08a7d4e4e493cec54a0e4;p=thirdparty%2Fsystemd.git test: add test case for show_menu() --- diff --git a/src/test/test-terminal-util.c b/src/test/test-terminal-util.c index 8ccde2341c8..909dda73a99 100644 --- a/src/test/test-terminal-util.c +++ b/src/test/test-terminal-util.c @@ -9,11 +9,15 @@ #include "alloc-util.h" #include "ansi-color.h" +#include "env-util.h" #include "errno-util.h" #include "fd-util.h" +#include "fileio.h" +#include "memfd-util.h" #include "path-util.h" #include "process-util.h" #include "stat-util.h" +#include "string-util.h" #include "strv.h" #include "terminal-util.h" #include "tests.h" @@ -417,4 +421,157 @@ TEST(terminal_new_session) { } } +static void show_menu_capture( + char **menu, + size_t n_columns, + size_t column_width, + const char *grey_prefix, + bool with_numbers, + const char *columns_env, + char **ret) { + + int r; + + /* Runs show_menu() in a forked child whose stdout is connected to a memfd, so we can capture its + * output verbatim and byte-compare it. The child sets $COLUMNS explicitly so we can exercise + * show_menu() at various terminal widths regardless of the actual terminal the test runs on. */ + + _cleanup_close_ int mfd = ASSERT_OK(memfd_new("test-show-menu")); + + r = pidref_safe_fork_full( + "test-show-menu", + (int[]) { -EBADF, mfd, STDERR_FILENO }, + /* except_fds= */ NULL, /* n_except_fds= */ 0, + FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGKILL|FORK_LOG|FORK_WAIT|FORK_REARRANGE_STDIO|FORK_FLUSH_STDIO, + /* ret= */ NULL); + ASSERT_OK(r); + if (r == 0) { + /* Child: stdout is now the memfd. */ + + ASSERT_OK(set_unset_env("COLUMNS", columns_env, /* overwrite= */ true)); + + /* Pin $LINES to a large value so the "press any key to proceed" pager (which would block on + * stdin) is never reached, and turn off colors so the captured output stays free of ANSI + * escape sequences. */ + ASSERT_OK_ERRNO(setenv("LINES", "1000", /* overwrite= */ true)); + ASSERT_OK_ERRNO(setenv("SYSTEMD_COLORS", "0", /* overwrite= */ true)); + reset_terminal_feature_caches(); + + ASSERT_OK(show_menu(menu, n_columns, column_width, /* ellipsize_percentage= */ 50, grey_prefix, with_numbers)); + ASSERT_OK_ZERO_ERRNO(fflush(stdout)); + + _exit(EXIT_SUCCESS); + } + + /* Parent: read back whatever the child wrote. The memfd's file offset is shared with the child's + * dup'd fd, so rewind before reading. */ + _cleanup_fclose_ FILE *f = ASSERT_NOT_NULL(fdopen(TAKE_FD(mfd), "re")); + rewind(f); + + _cleanup_free_ char *content = NULL; + ASSERT_OK(read_full_stream(f, &content, /* ret_size= */ NULL)); + + log_info("=== show_menu(n_columns=%zu, column_width=%zu, grey_prefix=%s, with_numbers=%s, COLUMNS=%s) ===\n%s", + n_columns, column_width, strnull(grey_prefix), yes_no(with_numbers), strnull(columns_env), content); + + if (ret) + *ret = TAKE_PTR(content); +} + +TEST(show_menu) { + char **menu = STRV_MAKE("alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf"); + + /* NULL list: must not crash (the assert(x) was dropped) and must produce no output at all. */ + FOREACH_STRING(cols, "200", "80", "10", "1") { + _cleanup_free_ char *content = NULL; + show_menu_capture(/* menu= */ NULL, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ cols, &content); + ASSERT_STREQ(content, ""); + } + + /* Empty (but non-NULL) list: also no output. */ + { + _cleanup_free_ char *content = NULL; + show_menu_capture(/* menu= */ STRV_EMPTY, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ false, /* columns_env= */ "80", &content); + ASSERT_STREQ(content, ""); + } + + /* The longest entry in 'menu' is 7 cells wide, so the column width is always clamped up to the 10-cell + * minimum, no matter how wide or narrow $COLUMNS is — every width must yield byte-identical output. + * This pins down the multi-column layout (3 columns, column-major numbering 1..7), the "never narrower + * than 10" clamp and the LESS_BY() underflow guard at absurdly tiny widths. */ + static const char menu_expected[] = + " 1) alpha 4) delta 7) golf \n" + " 2) bravo 5) echo \n" + " 3) charlie 6) foxtrot \n"; + + FOREACH_STRING(cols, "200", "80", "40", "20", "10", "5", "1") { + _cleanup_free_ char *content = NULL; + show_menu_capture(menu, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ cols, &content); + ASSERT_STREQ(content, menu_expected); + } + + /* $COLUMNS unset: stdout is a memfd (not a tty), so columns() falls back to its 80-column default, + * which gives the very same layout. */ + { + _cleanup_free_ char *content = NULL; + show_menu_capture(menu, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ NULL, &content); + ASSERT_STREQ(content, menu_expected); + } + + /* Single column, no numbers: one entry per row, each padded to the 10-cell minimum. */ + { + _cleanup_free_ char *content = NULL; + show_menu_capture(menu, /* n_columns= */ 1, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ false, /* columns_env= */ "200", &content); + ASSERT_STREQ(content, + "alpha \n" + "bravo \n" + "charlie \n" + "delta \n" + "echo \n" + "foxtrot \n" + "golf \n"); + } + + /* A menu whose longest entry (14 cells) exceeds the 10-cell minimum, so the column width now tracks + * the content (widest+1 == 15). At a wide terminal it stays a 3-column grid... */ + char **menu2 = STRV_MAKE("fourteen-chars", "short", "two"); + { + _cleanup_free_ char *content = NULL; + show_menu_capture(menu2, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ "200", &content); + ASSERT_STREQ(content, + " 1) fourteen-chars 2) short 3) two \n"); + } + + /* ...but at a narrow terminal show_menu() falls back to a single linear column (n_columns forced to + * 1), still wide enough to print every entry in full. */ + { + _cleanup_free_ char *content = NULL; + show_menu_capture(menu2, /* n_columns= */ 3, /* column_width= */ SIZE_MAX, + /* grey_prefix= */ NULL, /* with_numbers= */ true, /* columns_env= */ "30", &content); + ASSERT_STREQ(content, + " 1) fourteen-chars \n" + " 2) short \n" + " 3) two \n"); + } + + /* Explicit column width (the COLUMNS-derived path is skipped entirely) together with a grey prefix: + * matching entries get the prefix printed via the grey branch, non-matching ones don't. The captured + * bytes are identical either way since colors are disabled. */ + char **prefixed = STRV_MAKE("net.foo", "net.bar", "other", "net.baz"); + { + _cleanup_free_ char *content = NULL; + show_menu_capture(prefixed, /* n_columns= */ 2, /* column_width= */ 20, + /* grey_prefix= */ "net.", /* with_numbers= */ true, /* columns_env= */ "200", &content); + ASSERT_STREQ(content, + " 1) net.foo 3) other \n" + " 2) net.bar 4) net.baz \n"); + } +} + DEFINE_TEST_MAIN(LOG_INFO);