aboutsummaryrefslogtreecommitdiff
path: root/src/zen-test/zen-test.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/zen-test/zen-test.cpp')
-rw-r--r--src/zen-test/zen-test.cpp238
1 files changed, 238 insertions, 0 deletions
diff --git a/src/zen-test/zen-test.cpp b/src/zen-test/zen-test.cpp
new file mode 100644
index 000000000..3ee401045
--- /dev/null
+++ b/src/zen-test/zen-test.cpp
@@ -0,0 +1,238 @@
+// Copyright Epic Games, Inc. All Rights Reserved.
+
+#define _SILENCE_CXX17_C_HEADER_DEPRECATION_WARNING
+
+#if ZEN_WITH_TESTS
+
+# include "zen-test.h"
+
+# include <zencore/except.h>
+# include <zencore/filesystem.h>
+# include <zencore/fmtutils.h>
+# include <zencore/logging.h>
+# include <zencore/logging/registry.h>
+# include <zencore/process.h>
+# include <zencore/string.h>
+# include <zencore/testutils.h>
+# include <zencore/trace.h>
+# include <zenhttp/httpclient.h>
+# include <zenutil/config/commandlineoptions.h>
+# include <zenutil/logging/fullformatter.h>
+# include <zenutil/zenserverprocess.h>
+
+# include <filesystem>
+
+# if ZEN_PLATFORM_WINDOWS
+# include <zencore/windows.h>
+# else
+# include <cerrno>
+# include <unistd.h>
+# endif
+
+# include <zencore/memory/newdelete.h>
+
+//////////////////////////////////////////////////////////////////////////
+
+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<size_t>(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<zen::logging::FullFormatter>("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 <stdio.h>
+
+int
+main()
+{
+ printf("tests are disabled (ZEN_WITH_TESTS=0). Tests are only available in debug builds\n");
+}
+#endif