// Copyright Epic Games, Inc. All Rights Reserved. #include #if ZEN_WITH_TESTS # include "zenserver-test.h" # include # include # include # if ZEN_PLATFORM_WINDOWS # include # else # include # endif namespace zen::tests { using namespace std::literals; static std::filesystem::path GetAppStubPath() { return TestEnv.ProgramBaseDir() / ("zentest-appstub" ZEN_EXE_SUFFIX_LITERAL); } // Read all available data from the read end of a StdoutPipeHandles. // Must be called after CloseWriteEnd() so that the read will see EOF. 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; } TEST_SUITE_BEGIN("server.process"); ////////////////////////////////////////////////////////////////////////// TEST_CASE("pipe.capture_stdout") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); const std::string ExpectedOutput = "hello_from_pipe_test"; std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = fmt::format("zentest-appstub -echo={}", ExpectedOutput); CreateProcOptions Options; Options.StdoutPipe = &Pipe; CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); ProcessHandle Process; Process.Initialize(ProcResult); // Close the write end, then drain before Wait() to avoid deadlock if output fills the pipe buffer. Pipe.CloseWriteEnd(); std::string Output = ReadAllFromPipe(Pipe); Process.Wait(); // The appstub also prints "[zentest] exiting with exit code: 0\n" CHECK(Output.find(ExpectedOutput) != std::string::npos); CHECK_EQ(Process.GetExitCode(), 0); } TEST_CASE("pipe.capture_multiline") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = "zentest-appstub -echo=line1 -echo=line2 -echo=line3"; CreateProcOptions Options; Options.StdoutPipe = &Pipe; CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); ProcessHandle Process; Process.Initialize(ProcResult); Pipe.CloseWriteEnd(); std::string Output = ReadAllFromPipe(Pipe); Process.Wait(); CHECK(Output.find("line1") != std::string::npos); CHECK(Output.find("line2") != std::string::npos); CHECK(Output.find("line3") != std::string::npos); CHECK_EQ(Process.GetExitCode(), 0); } TEST_CASE("pipe.raii_cleanup") { // Verify that StdoutPipeHandles cleans up handles when it goes out of scope // (no leaked handles). We can't directly assert on handle counts, but we can // verify that creating and destroying many pipes doesn't fail. for (int i = 0; i < 100; ++i) { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); // Pipe goes out of scope here — destructor should close both ends } } TEST_CASE("pipe.move_semantics") { StdoutPipeHandles Original; REQUIRE(CreateStdoutPipe(Original)); // Move-construct a new pipe from Original StdoutPipeHandles Moved(std::move(Original)); # if ZEN_PLATFORM_WINDOWS CHECK(Moved.ReadHandle != nullptr); CHECK(Moved.WriteHandle != nullptr); CHECK(Original.ReadHandle == nullptr); CHECK(Original.WriteHandle == nullptr); # else CHECK(Moved.ReadFd >= 0); CHECK(Moved.WriteFd >= 0); CHECK(Original.ReadFd == -1); CHECK(Original.WriteFd == -1); # endif // Move-assign StdoutPipeHandles Assigned; Assigned = std::move(Moved); # if ZEN_PLATFORM_WINDOWS CHECK(Assigned.ReadHandle != nullptr); CHECK(Assigned.WriteHandle != nullptr); CHECK(Moved.ReadHandle == nullptr); CHECK(Moved.WriteHandle == nullptr); # else CHECK(Assigned.ReadFd >= 0); CHECK(Assigned.WriteFd >= 0); CHECK(Moved.ReadFd == -1); CHECK(Moved.WriteFd == -1); # endif // Assigned goes out of scope — destructor closes handles } TEST_CASE("pipe.close_is_idempotent") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); Pipe.Close(); // Calling Close again should be safe (no double-close) Pipe.Close(); # if ZEN_PLATFORM_WINDOWS CHECK(Pipe.ReadHandle == nullptr); CHECK(Pipe.WriteHandle == nullptr); # else CHECK(Pipe.ReadFd == -1); CHECK(Pipe.WriteFd == -1); # endif } TEST_CASE("pipe.close_write_end_only") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); Pipe.CloseWriteEnd(); # if ZEN_PLATFORM_WINDOWS CHECK(Pipe.ReadHandle != nullptr); CHECK(Pipe.WriteHandle == nullptr); # else CHECK(Pipe.ReadFd >= 0); CHECK(Pipe.WriteFd == -1); # endif // Remaining read handle cleaned up by destructor } TEST_CASE("pipe.capture_with_nonzero_exit") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = "zentest-appstub -echo=before_exit -f=42"; CreateProcOptions Options; Options.StdoutPipe = &Pipe; CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); ProcessHandle Process; Process.Initialize(ProcResult); Pipe.CloseWriteEnd(); std::string Output = ReadAllFromPipe(Pipe); Process.Wait(); CHECK(Output.find("before_exit") != std::string::npos); CHECK_EQ(Process.GetExitCode(), 42); } TEST_CASE("pipe.stderr_on_shared_pipe") { StdoutPipeHandles Pipe; REQUIRE(CreateStdoutPipe(Pipe)); std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = "zentest-appstub -echo=from_stdout -echoerr=from_stderr"; CreateProcOptions Options; Options.StdoutPipe = &Pipe; CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); ProcessHandle Process; Process.Initialize(ProcResult); Pipe.CloseWriteEnd(); std::string Output = ReadAllFromPipe(Pipe); Process.Wait(); // Both stdout and stderr content should appear on the shared pipe CHECK(Output.find("from_stdout") != std::string::npos); CHECK(Output.find("from_stderr") != std::string::npos); CHECK_EQ(Process.GetExitCode(), 0); } TEST_CASE("pipe.separate_stderr") { StdoutPipeHandles StdoutPipe; StdoutPipeHandles StderrPipe; REQUIRE(CreateStdoutPipe(StdoutPipe)); REQUIRE(CreateStdoutPipe(StderrPipe)); std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = "zentest-appstub -echo=on_stdout -echoerr=on_stderr"; CreateProcOptions Options; Options.StdoutPipe = &StdoutPipe; Options.StderrPipe = &StderrPipe; CreateProcResult ProcResult = CreateProc(AppStub, CommandLine, Options); ProcessHandle Process; Process.Initialize(ProcResult); StdoutPipe.CloseWriteEnd(); StderrPipe.CloseWriteEnd(); std::string StdoutOutput = ReadAllFromPipe(StdoutPipe); std::string StderrOutput = ReadAllFromPipe(StderrPipe); Process.Wait(); CHECK(StdoutOutput.find("on_stdout") != std::string::npos); CHECK(StderrOutput.find("on_stderr") != std::string::npos); // Verify separation: stderr content should NOT appear in stdout pipe CHECK(StdoutOutput.find("on_stderr") == std::string::npos); CHECK(StderrOutput.find("on_stdout") == std::string::npos); CHECK_EQ(Process.GetExitCode(), 0); } ////////////////////////////////////////////////////////////////////////// TEST_SUITE_END(); } // namespace zen::tests #endif