diff options
Diffstat (limited to 'src/zen-test')
| -rw-r--r-- | src/zen-test/artifactprovider-tests.cpp | 66 | ||||
| -rw-r--r-- | src/zen-test/suggestion-tests.cpp | 32 | ||||
| -rw-r--r-- | src/zen-test/trace-tests.cpp | 107 | ||||
| -rw-r--r-- | src/zen-test/utility-cmd-tests.cpp | 179 | ||||
| -rw-r--r-- | src/zen-test/xmake.lua | 19 | ||||
| -rw-r--r-- | src/zen-test/zen-test.cpp | 238 | ||||
| -rw-r--r-- | src/zen-test/zen-test.h | 34 |
7 files changed, 675 insertions, 0 deletions
diff --git a/src/zen-test/artifactprovider-tests.cpp b/src/zen-test/artifactprovider-tests.cpp new file mode 100644 index 000000000..48bbf8222 --- /dev/null +++ b/src/zen-test/artifactprovider-tests.cpp @@ -0,0 +1,66 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/zencore.h> + +#if ZEN_WITH_TESTS + +# include "zen-test.h" + +# include <zencore/filesystem.h> +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zenutil/testartifactprovider.h> + +namespace zen::tests { + +namespace { + + // Returns "<unset>" when the variable is unset, the literal value otherwise. + std::string DescribeEnvVar(std::string_view Name) + { + const std::string Value = GetEnvVariable(Name); + return Value.empty() ? std::string("<unset>") : Value; + } + + // Same as DescribeEnvVar but redacts the value, since the variable may be a credential. + // Reports presence and length without revealing contents. + std::string DescribeSecretEnvVar(std::string_view Name) + { + const std::string Value = GetEnvVariable(Name); + return Value.empty() ? std::string("<unset>") : fmt::format("<set, {} chars>", Value.size()); + } + +} // namespace + +TEST_SUITE_BEGIN("zen.artifactprovider"); + +TEST_CASE("probe.s3_readme" * doctest::skip(!S3TestArtifactsAvailable())) +{ + // Use a fresh cache so Exists() is forced through to S3 rather than being satisfied + // by a README cached from a prior run. + ScopedTemporaryDirectory CacheDir; + TestArtifactProviderOptions Opts; + Opts.CacheDir = CacheDir.Path(); + Ref<TestArtifactProvider> Provider = CreateTestArtifactProvider(std::move(Opts)); + REQUIRE_MESSAGE(Provider, "no test artifact provider could be created on this platform"); + + constexpr std::string_view kArtifactPath = "README.md"; + const std::string Description = Provider->Describe(); + + ZEN_INFO("Provider: {}", Description); + ZEN_INFO("ZEN_TEST_ARTIFACTS_S3={}", DescribeEnvVar(kTestArtifactsS3EnvVar)); + ZEN_INFO("AWS_DEFAULT_REGION={}", DescribeEnvVar("AWS_DEFAULT_REGION")); + ZEN_INFO("AWS_REGION={}", DescribeEnvVar("AWS_REGION")); + ZEN_INFO("AWS_ENDPOINT_URL={}", DescribeEnvVar("AWS_ENDPOINT_URL")); + ZEN_INFO("AWS_ACCESS_KEY_ID={}", DescribeSecretEnvVar("AWS_ACCESS_KEY_ID")); + ZEN_INFO("AWS_SECRET_ACCESS_KEY={}", DescribeSecretEnvVar("AWS_SECRET_ACCESS_KEY")); + ZEN_INFO("AWS_SESSION_TOKEN={}", DescribeSecretEnvVar("AWS_SESSION_TOKEN")); + + CHECK_MESSAGE(Provider->Exists(kArtifactPath), "'" << kArtifactPath << "' not available via " << Description); +} + +TEST_SUITE_END(); + +} // namespace zen::tests + +#endif diff --git a/src/zen-test/suggestion-tests.cpp b/src/zen-test/suggestion-tests.cpp new file mode 100644 index 000000000..8607c7bd5 --- /dev/null +++ b/src/zen-test/suggestion-tests.cpp @@ -0,0 +1,32 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/zencore.h> + +#if ZEN_WITH_TESTS + +# include "zen-test.h" + +# include <zencore/testing.h> + +namespace zen::tests { + +TEST_SUITE_BEGIN("zen.suggestion"); + +// Unit tests for the suggestion algorithm live in zenutil (util.suggest). This +// integration test exists solely to verify the CLI wires the suggestion output +// into the "Unknown command" handler. +TEST_CASE("unknown_top_level_command_suggests_close_match") +{ + const ZenCommandResult Result = RunZen("stauts"); + + CHECK(Result.ExitCode != 0); + CHECK(Result.Output.find("Unknown command") != std::string::npos); + CHECK_MESSAGE(Result.Output.find("status") != std::string::npos, + fmt::format("expected 'status' suggestion in output, got:\n{}", Result.Output)); +} + +TEST_SUITE_END(); + +} // namespace zen::tests + +#endif diff --git a/src/zen-test/trace-tests.cpp b/src/zen-test/trace-tests.cpp new file mode 100644 index 000000000..e57d30e6c --- /dev/null +++ b/src/zen-test/trace-tests.cpp @@ -0,0 +1,107 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/zencore.h> + +#if ZEN_WITH_TESTS + +# include "zen-test.h" + +# include <zencore/filesystem.h> +# include <zencore/testing.h> + +# include <system_error> + +namespace zen::tests { + +using namespace std::literals; + +TEST_SUITE_BEGIN("zen.trace"); + +TEST_CASE("help.top_level") +{ + ZenCommandResult Result = RunZen("trace --help"); + CHECK_EQ(Result.ExitCode, 0); + CHECK_MESSAGE(Result.Output.find("subcommands:") != std::string::npos, Result.Output); + CHECK_MESSAGE(Result.Output.find("analyze") != std::string::npos, Result.Output); + CHECK_MESSAGE(Result.Output.find("inspect") != std::string::npos, Result.Output); + CHECK_MESSAGE(Result.Output.find("serve") != std::string::npos, Result.Output); +} + +TEST_CASE("help.inspect") +{ + ZenCommandResult Result = RunZen("trace inspect --help"); + CHECK_EQ(Result.ExitCode, 0); + CHECK_MESSAGE(Result.Output.find("Inspect event schemas") != std::string::npos, Result.Output); +} + +TEST_CASE("inspect.missing_file") +{ + ZenCommandResult Result = RunZen("trace inspect this-file-does-not-exist.utrace"); + CHECK_NE(Result.ExitCode, 0); + CHECK_MESSAGE(Result.Output.find("File not found") != std::string::npos, Result.Output); +} + +TEST_CASE("inspect.generated_tracefile") +{ + const std::filesystem::path DataDir = TestEnv.CreateNewTestDir(); + const std::filesystem::path TraceFile = DataDir / "powercycle-trace.utrace"; + + ZenServerInstance Instance(TestEnv); + Instance.SetDataDir(DataDir); + + ExtendableStringBuilder<512> ServerArgs; + ServerArgs << "--tracefile=\""; + PathToUtf8(TraceFile.c_str(), ServerArgs); + ServerArgs << "\" --powercycle"; + + Instance.SpawnServer(TestEnv.GetNewPortNumber(), ServerArgs.ToView()); + CHECK_MESSAGE(Instance.WaitUntilReady(10000), Instance.GetLogOutput()); + + if (Instance.IsRunning()) + { + std::error_code Ec; + CHECK_MESSAGE(Instance.WaitUntilExited(10000, Ec), Instance.GetLogOutput()); + CHECK_MESSAGE(!Ec, Ec.message()); + } + + CHECK_EQ(Instance.Shutdown(), 0); + CHECK_MESSAGE(std::filesystem::exists(TraceFile), TraceFile.string()); + CHECK_MESSAGE(std::filesystem::file_size(TraceFile) > 0, TraceFile.string()); + + ExtendableStringBuilder<512> InspectArgs; + InspectArgs << "trace inspect \""; + PathToUtf8(TraceFile.c_str(), InspectArgs); + InspectArgs << "\""; + + ZenCommandResult InspectResult = RunZen(InspectArgs.ToView()); + CHECK_EQ(InspectResult.ExitCode, 0); + CHECK_MESSAGE(InspectResult.Output.find("Trace:") != std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("Events:") != std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("Types:") != std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("Event Schemas:") != std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("Events: 0") == std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("Types: 0") == std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("(uid=") != std::string::npos, InspectResult.Output); + CHECK_MESSAGE(InspectResult.Output.find("events=") != std::string::npos, InspectResult.Output); + + ExtendableStringBuilder<512> AnalyzeArgs; + AnalyzeArgs << "trace analyze --no-cache --symbols=off \""; + PathToUtf8(TraceFile.c_str(), AnalyzeArgs); + AnalyzeArgs << "\""; + + ZenCommandResult AnalyzeResult = RunZen(AnalyzeArgs.ToView()); + CHECK_EQ(AnalyzeResult.ExitCode, 0); + CHECK_MESSAGE(AnalyzeResult.Output.find("Trace:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Size:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Events:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Duration:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Threads:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Modules:") != std::string::npos, AnalyzeResult.Output); + CHECK_MESSAGE(AnalyzeResult.Output.find("Parsed:") != std::string::npos, AnalyzeResult.Output); +} + +TEST_SUITE_END(); + +} // namespace zen::tests + +#endif diff --git a/src/zen-test/utility-cmd-tests.cpp b/src/zen-test/utility-cmd-tests.cpp new file mode 100644 index 000000000..aa49d7299 --- /dev/null +++ b/src/zen-test/utility-cmd-tests.cpp @@ -0,0 +1,179 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include <zencore/zencore.h> + +#if ZEN_WITH_TESTS + +# include "zen-test.h" + +# include <zencore/compactbinarybuilder.h> +# include <zencore/filesystem.h> +# include <zencore/iobuffer.h> +# include <zencore/testing.h> +# include <zencore/testutils.h> + +namespace zen::tests { + +TEST_SUITE_BEGIN("zen.utility-cmd"); + +TEST_CASE("print.cbo_to_json") +{ + ScopedTemporaryDirectory TempDir; + + // Build a small compact binary object covering a few primitive field types. + CbObjectWriter Writer; + Writer << "Name" + << "ZenPrintTest"; + Writer << "Count" << int32_t(42); + Writer << "Enabled" << true; + + IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); + REQUIRE(Payload.GetSize() > 0); + + const std::filesystem::path CboPath = TempDir.Path() / "object.cbo"; + WriteFile(CboPath, Payload); + + const ZenCommandResult Result = RunZen(fmt::format(R"(print --source "{}")", CboPath.string())); + + REQUIRE_MESSAGE(Result.ExitCode == 0, fmt::format("zen print exited with code {}", Result.ExitCode)); + + // CompactBinaryToJson emits a JSON object; field names are quoted and values appear verbatim. + CHECK(Result.Output.find("\"Name\"") != std::string::npos); + CHECK(Result.Output.find("\"ZenPrintTest\"") != std::string::npos); + CHECK(Result.Output.find("\"Count\"") != std::string::npos); + CHECK(Result.Output.find("42") != std::string::npos); + CHECK(Result.Output.find("\"Enabled\"") != std::string::npos); + CHECK(Result.Output.find("true") != std::string::npos); +} + +TEST_CASE("print.show_type_info_annotates_output") +{ + ScopedTemporaryDirectory TempDir; + + CbObjectWriter Writer; + Writer << "Count" << int32_t(7); + + IoBuffer Payload = Writer.Save().GetBuffer().AsIoBuffer(); + const std::filesystem::path CboPath = TempDir.Path() / "object.cbo"; + WriteFile(CboPath, Payload); + + const ZenCommandResult Plain = RunZen(fmt::format(R"(print --source "{}")", CboPath.string())); + const ZenCommandResult Typed = RunZen(fmt::format(R"(print --show-type-info --source "{}")", CboPath.string())); + + REQUIRE(Plain.ExitCode == 0); + REQUIRE(Typed.ExitCode == 0); + + // --show-type-info prefixes values with type annotations like [IntegerPositive] that the plain variant omits. + CHECK(Typed.Output.size() > Plain.Output.size()); + CHECK(Typed.Output.find("[IntegerPositive]") != std::string::npos); + CHECK(Plain.Output.find("[IntegerPositive]") == std::string::npos); +} + +TEST_CASE("print.missing_source_fails") +{ + const ZenCommandResult Result = RunZen(R"(print)"); + + CHECK(Result.ExitCode != 0); +} + +TEST_CASE("print.non_cbo_file_fails") +{ + ScopedTemporaryDirectory TempDir; + + const std::filesystem::path GarbagePath = TempDir.Path() / "garbage.cbo"; + const std::string_view Garbage = "this is not compact binary data"; + WriteFile(GarbagePath, IoBuffer(IoBuffer::Wrap, Garbage.data(), Garbage.size())); + + const ZenCommandResult Result = RunZen(fmt::format(R"(print --source "{}")", GarbagePath.string())); + + CHECK(Result.ExitCode != 0); +} + +//////////////////////////////////////////////////////////////////////////////// +// wipe + +namespace { + + // Drop a small fixture tree of files and nested directories inside Root so wipe has + // something non-trivial to recurse over. + void PopulateWipeFixture(const std::filesystem::path& Root) + { + std::filesystem::create_directories(Root / "nested" / "deep"); + const std::string_view Contents = "hello"; + const IoBuffer Payload(IoBuffer::Wrap, Contents.data(), Contents.size()); + WriteFile(Root / "top.txt", Payload); + WriteFile(Root / "nested" / "mid.txt", Payload); + WriteFile(Root / "nested" / "deep" / "leaf.txt", Payload); + } + + bool DirectoryIsEmpty(const std::filesystem::path& Path) + { + std::error_code Ec; + return std::filesystem::is_empty(Path, Ec) && !Ec; + } + +} // namespace + +TEST_CASE("wipe.removes_directory_contents") +{ + ScopedTemporaryDirectory TempDir; + const std::filesystem::path Target = TempDir.Path() / "to-wipe"; + PopulateWipeFixture(Target); + + REQUIRE(std::filesystem::exists(Target / "top.txt")); + REQUIRE(std::filesystem::exists(Target / "nested" / "deep" / "leaf.txt")); + + const ZenCommandResult Result = RunZen(fmt::format(R"(wipe --yes --quiet --directory "{}")", Target.string())); + + REQUIRE_MESSAGE(Result.ExitCode == 0, fmt::format("zen wipe exited with code {}", Result.ExitCode)); + + CHECK_FALSE(std::filesystem::exists(Target / "top.txt")); + CHECK_FALSE(std::filesystem::exists(Target / "nested")); +} + +TEST_CASE("wipe.dryrun_keeps_files") +{ + ScopedTemporaryDirectory TempDir; + const std::filesystem::path Target = TempDir.Path() / "to-wipe"; + PopulateWipeFixture(Target); + + const ZenCommandResult Result = RunZen(fmt::format(R"(wipe --yes --quiet --dryrun --directory "{}")", Target.string())); + + REQUIRE_MESSAGE(Result.ExitCode == 0, fmt::format("zen wipe --dryrun exited with code {}", Result.ExitCode)); + + // --dryrun must leave the tree untouched. + CHECK(std::filesystem::exists(Target / "top.txt")); + CHECK(std::filesystem::exists(Target / "nested" / "mid.txt")); + CHECK(std::filesystem::exists(Target / "nested" / "deep" / "leaf.txt")); +} + +TEST_CASE("wipe.nonexistent_directory_is_noop") +{ + ScopedTemporaryDirectory TempDir; + const std::filesystem::path Missing = TempDir.Path() / "does-not-exist"; + REQUIRE_FALSE(std::filesystem::exists(Missing)); + + const ZenCommandResult Result = RunZen(fmt::format(R"(wipe --yes --quiet --directory "{}")", Missing.string())); + + // A missing directory should be silently skipped without failing. + CHECK(Result.ExitCode == 0); + CHECK_FALSE(std::filesystem::exists(Missing)); +} + +TEST_CASE("wipe.empty_directory") +{ + ScopedTemporaryDirectory TempDir; + const std::filesystem::path Target = TempDir.Path() / "empty"; + std::filesystem::create_directories(Target); + REQUIRE(DirectoryIsEmpty(Target)); + + const ZenCommandResult Result = RunZen(fmt::format(R"(wipe --yes --quiet --directory "{}")", Target.string())); + + CHECK(Result.ExitCode == 0); +} + +TEST_SUITE_END(); + +} // namespace zen::tests + +#endif diff --git a/src/zen-test/xmake.lua b/src/zen-test/xmake.lua new file mode 100644 index 000000000..e3a3757a2 --- /dev/null +++ b/src/zen-test/xmake.lua @@ -0,0 +1,19 @@ +-- Copyright Epic Games, Inc. All Rights Reserved. + +target("zen-test") + set_kind("binary") + set_group("tests") + add_headerfiles("**.h") + add_files("*.cpp") + add_files("zen-test.cpp", {unity_ignored = true }) + add_deps("zencore", "zenhttp", "zenutil") + add_deps("zen", {inherit=false}) + add_deps("zenserver", {inherit=false}) + add_deps("zentest-appstub", {inherit=false}) + add_packages("llhttp") + + if is_plat("macosx") then + add_ldflags("-framework CoreFoundation") + add_ldflags("-framework Security") + add_ldflags("-framework SystemConfiguration") + end 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 diff --git a/src/zen-test/zen-test.h b/src/zen-test/zen-test.h new file mode 100644 index 000000000..bab86ca88 --- /dev/null +++ b/src/zen-test/zen-test.h @@ -0,0 +1,34 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#if ZEN_WITH_TESTS + +# include <zencore/iobuffer.h> +# include <zencore/string.h> +# include <zencore/testing.h> +# include <zencore/testutils.h> +# include <zenutil/zenserverprocess.h> + +# include <filesystem> + +namespace zen::tests { + +extern zen::ZenServerEnvironment TestEnv; + +// Path to the zen CLI executable, located alongside the running test binary. +std::filesystem::path GetZenExecutablePath(); + +struct ZenCommandResult +{ + int ExitCode = -1; + std::string Output; // combined stdout + stderr +}; + +// Spawn the zen CLI with the given argument string (without the executable name) +// and capture combined stdout/stderr until exit. Blocks until the process exits. +ZenCommandResult RunZen(std::string_view Args); + +} // namespace zen::tests + +#endif |