diff options
author | Rodrigo Tobar <rtobarc@gmail.com> | 2023-01-04 03:01:42 +0800 |
---|---|---|
committer | Andrew Kaster <andrewdkaster@gmail.com> | 2023-04-09 18:09:23 -0600 |
commit | 150ffc73367077efb352dabea609df608af4f2e6 (patch) | |
tree | 8270c5c64c6c8440ecbe40b16262b169fdb957e1 | |
parent | 41fa1a1461df98126e4b3e9071e26d321adedc96 (diff) | |
download | serenity-150ffc73367077efb352dabea609df608af4f2e6.zip |
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.
-rw-r--r-- | Tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | Tests/Utilities/CMakeLists.txt | 7 | ||||
-rw-r--r-- | Tests/Utilities/TestSed.cpp | 179 |
3 files changed, 187 insertions, 0 deletions
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 <rtobarc@gmail.com>. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include <AK/ScopeGuard.h> +#include <AK/StringView.h> +#include <LibCore/File.h> +#include <LibCore/System.h> +#include <LibTest/Macros.h> +#include <LibTest/TestCase.h> +#include <spawn.h> +#include <unistd.h> + +class Process { +public: + struct ProcessOutputs { + AK::ByteBuffer standard_output; + AK::ByteBuffer standard_error; + }; + + static ErrorOr<OwnPtr<Process>> 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<char**>(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<Process>(pid, move(stdin_file), move(stdout_file), move(stderr_file)); + } + + Process(pid_t pid, NonnullOwnPtr<Core::File> stdin_file, NonnullOwnPtr<Core::File> stdout_file, NonnullOwnPtr<Core::File> stderr_file) + : m_pid(pid) + , m_stdin(move(stdin_file)) + , m_stdout(move(stdout_file)) + , m_stderr(move(stderr_file)) + { + } + + ErrorOr<void> write(StringView input) + { + TRY(m_stdin->write_until_depleted(input.bytes())); + m_stdin->close(); + return {}; + } + + bool write_lines(Span<DeprecatedString> 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<ProcessOutputs> 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<ProcessResult> 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<Core::File> m_stdin; + NonnullOwnPtr<Core::File> m_stdout; + NonnullOwnPtr<Core::File> m_stderr; +}; + +static void run_sed(Vector<char const*>&& 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<int>(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); +} |