Add tests for edit_replace_cmd().

* (status_msg_init): add MC_MOCKABLE attribute.
  * (status_msg_deinit): likewise.
  * (edit_search_update_callback): likewise.
  * (edit_dialog_replace_show): add MC_MOCKABLE and make public.
  * (edit_dialog_replace_prompt_show): likewise.
  * (edit_search_options): make public.
  * (B_REPLACE_ALL, B_REPLACE_ONE, B_SKIP_REPLACE): likewise.
  * (macros_list): init explicitly.
  * tests/src/editor/mc.charsets: add ASCII charset.
  * tests/src/editor/edit_replace_cmd.c: new file.
  * tests/src/editor/Makefile.am: add new test.

Signed-off-by: Andrew Borodin <aborodin@vmail.ru>
This commit is contained in:
Andrew Borodin 2025-02-09 11:40:24 +03:00
parent 97597cc60b
commit 2893ee212f
6 changed files with 530 additions and 159 deletions

View File

@ -84,9 +84,10 @@ gboolean mc_error_message (GError **mcerror, int *code);
status_msg_t *status_msg_create (const char *title, double delay, status_msg_cb init_cb,
status_msg_update_cb update_cb, status_msg_cb deinit_cb);
void status_msg_destroy (status_msg_t *sm);
void status_msg_init (status_msg_t *sm, const char *title, double delay, status_msg_cb init_cb,
status_msg_update_cb update_cb, status_msg_cb deinit_cb);
void status_msg_deinit (status_msg_t *sm);
MC_MOCKABLE void status_msg_init (status_msg_t *sm, const char *title, double delay,
status_msg_cb init_cb, status_msg_update_cb update_cb,
status_msg_cb deinit_cb);
MC_MOCKABLE void status_msg_deinit (status_msg_t *sm);
int status_msg_common_update (status_msg_t *sm);
void simple_status_msg_init_cb (status_msg_t *sm);

View File

@ -47,29 +47,7 @@
/*** global variables ****************************************************************************/
/*** file scope macro definitions ****************************************************************/
#define B_REPLACE_ALL (B_USER + 1)
#define B_REPLACE_ONE (B_USER + 2)
#define B_SKIP_REPLACE (B_USER + 3)
/*** file scope type declarations ****************************************************************/
typedef struct edit_search_options_t
{
mc_search_type_t type;
gboolean case_sens;
gboolean backwards;
gboolean only_in_selection;
gboolean whole_words;
gboolean all_codepages;
} edit_search_options_t;
/*** forward declarations (file scope functions) *************************************************/
/*** file scope variables ************************************************************************/
static edit_search_options_t edit_search_options = {
edit_search_options_t edit_search_options = {
.type = MC_SEARCH_T_NORMAL,
.case_sens = FALSE,
.backwards = FALSE,
@ -78,6 +56,21 @@ static edit_search_options_t edit_search_options = {
.all_codepages = FALSE,
};
/*** file scope macro definitions ****************************************************************/
/*** file scope type declarations ****************************************************************/
/*** forward declarations (file scope functions) *************************************************/
MC_MOCKABLE void edit_dialog_replace_show (WEdit *edit, const char *search_default,
const char *replace_default, char **search_text,
char **replace_text);
MC_MOCKABLE int edit_dialog_replace_prompt_show (WEdit *edit, char *from_text, char *to_text,
int xpos, int ypos);
/*** file scope variables ************************************************************************/
/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */
@ -165,136 +158,6 @@ edit_dialog_search_show (WEdit *edit)
/* --------------------------------------------------------------------------------------------- */
static void
edit_dialog_replace_show (WEdit *edit, const char *search_default, const char *replace_default,
/*@out@ */ char **search_text, /*@out@ */ char **replace_text)
{
size_t num_of_types = 0;
gchar **list_of_types;
if ((search_default == NULL) || (*search_default == '\0'))
search_default = INPUT_LAST_TEXT;
list_of_types = mc_search_get_types_strings_array (&num_of_types);
{
quick_widget_t quick_widgets[] = {
// clang-format off
QUICK_LABELED_INPUT (N_ ("Enter search string:"), input_label_above, search_default,
MC_HISTORY_SHARED_SEARCH, search_text, NULL, FALSE, FALSE,
INPUT_COMPLETE_NONE),
QUICK_LABELED_INPUT (N_ ("Enter replacement string:"), input_label_above,
replace_default, "replace", replace_text, NULL, FALSE, FALSE,
INPUT_COMPLETE_NONE),
QUICK_SEPARATOR (TRUE),
QUICK_START_COLUMNS,
QUICK_RADIO (num_of_types, (const char **) list_of_types,
(int *) &edit_search_options.type, NULL),
QUICK_NEXT_COLUMN,
QUICK_CHECKBOX (N_ ("Cas&e sensitive"), &edit_search_options.case_sens, NULL),
QUICK_CHECKBOX (N_ ("&Backwards"), &edit_search_options.backwards, NULL),
QUICK_CHECKBOX (N_ ("In se&lection"), &edit_search_options.only_in_selection, NULL),
QUICK_CHECKBOX (N_ ("&Whole words"), &edit_search_options.whole_words, NULL),
#ifdef HAVE_CHARSET
QUICK_CHECKBOX (N_ ("&All charsets"), &edit_search_options.all_codepages, NULL),
#endif
QUICK_STOP_COLUMNS,
QUICK_BUTTONS_OK_CANCEL,
QUICK_END,
// clang-format on
};
WRect r = { -1, -1, 0, 58 };
quick_dialog_t qdlg = {
.rect = r,
.title = N_ ("Replace"),
.help = "[Input Line Keys]",
.widgets = quick_widgets,
.callback = NULL,
.mouse_callback = NULL,
};
if (quick_dialog (&qdlg) != B_CANCEL)
edit->replace_mode = 0;
else
{
*replace_text = NULL;
*search_text = NULL;
}
}
g_strfreev (list_of_types);
}
/* --------------------------------------------------------------------------------------------- */
static int
edit_dialog_replace_prompt_show (WEdit *edit, char *from_text, char *to_text, int xpos, int ypos)
{
Widget *w = WIDGET (edit);
// dialog size
int dlg_height = 10;
int dlg_width;
char tmp[BUF_MEDIUM];
char *repl_from, *repl_to;
int retval;
if (xpos == -1)
xpos = w->rect.x + edit_options.line_state_width + 1;
if (ypos == -1)
ypos = w->rect.y + w->rect.lines / 2;
// Sometimes menu can hide replaced text. I don't like it
if ((edit->curs_row >= ypos - 1) && (edit->curs_row <= ypos + dlg_height - 1))
ypos -= dlg_height;
dlg_width = WIDGET (w->owner)->rect.cols - xpos - 1;
g_snprintf (tmp, sizeof (tmp), "\"%s\"", from_text);
repl_from = g_strdup (str_trunc (tmp, dlg_width - 7));
g_snprintf (tmp, sizeof (tmp), "\"%s\"", to_text);
repl_to = g_strdup (str_trunc (tmp, dlg_width - 7));
{
quick_widget_t quick_widgets[] = {
// clang-format off
QUICK_LABEL (repl_from, NULL),
QUICK_LABEL (N_ ("Replace with:"), NULL),
QUICK_LABEL (repl_to, NULL),
QUICK_START_BUTTONS (TRUE, TRUE),
QUICK_BUTTON (N_ ("&Replace"), B_ENTER, NULL, NULL),
QUICK_BUTTON (N_ ("A&ll"), B_REPLACE_ALL, NULL, NULL),
QUICK_BUTTON (N_ ("&Skip"), B_SKIP_REPLACE, NULL, NULL),
QUICK_BUTTON (N_ ("&Cancel"), B_CANCEL, NULL, NULL),
QUICK_END,
// clang-format on
};
WRect r = { ypos, xpos, 0, -1 };
quick_dialog_t qdlg = {
.rect = r,
.title = N_ ("Confirm replace"),
.help = NULL,
.widgets = quick_widgets,
.callback = NULL,
.mouse_callback = NULL,
};
retval = quick_dialog (&qdlg);
}
g_free (repl_from);
g_free (repl_to);
return retval;
}
/* --------------------------------------------------------------------------------------------- */
/**
* Get EOL symbol for searching.
*
@ -824,6 +687,136 @@ edit_search_cmd (WEdit *edit, gboolean again)
}
}
/* --------------------------------------------------------------------------------------------- */
void
edit_dialog_replace_show (WEdit *edit, const char *search_default, const char *replace_default,
/*@out@ */ char **search_text, /*@out@ */ char **replace_text)
{
size_t num_of_types = 0;
gchar **list_of_types;
if ((search_default == NULL) || (*search_default == '\0'))
search_default = INPUT_LAST_TEXT;
list_of_types = mc_search_get_types_strings_array (&num_of_types);
{
quick_widget_t quick_widgets[] = {
// clang-format off
QUICK_LABELED_INPUT (N_ ("Enter search string:"), input_label_above, search_default,
MC_HISTORY_SHARED_SEARCH, search_text, NULL, FALSE, FALSE,
INPUT_COMPLETE_NONE),
QUICK_LABELED_INPUT (N_ ("Enter replacement string:"), input_label_above,
replace_default, "replace", replace_text, NULL, FALSE, FALSE,
INPUT_COMPLETE_NONE),
QUICK_SEPARATOR (TRUE),
QUICK_START_COLUMNS,
QUICK_RADIO (num_of_types, (const char **) list_of_types,
(int *) &edit_search_options.type, NULL),
QUICK_NEXT_COLUMN,
QUICK_CHECKBOX (N_ ("Cas&e sensitive"), &edit_search_options.case_sens, NULL),
QUICK_CHECKBOX (N_ ("&Backwards"), &edit_search_options.backwards, NULL),
QUICK_CHECKBOX (N_ ("In se&lection"), &edit_search_options.only_in_selection, NULL),
QUICK_CHECKBOX (N_ ("&Whole words"), &edit_search_options.whole_words, NULL),
#ifdef HAVE_CHARSET
QUICK_CHECKBOX (N_ ("&All charsets"), &edit_search_options.all_codepages, NULL),
#endif
QUICK_STOP_COLUMNS,
QUICK_BUTTONS_OK_CANCEL,
QUICK_END,
// clang-format on
};
WRect r = { -1, -1, 0, 58 };
quick_dialog_t qdlg = {
.rect = r,
.title = N_ ("Replace"),
.help = "[Input Line Keys]",
.widgets = quick_widgets,
.callback = NULL,
.mouse_callback = NULL,
};
if (quick_dialog (&qdlg) != B_CANCEL)
edit->replace_mode = 0;
else
{
*replace_text = NULL;
*search_text = NULL;
}
}
g_strfreev (list_of_types);
}
/* --------------------------------------------------------------------------------------------- */
int
edit_dialog_replace_prompt_show (WEdit *edit, char *from_text, char *to_text, int xpos, int ypos)
{
Widget *w = WIDGET (edit);
// dialog size
int dlg_height = 10;
int dlg_width;
char tmp[BUF_MEDIUM];
char *repl_from, *repl_to;
int retval;
if (xpos == -1)
xpos = w->rect.x + edit_options.line_state_width + 1;
if (ypos == -1)
ypos = w->rect.y + w->rect.lines / 2;
// Sometimes menu can hide replaced text. I don't like it
if ((edit->curs_row >= ypos - 1) && (edit->curs_row <= ypos + dlg_height - 1))
ypos -= dlg_height;
dlg_width = WIDGET (w->owner)->rect.cols - xpos - 1;
g_snprintf (tmp, sizeof (tmp), "\"%s\"", from_text);
repl_from = g_strdup (str_trunc (tmp, dlg_width - 7));
g_snprintf (tmp, sizeof (tmp), "\"%s\"", to_text);
repl_to = g_strdup (str_trunc (tmp, dlg_width - 7));
{
quick_widget_t quick_widgets[] = {
// clang-format off
QUICK_LABEL (repl_from, NULL),
QUICK_LABEL (N_ ("Replace with:"), NULL),
QUICK_LABEL (repl_to, NULL),
QUICK_START_BUTTONS (TRUE, TRUE),
QUICK_BUTTON (N_ ("&Replace"), B_ENTER, NULL, NULL),
QUICK_BUTTON (N_ ("A&ll"), B_REPLACE_ALL, NULL, NULL),
QUICK_BUTTON (N_ ("&Skip"), B_SKIP_REPLACE, NULL, NULL),
QUICK_BUTTON (N_ ("&Cancel"), B_CANCEL, NULL, NULL),
QUICK_END,
// clang-format on
};
WRect r = { ypos, xpos, 0, -1 };
quick_dialog_t qdlg = {
.rect = r,
.title = N_ ("Confirm replace"),
.help = NULL,
.widgets = quick_widgets,
.callback = NULL,
.mouse_callback = NULL,
};
retval = quick_dialog (&qdlg);
}
g_free (repl_from);
g_free (repl_to);
return retval;
}
/* --------------------------------------------------------------------------------------------- */
/** call with edit = 0 before shutdown to close memory leaks */

View File

@ -3,10 +3,24 @@
/*** typedefs(not structures) and defined constants **********************************************/
#define B_REPLACE_ALL (B_USER + 1)
#define B_REPLACE_ONE (B_USER + 2)
#define B_SKIP_REPLACE (B_USER + 3)
/*** enums ***************************************************************************************/
/*** structures declarations (and typedefs of structures)*****************************************/
typedef struct edit_search_options_t
{
mc_search_type_t type;
gboolean case_sens;
gboolean backwards;
gboolean only_in_selection;
gboolean whole_words;
gboolean all_codepages;
} edit_search_options_t;
typedef struct
{
simple_status_msg_t status_msg; // base class
@ -18,6 +32,8 @@ typedef struct
/*** global variables defined in .c file *********************************************************/
extern edit_search_options_t edit_search_options;
/*** declarations of public functions ************************************************************/
gboolean edit_search_init (WEdit *edit, const char *s);
@ -25,7 +41,8 @@ void edit_search_deinit (WEdit *edit);
mc_search_cbret_t edit_search_cmd_callback (const void *user_data, off_t char_offset,
int *current_char);
mc_search_cbret_t edit_search_update_callback (const void *user_data, off_t char_offset);
MC_MOCKABLE mc_search_cbret_t edit_search_update_callback (const void *user_data,
off_t char_offset);
int edit_search_status_update_cb (status_msg_t *sm);
void edit_search_cmd (WEdit *edit, gboolean again);

View File

@ -211,7 +211,7 @@ int macro_index = -1;
/* macro stuff */
struct macro_action_t record_macro_buf[MAX_MACRO_LENGTH];
GArray *macros_list;
GArray *macros_list = NULL;
#endif
/*** file scope macro definitions ****************************************************************/

View File

@ -17,10 +17,14 @@ endif
EXTRA_DIST = mc.charsets test-data.txt.in
TESTS = \
edit_complete_word_cmd
edit_complete_word_cmd \
edit_replace_cmd
check_PROGRAMS = $(TESTS)
edit_complete_word_cmd_SOURCES = \
edit_complete_word_cmd.c
edit_replace_cmd_SOURCES = \
edit_replace_cmd.c

View File

@ -0,0 +1,356 @@
/*
src/editor - tests for edit_replace_cmd() function
Copyright (C) 2025
Free Software Foundation, Inc.
Written by:
Andrew Borodin <aborodin@vmail.ru>, 2025
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 <https://www.gnu.org/licenses/>.
*/
#define TEST_SUITE_NAME "/src/editor"
#include "tests/mctest.h"
#include <ctype.h>
#ifdef HAVE_CHARSET
# include "lib/charsets.h"
# include "src/selcodepage.h"
#endif
#include "src/editor/editwidget.h"
#include "src/editor/editmacros.h" // edit_load_macro_cmd()
#include "src/editor/editsearch.h"
static WGroup owner;
static WEdit *test_edit;
static gboolean only_in_selection = FALSE;
static const char *test_regex_in = "qwe\n" //
"qwe\n" //
"qwe\n" //
"qwe\n" //
"qwe\n" //
"qwe\n" //
"qwe\n" //
"qwe\n"; //
static const char *test_regex_out = "Xqwe\n" //
"Xqwe\n" //
"Xqwe\n" //
"Xqwe\n" //
"Xqwe\n" //
"Xqwe\n" //
"Xqwe\n" //
"Xqwe\n"; //
static const char *test_regex_selected_in = "qwe\n" //
"qwe\n" //
"qwe\n" // selected, mark1 = 8 (begin of line) or 9
"qwe\n" // selected
"qwe\n" // selected
"qwe\n" // mark2 = 20, begin of line
"qwe\n" //
"qwe\n"; //
static const char *test1_regex_selected_out = "qwe\n" //
"qwe\n" //
"Xqwe\n" // selected, mark1 = 8 (begin of line)
"Xqwe\n" // selected
"Xqwe\n" // selected
"qwe\n" // mark2 = 20, begin of line
"qwe\n" //
"qwe\n"; //
static const char *test2_regex_selected_out = "qwe\n" //
"qwe\n" //
"qwe\n" // selected, mark1 = 9 (not begin of line)
"Xqwe\n" // selected
"Xqwe\n" // selected
"qwe\n" // mark2 = 20, begin of line
"qwe\n" //
"qwe\n"; //
static const char *replace_regex_from = "^.*";
static const char *replace_regex_to = "X\\0";
/* --------------------------------------------------------------------------------------------- */
void edit_dialog_replace_show (WEdit *edit, const char *search_default, const char *replace_default,
char **search_text, char **replace_text);
int edit_dialog_replace_prompt_show (WEdit *edit, char *from_text, char *to_text, int xpos,
int ypos);
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
void
message (int flags, const char *title, const char *text, ...)
{
(void) flags;
(void) title;
(void) text;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
void
status_msg_init (status_msg_t *sm, const char *title, double delay, status_msg_cb init_cb,
status_msg_update_cb update_cb, status_msg_cb deinit_cb)
{
(void) sm;
(void) title;
(void) delay;
(void) init_cb;
(void) update_cb;
(void) deinit_cb;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
void
status_msg_deinit (status_msg_t *sm)
{
(void) sm;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
mc_search_cbret_t
edit_search_update_callback (const void *user_data, off_t char_offset)
{
(void) user_data;
(void) char_offset;
return MC_SEARCH_CB_OK;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
void
edit_dialog_replace_show (WEdit *edit, const char *search_default, const char *replace_default,
char **search_text, char **replace_text)
{
(void) edit;
(void) search_default;
(void) replace_default;
*search_text = g_strdup (replace_regex_from);
*replace_text = g_strdup (replace_regex_to);
edit_search_options.type = MC_SEARCH_T_REGEX;
edit_search_options.only_in_selection = only_in_selection;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
int
edit_dialog_replace_prompt_show (WEdit *edit, char *from_text, char *to_text, int xpos, int ypos)
{
(void) edit;
(void) from_text;
(void) to_text;
(void) xpos;
(void) ypos;
return B_REPLACE_ALL;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
void
edit_load_syntax (WEdit *edit, GPtrArray *pnames, const char *type)
{
(void) edit;
(void) pnames;
(void) type;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
int
edit_get_syntax_color (WEdit *edit, off_t byte_index)
{
(void) edit;
(void) byte_index;
return 0;
}
/* --------------------------------------------------------------------------------------------- */
/* @Mock */
gboolean
edit_load_macro_cmd (WEdit *edit)
{
(void) edit;
return FALSE;
}
/* --------------------------------------------------------------------------------------------- */
/* @Before */
static void
setup (void)
{
WRect r;
str_init_strings (NULL);
#ifdef HAVE_CHARSET
mc_global.sysconfig_dir = (char *) TEST_SHARE_DIR;
load_codepages_list ();
#endif
edit_options.filesize_threshold = (char *) "64M";
rect_init (&r, 0, 0, 24, 80);
test_edit = edit_init (NULL, &r, NULL);
memset (&owner, 0, sizeof (owner));
group_add_widget (&owner, WIDGET (test_edit));
mc_global.source_codepage = 0;
mc_global.display_codepage = 0;
#ifdef HAVE_CHARSET
cp_source = "ASCII";
cp_display = "ASCII";
#endif
do_set_codepage (0);
edit_set_codeset (test_edit);
}
/* --------------------------------------------------------------------------------------------- */
/* @After */
static void
teardown (void)
{
edit_clean (test_edit);
group_remove_widget (test_edit);
g_free (test_edit);
#ifdef HAVE_CHARSET
free_codepages_list ();
#endif
str_uninit_strings ();
}
/* --------------------------------------------------------------------------------------------- */
static void
test_replace_check (const char *test_out)
{
GString *actual_replaced_str;
actual_replaced_str = g_string_new ("");
for (off_t i = 0; i < test_edit->buffer.size; i++)
{
const int chr = edit_buffer_get_byte (&test_edit->buffer, i);
g_string_append_c (actual_replaced_str, chr);
}
mctest_assert_str_eq (actual_replaced_str->str, test_out);
g_string_free (actual_replaced_str, TRUE);
}
/* --------------------------------------------------------------------------------------------- */
START_TEST (test_replace_regex)
{
// given
only_in_selection = FALSE;
test_edit->mark1 = 0;
test_edit->mark2 = 0;
for (const char *ti = test_regex_in; *ti != '\0'; ti++)
edit_buffer_insert (&test_edit->buffer, *ti);
// when
edit_cursor_move (test_edit, 0);
edit_replace_cmd (test_edit, FALSE);
// then
test_replace_check (test_regex_out);
}
END_TEST
/* --------------------------------------------------------------------------------------------- */
START_TEST (test1_replace_regex_in_selection)
{
// given
only_in_selection = TRUE;
test_edit->mark1 = 8;
test_edit->mark2 = 20;
for (const char *ti = test_regex_selected_in; *ti != '\0'; ti++)
edit_buffer_insert (&test_edit->buffer, *ti);
// when
edit_cursor_move (test_edit, 0);
edit_replace_cmd (test_edit, FALSE);
// then
test_replace_check (test1_regex_selected_out);
}
END_TEST
/* --------------------------------------------------------------------------------------------- */
START_TEST (test2_replace_regex_in_selection)
{
// given
only_in_selection = TRUE;
test_edit->mark1 = 9;
test_edit->mark2 = 20;
for (const char *ti = test_regex_selected_in; *ti != '\0'; ti++)
edit_buffer_insert (&test_edit->buffer, *ti);
// when
edit_cursor_move (test_edit, 0);
edit_replace_cmd (test_edit, FALSE);
// then
test_replace_check (test2_regex_selected_out);
}
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_replace_regex);
tcase_add_test (tc_core, test1_replace_regex_in_selection);
tcase_add_test (tc_core, test2_replace_regex_in_selection);
// ***********************************
return mctest_run_all (tc_core);
}
/* --------------------------------------------------------------------------------------------- */