问题:`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/` 下行为不变。
284 lines
7.9 KiB
C++
284 lines
7.9 KiB
C++
/**
|
||
* @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);
|
||
}
|