From 8f008cf6570c7f31dfeaeb8b8b6aed2b2961e7ad Mon Sep 17 00:00:00 2001 From: Giovanni Alzetta Date: Wed, 9 Sep 2026 22:18:27 +0200 Subject: [PATCH] Ticket #4294: don't block mc's startup on the subshell's startup files init_subshell() used to fork the shell and then wait for it to source its startup files and report its first working directory, all before mc's panels were even created. With a heavy zsh setup (oh-my-zsh and friends) this delays the first screen by up to several seconds, and is what remains of ticket #4625 after the deadlock fixes. Now init_subshell() only forks the shell, injects the init string and registers the CWD pipe as a select channel. The rest of the handshake (reading the CWD, probing the persistent command buffer, forcing the initial cd) runs from that channel's callback once the shell is ready. The panels appear right away with the default prompt; the real one shows up when the handshake is done, through the existing load_prompt() channel on the pty. What changes in the event flow: - before: init_subshell() blocks in feed_subshell() -> panels are created -> main loop, where the pty channel reads prompts. - after: init_subshell() returns -> panels are created -> main loop, where the CWD pipe channel completes the handshake and removes itself. Until then the pty channel drains the shell's startup output, exactly like feed_subshell(QUIETLY) used to. What can happen while the handshake is still pending, and how it is handled: - Ctrl-O or running a command: invoke_subshell() completes the handshake synchronously first, i.e. it blocks like before, with the same 10 second limit. - the user changes directory in the panels: subshell_chdir() is ignored until the handshake is done. The forced initial cd targets the panel's directory instead of the shell's own one, so the shell catches up. - the shell dies (exec failure, "exit" in a startup file): sigchld_handler() removes both select channels and turns the subshell off, as the blocking code did after feed_subshell() failed. - the shell exits later and is restarted (invoke_subshell(), command.c, execute.c): init_subshell() resets subshell_initialized, so the new shell goes through the same handshake. - select channels are disabled only around do_executev() and toggle_subshell(), both of which go through invoke_subshell(), so the shell is never left in its self-SIGSTOP while nobody is listening. - the ordering of the persistent buffer probe vs. the first prompt (see ticket #4625) is untouched: the handshake still runs as one piece, only later. tests/src/subshell exercises the handshake completion, the synchronous fallback, chdir and shell death before and after the handshake, against a fake pty and CWD pipe. Signed-off-by: Giovanni Alzetta --- configure.ac | 1 + src/subshell/common.c | 79 +++- tests/src/Makefile.am | 4 + tests/src/subshell/Makefile.am | 30 ++ tests/src/subshell/subshell__async_init.c | 401 ++++++++++++++++++ tests/src/subshell/subshell__common.c | 109 +++++ .../subshell__peek_subshell_switch_key.c | 102 +++++ 7 files changed, 716 insertions(+), 10 deletions(-) create mode 100644 tests/src/subshell/Makefile.am create mode 100644 tests/src/subshell/subshell__async_init.c create mode 100644 tests/src/subshell/subshell__common.c create mode 100644 tests/src/subshell/subshell__peek_subshell_switch_key.c diff --git a/configure.ac b/configure.ac index 755e4a75e3..04fc3fe7b5 100644 --- a/configure.ac +++ b/configure.ac @@ -727,6 +727,7 @@ tests/lib/vfs/mc.charsets tests/lib/widget/Makefile tests/src/Makefile tests/src/filemanager/Makefile +tests/src/subshell/Makefile tests/src/editor/Makefile tests/src/editor/edit_complete_word_cmd_test_data.txt tests/src/vfs/Makefile diff --git a/src/subshell/common.c b/src/subshell/common.c index 8aab520450..340be6dc1a 100644 --- a/src/subshell/common.c +++ b/src/subshell/common.c @@ -180,7 +180,9 @@ typedef enum * the current command buffer and the location of the cursor. */ #define SHELL_BUFFER_KEYBINDING "_" -/*** forward declarations (file scope functions) *************************************************/ +/*** file scope functions ************************************************************************/ + +static int subshell_init_ready (int fd, void *info); /*** file scope variables ************************************************************************/ @@ -1702,8 +1704,6 @@ do_subshell_chdir (const vfs_path_t *vpath, gboolean force, gboolean update_prom void init_subshell (void) { - vfs_path_t *vfs_subshell_cwd; - // This must be remembered across calls to init_subshell() static char pty_name[BUF_SMALL]; @@ -1823,7 +1823,19 @@ init_subshell (void) g_free (precmd); } - // Wait until the subshell has started up and processed the command + // Finish the handshake asynchronously from the main loop once the shell is ready + subshell_initialized = FALSE; + add_select_channel (subshell_pipe[READ], subshell_init_ready, NULL); +} + +/* --------------------------------------------------------------------------------------------- */ +// Second half of init_subshell(): consume the shell's first CWD report, probe the persistent +// command buffer and get a fresh prompt printed. Runs from the main loop as soon as the shell +// is ready, or right away from invoke_subshell() if the user got there first. + +static void +subshell_finish_init (void) +{ subshell_state = RUNNING_COMMAND; tty_enable_interrupt_key (); if (!feed_subshell (QUIETLY, TRUE)) @@ -1841,19 +1853,63 @@ init_subshell (void) /* Force an initial `cd` command, even if the subshell is already in the target directory. * Testing the persistent command feature might have read and discarded the prompt. Just get - * a new one printed. See #4784#issuecomment-3435834623. */ - vfs_subshell_cwd = vfs_path_from_str (subshell_cwd); - do_subshell_chdir (vfs_subshell_cwd, TRUE, FALSE); - vfs_path_free (vfs_subshell_cwd, TRUE); + * a new one printed. See #4784#issuecomment-3435834623. + * + * The panel may have moved on while the shell was starting up (subshell_chdir() is a no-op + * until we get here), so aim at the panel's directory rather than the shell's. */ + do_subshell_chdir (subshell_get_cwd (), TRUE, FALSE); subshell_initialized = TRUE; } +/* --------------------------------------------------------------------------------------------- */ +// Remove both of the subshell's file descriptors from the main event loop; safe even if +// unregistered. + +static void +subshell_close_select_channels (void) +{ + delete_select_channel (mc_global.tty.subshell_pty); + delete_select_channel (subshell_pipe[READ]); +} + +/* --------------------------------------------------------------------------------------------- */ +// Event-loop callback: fires once the shell has written its CWD, i.e. reached its first prompt. + +static int +subshell_init_ready (int fd, void *info) +{ + (void) info; + + delete_select_channel (fd); + + if (subshell_alive) + subshell_finish_init (); + + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ +// Finish the handshake synchronously if it isn't done yet. Called from +// invoke_subshell(), so Ctrl-O and running a command always get a ready subshell. + +static void +subshell_ensure_initialized (void) +{ + if (!subshell_initialized) + { + delete_select_channel (subshell_pipe[READ]); + subshell_finish_init (); + } +} + /* --------------------------------------------------------------------------------------------- */ int invoke_subshell (const char *command, int how, vfs_path_t **new_dir_vpath) { + subshell_ensure_initialized (); + // Make the MC terminal transparent tcsetattr (STDOUT_FILENO, TCSANOW, &raw_mode); @@ -2099,7 +2155,8 @@ exit_subshell (void) void subshell_chdir (const vfs_path_t *vpath) { - if (mc_global.tty.use_subshell && vfs_current_is_local ()) + // Before the handshake is done, subshell_finish_init() takes care of the panel's directory + if (mc_global.tty.use_subshell && subshell_initialized && vfs_current_is_local ()) do_subshell_chdir (vpath, FALSE, FALSE); } @@ -2149,7 +2206,9 @@ sigchld_handler (MC_UNUSED int sig) { // The subshell has either exited normally or been killed subshell_alive = FALSE; - delete_select_channel (mc_global.tty.subshell_pty); + subshell_close_select_channels (); + if (!subshell_initialized) + mc_global.tty.use_subshell = FALSE; // Subshell died instantly, so don't use it if (WIFEXITED (status) && WEXITSTATUS (status) != FORK_FAILURE) { const int subshell_quit = diff --git a/tests/src/Makefile.am b/tests/src/Makefile.am index abba5ad1f2..5b5d202920 100644 --- a/tests/src/Makefile.am +++ b/tests/src/Makefile.am @@ -6,6 +6,10 @@ if USE_INTERNAL_EDIT SUBDIRS += editor endif +if ENABLE_SUBSHELL +SUBDIRS += subshell +endif + AM_CPPFLAGS = \ $(GLIB_CFLAGS) \ -I$(top_srcdir) \ diff --git a/tests/src/subshell/Makefile.am b/tests/src/subshell/Makefile.am new file mode 100644 index 0000000000..74a2e85f6a --- /dev/null +++ b/tests/src/subshell/Makefile.am @@ -0,0 +1,30 @@ +PACKAGE_STRING = "/src/subshell" + +AM_CPPFLAGS = \ + $(GLIB_CFLAGS) \ + -I$(top_srcdir) \ + -I$(top_srcdir)/lib/vfs \ + @CHECK_CFLAGS@ + +LIBS = @CHECK_LIBS@ \ + $(top_builddir)/src/libinternal.la \ + $(top_builddir)/lib/libmc.la + +if ENABLE_MCLIB +LIBS += $(GLIB_LIBS) +endif + +EXTRA_DIST = \ + subshell__common.c + +TESTS = \ + subshell__peek_subshell_switch_key \ + subshell__async_init + +check_PROGRAMS = $(TESTS) + +subshell__peek_subshell_switch_key_SOURCES = \ + subshell__peek_subshell_switch_key.c + +subshell__async_init_SOURCES = \ + subshell__async_init.c diff --git a/tests/src/subshell/subshell__async_init.c b/tests/src/subshell/subshell__async_init.c new file mode 100644 index 0000000000..bb1c91f969 --- /dev/null +++ b/tests/src/subshell/subshell__async_init.c @@ -0,0 +1,401 @@ +/* + src/subshell - tests for subshell_init_ready() and subshell_ensure_initialized() + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/src/subshell" + +#include "tests/mctest.h" + +#include +#include +#include +#include + +#include "src/vfs/local/local.h" + +#include "subshell__common.c" + +/* --------------------------------------------------------------------------------------------- */ + +static int +delete_select_channel__call_count_for_fd (int fd) +{ + int n = 0; + + for (guint i = 0; i < delete_select_channel__calls->len; i++) + if (g_array_index (delete_select_channel__calls, int, i) == fd) + n++; + + return n; +} + +static void +write_fake_cwd (const char *cwd) +{ + char line[MC_MAXPATHLEN + 2]; + const int n = g_snprintf (line, sizeof (line), "%s\n", cwd); + + write_all (subshell_pipe[WRITE], line, (size_t) n); +} + +static gboolean +fd_readable (int fd) +{ + fd_set read_set; + struct timeval no_wait = { 0 }; + + FD_ZERO (&read_set); + FD_SET (fd, &read_set); + + return select (fd + 1, &read_set, NULL, NULL, &no_wait) > 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +static int fake_pty[2] = { -1, -1 }; +static pid_t fake_shell_pid = -1; +static mc_shell_t test_shell; + +static void +kill_fake_shell (void) +{ + if (fake_shell_pid > 0) + { + kill (fake_shell_pid, SIGKILL); + waitpid (fake_shell_pid, NULL, 0); + fake_shell_pid = -1; + } +} + +/* Let the fake shell die and deliver its SIGCHLD to sigchld_handler() synchronously. */ +static void +kill_fake_shell_and_report (void) +{ + sigset_t chld_set, old_set; + siginfo_t info; + + sigemptyset (&chld_set); + sigaddset (&chld_set, SIGCHLD); + sigprocmask (SIG_BLOCK, &chld_set, &old_set); + + kill (fake_shell_pid, SIGKILL); + waitid (P_PID, (id_t) fake_shell_pid, &info, WEXITED | WNOWAIT); + test_sigchld_handler (SIGCHLD); + fake_shell_pid = -1; + + sigprocmask (SIG_SETMASK, &old_set, NULL); +} + +/* @Before */ +static void +setup (void) +{ + str_init_strings (NULL); + vfs_init (); + vfs_init_localfs (); + vfs_setup_work_dir (); + + // Skip prompt/cmdline widgets + mc_global.mc_run_mode = MC_RUN_VIEWER; + + test_shell.type = SHELL_SH; + test_shell.name = "sh"; + test_shell.path = (char *) "/bin/sh"; + test_shell.real_path = (char *) "/bin/sh"; + mc_global.shell = &test_shell; + + mc_global.tty.use_subshell = TRUE; + subshell_alive = TRUE; + subshell_stopped = FALSE; + subshell_initialized = FALSE; + subshell_state = INACTIVE; + use_persistent_buffer = FALSE; + subshell_cwd[0] = '\0'; + + subshell_pipe[READ] = -1; + subshell_pipe[WRITE] = -1; + + if (socketpair (AF_UNIX, SOCK_STREAM, 0, fake_pty) != 0) + { + fake_pty[0] = fake_pty[1] = -1; + ck_abort_msg ("Cannot create test pty"); + } + // SOCK_DGRAM keeps one CWD report per read(); a stream pipe can merge writes + if (socketpair (AF_UNIX, SOCK_DGRAM, 0, subshell_pipe) != 0) + { + subshell_pipe[READ] = -1; + subshell_pipe[WRITE] = -1; + ck_abort_msg ("Cannot create test pipe"); + } + mc_global.tty.subshell_pty = fake_pty[0]; + + fake_shell_pid = fork (); + if (fake_shell_pid == -1) + ck_abort_msg ("Cannot fork fake shell child"); + if (fake_shell_pid == 0) + { + for (;;) + raise (SIGSTOP); + } + + { + int status; + + // Reap the first SIGSTOP so synchronize() does not depend on catching SIGCHLD + if (waitpid (fake_shell_pid, &status, WUNTRACED) == -1 || !WIFSTOPPED (status)) + { + kill_fake_shell (); + ck_abort_msg ("Fake shell child did not stop"); + } + } + subshell_pid = fake_shell_pid; + subshell_stopped = TRUE; + + { + struct sigaction sa = { 0 }; + + sa.sa_handler = test_sigchld_handler; + sigemptyset (&sa.sa_mask); + sa.sa_flags = SA_RESTART; + sigaction (SIGCHLD, &sa, NULL); + } + + subshell_get_cwd__return_value = vfs_path_from_str ("/tmp"); + + delete_select_channel__calls = g_array_new (FALSE, FALSE, sizeof (int)); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + signal (SIGCHLD, SIG_DFL); + kill_fake_shell (); + + if (delete_select_channel__calls != NULL) + { + g_array_free (delete_select_channel__calls, TRUE); + delete_select_channel__calls = NULL; + } + + vfs_path_free (subshell_get_cwd__return_value, TRUE); + subshell_get_cwd__return_value = NULL; + + if (fake_pty[0] >= 0) + close (fake_pty[0]); + if (fake_pty[1] >= 0) + close (fake_pty[1]); + fake_pty[0] = fake_pty[1] = -1; + + if (subshell_pipe[READ] >= 0) + close (subshell_pipe[READ]); + if (subshell_pipe[WRITE] >= 0) + close (subshell_pipe[WRITE]); + subshell_pipe[READ] = subshell_pipe[WRITE] = -1; + + vfs_shut (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (init_ready_finishes_handshake_when_cwd_available) +{ + // given + write_fake_cwd ("/tmp"); // handshake + write_fake_cwd ("/tmp"); // forced initial cd in subshell_finish_init() + + // when + subshell_init_ready (subshell_pipe[READ], NULL); + + // then + mctest_assert_true (subshell_initialized); + mctest_assert_str_eq (subshell_cwd, "/tmp"); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (init_ready_bails_out_when_subshell_not_alive) +{ + // given + subshell_alive = FALSE; + + // when + subshell_init_ready (subshell_pipe[READ], NULL); + + // then + mctest_assert_false (subshell_initialized); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (ensure_initialized_finishes_pending_init) +{ + // given + write_fake_cwd ("/tmp"); // handshake + write_fake_cwd ("/tmp"); // forced initial cd in subshell_finish_init() + + mctest_assert_false (subshell_initialized); + + // when + subshell_ensure_initialized (); + + // then + mctest_assert_true (subshell_initialized); + mctest_assert_str_eq (subshell_cwd, "/tmp"); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (ensure_initialized_is_a_noop_once_already_initialized) +{ + // given + subshell_initialized = TRUE; + + // when + subshell_ensure_initialized (); + + // then + mctest_assert_true (subshell_initialized); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 0); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (chdir_is_ignored_until_initialized) +{ + // when + test_subshell_chdir (subshell_get_cwd__return_value); + + // then + mctest_assert_false (fd_readable (fake_pty[1])); + mctest_assert_str_eq (subshell_cwd, ""); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (chdir_is_sent_once_initialized) +{ + // given + subshell_initialized = TRUE; + g_strlcpy (subshell_cwd, "/", sizeof (subshell_cwd)); + write_fake_cwd ("/tmp"); // response to the command line clearing + write_fake_cwd ("/tmp"); // response to cd + + // when + test_subshell_chdir (subshell_get_cwd__return_value); + + // then + mctest_assert_true (fd_readable (fake_pty[1])); + mctest_assert_str_eq (subshell_cwd, "/tmp"); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (shell_death_before_handshake_disables_subshell) +{ + // when + kill_fake_shell_and_report (); + + // then + mctest_assert_false (subshell_alive); + mctest_assert_false (mc_global.tty.use_subshell); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (mc_global.tty.subshell_pty), 1); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (shell_death_after_handshake_keeps_subshell_enabled) +{ + // given + subshell_initialized = TRUE; + + // when + kill_fake_shell_and_report (); + + // then + mctest_assert_false (subshell_alive); + mctest_assert_true (mc_global.tty.use_subshell); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (close_select_channels_removes_both_fds) +{ + // when + subshell_close_select_channels (); + + // then + ck_assert_int_eq (delete_select_channel__call_count_for_fd (mc_global.tty.subshell_pty), 1); + ck_assert_int_eq (delete_select_channel__call_count_for_fd (subshell_pipe[READ]), 1); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + tcase_add_checked_fixture (tc_core, setup, teardown); + + // Add new tests here: *************** + tcase_add_test (tc_core, init_ready_finishes_handshake_when_cwd_available); + tcase_add_test (tc_core, init_ready_bails_out_when_subshell_not_alive); + tcase_add_test (tc_core, ensure_initialized_finishes_pending_init); + tcase_add_test (tc_core, ensure_initialized_is_a_noop_once_already_initialized); + tcase_add_test (tc_core, chdir_is_ignored_until_initialized); + tcase_add_test (tc_core, chdir_is_sent_once_initialized); + tcase_add_test (tc_core, shell_death_before_handshake_disables_subshell); + tcase_add_test (tc_core, shell_death_after_handshake_keeps_subshell_enabled); + tcase_add_test (tc_core, close_select_channels_removes_both_fds); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/tests/src/subshell/subshell__common.c b/tests/src/subshell/subshell__common.c new file mode 100644 index 0000000000..08c8250903 --- /dev/null +++ b/tests/src/subshell/subshell__common.c @@ -0,0 +1,109 @@ +/* + Common code for testing functions in src/subshell/common.c file. + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +/* Rename symbols that collide with libinternal.la / libmc.la. */ +#define init_subshell test_init_subshell +#define invoke_subshell test_invoke_subshell +#define flush_subshell test_flush_subshell +#define read_subshell_prompt test_read_subshell_prompt +#define do_update_prompt test_do_update_prompt +#define exit_subshell test_exit_subshell +#define subshell_chdir test_subshell_chdir +#define subshell_get_console_attributes test_subshell_get_console_attributes +#define sigchld_handler test_sigchld_handler +#define subshell_state test_subshell_state +#define subshell_prompt test_subshell_prompt +#define update_subshell_prompt test_update_subshell_prompt +#define should_read_new_subshell_prompt test_should_read_new_subshell_prompt +#define add_select_channel test_add_select_channel +#define delete_select_channel test_delete_select_channel +#define subshell_get_cwd test_subshell_get_cwd +#define subshell_handle_cons_saver test_subshell_handle_cons_saver +#define subshell_get_mainloop_quit test_subshell_get_mainloop_quit +#define subshell_set_mainloop_quit test_subshell_set_mainloop_quit +#define cmdline test_cmdline +#define setup_cmdline test_setup_cmdline + +#include "src/subshell/common.c" + +/* --------------------------------------------------------------------------------------------- */ + +/* @ThenReturnValue */ +static vfs_path_t *subshell_get_cwd__return_value; + +/* @Mock */ +const vfs_path_t * +test_subshell_get_cwd (void) +{ + return subshell_get_cwd__return_value; +} + +/* @Mock */ +void +test_subshell_handle_cons_saver (void) +{ +} + +WInput *test_cmdline = NULL; + +/* @Mock */ +void +test_setup_cmdline (void) +{ +} + +/* @Mock */ +int +test_subshell_get_mainloop_quit (void) +{ + return 0; +} + +/* @Mock */ +void +test_subshell_set_mainloop_quit (const int param_quit) +{ + (void) param_quit; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @CapturedValue */ +static GArray *delete_select_channel__calls; + +/* @Mock */ +void +test_add_select_channel (int fd, select_fn callback, void *info) +{ + (void) fd; + (void) callback; + (void) info; +} + +/* @Mock */ +void +test_delete_select_channel (int fd) +{ + g_array_append_val (delete_select_channel__calls, fd); +} + +/* --------------------------------------------------------------------------------------------- */ diff --git a/tests/src/subshell/subshell__peek_subshell_switch_key.c b/tests/src/subshell/subshell__peek_subshell_switch_key.c new file mode 100644 index 0000000000..e504e9582b --- /dev/null +++ b/tests/src/subshell/subshell__peek_subshell_switch_key.c @@ -0,0 +1,102 @@ +/* + src/subshell - tests for peek_subshell_switch_key() + + Copyright (C) 2026 + Free Software Foundation, Inc. + + This file is part of the Midnight Commander. + + The Midnight Commander is free software: you can redistribute it + and/or modify it under the terms of the GNU General Public License as + published by the Free Software Foundation, either version 3 of the License, + or (at your option) any later version. + + The Midnight Commander is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#define TEST_SUITE_NAME "/src/subshell" + +#include "tests/mctest.h" + +#include "subshell__common.c" + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (raw_ctrl_o_is_recognized) +{ + const char buf[] = { XCTRL ('o') & 255 }; + + mctest_assert_true (peek_subshell_switch_key (buf, 1)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (other_byte_is_not_recognized) +{ + const char buf[] = { 'x' }; + + mctest_assert_false (peek_subshell_switch_key (buf, 1)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (empty_buffer_is_not_recognized) +{ + mctest_assert_false (peek_subshell_switch_key ("", 0)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (kitty_ctrl_o_is_recognized) +{ + const char buf[] = ESC_STR "[111;5u"; + + mctest_assert_true (peek_subshell_switch_key (buf, sizeof (buf) - 1)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (kitty_plain_o_is_not_recognized) +{ + const char buf[] = ESC_STR "[111;1u"; + + mctest_assert_false (peek_subshell_switch_key (buf, sizeof (buf) - 1)); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +int +main (void) +{ + TCase *tc_core; + + tc_core = tcase_create ("Core"); + + // Add new tests here: *************** + tcase_add_test (tc_core, raw_ctrl_o_is_recognized); + tcase_add_test (tc_core, other_byte_is_not_recognized); + tcase_add_test (tc_core, empty_buffer_is_not_recognized); + tcase_add_test (tc_core, kitty_ctrl_o_is_recognized); + tcase_add_test (tc_core, kitty_plain_o_is_not_recognized); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */