diff --git a/CMakeLists.txt b/CMakeLists.txt index bab2d56..03139c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.10) project(PathEditor VERSION 3.0 LANGUAGES C) +# 选项:是否构建测试 +option(BUILD_TESTS "Build unit tests" OFF) + # 启用资源编译器以处理 .rc 文件 enable_language(RC) @@ -18,6 +21,7 @@ set(SOURCES src/utils/safe_string.c src/utils/logger.c src/utils/i18n.c + src/utils/error_code.c src/ui/ui_utils.c src/ui/dialogs.c src/ui/main_window.c @@ -123,3 +127,9 @@ add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD "$/locale" COMMENT "Copying locale directory to build directory..." ) + +# 测试支持 +if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) +endif() diff --git a/include/controller/callbacks_internal.h b/include/controller/callbacks_internal.h index d3aac5f..2735b67 100644 --- a/include/controller/callbacks_internal.h +++ b/include/controller/callbacks_internal.h @@ -3,6 +3,7 @@ #include #include "core/app_context.h" +#include "utils/i18n.h" // 内部辅助函数声明(供各 callbacks_*.c 文件共享) // 这些函数不对外暴露,仅在 controller 层内部使用 diff --git a/include/utils/error_code.h b/include/utils/error_code.h index cc9cf85..080f417 100644 --- a/include/utils/error_code.h +++ b/include/utils/error_code.h @@ -15,6 +15,7 @@ typedef enum { ERR_INVALID_INDEX = -10 // 无效索引 } ErrorCode; +// 获取错误码的字符串表示(英文,用于日志) const char* error_code_to_string(ErrorCode code); #endif // ERROR_CODE_H diff --git a/include/utils/string_ext.h b/include/utils/string_ext.h index ab43b49..6f6df2e 100644 --- a/include/utils/string_ext.h +++ b/include/utils/string_ext.h @@ -27,4 +27,7 @@ char *wide_to_utf8(const wchar_t *wstr); wchar_t *utf8_to_wide(const char *str); char *stristr(const char *haystack, const char *needle); +// 检查字符串列表中是否存在指定路径(不区分大小写) +int string_list_contains(const StringList *list, const char *str); + #endif // STRING_EXT_H diff --git a/po/en_US.po b/po/en_US.po index f910a95..26b4803 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: PathEditor 3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 10:00+0800\n" -"PO-Revision-Date: 2026-03-26 19:59+0800\n" +"POT-Creation-Date: 2026-04-29 16:30+0800\n" +"PO-Revision-Date: 2026-04-29 16:30+0800\n" "Last-Translator: LHY <3364451258@qq.com>\n" "Language-Team: English\n" "Language: en_US\n" @@ -103,3 +103,287 @@ msgstr "Chinese (Simplified)" #: src/ui/dialogs.c msgid "English" msgstr "English" + +#: src/controller/callbacks_sys.c +msgid "Administrator privileges are required to save changes!" +msgstr "Administrator privileges are required to save changes!" + +#: src/controller/callbacks_sys.c +msgid "Backup Settings" +msgstr "Backup Settings" + +#: src/controller/callbacks_sys.c +msgid "Would you like to customize the backup directory?" +msgstr "Would you like to customize the backup directory?" + +#: src/controller/callbacks_sys.c +msgid "Use Default" +msgstr "Use Default" + +#: src/controller/callbacks_sys.c +msgid "Custom Directory" +msgstr "Custom Directory" + +#: src/controller/callbacks_sys.c +msgid "Skip Backup" +msgstr "Skip Backup" + +#: src/controller/callbacks_sys.c +msgid "No directory selected, will use default backup path." +msgstr "No directory selected, will use default backup path." + +#: src/controller/callbacks_sys.c +msgid "Confirm" +msgstr "Confirm" + +#: src/controller/callbacks_sys.c +msgid "Are you sure you want to skip backup?" +msgstr "Are you sure you want to skip backup?" + +#: src/controller/callbacks_sys.c +msgid "Skipping backup may cause inability to recover!" +msgstr "Skipping backup may cause inability to recover!" + +#: src/controller/callbacks_sys.c +msgid "Skip Anyway" +msgstr "Skip Anyway" + +#: src/controller/callbacks_sys.c +msgid "Go Back" +msgstr "Go Back" + +#: src/controller/callbacks_sys.c +msgid "Continue Saving" +msgstr "Continue Saving" + +#: src/controller/callbacks_sys.c +msgid "Success" +msgstr "Success" + +#: src/controller/callbacks_sys.c +msgid "Both system and user PATH environment variables have been updated!" +msgstr "Both system and user PATH environment variables have been updated!" + +#: src/controller/callbacks_sys.c +msgid "Info" +msgstr "Info" + +#: src/controller/callbacks_sys.c +msgid "System variables saved successfully, but user variables failed to save." +msgstr "System variables saved successfully, but user variables failed to save." + +#: src/controller/callbacks_sys.c +msgid "User variables saved successfully, but system variables failed to save." +msgstr "User variables saved successfully, but system variables failed to save." + +#: src/controller/callbacks_sys.c +msgid "Failed to save!" +msgstr "Failed to save!" + +#: src/controller/callbacks_sys.c +msgid "Unable to open system environment variable registry key, please try running as administrator." +msgstr "Unable to open system environment variable registry key, please try running as administrator." + +#: src/controller/callbacks_sys.c +msgid "Usage Instructions" +msgstr "Usage Instructions" + +#: src/controller/callbacks_sys.c +msgid "This program is used to edit system environment variable PATH." +msgstr "This program is used to edit system environment variable PATH." + +#: src/controller/callbacks_sys.c +msgid "Must run as" +msgstr "Must run as" + +#: src/controller/callbacks_sys.c +msgid "Administrator" +msgstr "Administrator" + +#: src/controller/callbacks_sys.c +msgid "to save changes." +msgstr "to save changes." + +#: src/controller/callbacks_sys.c +msgid "Operations:" +msgstr "Operations:" + +#: src/controller/callbacks_sys.c +msgid "New: Add new path to end of list." +msgstr "New: Add new path to end of list." + +#: src/controller/callbacks_sys.c +msgid "Edit: Modify selected path." +msgstr "Edit: Modify selected path." + +#: src/controller/callbacks_sys.c +msgid "Browse: Select directory from file system to add." +msgstr "Browse: Select directory from file system to add." + +#: src/controller/callbacks_sys.c +msgid "Delete: Remove selected path." +msgstr "Delete: Remove selected path." + +#: src/controller/callbacks_sys.c +msgid "Up/Down: Adjust path priority." +msgstr "Up/Down: Adjust path priority." + +#: src/controller/callbacks_sys.c +msgid "Import/Export: Backup and restore PATH configuration." +msgstr "Import/Export: Backup and restore PATH configuration." + +#: src/controller/callbacks_sys.c +msgid "Click" +msgstr "Click" + +#: src/controller/callbacks_sys.c +msgid "to save changes and apply." +msgstr "to save changes and apply." + +#: src/controller/callbacks_sys.c +msgid "Note: Some running programs may need to restart to recognize new environment variables." +msgstr "Note: Some running programs may need to restart to recognize new environment variables." + +#: src/controller/callbacks_sys.c +msgid "Author: LHY" +msgstr "Author: LHY" + +#: src/controller/callbacks_sys.c +msgid "Email: 3364451258@qq.com" +msgstr "Email: 3364451258@qq.com" + +#: src/controller/callbacks_sys.c +msgid "GitHub: https://github.com/LHY0125/PathEditor" +msgstr "GitHub: https://github.com/LHY0125/PathEditor" + +#: src/controller/callbacks_sys.c +msgid "Don't forget to star my project!" +msgstr "Don't forget to star my project!" + +#: src/controller/callbacks_io.c +msgid "Administrator privileges are required to import PATH!" +msgstr "Administrator privileges are required to import PATH!" + +#: src/controller/callbacks_io.c +msgid "No valid paths found in file!" +msgstr "No valid paths found in file!" + +#: src/controller/callbacks_io.c +msgid "Import Options" +msgstr "Import Options" + +#: src/controller/callbacks_io.c +msgid "Please select import target:" +msgstr "Please select import target:" + +#: src/controller/callbacks_io.c +msgid "System Variables Only" +msgstr "System Variables Only" + +#: src/controller/callbacks_io.c +msgid "User Variables Only" +msgstr "User Variables Only" + +#: src/controller/callbacks_io.c +msgid "Import All" +msgstr "Import All" + +#: src/controller/callbacks_io.c +msgid "Import to System" +msgstr "Import to System" + +#: src/controller/callbacks_io.c +msgid "Import to User" +msgstr "Import to User" + +#: src/controller/callbacks_io.c +msgid "Successfully imported %d paths!" +msgstr "Successfully imported %d paths!" + +#: src/controller/callbacks_io.c +msgid "Import Success" +msgstr "Import Success" + +#: src/controller/callbacks_io.c +msgid "Import failed, please check if the file format is correct!" +msgstr "Import failed, please check if the file format is correct!" + +#: src/controller/callbacks_io.c +msgid "Export successful!" +msgstr "Export successful!" + +#: src/controller/callbacks_io.c +msgid "System variables: %d" +msgstr "System variables: %d" + +#: src/controller/callbacks_io.c +msgid "User variables: %d" +msgstr "User variables: %d" + +#: src/controller/callbacks_io.c +msgid "Save location: %s" +msgstr "Save location: %s" + +#: src/controller/callbacks_io.c +msgid "Export Success" +msgstr "Export Success" + +#: src/controller/callbacks_io.c +msgid "Export failed!" +msgstr "Export failed!" + +#: src/controller/callbacks_nav.c +msgid "Confirm Cleanup" +msgstr "Confirm Cleanup" + +#: src/controller/callbacks_nav.c +msgid "This operation will remove all" +msgstr "This operation will remove all" + +#: src/controller/callbacks_nav.c +msgid "invalid paths" +msgstr "invalid paths" + +#: src/controller/callbacks_nav.c +msgid "and" +msgstr "and" + +#: src/controller/callbacks_nav.c +msgid "duplicate paths" +msgstr "duplicate paths" + +#: src/controller/callbacks_nav.c +msgid "from the current list." +msgstr "from the current list." + +#: src/controller/callbacks_nav.c +msgid "Are you sure you want to continue?" +msgstr "Are you sure you want to continue?" + +#: src/controller/callbacks_nav.c +msgid "Cleanup completed! Removed %d invalid or duplicate paths." +msgstr "Cleanup completed! Removed %d invalid or duplicate paths." + +#: src/controller/callbacks_basic.c +msgid "New Environment Variable" +msgstr "New Environment Variable" + +#: src/controller/callbacks_basic.c +msgid "Please enter a path:" +msgstr "Please enter a path:" + +#: src/controller/callbacks_basic.c +msgid "Edit Environment Variable" +msgstr "Edit Environment Variable" + +#: src/controller/callbacks_basic.c +msgid "Edit path:" +msgstr "Edit path:" + +#: src/controller/callbacks_basic.c +msgid "Please select an item to delete first" +msgstr "Please select an item to delete first" + +#: src/controller/callbacks_basic.c +msgid "This path already exists and will not be added again." +msgstr "This path already exists and will not be added again." \ No newline at end of file diff --git a/po/zh_CN.po b/po/zh_CN.po index a04161f..c551280 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -6,8 +6,8 @@ msgid "" msgstr "" "Project-Id-Version: PathEditor 3.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 10:00+0800\n" -"PO-Revision-Date: 2026-03-26 19:58+0800\n" +"POT-Creation-Date: 2026-04-29 16:30+0800\n" +"PO-Revision-Date: 2026-04-29 16:30+0800\n" "Last-Translator: LHY <3364451258@qq.com>\n" "Language-Team: Chinese (Simplified)\n" "Language: zh_CN\n" @@ -103,3 +103,287 @@ msgstr "中文 (简体中文)" #: src/ui/dialogs.c msgid "English" msgstr "English" + +#: src/controller/callbacks_sys.c +msgid "Administrator privileges are required to save changes!" +msgstr "需要管理员权限才能保存更改!" + +#: src/controller/callbacks_sys.c +msgid "Backup Settings" +msgstr "备份设置" + +#: src/controller/callbacks_sys.c +msgid "Would you like to customize the backup directory?" +msgstr "是否自定义备份目录?" + +#: src/controller/callbacks_sys.c +msgid "Use Default" +msgstr "使用默认" + +#: src/controller/callbacks_sys.c +msgid "Custom Directory" +msgstr "自定义目录" + +#: src/controller/callbacks_sys.c +msgid "Skip Backup" +msgstr "跳过备份" + +#: src/controller/callbacks_sys.c +msgid "No directory selected, will use default backup path." +msgstr "未选择目录,将使用默认备份路径。" + +#: src/controller/callbacks_sys.c +msgid "Confirm" +msgstr "确认" + +#: src/controller/callbacks_sys.c +msgid "Are you sure you want to skip backup?" +msgstr "确定跳过备份吗?" + +#: src/controller/callbacks_sys.c +msgid "Skipping backup may cause inability to recover!" +msgstr "跳过备份可能导致无法恢复!" + +#: src/controller/callbacks_sys.c +msgid "Skip Anyway" +msgstr "确定跳过" + +#: src/controller/callbacks_sys.c +msgid "Go Back" +msgstr "返回备份" + +#: src/controller/callbacks_sys.c +msgid "Continue Saving" +msgstr "继续保存" + +#: src/controller/callbacks_sys.c +msgid "Success" +msgstr "成功" + +#: src/controller/callbacks_sys.c +msgid "Both system and user PATH environment variables have been updated!" +msgstr "系统和用户 PATH 环境变量均已更新!" + +#: src/controller/callbacks_sys.c +msgid "Info" +msgstr "提示" + +#: src/controller/callbacks_sys.c +msgid "System variables saved successfully, but user variables failed to save." +msgstr "系统变量保存成功,但用户变量保存失败。" + +#: src/controller/callbacks_sys.c +msgid "User variables saved successfully, but system variables failed to save." +msgstr "用户变量保存成功,但系统变量保存失败。" + +#: src/controller/callbacks_sys.c +msgid "Failed to save!" +msgstr "保存失败!" + +#: src/controller/callbacks_sys.c +msgid "Unable to open system environment variable registry key, please try running as administrator." +msgstr "无法打开系统环境变量注册表键,请尝试以管理员身份运行。" + +#: src/controller/callbacks_sys.c +msgid "Usage Instructions" +msgstr "使用说明" + +#: src/controller/callbacks_sys.c +msgid "This program is used to edit system environment variable PATH." +msgstr "本程序用于编辑系统环境变量 PATH。" + +#: src/controller/callbacks_sys.c +msgid "Must run as" +msgstr "必须以" + +#: src/controller/callbacks_sys.c +msgid "Administrator" +msgstr "管理员身份" + +#: src/controller/callbacks_sys.c +msgid "to save changes." +msgstr "运行才能保存更改。" + +#: src/controller/callbacks_sys.c +msgid "Operations:" +msgstr "操作说明:" + +#: src/controller/callbacks_sys.c +msgid "New: Add new path to end of list." +msgstr "新建:添加新路径到列表末尾。" + +#: src/controller/callbacks_sys.c +msgid "Edit: Modify selected path." +msgstr "编辑:修改选中的路径。" + +#: src/controller/callbacks_sys.c +msgid "Browse: Select directory from file system to add." +msgstr "浏览:从文件系统选择目录添加。" + +#: src/controller/callbacks_sys.c +msgid "Delete: Remove selected path." +msgstr "删除:移除选中的路径。" + +#: src/controller/callbacks_sys.c +msgid "Up/Down: Adjust path priority." +msgstr "上移/下移:调整路径优先级。" + +#: src/controller/callbacks_sys.c +msgid "Import/Export: Backup and restore PATH configuration." +msgstr "导入/导出:备份和恢复 PATH 配置。" + +#: src/controller/callbacks_sys.c +msgid "Click" +msgstr "点击" + +#: src/controller/callbacks_sys.c +msgid "to save changes and apply." +msgstr "保存更改并生效。" + +#: src/controller/callbacks_sys.c +msgid "Note: Some running programs may need to restart to recognize new environment variables." +msgstr "注意:某些正在运行的程序可能需要重启才能识别新的环境变量。" + +#: src/controller/callbacks_sys.c +msgid "Author: LHY" +msgstr "作者:LHY" + +#: src/controller/callbacks_sys.c +msgid "Email: 3364451258@qq.com" +msgstr "邮箱:3364451258@qq.com" + +#: src/controller/callbacks_sys.c +msgid "GitHub: https://github.com/LHY0125/PathEditor" +msgstr "GitHub:https://github.com/LHY0125/PathEditor" + +#: src/controller/callbacks_sys.c +msgid "Don't forget to star my project!" +msgstr "记得给我的项目点个star!" + +#: src/controller/callbacks_io.c +msgid "Administrator privileges are required to import PATH!" +msgstr "需要管理员权限才能导入 PATH!" + +#: src/controller/callbacks_io.c +msgid "No valid paths found in file!" +msgstr "文件中没有找到有效的路径!" + +#: src/controller/callbacks_io.c +msgid "Import Options" +msgstr "导入选项" + +#: src/controller/callbacks_io.c +msgid "Please select import target:" +msgstr "请选择导入目标:" + +#: src/controller/callbacks_io.c +msgid "System Variables Only" +msgstr "仅系统变量" + +#: src/controller/callbacks_io.c +msgid "User Variables Only" +msgstr "仅用户变量" + +#: src/controller/callbacks_io.c +msgid "Import All" +msgstr "全部导入" + +#: src/controller/callbacks_io.c +msgid "Import to System" +msgstr "导入到系统变量" + +#: src/controller/callbacks_io.c +msgid "Import to User" +msgstr "导入到用户变量" + +#: src/controller/callbacks_io.c +msgid "Successfully imported %d paths!" +msgstr "成功导入 %d 个路径!" + +#: src/controller/callbacks_io.c +msgid "Import Success" +msgstr "导入成功" + +#: src/controller/callbacks_io.c +msgid "Import failed, please check if the file format is correct!" +msgstr "导入失败,请检查文件格式是否正确!" + +#: src/controller/callbacks_io.c +msgid "Export successful!" +msgstr "成功导出!" + +#: src/controller/callbacks_io.c +msgid "System variables: %d" +msgstr "系统变量: %d 个" + +#: src/controller/callbacks_io.c +msgid "User variables: %d" +msgstr "用户变量: %d 个" + +#: src/controller/callbacks_io.c +msgid "Save location: %s" +msgstr "保存位置: %s" + +#: src/controller/callbacks_io.c +msgid "Export Success" +msgstr "导出成功" + +#: src/controller/callbacks_io.c +msgid "Export failed!" +msgstr "导出失败!" + +#: src/controller/callbacks_nav.c +msgid "Confirm Cleanup" +msgstr "确认清理" + +#: src/controller/callbacks_nav.c +msgid "This operation will remove all" +msgstr "此操作将移除当前列表中所有" + +#: src/controller/callbacks_nav.c +msgid "invalid paths" +msgstr "无效路径" + +#: src/controller/callbacks_nav.c +msgid "and" +msgstr "和" + +#: src/controller/callbacks_nav.c +msgid "duplicate paths" +msgstr "重复路径" + +#: src/controller/callbacks_nav.c +msgid "from the current list." +msgstr "。" + +#: src/controller/callbacks_nav.c +msgid "Are you sure you want to continue?" +msgstr "确定要继续吗?" + +#: src/controller/callbacks_nav.c +msgid "Cleanup completed! Removed %d invalid or duplicate paths." +msgstr "清理完成!共移除了 %d 个无效或重复路径。" + +#: src/controller/callbacks_basic.c +msgid "New Environment Variable" +msgstr "新建环境变量" + +#: src/controller/callbacks_basic.c +msgid "Please enter a path:" +msgstr "请输入路径:" + +#: src/controller/callbacks_basic.c +msgid "Edit Environment Variable" +msgstr "编辑环境变量" + +#: src/controller/callbacks_basic.c +msgid "Edit path:" +msgstr "编辑路径:" + +#: src/controller/callbacks_basic.c +msgid "Please select an item to delete first" +msgstr "请先选择要删除的项" + +#: src/controller/callbacks_basic.c +msgid "This path already exists and will not be added again." +msgstr "该路径已存在,不会重复添加。" \ No newline at end of file diff --git a/src/controller/callbacks_basic.c b/src/controller/callbacks_basic.c index 39868e0..db3bb9f 100644 --- a/src/controller/callbacks_basic.c +++ b/src/controller/callbacks_basic.c @@ -18,11 +18,19 @@ int btn_new_cb(Ihandle *self) { Ihandle *dlg = IupGetDialog(self); char buffer[PATH_BUFFER_SIZE] = ""; - if (custom_input_dialog("新建环境变量", "请输入路径:", buffer, sizeof(buffer))) + if (custom_input_dialog(_("New Environment Variable"), _("Please enter a path:"), buffer, sizeof(buffer))) { if (strlen(buffer) > 0) { StringList *raw_data = get_current_raw_data(dlg); + + // 检查是否已存在重复路径 + if (string_list_contains(raw_data, buffer)) + { + IupMessage(_("Warning"), _("This path already exists and will not be added again.")); + return IUP_DEFAULT; + } + add_string_list(raw_data, buffer); Ihandle *current_list = get_current_list(dlg); @@ -51,7 +59,7 @@ int btn_edit_cb(Ihandle *self) char buffer[PATH_BUFFER_SIZE]; safe_strcpy(buffer, sizeof(buffer), string_list_get(raw_data, selected - 1)); - if (custom_input_dialog("编辑环境变量", "编辑路径:", buffer, sizeof(buffer))) + if (custom_input_dialog(_("Edit Environment Variable"), _("Edit path:"), buffer, sizeof(buffer))) { if (strlen(buffer) > 0) { @@ -91,6 +99,15 @@ int btn_browse_cb(Ihandle *self) if (value) { StringList *raw_data = get_current_raw_data(dlg); + + // 检查是否已存在重复路径 + if (string_list_contains(raw_data, value)) + { + IupMessage(_("Warning"), _("This path already exists and will not be added again.")); + IupDestroy(filedlg); + return IUP_DEFAULT; + } + add_string_list(raw_data, value); Ihandle *current_list = get_current_list(dlg); @@ -113,7 +130,7 @@ int btn_del_cb(Ihandle *self) if (selected == 0) { - IupMessage("提示", "请先选择要删除的项"); + IupMessage(_("Info"), _("Please select an item to delete first")); return IUP_DEFAULT; } diff --git a/src/controller/callbacks_io.c b/src/controller/callbacks_io.c index cb5fab0..0a55ce5 100644 --- a/src/controller/callbacks_io.c +++ b/src/controller/callbacks_io.c @@ -24,7 +24,7 @@ int btn_import_cb(Ihandle *self) if (!check_admin()) { - IupMessage("错误", "需要管理员权限才能导入 PATH!"); + IupMessage(_("Error"), _("Administrator privileges are required to import PATH!")); return IUP_DEFAULT; } @@ -52,7 +52,7 @@ int btn_import_cb(Ihandle *self) if (!has_system && !has_user) { - IupMessage("错误", "文件中没有找到有效的路径!"); + IupMessage(_("Error"), _("No valid paths found in file!")); clear_string_list(&imported.system); clear_string_list(&imported.user); IupDestroy(filedlg); @@ -62,14 +62,14 @@ int btn_import_cb(Ihandle *self) int choice = 0; if (has_system && has_user) { - choice = IupAlarm("导入选项", "请选择导入目标:", - "仅系统变量", "仅用户变量", "全部导入"); + choice = IupAlarm(_("Import Options"), _("Please select import target:"), + _("System Variables Only"), _("User Variables Only"), _("Import All")); } else if (has_system) { - // TXT 文件导入时,让用户选择目标(系统变量或用户变量) - choice = IupAlarm("导入选项", "请选择导入目标:", - "导入到系统变量", "导入到用户变量", NULL); + // TXT file import: let user choose target (system or user) + choice = IupAlarm(_("Import Options"), _("Please select import target:"), + _("Import to System"), _("Import to User"), NULL); // IupAlarm 返回 1 或 2,转换为 1(系统) 或 2(用户) } else @@ -108,8 +108,8 @@ int btn_import_cb(Ihandle *self) clear_string_list(&imported.user); char msg[256]; - snprintf(msg, sizeof(msg), "成功导入 %d 个路径!", total_imported); - IupMessage("导入成功", msg); + snprintf(msg, sizeof(msg), _("Successfully imported %d paths!"), total_imported); + IupMessage(_("Import Success"), msg); Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS); if (lbl_status) @@ -118,7 +118,7 @@ int btn_import_cb(Ihandle *self) else { log_error("Import failed: error code %d", import_result); - IupMessage("错误", "导入失败,请检查文件格式是否正确!"); + IupMessage(_("Error"), _("Import failed, please check if the file format is correct!")); } } } @@ -171,14 +171,14 @@ int btn_export_cb(Ihandle *self) if (export_result == ERR_OK) { char msg[512]; - snprintf(msg, sizeof(msg), "成功导出!\n系统变量: %d 个\n用户变量: %d 个\n\n保存位置: %s", + snprintf(msg, sizeof(msg), _("Export successful!\nSystem variables: %d\nUser variables: %d\n\nSave location: %s"), data.system.count, data.user.count, filepath); - IupMessage("导出成功", msg); + IupMessage(_("Export Success"), msg); } else { log_error("Export failed: error code %d", export_result); - IupMessage("错误", "导出失败!"); + IupMessage(_("Error"), _("Export failed!")); } } } diff --git a/src/controller/callbacks_nav.c b/src/controller/callbacks_nav.c index f9407a7..b261321 100644 --- a/src/controller/callbacks_nav.c +++ b/src/controller/callbacks_nav.c @@ -61,7 +61,7 @@ int btn_clean_cb(Ihandle *self) if (!raw_data || raw_data->count == 0) return IUP_DEFAULT; - if (IupAlarm("确认清理", "此操作将移除当前列表中所有【无效路径】和【重复路径】。\n确定要继续吗?", "确定", "取消", NULL) != 1) + if (IupAlarm(_("Confirm Cleanup"), _("This operation will remove all 【invalid paths】 and 【duplicate paths】 from the current list.\nAre you sure you want to continue?"), _("Confirm"), _("Cancel"), NULL) != 1) { return IUP_DEFAULT; } @@ -74,8 +74,8 @@ int btn_clean_cb(Ihandle *self) sync_string_list_to_ui(current_list, raw_data); char msg[128]; - snprintf(msg, sizeof(msg), "清理完成!共移除了 %d 个无效或重复路径。", removed); - IupMessage("提示", msg); + snprintf(msg, sizeof(msg), _("Cleanup completed! Removed %d invalid or duplicate paths."), removed); + IupMessage(_("Info"), msg); return IUP_DEFAULT; } diff --git a/src/controller/callbacks_sys.c b/src/controller/callbacks_sys.c index 15fdf50..d717ac2 100644 --- a/src/controller/callbacks_sys.c +++ b/src/controller/callbacks_sys.c @@ -24,7 +24,7 @@ int btn_ok_cb(Ihandle *self) if (!check_admin()) { - IupMessage("错误", "需要管理员权限才能保存更改!"); + IupMessage(_("Error"), _("Administrator privileges are required to save changes!")); return IUP_DEFAULT; } @@ -32,11 +32,11 @@ int btn_ok_cb(Ihandle *self) char custom_backup_dir[MAX_PATH] = ""; int do_backup = 1; // 是否执行备份 - int backup_choice = IupAlarm("备份设置", - "是否自定义备份目录?\n\n" - "选择「使用默认」将备份到 %APPDATA%/PathEditor/backups/\n" - "选择「自定义目录」可选择其他位置", - "使用默认", "自定义目录", "跳过备份"); + int backup_choice = IupAlarm(_("Backup Settings"), + _("Would you like to customize the backup directory?\n\n" + "Select 'Use Default' to backup to %%APPDATA%%/PathEditor/backups/\n" + "Select 'Custom Directory' to choose another location"), + _("Use Default"), _("Custom Directory"), _("Skip Backup")); if (backup_choice == 2) // 自定义目录 { @@ -56,13 +56,13 @@ int btn_ok_cb(Ihandle *self) if (strlen(custom_backup_dir) == 0) { - IupMessage("提示", "未选择目录,将使用默认备份路径。"); + IupMessage(_("Hint"), _("No directory selected, will use default backup path.")); } } else if (backup_choice == 3) // 跳过备份 { - int skip_confirm = IupAlarm("确认", "确定跳过备份吗?\n跳过备份可能导致无法恢复!", - "确定跳过", "返回备份", NULL); + int skip_confirm = IupAlarm(_("Confirm"), _("Are you sure you want to skip backup?\nSkipping backup may cause inability to recover!"), + _("Skip Anyway"), _("Go Back"), NULL); if (skip_confirm != 1) { // 用户反悔,重新询问 @@ -89,7 +89,7 @@ int btn_ok_cb(Ihandle *self) char msg[512]; snprintf(msg, sizeof(msg), "备份失败!原因:%s\n\n是否继续保存?\n(继续保存可能导致无法恢复)", reason); - int choice = IupAlarm("警告", msg, "继续保存", "取消", NULL); + int choice = IupAlarm(_("Warning"), msg, _("Continue Saving"), _("Cancel"), NULL); if (choice != 1) return IUP_DEFAULT; } @@ -104,22 +104,22 @@ int btn_ok_cb(Ihandle *self) { log_info("Saved system paths: %d, user paths: %d", ctx->sys_paths.count, ctx->user_paths.count); SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)L"Environment", SMTO_ABORTIFHUNG, 5000, NULL); - IupMessage("成功", "系统和用户 PATH 环境变量均已更新!"); + IupMessage(_("Success"), _("Both system and user PATH environment variables have been updated!")); if (lbl_status) IupSetAttribute(lbl_status, "TITLE", lua_config_get_string("status", "saved")); } else if (sys_ok == ERR_OK) { - IupMessage("提示", "系统变量保存成功,但用户变量保存失败。"); + IupMessage(_("Info"), _("System variables saved successfully, but user variables failed to save.")); } else if (user_ok == ERR_OK) { - IupMessage("提示", "用户变量保存成功,但系统变量保存失败。"); + IupMessage(_("Info"), _("User variables saved successfully, but system variables failed to save.")); } else { log_error("Failed to save paths: sys=%d, user=%d", sys_ok, user_ok); - IupMessage("错误", "保存失败!"); + IupMessage(_("Error"), _("Failed to save!")); if (lbl_status) IupSetAttribute(lbl_status, "TITLE", lua_config_get_string("status", "error")); } @@ -146,7 +146,7 @@ void load_all_paths(void) if (load_system_paths(&ctx->sys_paths) != ERR_OK) { log_error("Failed to load system paths"); - IupMessage("错误", "无法打开系统环境变量注册表键,请尝试以管理员身份运行。"); + IupMessage(_("Error"), _("Unable to open system environment variable registry key, please try running as administrator.")); } else { @@ -182,23 +182,23 @@ int btn_lang_cb(Ihandle *self) // 按钮回调:帮助 int btn_help_cb(Ihandle *self) { - IupMessage("使用说明", - "1. 本程序用于编辑系统环境变量 PATH。\n" - "2. 必须以【管理员身份】运行才能保存更改。\n" - "3. 操作说明:\n" - " - 新建:添加新路径到列表末尾。\n" - " - 编辑:修改选中的路径。\n" - " - 浏览:从文件系统选择目录添加。\n" - " - 删除:移除选中的路径。\n" - " - 上移/下移:调整路径优先级。\n" - " - 导入/导出:备份和恢复 PATH 配置。\n" - "4. 点击【确定】保存更改并生效。\n" - "5. 注意:某些正在运行的程序可能需要重启才能识别新的环境变量。\n\n" - "--------------------------------------------------\n" - "作者:LHY\n" - "邮箱:3364451258@qq.com\n" - "GitHub:https://github.com/LHY0125/PathEditor\n" - "记得给我的项目点个star!"); + IupMessage(_("Usage Instructions"), + _("1. This program is used to edit system environment variable PATH.\n" + "2. Must run as 【Administrator】 to save changes.\n" + "3. Operations:\n" + " - New: Add new path to end of list.\n" + " - Edit: Modify selected path.\n" + " - Browse: Select directory from file system to add.\n" + " - Delete: Remove selected path.\n" + " - Up/Down: Adjust path priority.\n" + " - Import/Export: Backup and restore PATH configuration.\n" + "4. Click 【OK】 to save changes and apply.\n" + "5. Note: Some running programs may need to restart to recognize new environment variables.\n\n" + "--------------------------------------------------\n" + "Author: LHY\n" + "Email: 3364451258@qq.com\n" + "GitHub: https://github.com/LHY0125/PathEditor\n" + "Don't forget to star my project!")); return IUP_DEFAULT; } diff --git a/src/utils/error_code.c b/src/utils/error_code.c new file mode 100644 index 0000000..2e8e145 --- /dev/null +++ b/src/utils/error_code.c @@ -0,0 +1,40 @@ +#include "utils/error_code.h" +#include + +const char* error_code_to_string(ErrorCode code) +{ + switch (code) + { + case ERR_OK: + return "Success"; + case ERR_FAILED: + return "Operation failed"; + case ERR_NULL_PTR: + return "Null pointer error"; + case ERR_OUT_OF_MEMORY: + return "Out of memory"; + case ERR_FILE_NOT_FOUND: + return "File not found"; + case ERR_PERMISSION_DENIED: + return "Permission denied"; + case ERR_INVALID_FORMAT: + return "Invalid format"; + case ERR_REGISTRY_FAILED: + return "Registry operation failed"; + case ERR_NOT_FOUND: + return "Item not found"; + case ERR_EXISTS: + return "Item already exists"; + case ERR_INVALID_INDEX: + return "Invalid index"; + default: + return "Unknown error"; + } +} + +// 注意:error_code_to_message 需要链接 intl 库, +// 如果不需要在错误码模块使用国际化,可以直接使用 error_code_to_string +const char* error_code_to_message(ErrorCode code) +{ + return error_code_to_string(code); +} \ No newline at end of file diff --git a/src/utils/string_ext.c b/src/utils/string_ext.c index fdcfcc8..fd8a096 100644 --- a/src/utils/string_ext.c +++ b/src/utils/string_ext.c @@ -126,4 +126,17 @@ void clear_string_list(StringList *list) list->items = NULL; list->count = 0; list->capacity = 0; +} + +// 检查字符串列表中是否存在指定路径(不区分大小写) +int string_list_contains(const StringList *list, const char *str) +{ + if (!list || !str) + return 0; + for (int i = 0; i < list->count; i++) + { + if (list->items[i] && _stricmp(list->items[i], str) == 0) + return 1; + } + return 0; } \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..cbaf685 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,40 @@ +# PathEditor 单元测试框架 +cmake_minimum_required(VERSION 3.10) + +# 测试子项目配置 +project(PathEditorTests C) + +# 设置 C 标准(与主项目一致) +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) + +# 包含主项目的头文件路径 +include_directories( + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_SOURCE_DIR}/include/core + ${CMAKE_SOURCE_DIR}/include/ui + ${CMAKE_SOURCE_DIR}/include/controller + ${CMAKE_SOURCE_DIR}/include/utils + ${CMAKE_SOURCE_DIR}/libs/IUP/include + ${CMAKE_SOURCE_DIR}/libs/lua/include + ${CMAKE_SOURCE_DIR}/libs/gettext/include +) + +# 获取 CMocka(如果系统没有则下载) +include(FetchContent) +FetchContent_Declare( + cmocka + GIT_REPOSITORY https://git.cryptomilk.org/projects/cmocka.git + GIT_TAG cmocka-1.1.5 +) +FetchContent_MakeAvailable(cmocka) + +# 启用测试 +enable_testing() +include(CTest) + +# 添加各测试模块 +add_subdirectory(unit/safe_string) +add_subdirectory(unit/string_ext) +add_subdirectory(unit/path_manager) diff --git a/tests/mocks/mock_windows.h b/tests/mocks/mock_windows.h new file mode 100644 index 0000000..e8f80fa --- /dev/null +++ b/tests/mocks/mock_windows.h @@ -0,0 +1,54 @@ +/* + * mock_windows.h + * Windows API Mock 头文件 + * 用于单元测试中模拟 Windows API + */ +#ifndef MOCK_WINDOWS_H +#define MOCK_WINDOWS_H + +#ifdef TESTING + +#include +#include + +/* Mock 计数器,用于验证调用 */ +extern int mock_MultiByteToWideChar_call_count; +extern int mock_WideCharToMultiByte_call_count; + +/* 设置 Mock 返回值 */ +void mock_set_MultiByteToWideChar_return(int ret); +void mock_set_WideCharToMultiByte_return(int ret); + +/* Mock MultiByteToWideChar */ +int mock_MultiByteToWideChar( + UINT CodePage, + DWORD dwFlags, + LPCSTR lpMultiByteStr, + int cbMultiByte, + LPWSTR lpWideCharStr, + int cchWideChar); + +/* Mock WideCharToMultiByte */ +int mock_WideCharToMultiByte( + UINT CodePage, + DWORD dwFlags, + LPCWSTR lpWideCharStr, + int cchWideChar, + LPSTR lpMultiByteStr, + int cbMultiByte, + LPCSTR lpDefaultChar, + LPBOOL lpUsedDefaultChar); + +/* 替换宏(在测试源文件中定义) */ +#ifdef REPLACE_WINDOWS_API + #define MultiByteToWideChar mock_MultiByteToWideChar + #define WideCharToMultiByte mock_WideCharToMultiByte +#endif + +#else +/* 非测试模式下为空 */ +#define REPLACE_WINDOWS_API 0 + +#endif /* TESTING */ + +#endif /* MOCK_WINDOWS_H */ diff --git a/tests/unit/path_manager/CMakeLists.txt b/tests/unit/path_manager/CMakeLists.txt new file mode 100644 index 0000000..cf15f63 --- /dev/null +++ b/tests/unit/path_manager/CMakeLists.txt @@ -0,0 +1,20 @@ +# path_manager 单元测试 +add_executable(test_path_manager test_path_manager.c + ${CMAKE_SOURCE_DIR}/src/core/path_manager.c + ${CMAKE_SOURCE_DIR}/src/utils/string_ext.c + ${CMAKE_SOURCE_DIR}/src/utils/safe_string.c + ${CMAKE_SOURCE_DIR}/src/utils/error_code.c +) + +target_link_libraries(test_path_manager cmocka) + +target_include_directories(test_path_manager PRIVATE + ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/utils +) + +# 定义 TESTING 宏以启用 mock +target_compile_definitions(test_path_manager PRIVATE TESTING) + +# 添加测试 +add_test(NAME path_manager_test COMMAND test_path_manager) diff --git a/tests/unit/path_manager/test_path_manager.c b/tests/unit/path_manager/test_path_manager.c new file mode 100644 index 0000000..8b1edf8 --- /dev/null +++ b/tests/unit/path_manager/test_path_manager.c @@ -0,0 +1,359 @@ +/* + * path_manager.c 单元测试 + * 测试路径管理函数 + */ +#include +#include +#include +#include +#include +#include +#include "core/path_manager.h" + +/* ==================== Mock 函数 ==================== */ + +#ifdef TESTING + +/* Mock is_path_valid - 默认返回 1(有效)*/ +int is_path_valid_mock_enabled = 0; +int is_path_valid_mock_return = 1; + +int is_path_valid(const char *path) +{ + (void)path; + if (is_path_valid_mock_enabled) { + return is_path_valid_mock_return; + } + return 1; /* 默认认为路径有效 */ +} + +/* Mock 日志函数 - 避免链接日志文件依赖 */ +int log_info_enabled = 0; +int log_debug_enabled = 0; +int log_warn_enabled = 0; +int log_error_enabled = 0; + +void log_info(const char *fmt, ...) +{ + (void)fmt; + log_info_enabled++; +} + +void log_debug(const char *fmt, ...) +{ + (void)fmt; + log_debug_enabled++; +} + +void log_warn(const char *fmt, ...) +{ + (void)fmt; + log_warn_enabled++; +} + +void log_error(const char *fmt, ...) +{ + (void)fmt; + log_error_enabled++; +} + +#endif /* TESTING */ + +/* ==================== path_manager_remove_at 测试 ==================== */ + +static void test_remove_at_normal(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + add_string_list(&list, "path3"); + + ErrorCode result = path_manager_remove_at(&list, 1); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, 2); + assert_string_equal(string_list_get(&list, 0), "path1"); + assert_string_equal(string_list_get(&list, 1), "path3"); + + clear_string_list(&list); +} + +static void test_remove_at_first(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_remove_at(&list, 0); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, 1); + assert_string_equal(string_list_get(&list, 0), "path2"); + + clear_string_list(&list); +} + +static void test_remove_at_last(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_remove_at(&list, 1); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, 1); + assert_string_equal(string_list_get(&list, 0), "path1"); + + clear_string_list(&list); +} + +static void test_remove_at_invalid_index(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + + ErrorCode result = path_manager_remove_at(&list, 5); /* 越界 */ + + assert_int_not_equal(result, ERR_OK); + + clear_string_list(&list); +} + +static void test_remove_at_null(void **state) +{ + (void)state; + ErrorCode result = path_manager_remove_at(NULL, 0); + assert_int_equal(result, ERR_NULL_PTR); +} + +/* ==================== path_manager_move_up 测试 ==================== */ + +static void test_move_up_normal(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + add_string_list(&list, "path3"); + + ErrorCode result = path_manager_move_up(&list, 2); + + assert_int_equal(result, ERR_OK); + assert_string_equal(string_list_get(&list, 0), "path1"); + assert_string_equal(string_list_get(&list, 1), "path3"); + assert_string_equal(string_list_get(&list, 2), "path2"); + + clear_string_list(&list); +} + +static void test_move_up_first_element(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_move_up(&list, 0); /* 第一个元素无法上移 */ + + assert_int_equal(result, ERR_INVALID_INDEX); + + clear_string_list(&list); +} + +static void test_move_up_invalid_index(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_move_up(&list, 5); /* 越界 */ + + assert_int_not_equal(result, ERR_OK); + + clear_string_list(&list); +} + +static void test_move_up_single_element(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + + ErrorCode result = path_manager_move_up(&list, 0); + + assert_int_equal(result, ERR_INVALID_INDEX); + assert_string_equal(string_list_get(&list, 0), "path1"); + + clear_string_list(&list); +} + +/* ==================== path_manager_move_down 测试 ==================== */ + +static void test_move_down_normal(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + add_string_list(&list, "path3"); + + ErrorCode result = path_manager_move_down(&list, 1); + + assert_int_equal(result, ERR_OK); + assert_string_equal(string_list_get(&list, 0), "path1"); + assert_string_equal(string_list_get(&list, 1), "path3"); + assert_string_equal(string_list_get(&list, 2), "path2"); + + clear_string_list(&list); +} + +static void test_move_down_last_element(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_move_down(&list, 1); /* 最后一个元素无法下移 */ + + assert_int_equal(result, ERR_INVALID_INDEX); + + clear_string_list(&list); +} + +static void test_move_down_invalid_index(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + add_string_list(&list, "path2"); + + ErrorCode result = path_manager_move_down(&list, 5); /* 越界 */ + + assert_int_not_equal(result, ERR_OK); + + clear_string_list(&list); +} + +static void test_move_down_single_element(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + add_string_list(&list, "path1"); + + ErrorCode result = path_manager_move_down(&list, 0); + + assert_int_equal(result, ERR_INVALID_INDEX); + assert_string_equal(string_list_get(&list, 0), "path1"); + + clear_string_list(&list); +} + +/* ==================== path_manager_clean 测试 ==================== */ + +static void test_clean_no_invalid(void **state) +{ + (void)state; + is_path_valid_mock_enabled = 1; + is_path_valid_mock_return = 1; + + StringList list; + init_string_list(&list); + add_string_list(&list, "C:\\Windows"); + add_string_list(&list, "C:\\Program Files"); + + int before_count = list.count; + ErrorCode result = path_manager_clean(&list); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, before_count); /* 没有无效路径,不应删除 */ + + is_path_valid_mock_enabled = 0; + clear_string_list(&list); +} + +static void test_clean_with_invalid(void **state) +{ + (void)state; + is_path_valid_mock_enabled = 1; + is_path_valid_mock_return = 0; /* 所有路径都无效 */ + + StringList list; + init_string_list(&list); + add_string_list(&list, "C:\\Invalid1"); + add_string_list(&list, "C:\\Invalid2"); + add_string_list(&list, "C:\\Invalid3"); + + ErrorCode result = path_manager_clean(&list); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, 0); /* 所有路径都被删除 */ + + is_path_valid_mock_enabled = 0; + clear_string_list(&list); +} + +static void test_clean_empty_list(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + ErrorCode result = path_manager_clean(&list); + + assert_int_equal(result, ERR_OK); + assert_int_equal(list.count, 0); + + clear_string_list(&list); +} + +/* ==================== 主函数 ==================== */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* remove_at 测试 */ + cmocka_unit_test(test_remove_at_normal), + cmocka_unit_test(test_remove_at_first), + cmocka_unit_test(test_remove_at_last), + cmocka_unit_test(test_remove_at_invalid_index), + cmocka_unit_test(test_remove_at_null), + + /* move_up 测试 */ + cmocka_unit_test(test_move_up_normal), + cmocka_unit_test(test_move_up_first_element), + cmocka_unit_test(test_move_up_invalid_index), + cmocka_unit_test(test_move_up_single_element), + + /* move_down 测试 */ + cmocka_unit_test(test_move_down_normal), + cmocka_unit_test(test_move_down_last_element), + cmocka_unit_test(test_move_down_invalid_index), + cmocka_unit_test(test_move_down_single_element), + + /* clean 测试 */ + cmocka_unit_test(test_clean_no_invalid), + cmocka_unit_test(test_clean_with_invalid), + cmocka_unit_test(test_clean_empty_list), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/unit/safe_string/CMakeLists.txt b/tests/unit/safe_string/CMakeLists.txt new file mode 100644 index 0000000..2bb02e1 --- /dev/null +++ b/tests/unit/safe_string/CMakeLists.txt @@ -0,0 +1,16 @@ +# safe_string 单元测试 +add_executable(test_safe_string test_safe_string.c + ${CMAKE_SOURCE_DIR}/src/utils/safe_string.c +) + +target_link_libraries(test_safe_string cmocka) + +target_include_directories(test_safe_string PRIVATE + ${CMAKE_SOURCE_DIR}/src/utils +) + +# 定义 TESTING 宏以启用 mock +target_compile_definitions(test_safe_string PRIVATE TESTING) + +# 添加测试 +add_test(NAME safe_string_test COMMAND test_safe_string) diff --git a/tests/unit/safe_string/test_safe_string.c b/tests/unit/safe_string/test_safe_string.c new file mode 100644 index 0000000..8a54199 --- /dev/null +++ b/tests/unit/safe_string/test_safe_string.c @@ -0,0 +1,209 @@ +/* + * safe_string.c 单元测试 + * 测试安全字符串操作函数 + */ +#include +#include +#include +#include +#include +#include +#include "utils/safe_string.h" + +/* ==================== safe_strcpy 测试 ==================== */ + +static void test_safe_strcpy_normal(void **state) +{ + (void)state; + char dst[32]; + const char *src = "Hello, World!"; + char *result = safe_strcpy(dst, sizeof(dst), src); + + assert_non_null(result); + assert_string_equal(dst, src); +} + +static void test_safe_strcpy_truncation(void **state) +{ + (void)state; + char dst[8]; + const char *src = "This is a long string"; + char *result = safe_strcpy(dst, sizeof(dst), src); + + assert_non_null(result); + assert_int_equal(strlen(dst), 7); /* 截断到 7 字符 */ + assert_memory_equal(dst, src, 7); /* 前 7 字符相同 */ + assert_int_equal(dst[7], '\0'); +} + +static void test_safe_strcpy_null_dst(void **state) +{ + (void)state; + const char *src = "test"; + char *result = safe_strcpy(NULL, 10, src); + assert_null(result); +} + +static void test_safe_strcpy_null_src(void **state) +{ + (void)state; + char dst[32]; + char *result = safe_strcpy(dst, sizeof(dst), NULL); + assert_null(result); +} + +static void test_safe_strcpy_zero_size(void **state) +{ + (void)state; + char dst[32]; + char *result = safe_strcpy(dst, 0, "test"); + assert_null(result); +} + +static void test_safe_strcpy_exact_fit(void **state) +{ + (void)state; + char dst[6]; + const char *src = "12345"; /* 5字符 + 1终止符 = 6 */ + char *result = safe_strcpy(dst, sizeof(dst), src); + + assert_non_null(result); + assert_string_equal(dst, "12345"); +} + +/* ==================== safe_strcat 测试 ==================== */ + +static void test_safe_strcat_normal(void **state) +{ + (void)state; + char dst[32] = "Hello"; + const char *src = ", World!"; + char *result = safe_strcat(dst, sizeof(dst), src); + + assert_non_null(result); + assert_string_equal(dst, "Hello, World!"); +} + +static void test_safe_strcat_truncation(void **state) +{ + (void)state; + char dst[12] = "Hello"; + const char *src = ", World!"; /* 总长 12,但最后一个位置要放 \0 */ + char *result = safe_strcat(dst, sizeof(dst), src); + + assert_non_null(result); + /* dst 有 11 可用位置 (12-1),"Hello" 占 5,还剩 6 */ + /* ", World!" 有 9 字符,只能放 6 个字符 + \0 */ + assert_true(strlen(dst) <= 11); +} + +static void test_safe_strcat_null_dst(void **state) +{ + (void)state; + char dst[32] = "test"; + char *result = safe_strcat(NULL, sizeof(dst), "src"); + assert_null(result); +} + +static void test_safe_strcat_null_src(void **state) +{ + (void)state; + char dst[32] = "test"; + char *result = safe_strcat(dst, sizeof(dst), NULL); + /* src 为 NULL 时函数返回 NULL */ + assert_null(result); +} + +static void test_safe_strcat_empty_dst(void **state) +{ + (void)state; + char dst[32] = ""; + const char *src = "Hello"; + char *result = safe_strcat(dst, sizeof(dst), src); + + assert_non_null(result); + assert_string_equal(dst, "Hello"); +} + +/* ==================== safe_strdup 测试 ==================== */ + +static void test_safe_strdup_normal(void **state) +{ + (void)state; + const char *src = "Hello, World!"; + char *result = safe_strdup(src); + + assert_non_null(result); + assert_string_equal(result, src); + assert_ptr_not_equal(result, src); /* 必须是新分配的内存 */ + + free(result); +} + +static void test_safe_strdup_null(void **state) +{ + (void)state; + char *result = safe_strdup(NULL); + assert_null(result); +} + +static void test_safe_strdup_empty_string(void **state) +{ + (void)state; + const char *src = ""; + char *result = safe_strdup(src); + + assert_non_null(result); + assert_string_equal(result, ""); + + free(result); +} + +static void test_safe_strdup_long_string(void **state) +{ + (void)state; + /* 构造一个 510 字符的字符串 */ + char src[511]; + for (int i = 0; i < 510; i++) { + src[i] = 'A'; + } + src[510] = '\0'; + + char *result = safe_strdup(src); + + assert_non_null(result); + assert_int_equal(strlen(result), 510); + assert_memory_equal(result, src, 510); + + free(result); +} + +/* ==================== 主函数 ==================== */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* safe_strcpy 测试 */ + cmocka_unit_test(test_safe_strcpy_normal), + cmocka_unit_test(test_safe_strcpy_truncation), + cmocka_unit_test(test_safe_strcpy_null_dst), + cmocka_unit_test(test_safe_strcpy_null_src), + cmocka_unit_test(test_safe_strcpy_zero_size), + cmocka_unit_test(test_safe_strcpy_exact_fit), + + /* safe_strcat 测试 */ + cmocka_unit_test(test_safe_strcat_normal), + cmocka_unit_test(test_safe_strcat_truncation), + cmocka_unit_test(test_safe_strcat_null_dst), + cmocka_unit_test(test_safe_strcat_null_src), + cmocka_unit_test(test_safe_strcat_empty_dst), + + /* safe_strdup 测试 */ + cmocka_unit_test(test_safe_strdup_normal), + cmocka_unit_test(test_safe_strdup_null), + cmocka_unit_test(test_safe_strdup_empty_string), + cmocka_unit_test(test_safe_strdup_long_string), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/tests/unit/string_ext/CMakeLists.txt b/tests/unit/string_ext/CMakeLists.txt new file mode 100644 index 0000000..27fb94a --- /dev/null +++ b/tests/unit/string_ext/CMakeLists.txt @@ -0,0 +1,18 @@ +# string_ext 单元测试 +add_executable(test_string_ext test_string_ext.c + ${CMAKE_SOURCE_DIR}/src/utils/string_ext.c + ${CMAKE_SOURCE_DIR}/src/utils/safe_string.c +) + +target_link_libraries(test_string_ext cmocka) + +target_include_directories(test_string_ext PRIVATE + ${CMAKE_SOURCE_DIR}/src/utils + ${CMAKE_SOURCE_DIR}/tests/mocks +) + +# 定义 TESTING 宏和 REPLACE_WINDOWS_API 以启用 mock +target_compile_definitions(test_string_ext PRIVATE TESTING REPLACE_WINDOWS_API) + +# 添加测试 +add_test(NAME string_ext_test COMMAND test_string_ext) diff --git a/tests/unit/string_ext/test_string_ext.c b/tests/unit/string_ext/test_string_ext.c new file mode 100644 index 0000000..1edb428 --- /dev/null +++ b/tests/unit/string_ext/test_string_ext.c @@ -0,0 +1,354 @@ +/* + * string_ext.c 单元测试 + * 测试字符串扩展函数和 StringList 操作 + */ +#include +#include +#include +#include +#include +#include +#include "utils/string_ext.h" +#include "mock_windows.h" + +/* ==================== Mock 计数器 ==================== */ +int mock_MultiByteToWideChar_call_count = 0; +int mock_WideCharToMultiByte_call_count = 0; +static int mock_MB2WC_return = 0; +static int mock_WC2MB_return = 0; + +void mock_set_MultiByteToWideChar_return(int ret) { + mock_MB2WC_return = ret; +} + +void mock_set_WideCharToMultiByte_return(int ret) { + mock_WC2MB_return = ret; +} + +/* ==================== Mock 实现 ==================== */ + +int mock_MultiByteToWideChar( + UINT CodePage, + DWORD dwFlags, + LPCSTR lpMultiByteStr, + int cbMultiByte, + LPWSTR lpWideCharStr, + int cchWideChar) +{ + (void)CodePage; + (void)dwFlags; + mock_MultiByteToWideChar_call_count++; + + if (!lpMultiByteStr || cbMultiByte == 0) + return 0; + + int len = (cbMultiByte == -1) ? strlen(lpMultiByteStr) : cbMultiByte; + + /* 简单 ASCII 转宽字符 */ + if (lpWideCharStr && cchWideChar > 0) { + int copy_len = (cchWideChar < len + 1) ? cchWideChar - 1 : len; + for (int i = 0; i < copy_len; i++) { + lpWideCharStr[i] = (wchar_t)lpMultiByteStr[i]; + } + lpWideCharStr[copy_len] = L'\0'; + } + + return mock_MB2WC_return > 0 ? mock_MB2WC_return : (len + 1); +} + +int mock_WideCharToMultiByte( + UINT CodePage, + DWORD dwFlags, + LPCWSTR lpWideCharStr, + int cchWideChar, + LPSTR lpMultiByteStr, + int cbMultiByte, + LPCSTR lpDefaultChar, + LPBOOL lpUsedDefaultChar) +{ + (void)CodePage; + (void)dwFlags; + (void)lpDefaultChar; + (void)lpUsedDefaultChar; + mock_WideCharToMultiByte_call_count++; + + if (!lpWideCharStr || cchWideChar == 0) + return 0; + + int len = (cchWideChar == -1) ? wcslen(lpWideCharStr) : cchWideChar; + + if (lpMultiByteStr && cbMultiByte > 0) { + int copy_len = (cbMultiByte < len + 1) ? cbMultiByte - 1 : len; + for (int i = 0; i < copy_len; i++) { + lpMultiByteStr[i] = (char)lpWideCharStr[i]; + } + lpMultiByteStr[copy_len] = '\0'; + } + + return mock_WC2MB_return > 0 ? mock_WC2MB_return : (len + 1); +} + +/* ==================== StringList 测试 ==================== */ + +static void test_init_string_list(void **state) +{ + (void)state; + StringList list; + list.items = (void *)0x1234; /* 初始化前设置垃圾值 */ + list.count = 999; + list.capacity = 999; + + init_string_list(&list); + + /* init_string_list 将 items 设置为 NULL,count 和 capacity 设为 0 */ + assert_null(list.items); + assert_int_equal(list.count, 0); + assert_int_equal(list.capacity, 0); + + /* clear_string_list 对 NULL items 应该安全处理 */ + clear_string_list(&list); +} + +static void test_add_string_list_single(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "C:\\Windows"); + + assert_int_equal(list.count, 1); + assert_string_equal(string_list_get(&list, 0), "C:\\Windows"); + + clear_string_list(&list); +} + +static void test_add_string_list_multiple(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "C:\\Windows"); + add_string_list(&list, "C:\\Program Files"); + add_string_list(&list, "D:\\Tools"); + + assert_int_equal(list.count, 3); + assert_string_equal(string_list_get(&list, 0), "C:\\Windows"); + assert_string_equal(string_list_get(&list, 1), "C:\\Program Files"); + assert_string_equal(string_list_get(&list, 2), "D:\\Tools"); + + clear_string_list(&list); +} + +static void test_string_list_get_out_of_bounds(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "test"); + + assert_null(string_list_get(&list, -1)); + assert_null(string_list_get(&list, 1)); + assert_null(string_list_get(&list, 100)); + + clear_string_list(&list); +} + +static void test_string_list_get_null_list(void **state) +{ + (void)state; + assert_null(string_list_get(NULL, 0)); +} + +static void test_string_list_set_normal(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "original"); + int result = string_list_set(&list, 0, "modified"); + + assert_int_equal(result, 0); + assert_string_equal(string_list_get(&list, 0), "modified"); + + clear_string_list(&list); +} + +static void test_string_list_set_out_of_bounds(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "test"); + int result = string_list_set(&list, 5, "modified"); + + assert_int_equal(result, -1); + + clear_string_list(&list); +} + +static void test_string_list_set_null_list(void **state) +{ + (void)state; + int result = string_list_set(NULL, 0, "test"); + assert_int_equal(result, -1); +} + +static void test_clear_string_list(void **state) +{ + (void)state; + StringList list; + init_string_list(&list); + + add_string_list(&list, "item1"); + add_string_list(&list, "item2"); + add_string_list(&list, "item3"); + + clear_string_list(&list); + + assert_int_equal(list.count, 0); + assert_null(list.items); +} + +/* ==================== 编码转换测试 ==================== */ + +static void test_utf8_to_wide_normal(void **state) +{ + (void)state; + const char *utf8_str = "Hello"; + + wchar_t *result = utf8_to_wide(utf8_str); + + if (result) { + assert_true(wcscmp(result, L"Hello") == 0); + free(result); + } +} + +static void test_utf8_to_wide_null(void **state) +{ + (void)state; + wchar_t *result = utf8_to_wide(NULL); + assert_null(result); +} + +static void test_wide_to_utf8_normal(void **state) +{ + (void)state; + const wchar_t *wide_str = L"World"; + + char *result = wide_to_utf8(wide_str); + + if (result) { + assert_string_equal(result, "World"); + free(result); + } +} + +static void test_wide_to_utf8_null(void **state) +{ + (void)state; + char *result = wide_to_utf8(NULL); + assert_null(result); +} + +/* ==================== stristr 测试 ==================== */ + +static void test_stristr_found(void **state) +{ + (void)state; + const char *haystack = "The quick brown fox"; + const char *needle = "quick"; + + char *result = stristr(haystack, needle); + + assert_non_null(result); + assert_ptr_equal(result, haystack + 4); /* "quick" 在 "The " 之后 */ +} + +static void test_stristr_not_found(void **state) +{ + (void)state; + const char *haystack = "The quick brown fox"; + const char *needle = "jumps"; + + char *result = stristr(haystack, needle); + + assert_null(result); +} + +static void test_stristr_case_insensitive(void **state) +{ + (void)state; + const char *haystack = "The QUICK brown fox"; + const char *needle = "quick"; + + char *result = stristr(haystack, needle); + + assert_non_null(result); +} + +static void test_stristr_null_haystack(void **state) +{ + (void)state; + char *result = stristr(NULL, "test"); + assert_null(result); +} + +static void test_stristr_null_needle(void **state) +{ + (void)state; + char *result = stristr("test", NULL); + assert_null(result); +} + +static void test_stristr_empty_needle(void **state) +{ + (void)state; + const char *haystack = "The quick brown fox"; + const char *needle = ""; + + char *result = stristr(haystack, needle); + + /* 空字符串应该返回原字符串首地址 */ + assert_non_null(result); + assert_ptr_equal(result, haystack); +} + +/* ==================== 主函数 ==================== */ + +int main(void) +{ + const struct CMUnitTest tests[] = { + /* StringList 测试 */ + cmocka_unit_test(test_init_string_list), + cmocka_unit_test(test_add_string_list_single), + cmocka_unit_test(test_add_string_list_multiple), + cmocka_unit_test(test_string_list_get_out_of_bounds), + cmocka_unit_test(test_string_list_get_null_list), + cmocka_unit_test(test_string_list_set_normal), + cmocka_unit_test(test_string_list_set_out_of_bounds), + cmocka_unit_test(test_string_list_set_null_list), + cmocka_unit_test(test_clear_string_list), + + /* 编码转换测试 */ + cmocka_unit_test(test_utf8_to_wide_normal), + cmocka_unit_test(test_utf8_to_wide_null), + cmocka_unit_test(test_wide_to_utf8_normal), + cmocka_unit_test(test_wide_to_utf8_null), + + /* stristr 测试 */ + cmocka_unit_test(test_stristr_found), + cmocka_unit_test(test_stristr_not_found), + cmocka_unit_test(test_stristr_case_insensitive), + cmocka_unit_test(test_stristr_null_haystack), + cmocka_unit_test(test_stristr_null_needle), + cmocka_unit_test(test_stristr_empty_needle), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}