From 150ffc73367077efb352dabea609df608af4f2e6 Mon Sep 17 00:00:00 2001 From: Rodrigo Tobar Date: Wed, 4 Jan 2023 03:01:42 +0800 Subject: [PATCH] Tests: Add tests for sed utility While the tests for sed itself are simple to begin with, some infrastructure was needed to make them simple. Firstly, there was no home for tests for the applications under Utilities, so I had to create a new subdirectory under Tests to host them. Secondly, and more importantly, there was previously no easy way to launch an executable and easily feed it with data for its stdin, then read its stdout/err and exit code. Looking around the repo I found that the JS tests do a very similar thing though, so I decided to adapt that solution for these tests, but with the higher purpose of someday moving this new Process class to LibCore/Process, where the existing spawn helpers are still very low level, and there is no representation of a Process object that one can easily interact with. Note that this Process implementation is very simple, offers limited functionality, and it doesn't use the EventLoop, so it can break on long inputs/outputs depending on the executable behavior. --- Tests/CMakeLists.txt | 1 + Tests/Utilities/CMakeLists.txt | 7 ++ Tests/Utilities/TestSed.cpp | 179 +++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 Tests/Utilities/CMakeLists.txt create mode 100644 Tests/Utilities/TestSed.cpp diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt index abb04d311a..c5a07289f4 100644 --- a/Tests/CMakeLists.txt +++ b/Tests/CMakeLists.txt @@ -28,3 +28,4 @@ add_subdirectory(LibXML) add_subdirectory(LibCrypto) add_subdirectory(LibTLS) add_subdirectory(Spreadsheet) +add_subdirectory(Utilities) diff --git a/Tests/Utilities/CMakeLists.txt b/Tests/Utilities/CMakeLists.txt new file mode 100644 index 0000000000..43e309ce5c --- /dev/null +++ b/Tests/Utilities/CMakeLists.txt @@ -0,0 +1,7 @@ +set(TEST_SOURCES + TestSed.cpp +) + +foreach(source IN LISTS TEST_SOURCES) + serenity_test("${source}" Utilities) +endforeach() diff --git a/Tests/Utilities/TestSed.cpp b/Tests/Utilities/TestSed.cpp new file mode 100644 index 0000000000..14b5b27639 --- /dev/null +++ b/Tests/Utilities/TestSed.cpp @@ -0,0 +1,179 @@ +/* + * Copyright (c) 2023, Rodrigo Tobar . + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +class Process { +public: + struct ProcessOutputs { + AK::ByteBuffer standard_output; + AK::ByteBuffer standard_error; + }; + + static ErrorOr> create(StringView command, char const* const arguments[]) + { + auto stdin_fds = TRY(Core::System::pipe2(O_CLOEXEC)); + auto stdout_fds = TRY(Core::System::pipe2(O_CLOEXEC)); + auto stderr_fds = TRY(Core::System::pipe2(O_CLOEXEC)); + + posix_spawn_file_actions_t file_actions; + posix_spawn_file_actions_init(&file_actions); + posix_spawn_file_actions_adddup2(&file_actions, stdin_fds[0], STDIN_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, stdout_fds[1], STDOUT_FILENO); + posix_spawn_file_actions_adddup2(&file_actions, stderr_fds[1], STDERR_FILENO); + + auto pid = TRY(Core::System::posix_spawnp(command, &file_actions, nullptr, const_cast(arguments), environ)); + + posix_spawn_file_actions_destroy(&file_actions); + ArmedScopeGuard runner_kill { [&pid] { kill(pid, SIGKILL); } }; + + TRY(Core::System::close(stdin_fds[0])); + TRY(Core::System::close(stdout_fds[1])); + TRY(Core::System::close(stderr_fds[1])); + + auto stdin_file = TRY(Core::File::adopt_fd(stdin_fds[1], Core::File::OpenMode::Write)); + auto stdout_file = TRY(Core::File::adopt_fd(stdout_fds[0], Core::File::OpenMode::Read)); + auto stderr_file = TRY(Core::File::adopt_fd(stderr_fds[0], Core::File::OpenMode::Read)); + + runner_kill.disarm(); + + return make(pid, move(stdin_file), move(stdout_file), move(stderr_file)); + } + + Process(pid_t pid, NonnullOwnPtr stdin_file, NonnullOwnPtr stdout_file, NonnullOwnPtr stderr_file) + : m_pid(pid) + , m_stdin(move(stdin_file)) + , m_stdout(move(stdout_file)) + , m_stderr(move(stderr_file)) + { + } + + ErrorOr write(StringView input) + { + TRY(m_stdin->write_until_depleted(input.bytes())); + m_stdin->close(); + return {}; + } + + bool write_lines(Span lines) + { + // It's possible the process dies before we can write all the tests + // to the stdin. So make sure that we don't crash but just stop writing. + struct sigaction action_handler { + .sa_handler = SIG_IGN, .sa_mask = {}, .sa_flags = 0, + }; + struct sigaction old_action_handler; + if (sigaction(SIGPIPE, &action_handler, &old_action_handler) < 0) { + perror("sigaction"); + return false; + } + + for (DeprecatedString const& line : lines) { + if (m_stdin->write_until_depleted(DeprecatedString::formatted("{}\n", line).bytes()).is_error()) + break; + } + + // Ensure that the input stream ends here, whether we were able to write all lines or not + m_stdin->close(); + + // It's not really a problem if this signal failed + if (sigaction(SIGPIPE, &old_action_handler, nullptr) < 0) + perror("sigaction"); + + return true; + } + + ErrorOr read_all() + { + return ProcessOutputs { TRY(m_stdout->read_until_eof()), TRY(m_stderr->read_until_eof()) }; + } + + enum class ProcessResult { + Running, + DoneWithZeroExitCode, + Failed, + FailedFromTimeout, + Unknown, + }; + + ErrorOr status(int options = 0) + { + if (m_pid == -1) + return ProcessResult::Unknown; + + m_stdin->close(); + + auto wait_result = TRY(Core::System::waitpid(m_pid, options)); + if (wait_result.pid == 0) { + // Attempt to kill it, since it has not finished yet somehow + return ProcessResult::Running; + } + m_pid = -1; + + if (WIFSIGNALED(wait_result.status) && WTERMSIG(wait_result.status) == SIGALRM) + return ProcessResult::FailedFromTimeout; + + if (WIFEXITED(wait_result.status) && WEXITSTATUS(wait_result.status) == 0) + return ProcessResult::DoneWithZeroExitCode; + + return ProcessResult::Failed; + } + +private: + pid_t m_pid; + NonnullOwnPtr m_stdin; + NonnullOwnPtr m_stdout; + NonnullOwnPtr m_stderr; +}; + +static void run_sed(Vector&& arguments, StringView standard_input, StringView expected_stdout) +{ + MUST(arguments.try_insert(0, "sed")); + MUST(arguments.try_append(nullptr)); + auto sed = MUST(Process::create("sed"sv, arguments.data())); + MUST(sed->write(standard_input)); + auto [stdout, stderr] = MUST(sed->read_all()); + auto status = MUST(sed->status()); + if (status != Process::ProcessResult::DoneWithZeroExitCode) { + FAIL(DeprecatedString::formatted("sed didn't exit cleanly: status: {}, stdout:{}, stderr: {}", static_cast(status), StringView { stdout.bytes() }, StringView { stderr.bytes() })); + } + EXPECT_EQ(StringView { expected_stdout.bytes() }, StringView { stdout.bytes() }); +} + +TEST_CASE(print_lineno) +{ + run_sed({ "=", "-n" }, "hi"sv, "1\n"sv); + run_sed({ "=", "-n" }, "hi\n"sv, "1\n"sv); + run_sed({ "=", "-n" }, "hi\nho"sv, "1\n2\n"sv); + run_sed({ "=", "-n" }, "hi\nho\n"sv, "1\n2\n"sv); +} + +TEST_CASE(s) +{ + run_sed({ "s/a/b/g" }, "aa\n"sv, "bb\n"sv); + run_sed({ "s/././g" }, "aa\n"sv, "..\n"sv); + run_sed({ "s/a/b/p" }, "a\n"sv, "b\nb\n"sv); + run_sed({ "s/a/b/p", "-n" }, "a\n"sv, "b\n"sv); + run_sed({ "1s/a/b/" }, "a\na"sv, "b\na\n"sv); + run_sed({ "1s/a/b/p", "-n" }, "a\na"sv, "b\n"sv); +} + +TEST_CASE(hold_space) +{ + run_sed({ "1h; 2x; 2p", "-n" }, "hi\nbye"sv, "hi\n"sv); +} + +TEST_CASE(complex) +{ + run_sed({ "h; x; s/./*/gp; x; h; p; x; s/./*/gp", "-n" }, "hello serenity"sv, "**************\nhello serenity\n**************\n"sv); +}