Files
SimpleRemoter/test/IniParser_test.cpp
2026-01-29 19:35:40 +01:00

560 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// IniParser_test.cpp - CIniParser 单元测试
// 编译: cl /EHsc /W4 IniParser_test.cpp /Fe:IniParser_test.exe
// 运行: IniParser_test.exe
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include "../common/IniParser.h"
static int g_total = 0;
static int g_passed = 0;
static int g_failed = 0;
#define TEST_ASSERT(expr, msg) do { \
g_total++; \
if (expr) { g_passed++; } \
else { g_failed++; printf(" FAIL: %s\n %s:%d\n", msg, __FILE__, __LINE__); } \
} while(0)
#define TEST_STR_EQ(actual, expected, msg) do { \
g_total++; \
if (std::string(actual) == std::string(expected)) { g_passed++; } \
else { g_failed++; printf(" FAIL: %s\n expected: \"%s\"\n actual: \"%s\"\n %s:%d\n", \
msg, expected, actual, __FILE__, __LINE__); } \
} while(0)
// 辅助:写入临时文件
static std::string WriteTempFile(const char* name, const char* content)
{
std::string path = std::string("_test_") + name + ".ini";
FILE* f = nullptr;
#ifdef _MSC_VER
fopen_s(&f, path.c_str(), "w");
#else
f = fopen(path.c_str(), "w");
#endif
if (f) {
fputs(content, f);
fclose(f);
}
return path;
}
static void CleanupFile(const std::string& path)
{
remove(path.c_str());
}
// ============================================
// Test 1: 基本 key=value 解析
// ============================================
void Test_BasicKeyValue()
{
printf("[Test 1] Basic key=value parsing\n");
std::string path = WriteTempFile("basic",
"[Strings]\n"
"hello=world\n"
"foo=bar\n"
);
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed");
TEST_STR_EQ(ini.GetValue("Strings", "hello"), "world", "hello -> world");
TEST_STR_EQ(ini.GetValue("Strings", "foo"), "bar", "foo -> bar");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Section size should be 2");
CleanupFile(path);
}
// ============================================
// Test 2: key 尾部空格保留(核心特性)
// ============================================
void Test_KeyTrailingSpace()
{
printf("[Test 2] Key trailing space preserved\n");
// 模拟: "请输入主机备注: =Enter host note:"
// key 是 "请输入主机备注: "(冒号+空格),不能被 trim
std::string path = WriteTempFile("trailing_space",
"[Strings]\n"
"key_no_space=value1\n"
"key_with_space =value2\n"
"key_with_2spaces =value3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key_no_space"), "value1",
"key without trailing space");
TEST_STR_EQ(ini.GetValue("Strings", "key_with_space "), "value2",
"key with 1 trailing space (must preserve)");
TEST_STR_EQ(ini.GetValue("Strings", "key_with_2spaces "), "value3",
"key with 2 trailing spaces (must preserve)");
// 不带空格的查找应该找不到
TEST_STR_EQ(ini.GetValue("Strings", "key_with_space", "NOT_FOUND"), "NOT_FOUND",
"key without trailing space should NOT match");
CleanupFile(path);
}
// ============================================
// Test 3: value 中含特殊字符
// ============================================
void Test_SpecialCharsInValue()
{
printf("[Test 3] Special characters in value\n");
std::string path = WriteTempFile("special_chars",
"[Strings]\n"
"menu=Menu(&F)\n"
"addr=<IP:PORT>\n"
"fmt=%s connected %d times\n"
"paren=(auto-restore on expiry)\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "menu"), "Menu(&F)", "value with (&F)");
TEST_STR_EQ(ini.GetValue("Strings", "addr"), "<IP:PORT>", "value with <IP:PORT>");
TEST_STR_EQ(ini.GetValue("Strings", "fmt"), "%s connected %d times", "value with %s %d");
TEST_STR_EQ(ini.GetValue("Strings", "paren"), "(auto-restore on expiry)", "value with parens");
CleanupFile(path);
}
// ============================================
// Test 4: 注释行跳过
// ============================================
void Test_Comments()
{
printf("[Test 4] Comment lines skipped\n");
std::string path = WriteTempFile("comments",
"; This is a comment\n"
"# This is also a comment\n"
"[Strings]\n"
"; ============================================\n"
"# Section header comment\n"
"key1=value1\n"
"; key2=should_not_exist\n"
"key3=value3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "key1 exists");
TEST_STR_EQ(ini.GetValue("Strings", "key3"), "value3", "key3 exists");
TEST_STR_EQ(ini.GetValue("Strings", "key2", "NOT_FOUND"), "NOT_FOUND",
"commented key2 should not exist");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Only 2 keys (comments excluded)");
CleanupFile(path);
}
// ============================================
// Test 5: 空行跳过
// ============================================
void Test_EmptyLines()
{
printf("[Test 5] Empty lines skipped\n");
std::string path = WriteTempFile("empty_lines",
"\n"
"\n"
"[Strings]\n"
"\n"
"key1=value1\n"
"\n"
"\n"
"key2=value2\n"
"\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "2 keys despite empty lines");
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "key1");
TEST_STR_EQ(ini.GetValue("Strings", "key2"), "value2", "key2");
CleanupFile(path);
}
// ============================================
// Test 6: section 切换
// ============================================
void Test_MultipleSections()
{
printf("[Test 6] Multiple sections\n");
std::string path = WriteTempFile("sections",
"[Strings]\n"
"key1=value1\n"
"key2=value2\n"
"[Other]\n"
"key1=other_value1\n"
"key3=other_value3\n"
"[Strings2]\n"
"keyA=valueA\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "key1"), "value1", "Strings.key1");
TEST_STR_EQ(ini.GetValue("Strings", "key2"), "value2", "Strings.key2");
TEST_STR_EQ(ini.GetValue("Other", "key1"), "other_value1", "Other.key1");
TEST_STR_EQ(ini.GetValue("Other", "key3"), "other_value3", "Other.key3");
TEST_STR_EQ(ini.GetValue("Strings2", "keyA"), "valueA", "Strings2.keyA");
// Strings section should not contain Other section's keys
TEST_STR_EQ(ini.GetValue("Strings", "key3", "NOT_FOUND"), "NOT_FOUND",
"Strings should not have Other's key3");
TEST_ASSERT(ini.GetSectionSize("Strings") == 2, "Strings has 2 keys");
TEST_ASSERT(ini.GetSectionSize("Other") == 2, "Other has 2 keys");
TEST_ASSERT(ini.GetSectionSize("Strings2") == 1, "Strings2 has 1 key");
CleanupFile(path);
}
// ============================================
// Test 7: 大文件(超过 32KB
// ============================================
void Test_LargeFile()
{
printf("[Test 7] Large file (>32KB)\n");
std::string path = std::string("_test_large.ini");
FILE* f = nullptr;
#ifdef _MSC_VER
fopen_s(&f, path.c_str(), "w");
#else
f = fopen(path.c_str(), "w");
#endif
if (!f) {
printf(" SKIP: Cannot create temp file\n");
return;
}
fputs("[Strings]\n", f);
// 写入大量条目使文件超过 32KB
const int entryCount = 2000;
for (int i = 0; i < entryCount; i++) {
fprintf(f, "key_%04d=value_for_entry_number_%04d_padding_text_here\n", i, i);
}
// 在文件末尾写一个特殊条目
fputs("last_key=last_value\n", f);
fclose(f);
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed for large file");
// 验证首尾和中间的条目
TEST_STR_EQ(ini.GetValue("Strings", "key_0000"),
"value_for_entry_number_0000_padding_text_here",
"First entry");
TEST_STR_EQ(ini.GetValue("Strings", "key_0999"),
"value_for_entry_number_0999_padding_text_here",
"Middle entry");
TEST_STR_EQ(ini.GetValue("Strings", "key_1999"),
"value_for_entry_number_1999_padding_text_here",
"Last numbered entry");
TEST_STR_EQ(ini.GetValue("Strings", "last_key"), "last_value",
"Entry at very end of large file");
size_t size = ini.GetSectionSize("Strings");
TEST_ASSERT(size == entryCount + 1,
"Section size should be entryCount + 1 (last_key)");
printf(" File has %d entries, all readable\n", (int)size);
CleanupFile(path);
}
// ============================================
// Test 8: 文件不存在
// ============================================
void Test_FileNotExist()
{
printf("[Test 8] File not exist\n");
CIniParser ini;
TEST_ASSERT(!ini.LoadFile("_nonexistent_file_12345.ini"), "LoadFile should return false");
TEST_ASSERT(!ini.LoadFile(nullptr), "LoadFile(nullptr) should return false");
TEST_ASSERT(!ini.LoadFile(""), "LoadFile('') should return false");
TEST_ASSERT(ini.GetSection("Strings") == nullptr, "No sections after failed load");
}
// ============================================
// Test 9: 空文件
// ============================================
void Test_EmptyFile()
{
printf("[Test 9] Empty file\n");
std::string path = WriteTempFile("empty", "");
CIniParser ini;
TEST_ASSERT(ini.LoadFile(path.c_str()), "LoadFile should succeed for empty file");
TEST_ASSERT(ini.GetSection("Strings") == nullptr, "No Strings section in empty file");
TEST_ASSERT(ini.GetSectionSize("Strings") == 0, "Section size is 0");
CleanupFile(path);
}
// ============================================
// Test 10: value 中含 '='(只按第一个 '=' 分割)
// ============================================
void Test_EqualsInValue()
{
printf("[Test 10] Equals sign in value\n");
std::string path = WriteTempFile("equals",
"[Strings]\n"
"formula=a=b+c\n"
"equation=x=1=2=3\n"
"normal=hello\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "formula"), "a=b+c",
"value with one '=' should keep it");
TEST_STR_EQ(ini.GetValue("Strings", "equation"), "x=1=2=3",
"value with multiple '=' should keep all");
TEST_STR_EQ(ini.GetValue("Strings", "normal"), "hello",
"normal value unaffected");
CleanupFile(path);
}
// ============================================
// Test 11: key 中含 \r\n 转义序列
// ============================================
void Test_EscapeCRLF_InKey()
{
printf("[Test 11] Escape \\r\\n in key\n");
// INI 文件中写字面量 \r\n解析器应转为真正的 0x0D 0x0A
// 模拟代码中: _TR("\n编译日期: ") 和 _TR("操作失败\r\n请重试")
std::string path = WriteTempFile("escape_key",
"[Strings]\n"
"\\n compile date: =\\n Build Date: \n"
"fail\\r\\nretry=Fail\\r\\nRetry\n"
"line1\\nline2\\nline3=L1\\nL2\\nL3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// key "\n compile date: " (真正的换行 + 文本)
TEST_STR_EQ(ini.GetValue("Strings", "\n compile date: "), "\n Build Date: ",
"key with \\n at start");
// key "fail\r\nretry" (真正的 CR+LF)
TEST_STR_EQ(ini.GetValue("Strings", "fail\r\nretry"), "Fail\r\nRetry",
"key with \\r\\n in middle");
// key 含多个 \n
TEST_STR_EQ(ini.GetValue("Strings", "line1\nline2\nline3"), "L1\nL2\nL3",
"key with multiple \\n");
CleanupFile(path);
}
// ============================================
// Test 12: value 中含 \r\n 转义序列
// ============================================
void Test_EscapeCRLF_InValue()
{
printf("[Test 12] Escape \\r\\n in value\n");
std::string path = WriteTempFile("escape_value",
"[Strings]\n"
"msg=hello\\r\\nworld\n"
"multiline=line1\\nline2\\nline3\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "msg"), "hello\r\nworld",
"value with \\r\\n");
TEST_STR_EQ(ini.GetValue("Strings", "multiline"), "line1\nline2\nline3",
"value with multiple \\n");
CleanupFile(path);
}
// ============================================
// Test 13: \\ 和 \" 转义
// ============================================
void Test_EscapeBackslashAndQuote()
{
printf("[Test 13] Escape \\\\ and \\\" sequences\n");
std::string path = WriteTempFile("escape_bsq",
"[Strings]\n"
"path=C:\\\\Users\\\\test\n"
"quoted=say \\\"hello\\\"\n"
"mixed=\\\"line1\\n line2\\\"\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "path"), "C:\\Users\\test",
"double backslash -> single backslash");
TEST_STR_EQ(ini.GetValue("Strings", "quoted"), "say \"hello\"",
"escaped quotes");
TEST_STR_EQ(ini.GetValue("Strings", "mixed"), "\"line1\n line2\"",
"mixed \\\" and \\n");
CleanupFile(path);
}
// ============================================
// Test 14: \t 转义
// ============================================
void Test_EscapeTab()
{
printf("[Test 14] Escape \\t sequence\n");
std::string path = WriteTempFile("escape_tab",
"[Strings]\n"
"col=name\\tvalue\n"
"header=ID\\tName\\tStatus\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
TEST_STR_EQ(ini.GetValue("Strings", "col"), "name\tvalue",
"\\t -> tab");
TEST_STR_EQ(ini.GetValue("Strings", "header"), "ID\tName\tStatus",
"multiple \\t");
CleanupFile(path);
}
// ============================================
// Test 15: 未知转义保留原样
// ============================================
void Test_UnknownEscapePassthrough()
{
printf("[Test 15] Unknown escape passthrough\n");
std::string path = WriteTempFile("escape_unknown",
"[Strings]\n"
"unknown=hello\\xworld\n"
"trailing_bs=end\\\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// \x 不是已知转义,应保留反斜杠
TEST_STR_EQ(ini.GetValue("Strings", "unknown"), "hello\\xworld",
"unknown \\x keeps backslash");
// 行尾的孤立反斜杠fgets 去掉换行后,最后一个字符是 \
TEST_STR_EQ(ini.GetValue("Strings", "trailing_bs"), "end\\",
"trailing backslash preserved");
CleanupFile(path);
}
// ============================================
// Test 16: key 中转义与尾部空格组合
// ============================================
void Test_EscapeWithTrailingSpace()
{
printf("[Test 16] Escape + trailing space in key\n");
// 模拟: _TR("\n编译日期: ") — key 以 \n 开头,以冒号+空格结尾
std::string path = WriteTempFile("escape_trail",
"[Strings]\n"
"\\n date: =\\n Date: \n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// key 是 "\n date: "(真正换行 + 文本 + 尾部空格)
TEST_STR_EQ(ini.GetValue("Strings", "\n date: "), "\n Date: ",
"escape \\n + trailing space in key");
// 不带尾部空格应找不到
TEST_STR_EQ(ini.GetValue("Strings", "\n date:", "NOT_FOUND"), "NOT_FOUND",
"without trailing space should not match");
CleanupFile(path);
}
// ============================================
// Test 17: key 以 '[' 开头(不是 section 头)
// ============================================
void Test_BracketKey()
{
printf("[Test 17] Key starting with '[' (not a section header)\n");
// 模拟: _TR("[使用FRP]") 和 _TR("[未使用FRP]")
std::string path = WriteTempFile("bracket_key",
"[Strings]\n"
"normal=value1\n"
"[tag1]=[Tag One]\n"
"[tag2]=[Tag Two]\n"
"after=value2\n"
);
CIniParser ini;
ini.LoadFile(path.c_str());
// [tag1]=[Tag One] 应该是 key=value不是 section 头
TEST_STR_EQ(ini.GetValue("Strings", "[tag1]"), "[Tag One]",
"[tag1] parsed as key, not section");
TEST_STR_EQ(ini.GetValue("Strings", "[tag2]"), "[Tag Two]",
"[tag2] parsed as key, not section");
// 前后的普通 key 应仍在 Strings section
TEST_STR_EQ(ini.GetValue("Strings", "normal"), "value1",
"normal key before bracket keys");
TEST_STR_EQ(ini.GetValue("Strings", "after"), "value2",
"normal key after bracket keys still in Strings");
TEST_ASSERT(ini.GetSectionSize("Strings") == 4, "Strings has 4 keys");
// 不应该有 tag1 或 tag2 section
TEST_ASSERT(ini.GetSection("tag1") == nullptr, "no tag1 section");
TEST_ASSERT(ini.GetSection("tag2") == nullptr, "no tag2 section");
CleanupFile(path);
}
// ============================================
// main
// ============================================
int main()
{
printf("=== CIniParser Tests ===\n\n");
Test_BasicKeyValue();
Test_KeyTrailingSpace();
Test_SpecialCharsInValue();
Test_Comments();
Test_EmptyLines();
Test_MultipleSections();
Test_LargeFile();
Test_FileNotExist();
Test_EmptyFile();
Test_EqualsInValue();
Test_EscapeCRLF_InKey();
Test_EscapeCRLF_InValue();
Test_EscapeBackslashAndQuote();
Test_EscapeTab();
Test_UnknownEscapePassthrough();
Test_EscapeWithTrailingSpace();
Test_BracketKey();
printf("\n=== Results: %d/%d passed", g_passed, g_total);
if (g_failed > 0)
printf(", %d FAILED", g_failed);
printf(" ===\n");
return g_failed > 0 ? 1 : 0;
}