/** * @file Main.cpp * @brief 单元测试执行器(控制台程序,无窗口、无 GUI 初始化)。 * * 命令行(全部可选): * -list 只列出用例名,不执行 * -only 只执行名字包含 的用例 * -report 报告文件路径(默认 `../Temp/tools/UnitTests-report.txt`) * -verbose 打印每个用例的开始/结束 * * 退出码:0 = 全部通过;1 = 有失败;2 = 参数错误 / 没有任何用例。 * * ⚠️ 本工具**不创建窗口、不初始化 GUI、不读用户输入**,可在后台任意次运行。 * ⚠️ 报告与截图类临时产物一律写到 `Temp/`(已被 .gitignore 忽略),不要写进源码目录。 */ #include #include #include #include #include #include "UnitTestFramework.hpp" namespace { std::FILE* g_report = nullptr; bool g_verbose = false; /** @brief 同时输出到标准输出与报告文件。 */ void Emit(const char* format, ...) { char buffer[4096]; va_list arguments; va_start(arguments, format); const int length = std::vsnprintf(buffer, sizeof(buffer), format, arguments); va_end(arguments); if (length > 0) { std::fputs(buffer, stdout); std::fflush(stdout); if (g_report != nullptr) { std::fputs(buffer, g_report); } } } /** @brief 递归创建目录(仅支持 ASCII 路径;失败静默忽略)。 */ void EnsureDirectory(const std::wstring& path) { if (path.empty()) { return; } for (std::size_t index = 1; index <= path.size(); ++index) { if (index == path.size() || path[index] == L'/' || path[index] == L'\\') { const std::wstring prefix = path.substr(0, index); CreateDirectoryW(prefix.c_str(), nullptr); } } } /** @brief 从 UTF-8 窄字符串转换为宽字符串(只用于路径)。 */ std::wstring Widen(const char* text) { const int length = MultiByteToWideChar(CP_ACP, 0, text, -1, nullptr, 0); if (length <= 1) { return std::wstring(); } std::wstring result(static_cast(length - 1), L'\0'); MultiByteToWideChar(CP_ACP, 0, text, -1, &result[0], length); return result; } /** @brief 打开报告文件(顺带创建 `Temp/tools`)。 */ std::FILE* OpenReport(const char* path) { std::string wide = path; // 仅 ASCII,用于切分目录 const std::size_t slash = wide.find_last_of("/\\"); if (slash != std::string::npos) { EnsureDirectory(Widen(wide.substr(0, slash).c_str())); } return std::fopen(path, "wb"); } } // namespace int main(int argc, char** argv) { const char* only = nullptr; const char* reportPath = "../Temp/tools/UnitTests-report.txt"; for (int index = 1; index < argc; ++index) { const char* argument = argv[index]; if (std::strcmp(argument, "-list") == 0) { for (const jdi::test::TestCase& testCase : jdi::test::Registry()) { std::printf("%s\n", testCase.name); } return 0; } if (std::strcmp(argument, "-only") == 0 && index + 1 < argc) { only = argv[++index]; continue; } if (std::strcmp(argument, "-report") == 0 && index + 1 < argc) { reportPath = argv[++index]; continue; } if (std::strcmp(argument, "-verbose") == 0) { g_verbose = true; continue; } std::printf("unknown argument: %s\n", argument); return 2; } g_report = OpenReport(reportPath); Emit("==============================================================\n"); Emit(" Just DataInspector - unit tests\n"); Emit(" report: %s\n", reportPath); Emit("==============================================================\n"); int executed = 0; for (const jdi::test::TestCase& testCase : jdi::test::Registry()) { if (only != nullptr && std::strstr(testCase.name, only) == nullptr) { continue; } ++executed; const int failedBefore = jdi::test::FailedChecks(); jdi::test::CaseFailed() = false; Emit("[RUN ] %s\n", testCase.name); testCase.function(); const int failed = jdi::test::FailedChecks() - failedBefore; if (failed == 0) { if (g_verbose) { Emit("[ OK ] %s\n", testCase.name); } } } Emit("--------------------------------------------------------------\n"); Emit("SUMMARY checks=%d failed=%d cases=%d result=%s\n", jdi::test::TotalChecks(), jdi::test::FailedChecks(), executed, (jdi::test::FailedChecks() == 0 && executed > 0) ? "PASS" : "FAIL"); Emit("--------------------------------------------------------------\n"); if (g_report != nullptr) { std::fclose(g_report); g_report = nullptr; } if (executed == 0) { return 2; } return (jdi::test::FailedChecks() == 0) ? 0 : 1; }