// 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 # 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; } // Write all of @p Data to the write end of a StdinPipeHandles. Returns true on success. static bool WriteAllToPipe(StdinPipeHandles& Pipe, std::string_view Data) { # if ZEN_PLATFORM_WINDOWS const char* Ptr = Data.data(); size_t Remaining = Data.size(); while (Remaining > 0) { DWORD BytesWritten = 0; if (!::WriteFile(Pipe.WriteHandle, Ptr, static_cast(Remaining), &BytesWritten, nullptr) || BytesWritten == 0) { return false; } Ptr += BytesWritten; Remaining -= BytesWritten; } return true; # else const char* Ptr = Data.data(); size_t Remaining = Data.size(); while (Remaining > 0) { ssize_t BytesWritten = write(Pipe.WriteFd, Ptr, Remaining); if (BytesWritten <= 0) { if (BytesWritten < 0 && errno == EINTR) { continue; } return false; } Ptr += BytesWritten; Remaining -= static_cast(BytesWritten); } return true; # endif } 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; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); // 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; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); 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; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); 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; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); 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; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); 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_CASE("stdin_pipe.echo_round_trip") { StdinPipeHandles StdinPipe; StdoutPipeHandles StdoutPipe; REQUIRE(CreateStdinPipe(StdinPipe)); REQUIRE(CreateStdoutPipe(StdoutPipe)); const std::string Input = "hello-from-stdin\nsecond-line\n"; std::filesystem::path AppStub = GetAppStubPath(); std::string CommandLine = "zentest-appstub -stdin_echo"; CreateProcOptions Options; Options.StdinPipe = &StdinPipe; Options.StdoutPipe = &StdoutPipe; ProcessHandle Process(CreateProc(AppStub, CommandLine, Options)); // Close the read end (child side) so that when we finish writing and close the write end, // the child sees EOF. Also close the stdout write end so our reads see EOF after the child exits. StdinPipe.CloseReadEnd(); StdoutPipe.CloseWriteEnd(); REQUIRE(WriteAllToPipe(StdinPipe, Input)); StdinPipe.CloseWriteEnd(); // signals EOF to the child std::string Output = ReadAllFromPipe(StdoutPipe); Process.Wait(); CHECK(Output.find(Input) != std::string::npos); CHECK_EQ(Process.GetExitCode(), 0); } TEST_CASE("stdin_pipe.raii_cleanup") { for (int i = 0; i < 100; ++i) { StdinPipeHandles Pipe; REQUIRE(CreateStdinPipe(Pipe)); } } TEST_CASE("stdin_pipe.move_semantics") { StdinPipeHandles Original; REQUIRE(CreateStdinPipe(Original)); StdinPipeHandles 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 StdinPipeHandles 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 } TEST_CASE("stdin_pipe.close_is_idempotent") { StdinPipeHandles Pipe; REQUIRE(CreateStdinPipe(Pipe)); Pipe.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_SUITE_END(); } // namespace zen::tests #endif