feat(test): 添加单元测试框架并完善国际化支持

- 新增 CMake 测试框架配置,支持 safe_string、string_ext 和 path_manager 模块的单元测试
- 实现 Windows API Mock 机制,便于测试编码转换函数
- 添加 error_code 模块的字符串表示函数,支持英文错误日志
- 在 UI 回调函数中集成国际化翻译,覆盖新建、编辑、导入导出等操作提示
- 扩展 string_list 功能,新增重复路径检查函数
- 更新翻译文件,同步所有用户界面的中英文文本
This commit is contained in:
2026-04-30 11:38:05 +08:00
parent ceed90aea8
commit cf19a37a97
20 changed files with 1778 additions and 55 deletions
+10
View File
@@ -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
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/locale"
COMMENT "Copying locale directory to build directory..."
)
# 测试支持
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
+1
View File
@@ -3,6 +3,7 @@
#include <iup.h>
#include "core/app_context.h"
#include "utils/i18n.h"
// 内部辅助函数声明(供各 callbacks_*.c 文件共享)
// 这些函数不对外暴露,仅在 controller 层内部使用
+1
View File
@@ -15,6 +15,7 @@ typedef enum {
ERR_INVALID_INDEX = -10 // 无效索引
} ErrorCode;
// 获取错误码的字符串表示(英文,用于日志)
const char* error_code_to_string(ErrorCode code);
#endif // ERROR_CODE_H
+3
View File
@@ -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
+286 -2
View File
@@ -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."
+286 -2
View File
@@ -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 "GitHubhttps://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 "该路径已存在,不会重复添加。"
+20 -3
View File
@@ -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;
}
+13 -13
View File
@@ -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!"));
}
}
}
+3 -3
View File
@@ -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;
}
+32 -32
View File
@@ -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"
"GitHubhttps://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;
}
+40
View File
@@ -0,0 +1,40 @@
#include "utils/error_code.h"
#include <string.h>
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);
}
+13
View File
@@ -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;
}
+40
View File
@@ -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)
+54
View File
@@ -0,0 +1,54 @@
/*
* mock_windows.h
* Windows API Mock 头文件
* 用于单元测试中模拟 Windows API
*/
#ifndef MOCK_WINDOWS_H
#define MOCK_WINDOWS_H
#ifdef TESTING
#include <windows.h>
#include <wchar.h>
/* 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 */
+20
View File
@@ -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)
+359
View File
@@ -0,0 +1,359 @@
/*
* path_manager.c 单元测试
* 测试路径管理函数
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#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);
}
+16
View File
@@ -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)
+209
View File
@@ -0,0 +1,209 @@
/*
* safe_string.c 单元测试
* 测试安全字符串操作函数
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#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);
}
+18
View File
@@ -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)
+354
View File
@@ -0,0 +1,354 @@
/*
* string_ext.c 单元测试
* 测试字符串扩展函数和 StringList 操作
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#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 设置为 NULLcount 和 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);
}