Files
Just-DataInspector/Tools/OffscreenChecks/Src/Main.cpp
T
DeepSeek V4.1 Flash 35b67b216e test(tools): 默认数据/报告路径同时支持在仓库根目录运行
问题:`OffscreenChecks` 的默认 `-data` 只探测 `../TestData/...`,默认 `-report`
也固定写 `../Temp/tools/...`;在**仓库根目录**运行时会静默报
"no data file"(退出码 2),容易被误认为工具坏了(本轮实际踩到)。

改动:
* 默认数据文件候选同时包含 `../TestData/X` 与 `TestData/X`;
* 报告打不开时,若路径以 `../Temp/` 开头则去掉前缀重试一次;
* `Tools/README.md` 补充说明两种调用方式等价,显式 `-data/-out/-report`
  仍以当前工作目录为准。

验证:在仓库根目录执行 `Tools\OffscreenChecks\Build\OffscreenChecks.exe -all`
→ 6/6 检查 PASS,`checks=108 failed=0`;在 `Tools/` 下行为不变。
2026-11-04 14:39:27 +08:00

284 lines
7.9 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.
/**
* @file Main.cpp
* @brief 离屏验证工具入口(控制台)。
*
* 用法:
* OffscreenChecks -list
* OffscreenChecks -run smoke
* OffscreenChecks -all
* OffscreenChecks -run panels -mode record 记录面板像素基线到 Temp/tools
* OffscreenChecks -run panels -mode compare 与基线比对(重构前后 A/B)
* OffscreenChecks -run frames -frames 12 连续抓 12 帧
* OffscreenChecks -data "../TestData/BIG_DATA.csv" -all
*
* 可选:`-out <dir>`(默认 `../Temp/tools`)、`-report <path>`、`-no-png`。
*
* 退出码:0 = 全部通过;1 = 有失败断言;2 = 参数错误 / 数据加载失败。
*
* ⚠️ 窗口一律 `SW_HIDE`:不显示、不抢焦点、不移动真实鼠标。
* ⚠️ 临时产物(PNG / 基线 / 报告)统一写 `Temp/`(已被 .gitignore 忽略)。
*/
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include "CheckRunner.hpp"
#include "OffscreenSupport.hpp"
namespace
{
std::FILE* g_report = nullptr;
/** @brief 窄路径(本地代码页)→ 宽字符串。 */
std::wstring Widen(const std::string& text)
{
const int length = MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, nullptr, 0);
if (length <= 1)
{
return std::wstring();
}
std::wstring wide(static_cast<std::size_t>(length - 1), L'\0');
MultiByteToWideChar(CP_ACP, 0, text.c_str(), -1, &wide[0], length);
return wide;
}
/** @brief 判断文件是否存在。 */
bool FileExists(const std::string& path)
{
const std::wstring wide = Widen(path);
if (wide.empty())
{
return false;
}
HANDLE handle = CreateFileW(wide.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
return false;
}
CloseHandle(handle);
return true;
}
} // namespace
namespace jdi
{
namespace tools
{
void EmitLine(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)
{
return;
}
std::fputs(buffer, stdout);
std::fflush(stdout);
if (g_report != nullptr)
{
std::fputs(buffer, g_report);
std::fflush(g_report);
}
}
} // namespace tools
} // namespace jdi
int main(int argc, char** argv)
{
using namespace jdi::tools;
CheckOptions options;
options.outDir = "../Temp/tools";
options.mode = "verify";
std::string reportPath = "../Temp/tools/offscreen-report.txt";
std::string singleCheck;
bool runAll = false;
bool listOnly = false;
for (int index = 1; index < argc; ++index)
{
const char* argument = argv[index];
if (std::strcmp(argument, "-list") == 0)
{
listOnly = true;
}
else if (std::strcmp(argument, "-run") == 0 && index + 1 < argc)
{
singleCheck = argv[++index];
}
else if (std::strcmp(argument, "-all") == 0)
{
runAll = true;
}
else if (std::strcmp(argument, "-data") == 0 && index + 1 < argc)
{
options.dataPath = argv[++index];
}
else if (std::strcmp(argument, "-out") == 0 && index + 1 < argc)
{
options.outDir = argv[++index];
}
else if (std::strcmp(argument, "-report") == 0 && index + 1 < argc)
{
reportPath = argv[++index];
}
else if (std::strcmp(argument, "-mode") == 0 && index + 1 < argc)
{
options.mode = argv[++index];
}
else if (std::strcmp(argument, "-frames") == 0 && index + 1 < argc)
{
options.frames = std::atoi(argv[++index]);
}
else if (std::strcmp(argument, "-no-png") == 0)
{
options.savePng = false;
}
else
{
std::printf("unknown argument: %s\n", argument);
return 2;
}
}
if (listOnly)
{
for (const CheckDescriptor& descriptor : AvailableChecks())
{
std::printf("%-10s %s\n", descriptor.name, descriptor.summary);
}
return 0;
}
EnsureDirectory(options.outDir);
// 报告路径同样要能在两种工作目录下工作(见上面 `-data` 的说明)。
g_report = std::fopen(reportPath.c_str(), "wb");
if (g_report == nullptr && reportPath.rfind("../Temp/", 0) == 0)
{
reportPath = reportPath.substr(3); // 去掉 "../"
g_report = std::fopen(reportPath.c_str(), "wb");
}
EmitLine("==============================================================\n");
EmitLine(" Just DataInspector - offscreen checks\n");
EmitLine(" mode=%s out=%s report=%s\n", options.mode.c_str(), options.outDir.c_str(), reportPath.c_str());
EmitLine("==============================================================\n");
// 数据文件:命令行优先,否则在 TestData 里挑一个可用的样本。
// ⚠️ 默认路径同时支持「在 Tools/ 下运行」与「在仓库根目录运行」两种习惯:
// 前者要 `../TestData/...`,后者要 `TestData/...`。只认一种会让另一种
// 静默报 "no data file"(本轮就踩过,容易被误认为工具坏了)。
if (options.dataPath.empty())
{
static const char* const candidates[] = {"../TestData/SCATTER_TEST.csv", "TestData/SCATTER_TEST.csv",
"../TestData/[1].CSV", "TestData/[1].CSV",
"../TestData/BIG_DATA.csv", "TestData/BIG_DATA.csv",
"../TestData/Problem_Data.CSV",
"TestData/Problem_Data.CSV"};
for (const char* const candidate : candidates)
{
if (FileExists(candidate))
{
options.dataPath = candidate;
break;
}
}
}
if (options.dataPath.empty() || !FileExists(options.dataPath))
{
EmitLine("[FAIL] no data file: pass -data <path>(TestData/ 不入库,需本地准备)\n");
if (g_report != nullptr)
{
std::fclose(g_report);
}
return 2;
}
EmitLine(" data=%s\n", options.dataPath.c_str());
Session session;
if (!session.Start())
{
EmitLine("[FAIL] session start (hidden window creation)\n");
if (g_report != nullptr)
{
std::fclose(g_report);
}
return 2;
}
if (!session.Load(options.dataPath))
{
EmitLine("[FAIL] load data file\n");
session.Stop();
if (g_report != nullptr)
{
std::fclose(g_report);
}
return 2;
}
EmitLine(" loaded: groups=%d lanes=%d\n", static_cast<int>(session.App().m_dataSet.GroupCount()),
session.LaneCount());
std::vector<const CheckDescriptor*> selected;
if (!singleCheck.empty())
{
const CheckDescriptor* descriptor = FindCheck(singleCheck);
if (descriptor == nullptr)
{
EmitLine("[FAIL] unknown check: %s (use -list)\n", singleCheck.c_str());
session.Stop();
if (g_report != nullptr)
{
std::fclose(g_report);
}
return 2;
}
selected.push_back(descriptor);
}
else
{
for (const CheckDescriptor& descriptor : AvailableChecks())
{
selected.push_back(&descriptor);
}
(void)runAll;
}
int executed = 0;
for (const CheckDescriptor* descriptor : selected)
{
const int failedBefore = CheckFailed();
EmitLine("--------------------------------------------------------------\n");
EmitLine("[CHECK] %s - %s\n", descriptor->name, descriptor->summary);
descriptor->run(session, options);
++executed;
const int failed = CheckFailed() - failedBefore;
EmitLine("[CHECK] %s => %s\n", descriptor->name, (failed == 0) ? "PASS" : "FAIL");
}
EmitLine("--------------------------------------------------------------\n");
EmitLine("SUMMARY checks=%d failed=%d groups=%d result=%s\n", CheckTotal(), CheckFailed(), executed,
(CheckFailed() == 0) ? "PASS" : "FAIL");
EmitLine("--------------------------------------------------------------\n");
session.Stop();
if (g_report != nullptr)
{
std::fclose(g_report);
g_report = nullptr;
}
// ⚠️ 用 `_Exit` 收尾:`Application` / `PlotView` 是进程级单例,析构发生在 `GdiplusShutdown`
// 之后会访问违例(历史踩过,退出码 0xC0000005)。
const int code = (CheckFailed() == 0) ? 0 : 1;
std::_Exit(code);
}