ladybird/Tests/Utilities/TestPatch.cpp
Shannon Booth dd373eacbc LibDiff+patch: Support multiple patches in a single patch file
Multiple patches may be concatenated in the same patch file, such as git
commits which are changing multiple files at the same time. To handle
this, parse each patch in order in the patch file, and apply each patch
sequentially.

To determine whether we are at the end of a patch (and not just parsing
another hunk) the parser will look for a leading '@@ ' after every hunk.
If that is found, there is another hunk. Otherwise, we must be at the
end of this patch.
2023-07-30 07:47:22 +01:00

190 lines
5.2 KiB
C++

/*
* Copyright (c) 2023, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/StringView.h>
#include <LibCore/Command.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibFileSystem/FileSystem.h>
#include <LibTest/Macros.h>
#include <LibTest/TestCase.h>
static constexpr char const* s_test_dir = "/tmp/patch-test";
#define EXPECT_FILE_EQ(file_path, expected_content) \
do { \
auto output = MUST(Core::File::open(file_path, Core::File::OpenMode::Read)); \
auto content = MUST(output->read_until_eof()); \
EXPECT_EQ(StringView { content }, expected_content); \
} while (false)
class PatchSetup {
public:
PatchSetup()
{
clean_up(); // Just in case something was left behind from beforehand.
MUST(Core::System::mkdir(StringView { s_test_dir, strlen(s_test_dir) }, 0755));
}
~PatchSetup()
{
clean_up();
}
private:
static void clean_up()
{
auto result = FileSystem::remove(StringView { s_test_dir, strlen(s_test_dir) }, FileSystem::RecursionMode::Allowed);
if (result.is_error())
VERIFY(result.error().is_errno() && result.error().code() == ENOENT);
}
};
static void run_patch(Vector<char const*>&& arguments, StringView standard_input, StringView expected_stdout)
{
// Ask patch to run the test in a temporary directory so we don't leave any files around.
Vector<char const*> args_with_chdir = { "patch", "-d", s_test_dir };
args_with_chdir.extend(arguments);
args_with_chdir.append(nullptr);
auto patch = MUST(Core::Command::create("patch"sv, args_with_chdir.data()));
MUST(patch->write(standard_input));
auto [stdout, stderr] = MUST(patch->read_all());
auto status = MUST(patch->status());
if (status != Core::Command::ProcessResult::DoneWithZeroExitCode) {
FAIL(MUST(String::formatted("patch 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(basic_change_patch)
{
PatchSetup setup;
auto patch = R"(
--- a
+++ b
@@ -1,3 +1,3 @@
1
-2
+b
3
)"sv;
auto file = "1\n2\n3\n"sv;
auto input = MUST(Core::File::open(MUST(String::formatted("{}/a", s_test_dir)), Core::File::OpenMode::Write));
MUST(input->write_until_depleted(file.bytes()));
run_patch({}, patch, "patching file a\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/a", s_test_dir)), "1\nb\n3\n");
}
TEST_CASE(basic_addition_patch_from_empty_file)
{
PatchSetup setup;
auto patch = R"(
--- /dev/null
+++ a
@@ -0,0 +1,3 @@
+1
+2
+3
)"sv;
auto file = ""sv;
auto input = MUST(Core::File::open(MUST(String::formatted("{}/a", s_test_dir)), Core::File::OpenMode::Write));
MUST(input->write_until_depleted(file.bytes()));
run_patch({}, patch, "patching file a\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/a", s_test_dir)), "1\n2\n3\n");
}
TEST_CASE(strip_path_to_basename)
{
PatchSetup setup;
auto patch = R"(
--- /dev/null
+++ a/bunch/of/../folders/stripped/to/basename
@@ -0,0 +1 @@
+Hello, friends!
)"sv;
auto file = ""sv;
auto input = MUST(Core::File::open(MUST(String::formatted("{}/basename", s_test_dir)), Core::File::OpenMode::Write));
MUST(input->write_until_depleted(file.bytes()));
run_patch({}, patch, "patching file basename\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/basename", s_test_dir)), "Hello, friends!\n");
}
TEST_CASE(strip_path_partially)
{
PatchSetup setup;
auto patch = R"(
--- /dev/null
+++ a/bunch/of/../folders/stripped/to/basename
@@ -0,0 +1 @@
+Hello, friends!
)"sv;
MUST(Core::System::mkdir(MUST(String::formatted("{}/to", s_test_dir)), 0755));
auto file = ""sv;
auto input = MUST(Core::File::open(MUST(String::formatted("{}/to/basename", s_test_dir)), Core::File::OpenMode::Write));
MUST(input->write_until_depleted(file.bytes()));
run_patch({ "-p6" }, patch, "patching file to/basename\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/to/basename", s_test_dir)), "Hello, friends!\n");
}
TEST_CASE(add_file_from_scratch)
{
PatchSetup setup;
auto patch = R"(
--- /dev/null
+++ a/file_to_add
@@ -0,0 +1 @@
+Hello, friends!
)"sv;
run_patch({}, patch, "patching file file_to_add\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/file_to_add", s_test_dir)), "Hello, friends!\n");
}
TEST_CASE(two_patches_in_single_patch_file)
{
PatchSetup setup;
auto patch = R"(
--- /dev/null
+++ a/first_file_to_add
@@ -0,0 +1 @@
+Hello, friends!
--- /dev/null
+++ a/second_file_to_add
@@ -0,0 +1 @@
+Hello, friends!
)"sv;
run_patch({}, patch, "patching file first_file_to_add\n"
"patching file second_file_to_add\n"sv);
EXPECT_FILE_EQ(MUST(String::formatted("{}/first_file_to_add", s_test_dir)), "Hello, friends!\n");
EXPECT_FILE_EQ(MUST(String::formatted("{}/second_file_to_add", s_test_dir)), "Hello, friends!\n");
}