| 1 | # frozen_string_literal: true |
| 2 | |
| 3 | require "rails_helper" |
| 4 | |
| 5 | RSpec.describe GithubPatchIndex do |
| 6 | # a.rb: new-file lines 1 (context), 2 (added), 3 (context); line 4 removed only. |
| 7 | let(:single_hunk_patch) { "@@ -1,3 +1,3 @@\n line one\n+line two\n line three\n-line four" } |
| 8 | |
| 9 | let(:multi_hunk_patch) do |
| 10 | "@@ -1,2 +1,3 @@\n a\n+b\n c\n@@ -10,2 +11,3 @@\n x\n+y\n z" |
| 11 | end |
| 12 | |
| 13 | def index_for(patch, filename: "a.rb") |
| 14 | described_class.new([ { "filename" => filename, "patch" => patch } ]) |
| 15 | end |
| 16 | |
| 17 | describe "#valid_line?" do |
| 18 | it "accepts added and context lines by new-file number" do |
| 19 | index = index_for(single_hunk_patch) |
| 20 | expect(index.valid_line?("a.rb", 1)).to be true |
| 21 | expect(index.valid_line?("a.rb", 2)).to be true |
| 22 | expect(index.valid_line?("a.rb", 3)).to be true |
| 23 | end |
| 24 | |
| 25 | it "rejects lines outside the hunks" do |
| 26 | index = index_for(single_hunk_patch) |
| 27 | expect(index.valid_line?("a.rb", 4)).to be false |
| 28 | expect(index.valid_line?("a.rb", 100)).to be false |
| 29 | end |
| 30 | |
| 31 | it "rejects unknown files" do |
| 32 | expect(index_for(single_hunk_patch).valid_line?("other.rb", 1)).to be false |
| 33 | end |
| 34 | |
| 35 | it "indexes every hunk of a multi-hunk patch" do |
| 36 | index = index_for(multi_hunk_patch) |
| 37 | expect(index.valid_line?("a.rb", 2)).to be true # + in hunk 1 |
| 38 | expect(index.valid_line?("a.rb", 12)).to be true # + in hunk 2 |
| 39 | expect(index.valid_line?("a.rb", 7)).to be false # between hunks |
| 40 | end |
| 41 | |
| 42 | it "treats a file without a patch (binary) as having no commentable lines" do |
| 43 | index = described_class.new([ { "filename" => "logo.png", "patch" => nil } ]) |
| 44 | expect(index.valid_line?("logo.png", 1)).to be false |
| 45 | end |
| 46 | |
| 47 | it "handles a deleted file (only removed lines) with no commentable lines" do |
| 48 | index = index_for("@@ -1,2 +0,0 @@\n-gone\n-also gone", filename: "dead.rb") |
| 49 | expect(index.valid_line?("dead.rb", 1)).to be false |
| 50 | end |
| 51 | end |
| 52 | |
| 53 | describe "#clamp" do |
| 54 | let(:index) { index_for(single_hunk_patch) } |
| 55 | |
| 56 | it "returns a valid line unchanged" do |
| 57 | expect(index.clamp("a.rb", 2)).to eq(2) |
| 58 | end |
| 59 | |
| 60 | it "snaps a near miss to the nearest commentable line" do |
| 61 | expect(index.clamp("a.rb", 5)).to eq(3) |
| 62 | end |
| 63 | |
| 64 | it "drops a far miss" do |
| 65 | expect(index.clamp("a.rb", 50)).to be_nil |
| 66 | end |
| 67 | |
| 68 | it "drops findings on unknown files or non-integer lines" do |
| 69 | expect(index.clamp("other.rb", 2)).to be_nil |
| 70 | expect(index.clamp("a.rb", nil)).to be_nil |
| 71 | end |
| 72 | end |
| 73 | |
| 74 | describe ".annotate_patch" do |
| 75 | it "prefixes added and context lines with new-file numbers, leaves removed lines bare" do |
| 76 | annotated = described_class.annotate_patch(single_hunk_patch) |
| 77 | lines = annotated.lines.map(&:chomp) |
| 78 | |
| 79 | expect(lines[0]).to eq("@@ -1,3 +1,3 @@") |
| 80 | expect(lines[1]).to match(/\A\s+1 line one\z/) |
| 81 | expect(lines[2]).to match(/\A\s+2 \+line two\z/) |
| 82 | expect(lines[3]).to match(/\A\s+3 line three\z/) |
| 83 | expect(lines[4]).to eq("-line four") |
| 84 | end |
| 85 | |
| 86 | it "restarts numbering at each hunk header" do |
| 87 | annotated = described_class.annotate_patch(multi_hunk_patch) |
| 88 | expect(annotated).to include(" 11 x") |
| 89 | expect(annotated).to include(" 12 +y") |
| 90 | end |
| 91 | |
| 92 | it "returns an empty string for a missing patch" do |
| 93 | expect(described_class.annotate_patch(nil)).to eq("") |
| 94 | end |
| 95 | end |
| 96 | end |