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); +} + +/* --------------------------------------------------------------------------------------------- */