diff --git a/lib/widget/wtools.h b/lib/widget/wtools.h index 510f3f51d..e9e9ee839 100644 --- a/lib/widget/wtools.h +++ b/lib/widget/wtools.h @@ -70,7 +70,7 @@ char *input_dialog_help (const char *header, const char *text, const char *help, char *input_expand_dialog (const char *header, const char *text, const char *history_name, const char *def_text, input_complete_t completion_flags); -int query_dialog (const char *header, const char *text, int flags, int count, ...); +MC_MOCKABLE int query_dialog (const char *header, const char *text, int flags, int count, ...); void query_set_sel (int new_sel); /* Create message box but don't dismiss it yet, not background safe */ diff --git a/src/filemanager/file.c b/src/filemanager/file.c index d4b08ab39..30ac223b8 100644 --- a/src/filemanager/file.c +++ b/src/filemanager/file.c @@ -2762,10 +2762,11 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa // src_read if (mc_ctl (src_desc, VFS_CTL_IS_NOTREADY, 0) == 0) - while ((n_read = mc_read (src_desc, buf, bufsize)) < 0 && !ctx->ignore_all) + while ((n_read = mc_read (src_desc, buf, bufsize)) < 0) { - return_status = - file_error (ctx, TRUE, _ ("Cannot read source file\n%s"), src_path); + return_status = ctx->ignore_all + ? FILE_IGNORE_ALL + : file_error (ctx, TRUE, _ ("Cannot read source file\n%s"), src_path); if (return_status == FILE_RETRY) continue; if (return_status == FILE_IGNORE_ALL) @@ -2923,13 +2924,21 @@ copy_file_file (file_op_context_t *ctx, const char *src_path, const char *dst_pa if (dst_status == DEST_SHORT_QUERY) { - // Query to remove short file - if (query_dialog (Q_ ("DialogTitle|Copy"), _ ("Incomplete file was retrieved"), D_ERROR, 2, - _ ("&Delete"), _ ("&Keep")) - == 0) - dst_status = DEST_SHORT_DELETE; + if (ctx->ignore_all) + { + // "Ignore all" answers this question too, but never deletes pre-existing data + dst_status = appending ? DEST_SHORT_KEEP : DEST_SHORT_DELETE; + } else - dst_status = DEST_SHORT_KEEP; + { + // Query to remove short file + if (query_dialog (Q_ ("DialogTitle|Copy"), _ ("Incomplete file was retrieved"), D_ERROR, + 2, _ ("&Delete"), _ ("&Keep")) + == 0) + dst_status = DEST_SHORT_DELETE; + else + dst_status = DEST_SHORT_KEEP; + } } if (dst_status == DEST_SHORT_DELETE) diff --git a/tests/src/filemanager/Makefile.am b/tests/src/filemanager/Makefile.am index 7a902ce0b..823010a7e 100644 --- a/tests/src/filemanager/Makefile.am +++ b/tests/src/filemanager/Makefile.am @@ -17,6 +17,7 @@ endif TESTS = \ cd_to \ + copy_file_file \ examine_cd \ exec_get_export_variables_ext \ ext__exec_make_shell_string \ @@ -29,6 +30,9 @@ check_PROGRAMS = $(TESTS) cd_to_SOURCES = \ cd_to.c +copy_file_file_SOURCES = \ + copy_file_file.c + examine_cd_SOURCES = \ examine_cd.c diff --git a/tests/src/filemanager/copy_file_file.c b/tests/src/filemanager/copy_file_file.c new file mode 100644 index 000000000..a9e1feb68 --- /dev/null +++ b/tests/src/filemanager/copy_file_file.c @@ -0,0 +1,230 @@ +/* + src/filemanager - tests for copy_file_file() function + + 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/filemanager" + +#include "tests/mctest.h" + +#include + +#include "src/vfs/local/local.c" + +#include "src/filemanager/file.c" + +/* --------------------------------------------------------------------------------------------- */ + +/* Give up injecting read errors after this many calls, so that a regression makes the test + fail instead of hanging forever */ +#define READ_ERRORS_MAX 1000 + +static const char test_content[] = "Midnight Commander copy_file_file() test data\n"; + +static char *test_dir = NULL; +static char *src_path = NULL; +static char *dst_path = NULL; + +static gboolean read_fails = FALSE; +static int read_calls = 0; +static int query_dialog_calls = 0; + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +mc_refresh (void) +{ +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +void +message (int flags, const char *title, const char *text, ...) +{ + (void) flags; + (void) title; + (void) text; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Mock */ +int +query_dialog (const char *header, const char *text, int flags, int count, ...) +{ + (void) header; + (void) text; + (void) flags; + (void) count; + + query_dialog_calls++; + + // "Incomplete file was retrieved": Delete + return 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* Fault injection at the VFS layer: fail every read of the local file system with EIO, like + reading a damaged sector does */ +static ssize_t +test_local_read (void *data, char *buffer, size_t count) +{ + read_calls++; + + if (read_fails && read_calls <= READ_ERRORS_MAX) + { + errno = EIO; + return -1; + } + + if (read_fails) + return 0; + + return local_read (data, buffer, count); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Before */ +static void +setup (void) +{ + str_init_strings (NULL); + + vfs_init (); + vfs_init_localfs (); + vfs_setup_work_dir (); + + vfs_local_ops->read = test_local_read; + + // no UI in tests + nice_rotating_dash = FALSE; + verbose = FALSE; + // force the read()/write() loop instead of reflinking the file + mc_global.vfs.file_cloning = FALSE; + mc_global.vfs.preallocate_space = FALSE; + + test_dir = g_dir_make_tmp ("mc-test-copy_file_file-XXXXXX", NULL); + ck_assert_ptr_nonnull (test_dir); + src_path = g_build_filename (test_dir, "src", (char *) NULL); + dst_path = g_build_filename (test_dir, "dst", (char *) NULL); + ck_assert (g_file_set_contents (src_path, test_content, -1, NULL)); + + read_fails = FALSE; + read_calls = 0; + query_dialog_calls = 0; +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @After */ +static void +teardown (void) +{ + (void) unlink (dst_path); + (void) unlink (src_path); + (void) rmdir (test_dir); + g_free (dst_path); + g_free (src_path); + g_free (test_dir); + + vfs_shut (); + str_uninit_strings (); +} + +/* --------------------------------------------------------------------------------------------- */ + +/* @Test */ +START_TEST (test_copy_file_file_ok) +{ + // given + file_op_context_t *ctx; + FileProgressStatus status; + char *dst_content = NULL; + + ctx = file_op_context_new (OP_COPY); + + // when + status = copy_file_file (ctx, src_path, dst_path); + + // then + ck_assert_int_eq (status, FILE_CONT); + ck_assert (g_file_get_contents (dst_path, &dst_content, NULL, NULL)); + mctest_assert_str_eq (dst_content, test_content); + ck_assert_int_eq (query_dialog_calls, 0); + + g_free (dst_content); + file_op_context_destroy (ctx); +} +END_TEST + +/* --------------------------------------------------------------------------------------------- */ + +/* Ticket #4631: after "Ignore all", a read error must skip the file instead of retrying the + failing read forever */ + +/* @Test */ +START_TEST (test_copy_file_file_read_error_ignore_all) +{ + // given + file_op_context_t *ctx; + FileProgressStatus status; + + ctx = file_op_context_new (OP_COPY); + ctx->ignore_all = TRUE; + read_fails = TRUE; + + // when + status = copy_file_file (ctx, src_path, dst_path); + + // then + ck_assert_int_eq (read_calls, 1); + ck_assert_int_eq (status, FILE_IGNORE_ALL); + ck_assert (ctx->ignore_all); + // the incomplete target file is deleted + ck_assert (!g_file_test (dst_path, G_FILE_TEST_EXISTS)); + + file_op_context_destroy (ctx); +} +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, test_copy_file_file_ok); + tcase_add_test (tc_core, test_copy_file_file_read_error_ignore_all); + // *********************************** + + return mctest_run_all (tc_core); +} + +/* --------------------------------------------------------------------------------------------- */