feat: 重写为 Tauri + React + TypeScript (v4.0)

完全移除旧 C+IUP 代码,改用 Tauri 2.x + React 19 + TypeScript + Rust 技术栈重写。
功能与 v3.1 完全等价:

- React 前端:Tailwind CSS 4、Zustand 状态管理、i18next 国际化
- Rust 后端:winreg 注册表读写、Win32 API FFI 调用
- 核心逻辑:StringList、UndoRedoManager、PathManager、Import/Export
- 深色模式、中英文切换、键盘快捷键、合并预览
- 66 个 Vitest 单元测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 18:32:54 +08:00
parent cdcfd8e0a7
commit 48129a8908
2545 changed files with 12608 additions and 142894 deletions
-18
View File
@@ -1,18 +0,0 @@
# 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
@@ -1,107 +0,0 @@
/*
* 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);
}
+146
View File
@@ -0,0 +1,146 @@
import { describe, it, expect } from 'vitest';
import {
exportToJson,
exportToCsv,
importFromJson,
importFromCsv,
importFromTxt,
importFromContent,
detectExportFormat,
flattenImportResult,
} from '../../src/core/import-export';
const sampleData = {
system: ['C:\\Windows', 'C:\\Program Files'],
user: ['C:\\Users\\me\\AppData'],
};
describe('exportToJson', () => {
it('导出结构化 JSON', () => {
const json = exportToJson(sampleData);
const parsed = JSON.parse(json);
expect(parsed.version).toBe('1.0');
expect(parsed.type).toBe('PathEditor');
expect(parsed.system).toEqual(sampleData.system);
expect(parsed.user).toEqual(sampleData.user);
expect(parsed.exported).toBeDefined();
});
});
describe('importFromJson', () => {
it('正确导入 JSON', () => {
const json = JSON.stringify(sampleData);
const result = importFromJson(json);
expect(result.system).toEqual(sampleData.system);
expect(result.user).toEqual(sampleData.user);
});
it('过滤空字符串', () => {
const json = JSON.stringify({ system: ['C:\\', '', ' '], user: [] });
const result = importFromJson(json);
expect(result.system).toEqual(['C:\\']);
});
});
describe('exportToCsv', () => {
it('导出 CSV 含 BOM', () => {
const csv = exportToCsv(sampleData);
expect(csv.startsWith('')).toBe(true);
expect(csv).toContain('type,path');
expect(csv).toContain('system,C:\\Windows');
expect(csv).toContain('user,C:\\Users\\me\\AppData');
});
it('CSV 字段转义', () => {
const data = { system: ['C:\\Path,with,commas'], user: [] };
const csv = exportToCsv(data);
expect(csv).toContain('"C:\\Path,with,commas"');
});
it('CSV 双引号转义', () => {
const data = { system: ['Path with "quotes"'], user: [] };
const csv = exportToCsv(data);
expect(csv).toContain('"Path with ""quotes"""');
});
});
describe('importFromCsv', () => {
it('正确导入 CSV', () => {
const csv = 'type,path\nsystem,C:\\Windows\nuser,C:\\AppData\n';
const result = importFromCsv(csv);
expect(result.system).toEqual(['C:\\Windows']);
expect(result.user).toEqual(['C:\\AppData']);
});
it('跳过未知类型', () => {
const csv = 'type,path\nother,C:\\Unknown\nsystem,C:\\Valid';
const result = importFromCsv(csv);
expect(result.system.length).toBe(1);
expect(result.user.length).toBe(0);
});
it('处理带引号的 CSV 字段', () => {
const csv = 'type,path\nsystem,"C:\\Path,With,Commas"';
const result = importFromCsv(csv);
expect(result.system).toEqual(['C:\\Path,With,Commas']);
});
});
describe('importFromTxt', () => {
it('逐行导入,跳过注释和空行', () => {
const txt = '# 这是注释\nC:\\Windows\n\nD:\\Projects\n# 另一个注释';
const paths = importFromTxt(txt);
expect(paths).toEqual(['C:\\Windows', 'D:\\Projects']);
});
it('跳过 BOM', () => {
const txt = 'C:\\Windows';
const paths = importFromTxt(txt);
expect(paths).toEqual(['C:\\Windows']);
});
});
describe('importFromContent', () => {
it('根据扩展名选择格式', () => {
const csvContent = 'type,path\nsystem,C:\\Test';
const jsonContent = JSON.stringify({ system: ['C:\\Test'], user: [] });
const txtContent = 'C:\\Test';
expect(importFromContent(csvContent, 'test.csv').system).toEqual(['C:\\Test']);
expect(importFromContent(jsonContent, 'test.json').system).toEqual(['C:\\Test']);
expect(importFromContent(txtContent, 'test.txt').system).toEqual(['C:\\Test']);
});
});
describe('detectExportFormat', () => {
it('.csv 检测为 CSV', () => {
expect(detectExportFormat('data.CSV')).toBe('csv');
});
it('其他扩展名检测为 JSON', () => {
expect(detectExportFormat('data.json')).toBe('json');
expect(detectExportFormat('data.txt')).toBe('json');
});
});
describe('flattenImportResult', () => {
const data = { system: ['S1'], user: ['U1'] };
it('仅系统', () => {
const r = flattenImportResult(data, 'system');
expect(r.system).toEqual(['S1']);
expect(r.user).toEqual([]);
});
it('仅用户', () => {
const r = flattenImportResult(data, 'user');
expect(r.system).toEqual([]);
expect(r.user).toEqual(['U1']);
});
it('两者都导入', () => {
const r = flattenImportResult(data, 'both');
expect(r.system).toEqual(['S1']);
expect(r.user).toEqual(['U1']);
});
});
-23
View File
@@ -1,23 +0,0 @@
# 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)
@@ -1,324 +0,0 @@
/*
* 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);
}
+100
View File
@@ -0,0 +1,100 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
pathRemoveAt,
pathMoveUp,
pathMoveDown,
pathClean,
batchRemoveAt,
} from '../../src/core/path-manager';
import { StringList } from '../../src/core/string-list';
// 模拟验证函数:所有路径都"有效"
const alwaysValid = () => true;
// 模拟验证函数:C:\\Invalid 无效
const validateFn = (path: string) => !path.includes('Invalid');
describe('pathRemoveAt', () => {
it('删除指定索引', () => {
const list = StringList.fromArray(['a', 'b', 'c']);
pathRemoveAt(list, 1);
expect(list.toArray()).toEqual(['a', 'c']);
});
});
describe('pathMoveUp', () => {
it('上移元素', () => {
const list = StringList.fromArray(['a', 'b', 'c']);
pathMoveUp(list, 1);
expect(list.toArray()).toEqual(['b', 'a', 'c']);
});
it('第一个元素不能上移', () => {
const list = StringList.fromArray(['a', 'b']);
expect(pathMoveUp(list, 0)).toBe(false);
expect(list.toArray()).toEqual(['a', 'b']);
});
it('无效索引不能上移', () => {
const list = StringList.fromArray(['a']);
expect(pathMoveUp(list, -1)).toBe(false);
expect(pathMoveUp(list, 5)).toBe(false);
});
});
describe('pathMoveDown', () => {
it('下移元素', () => {
const list = StringList.fromArray(['a', 'b', 'c']);
pathMoveDown(list, 0);
expect(list.toArray()).toEqual(['b', 'a', 'c']);
});
it('最后一个元素不能下移', () => {
const list = StringList.fromArray(['a', 'b']);
expect(pathMoveDown(list, 1)).toBe(false);
});
});
describe('batchRemoveAt', () => {
it('批量删除(按从大到小排序)', () => {
const list = StringList.fromArray(['a', 'b', 'c', 'd', 'e']);
batchRemoveAt(list, [0, 2, 4]);
expect(list.toArray()).toEqual(['b', 'd']);
});
it('删除乱序索引', () => {
const list = StringList.fromArray(['a', 'b', 'c', 'd']);
batchRemoveAt(list, [3, 0]);
expect(list.toArray()).toEqual(['b', 'c']);
});
});
describe('pathClean', () => {
it('移除无效路径', () => {
const list = StringList.fromArray(['C:\\Valid', 'C:\\Invalid', 'D:\\Valid']);
const removed = pathClean(list, validateFn);
expect(list.toArray()).toEqual(['C:\\Valid', 'D:\\Valid']);
expect(removed).toEqual(['C:\\Invalid']);
});
it('移除重复路径(保留一个)', () => {
const list = StringList.fromArray(['C:\\Valid', 'C:\\Valid', 'D:\\Valid']);
const removed = pathClean(list, alwaysValid);
expect(list.length).toBe(2);
expect(removed.length).toBeGreaterThanOrEqual(1);
});
it('全部有效无变化', () => {
const list = StringList.fromArray(['C:\\a', 'D:\\b']);
const removed = pathClean(list, alwaysValid);
expect(list.toArray()).toEqual(['C:\\a', 'D:\\b']);
expect(removed.length).toBe(0);
});
it('全部无效全部移除', () => {
const list = StringList.fromArray(['C:\\Invalid1', 'C:\\Invalid2']);
const removed = pathClean(list, validateFn);
expect(list.length).toBe(0);
expect(removed.length).toBe(2);
});
});
-20
View File
@@ -1,20 +0,0 @@
# path_manager 单元测试
add_executable(test_path_manager test_path_manager.c
${CMAKE_SOURCE_DIR}/src/core/path_manager.c
${CMAKE_SOURCE_DIR}/src/utils/string_ext.c
${CMAKE_SOURCE_DIR}/src/utils/safe_string.c
${CMAKE_SOURCE_DIR}/src/utils/error_code.c
)
target_link_libraries(test_path_manager cmocka)
target_include_directories(test_path_manager PRIVATE
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/utils
)
# 定义 TESTING 宏以启用 mock
target_compile_definitions(test_path_manager PRIVATE TESTING)
# 添加测试
add_test(NAME path_manager_test COMMAND test_path_manager)
-359
View File
@@ -1,359 +0,0 @@
/*
* path_manager.c 单元测试
* 测试路径管理函数
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include "core/path_manager.h"
/* ==================== Mock 函数 ==================== */
#ifdef TESTING
/* Mock is_path_valid - 默认返回 1(有效)*/
int is_path_valid_mock_enabled = 0;
int is_path_valid_mock_return = 1;
int is_path_valid(const char *path)
{
(void)path;
if (is_path_valid_mock_enabled) {
return is_path_valid_mock_return;
}
return 1; /* 默认认为路径有效 */
}
/* Mock 日志函数 - 避免链接日志文件依赖 */
int log_info_enabled = 0;
int log_debug_enabled = 0;
int log_warn_enabled = 0;
int log_error_enabled = 0;
void log_info(const char *fmt, ...)
{
(void)fmt;
log_info_enabled++;
}
void log_debug(const char *fmt, ...)
{
(void)fmt;
log_debug_enabled++;
}
void log_warn(const char *fmt, ...)
{
(void)fmt;
log_warn_enabled++;
}
void log_error(const char *fmt, ...)
{
(void)fmt;
log_error_enabled++;
}
#endif /* TESTING */
/* ==================== path_manager_remove_at 测试 ==================== */
static void test_remove_at_normal(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
add_string_list(&list, "path3");
ErrorCode result = path_manager_remove_at(&list, 1);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, 2);
assert_string_equal(string_list_get(&list, 0), "path1");
assert_string_equal(string_list_get(&list, 1), "path3");
clear_string_list(&list);
}
static void test_remove_at_first(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_remove_at(&list, 0);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, 1);
assert_string_equal(string_list_get(&list, 0), "path2");
clear_string_list(&list);
}
static void test_remove_at_last(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_remove_at(&list, 1);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, 1);
assert_string_equal(string_list_get(&list, 0), "path1");
clear_string_list(&list);
}
static void test_remove_at_invalid_index(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
ErrorCode result = path_manager_remove_at(&list, 5); /* 越界 */
assert_int_not_equal(result, ERR_OK);
clear_string_list(&list);
}
static void test_remove_at_null(void **state)
{
(void)state;
ErrorCode result = path_manager_remove_at(NULL, 0);
assert_int_equal(result, ERR_NULL_PTR);
}
/* ==================== path_manager_move_up 测试 ==================== */
static void test_move_up_normal(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
add_string_list(&list, "path3");
ErrorCode result = path_manager_move_up(&list, 2);
assert_int_equal(result, ERR_OK);
assert_string_equal(string_list_get(&list, 0), "path1");
assert_string_equal(string_list_get(&list, 1), "path3");
assert_string_equal(string_list_get(&list, 2), "path2");
clear_string_list(&list);
}
static void test_move_up_first_element(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_move_up(&list, 0); /* 第一个元素无法上移 */
assert_int_equal(result, ERR_INVALID_INDEX);
clear_string_list(&list);
}
static void test_move_up_invalid_index(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_move_up(&list, 5); /* 越界 */
assert_int_not_equal(result, ERR_OK);
clear_string_list(&list);
}
static void test_move_up_single_element(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
ErrorCode result = path_manager_move_up(&list, 0);
assert_int_equal(result, ERR_INVALID_INDEX);
assert_string_equal(string_list_get(&list, 0), "path1");
clear_string_list(&list);
}
/* ==================== path_manager_move_down 测试 ==================== */
static void test_move_down_normal(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
add_string_list(&list, "path3");
ErrorCode result = path_manager_move_down(&list, 1);
assert_int_equal(result, ERR_OK);
assert_string_equal(string_list_get(&list, 0), "path1");
assert_string_equal(string_list_get(&list, 1), "path3");
assert_string_equal(string_list_get(&list, 2), "path2");
clear_string_list(&list);
}
static void test_move_down_last_element(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_move_down(&list, 1); /* 最后一个元素无法下移 */
assert_int_equal(result, ERR_INVALID_INDEX);
clear_string_list(&list);
}
static void test_move_down_invalid_index(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
add_string_list(&list, "path2");
ErrorCode result = path_manager_move_down(&list, 5); /* 越界 */
assert_int_not_equal(result, ERR_OK);
clear_string_list(&list);
}
static void test_move_down_single_element(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "path1");
ErrorCode result = path_manager_move_down(&list, 0);
assert_int_equal(result, ERR_INVALID_INDEX);
assert_string_equal(string_list_get(&list, 0), "path1");
clear_string_list(&list);
}
/* ==================== path_manager_clean 测试 ==================== */
static void test_clean_no_invalid(void **state)
{
(void)state;
is_path_valid_mock_enabled = 1;
is_path_valid_mock_return = 1;
StringList list;
init_string_list(&list);
add_string_list(&list, "C:\\Windows");
add_string_list(&list, "C:\\Program Files");
int before_count = list.count;
ErrorCode result = path_manager_clean(&list);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, before_count); /* 没有无效路径,不应删除 */
is_path_valid_mock_enabled = 0;
clear_string_list(&list);
}
static void test_clean_with_invalid(void **state)
{
(void)state;
is_path_valid_mock_enabled = 1;
is_path_valid_mock_return = 0; /* 所有路径都无效 */
StringList list;
init_string_list(&list);
add_string_list(&list, "C:\\Invalid1");
add_string_list(&list, "C:\\Invalid2");
add_string_list(&list, "C:\\Invalid3");
ErrorCode result = path_manager_clean(&list);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, 0); /* 所有路径都被删除 */
is_path_valid_mock_enabled = 0;
clear_string_list(&list);
}
static void test_clean_empty_list(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
ErrorCode result = path_manager_clean(&list);
assert_int_equal(result, ERR_OK);
assert_int_equal(list.count, 0);
clear_string_list(&list);
}
/* ==================== 主函数 ==================== */
int main(void)
{
const struct CMUnitTest tests[] = {
/* remove_at 测试 */
cmocka_unit_test(test_remove_at_normal),
cmocka_unit_test(test_remove_at_first),
cmocka_unit_test(test_remove_at_last),
cmocka_unit_test(test_remove_at_invalid_index),
cmocka_unit_test(test_remove_at_null),
/* move_up 测试 */
cmocka_unit_test(test_move_up_normal),
cmocka_unit_test(test_move_up_first_element),
cmocka_unit_test(test_move_up_invalid_index),
cmocka_unit_test(test_move_up_single_element),
/* move_down 测试 */
cmocka_unit_test(test_move_down_normal),
cmocka_unit_test(test_move_down_last_element),
cmocka_unit_test(test_move_down_invalid_index),
cmocka_unit_test(test_move_down_single_element),
/* clean 测试 */
cmocka_unit_test(test_clean_no_invalid),
cmocka_unit_test(test_clean_with_invalid),
cmocka_unit_test(test_clean_empty_list),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
-16
View File
@@ -1,16 +0,0 @@
# safe_string 单元测试
add_executable(test_safe_string test_safe_string.c
${CMAKE_SOURCE_DIR}/src/utils/safe_string.c
)
target_link_libraries(test_safe_string cmocka)
target_include_directories(test_safe_string PRIVATE
${CMAKE_SOURCE_DIR}/src/utils
)
# 定义 TESTING 宏以启用 mock
target_compile_definitions(test_safe_string PRIVATE TESTING)
# 添加测试
add_test(NAME safe_string_test COMMAND test_safe_string)
-209
View File
@@ -1,209 +0,0 @@
/*
* safe_string.c 单元测试
* 测试安全字符串操作函数
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include "utils/safe_string.h"
/* ==================== safe_strcpy 测试 ==================== */
static void test_safe_strcpy_normal(void **state)
{
(void)state;
char dst[32];
const char *src = "Hello, World!";
char *result = safe_strcpy(dst, sizeof(dst), src);
assert_non_null(result);
assert_string_equal(dst, src);
}
static void test_safe_strcpy_truncation(void **state)
{
(void)state;
char dst[8];
const char *src = "This is a long string";
char *result = safe_strcpy(dst, sizeof(dst), src);
assert_non_null(result);
assert_int_equal(strlen(dst), 7); /* 截断到 7 字符 */
assert_memory_equal(dst, src, 7); /* 前 7 字符相同 */
assert_int_equal(dst[7], '\0');
}
static void test_safe_strcpy_null_dst(void **state)
{
(void)state;
const char *src = "test";
char *result = safe_strcpy(NULL, 10, src);
assert_null(result);
}
static void test_safe_strcpy_null_src(void **state)
{
(void)state;
char dst[32];
char *result = safe_strcpy(dst, sizeof(dst), NULL);
assert_null(result);
}
static void test_safe_strcpy_zero_size(void **state)
{
(void)state;
char dst[32];
char *result = safe_strcpy(dst, 0, "test");
assert_null(result);
}
static void test_safe_strcpy_exact_fit(void **state)
{
(void)state;
char dst[6];
const char *src = "12345"; /* 5字符 + 1终止符 = 6 */
char *result = safe_strcpy(dst, sizeof(dst), src);
assert_non_null(result);
assert_string_equal(dst, "12345");
}
/* ==================== safe_strcat 测试 ==================== */
static void test_safe_strcat_normal(void **state)
{
(void)state;
char dst[32] = "Hello";
const char *src = ", World!";
char *result = safe_strcat(dst, sizeof(dst), src);
assert_non_null(result);
assert_string_equal(dst, "Hello, World!");
}
static void test_safe_strcat_truncation(void **state)
{
(void)state;
char dst[12] = "Hello";
const char *src = ", World!"; /* 总长 12,但最后一个位置要放 \0 */
char *result = safe_strcat(dst, sizeof(dst), src);
assert_non_null(result);
/* dst 有 11 可用位置 (12-1)"Hello" 占 5,还剩 6 */
/* ", World!" 有 9 字符,只能放 6 个字符 + \0 */
assert_true(strlen(dst) <= 11);
}
static void test_safe_strcat_null_dst(void **state)
{
(void)state;
char dst[32] = "test";
char *result = safe_strcat(NULL, sizeof(dst), "src");
assert_null(result);
}
static void test_safe_strcat_null_src(void **state)
{
(void)state;
char dst[32] = "test";
char *result = safe_strcat(dst, sizeof(dst), NULL);
/* src 为 NULL 时函数返回 NULL */
assert_null(result);
}
static void test_safe_strcat_empty_dst(void **state)
{
(void)state;
char dst[32] = "";
const char *src = "Hello";
char *result = safe_strcat(dst, sizeof(dst), src);
assert_non_null(result);
assert_string_equal(dst, "Hello");
}
/* ==================== safe_strdup 测试 ==================== */
static void test_safe_strdup_normal(void **state)
{
(void)state;
const char *src = "Hello, World!";
char *result = safe_strdup(src);
assert_non_null(result);
assert_string_equal(result, src);
assert_ptr_not_equal(result, src); /* 必须是新分配的内存 */
free(result);
}
static void test_safe_strdup_null(void **state)
{
(void)state;
char *result = safe_strdup(NULL);
assert_null(result);
}
static void test_safe_strdup_empty_string(void **state)
{
(void)state;
const char *src = "";
char *result = safe_strdup(src);
assert_non_null(result);
assert_string_equal(result, "");
free(result);
}
static void test_safe_strdup_long_string(void **state)
{
(void)state;
/* 构造一个 510 字符的字符串 */
char src[511];
for (int i = 0; i < 510; i++) {
src[i] = 'A';
}
src[510] = '\0';
char *result = safe_strdup(src);
assert_non_null(result);
assert_int_equal(strlen(result), 510);
assert_memory_equal(result, src, 510);
free(result);
}
/* ==================== 主函数 ==================== */
int main(void)
{
const struct CMUnitTest tests[] = {
/* safe_strcpy 测试 */
cmocka_unit_test(test_safe_strcpy_normal),
cmocka_unit_test(test_safe_strcpy_truncation),
cmocka_unit_test(test_safe_strcpy_null_dst),
cmocka_unit_test(test_safe_strcpy_null_src),
cmocka_unit_test(test_safe_strcpy_zero_size),
cmocka_unit_test(test_safe_strcpy_exact_fit),
/* safe_strcat 测试 */
cmocka_unit_test(test_safe_strcat_normal),
cmocka_unit_test(test_safe_strcat_truncation),
cmocka_unit_test(test_safe_strcat_null_dst),
cmocka_unit_test(test_safe_strcat_null_src),
cmocka_unit_test(test_safe_strcat_empty_dst),
/* safe_strdup 测试 */
cmocka_unit_test(test_safe_strdup_normal),
cmocka_unit_test(test_safe_strdup_null),
cmocka_unit_test(test_safe_strdup_empty_string),
cmocka_unit_test(test_safe_strdup_long_string),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+91
View File
@@ -0,0 +1,91 @@
import { describe, it, expect } from 'vitest';
import { StringList } from '../../src/core/string-list';
describe('StringList', () => {
it('初始为空', () => {
const list = new StringList();
expect(list.length).toBe(0);
});
it('添加和读取元素', () => {
const list = new StringList();
list.add('C:\\Windows');
list.add('C:\\Users');
expect(list.length).toBe(2);
expect(list.get(0)).toBe('C:\\Windows');
expect(list.get(1)).toBe('C:\\Users');
expect(list.get(99)).toBeUndefined();
});
it('在指定位置插入', () => {
const list = new StringList();
list.add('a');
list.add('c');
list.insertAt(1, 'b');
expect(list.toArray()).toEqual(['a', 'b', 'c']);
});
it('在开头插入', () => {
const list = StringList.fromArray(['b', 'c']);
list.insertAt(0, 'a');
expect(list.toArray()).toEqual(['a', 'b', 'c']);
});
it('删除指定位置', () => {
const list = StringList.fromArray(['a', 'b', 'c']);
list.removeAt(1);
expect(list.toArray()).toEqual(['a', 'c']);
});
it('设置元素', () => {
const list = StringList.fromArray(['old']);
list.set(0, 'new');
expect(list.get(0)).toBe('new');
});
it('不区分大小写查找', () => {
const list = StringList.fromArray(['C:\\Windows', 'C:\\Users']);
expect(list.contains('c:\\windows')).toBe(true);
expect(list.contains('C:\\WINDOWS')).toBe(true);
expect(list.contains('C:\\Other')).toBe(false);
});
it('不区分大小写索引', () => {
const list = StringList.fromArray(['C:\\Windows', 'C:\\Users']);
expect(list.indexOfIgnoreCase('c:\\windows')).toBe(0);
expect(list.indexOfIgnoreCase('c:\\users')).toBe(1);
expect(list.indexOfIgnoreCase('nope')).toBe(-1);
});
it('交换元素', () => {
const list = StringList.fromArray(['a', 'b']);
list.swap(0, 1);
expect(list.toArray()).toEqual(['b', 'a']);
});
it('清空', () => {
const list = StringList.fromArray(['a', 'b', 'c']);
list.clear();
expect(list.length).toBe(0);
});
it('深拷贝', () => {
const original = StringList.fromArray(['a', 'b']);
const cloned = original.clone();
cloned.set(0, 'modified');
expect(original.get(0)).toBe('a');
expect(cloned.get(0)).toBe('modified');
});
it('fromArray 和 toArray', () => {
const arr = ['x', 'y', 'z'];
const list = StringList.fromArray(arr);
expect(list.toArray()).toEqual(arr);
expect(list.length).toBe(3);
});
it('all 返回只读数组', () => {
const list = StringList.fromArray(['a', 'b']);
expect(list.all).toEqual(['a', 'b']);
});
});
-18
View File
@@ -1,18 +0,0 @@
# string_ext 单元测试
add_executable(test_string_ext test_string_ext.c
${CMAKE_SOURCE_DIR}/src/utils/string_ext.c
${CMAKE_SOURCE_DIR}/src/utils/safe_string.c
)
target_link_libraries(test_string_ext cmocka)
target_include_directories(test_string_ext PRIVATE
${CMAKE_SOURCE_DIR}/src/utils
${CMAKE_SOURCE_DIR}/tests/mocks
)
# 定义 TESTING 宏和 REPLACE_WINDOWS_API 以启用 mock
target_compile_definitions(test_string_ext PRIVATE TESTING REPLACE_WINDOWS_API)
# 添加测试
add_test(NAME string_ext_test COMMAND test_string_ext)
-511
View File
@@ -1,511 +0,0 @@
/*
* string_ext.c 单元测试
* 测试字符串扩展函数和 StringList 操作
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include "utils/string_ext.h"
#include "mock_windows.h"
/* ==================== Mock 计数器 ==================== */
int mock_MultiByteToWideChar_call_count = 0;
int mock_WideCharToMultiByte_call_count = 0;
static int mock_MB2WC_return = 0;
static int mock_WC2MB_return = 0;
void mock_set_MultiByteToWideChar_return(int ret) {
mock_MB2WC_return = ret;
}
void mock_set_WideCharToMultiByte_return(int ret) {
mock_WC2MB_return = ret;
}
/* ==================== Mock 实现 ==================== */
int mock_MultiByteToWideChar(
UINT CodePage,
DWORD dwFlags,
LPCSTR lpMultiByteStr,
int cbMultiByte,
LPWSTR lpWideCharStr,
int cchWideChar)
{
(void)CodePage;
(void)dwFlags;
mock_MultiByteToWideChar_call_count++;
if (!lpMultiByteStr || cbMultiByte == 0)
return 0;
int len = (cbMultiByte == -1) ? strlen(lpMultiByteStr) : cbMultiByte;
/* 简单 ASCII 转宽字符 */
if (lpWideCharStr && cchWideChar > 0) {
int copy_len = (cchWideChar < len + 1) ? cchWideChar - 1 : len;
for (int i = 0; i < copy_len; i++) {
lpWideCharStr[i] = (wchar_t)lpMultiByteStr[i];
}
lpWideCharStr[copy_len] = L'\0';
}
return mock_MB2WC_return > 0 ? mock_MB2WC_return : (len + 1);
}
int mock_WideCharToMultiByte(
UINT CodePage,
DWORD dwFlags,
LPCWSTR lpWideCharStr,
int cchWideChar,
LPSTR lpMultiByteStr,
int cbMultiByte,
LPCSTR lpDefaultChar,
LPBOOL lpUsedDefaultChar)
{
(void)CodePage;
(void)dwFlags;
(void)lpDefaultChar;
(void)lpUsedDefaultChar;
mock_WideCharToMultiByte_call_count++;
if (!lpWideCharStr || cchWideChar == 0)
return 0;
int len = (cchWideChar == -1) ? wcslen(lpWideCharStr) : cchWideChar;
if (lpMultiByteStr && cbMultiByte > 0) {
int copy_len = (cbMultiByte < len + 1) ? cbMultiByte - 1 : len;
for (int i = 0; i < copy_len; i++) {
lpMultiByteStr[i] = (char)lpWideCharStr[i];
}
lpMultiByteStr[copy_len] = '\0';
}
return mock_WC2MB_return > 0 ? mock_WC2MB_return : (len + 1);
}
/* ==================== StringList 测试 ==================== */
static void test_init_string_list(void **state)
{
(void)state;
StringList list;
list.items = (void *)0x1234; /* 初始化前设置垃圾值 */
list.count = 999;
list.capacity = 999;
init_string_list(&list);
/* init_string_list 将 items 设置为 NULLcount 和 capacity 设为 0 */
assert_null(list.items);
assert_int_equal(list.count, 0);
assert_int_equal(list.capacity, 0);
/* clear_string_list 对 NULL items 应该安全处理 */
clear_string_list(&list);
}
static void test_add_string_list_single(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "C:\\Windows");
assert_int_equal(list.count, 1);
assert_string_equal(string_list_get(&list, 0), "C:\\Windows");
clear_string_list(&list);
}
static void test_add_string_list_multiple(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "C:\\Windows");
add_string_list(&list, "C:\\Program Files");
add_string_list(&list, "D:\\Tools");
assert_int_equal(list.count, 3);
assert_string_equal(string_list_get(&list, 0), "C:\\Windows");
assert_string_equal(string_list_get(&list, 1), "C:\\Program Files");
assert_string_equal(string_list_get(&list, 2), "D:\\Tools");
clear_string_list(&list);
}
static void test_string_list_get_out_of_bounds(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "test");
assert_null(string_list_get(&list, -1));
assert_null(string_list_get(&list, 1));
assert_null(string_list_get(&list, 100));
clear_string_list(&list);
}
static void test_string_list_get_null_list(void **state)
{
(void)state;
assert_null(string_list_get(NULL, 0));
}
static void test_string_list_set_normal(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "original");
int result = string_list_set(&list, 0, "modified");
assert_int_equal(result, 0);
assert_string_equal(string_list_get(&list, 0), "modified");
clear_string_list(&list);
}
static void test_string_list_set_out_of_bounds(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "test");
int result = string_list_set(&list, 5, "modified");
assert_int_equal(result, -1);
clear_string_list(&list);
}
static void test_string_list_set_null_list(void **state)
{
(void)state;
int result = string_list_set(NULL, 0, "test");
assert_int_equal(result, -1);
}
static void test_clear_string_list(void **state)
{
(void)state;
StringList list;
init_string_list(&list);
add_string_list(&list, "item1");
add_string_list(&list, "item2");
add_string_list(&list, "item3");
clear_string_list(&list);
assert_int_equal(list.count, 0);
assert_null(list.items);
}
/* ==================== 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)
{
(void)state;
const char *utf8_str = "Hello";
wchar_t *result = utf8_to_wide(utf8_str);
if (result) {
assert_true(wcscmp(result, L"Hello") == 0);
free(result);
}
}
static void test_utf8_to_wide_null(void **state)
{
(void)state;
wchar_t *result = utf8_to_wide(NULL);
assert_null(result);
}
static void test_wide_to_utf8_normal(void **state)
{
(void)state;
const wchar_t *wide_str = L"World";
char *result = wide_to_utf8(wide_str);
if (result) {
assert_string_equal(result, "World");
free(result);
}
}
static void test_wide_to_utf8_null(void **state)
{
(void)state;
char *result = wide_to_utf8(NULL);
assert_null(result);
}
/* ==================== stristr 测试 ==================== */
static void test_stristr_found(void **state)
{
(void)state;
const char *haystack = "The quick brown fox";
const char *needle = "quick";
char *result = stristr(haystack, needle);
assert_non_null(result);
assert_ptr_equal(result, haystack + 4); /* "quick" 在 "The " 之后 */
}
static void test_stristr_not_found(void **state)
{
(void)state;
const char *haystack = "The quick brown fox";
const char *needle = "jumps";
char *result = stristr(haystack, needle);
assert_null(result);
}
static void test_stristr_case_insensitive(void **state)
{
(void)state;
const char *haystack = "The QUICK brown fox";
const char *needle = "quick";
char *result = stristr(haystack, needle);
assert_non_null(result);
}
static void test_stristr_null_haystack(void **state)
{
(void)state;
char *result = stristr(NULL, "test");
assert_null(result);
}
static void test_stristr_null_needle(void **state)
{
(void)state;
char *result = stristr("test", NULL);
assert_null(result);
}
static void test_stristr_empty_needle(void **state)
{
(void)state;
const char *haystack = "The quick brown fox";
const char *needle = "";
char *result = stristr(haystack, needle);
/* 空字符串应该返回原字符串首地址 */
assert_non_null(result);
assert_ptr_equal(result, haystack);
}
/* ==================== 主函数 ==================== */
int main(void)
{
const struct CMUnitTest tests[] = {
/* StringList 测试 */
cmocka_unit_test(test_init_string_list),
cmocka_unit_test(test_add_string_list_single),
cmocka_unit_test(test_add_string_list_multiple),
cmocka_unit_test(test_string_list_get_out_of_bounds),
cmocka_unit_test(test_string_list_get_null_list),
cmocka_unit_test(test_string_list_set_normal),
cmocka_unit_test(test_string_list_set_out_of_bounds),
cmocka_unit_test(test_string_list_set_null_list),
cmocka_unit_test(test_clear_string_list),
/* 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),
cmocka_unit_test(test_wide_to_utf8_normal),
cmocka_unit_test(test_wide_to_utf8_null),
/* stristr 测试 */
cmocka_unit_test(test_stristr_found),
cmocka_unit_test(test_stristr_not_found),
cmocka_unit_test(test_stristr_case_insensitive),
cmocka_unit_test(test_stristr_null_haystack),
cmocka_unit_test(test_stristr_null_needle),
cmocka_unit_test(test_stristr_empty_needle),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}
+223
View File
@@ -0,0 +1,223 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
UndoRedoManager,
OperationType,
TargetType,
type OpRecord,
} from '../../src/core/undo-redo';
import { StringList } from '../../src/core/string-list';
function makeRecord(
type: OperationType,
target: TargetType,
index: number,
count: number,
oldPaths: string[],
newPaths: string[],
): OpRecord {
return { type, target, index, count, oldPaths, newPaths };
}
describe('UndoRedoManager', () => {
let mgr: UndoRedoManager;
let sysPaths: StringList;
let userPaths: StringList;
beforeEach(() => {
mgr = new UndoRedoManager(50);
sysPaths = StringList.fromArray(['C:\\Windows', 'C:\\Program Files']);
userPaths = StringList.fromArray(['C:\\Users\\me\\AppData']);
});
// ── 基本状态 ──
it('初始不可撤销不可重做', () => {
expect(mgr.canUndo()).toBe(false);
expect(mgr.canRedo()).toBe(false);
});
// ── ADD ──
it('ADD 撤销/重做', () => {
sysPaths.add('C:\\NewPath');
mgr.push(
makeRecord(OperationType.ADD, TargetType.SYSTEM, 2, 1, [], ['C:\\NewPath']),
);
expect(mgr.canUndo()).toBe(true);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Windows', 'C:\\Program Files']);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Windows', 'C:\\Program Files', 'C:\\NewPath']);
});
// ── DELETE ──
it('DELETE 撤销/重做', () => {
const removed = sysPaths.get(0)!;
mgr.push(
makeRecord(OperationType.DELETE, TargetType.SYSTEM, 0, 1, [removed], []),
);
sysPaths.removeAt(0);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.get(0)).toBe(removed);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Program Files']);
});
// ── EDIT ──
it('EDIT 撤销/重做', () => {
const oldVal = sysPaths.get(0)!;
mgr.push(
makeRecord(OperationType.EDIT, TargetType.SYSTEM, 0, 1, [oldVal], ['C:\\Edited']),
);
sysPaths.set(0, 'C:\\Edited');
mgr.undo(sysPaths, userPaths);
expect(sysPaths.get(0)).toBe(oldVal);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.get(0)).toBe('C:\\Edited');
});
// ── MOVE_UP ──
it('MOVE_UP 撤销/重做', () => {
mgr.push(
makeRecord(OperationType.MOVE_UP, TargetType.SYSTEM, 1, 1, [], []),
);
sysPaths.swap(0, 1);
expect(sysPaths.toArray()).toEqual(['C:\\Program Files', 'C:\\Windows']);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Windows', 'C:\\Program Files']);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Program Files', 'C:\\Windows']);
});
// ── MOVE_DOWN ──
it('MOVE_DOWN 撤销/重做', () => {
mgr.push(
makeRecord(OperationType.MOVE_DOWN, TargetType.SYSTEM, 0, 1, [], []),
);
sysPaths.swap(0, 1);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Windows', 'C:\\Program Files']);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(['C:\\Program Files', 'C:\\Windows']);
});
// ── CLEAN ──
it('CLEAN 撤销/重做', () => {
const oldPaths = sysPaths.toArray();
const newPaths = ['C:\\Windows']; // 假设 Program Files 被清理掉了
mgr.push(
makeRecord(OperationType.CLEAN, TargetType.SYSTEM, 0, 2, oldPaths, newPaths),
);
sysPaths.clear();
for (const p of newPaths) sysPaths.add(p);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(oldPaths);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(newPaths);
});
// ── CLEAR ──
it('CLEAR 撤销/重做', () => {
const oldPaths = sysPaths.toArray();
mgr.push(
makeRecord(OperationType.CLEAR, TargetType.SYSTEM, 0, 2, oldPaths, []),
);
sysPaths.clear();
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(oldPaths);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.length).toBe(0);
});
// ── IMPORT ──
it('IMPORT 撤销/重做', () => {
const oldPaths = sysPaths.toArray();
const imported = ['C:\\New1', 'C:\\New2'];
mgr.push(
makeRecord(OperationType.IMPORT, TargetType.SYSTEM, 0, 2, oldPaths, imported),
);
sysPaths.clear();
for (const p of imported) sysPaths.add(p);
mgr.undo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(oldPaths);
mgr.redo(sysPaths, userPaths);
expect(sysPaths.toArray()).toEqual(imported);
});
// ── 重做分支截断 ──
it('新操作后截断重做分支', () => {
mgr.push(
makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], ['first']),
);
mgr.undo(sysPaths, userPaths);
expect(mgr.canRedo()).toBe(true);
// 推入新操作,重做分支被截断
mgr.push(
makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], ['second']),
);
expect(mgr.canRedo()).toBe(false);
});
// ── 历史限制 ──
it('超出最大历史容量时移除最旧记录', () => {
const small = new UndoRedoManager(3);
for (let i = 0; i < 5; i++) {
small.push(
makeRecord(OperationType.ADD, TargetType.SYSTEM, 0, 1, [], [`path_${i}`]),
);
}
expect(small.historyLength).toBe(3);
});
// ── USER 目标 ──
it('操作 USER 路径', () => {
userPaths.add('C:\\NewUserPath');
mgr.push(
makeRecord(OperationType.ADD, TargetType.USER, 1, 1, [], ['C:\\NewUserPath']),
);
mgr.undo(sysPaths, userPaths);
expect(userPaths.toArray()).toEqual(['C:\\Users\\me\\AppData']);
expect(sysPaths.toArray()).toEqual(['C:\\Windows', 'C:\\Program Files']);
});
});
-24
View File
@@ -1,24 +0,0 @@
# 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
@@ -1,637 +0,0 @@
/*
* 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);
}
+62
View File
@@ -0,0 +1,62 @@
import { describe, it, expect } from 'vitest';
import { is_valid_path_format, join_path, split_path } from '../../src/core/validation';
describe('is_valid_path_format', () => {
it('合法的驱动器路径', () => {
expect(is_valid_path_format('C:\\Windows\\System32')).toBe(true);
expect(is_valid_path_format('D:/Projects')).toBe(true);
expect(is_valid_path_format('E:\\')).toBe(true);
});
it('合法的 UNC 路径', () => {
expect(is_valid_path_format('\\\\server\\share')).toBe(true);
expect(is_valid_path_format('//server/share')).toBe(true);
});
it('环境变量路径', () => {
expect(is_valid_path_format('%JAVA_HOME%\\bin')).toBe(true);
expect(is_valid_path_format('%APPDATA%')).toBe(true);
});
it('包含分隔符的相对路径', () => {
expect(is_valid_path_format('bin/debug')).toBe(true);
expect(is_valid_path_format('tools\\utils')).toBe(true);
});
it('空字符串非法', () => {
expect(is_valid_path_format('')).toBe(false);
expect(is_valid_path_format(' ')).toBe(false);
});
it('纯名称不包含分隔符也非法', () => {
expect(is_valid_path_format('notepad.exe')).toBe(false);
});
});
describe('join_path', () => {
it('用分号连接', () => {
expect(join_path(['C:\\', 'D:\\'])).toBe('C:\\;D:\\');
});
it('空数组返回空字符串', () => {
expect(join_path([])).toBe('');
});
});
describe('split_path', () => {
it('用分号分割', () => {
expect(split_path('C:\\;D:\\;E:\\')).toEqual(['C:\\', 'D:\\', 'E:\\']);
});
it('过滤空字符串', () => {
expect(split_path('C:\\;;D:\\')).toEqual(['C:\\', 'D:\\']);
});
it('去除首尾空格', () => {
expect(split_path(' C:\\ ; D:\\ ')).toEqual(['C:\\', 'D:\\']);
});
it('空字符串返回空数组', () => {
expect(split_path('')).toEqual([]);
});
});