# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Git::DiffCollection do before do stub_const('MutatingConstantIterator', Class.new) MutatingConstantIterator.class_eval do include Enumerable attr_reader :size def initialize(count, value) @count = count @size = count @value = value end def each return enum_for(:each) unless block_given? loop do break if @count == 0 # It is critical to decrement before yielding. We may never reach the lines after 'yield'. @count -= 1 yield @value end end end end let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } shared_examples 'overflow stuff' do it 'returns the expected overflow values' do subject.overflow? expect(subject.overflow_max_bytes?).to eq(overflow_max_bytes) expect(subject.overflow_max_files?).to eq(overflow_max_files) expect(subject.overflow_max_lines?).to eq(overflow_max_lines) end end subject do Gitlab::Git::DiffCollection.new( iterator, max_files: max_files, max_lines: max_lines, limits: limits, expanded: expanded ) end let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) } let(:file_count) { 0 } let(:line_length) { 1 } let(:line_count) { 1 } let(:max_files) { 10 } let(:max_lines) { 100 } let(:limits) { true } let(:expanded) { true } describe '#to_a' do subject { super().to_a } it { is_expected.to be_kind_of ::Array } end describe '#decorate!' do let(:file_count) { 3 } it 'modifies the array in place' do count = 0 subject.decorate! { |d| !d.nil? && count += 1 } expect(subject.to_a).to eq([1, 2, 3]) expect(count).to eq(3) end it 'avoids future iterator iterations' do subject.decorate! { |d| d unless d.nil? } expect(iterator).not_to receive(:each) subject.overflow? end end context 'overflow handling' do subject { super() } let(:collapsed_safe_files) { false } let(:collapsed_safe_lines) { false } context 'adding few enough files' do let(:file_count) { 3 } context 'and few enough lines' do let(:line_count) { 10 } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('3') } end describe '#size' do it { expect(subject.size).to eq(3) } it 'does not change after peeking' do subject.any? expect(subject.size).to eq(3) end end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end context 'when limiting is disabled' do let(:limits) { false } let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('3') } end describe '#size' do it { expect(subject.size).to eq(3) } it 'does not change after peeking' do subject.any? expect(subject.size).to eq(3) end end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end end end context 'and too many lines' do let(:line_count) { 1000 } let(:overflow_max_lines) { true } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_truthy } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('0+') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq 1000 } end it { expect(subject.size).to eq(0) } context 'when limiting is disabled' do let(:limits) { false } let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('3') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(3) } end end end context 'adding too many files' do let(:file_count) { 11 } let(:overflow_max_files) { true } context 'and few enough lines' do let(:line_count) { 1 } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_truthy } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('10+') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq 10 } end it { expect(subject.size).to eq(10) } context 'when limiting is disabled' do let(:limits) { false } let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('11') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(11) } end end context 'and too many lines' do let(:line_count) { 30 } let(:overflow_max_lines) { true } let(:overflow_max_files) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_truthy } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('3+') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq 120 } end it { expect(subject.size).to eq(3) } context 'when limiting is disabled' do let(:limits) { false } let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('11') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(11) } end end end context 'adding exactly the maximum number of files' do let(:file_count) { 10 } context 'and few enough lines' do let(:line_count) { 1 } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('10') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(10) } end end context 'adding too many bytes' do let(:file_count) { 10 } let(:line_length) { 5200 } let(:overflow_max_bytes) { true } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_truthy } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('9+') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(9) } context 'when limiting is disabled' do let(:limits) { false } let(:overflow_max_bytes) { false } let(:overflow_max_files) { false } let(:overflow_max_lines) { false } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_falsey } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('10') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq file_count * line_count } end it { expect(subject.size).to eq(10) } end end end describe 'empty collection' do subject { Gitlab::Git::DiffCollection.new([]) } it_behaves_like 'overflow stuff' describe '#overflow?' do subject { super().overflow? } it { is_expected.to be_falsey } end describe '#empty?' do subject { super().empty? } it { is_expected.to be_truthy } end describe '#size' do subject { super().size } it { is_expected.to eq(0) } end describe '#real_size' do subject { super().real_size } it { is_expected.to eq('0') } end describe '#line_count' do subject { super().line_count } it { is_expected.to eq 0 } end end describe '#each' do context 'when diff are too large' do let(:collection) do Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }]) end it 'yields Diff instances even when they are too large' do expect { |b| collection.each(&b) } .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'prunes diffs that are too large' do diff = nil collection.each do |d| diff = d end expect(diff.diff).to eq('') end end context 'when diff is quite large will collapse by default' do let(:iterator) { [{ diff: 'a' * 20480 }] } context 'when no collapse is set' do let(:expanded) { true } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) } .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do diff = nil subject.each do |d| diff = d end expect(diff.diff).not_to eq('') end end context 'when no collapse is unset' do let(:expanded) { false } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) } .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end context 'single-file collections' do it 'does not prune diffs' do diff = nil subject.each do |d| diff = d end expect(diff.diff).not_to eq('') end end context 'multi-file collections' do let(:iterator) { [{ diff: 'b' }, { diff: 'a' * 20480 }] } it 'prunes diffs that are quite big' do diff = nil subject.each do |d| diff = d end expect(diff.diff).to eq('') end end context 'when go over safe limits on files' do let(:iterator) { [fake_diff(1, 1)] * 4 } before do allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) .and_return({ max_files: 2, max_lines: max_lines }) end it 'prunes diffs by default even little ones and sets collapsed_safe_files true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') else # 90 lines expect(d.diff).to eq('') end end expect(subject.collapsed_safe_files?).to eq(true) end end context 'when go over safe limits on lines' do let(:iterator) do [ fake_diff(1, 45), fake_diff(1, 45), fake_diff(1, 20480), fake_diff(1, 1) ] end before do allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) .and_return({ max_files: max_files, max_lines: 80 }) end it 'prunes diffs by default even little ones and sets collapsed_safe_lines true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') else # 90 lines expect(d.diff).to eq('') end end expect(subject.collapsed_safe_lines?).to eq(true) end end context 'when go over safe limits on bytes' do let(:iterator) do [ fake_diff(5, 10), fake_diff(5000, 10), fake_diff(5, 10), fake_diff(5, 10) ] end before do allow(Gitlab::CurrentSettings).to receive(:diff_max_patch_bytes).and_return(1.megabyte) allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) .and_return({ max_files: 4, max_lines: 3000 }) end it 'prunes diffs by default even little ones and sets collapsed_safe_bytes true' do subject.each_with_index do |d, i| if i < 2 expect(d.diff).not_to eq('') else # > 80 bytes expect(d.diff).to eq('') end end expect(subject.collapsed_safe_bytes?).to eq(true) end end end context 'when limiting is disabled' do let(:limits) { false } it 'yields Diff instances even when they are quite big' do expect { |b| subject.each(&b) } .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do diff = nil subject.each do |d| diff = d end expect(diff.diff).not_to eq('') end end end context 'when offset_index is given' do subject do Gitlab::Git::DiffCollection.new( iterator, max_files: max_files, max_lines: max_lines, limits: limits, offset_index: 2, expanded: expanded ) end def diff(raw) raw['diff'] end let(:iterator) do [ fake_diff(1, 1), fake_diff(2, 2), fake_diff(3, 3), fake_diff(4, 4) ] end it 'does not yield diffs before the offset' do expect(subject.to_a.map(&:diff)).to eq( [ diff(fake_diff(3, 3)), diff(fake_diff(4, 4)) ] ) end context 'when go over safe limits on bytes' do let(:iterator) do [ fake_diff(1, 10), # 10 fake_diff(1, 10), # 20 fake_diff(1, 15), # 35 fake_diff(1, 20), # 55 fake_diff(1, 45), # 100 - limit hit fake_diff(1, 45), fake_diff(1, 20480), fake_diff(1, 1) ] end before do allow(Gitlab::Git::DiffCollection) .to receive(:default_limits) .and_return({ max_files: max_files, max_lines: 80 }) end it 'considers size of diffs before the offset for prunning' do expect(subject.to_a.map(&:diff)).to eq( [ diff(fake_diff(1, 15)), diff(fake_diff(1, 20)) ] ) end end end end describe '.limits' do let(:options) { {} } subject { described_class.limits(options) } context 'when options do not include max_patch_bytes_for_file_extension' do it 'sets max_patch_bytes_for_file_extension as empty' do expect(subject[:max_patch_bytes_for_file_extension]).to eq({}) end end context 'when options include max_patch_bytes_for_file_extension' do let(:options) { { max_patch_bytes_for_file_extension: { '.file' => 1 } } } it 'sets value for max_patch_bytes_for_file_extension' do expect(subject[:max_patch_bytes_for_file_extension]).to eq({ '.file' => 1 }) end end end def fake_diff(line_length, line_count) { 'diff' => "#{'a' * line_length}\n" * line_count } end end