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:
2026-05-03 01:52:06 +08:00
parent 720ebb535d
commit cdcfd8e0a7
27 changed files with 1823 additions and 110 deletions
+3
View File
@@ -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)
+18
View File
@@ -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)
+107
View File
@@ -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);
}
+23
View File
@@ -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);
}
+157
View File
@@ -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),
+24
View File
@@ -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)
+637
View File
@@ -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);
}