// Copyright Epic Games, Inc. All Rights Reserved. #define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING #if ZEN_WITH_TESTS # include "zen-test.h" # include # include # include # include # include # include # include # include # include # include # include # include # include # include # if ZEN_PLATFORM_WINDOWS # include # else # include # include # endif # include ////////////////////////////////////////////////////////////////////////// using namespace std::literals; ////////////////////////////////////////////////////////////////////////// namespace zen::tests { zen::ZenServerEnvironment TestEnv; static std::filesystem::path s_ProgramBaseDir; std::filesystem::path GetZenExecutablePath() { return s_ProgramBaseDir / "zen" ZEN_EXE_SUFFIX_LITERAL; } static std::string ReadAllFromPipe(StdoutPipeHandles& Pipe) { std::string Result; char Buffer[4096]; # if ZEN_PLATFORM_WINDOWS DWORD BytesRead = 0; while (::ReadFile(Pipe.ReadHandle, Buffer, sizeof(Buffer), &BytesRead, nullptr) && BytesRead > 0) { Result.append(Buffer, BytesRead); } # else ssize_t BytesRead = 0; while ((BytesRead = read(Pipe.ReadFd, Buffer, sizeof(Buffer))) > 0) { Result.append(Buffer, static_cast(BytesRead)); } # endif return Result; } ZenCommandResult RunZen(std::string_view Args) { StdoutPipeHandles Pipe; if (!CreateStdoutPipe(Pipe)) { return {.ExitCode = -1, .Output = "failed to create stdout pipe"}; } const std::filesystem::path ZenExe = GetZenExecutablePath(); const std::string CommandLine = fmt::format("zen {}", Args); CreateProcOptions Options; Options.StdoutPipe = &Pipe; // stderr shares the stdout pipe when StderrPipe is null ProcessHandle Process(CreateProc(ZenExe, CommandLine, Options)); // Drain output before Wait() so the child doesn't block on a full pipe buffer. Pipe.CloseWriteEnd(); std::string Output = ReadAllFromPipe(Pipe); Process.Wait(); return {.ExitCode = Process.GetExitCode(), .Output = std::move(Output)}; } } // namespace zen::tests int main(int argc, char** argv) { # if ZEN_PLATFORM_WINDOWS setlocale(LC_ALL, "en_us.UTF8"); # endif // ZEN_PLATFORM_WINDOWS using namespace std::literals; using namespace zen; zen::CommandLineConverter ArgConverter(argc, argv); # if ZEN_WITH_TRACE zen::TraceInit("zen-test"); TraceOptions TraceCommandlineOptions; if (GetTraceOptionsFromCommandline(TraceCommandlineOptions)) { TraceConfigure(TraceCommandlineOptions); } # endif // ZEN_WITH_TRACE zen::logging::InitializeLogging(); zen::logging::SetLogLevel(zen::logging::Debug); zen::logging::Registry::Instance().SetFormatter( std::make_unique("test", std::chrono::system_clock::now())); std::filesystem::path ProgramBaseDir = GetRunningExecutablePath().parent_path(); std::filesystem::path TestBaseDir = std::filesystem::current_path() / ".test"; zen::tests::s_ProgramBaseDir = ProgramBaseDir; // This is pretty janky because we're passing most of the options through to the test // framework, so we can't just use cxxopts (I think). This should ideally be cleaned up // somehow in the future std::string ServerClass; bool Verbose = false; bool KillStale = false; for (int i = 1; i < argc; ++i) { if (argv[i] == "--http"sv) { if ((i + 1) < argc) { ServerClass = argv[++i]; } } else if (std::string_view Arg(argv[i]); Arg.starts_with("--httpclient="sv)) { std::string_view Value = Arg.substr(13); zen::SetDefaultHttpClientBackend(Value); } else if (argv[i] == "--verbose"sv) { Verbose = true; } else if (argv[i] == "--kill-stale-processes"sv) { KillStale = true; } } // Since GHA runners can leave processes behind from a previous test run, we need // to be able to clean up any stale server processes before starting the tests to // avoid interference if (KillStale) { ZEN_INFO("Killing any stale processes from previous test runs..."); auto KillStaleProcesses = [](const std::filesystem::path& Executable) { ZEN_INFO(" Looking for stale '{}' processes...", Executable.filename()); for (;;) { ProcessHandle StaleProcess; std::error_code Ec = FindProcess(Executable, StaleProcess, /*IncludeSelf*/ false); if (Ec || !StaleProcess.IsValid()) { break; } ZEN_WARN("====> Found stale '{}' process (pid {}) from a previous test run - terminating it", Executable.filename(), StaleProcess.Pid()); StaleProcess.Terminate(0); } }; KillStaleProcesses(ProgramBaseDir / "zen" ZEN_EXE_SUFFIX_LITERAL); KillStaleProcesses(ProgramBaseDir / "zenserver" ZEN_EXE_SUFFIX_LITERAL); KillStaleProcesses(ProgramBaseDir / "zen-test" ZEN_EXE_SUFFIX_LITERAL); KillStaleProcesses(ProgramBaseDir / "zentest-appstub" ZEN_EXE_SUFFIX_LITERAL); } zen::tests::TestEnv.InitializeForTest(ProgramBaseDir, TestBaseDir, ServerClass); if (Verbose) { zen::tests::TestEnv.SetPassthroughOutput(true); } ZEN_INFO("Running tests...(base dir: '{}')", TestBaseDir); zen::testing::TestRunner Runner; Runner.ApplyCommandLine(argc, argv, "zen.*"); return Runner.Run(); } namespace zen::tests { TEST_SUITE_BEGIN("zen.zen-test"); TEST_CASE("scaffolding.executable_present") { const std::filesystem::path ZenExe = GetZenExecutablePath(); CHECK_MESSAGE(std::filesystem::exists(ZenExe), fmt::format("zen executable not found at '{}'", ZenExe.string())); } TEST_SUITE_END(); } // namespace zen::tests #else # include int main() { printf("tests are disabled (ZEN_WITH_TESTS=0). Tests are only available in debug builds\n"); } #endif