Files
XCEngine/tests/UI/Editor/smoke/xceditor_smoke_runner.cpp

308 lines
8.9 KiB
C++
Raw Normal View History

#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
namespace {
constexpr auto kLaunchTimeout = std::chrono::seconds(20);
constexpr auto kShutdownTimeout = std::chrono::seconds(25);
constexpr auto kPollInterval = std::chrono::milliseconds(100);
constexpr const char* kReadyTrace = "[app] shell runtime initialized:";
struct ProcessWindowSearch {
DWORD processId = 0;
HWND hwnd = nullptr;
bool requireVisible = false;
};
std::wstring QuoteCommandLine(const std::filesystem::path& executablePath) {
return L"\"" + executablePath.wstring() + L"\"";
}
void PrintFailure(std::string_view message) {
2026-04-27 01:33:25 +08:00
std::cerr << "[xceditor_smoke] " << message << '\n';
}
std::string ReadTextFile(const std::filesystem::path& path) {
std::ifstream stream(path, std::ios::in | std::ios::binary);
if (!stream.is_open()) {
return {};
}
std::ostringstream buffer = {};
buffer << stream.rdbuf();
return buffer.str();
}
bool FileContains(const std::filesystem::path& path, std::string_view needle) {
const std::string content = ReadTextFile(path);
return !content.empty() && content.find(needle) != std::string::npos;
}
BOOL CALLBACK FindProcessWindowProc(HWND hwnd, LPARAM lParam) {
auto& search = *reinterpret_cast<ProcessWindowSearch*>(lParam);
DWORD processId = 0;
GetWindowThreadProcessId(hwnd, &processId);
if (processId != search.processId) {
return TRUE;
}
if (search.requireVisible && !IsWindowVisible(hwnd)) {
return TRUE;
}
RECT clientRect = {};
if (!GetClientRect(hwnd, &clientRect)) {
return TRUE;
}
const LONG width = clientRect.right - clientRect.left;
const LONG height = clientRect.bottom - clientRect.top;
if (width <= 0 || height <= 0) {
return TRUE;
}
search.hwnd = hwnd;
return FALSE;
}
HWND FindProcessWindow(DWORD processId, bool requireVisible) {
ProcessWindowSearch search = {};
search.processId = processId;
search.requireVisible = requireVisible;
EnumWindows(&FindProcessWindowProc, reinterpret_cast<LPARAM>(&search));
return search.hwnd;
}
bool RemoveFileIfPresent(const std::filesystem::path& path) {
std::error_code errorCode = {};
if (!std::filesystem::exists(path, errorCode)) {
return true;
}
std::filesystem::remove(path, errorCode);
return !errorCode;
}
bool PrepareLogDirectory(
const std::filesystem::path& logDirectory,
const std::filesystem::path& runtimeLogPath,
const std::filesystem::path& crashLogPath) {
std::error_code errorCode = {};
std::filesystem::create_directories(logDirectory, errorCode);
if (errorCode) {
PrintFailure("failed to create log directory: " + logDirectory.string());
return false;
}
if (!RemoveFileIfPresent(runtimeLogPath)) {
PrintFailure("failed to clear runtime log: " + runtimeLogPath.string());
return false;
}
if (!RemoveFileIfPresent(crashLogPath)) {
PrintFailure("failed to clear crash log: " + crashLogPath.string());
return false;
}
return true;
}
bool LaunchEditorProcess(
const std::filesystem::path& executablePath,
PROCESS_INFORMATION& outProcessInfo) {
SetEnvironmentVariableW(L"XCUI_AUTO_CAPTURE_ON_STARTUP", L"0");
SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST", L"1");
SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_DURATION_SECONDS", L"12");
SetEnvironmentVariableW(L"XCUIEDITOR_SMOKE_TEST_FRAME_LIMIT", nullptr);
STARTUPINFOW startupInfo = {};
startupInfo.cb = sizeof(startupInfo);
std::wstring commandLine = QuoteCommandLine(executablePath);
std::vector<wchar_t> mutableCommandLine(commandLine.begin(), commandLine.end());
mutableCommandLine.push_back(L'\0');
ZeroMemory(&outProcessInfo, sizeof(outProcessInfo));
return CreateProcessW(
executablePath.c_str(),
mutableCommandLine.data(),
nullptr,
nullptr,
FALSE,
0,
nullptr,
nullptr,
&startupInfo,
&outProcessInfo) == TRUE;
}
bool WaitForEditorReady(
HANDLE processHandle,
const std::filesystem::path& runtimeLogPath) {
const auto deadline = std::chrono::steady_clock::now() + kLaunchTimeout;
while (std::chrono::steady_clock::now() < deadline) {
if (WaitForSingleObject(processHandle, 0) == WAIT_OBJECT_0) {
return false;
}
if (FileContains(runtimeLogPath, kReadyTrace)) {
return true;
}
Sleep(static_cast<DWORD>(kPollInterval.count()));
}
return false;
}
bool WaitForEditorExit(HANDLE processHandle, DWORD timeoutMilliseconds, DWORD& outExitCode) {
if (WaitForSingleObject(processHandle, timeoutMilliseconds) != WAIT_OBJECT_0) {
return false;
}
outExitCode = 0;
return GetExitCodeProcess(processHandle, &outExitCode) == TRUE;
}
std::string BuildEarlyExitReason(
HANDLE processHandle,
const std::filesystem::path& runtimeLogPath,
const std::filesystem::path& crashLogPath) {
DWORD exitCode = 0;
GetExitCodeProcess(processHandle, &exitCode);
std::ostringstream message = {};
message << "editor exited before ready, code=" << exitCode;
const std::string runtimeLog = ReadTextFile(runtimeLogPath);
if (!runtimeLog.empty()) {
message << "\nruntime.log:\n" << runtimeLog;
}
const std::string crashLog = ReadTextFile(crashLogPath);
if (!crashLog.empty()) {
message << "\ncrash.log:\n" << crashLog;
}
return message.str();
}
std::string BuildTimeoutReason(
const std::filesystem::path& runtimeLogPath,
const std::filesystem::path& crashLogPath) {
std::ostringstream message = {};
message << "editor did not reach ready state within "
<< std::chrono::duration_cast<std::chrono::milliseconds>(kLaunchTimeout).count()
<< "ms";
const std::string runtimeLog = ReadTextFile(runtimeLogPath);
if (!runtimeLog.empty()) {
message << "\nruntime.log:\n" << runtimeLog;
}
const std::string crashLog = ReadTextFile(crashLogPath);
if (!crashLog.empty()) {
message << "\ncrash.log:\n" << crashLog;
}
return message.str();
}
void CloseProcessHandles(PROCESS_INFORMATION& processInfo) {
if (processInfo.hThread != nullptr) {
CloseHandle(processInfo.hThread);
processInfo.hThread = nullptr;
}
if (processInfo.hProcess != nullptr) {
CloseHandle(processInfo.hProcess);
processInfo.hProcess = nullptr;
}
}
} // namespace
int wmain(int argc, wchar_t* argv[]) {
if (argc < 2 || argv[1] == nullptr || argv[1][0] == L'\0') {
2026-04-27 01:33:25 +08:00
PrintFailure("missing XCEditor path");
return 1;
}
const std::filesystem::path executablePath = std::filesystem::path(argv[1]).lexically_normal();
if (!std::filesystem::exists(executablePath)) {
2026-04-27 01:33:25 +08:00
PrintFailure("XCEditor not found: " + executablePath.string());
return 1;
}
const std::filesystem::path logDirectory =
(executablePath.parent_path() / "logs").lexically_normal();
const std::filesystem::path runtimeLogPath =
(logDirectory / "runtime.log").lexically_normal();
const std::filesystem::path crashLogPath =
(logDirectory / "crash.log").lexically_normal();
if (!PrepareLogDirectory(logDirectory, runtimeLogPath, crashLogPath)) {
return 1;
}
PROCESS_INFORMATION processInfo = {};
if (!LaunchEditorProcess(executablePath, processInfo)) {
2026-04-27 01:33:25 +08:00
PrintFailure("failed to launch XCEditor");
return 1;
}
if (!WaitForEditorReady(
processInfo.hProcess,
runtimeLogPath)) {
const DWORD waitState = WaitForSingleObject(processInfo.hProcess, 0);
const std::string message =
waitState == WAIT_OBJECT_0
? BuildEarlyExitReason(processInfo.hProcess, runtimeLogPath, crashLogPath)
: BuildTimeoutReason(runtimeLogPath, crashLogPath);
PrintFailure(message);
TerminateProcess(processInfo.hProcess, 2);
CloseProcessHandles(processInfo);
return 1;
}
DWORD exitCode = 0;
if (!WaitForEditorExit(
processInfo.hProcess,
static_cast<DWORD>(
std::chrono::duration_cast<std::chrono::milliseconds>(kShutdownTimeout).count()),
exitCode)) {
PrintFailure("editor did not exit after WM_CLOSE");
TerminateProcess(processInfo.hProcess, 4);
CloseProcessHandles(processInfo);
return 1;
}
CloseProcessHandles(processInfo);
if (exitCode != 0) {
PrintFailure("editor exited with non-zero code: " + std::to_string(exitCode));
return 1;
}
const std::string crashLog = ReadTextFile(crashLogPath);
if (!crashLog.empty()) {
PrintFailure("crash log was generated:\n" + crashLog);
return 1;
}
return 0;
}