blob: e1f70f19f11d58ad97972ca3d1659477f574dfae [file] [log] [blame]
// Copyright 2017-2020 The Verible Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "common/strings/patch.h"
#include <initializer_list>
#include <sstream>
#include <vector>
#include "absl/status/status.h"
#include "absl/strings/match.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/string_view.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace verible {
using ::testing::ElementsAre;
using ::testing::ElementsAreArray;
namespace internal {
namespace {
static std::vector<MarkedLine> MakeMarkedLines(
const std::vector<absl::string_view>& lines) {
std::vector<MarkedLine> result;
for (const auto& line : lines) {
result.emplace_back(line);
}
return result;
}
TEST(MarkedLineConstructionTest, BadLine) {
EXPECT_DEATH(MarkedLine(""), "must start with a marker");
EXPECT_DEATH(MarkedLine("?"), "Unexpected marker '?'");
}
TEST(MarkedLineEquality, EqualityTests) {
EXPECT_NE(MarkedLine(" "), MarkedLine("+"));
EXPECT_NE(MarkedLine(" "), MarkedLine("-"));
EXPECT_EQ(MarkedLine(" "), MarkedLine(" "));
EXPECT_NE(MarkedLine(" "), MarkedLine(" "));
EXPECT_NE(MarkedLine(" 1"), MarkedLine(" 2"));
}
TEST(MarkedLineParseTest, InvalidInputs) {
constexpr absl::string_view kTestCases[] = {
"", "x", "x213", "abc", "diff", "====",
};
for (const auto& test : kTestCases) {
MarkedLine m;
EXPECT_FALSE(m.Parse(test).ok()) << " input: \"" << test << '"';
}
}
struct MarkedLineTestCase {
absl::string_view input;
char expected_mark;
absl::string_view expected_text;
};
TEST(MarkedLineParseTest, ValidInputs) {
constexpr MarkedLineTestCase kTestCases[] = {
{" ", ' ', ""}, {" x", ' ', "x"}, {" x213", ' ', "x213"},
{" abc", ' ', " abc"}, {"-abc", '-', "abc"}, {"+abc", '+', "abc"},
{"- abc", '-', " abc"}, {"+ abc", '+', " abc"}, {"---", '-', "--"},
{"+++", '+', "++"}, {"-", '-', ""}, {"+", '+', ""},
};
for (const auto& test : kTestCases) {
MarkedLine m;
EXPECT_TRUE(m.Parse(test.input).ok()) << " input: \"" << test.input << '"';
EXPECT_EQ(m.Marker(), test.expected_mark);
EXPECT_EQ(m.Text(), test.expected_text);
}
}
TEST(MarkedLinePrintTest, Print) {
constexpr absl::string_view kTestCases[] = {
" ", "+", "-", " 1 2 3", "-xyz", "+\tabc",
};
for (const auto& test : kTestCases) {
MarkedLine m;
const auto status = m.Parse(test);
ASSERT_TRUE(status.ok()) << status.message();
std::ostringstream stream;
stream << m;
EXPECT_EQ(stream.str(), test);
}
}
TEST(HunkIndicesEqualityTest, Comparisons) {
EXPECT_EQ((HunkIndices{1, 2}), (HunkIndices{1, 2}));
EXPECT_NE((HunkIndices{1, 1}), (HunkIndices{1, 2}));
EXPECT_NE((HunkIndices{1, 2}), (HunkIndices{1, 1}));
EXPECT_NE((HunkIndices{1, 2}), (HunkIndices{2, 2}));
EXPECT_NE((HunkIndices{3, 2}), (HunkIndices{1, 2}));
}
TEST(HunkIndicesParseTest, InvalidInputs) {
constexpr absl::string_view kTestCases[] = {
"", ",", "4,", ",5", "2,b", "x,2", "4,5,", "1,2,3",
};
for (const auto& test : kTestCases) {
HunkIndices h;
EXPECT_FALSE(h.Parse(test).ok()) << " input: \"" << test << '"';
}
}
struct HunkIndicesTestCase {
absl::string_view input;
int expected_start;
int expected_count;
};
TEST(HunkIndicesParseAndPrintTest, ValidInputs) {
constexpr HunkIndicesTestCase kTestCases[] = {
{"1,1", 1, 1},
{"14,92", 14, 92},
};
for (const auto& test : kTestCases) {
HunkIndices h;
EXPECT_TRUE(h.Parse(test.input).ok()) << " input: \"" << test.input << '"';
EXPECT_EQ(h.start, test.expected_start);
EXPECT_EQ(h.count, test.expected_count);
// Use same data to test printing
std::ostringstream stream;
stream << h;
EXPECT_EQ(stream.str(), test.input);
}
}
TEST(HunkHeaderEqualityTest, Comparisons) {
EXPECT_EQ((HunkHeader{{1, 2}, {3, 4}, ""}), (HunkHeader{{1, 2}, {3, 4}, ""}));
EXPECT_NE((HunkHeader{{1, 2}, {3, 4}, ""}), (HunkHeader{{2, 2}, {3, 4}, ""}));
EXPECT_NE((HunkHeader{{1, 2}, {3, 4}, ""}), (HunkHeader{{1, 1}, {3, 4}, ""}));
EXPECT_NE((HunkHeader{{1, 2}, {3, 4}, ""}), (HunkHeader{{1, 2}, {1, 4}, ""}));
EXPECT_NE((HunkHeader{{1, 2}, {3, 4}, ""}), (HunkHeader{{1, 2}, {3, 5}, ""}));
EXPECT_NE((HunkHeader{{1, 2}, {3, 4}, ""}),
(HunkHeader{{1, 2}, {3, 4}, " "}));
}
TEST(HunkHeaderParseTest, InvalidInputs) {
// If any one character is deleted from this example, it becomes invalid.
constexpr absl::string_view kValidText = "@@ -4,8 +5,6 @@";
for (size_t i = 0; i < kValidText.length(); ++i) {
std::string deleted(kValidText);
deleted.erase(i);
HunkHeader h;
EXPECT_FALSE(h.Parse(deleted).ok()) << " input: \"" << deleted << '"';
}
for (size_t i = 0; i < kValidText.length(); ++i) {
absl::string_view deleted(kValidText.substr(0, i));
HunkHeader h;
EXPECT_FALSE(h.Parse(deleted).ok()) << " input: \"" << deleted << '"';
}
for (size_t i = 1; i < kValidText.length(); ++i) {
absl::string_view deleted(kValidText.substr(i));
HunkHeader h;
EXPECT_FALSE(h.Parse(deleted).ok()) << " input: \"" << deleted << '"';
}
}
TEST(HunkHeaderParseTest, MalformedOldRange) {
constexpr absl::string_view kInvalidText = "@@ 4,8 +5,6 @@";
HunkHeader h;
const auto status = h.Parse(kInvalidText);
EXPECT_FALSE(status.ok()) << " input: \"" << kInvalidText << '"';
EXPECT_TRUE(absl::StrContains(status.message(),
"old-file range should start with '-'"))
<< " got: " << status.message();
}
TEST(HunkHeaderParseTest, MalformedNewRange) {
constexpr absl::string_view kInvalidText = "@@ -4,8 5,6 @@";
HunkHeader h;
const auto status = h.Parse(kInvalidText);
EXPECT_FALSE(status.ok()) << " input: \"" << kInvalidText << '"';
EXPECT_TRUE(absl::StrContains(status.message(),
"new-file range should start with '+'"))
<< " got: " << status.message();
}
TEST(HunkHeaderParseAndPrintTest, ValidInput) {
constexpr absl::string_view kValidText = "@@ -14,8 +5,16 @@";
HunkHeader h;
EXPECT_TRUE(h.Parse(kValidText).ok()) << " input: \"" << kValidText << '"';
EXPECT_EQ(h.old_range.start, 14);
EXPECT_EQ(h.old_range.count, 8);
EXPECT_EQ(h.new_range.start, 5);
EXPECT_EQ(h.new_range.count, 16);
EXPECT_EQ(h.context, "");
// Validate reversibility.
std::ostringstream stream;
stream << h;
EXPECT_EQ(stream.str(), kValidText);
}
TEST(HunkHeaderParseAndPrintTest, ValidInputWithContext) {
constexpr absl::string_view kValidText("@@ -4,28 +51,6 @@ void foo::bar() {");
HunkHeader h;
EXPECT_TRUE(h.Parse(kValidText).ok()) << " input: \"" << kValidText << '"';
EXPECT_EQ(h.old_range.start, 4);
EXPECT_EQ(h.old_range.count, 28);
EXPECT_EQ(h.new_range.start, 51);
EXPECT_EQ(h.new_range.count, 6);
EXPECT_EQ(h.context, " void foo::bar() {");
// Validate reversibility.
std::ostringstream stream;
stream << h;
EXPECT_EQ(stream.str(), kValidText);
}
TEST(SourceInfoParseTest, InvalidInputs) {
constexpr absl::string_view kTestCases[] = {
"", // path must be non-empty
};
for (const auto& test : kTestCases) {
SourceInfo info;
EXPECT_FALSE(info.Parse(test).ok()) << " input: \"" << test << '"';
}
}
TEST(SourceInfoParseAndPrintTest, ValidInputsPathOnly) {
constexpr absl::string_view kPaths[] = {
"a.txt",
"p/q/a.txt",
"/p/q/a.txt",
};
for (const auto& path : kPaths) {
SourceInfo info;
EXPECT_TRUE(info.Parse(path).ok());
EXPECT_EQ(info.path, path);
EXPECT_TRUE(info.timestamp.empty());
// Validate reversibility.
std::ostringstream stream;
stream << info;
EXPECT_EQ(stream.str(), path);
}
}
TEST(SourceInfoParseAndPrintTest, ValidInputsWithTimestamps) {
constexpr absl::string_view kPaths[] = {
"a.txt",
"p/q/a.txt",
"/p/q/a.txt",
};
constexpr absl::string_view kTimes[] = {
"2020-02-02",
"2020-02-02 20:22:02",
"2020-02-02 20:22:02.000000",
"2020-02-02 20:22:02.000000 -0700",
};
for (const auto& path : kPaths) {
for (const auto& time : kTimes) {
SourceInfo info;
const std::string text(absl::StrCat(path, "\t", time));
EXPECT_TRUE(info.Parse(text).ok());
EXPECT_EQ(info.path, path);
EXPECT_EQ(info.timestamp, time);
// Validate reversibility.
std::ostringstream stream;
stream << info;
EXPECT_EQ(stream.str(), text);
}
}
}
TEST(HunkEqualityTest, Comparisons) {
const std::vector<absl::string_view> lines{
" a",
"-b",
"+c",
" d",
};
EXPECT_EQ(Hunk(1, 1, lines.begin(), lines.end()), //
Hunk(1, 1, lines.begin(), lines.end()));
EXPECT_NE(Hunk(1, 1, lines.begin(), lines.end()), //
Hunk(2, 1, lines.begin(), lines.end()));
EXPECT_NE(Hunk(1, 1, lines.begin(), lines.end()), //
Hunk(1, 3, lines.begin(), lines.end()));
EXPECT_NE(Hunk(2, 4, lines.begin(), lines.end()), //
Hunk(2, 4, lines.begin() + 1, lines.end()));
EXPECT_NE(Hunk(2, 4, lines.begin(), lines.end()), //
Hunk(2, 4, lines.begin(), lines.end() - 1));
}
TEST(HunkParseTest, InvalidInputs) {
const std::vector<absl::string_view> kTestCases[] = {
// malformed headers:
{"@@ -1,0 +2,0 @"},
{"@ -1,0 +2,0 @@"},
{"@@ -1,0+2,0 @@"},
// malformed MarkedLines:
{"@@ -1,1 +2,1 @@", ""}, // missing marker character
{"@@ -1,1 +2,1 @@", "missing leading marker character"},
// inconsistent line counts:
{"@@ -1,0 +2,0 @@", "-unexpected"},
{"@@ -1,0 +2,0 @@", "+unexpected"},
{"@@ -1,0 +2,0 @@", " unexpected"},
{
"@@ -1,1 +2,0 @@",
// missing: "-..."
},
{
"@@ -1,0 +2,1 @@",
// missing: "+..."
},
{
"@@ -1,1 +2,1 @@",
// missing: " ..."
},
};
for (const auto& lines : kTestCases) {
Hunk hunk;
const LineRange range(lines.begin(), lines.end());
EXPECT_FALSE(hunk.Parse(range).ok());
}
}
struct UpdateHeaderTestCase {
absl::string_view fixed_header;
std::vector<absl::string_view> payload;
};
TEST(HunkUpdateHeaderTest, Various) {
constexpr absl::string_view kNonsenseHeader = "@@ -222,999 +333,999 @@";
const UpdateHeaderTestCase kTestCases[] = {
{"@@ -222,0 +333,0 @@", {/* empty lines */}},
{"@@ -222,1 +333,0 @@",
{
"-removed",
}},
{"@@ -222,0 +333,1 @@",
{
"+added",
}},
{"@@ -222,1 +333,1 @@",
{
" common",
}},
{"@@ -222,4 +333,3 @@",
{" common", "-removed", "-removed2", "+added", " common again"}},
};
for (const auto& test : kTestCases) {
std::vector<absl::string_view> lines;
lines.push_back(kNonsenseHeader);
lines.insert(lines.end(), test.payload.begin(), test.payload.end());
const LineRange range(lines.begin(), lines.end());
Hunk hunk;
EXPECT_FALSE(hunk.Parse(range).ok());
hunk.UpdateHeader();
const auto status = hunk.IsValid();
EXPECT_TRUE(status.ok()) << status.message();
std::ostringstream stream;
stream << hunk.Header();
EXPECT_EQ(stream.str(), test.fixed_header);
}
}
struct AddedLinesTestCase {
std::vector<absl::string_view> hunk_text;
LineNumberSet expected_added_lines;
};
TEST(HunkAddedLinesTest, Various) {
const AddedLinesTestCase kTestCases[] = {
{
{
"@@ -7,1 +8,1 @@",
" common line, not added",
},
{},
},
{
{
"@@ -7,2 +8,1 @@",
"-deleted line",
" common line, not added",
},
{},
},
{
{"@@ -7,2 +8,1 @@", " common line, not added", "-deleted line"},
{},
},
{
{
"@@ -7,4 +8,2 @@",
" common line, not added",
"-deleted line",
"-deleted line 2",
" common line, not added",
},
{},
},
{
{
"@@ -7,1 +8,2 @@",
" common line, not added",
"+added line",
},
{{9, 10}},
},
{
{
"@@ -7,1 +8,2 @@",
"+added line",
" common line, not added",
},
{{8, 9}},
},
{
{
"@@ -17,2 +28,4 @@",
" common line, not added",
"+added line",
"+added line 2",
" common line, not added",
},
{{29, 31}},
},
{
{
"@@ -7,3 +4,3 @@",
" common line, not added",
"-deleted line",
"+added line",
" common line, not added",
},
{{5, 6}},
},
{
{
"@@ -7,3 +4,3 @@",
" common line, not added",
"+added line",
"-deleted line",
" common line, not added",
},
{{5, 6}},
},
{
{
"@@ -380,8 +401,12 @@",
" common line, not added",
"+added line",
"+added line 2",
" nothing interesting",
" ",
"-delete me",
"+replacement",
" ",
" nothing interesting",
" ",
"+added line",
"+added line 2",
" common line, not added",
},
{{402, 404}, {406, 407}, {410, 412}},
},
};
for (const auto& test : kTestCases) {
const LineRange range(test.hunk_text.begin(), test.hunk_text.end());
Hunk hunk;
const auto status = hunk.Parse(range);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(hunk.AddedLines(), test.expected_added_lines);
}
}
TEST(HunkSplitTest, ExpectedSingletons) {
const std::vector<absl::string_view> kTestCases[] = {
{" no-change"},
{"+one addition"},
{"-one deletion"},
{" no-change", //
" multi-no-change"},
{" same", //
"+added", //
" same"},
{" same", //
"+added", //
"+added more", //
" same"},
{" same", //
"-removed", //
" same"},
{" same", //
"-removed", //
"-removed more", //
" same"},
{" same", //
"-replace this", //
"+with this", //
" same"},
{" same", //
"-replace this", //
"-and this", //
"+with this", //
" same"},
{" same", //
"-replace this", //
"+with this", //
"+and this", //
" same"},
{" same", //
" same more context", //
"-replace this", //
"-and this", //
"+with this", //
"+and this", //
" same more context", //
" same"},
};
for (const auto& test : kTestCases) {
const std::vector<MarkedLine> lines(MakeMarkedLines(test));
// can pick any starting line numbers, doesn't matter
const Hunk hunk(10, 12, lines.begin(), lines.end());
const std::vector<Hunk> splits(hunk.Split());
// expect returned list contains exactly the original hunk
EXPECT_THAT(splits, ElementsAre(hunk));
}
}
struct ExpectedHunk {
int old_starting_line;
int new_starting_line;
std::pair<int, int> marked_line_offsets;
};
static std::vector<Hunk> MakeExpectedHunks(
const Hunk& original, const std::vector<ExpectedHunk>& hunk_infos) {
const auto& marked_lines = original.MarkedLines();
std::vector<Hunk> results;
for (const auto& hunk_info : hunk_infos) {
results.emplace_back(
hunk_info.old_starting_line, hunk_info.new_starting_line,
marked_lines.begin() + hunk_info.marked_line_offsets.first,
marked_lines.begin() + hunk_info.marked_line_offsets.second);
}
return results;
}
struct HunkSplitTestCase {
std::vector<MarkedLine> marked_lines;
Hunk original_hunk;
std::vector<Hunk> expected_sub_hunks;
HunkSplitTestCase(int old_starting_line, int new_starting_line,
const std::vector<absl::string_view>& lines,
const std::vector<ExpectedHunk>& sub_hunks)
: marked_lines(MakeMarkedLines(lines)),
original_hunk(old_starting_line, new_starting_line,
marked_lines.begin(), marked_lines.end()),
expected_sub_hunks(MakeExpectedHunks(original_hunk, sub_hunks)) {}
};
TEST(HunkSplitTest, MultipleSubHunks) {
const HunkSplitTestCase kTestCases[] = {
HunkSplitTestCase(4, 6,
{" no-change", //
"-remove", //
"+replace", //
" no-change"},
{
// one hunk (no split)
{4, 6, {0, 4}},
}),
HunkSplitTestCase(4, 5,
{"+insert", //
" coins", //
"+to continue"},
{
// two hunks
{4, 5, {0, 1}},
{4, 6, {1, 3}},
}),
HunkSplitTestCase(4, 5,
{"+insert", //
" more", //
" coins", //
"+to continue"},
{
// two hunks
{4, 5, {0, 1}},
{4, 6, {1, 4}},
}),
HunkSplitTestCase(7, 7,
{"-insert", //
" coins", //
"-to continue"},
{
// two hunks
{7, 7, {0, 1}},
{8, 7, {1, 3}},
}),
HunkSplitTestCase(7, 7,
{"-move", //
"-these", //
" untouched", //
"+move", "+these"},
{
// two hunks
{7, 7, {0, 2}},
{9, 7, {2, 5}},
}),
HunkSplitTestCase(2, 1,
{" context", //
"-in", //
"+space", //
" no one", //
"-can", //
"-hear", //
"+you", //
"+scream"},
{
// two hunks
{2, 1, {0, 3}},
{4, 3, {3, 8}},
}),
HunkSplitTestCase(3, 4,
{" delete", //
"-every", //
" other", //
"-line", //
" ", //
"-delete", //
" every", //
"-other", //
" line"},
{
// many hunks
{3, 4, {0, 2}},
{5, 5, {2, 4}},
{7, 6, {4, 6}},
{9, 7, {6, 9}},
}),
HunkSplitTestCase(10, 10,
{" a", //
"-long long", //
"+time", //
"-ago", //
" in", //
"+a", //
"+galaxy", //
" far", //
"-far", //
" away"},
{
// many hunks
{10, 10, {0, 4}},
{13, 12, {4, 7}},
{14, 15, {7, 10}},
}),
};
for (const auto& test : kTestCases) {
EXPECT_THAT(test.original_hunk.Split(),
ElementsAreArray(test.expected_sub_hunks));
}
}
TEST(HunkParseAndPrintTest, ValidInputs) {
const std::vector<absl::string_view> kTestCases[] = {
{"@@ -1,0 +2,0 @@"}, // 0 line counts, technically consistent
{"@@ -1,2 +2,2 @@", // 2 lines of context, common to before/after
" same1", //
" same2"},
{"@@ -1,2 +2,2 @@ int foo(void) {", // additional context
" same1", //
" same2"},
{"@@ -1,2 +2,0 @@", // only deletions, no context lines
"-erase me", //
"-erase me too"},
{"@@ -1,0 +2,2 @@", // only additions, no context lines
"+new line 1", //
"+new line 2"},
{"@@ -1,1 +2,1 @@", // 0 lines of context, 1 line changed
"-at first I was like whoa", //
"+and then I was like WHOA"},
{"@@ -1,3 +2,4 @@", // with 1 line of surrounding context
" common line1", //
"-at first I was like whoa", //
"+and then I was like WHOA", //
"+ and then like whoa", //
" common line2"},
};
for (const auto& lines : kTestCases) {
Hunk hunk;
const LineRange range(lines.begin(), lines.end());
const auto status = hunk.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
// Validate reversibility.
std::ostringstream stream;
stream << hunk;
EXPECT_EQ(stream.str(), absl::StrJoin(lines, "\n") + "\n");
}
}
TEST(HunkVerifyAgainstOriginalLinesTest, LineNumberOutOfBounds) {
const std::vector<absl::string_view> kHunkText = {
{
"@@ -2,3 +4,3 @@", // dont' care about position in new-file
" line2", //
"-line3", //
"+line pi", //
" line4", // this line doesn't exist in original
},
};
const std::vector<absl::string_view> kOriginal = {
"line1", "line2", "line3",
// no line4
};
Hunk hunk;
{
const LineRange range(kHunkText.begin(), kHunkText.end());
const auto status = hunk.Parse(range);
ASSERT_TRUE(status.ok()) << status.message();
}
{
const auto status = hunk.VerifyAgainstOriginalLines(kOriginal);
EXPECT_EQ(status.code(), absl::StatusCode::kOutOfRange);
EXPECT_TRUE(absl::StrContains(status.message(), "references line 4"));
EXPECT_TRUE(absl::StrContains(status.message(), "with only 3 lines"));
}
}
TEST(HunkVerifyAgainstOriginalLinesTest, InconsistentRetainedLine) {
const std::vector<absl::string_view> kHunkText = {
{
"@@ -2,2 +4,2 @@",
" line2", //
"-line3", //
"+line pi", //
},
};
const std::vector<absl::string_view> kOriginal = {
"line1",
"line2 different",
"line3",
};
Hunk hunk;
{
const LineRange range(kHunkText.begin(), kHunkText.end());
const auto status = hunk.Parse(range);
ASSERT_TRUE(status.ok()) << status.message();
}
{
const auto status = hunk.VerifyAgainstOriginalLines(kOriginal);
EXPECT_EQ(status.code(), absl::StatusCode::kDataLoss);
EXPECT_TRUE(absl::StrContains(status.message(),
"Patch is inconsistent with original file"));
}
}
TEST(HunkVerifyAgainstOriginalLinesTest, InconsistentDeletedLine) {
const std::vector<absl::string_view> kHunkText = {
{
"@@ -2,2 +4,2 @@",
" line2", //
"-line3", //
"+line pi", //
},
};
const std::vector<absl::string_view> kOriginal = {
"line1",
"line2",
"line3 different",
};
Hunk hunk;
{
const LineRange range(kHunkText.begin(), kHunkText.end());
const auto status = hunk.Parse(range);
ASSERT_TRUE(status.ok()) << status.message();
}
{
const auto status = hunk.VerifyAgainstOriginalLines(kOriginal);
EXPECT_EQ(status.code(), absl::StatusCode::kDataLoss);
EXPECT_TRUE(absl::StrContains(status.message(),
"Patch is inconsistent with original file"));
}
}
TEST(FilePatchParseTest, InvalidInputs) {
const std::vector<absl::string_view> kTestCases[] = {
{}, // empty range is invalid
{""}, // no "---" marker for source info
{
"--- /path/to/file.txt\t2020-03-30", // no "+++" marker for source
// info
},
{
"--- /path/to/file.txt\t2020-03-29",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -2,1 +3,1 @@", // hunk line counts are inconsistent
},
{
"--- /path/to/file.txt\t2020-03-29",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,0 +13,0 @@", // empty, but ok
"@@ -42,1 +43,1 @@", // hunk line counts are inconsistent
},
{
"--- /path/to/file.txt\t2020-03-29",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -2,1 +3,1 @@",
"malformed line does not begin with [ -+]",
},
};
for (const auto& lines : kTestCases) {
const LineRange range(lines.begin(), lines.end());
FilePatch file_patch;
EXPECT_FALSE(file_patch.Parse(range).ok()) << "lines:\n"
<< absl::StrJoin(lines, "\n");
}
}
TEST(FilePatchParseAndPrintTest, ValidInputs) {
const std::vector<absl::string_view> kTestCases[] = {
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
// no hunks, but still valid
},
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,0 +13,0 @@", // empty, but ok
},
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,1 +13,1 @@",
" no change here",
},
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,3 +13,2 @@",
" no change here",
"-delete me",
" no change here",
},
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,2 +13,3 @@",
" no change here",
"+add me",
" no change here",
},
{
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,3 +13,2 @@", // first hunk
" no change here",
"-delete me",
" no change here",
"@@ -52,2 +53,3 @@", // second hunk
" no change here",
"+add me",
" no change here",
},
{
// one line of file metadata
"==== //depot/p4/style/path/to/file.txt#4 - local/path/to/file.txt "
"====",
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,1 +13,1 @@",
" no change here",
},
{
// one line of file metadata
"diff -u a/path/to/file.txt b/path/to/file.txt",
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,1 +13,1 @@",
" no change here",
},
};
for (const auto& lines : kTestCases) {
const LineRange range(lines.begin(), lines.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
// Validate reversibility.
std::ostringstream stream;
stream << file_patch;
EXPECT_EQ(stream.str(), absl::StrJoin(lines, "\n") + "\n");
}
}
TEST(FilePatchIsNewFileTest, NewFile) {
const std::vector<absl::string_view> kInput = {
"--- /dev/null\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -0,0 +1,2 @@",
"+new content 1",
"+new content 2",
};
const LineRange range(kInput.begin(), kInput.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
EXPECT_TRUE(file_patch.IsNewFile());
}
TEST(FilePatchIsNewFileTest, ExistingFile) {
const std::vector<absl::string_view> kInput = {
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,1 +13,1 @@",
" no change here",
};
const LineRange range(kInput.begin(), kInput.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
EXPECT_FALSE(file_patch.IsNewFile());
}
TEST(FilePatchIsDeletedFileTest, DeletedFile) {
const std::vector<absl::string_view> kInput = {
"--- /path/to/file.txt\t2020-03-30",
"+++ /dev/null\t2020-03-30",
"@@ -1,2 +0,0 @@",
"-deleted content 1",
"-deleted content 2",
};
const LineRange range(kInput.begin(), kInput.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
EXPECT_TRUE(file_patch.IsDeletedFile());
}
TEST(FilePatchIsDeletedFileTest, ExistingFile) {
const std::vector<absl::string_view> kInput = {
"--- /path/to/file.txt\t2020-03-30",
"+++ /path/to/file.txt\t2020-03-30",
"@@ -12,2 +13,2 @@",
" no change here",
"+you win some",
"-you lose some",
};
const LineRange range(kInput.begin(), kInput.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
EXPECT_TRUE(status.ok()) << status.message();
EXPECT_FALSE(file_patch.IsDeletedFile());
}
TEST(FilePatchAddedLinesTest, Various) {
const AddedLinesTestCase kTestCases[] = {
{
{
"--- /path/to/file.txt\t2019-12-01",
"+++ /path/to/file.txt\t2019-12-31",
"@@ -12,1 +13,1 @@",
" no change here",
},
{},
},
{
{
"--- /path/to/file.txt\t2019-12-01",
"+++ /path/to/file.txt\t2019-12-31",
"@@ -12,1 +13,1 @@",
" no change here",
"@@ -21,3 +20,2 @@",
" ",
"-bye",
" ",
"@@ -31,2 +43,4 @@",
" ",
"+hello", // line 45
"+world", // line 46
" ",
"@@ -61,3 +80,3 @@",
" ",
"-adios",
"+hola", // line 81
" ",
},
{{44, 46}, {81, 82}},
},
};
for (const auto& test : kTestCases) {
const LineRange range(test.hunk_text.begin(), test.hunk_text.end());
FilePatch file_patch;
const auto status = file_patch.Parse(range);
ASSERT_TRUE(status.ok()) << status.message();
EXPECT_EQ(file_patch.AddedLines(), test.expected_added_lines);
}
}
class FilePatchPickApplyTest : public FilePatch, public ::testing::Test {
protected:
absl::Status ParseLines(const std::vector<absl::string_view>& lines) {
const LineRange range(lines.begin(), lines.end());
return Parse(range);
}
static absl::Status NullFileReader(absl::string_view filename,
std::string* contents) {
contents->clear();
return absl::OkStatus();
}
static absl::Status NullFileWriter(absl::string_view filename,
absl::string_view contents) {
return absl::OkStatus();
}
};
// Takes the place of a real file on the filesystem.
// TODO(fangism): move this to a "file_test_util" library.
struct StringFile {
const absl::string_view path;
const absl::string_view contents;
};
class StringFileSequence {
public:
explicit StringFileSequence(const std::vector<StringFile>& files)
: files_(files) {}
StringFileSequence(std::initializer_list<StringFile> files) : files_(files) {}
protected:
const std::vector<StringFile> files_;
// stateful value needed inside std::function to emulate a sequence of calls
size_t index_ = 0;
};
// Functor that can mimic a sequence of calls to file::GetContents()
// Can be passed anywhere that takes a FileReaderFunction.
// TODO(fangism): move this to a "file_test_util" library.
struct ReadStringFileSequence : public StringFileSequence {
explicit ReadStringFileSequence(const std::vector<StringFile>& files)
: StringFileSequence(files) {}
ReadStringFileSequence(std::initializer_list<StringFile> files)
: StringFileSequence(files) {}
// like file::GetContents()
absl::Status operator()(absl::string_view filename, std::string* dest) {
// ASSERT_LT(i, files_.size()); // can't use ASSERT_* which returns void
if (index_ >= files_.size()) {
return absl::OutOfRangeError(
absl::StrCat("No more files to read beyond index=", index_));
}
const auto& file = files_[index_];
EXPECT_EQ(filename, file.path) << " at index " << index_;
dest->assign(file.contents.begin(), file.contents.end());
++index_;
return absl::OkStatus(); // "file" is successfully read
}
};
// Functor that can mimic a sequence of calls to file::SetContents()
// Can be passed anywhere that takes a FileWriterFunction.
// TODO(fangism): move this to a "file_test_util" library.
struct ExpectStringFileSequence : public StringFileSequence {
explicit ExpectStringFileSequence(const std::vector<StringFile>& files)
: StringFileSequence(files) {}
ExpectStringFileSequence(std::initializer_list<StringFile> files)
: StringFileSequence(files) {}
// like file::SetContents()
absl::Status operator()(absl::string_view filename, absl::string_view src) {
// ASSERT_LT(i, files_.size()); // can't use ASSERT_* which returns void
if (index_ >= files_.size()) {
return absl::OutOfRangeError(
absl::StrCat("No more files to compare beyond index=", index_));
}
const auto& file = files_[index_];
EXPECT_EQ(filename, file.path) << " at index " << index_;
EXPECT_EQ(file.contents, src) << " at index " << index_;
++index_;
return absl::OkStatus(); // "file" is successfully written
}
};
TEST_F(FilePatchPickApplyTest, ErrorReadingFile) {
std::istringstream ins;
std::ostringstream outs;
constexpr absl::string_view kErrorMessage = "File not found.";
auto error_file_reader = [=](absl::string_view filename,
std::string* contents) {
return absl::NotFoundError(kErrorMessage);
};
const auto status = PickApply(ins, outs, error_file_reader, &NullFileWriter);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.message(), kErrorMessage);
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(FilePatchPickApplyTest, IgnoreNewFile) {
{
const std::vector<absl::string_view> kHunkText{
"--- /dev/null\t2012-01-01",
"+++ foo.txt\t2012-01-01",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
const auto status = PickApply(ins, outs, &NullFileReader, &NullFileWriter);
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(FilePatchPickApplyTest, IgnoreDeletedFile) {
{
const std::vector<absl::string_view> kHunkText{
"--- bar.txt\t2012-01-01",
"+++ /dev/null\t2012-01-01",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
const auto status = PickApply(ins, outs, &NullFileReader, &NullFileWriter);
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(FilePatchPickApplyTest, EmptyPatchNoPrompt) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
constexpr absl::string_view kOriginal = "aaa\nbbb\nccc\n";
constexpr absl::string_view kExpected = kOriginal; // no change
const auto status =
PickApply(ins, outs, //
internal::ReadStringFileSequence({{"foo.txt", kOriginal}}),
internal::ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(FilePatchPickApplyTest, ErrorWritingFileInPlace) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
constexpr absl::string_view kOriginal = "aaa\nbbb\nccc\n";
constexpr absl::string_view kErrorMessage = "Cannot write file.";
auto error_file_writer = [=](absl::string_view, absl::string_view) {
return absl::PermissionDeniedError(kErrorMessage);
};
const auto status = PickApply(
ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}), error_file_writer);
EXPECT_FALSE(status.ok());
EXPECT_EQ(status.message(), kErrorMessage);
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(FilePatchPickApplyTest, OneHunkNotApplied) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb",
"-ccc", // patch proposes to delete this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("n\n"); // user declines patch hunk
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected = kOriginal;
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, PatchInconsistentWithOriginalText) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb", // inconsistent with original
"-ccc", // patch proposes to delete this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\n"); // user accepts hunk
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb-different\n" // inconsistent with patch
"ccc\n" // deleted by hunk
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_EQ(status.code(), absl::StatusCode::kDataLoss);
}
TEST_F(FilePatchPickApplyTest, OneDeletionAccepted) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb",
"-ccc", // patch proposes to delete this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\n"); // user accepts hunk
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // deleted by hunk
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, OneInsertionAccepted) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,2 +2,3 @@",
" bbb",
"+bbb.5", // patch proposes to insert this line
" ccc",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\n"); // user accepts hunk
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"bbb.5\n" // inserted
"ccc\n"
"ddd\n"
"eee\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, OneReplacementAccepted) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\n"); // user accepts hunk
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"C++\n" // replaced
"ddd\n"
"eee\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, HelpFirstThenAcceptHunk) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("?\ny\n"); // help, accept
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"C++\n" // replaced
"ddd\n"
"eee\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
EXPECT_TRUE(absl::StrContains(outs.str(), "print this help"));
}
TEST_F(FilePatchPickApplyTest, HunksOutOfOrder) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
"@@ -2,3 +2,3 @@", // bad ordering
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\nn\n"); // accept, reject
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"C++\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_FALSE(status.ok());
EXPECT_TRUE(absl::StrContains(status.message(), "not properly ordered"));
}
TEST_F(FilePatchPickApplyTest, AcceptOnlyFirstOfTwoHunks) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\nn\n"); // accept, reject
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"C++\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AcceptOnlySecondOfTwoHunks) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("n\ny\n"); // reject, accept
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // kept
"ddd\n"
"eee\n"
"fff\n" // changed
"ggg\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ccc\n" // kept
"ddd\n"
"eee\n"
"fangism\n" // changed
"ggg\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, SplitThenAcceptOnlyFirstOfTwoHunks) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,6 +2,6 @@", // one large splittable hunk
" bbb",
"-ccc", // elect to apply
"+C++", // this change
" ddd",
" eee",
"-fff", // but not
"+fangism", // this change
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("s\ny\nn\n"); // split, accept, reject
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"C++\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, SplitThenAcceptOnlySecondOfTwoHunks) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,6 +2,6 @@", // one large splittable hunk
" bbb",
"-ccc", // elect to reject
"+C++", // this change
" ddd",
" eee",
"-fff", // but accept
"+fangism", // this change
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("s\nn\ny\n"); // split, reject, accept
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // kept
"ddd\n"
"eee\n"
"fff\n" // replaced
"ggg\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ccc\n" // kept
"ddd\n"
"eee\n"
"fangism\n" // replaced
"ggg\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AbortRightAway) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("q\n"); // quit (abandon)
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected = kOriginal;
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, TreatEndOfUserInputAsAbort) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins; // empty, end of user input
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected = kOriginal;
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AbortFileAfterAcceptingOneHunk) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,3 @@",
" bbb",
"-ccc", // patch proposes to replace this line
"+C++", // with this line
" ddd",
"@@ -5,3 +5,3 @@",
" eee",
"-fff",
"+fangism",
" ggg",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\nq\n"); // accept, quit (abandon)
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n" // replaced
"ddd\n"
"eee\n"
"fff\n"
"ggg\n";
constexpr absl::string_view kExpected = kOriginal;
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AcceptTwoDeletions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb",
"-ccc", // delete this line
" ddd",
"@@ -6,3 +5,2 @@",
" fff",
"-ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\ny\n"); // accept, accept
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n"
"hhh\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n"
"fff\n"
"hhh\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AcceptAllDeletions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb",
"-ccc", // delete this line
" ddd",
"@@ -6,3 +5,2 @@",
" fff",
"-ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("a\n"); // accept all
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n"
"hhh\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n"
"fff\n"
"hhh\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, RejectAllDeletions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,3 +2,2 @@",
" bbb",
"-ccc", // delete this line
" ddd",
"@@ -6,3 +5,2 @@",
" fff",
"-ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("d\n"); // accept all
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n"
"hhh\n";
constexpr absl::string_view kExpected = kOriginal; // no changes
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AcceptTwoInsertions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,2 +2,3 @@",
" bbb",
"+ccc", // delete this line
" ddd",
"@@ -5,2 +6,3 @@",
" fff",
"+ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\ny\n"); // accept, accept
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n"
"fff\n"
"hhh\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n"
"hhh\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, AcceptAllInsertions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,2 +2,3 @@",
" bbb",
"+ccc", // delete this line
" ddd",
"@@ -5,2 +6,3 @@",
" fff",
"+ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("a\n"); // accept all
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n"
"fff\n"
"hhh\n";
constexpr absl::string_view kExpected =
"aaa\n"
"bbb\n"
"ccc\n"
"ddd\n"
"eee\n"
"fff\n"
"ggg\n"
"hhh\n";
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(FilePatchPickApplyTest, RejectAllInsertions) {
{
const std::vector<absl::string_view> kHunkText{
"--- foo.txt\t2012-01-01",
"+++ foo-formatted.txt\t2012-01-01",
"@@ -2,2 +2,3 @@",
" bbb",
"+ccc", // delete this line
" ddd",
"@@ -5,2 +6,3 @@",
" fff",
"+ggg", // delete this line
" hhh",
};
const auto status = ParseLines(kHunkText);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("d\n"); // reject all
std::ostringstream outs;
constexpr absl::string_view kOriginal =
"aaa\n"
"bbb\n"
"ddd\n"
"eee\n"
"fff\n"
"hhh\n";
constexpr absl::string_view kExpected = kOriginal;
const auto status =
PickApply(ins, outs, //
ReadStringFileSequence({{"foo.txt", kOriginal}}),
ExpectStringFileSequence({{"foo.txt", kExpected}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
} // namespace
} // namespace internal
namespace {
// public interface tests
TEST(PatchSetParseTest, InvalidInputs) {
constexpr absl::string_view kTestCases[] = {
// no "+++" marker for source
"--- /path/to/file.txt\t2020-03-30\n",
// hunk line counts are inconsistent
"--- /path/to/file.txt\t2020-03-29\n"
"+++ /path/to/file.txt\t2020-03-30\n"
"@@ -2,1 +3,1 @@\n",
// second hunk line counts are inconsistent
"--- /path/to/file.txt\t2020-03-29\n"
"+++ /path/to/file.txt\t2020-03-30\n"
"@@ -12,0 +13,0 @@\n"
"@@ -42,1 +43,1 @@\n",
// malformed hunk marked-line
"--- /path/to/file.txt\t2020-03-29\n"
"+++ /path/to/file.txt\t2020-03-30\n"
"@@ -2,1 +3,1 @@\n"
"malformed line does not begin with [ -+]",
};
for (const auto& patch_contents : kTestCases) {
PatchSet patch_set;
EXPECT_FALSE(patch_set.Parse(patch_contents).ok()) << "contents:\n"
<< patch_contents;
}
}
TEST(PatchSetParseAndPrintTest, ValidInputs) {
constexpr absl::string_view kTestCases[] = {
// no metadata here
"--- /path/to/file.txt\t2020-03-30\n"
"+++ /path/to/file.txt\t2020-03-30\n"
"@@ -12,3 +13,2 @@\n"
" no change here\n"
"-delete me\n"
" no change here\n"
"@@ -52,2 +53,3 @@\n"
" no change here\n"
"+add me\n"
" no change here\n",
// with patchset metadata here
"metadata\n"
"From: hobbit@fryingpan.org\n"
"To: hobbit@fire.org\n"
"metadata\n"
"\n"
"--- /path/to/file.txt\t2020-03-30\n"
"+++ /path/to/file.txt\t2020-03-30\n"
"@@ -12,3 +13,2 @@\n"
" no change here\n"
"-delete me\n"
" no change here\n"
"@@ -52,2 +53,3 @@\n"
" no change here\n"
"+add me\n"
" no change here\n",
// with file metadata in two files
"diff -u /path/to/file1.txt local/path/to/file1.txt\n"
"--- /path/to/file1.txt\t2020-03-30\n"
"+++ /path/to/file1.txt\t2020-03-30\n"
"@@ -12,3 +13,2 @@\n"
" no change here\n"
"-delete me\n"
" no change here\n"
"diff -u /path/to/file2.txt local/path/to/file2.txt\n"
"--- /path/to/file2.txt\t2020-03-30\n"
"+++ /path/to/file2.txt\t2020-03-30\n"
"@@ -52,2 +53,3 @@\n"
" no change here\n"
"+add me\n"
" no change here\n",
// with patchset metadata and file metadata in two files
"From: author@fryingpan.org\n"
"To: reviewer@fire.org\n"
"\n"
"diff -u /path/to/file1.txt local/path/to/file1.txt\n"
"--- /path/to/file1.txt\t2020-03-30\n"
"+++ /path/to/file1.txt\t2020-03-30\n"
"@@ -12,3 +13,2 @@\n"
" no change here\n"
"-delete me\n"
" no change here\n"
"diff -u /path/to/file2.txt local/path/to/file2.txt\n"
"--- /path/to/file2.txt\t2020-03-30\n"
"+++ /path/to/file2.txt\t2020-03-30\n"
"@@ -52,2 +53,3 @@\n"
" no change here\n"
"+add me\n"
" no change here\n",
};
for (const auto& patch_contents : kTestCases) {
PatchSet patch_set;
const auto status = patch_set.Parse(patch_contents);
EXPECT_TRUE(status.ok()) << status.message();
// Validate reversibility.
std::ostringstream stream;
stream << patch_set;
EXPECT_EQ(stream.str(), patch_contents);
}
}
TEST(PatchSetAddedLinesMapTest, NewAndExistingFile) {
constexpr absl::string_view patch_contents = //
"diff -u /dev/null local/path/to/file1.txt\n"
"--- /dev/null\t2020-03-30\n"
"+++ /path/to/file1.txt\t2020-03-30\n" // new file
"@@ -0,0 +1,2 @@\n"
"+add me\n"
"+add me too\n"
"--- /path/to/file2.txt\t2020-03-30\n"
"+++ /path/to/file2.txt\t2020-03-30\n" // existing file
"@@ -52,2 +53,4 @@\n"
" no change here\n"
"+add me\n"
"+add me too\n"
" no change here\n"
"diff -u local/path/to/file3.txt /dev/null\n"
"--- /path/to/file3.txt\t2020-03-30\n" // deleted file
"+++ /dev/null\t2020-03-30\n"
"@@ -1,2 +0,0 @@\n"
"-bye\n"
"-bye\n";
PatchSet patch_set;
const auto status = patch_set.Parse(patch_contents);
ASSERT_TRUE(status.ok()) << status.message();
typedef FileLineNumbersMap::value_type P;
EXPECT_THAT(patch_set.AddedLinesMap(false),
ElementsAre(P{"/path/to/file1.txt", {}},
P{"/path/to/file2.txt", {{54, 56}}}));
EXPECT_THAT(patch_set.AddedLinesMap(true),
ElementsAre(P{"/path/to/file1.txt", {{1, 3}}},
P{"/path/to/file2.txt", {{54, 56}}}));
// Neither case should include deleted files like file3.txt
}
class PatchSetPickApplyTest : public PatchSet, public ::testing::Test {};
TEST_F(PatchSetPickApplyTest, EmptyFilePatchHunks) {
{
constexpr absl::string_view patch_contents = //
"diff -u /dev/null local/path/to/file1.txt\n"
"--- foo/bar.txt\t2020-03-30\n"
"+++ foo/bar-formatted.txt\t2020-03-30\n";
const auto status = Parse(patch_contents);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
// No file I/O or prompting because patch is empty.
const auto status = PickApply(
ins, outs, //
internal::ReadStringFileSequence({{"foo/bar.txt", "don't care\n"}}),
internal::ExpectStringFileSequence({{"foo/bar.txt", "don't care\n"}}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(PatchSetPickApplyTest, MultipleEmptyFilePatchHunks) {
{
constexpr absl::string_view patch_contents = //
"diff -u /dev/null local/path/to/file1.txt\n"
"--- foo/bar.txt\t2020-03-30\n"
"+++ foo/bar-formatted.txt\t2020-03-30\n"
"--- bar/foo.txt\t2020-03-30\n"
"+++ bar/foo-formatted.txt\t2020-03-30\n";
const auto status = Parse(patch_contents);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins;
std::ostringstream outs;
// No file I/O or prompting because patches are empty.
const auto status = PickApply(ins, outs, //
internal::ReadStringFileSequence({
{"foo/bar.txt", "don't care\n"},
{"bar/foo.txt", "don't care\n"},
}),
internal::ExpectStringFileSequence({
{"foo/bar.txt", "don't care\n"},
{"bar/foo.txt", "don't care\n"},
}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_TRUE(outs.str().empty()) << "Unexpected: " << outs.str();
}
TEST_F(PatchSetPickApplyTest, MultipleNonEmptyFilePatchHunks) {
{
constexpr absl::string_view patch_contents = //
"diff -u /dev/null local/path/to/file1.txt\n"
"--- foo/bar.txt\t2020-03-30\n"
"+++ foo/bar-formatted.txt\t2020-03-30\n"
"@@ -1,3 +1,2 @@\n"
" you\n"
"-lose\n"
" some\n"
"--- bar/foo.txt\t2020-03-30\n"
"+++ bar/foo-formatted.txt\t2020-03-30\n"
"@@ -1,2 +1,3 @@\n"
" you\n"
"+win\n"
" some\n";
const auto status = Parse(patch_contents);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\ny\n"); // accept one hunk in each file
std::ostringstream outs;
// No file I/O or prompting because patches are empty.
const auto status = PickApply(ins, outs, //
internal::ReadStringFileSequence({
{"foo/bar.txt", "you\nlose\nsome\n"},
{"bar/foo.txt", "you\nsome\n"},
}),
internal::ExpectStringFileSequence({
{"foo/bar.txt", "you\nsome\n"},
{"bar/foo.txt", "you\nwin\nsome\n"},
}));
EXPECT_TRUE(status.ok()) << "Got: " << status.message();
EXPECT_FALSE(outs.str().empty());
}
TEST_F(PatchSetPickApplyTest, FirstFilePatchOutOfOrder) {
{
constexpr absl::string_view patch_contents = //
"diff -u /dev/null local/path/to/file1.txt\n"
"--- foo/bar.txt\t2020-03-30\n"
"+++ foo/bar-formatted.txt\t2020-03-30\n"
"@@ -4,3 +3,2 @@\n" // out-of-order
" out\n"
"-of\n"
" order\n"
"@@ -1,3 +1,2 @@\n"
" you\n"
"-lose\n"
" some\n"
"--- bar/foo.txt\t2020-03-30\n"
"+++ bar/foo-formatted.txt\t2020-03-30\n"
"@@ -1,2 +1,3 @@\n"
" you\n"
"+win\n"
" some\n";
const auto status = Parse(patch_contents);
ASSERT_TRUE(status.ok()) << status.message();
}
std::istringstream ins("y\ny\n"); // accept
std::ostringstream outs;
// No file I/O or prompting because patches are empty.
const auto status =
PickApply(ins, outs, //
internal::ReadStringFileSequence({
{"foo/bar.txt", "you\nlose\nsome\nout\nof\norder"},
{"bar/foo.txt", "you\nsome\n"},
}),
internal::ExpectStringFileSequence({
{"foo/bar.txt", "you\nsome\n"},
{"bar/foo.txt", "you\nwin\nsome\n"},
}));
EXPECT_FALSE(status.ok());
EXPECT_TRUE(absl::StrContains(status.message(), "not properly ordered"));
}
} // namespace
} // namespace verible