summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRodrigo Tobar <rtobarc@gmail.com>2023-01-04 03:01:42 +0800
committerAndrew Kaster <andrewdkaster@gmail.com>2023-04-09 18:09:23 -0600
commit150ffc73367077efb352dabea609df608af4f2e6 (patch)
tree8270c5c64c6c8440ecbe40b16262b169fdb957e1
parent41fa1a1461df98126e4b3e9071e26d321adedc96 (diff)
downloadserenity-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.txt1
-rw-r--r--Tests/Utilities/CMakeLists.txt7
-rw-r--r--Tests/Utilities/TestSed.cpp179
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);
+}