mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-06-29 01:45:54 +08:00
feat: CSV 导入导出、导入撤销支持及多项 bug 修复
功能: - 新增 CSV 格式导入导出支持(含 BOM 处理、引号转义、智能标题行检测) - 导入操作支持撤销/重做 - 保存时 PATH 长度检查与警告 - 深色模式状态持久化(darkmode.txt) - 提取 get_current_target/push_record 为共享函数,消除控制器层重复代码 - 新增 string_list_insert_at,修复撤销删除时的索引恢复 - 新增 undo_redo、error_code、import_export 单元测试 Bug 修复: - 修复备份目录对话框和失败原因的硬编码中文字符串 - 提取 get_exe_dir 到 os_env 消除 i18n.c/ui_utils.c 重复定义 - 修复导入撤销 old_sys/old_user 内存管理(push 后置 NULL 防止重复释放) - 修复 CSV 导出转义与导入解析不一致(移除反斜杠转义,依赖 CSV 引号机制) - 修正 PATH 长度 8191 限制描述为 "command line safe limit"
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <iup.h>
|
||||
#include "core/app_context.h"
|
||||
#include "core/undo_redo.h"
|
||||
#include "utils/i18n.h"
|
||||
|
||||
// 内部辅助函数声明(供各 callbacks_*.c 文件共享)
|
||||
@@ -26,4 +27,11 @@ void refresh_undo_redo_buttons(Ihandle *dlg);
|
||||
// 同步合并预览列表
|
||||
void sync_merged_list(Ihandle *dlg);
|
||||
|
||||
// 获取当前 Tab 对应的 TargetType
|
||||
TargetType get_current_target(Ihandle *dlg);
|
||||
|
||||
// 创建并推送撤销记录
|
||||
void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
|
||||
char **old_paths, char **new_paths);
|
||||
|
||||
#endif // CALLBACKS_INTERNAL_H
|
||||
|
||||
@@ -20,5 +20,6 @@ void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list);
|
||||
// 深色模式状态管理
|
||||
void set_dark_mode(int enabled);
|
||||
int get_dark_mode(void);
|
||||
void init_dark_mode(void);
|
||||
|
||||
#endif // UI_UTILS_H
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
#define OS_ENV_H
|
||||
|
||||
#include "utils/error_code.h"
|
||||
#include <stddef.h>
|
||||
|
||||
// 获取可执行文件所在目录(带缓存)
|
||||
// buf: 输出缓冲区,size: 缓冲区大小
|
||||
void get_exe_dir(char *buf, size_t size);
|
||||
|
||||
// 检查是否以管理员权限运行
|
||||
int check_admin(void);
|
||||
|
||||
@@ -14,6 +14,7 @@ typedef struct
|
||||
// 字符串列表
|
||||
void init_string_list(StringList *list);
|
||||
void add_string_list(StringList *list, const char *str);
|
||||
int string_list_insert_at(StringList *list, int index, const char *str);
|
||||
void clear_string_list(StringList *list);
|
||||
|
||||
// 访问器函数 - 安全访问内部数据
|
||||
|
||||
+28
@@ -415,3 +415,31 @@ msgstr "Dark Mode"
|
||||
#: src/ui/main_window.c
|
||||
msgid "Light Mode"
|
||||
msgstr "Light Mode"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Select backup directory"
|
||||
msgstr "Select backup directory"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Unknown error"
|
||||
msgstr "Unknown error"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to get AppData path"
|
||||
msgstr "Failed to get AppData path"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to create backup directory or file"
|
||||
msgstr "Failed to create backup directory or file"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to read PATH from registry"
|
||||
msgstr "Failed to read PATH from registry"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Backup failed! Reason: %s\n\nContinue saving?\n(Continuing may prevent recovery)"
|
||||
msgstr "Backup failed! Reason: %s\n\nContinue saving?\n(Continuing may prevent recovery)"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Total PATH length: %d characters (command line safe limit: 8191)\n"
|
||||
msgstr "Total PATH length: %d characters (command line safe limit: 8191)\n"
|
||||
+28
@@ -415,3 +415,31 @@ msgstr "深色模式"
|
||||
#: src/ui/main_window.c
|
||||
msgid "Light Mode"
|
||||
msgstr "浅色模式"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Select backup directory"
|
||||
msgstr "选择备份目录"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Unknown error"
|
||||
msgstr "未知错误"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to get AppData path"
|
||||
msgstr "无法获取 AppData 路径"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to create backup directory or file"
|
||||
msgstr "无法创建备份目录或文件"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Failed to read PATH from registry"
|
||||
msgstr "无法读取注册表中的 PATH 值"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Backup failed! Reason: %s\n\nContinue saving?\n(Continuing may prevent recovery)"
|
||||
msgstr "备份失败!原因:%s\n\n是否继续保存?\n(继续保存可能导致无法恢复)"
|
||||
|
||||
#: src/controller/callbacks_sys.c
|
||||
msgid "Total PATH length: %d characters (command line safe limit: 8191)\n"
|
||||
msgstr "总 PATH 长度:%d 字符(命令行安全限制:8191)\n"
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "utils/ui_constants.h"
|
||||
#include "ui/ui_utils.h"
|
||||
#include <iup.h>
|
||||
#include <string.h>
|
||||
|
||||
// 辅助函数:获取主对话框
|
||||
Ihandle *get_main_dlg(void)
|
||||
@@ -84,3 +85,34 @@ void sync_merged_list(Ihandle *dlg)
|
||||
IupSetInt(list_merged, "COUNT", total);
|
||||
refresh_single_list_style(list_merged);
|
||||
}
|
||||
|
||||
// 获取当前 Tab 对应的 TargetType
|
||||
TargetType get_current_target(Ihandle *dlg)
|
||||
{
|
||||
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
|
||||
if (tabs)
|
||||
{
|
||||
int pos = IupGetInt(tabs, "VALUEPOS");
|
||||
return (pos == 0) ? TARGET_SYSTEM : TARGET_USER;
|
||||
}
|
||||
return TARGET_USER;
|
||||
}
|
||||
|
||||
// 辅助函数:创建并推送撤销记录
|
||||
void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
|
||||
char **old_paths, char **new_paths)
|
||||
{
|
||||
AppContext *ctx = get_app_context_from_dlg(dlg);
|
||||
if (!ctx || !ctx->undo_redo_mgr)
|
||||
return;
|
||||
|
||||
OpRecord record;
|
||||
record.type = op_type;
|
||||
record.target = get_current_target(dlg);
|
||||
record.index = index;
|
||||
record.count = count;
|
||||
record.old_paths = old_paths;
|
||||
record.new_paths = new_paths;
|
||||
|
||||
push_undo_record(ctx->undo_redo_mgr, &record);
|
||||
}
|
||||
|
||||
@@ -14,37 +14,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// 辅助函数:检查当前目标是系统还是用户
|
||||
static TargetType get_current_target(Ihandle *dlg)
|
||||
{
|
||||
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
|
||||
if (tabs)
|
||||
{
|
||||
int tab = IupGetInt(tabs, "VALUE");
|
||||
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
|
||||
}
|
||||
return TARGET_USER;
|
||||
}
|
||||
|
||||
// 辅助函数:创建并推送撤销记录
|
||||
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
|
||||
char **old_paths, char **new_paths)
|
||||
{
|
||||
AppContext *ctx = get_app_context_from_dlg(dlg);
|
||||
if (!ctx || !ctx->undo_redo_mgr)
|
||||
return;
|
||||
|
||||
OpRecord record;
|
||||
record.type = op_type;
|
||||
record.target = get_current_target(dlg);
|
||||
record.index = index;
|
||||
record.count = count;
|
||||
record.old_paths = old_paths;
|
||||
record.new_paths = new_paths;
|
||||
|
||||
push_undo_record(ctx->undo_redo_mgr, &record);
|
||||
}
|
||||
|
||||
// 按钮回调:新建
|
||||
int btn_new_cb(Ihandle *self)
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/app_context.h"
|
||||
#include "core/import_export.h"
|
||||
#include "core/lua_config.h"
|
||||
#include "core/undo_redo.h"
|
||||
#include "utils/string_ext.h"
|
||||
#include "utils/safe_string.h"
|
||||
#include "utils/error_code.h"
|
||||
@@ -12,6 +13,7 @@
|
||||
#include "ui/ui_utils.h"
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <windows.h>
|
||||
|
||||
// 按钮回调:导入
|
||||
@@ -32,7 +34,7 @@ int btn_import_cb(Ihandle *self)
|
||||
IupSetAttribute(filedlg, "DIALOGTYPE", "OPEN");
|
||||
IupSetAttribute(filedlg, "TITLE", lua_config_get_string("label", "import_title"));
|
||||
IupSetAttribute(filedlg, "FILTER", "json");
|
||||
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*");
|
||||
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|CSV 文件 (*.csv)|*.csv|文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*");
|
||||
|
||||
IupPopup(filedlg, IUP_CENTER, IUP_CENTER);
|
||||
|
||||
@@ -59,6 +61,23 @@ int btn_import_cb(Ihandle *self)
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
|
||||
// 保存导入前的状态用于撤销
|
||||
char **old_sys = NULL, **old_user = NULL;
|
||||
int old_sys_count = ctx->sys_paths.count;
|
||||
int old_user_count = ctx->user_paths.count;
|
||||
if (old_sys_count > 0)
|
||||
{
|
||||
old_sys = (char **)malloc(old_sys_count * sizeof(char *));
|
||||
for (int i = 0; i < old_sys_count; i++)
|
||||
old_sys[i] = _strdup(string_list_get(&ctx->sys_paths, i));
|
||||
}
|
||||
if (old_user_count > 0)
|
||||
{
|
||||
old_user = (char **)malloc(old_user_count * sizeof(char *));
|
||||
for (int i = 0; i < old_user_count; i++)
|
||||
old_user[i] = _strdup(string_list_get(&ctx->user_paths, i));
|
||||
}
|
||||
|
||||
int choice = 0;
|
||||
if (has_system && has_user)
|
||||
{
|
||||
@@ -81,6 +100,32 @@ int btn_import_cb(Ihandle *self)
|
||||
|
||||
if (choice == 1 || choice == 3)
|
||||
{
|
||||
// 记录系统变量导入的撤销信息
|
||||
char **new_sys = NULL;
|
||||
if (imported.system.count > 0)
|
||||
{
|
||||
new_sys = (char **)malloc(imported.system.count * sizeof(char *));
|
||||
for (int i = 0; i < imported.system.count; i++)
|
||||
new_sys[i] = _strdup(imported.system.items[i]);
|
||||
}
|
||||
OpRecord rec = {0};
|
||||
rec.type = OP_IMPORT;
|
||||
rec.target = TARGET_SYSTEM;
|
||||
rec.index = 0;
|
||||
rec.count = imported.system.count;
|
||||
rec.old_paths = old_sys;
|
||||
rec.new_paths = new_sys;
|
||||
push_undo_record(ctx->undo_redo_mgr, &rec);
|
||||
// push_undo_record 会深拷贝,释放临时副本
|
||||
if (new_sys)
|
||||
{
|
||||
for (int i = 0; i < imported.system.count; i++)
|
||||
free(new_sys[i]);
|
||||
free(new_sys);
|
||||
}
|
||||
// old_sys 已被深拷贝到撤销管理器,置 NULL 避免末尾重复释放
|
||||
old_sys = NULL;
|
||||
|
||||
clear_string_list(&ctx->sys_paths);
|
||||
for (int i = 0; i < imported.system.count; i++)
|
||||
{
|
||||
@@ -93,6 +138,32 @@ int btn_import_cb(Ihandle *self)
|
||||
|
||||
if (choice == 2 || choice == 3)
|
||||
{
|
||||
// 记录用户变量导入的撤销信息
|
||||
char **new_user = NULL;
|
||||
if (imported.user.count > 0)
|
||||
{
|
||||
new_user = (char **)malloc(imported.user.count * sizeof(char *));
|
||||
for (int i = 0; i < imported.user.count; i++)
|
||||
new_user[i] = _strdup(imported.user.items[i]);
|
||||
}
|
||||
OpRecord rec = {0};
|
||||
rec.type = OP_IMPORT;
|
||||
rec.target = TARGET_USER;
|
||||
rec.index = 0;
|
||||
rec.count = imported.user.count;
|
||||
rec.old_paths = old_user;
|
||||
rec.new_paths = new_user;
|
||||
push_undo_record(ctx->undo_redo_mgr, &rec);
|
||||
// push_undo_record 会深拷贝,释放临时副本
|
||||
if (new_user)
|
||||
{
|
||||
for (int i = 0; i < imported.user.count; i++)
|
||||
free(new_user[i]);
|
||||
free(new_user);
|
||||
}
|
||||
// old_user 已被深拷贝到撤销管理器,置 NULL 避免末尾重复释放
|
||||
old_user = NULL;
|
||||
|
||||
clear_string_list(&ctx->user_paths);
|
||||
for (int i = 0; i < imported.user.count; i++)
|
||||
{
|
||||
@@ -103,6 +174,20 @@ int btn_import_cb(Ihandle *self)
|
||||
total_imported += imported.user.count;
|
||||
}
|
||||
|
||||
// 释放未被撤销记录使用的旧状态
|
||||
if (old_sys)
|
||||
{
|
||||
for (int i = 0; i < old_sys_count; i++)
|
||||
free(old_sys[i]);
|
||||
free(old_sys);
|
||||
}
|
||||
if (old_user)
|
||||
{
|
||||
for (int i = 0; i < old_user_count; i++)
|
||||
free(old_user[i]);
|
||||
free(old_user);
|
||||
}
|
||||
|
||||
// 释放导入数据
|
||||
clear_string_list(&imported.system);
|
||||
clear_string_list(&imported.user);
|
||||
@@ -114,6 +199,8 @@ int btn_import_cb(Ihandle *self)
|
||||
Ihandle *lbl_status = IupGetDialogChild(dlg, CTRL_LBL_STATUS);
|
||||
if (lbl_status)
|
||||
IupSetAttribute(lbl_status, "TITLE", lua_config_get_string("status", "loaded"));
|
||||
|
||||
refresh_undo_redo_buttons(dlg);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -11,37 +11,6 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
// 辅助函数:检查当前目标是系统还是用户
|
||||
static TargetType get_current_target(Ihandle *dlg)
|
||||
{
|
||||
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
|
||||
if (tabs)
|
||||
{
|
||||
int tab = IupGetInt(tabs, "VALUE");
|
||||
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
|
||||
}
|
||||
return TARGET_USER;
|
||||
}
|
||||
|
||||
// 辅助函数:创建并推送撤销记录
|
||||
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
|
||||
char **old_paths, char **new_paths)
|
||||
{
|
||||
AppContext *ctx = get_app_context_from_dlg(dlg);
|
||||
if (!ctx || !ctx->undo_redo_mgr)
|
||||
return;
|
||||
|
||||
OpRecord record;
|
||||
record.type = op_type;
|
||||
record.target = get_current_target(dlg);
|
||||
record.index = index;
|
||||
record.count = count;
|
||||
record.old_paths = old_paths;
|
||||
record.new_paths = new_paths;
|
||||
|
||||
push_undo_record(ctx->undo_redo_mgr, &record);
|
||||
}
|
||||
|
||||
// 按钮回调:上移
|
||||
int btn_up_cb(Ihandle *self)
|
||||
{
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#include "controller/callbacks_internal.h"
|
||||
#include "core/app_context.h"
|
||||
#include "core/lua_config.h"
|
||||
#include "core/undo_redo.h"
|
||||
#include "utils/string_ext.h"
|
||||
#include "utils/safe_string.h"
|
||||
#include "utils/ui_constants.h"
|
||||
#include "ui/ui_utils.h"
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <windows.h>
|
||||
|
||||
// 搜索回调
|
||||
@@ -65,10 +67,17 @@ int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y
|
||||
if (txt_search)
|
||||
IupSetAttribute(txt_search, "VALUE", "");
|
||||
|
||||
// 记录撤销信息
|
||||
char *path_copy = _strdup(filename);
|
||||
char *paths[1] = {path_copy};
|
||||
push_record(dlg, OP_ADD, raw_data->count, 1, NULL, paths);
|
||||
free(path_copy);
|
||||
|
||||
add_string_list(raw_data, filename);
|
||||
sync_string_list_to_ui(current_list, raw_data);
|
||||
|
||||
IupSetInt(current_list, "VALUE", raw_data->count);
|
||||
refresh_undo_redo_buttons(dlg);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ int btn_ok_cb(Ihandle *self)
|
||||
{
|
||||
Ihandle *filedlg = IupFileDlg();
|
||||
IupSetAttribute(filedlg, "DIALOGTYPE", "DIR");
|
||||
IupSetAttribute(filedlg, "TITLE", "选择备份目录");
|
||||
IupSetAttribute(filedlg, "TITLE", _("Select backup directory"));
|
||||
|
||||
IupPopup(filedlg, IUP_CENTER, IUP_CENTER);
|
||||
|
||||
@@ -79,22 +79,72 @@ int btn_ok_cb(Ihandle *self)
|
||||
if (backup_result != ERR_OK)
|
||||
{
|
||||
log_error("Backup failed: error code %d", backup_result);
|
||||
const char *reason = "未知错误";
|
||||
const char *reason = _("Unknown error");
|
||||
if (backup_result == ERR_FAILED)
|
||||
reason = "无法获取 AppData 路径";
|
||||
reason = _("Failed to get AppData path");
|
||||
else if (backup_result == ERR_FILE_NOT_FOUND)
|
||||
reason = "无法创建备份目录或文件";
|
||||
reason = _("Failed to create backup directory or file");
|
||||
else if (backup_result == ERR_REGISTRY_FAILED)
|
||||
reason = "无法读取注册表中的 PATH 值";
|
||||
reason = _("Failed to read PATH from registry");
|
||||
|
||||
char msg[512];
|
||||
snprintf(msg, sizeof(msg), "备份失败!原因:%s\n\n是否继续保存?\n(继续保存可能导致无法恢复)", reason);
|
||||
snprintf(msg, sizeof(msg), _("Backup failed! Reason: %s\n\nContinue saving?\n(Continuing may prevent recovery)"), reason);
|
||||
int choice = IupAlarm(_("Warning"), msg, _("Continue Saving"), _("Cancel"), NULL);
|
||||
if (choice != 1)
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
// PATH 长度检查
|
||||
{
|
||||
int sys_len = 0, user_len = 0;
|
||||
for (int i = 0; i < ctx->sys_paths.count; i++)
|
||||
{
|
||||
const char *p = string_list_get(&ctx->sys_paths, i);
|
||||
if (p) sys_len += (int)strlen(p) + 1; // +1 for semicolon
|
||||
}
|
||||
for (int i = 0; i < ctx->user_paths.count; i++)
|
||||
{
|
||||
const char *p = string_list_get(&ctx->user_paths, i);
|
||||
if (p) user_len += (int)strlen(p) + 1;
|
||||
}
|
||||
|
||||
int warnings = 0;
|
||||
char warn_msg[1024] = "";
|
||||
|
||||
if (sys_len > 2048)
|
||||
{
|
||||
snprintf(warn_msg, sizeof(warn_msg),
|
||||
_("System PATH length: %d characters (recommended max: 2048)\n"), sys_len);
|
||||
warnings++;
|
||||
}
|
||||
if (user_len > 2048)
|
||||
{
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp),
|
||||
_("User PATH length: %d characters (recommended max: 2048)\n"), user_len);
|
||||
strncat(warn_msg, tmp, sizeof(warn_msg) - strlen(warn_msg) - 1);
|
||||
warnings++;
|
||||
}
|
||||
if (sys_len + user_len > 8191)
|
||||
{
|
||||
char tmp[256];
|
||||
snprintf(tmp, sizeof(tmp),
|
||||
_("Total PATH length: %d characters (command line safe limit: 8191)\n"), sys_len + user_len);
|
||||
strncat(warn_msg, tmp, sizeof(warn_msg) - strlen(warn_msg) - 1);
|
||||
warnings++;
|
||||
}
|
||||
|
||||
if (warnings > 0)
|
||||
{
|
||||
strncat(warn_msg, _("\nSaving may cause system instability. Continue?"),
|
||||
sizeof(warn_msg) - strlen(warn_msg) - 1);
|
||||
int choice = IupAlarm(_("PATH Length Warning"), warn_msg, _("Continue Saving"), _("Cancel"), NULL);
|
||||
if (choice != 1)
|
||||
return IUP_DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorCode sys_ok = save_system_paths(&ctx->sys_paths);
|
||||
ErrorCode user_ok = save_user_paths(&ctx->user_paths);
|
||||
|
||||
|
||||
+137
-12
@@ -5,6 +5,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <time.h>
|
||||
|
||||
// 获取当前日期时间
|
||||
@@ -103,14 +104,6 @@ static char *escape_csv_field(const char *str)
|
||||
*p++ = '"';
|
||||
*p++ = '"';
|
||||
break;
|
||||
case '\n':
|
||||
*p++ = '\\';
|
||||
*p++ = 'n';
|
||||
break;
|
||||
case '\r':
|
||||
*p++ = '\\';
|
||||
*p++ = 'r';
|
||||
break;
|
||||
default:
|
||||
*p++ = str[i];
|
||||
break;
|
||||
@@ -169,7 +162,7 @@ static ErrorCode export_paths_to_json(const ExportData *data, FILE *fp)
|
||||
|
||||
// 导出 PATH 到 CSV 文件
|
||||
// 格式:type,path
|
||||
// type: "system" 或 "user"
|
||||
// type: system 或 user
|
||||
static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
{
|
||||
// 写入 UTF-8 BOM
|
||||
@@ -186,7 +179,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
char *escaped = escape_csv_field(data->system.items[i]);
|
||||
if (escaped)
|
||||
{
|
||||
fprintf(fp, "\"system\",\"%s\"\n", escaped);
|
||||
fprintf(fp, "system,%s\n", escaped);
|
||||
free(escaped);
|
||||
}
|
||||
}
|
||||
@@ -200,7 +193,7 @@ static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||
char *escaped = escape_csv_field(data->user.items[i]);
|
||||
if (escaped)
|
||||
{
|
||||
fprintf(fp, "\"user\",\"%s\"\n", escaped);
|
||||
fprintf(fp, "user,%s\n", escaped);
|
||||
free(escaped);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +256,7 @@ static void trim_whitespace(char *str)
|
||||
start++;
|
||||
|
||||
char *end = str + strlen(str) - 1;
|
||||
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
|
||||
while (end >= start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r'))
|
||||
*end-- = '\0';
|
||||
|
||||
if (start != str)
|
||||
@@ -289,6 +282,133 @@ static int is_json_file(const char *filepath)
|
||||
return ext && _stricmp(ext, ".json") == 0;
|
||||
}
|
||||
|
||||
// 检查文件是否为 CSV 格式(通过扩展名)
|
||||
static int is_csv_file(const char *filepath)
|
||||
{
|
||||
const char *ext = strrchr(filepath, '.');
|
||||
return ext && _stricmp(ext, ".csv") == 0;
|
||||
}
|
||||
|
||||
// 解析 CSV 字段(处理引号包围的字段)
|
||||
// 返回值:指向下一个字段的指针,或 NULL
|
||||
static const char *parse_csv_field(const char *line, char *field, int field_size)
|
||||
{
|
||||
if (!line || !field || field_size <= 0)
|
||||
return NULL;
|
||||
|
||||
const char *p = line;
|
||||
char *out = field;
|
||||
char *end = field + field_size - 1;
|
||||
|
||||
if (*p == '"')
|
||||
{
|
||||
// 引号包围的字段
|
||||
p++; // 跳过开始引号
|
||||
while (*p && out < end)
|
||||
{
|
||||
if (*p == '"')
|
||||
{
|
||||
if (*(p + 1) == '"')
|
||||
{
|
||||
// 转义的引号 ""
|
||||
*out++ = '"';
|
||||
p += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 结束引号
|
||||
p++; // 跳过结束引号
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
*out++ = *p++;
|
||||
}
|
||||
}
|
||||
// 跳过逗号分隔符
|
||||
if (*p == ',') p++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 非引号字段(按逗号分隔)
|
||||
while (*p && *p != ',' && out < end)
|
||||
{
|
||||
*out++ = *p++;
|
||||
}
|
||||
if (*p == ',') p++;
|
||||
}
|
||||
|
||||
*out = '\0';
|
||||
return p;
|
||||
}
|
||||
|
||||
// 导入 CSV 格式的 PATH 文件
|
||||
static ErrorCode import_paths_from_csv(const char *filepath, ExportData *data)
|
||||
{
|
||||
FILE *fp = fopen(filepath, "rb");
|
||||
if (!fp)
|
||||
{
|
||||
log_error("Failed to open file for import: %s", filepath);
|
||||
return ERR_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
char line[4096];
|
||||
int line_num = 0;
|
||||
int header_skipped = 0;
|
||||
|
||||
while (fgets(line, sizeof(line), fp))
|
||||
{
|
||||
line_num++;
|
||||
trim_whitespace(line);
|
||||
|
||||
if (line[0] == '\0')
|
||||
continue;
|
||||
|
||||
// 跳过 UTF-8 BOM
|
||||
const char *start = line;
|
||||
if ((unsigned char)start[0] == 0xEF &&
|
||||
(unsigned char)start[1] == 0xBB &&
|
||||
(unsigned char)start[2] == 0xBF)
|
||||
{
|
||||
start += 3;
|
||||
}
|
||||
|
||||
// 智能检测标题行:第一行包含 "type" 和 "path" 则视为标题
|
||||
if (!header_skipped)
|
||||
{
|
||||
header_skipped = 1;
|
||||
char lower[256];
|
||||
strncpy(lower, start, sizeof(lower) - 1);
|
||||
lower[sizeof(lower) - 1] = '\0';
|
||||
for (char *p = lower; *p; p++) *p = (char)tolower((unsigned char)*p);
|
||||
if (strstr(lower, "type") && strstr(lower, "path"))
|
||||
continue;
|
||||
}
|
||||
|
||||
char type[32] = {0};
|
||||
char path[4096] = {0};
|
||||
|
||||
start = parse_csv_field(start, type, sizeof(type));
|
||||
if (!start)
|
||||
continue;
|
||||
parse_csv_field(start, path, sizeof(path));
|
||||
|
||||
if (path[0] == '\0')
|
||||
continue;
|
||||
|
||||
if (_stricmp(type, "system") == 0)
|
||||
add_string_list(&data->system, path);
|
||||
else if (_stricmp(type, "user") == 0)
|
||||
add_string_list(&data->user, path);
|
||||
}
|
||||
|
||||
fclose(fp);
|
||||
log_info("Imported paths from CSV file: sys=%d, user=%d, file=%s",
|
||||
data->system.count, data->user.count, filepath);
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
// 检查引号前是否有奇数个连续反斜杠(奇数个表示引号被转义)
|
||||
static int is_quote_escaped(const char *quote_pos, const char *line_start)
|
||||
{
|
||||
@@ -311,6 +431,11 @@ ErrorCode import_paths_from_file(const char *filepath, ExportData *data)
|
||||
init_string_list(&data->system);
|
||||
init_string_list(&data->user);
|
||||
|
||||
if (is_csv_file(filepath))
|
||||
{
|
||||
return import_paths_from_csv(filepath, data);
|
||||
}
|
||||
|
||||
if (!is_json_file(filepath))
|
||||
{
|
||||
FILE *fp = fopen(filepath, "rb");
|
||||
|
||||
+5
-18
@@ -149,11 +149,11 @@ int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||
break;
|
||||
|
||||
case OP_DELETE:
|
||||
// 撤销删除:恢复被删除的路径
|
||||
// 撤销删除:恢复被删除的路径到原始位置
|
||||
for (int i = 0; i < rec->count; i++)
|
||||
{
|
||||
if (rec->old_paths[i])
|
||||
add_string_list(target, rec->old_paths[i]);
|
||||
string_list_insert_at(target, rec->index + i, rec->old_paths[i]);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -221,23 +221,10 @@ int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||
break;
|
||||
|
||||
case OP_DELETE:
|
||||
// 重做删除:重新删除路径
|
||||
for (int i = 0; i < rec->count; i++)
|
||||
// 重做删除:从高索引到低索引删除,避免索引偏移
|
||||
for (int i = rec->count - 1; i >= 0; i--)
|
||||
{
|
||||
// 找到并删除对应路径
|
||||
for (int j = 0; j < target->count; j++)
|
||||
{
|
||||
if (target->items[j] && rec->old_paths[i] &&
|
||||
strcmp(target->items[j], rec->old_paths[i]) == 0)
|
||||
{
|
||||
free(target->items[j]);
|
||||
// 移动后面的元素
|
||||
for (int k = j; k < target->count - 1; k++)
|
||||
target->items[k] = target->items[k + 1];
|
||||
target->count--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
path_manager_remove_at(target, rec->index + i);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
+24
-6
@@ -7,6 +7,13 @@
|
||||
#include "core/app_context.h"
|
||||
#include "utils/ui_constants.h"
|
||||
#include "core/lua_config.h"
|
||||
#include "utils/string_ext.h"
|
||||
#include "utils/os_env.h"
|
||||
#include "utils/logger.h"
|
||||
#include "utils/i18n.h"
|
||||
#include "controller/callbacks.h"
|
||||
#include "ui/main_window.h"
|
||||
#include "ui/ui_utils.h"
|
||||
|
||||
// 需要在非管理员模式禁用的按钮列表
|
||||
#define ADMIN_DISABLE_COUNT 10
|
||||
@@ -15,12 +22,6 @@ static const char* ADMIN_DISABLE_BUTTONS[] = {
|
||||
CTRL_BTN_UP, CTRL_BTN_DOWN, CTRL_BTN_CLEAN, CTRL_BTN_OK,
|
||||
CTRL_BTN_IMPORT, CTRL_BTN_EXPORT
|
||||
};
|
||||
#include "utils/string_ext.h"
|
||||
#include "utils/os_env.h"
|
||||
#include "utils/logger.h"
|
||||
#include "utils/i18n.h"
|
||||
#include "controller/callbacks.h"
|
||||
#include "ui/main_window.h"
|
||||
|
||||
/*
|
||||
!编译命令:
|
||||
@@ -65,6 +66,9 @@ int main(int argc, char **argv)
|
||||
// 初始化国际化系统
|
||||
i18n_init(NULL);
|
||||
|
||||
// 初始化深色模式(从配置文件加载)
|
||||
init_dark_mode();
|
||||
|
||||
// 在管理员模式下,解决无法拖拽文件到列表框的问题 (UIPI)
|
||||
// 需要加载 User32.dll 获取 ChangeWindowMessageFilter 函数
|
||||
HMODULE hUser32 = LoadLibraryW(L"user32.dll");
|
||||
@@ -97,6 +101,20 @@ int main(int argc, char **argv)
|
||||
|
||||
Ihandle *dlg = create_main_window();
|
||||
|
||||
// 如果深色模式已启用,应用深色主题
|
||||
if (get_dark_mode())
|
||||
{
|
||||
const char *dark_bg = lua_config_get_string("theme", "dark_bg");
|
||||
if (dark_bg && *dark_bg)
|
||||
IupSetAttribute(dlg, "BGCOLOR", dark_bg);
|
||||
|
||||
// 更新深色模式按钮标题
|
||||
Ihandle *btn_darkmode = IupGetDialogChild(dlg, CTRL_BTN_DARKMODE);
|
||||
if (btn_darkmode)
|
||||
IupSetAttribute(btn_darkmode, "TITLE",
|
||||
_(lua_config_get_string("button", "lightmode")));
|
||||
}
|
||||
|
||||
// 绑定上下文到对话框
|
||||
IupSetAttribute(dlg, "APP_CONTEXT", (char *)ctx);
|
||||
// 注册主窗口句柄,方便其他地方获取
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "ui/ui_utils.h"
|
||||
#include "utils/os_env.h"
|
||||
#include "utils/string_ext.h"
|
||||
#include "utils/logger.h"
|
||||
#include "core/lua_config.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
@@ -8,9 +9,41 @@
|
||||
|
||||
static int g_dark_mode = 0;
|
||||
|
||||
static void load_dark_mode(void)
|
||||
{
|
||||
char path[512];
|
||||
get_exe_dir(path, sizeof(path));
|
||||
strncat(path, "\\darkmode.txt", sizeof(path) - strlen(path) - 1);
|
||||
|
||||
FILE *fp = fopen(path, "r");
|
||||
if (fp)
|
||||
{
|
||||
char val[8] = {0};
|
||||
if (fgets(val, sizeof(val), fp))
|
||||
g_dark_mode = (atoi(val) == 1) ? 1 : 0;
|
||||
fclose(fp);
|
||||
log_info("Loaded dark mode: %d", g_dark_mode);
|
||||
}
|
||||
}
|
||||
|
||||
static void save_dark_mode(void)
|
||||
{
|
||||
char path[512];
|
||||
get_exe_dir(path, sizeof(path));
|
||||
strncat(path, "\\darkmode.txt", sizeof(path) - strlen(path) - 1);
|
||||
|
||||
FILE *fp = fopen(path, "w");
|
||||
if (fp)
|
||||
{
|
||||
fprintf(fp, "%d\n", g_dark_mode);
|
||||
fclose(fp);
|
||||
}
|
||||
}
|
||||
|
||||
void set_dark_mode(int enabled)
|
||||
{
|
||||
g_dark_mode = enabled;
|
||||
save_dark_mode();
|
||||
}
|
||||
|
||||
int get_dark_mode(void)
|
||||
@@ -18,6 +51,12 @@ int get_dark_mode(void)
|
||||
return g_dark_mode;
|
||||
}
|
||||
|
||||
// 初始化深色模式(启动时调用)
|
||||
void init_dark_mode(void)
|
||||
{
|
||||
load_dark_mode();
|
||||
}
|
||||
|
||||
// 刷新列表样式(斑马纹 + 有效性检查)
|
||||
void refresh_single_list_style(Ihandle *list)
|
||||
{
|
||||
|
||||
+14
-3
@@ -1,5 +1,6 @@
|
||||
#include "utils/i18n.h"
|
||||
#include "utils/logger.h"
|
||||
#include "utils/os_env.h"
|
||||
#include "core/lua_config.h"
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
@@ -10,7 +11,11 @@ static char locale_path[256] = {0};
|
||||
|
||||
static void load_saved_language(void)
|
||||
{
|
||||
FILE *fp = fopen("language.txt", "r");
|
||||
char path[512];
|
||||
get_exe_dir(path, sizeof(path));
|
||||
strncat(path, "\\language.txt", sizeof(path) - strlen(path) - 1);
|
||||
|
||||
FILE *fp = fopen(path, "r");
|
||||
if (fp != NULL)
|
||||
{
|
||||
char lang[16] = {0};
|
||||
@@ -39,7 +44,9 @@ void i18n_init(const char *default_lang)
|
||||
{
|
||||
setlocale(LC_ALL, "");
|
||||
|
||||
snprintf(locale_path, sizeof(locale_path), "./locale");
|
||||
char dir[256];
|
||||
get_exe_dir(dir, sizeof(dir));
|
||||
snprintf(locale_path, sizeof(locale_path), "%s/locale", dir);
|
||||
bindtextdomain("messages", locale_path);
|
||||
textdomain("messages");
|
||||
|
||||
@@ -96,7 +103,11 @@ void i18n_change_language(const char *lang)
|
||||
snprintf(new_lang_path, sizeof(new_lang_path), "%s/%s/LC_MESSAGES/%s.mo", locale_path, lang, lang);
|
||||
bindtextdomain("messages", new_lang_path);
|
||||
|
||||
FILE *fp = fopen("language.txt", "w");
|
||||
char path[512];
|
||||
get_exe_dir(path, sizeof(path));
|
||||
strncat(path, "\\language.txt", sizeof(path) - strlen(path) - 1);
|
||||
|
||||
FILE *fp = fopen(path, "w");
|
||||
if (fp != NULL)
|
||||
{
|
||||
fprintf(fp, "%s\n", lang);
|
||||
|
||||
@@ -5,10 +5,33 @@
|
||||
#include <windows.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <direct.h>
|
||||
#include <shlobj.h>
|
||||
|
||||
// 获取可执行文件所在目录(带缓存)
|
||||
static char s_exe_dir[256] = {0};
|
||||
|
||||
void get_exe_dir(char *buf, size_t size)
|
||||
{
|
||||
if (s_exe_dir[0] != '\0')
|
||||
{
|
||||
strncpy(buf, s_exe_dir, size - 1);
|
||||
buf[size - 1] = '\0';
|
||||
return;
|
||||
}
|
||||
DWORD len = GetModuleFileNameA(NULL, buf, (DWORD)size);
|
||||
if (len > 0 && len < size)
|
||||
{
|
||||
char *last_sep = strrchr(buf, '\\');
|
||||
if (!last_sep) last_sep = strrchr(buf, '/');
|
||||
if (last_sep) *last_sep = '\0';
|
||||
}
|
||||
strncpy(s_exe_dir, buf, sizeof(s_exe_dir) - 1);
|
||||
s_exe_dir[sizeof(s_exe_dir) - 1] = '\0';
|
||||
}
|
||||
|
||||
// 检查管理员权限
|
||||
int check_admin(void)
|
||||
{
|
||||
|
||||
@@ -92,6 +92,36 @@ void add_string_list(StringList *list, const char *str)
|
||||
list->count++;
|
||||
}
|
||||
|
||||
// 在指定索引位置插入字符串(后续元素后移)
|
||||
int string_list_insert_at(StringList *list, int index, const char *str)
|
||||
{
|
||||
if (!list || !str || index < 0 || index > list->count)
|
||||
return -1;
|
||||
|
||||
// 扩容检查
|
||||
if (list->count >= list->capacity)
|
||||
{
|
||||
int new_capacity = (list->capacity == 0) ? 16 : list->capacity * 2;
|
||||
char **new_items = (char **)realloc(list->items, new_capacity * sizeof(char *));
|
||||
if (!new_items)
|
||||
return -1;
|
||||
list->items = new_items;
|
||||
list->capacity = new_capacity;
|
||||
}
|
||||
|
||||
char *dup = _strdup(str);
|
||||
if (!dup)
|
||||
return -1;
|
||||
|
||||
// 后移元素
|
||||
for (int i = list->count; i > index; i--)
|
||||
list->items[i] = list->items[i - 1];
|
||||
|
||||
list->items[index] = dup;
|
||||
list->count++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 获取指定索引的字符串(只读)
|
||||
const char *string_list_get(const StringList *list, int index)
|
||||
{
|
||||
|
||||
@@ -38,3 +38,6 @@ include(CTest)
|
||||
add_subdirectory(unit/safe_string)
|
||||
add_subdirectory(unit/string_ext)
|
||||
add_subdirectory(unit/path_manager)
|
||||
add_subdirectory(unit/undo_redo)
|
||||
add_subdirectory(unit/error_code)
|
||||
add_subdirectory(unit/import_export)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# error_code 单元测试
|
||||
add_executable(test_error_code test_error_code.c
|
||||
${CMAKE_SOURCE_DIR}/src/utils/error_code.c
|
||||
)
|
||||
|
||||
target_link_libraries(test_error_code cmocka)
|
||||
|
||||
target_include_directories(test_error_code PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
add_custom_command(TARGET test_error_code POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_BINARY_DIR}/_deps/cmocka-build/src/cmocka.dll
|
||||
$<TARGET_FILE_DIR:test_error_code>
|
||||
)
|
||||
|
||||
add_test(NAME error_code_test COMMAND test_error_code)
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* error_code.c 单元测试
|
||||
* 测试错误码字符串映射
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
#include <string.h>
|
||||
#include "utils/error_code.h"
|
||||
|
||||
/* ==================== error_code_to_string 测试 ==================== */
|
||||
|
||||
static void test_err_ok(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_OK), "Success");
|
||||
}
|
||||
|
||||
static void test_err_failed(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_FAILED), "Operation failed");
|
||||
}
|
||||
|
||||
static void test_err_null_ptr(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_NULL_PTR), "Null pointer error");
|
||||
}
|
||||
|
||||
static void test_err_out_of_memory(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_OUT_OF_MEMORY), "Out of memory");
|
||||
}
|
||||
|
||||
static void test_err_file_not_found(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_FILE_NOT_FOUND), "File not found");
|
||||
}
|
||||
|
||||
static void test_err_permission_denied(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_PERMISSION_DENIED), "Permission denied");
|
||||
}
|
||||
|
||||
static void test_err_invalid_format(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_INVALID_FORMAT), "Invalid format");
|
||||
}
|
||||
|
||||
static void test_err_registry_failed(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_REGISTRY_FAILED), "Registry operation failed");
|
||||
}
|
||||
|
||||
static void test_err_not_found(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_NOT_FOUND), "Item not found");
|
||||
}
|
||||
|
||||
static void test_err_exists(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_EXISTS), "Item already exists");
|
||||
}
|
||||
|
||||
static void test_err_invalid_index(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string(ERR_INVALID_INDEX), "Invalid index");
|
||||
}
|
||||
|
||||
static void test_unknown_error_code(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_string_equal(error_code_to_string((ErrorCode)9999), "Unknown error");
|
||||
assert_string_equal(error_code_to_string((ErrorCode)-99), "Unknown error");
|
||||
}
|
||||
|
||||
/* ==================== 主函数 ==================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
cmocka_unit_test(test_err_ok),
|
||||
cmocka_unit_test(test_err_failed),
|
||||
cmocka_unit_test(test_err_null_ptr),
|
||||
cmocka_unit_test(test_err_out_of_memory),
|
||||
cmocka_unit_test(test_err_file_not_found),
|
||||
cmocka_unit_test(test_err_permission_denied),
|
||||
cmocka_unit_test(test_err_invalid_format),
|
||||
cmocka_unit_test(test_err_registry_failed),
|
||||
cmocka_unit_test(test_err_not_found),
|
||||
cmocka_unit_test(test_err_exists),
|
||||
cmocka_unit_test(test_err_invalid_index),
|
||||
cmocka_unit_test(test_unknown_error_code),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# import_export 单元测试
|
||||
add_executable(test_import_export test_import_export.c
|
||||
${CMAKE_SOURCE_DIR}/src/core/import_export.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_import_export cmocka)
|
||||
|
||||
target_include_directories(test_import_export PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_definitions(test_import_export PRIVATE TESTING)
|
||||
|
||||
add_custom_command(TARGET test_import_export POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_BINARY_DIR}/_deps/cmocka-build/src/cmocka.dll
|
||||
$<TARGET_FILE_DIR:test_import_export>
|
||||
)
|
||||
|
||||
add_test(NAME import_export_test COMMAND test_import_export)
|
||||
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* import_export.c 单元测试
|
||||
* 测试 is_valid_path_format 和文件导入导出
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include "core/import_export.h"
|
||||
#include "utils/string_ext.h"
|
||||
|
||||
/* ==================== Mock 函数 ==================== */
|
||||
|
||||
#ifdef TESTING
|
||||
|
||||
void log_info(const char *fmt, ...) { (void)fmt; }
|
||||
void log_debug(const char *fmt, ...) { (void)fmt; }
|
||||
void log_warn(const char *fmt, ...) { (void)fmt; }
|
||||
void log_error(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
#endif
|
||||
|
||||
/* ==================== is_valid_path_format 测试 ==================== */
|
||||
|
||||
static void test_valid_drive_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format("C:\\Windows"), 1);
|
||||
assert_int_equal(is_valid_path_format("D:\\Program Files"), 1);
|
||||
assert_int_equal(is_valid_path_format("c:\\test"), 1);
|
||||
}
|
||||
|
||||
static void test_valid_unc_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format("\\\\server\\share"), 1);
|
||||
}
|
||||
|
||||
static void test_valid_env_var(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format("%JAVA_HOME%\\bin"), 1);
|
||||
assert_int_equal(is_valid_path_format("%PATH%"), 1);
|
||||
}
|
||||
|
||||
static void test_valid_relative_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format(".\\subdir"), 1);
|
||||
assert_int_equal(is_valid_path_format("subdir\\file"), 1);
|
||||
assert_int_equal(is_valid_path_format("subdir/file"), 1);
|
||||
}
|
||||
|
||||
static void test_null_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format(NULL), 0);
|
||||
}
|
||||
|
||||
static void test_empty_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(is_valid_path_format(""), 0);
|
||||
}
|
||||
|
||||
static void test_invalid_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* 没有分隔符、没有环境变量、不是 UNC 路径 */
|
||||
assert_int_equal(is_valid_path_format("justaname"), 0);
|
||||
}
|
||||
|
||||
static void test_drive_only(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* C: 后面没有路径分隔符 */
|
||||
assert_int_equal(is_valid_path_format("C:"), 1); /* colon后是 \0 */
|
||||
}
|
||||
|
||||
/* ==================== export/import 文件测试 ==================== */
|
||||
|
||||
static void test_export_json(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
init_string_list(&data.system);
|
||||
init_string_list(&data.user);
|
||||
add_string_list(&data.system, "C:\\Windows");
|
||||
add_string_list(&data.user, "C:\\Users\\test");
|
||||
|
||||
const char *tmpfile = "test_export.json";
|
||||
ErrorCode result = export_paths_to_file(&data, tmpfile);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
|
||||
/* 验证文件内容 */
|
||||
FILE *fp = fopen(tmpfile, "r");
|
||||
assert_non_null(fp);
|
||||
char buffer[4096];
|
||||
fread(buffer, 1, sizeof(buffer) - 1, fp);
|
||||
fclose(fp);
|
||||
|
||||
assert_non_null(strstr(buffer, "\"version\""));
|
||||
assert_non_null(strstr(buffer, "\"system\""));
|
||||
assert_non_null(strstr(buffer, "\"user\""));
|
||||
assert_non_null(strstr(buffer, "C:\\\\Windows"));
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
clear_string_list(&data.user);
|
||||
}
|
||||
|
||||
static void test_export_csv(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
init_string_list(&data.system);
|
||||
init_string_list(&data.user);
|
||||
add_string_list(&data.system, "C:\\Windows");
|
||||
|
||||
const char *tmpfile = "test_export.csv";
|
||||
ErrorCode result = export_paths_to_format(&data, tmpfile, EXPORT_CSV);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
|
||||
FILE *fp = fopen(tmpfile, "r");
|
||||
assert_non_null(fp);
|
||||
char buffer[4096];
|
||||
fread(buffer, 1, sizeof(buffer) - 1, fp);
|
||||
fclose(fp);
|
||||
|
||||
assert_non_null(strstr(buffer, "type,path"));
|
||||
assert_non_null(strstr(buffer, "system,"));
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
clear_string_list(&data.user);
|
||||
}
|
||||
|
||||
static void test_export_null_data(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ErrorCode result = export_paths_to_file(NULL, "test.json");
|
||||
assert_int_equal(result, ERR_NULL_PTR);
|
||||
}
|
||||
|
||||
static void test_export_null_path(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
init_string_list(&data.system);
|
||||
ErrorCode result = export_paths_to_file(&data, NULL);
|
||||
assert_int_equal(result, ERR_NULL_PTR);
|
||||
clear_string_list(&data.system);
|
||||
}
|
||||
|
||||
static void test_import_txt(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* 创建临时 TXT 文件(二进制模式避免 \r\n 转换) */
|
||||
const char *tmpfile = "test_import.txt";
|
||||
FILE *fp = fopen(tmpfile, "wb");
|
||||
assert_non_null(fp);
|
||||
fprintf(fp, "C:\\Path1\n");
|
||||
fprintf(fp, "C:\\Path2\n");
|
||||
fprintf(fp, "# comment\n");
|
||||
fprintf(fp, "\n");
|
||||
fprintf(fp, "C:\\Path3\n");
|
||||
fclose(fp);
|
||||
|
||||
ExportData data;
|
||||
ErrorCode result = import_paths_from_file(tmpfile, &data);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
assert_int_equal(data.system.count, 3);
|
||||
assert_string_equal(string_list_get(&data.system, 0), "C:\\Path1");
|
||||
assert_string_equal(string_list_get(&data.system, 1), "C:\\Path2");
|
||||
assert_string_equal(string_list_get(&data.system, 2), "C:\\Path3");
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
}
|
||||
|
||||
static void test_import_null_filepath(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
ErrorCode result = import_paths_from_file(NULL, &data);
|
||||
assert_int_equal(result, ERR_NULL_PTR);
|
||||
}
|
||||
|
||||
static void test_import_null_data(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ErrorCode result = import_paths_from_file("test.txt", NULL);
|
||||
assert_int_equal(result, ERR_NULL_PTR);
|
||||
}
|
||||
|
||||
static void test_import_nonexistent_file(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
ErrorCode result = import_paths_from_file("nonexistent_file_12345.txt", &data);
|
||||
assert_int_equal(result, ERR_FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
static void test_import_csv(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* 创建临时 CSV 文件 */
|
||||
const char *tmpfile = "test_import.csv";
|
||||
FILE *fp = fopen(tmpfile, "wb");
|
||||
assert_non_null(fp);
|
||||
fprintf(fp, "\xEF\xBB\xBF"); /* UTF-8 BOM */
|
||||
fprintf(fp, "type,path\n");
|
||||
fprintf(fp, "system,C:\\Windows\n");
|
||||
fprintf(fp, "system,C:\\Program Files\n");
|
||||
fprintf(fp, "user,C:\\Users\\test\n");
|
||||
fclose(fp);
|
||||
|
||||
ExportData data;
|
||||
ErrorCode result = import_paths_from_file(tmpfile, &data);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
assert_int_equal(data.system.count, 2);
|
||||
assert_int_equal(data.user.count, 1);
|
||||
assert_string_equal(string_list_get(&data.system, 0), "C:\\Windows");
|
||||
assert_string_equal(string_list_get(&data.system, 1), "C:\\Program Files");
|
||||
assert_string_equal(string_list_get(&data.user, 0), "C:\\Users\\test");
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
clear_string_list(&data.user);
|
||||
}
|
||||
|
||||
static void test_import_csv_quoted(void **state)
|
||||
{
|
||||
(void)state;
|
||||
/* 创建带引号字段的 CSV 文件 */
|
||||
const char *tmpfile = "test_import_quoted.csv";
|
||||
FILE *fp = fopen(tmpfile, "wb");
|
||||
assert_non_null(fp);
|
||||
fprintf(fp, "type,path\n");
|
||||
fprintf(fp, "system,\"C:\\Program Files\"\n");
|
||||
fprintf(fp, "user,\"C:\\My\"\"Path\"\n"); /* 转义引号 */
|
||||
fclose(fp);
|
||||
|
||||
ExportData data;
|
||||
ErrorCode result = import_paths_from_file(tmpfile, &data);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
assert_int_equal(data.system.count, 1);
|
||||
assert_int_equal(data.user.count, 1);
|
||||
assert_string_equal(string_list_get(&data.system, 0), "C:\\Program Files");
|
||||
assert_string_equal(string_list_get(&data.user, 0), "C:\\My\"Path");
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
clear_string_list(&data.user);
|
||||
}
|
||||
|
||||
static void test_export_csv_format(void **state)
|
||||
{
|
||||
(void)state;
|
||||
ExportData data;
|
||||
init_string_list(&data.system);
|
||||
init_string_list(&data.user);
|
||||
add_string_list(&data.system, "C:\\Windows");
|
||||
add_string_list(&data.user, "C:\\Users");
|
||||
|
||||
const char *tmpfile = "test_csv_format.csv";
|
||||
ErrorCode result = export_paths_to_format(&data, tmpfile, EXPORT_CSV);
|
||||
assert_int_equal(result, ERR_OK);
|
||||
|
||||
/* 验证 CSV 格式:type,path(不带双重引号) */
|
||||
FILE *fp = fopen(tmpfile, "rb");
|
||||
assert_non_null(fp);
|
||||
char buffer[4096];
|
||||
fread(buffer, 1, sizeof(buffer) - 1, fp);
|
||||
fclose(fp);
|
||||
|
||||
assert_non_null(strstr(buffer, "system,\"C:\\Windows\""));
|
||||
assert_non_null(strstr(buffer, "user,\"C:\\Users\""));
|
||||
/* 不应该有双重引号 */
|
||||
assert_null(strstr(buffer, "\"\""));
|
||||
|
||||
remove(tmpfile);
|
||||
clear_string_list(&data.system);
|
||||
clear_string_list(&data.user);
|
||||
}
|
||||
|
||||
/* ==================== 主函数 ==================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
/* is_valid_path_format */
|
||||
cmocka_unit_test(test_valid_drive_path),
|
||||
cmocka_unit_test(test_valid_unc_path),
|
||||
cmocka_unit_test(test_valid_env_var),
|
||||
cmocka_unit_test(test_valid_relative_path),
|
||||
cmocka_unit_test(test_null_path),
|
||||
cmocka_unit_test(test_empty_path),
|
||||
cmocka_unit_test(test_invalid_path),
|
||||
cmocka_unit_test(test_drive_only),
|
||||
|
||||
/* export */
|
||||
cmocka_unit_test(test_export_json),
|
||||
cmocka_unit_test(test_export_csv),
|
||||
cmocka_unit_test(test_export_null_data),
|
||||
cmocka_unit_test(test_export_null_path),
|
||||
|
||||
/* import */
|
||||
cmocka_unit_test(test_import_txt),
|
||||
cmocka_unit_test(test_import_null_filepath),
|
||||
cmocka_unit_test(test_import_null_data),
|
||||
cmocka_unit_test(test_import_nonexistent_file),
|
||||
cmocka_unit_test(test_import_csv),
|
||||
cmocka_unit_test(test_import_csv_quoted),
|
||||
|
||||
/* CSV format */
|
||||
cmocka_unit_test(test_export_csv_format),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
@@ -214,6 +214,150 @@ static void test_clear_string_list(void **state)
|
||||
assert_null(list.items);
|
||||
}
|
||||
|
||||
/* ==================== string_list_insert_at 测试 ==================== */
|
||||
|
||||
static void test_insert_at_beginning(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "B");
|
||||
add_string_list(&list, "C");
|
||||
|
||||
int result = string_list_insert_at(&list, 0, "A");
|
||||
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(list.count, 3);
|
||||
assert_string_equal(string_list_get(&list, 0), "A");
|
||||
assert_string_equal(string_list_get(&list, 1), "B");
|
||||
assert_string_equal(string_list_get(&list, 2), "C");
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_insert_at_middle(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "A");
|
||||
add_string_list(&list, "C");
|
||||
|
||||
int result = string_list_insert_at(&list, 1, "B");
|
||||
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(list.count, 3);
|
||||
assert_string_equal(string_list_get(&list, 0), "A");
|
||||
assert_string_equal(string_list_get(&list, 1), "B");
|
||||
assert_string_equal(string_list_get(&list, 2), "C");
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_insert_at_end(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "A");
|
||||
|
||||
int result = string_list_insert_at(&list, 1, "B");
|
||||
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(list.count, 2);
|
||||
assert_string_equal(string_list_get(&list, 0), "A");
|
||||
assert_string_equal(string_list_get(&list, 1), "B");
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_insert_at_empty_list(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
int result = string_list_insert_at(&list, 0, "A");
|
||||
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(list.count, 1);
|
||||
assert_string_equal(string_list_get(&list, 0), "A");
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_insert_at_invalid_index(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "A");
|
||||
|
||||
assert_int_equal(string_list_insert_at(&list, -1, "B"), -1);
|
||||
assert_int_equal(string_list_insert_at(&list, 5, "B"), -1);
|
||||
assert_int_equal(list.count, 1);
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_insert_at_null(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(string_list_insert_at(NULL, 0, "A"), -1);
|
||||
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
assert_int_equal(string_list_insert_at(&list, 0, NULL), -1);
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
/* ==================== string_list_contains 测试 ==================== */
|
||||
|
||||
static void test_contains_found(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "C:\\Windows");
|
||||
add_string_list(&list, "C:\\Program Files");
|
||||
|
||||
assert_int_equal(string_list_contains(&list, "C:\\Windows"), 1);
|
||||
assert_int_equal(string_list_contains(&list, "c:\\windows"), 1); /* 不区分大小写 */
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_contains_not_found(void **state)
|
||||
{
|
||||
(void)state;
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
|
||||
add_string_list(&list, "C:\\Windows");
|
||||
|
||||
assert_int_equal(string_list_contains(&list, "D:\\Tools"), 0);
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
static void test_contains_null(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(string_list_contains(NULL, "test"), 0);
|
||||
|
||||
StringList list;
|
||||
init_string_list(&list);
|
||||
assert_int_equal(string_list_contains(&list, NULL), 0);
|
||||
|
||||
clear_string_list(&list);
|
||||
}
|
||||
|
||||
/* ==================== 编码转换测试 ==================== */
|
||||
|
||||
static void test_utf8_to_wide_normal(void **state)
|
||||
@@ -335,6 +479,19 @@ int main(void)
|
||||
cmocka_unit_test(test_string_list_set_null_list),
|
||||
cmocka_unit_test(test_clear_string_list),
|
||||
|
||||
/* insert_at 测试 */
|
||||
cmocka_unit_test(test_insert_at_beginning),
|
||||
cmocka_unit_test(test_insert_at_middle),
|
||||
cmocka_unit_test(test_insert_at_end),
|
||||
cmocka_unit_test(test_insert_at_empty_list),
|
||||
cmocka_unit_test(test_insert_at_invalid_index),
|
||||
cmocka_unit_test(test_insert_at_null),
|
||||
|
||||
/* contains 测试 */
|
||||
cmocka_unit_test(test_contains_found),
|
||||
cmocka_unit_test(test_contains_not_found),
|
||||
cmocka_unit_test(test_contains_null),
|
||||
|
||||
/* 编码转换测试 */
|
||||
cmocka_unit_test(test_utf8_to_wide_normal),
|
||||
cmocka_unit_test(test_utf8_to_wide_null),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# undo_redo 单元测试
|
||||
add_executable(test_undo_redo test_undo_redo.c
|
||||
${CMAKE_SOURCE_DIR}/src/core/undo_redo.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_undo_redo cmocka)
|
||||
|
||||
target_include_directories(test_undo_redo PRIVATE
|
||||
${CMAKE_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_definitions(test_undo_redo PRIVATE TESTING)
|
||||
|
||||
add_custom_command(TARGET test_undo_redo POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
${CMAKE_BINARY_DIR}/_deps/cmocka-build/src/cmocka.dll
|
||||
$<TARGET_FILE_DIR:test_undo_redo>
|
||||
)
|
||||
|
||||
add_test(NAME undo_redo_test COMMAND test_undo_redo)
|
||||
@@ -0,0 +1,637 @@
|
||||
/*
|
||||
* undo_redo.c 单元测试
|
||||
* 测试撤销/重做管理器
|
||||
*/
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "core/undo_redo.h"
|
||||
#include "core/path_manager.h"
|
||||
#include "utils/string_ext.h"
|
||||
|
||||
/* ==================== Mock 函数 ==================== */
|
||||
|
||||
#ifdef TESTING
|
||||
|
||||
int is_path_valid(const char *path)
|
||||
{
|
||||
(void)path;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void log_info(const char *fmt, ...) { (void)fmt; }
|
||||
void log_debug(const char *fmt, ...) { (void)fmt; }
|
||||
void log_warn(const char *fmt, ...) { (void)fmt; }
|
||||
void log_error(const char *fmt, ...) { (void)fmt; }
|
||||
|
||||
#endif
|
||||
|
||||
/* ==================== 辅助函数 ==================== */
|
||||
|
||||
static OpRecord make_add_record(TargetType target, const char *path)
|
||||
{
|
||||
OpRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
rec.type = OP_ADD;
|
||||
rec.target = target;
|
||||
rec.index = -1;
|
||||
rec.count = 1;
|
||||
rec.old_paths = NULL;
|
||||
char **np = (char **)malloc(sizeof(char *));
|
||||
np[0] = _strdup(path);
|
||||
rec.new_paths = np;
|
||||
return rec;
|
||||
}
|
||||
|
||||
static OpRecord make_delete_record(TargetType target, int index, const char *path)
|
||||
{
|
||||
OpRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
rec.type = OP_DELETE;
|
||||
rec.target = target;
|
||||
rec.index = index;
|
||||
rec.count = 1;
|
||||
char **op = (char **)malloc(sizeof(char *));
|
||||
op[0] = _strdup(path);
|
||||
rec.old_paths = op;
|
||||
rec.new_paths = NULL;
|
||||
return rec;
|
||||
}
|
||||
|
||||
static OpRecord make_edit_record(TargetType target, int index, const char *old_path, const char *new_path)
|
||||
{
|
||||
OpRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
rec.type = OP_EDIT;
|
||||
rec.target = target;
|
||||
rec.index = index;
|
||||
rec.count = 1;
|
||||
char **op = (char **)malloc(sizeof(char *));
|
||||
op[0] = _strdup(old_path);
|
||||
rec.old_paths = op;
|
||||
char **np = (char **)malloc(sizeof(char *));
|
||||
np[0] = _strdup(new_path);
|
||||
rec.new_paths = np;
|
||||
return rec;
|
||||
}
|
||||
|
||||
static OpRecord make_move_record(OperationType type, TargetType target, int index)
|
||||
{
|
||||
OpRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
rec.type = type;
|
||||
rec.target = target;
|
||||
rec.index = index;
|
||||
rec.count = 1;
|
||||
rec.old_paths = NULL;
|
||||
rec.new_paths = NULL;
|
||||
return rec;
|
||||
}
|
||||
|
||||
static OpRecord make_clean_record(TargetType target, StringList *old_list)
|
||||
{
|
||||
OpRecord rec;
|
||||
memset(&rec, 0, sizeof(rec));
|
||||
rec.type = OP_CLEAN;
|
||||
rec.target = target;
|
||||
rec.index = -1;
|
||||
rec.count = old_list->count;
|
||||
if (old_list->count > 0)
|
||||
{
|
||||
char **op = (char **)malloc(old_list->count * sizeof(char *));
|
||||
for (int i = 0; i < old_list->count; i++)
|
||||
op[i] = _strdup(old_list->items[i]);
|
||||
rec.old_paths = op;
|
||||
}
|
||||
else
|
||||
{
|
||||
rec.old_paths = NULL;
|
||||
}
|
||||
rec.new_paths = NULL;
|
||||
return rec;
|
||||
}
|
||||
|
||||
/* ==================== 创建/销毁测试 ==================== */
|
||||
|
||||
static void test_create_manager(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
assert_non_null(mgr);
|
||||
assert_int_equal(mgr->max_size, 10);
|
||||
assert_int_equal(mgr->current, -1);
|
||||
assert_int_equal(mgr->count, 0);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_create_manager_default_size(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(0);
|
||||
assert_non_null(mgr);
|
||||
assert_int_equal(mgr->max_size, 50); /* DEFAULT_MAX_UNDO_RECORDS */
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_destroy_null(void **state)
|
||||
{
|
||||
(void)state;
|
||||
destroy_undo_redo_manager(NULL); /* 不应崩溃 */
|
||||
}
|
||||
|
||||
/* ==================== can_undo/can_redo 测试 ==================== */
|
||||
|
||||
static void test_can_undo_redo_empty(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
assert_int_equal(can_undo(mgr), 0);
|
||||
assert_int_equal(can_redo(mgr), 0);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_can_undo_null(void **state)
|
||||
{
|
||||
(void)state;
|
||||
assert_int_equal(can_undo(NULL), 0);
|
||||
assert_int_equal(can_redo(NULL), 0);
|
||||
}
|
||||
|
||||
/* ==================== push_undo_record 测试 ==================== */
|
||||
|
||||
static void test_push_record(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
OpRecord rec = make_add_record(TARGET_USER, "C:\\Test");
|
||||
|
||||
int result = push_undo_record(mgr, &rec);
|
||||
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(mgr->count, 1);
|
||||
assert_int_equal(mgr->current, 0);
|
||||
assert_int_equal(can_undo(mgr), 1);
|
||||
assert_int_equal(can_redo(mgr), 0);
|
||||
|
||||
/* 清理 */
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_push_null_mgr(void **state)
|
||||
{
|
||||
(void)state;
|
||||
OpRecord rec = make_add_record(TARGET_USER, "C:\\Test");
|
||||
int result = push_undo_record(NULL, &rec);
|
||||
assert_int_equal(result, -1);
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
}
|
||||
|
||||
static void test_push_null_record(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
int result = push_undo_record(mgr, NULL);
|
||||
assert_int_equal(result, -1);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== OP_ADD undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_add(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
/* 添加路径 */
|
||||
add_string_list(&user, "C:\\Test");
|
||||
OpRecord rec = make_add_record(TARGET_USER, "C:\\Test");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
/* 撤销添加 */
|
||||
int result = undo(mgr, &sys, &user);
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(user.count, 0);
|
||||
assert_int_equal(can_redo(mgr), 1);
|
||||
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_redo_add(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Test");
|
||||
OpRecord rec = make_add_record(TARGET_USER, "C:\\Test");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
undo(mgr, &sys, &user);
|
||||
assert_int_equal(user.count, 0);
|
||||
|
||||
/* 重做添加 */
|
||||
int result = redo(mgr, &sys, &user);
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(user.count, 1);
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\Test");
|
||||
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== OP_DELETE undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_delete(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Path1");
|
||||
add_string_list(&user, "C:\\Path2");
|
||||
|
||||
/* 记录删除操作 */
|
||||
OpRecord rec = make_delete_record(TARGET_USER, 0, "C:\\Path1");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
/* 模拟删除 */
|
||||
path_manager_remove_at(&user, 0);
|
||||
assert_int_equal(user.count, 1);
|
||||
|
||||
/* 撤销删除 */
|
||||
int result = undo(mgr, &sys, &user);
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(user.count, 2);
|
||||
|
||||
free(rec.old_paths[0]);
|
||||
free(rec.old_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_redo_delete(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Path1");
|
||||
add_string_list(&user, "C:\\Path2");
|
||||
|
||||
OpRecord rec = make_delete_record(TARGET_USER, 0, "C:\\Path1");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
path_manager_remove_at(&user, 0);
|
||||
undo(mgr, &sys, &user);
|
||||
assert_int_equal(user.count, 2);
|
||||
|
||||
/* 重做删除 */
|
||||
int result = redo(mgr, &sys, &user);
|
||||
assert_int_equal(result, 0);
|
||||
assert_int_equal(user.count, 1);
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\Path2");
|
||||
|
||||
free(rec.old_paths[0]);
|
||||
free(rec.old_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== OP_EDIT undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_edit(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Old");
|
||||
|
||||
OpRecord rec = make_edit_record(TARGET_USER, 0, "C:\\Old", "C:\\New");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
/* 模拟编辑 */
|
||||
string_list_set(&user, 0, "C:\\New");
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\New");
|
||||
|
||||
/* 撤销编辑 */
|
||||
undo(mgr, &sys, &user);
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\Old");
|
||||
|
||||
free(rec.old_paths[0]);
|
||||
free(rec.old_paths);
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_redo_edit(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Old");
|
||||
|
||||
OpRecord rec = make_edit_record(TARGET_USER, 0, "C:\\Old", "C:\\New");
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
string_list_set(&user, 0, "C:\\New");
|
||||
undo(mgr, &sys, &user);
|
||||
|
||||
/* 重做编辑 */
|
||||
redo(mgr, &sys, &user);
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\New");
|
||||
|
||||
free(rec.old_paths[0]);
|
||||
free(rec.old_paths);
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== OP_MOVE undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_move_up(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "A");
|
||||
add_string_list(&user, "B");
|
||||
add_string_list(&user, "C");
|
||||
|
||||
/* 记录上移操作 (index=2, C 上移到 B 前面) */
|
||||
OpRecord rec = make_move_record(OP_MOVE_UP, TARGET_USER, 2);
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
/* 模拟上移 */
|
||||
path_manager_move_up(&user, 2);
|
||||
assert_string_equal(string_list_get(&user, 0), "A");
|
||||
assert_string_equal(string_list_get(&user, 1), "C");
|
||||
assert_string_equal(string_list_get(&user, 2), "B");
|
||||
|
||||
/* 撤销上移 */
|
||||
undo(mgr, &sys, &user);
|
||||
assert_string_equal(string_list_get(&user, 0), "A");
|
||||
assert_string_equal(string_list_get(&user, 1), "B");
|
||||
assert_string_equal(string_list_get(&user, 2), "C");
|
||||
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_redo_move_up(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "A");
|
||||
add_string_list(&user, "B");
|
||||
add_string_list(&user, "C");
|
||||
|
||||
OpRecord rec = make_move_record(OP_MOVE_UP, TARGET_USER, 2);
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
path_manager_move_up(&user, 2);
|
||||
undo(mgr, &sys, &user);
|
||||
|
||||
/* 重做上移 */
|
||||
redo(mgr, &sys, &user);
|
||||
assert_string_equal(string_list_get(&user, 1), "C");
|
||||
assert_string_equal(string_list_get(&user, 2), "B");
|
||||
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== OP_CLEAN undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_clean(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
add_string_list(&user, "C:\\Valid");
|
||||
add_string_list(&user, "C:\\Invalid");
|
||||
add_string_list(&user, "C:\\AlsoValid");
|
||||
|
||||
/* 记录清理前的列表 */
|
||||
OpRecord rec = make_clean_record(TARGET_USER, &user);
|
||||
push_undo_record(mgr, &rec);
|
||||
|
||||
/* 模拟清理(清空) */
|
||||
clear_string_list(&user);
|
||||
assert_int_equal(user.count, 0);
|
||||
|
||||
/* 撤销清理 */
|
||||
undo(mgr, &sys, &user);
|
||||
assert_int_equal(user.count, 3);
|
||||
assert_string_equal(string_list_get(&user, 0), "C:\\Valid");
|
||||
assert_string_equal(string_list_get(&user, 1), "C:\\Invalid");
|
||||
assert_string_equal(string_list_get(&user, 2), "C:\\AlsoValid");
|
||||
|
||||
/* 清理 OpRecord 中的 old_paths */
|
||||
for (int i = 0; i < rec.count; i++)
|
||||
free(rec.old_paths[i]);
|
||||
free(rec.old_paths);
|
||||
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== 连续 undo/redo 测试 ==================== */
|
||||
|
||||
static void test_multiple_undo_redo(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
/* 3 次添加 */
|
||||
OpRecord r1 = make_add_record(TARGET_USER, "A");
|
||||
OpRecord r2 = make_add_record(TARGET_USER, "B");
|
||||
OpRecord r3 = make_add_record(TARGET_USER, "C");
|
||||
|
||||
add_string_list(&user, "A");
|
||||
push_undo_record(mgr, &r1);
|
||||
add_string_list(&user, "B");
|
||||
push_undo_record(mgr, &r2);
|
||||
add_string_list(&user, "C");
|
||||
push_undo_record(mgr, &r3);
|
||||
|
||||
assert_int_equal(user.count, 3);
|
||||
|
||||
/* 连续撤销 3 次 */
|
||||
undo(mgr, &sys, &user);
|
||||
undo(mgr, &sys, &user);
|
||||
undo(mgr, &sys, &user);
|
||||
assert_int_equal(user.count, 0);
|
||||
assert_int_equal(can_undo(mgr), 0);
|
||||
assert_int_equal(can_redo(mgr), 1);
|
||||
|
||||
/* 连续重做 3 次 */
|
||||
redo(mgr, &sys, &user);
|
||||
redo(mgr, &sys, &user);
|
||||
redo(mgr, &sys, &user);
|
||||
assert_int_equal(user.count, 3);
|
||||
assert_int_equal(can_redo(mgr), 0);
|
||||
|
||||
free(r1.new_paths[0]); free(r1.new_paths);
|
||||
free(r2.new_paths[0]); free(r2.new_paths);
|
||||
free(r3.new_paths[0]); free(r3.new_paths);
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== 空栈 undo/redo 测试 ==================== */
|
||||
|
||||
static void test_undo_empty_stack(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
int result = undo(mgr, &sys, &user);
|
||||
assert_int_equal(result, -1);
|
||||
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
static void test_redo_empty_stack(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
StringList sys, user;
|
||||
init_string_list(&sys);
|
||||
init_string_list(&user);
|
||||
|
||||
int result = redo(mgr, &sys, &user);
|
||||
assert_int_equal(result, -1);
|
||||
|
||||
clear_string_list(&sys);
|
||||
clear_string_list(&user);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== clear_undo_redo_history 测试 ==================== */
|
||||
|
||||
static void test_clear_history(void **state)
|
||||
{
|
||||
(void)state;
|
||||
UndoRedoManager *mgr = create_undo_redo_manager(10);
|
||||
|
||||
OpRecord rec = make_add_record(TARGET_USER, "C:\\Test");
|
||||
push_undo_record(mgr, &rec);
|
||||
assert_int_equal(mgr->count, 1);
|
||||
|
||||
clear_undo_redo_history(mgr);
|
||||
assert_int_equal(mgr->count, 0);
|
||||
assert_int_equal(mgr->current, -1);
|
||||
assert_int_equal(can_undo(mgr), 0);
|
||||
|
||||
free(rec.new_paths[0]);
|
||||
free(rec.new_paths);
|
||||
destroy_undo_redo_manager(mgr);
|
||||
}
|
||||
|
||||
/* ==================== 主函数 ==================== */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
const struct CMUnitTest tests[] = {
|
||||
/* 创建/销毁 */
|
||||
cmocka_unit_test(test_create_manager),
|
||||
cmocka_unit_test(test_create_manager_default_size),
|
||||
cmocka_unit_test(test_destroy_null),
|
||||
|
||||
/* can_undo/can_redo */
|
||||
cmocka_unit_test(test_can_undo_redo_empty),
|
||||
cmocka_unit_test(test_can_undo_null),
|
||||
|
||||
/* push_undo_record */
|
||||
cmocka_unit_test(test_push_record),
|
||||
cmocka_unit_test(test_push_null_mgr),
|
||||
cmocka_unit_test(test_push_null_record),
|
||||
|
||||
/* OP_ADD */
|
||||
cmocka_unit_test(test_undo_add),
|
||||
cmocka_unit_test(test_redo_add),
|
||||
|
||||
/* OP_DELETE */
|
||||
cmocka_unit_test(test_undo_delete),
|
||||
cmocka_unit_test(test_redo_delete),
|
||||
|
||||
/* OP_EDIT */
|
||||
cmocka_unit_test(test_undo_edit),
|
||||
cmocka_unit_test(test_redo_edit),
|
||||
|
||||
/* OP_MOVE */
|
||||
cmocka_unit_test(test_undo_move_up),
|
||||
cmocka_unit_test(test_redo_move_up),
|
||||
|
||||
/* OP_CLEAN */
|
||||
cmocka_unit_test(test_undo_clean),
|
||||
|
||||
/* 连续操作 */
|
||||
cmocka_unit_test(test_multiple_undo_redo),
|
||||
|
||||
/* 边界情况 */
|
||||
cmocka_unit_test(test_undo_empty_stack),
|
||||
cmocka_unit_test(test_redo_empty_stack),
|
||||
cmocka_unit_test(test_clear_history),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in New Issue
Block a user