# frozen_string_literal: true require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Parsers::Coverage::SaxDocument do subject(:parse_report) { Nokogiri::XML::SAX::Parser.new(described_class.new(coverage_report, project_path, paths)).parse(cobertura) } describe '#parse!' do let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new } let(:project_path) { 'foo/bar' } let(:paths) { ['app/user.rb'] } let(:cobertura) do <<~EOF #{sources_xml} #{classes_xml} EOF end context 'when data is Cobertura style XML' do shared_examples_for 'ignoring sources, project_path, and worktree_paths' do context 'when there is no ' do let(:classes_xml) { '' } it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'when there is a single ' do context 'with no lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'with a single line' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) end end context 'without a package parent' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 } }) end end context 'with multiple lines and methods info' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) end end end context 'when there are multiple packages' do let(:cobertura) do <<~EOF EOF end it 'parses XML and returns coverage information per class' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app1.rb' => { 1 => 2 }, 'app2.rb' => { 11 => 3 } }) end end context 'when there are multiple ' do context 'without a package parent' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns coverage information per class' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2 }, 'foo.rb' => { 6 => 1 } }) end end context 'with the same filename and different lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with merged coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) end end context 'with the same filename and lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with summed-up coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 3, 2 => 1 } }) end end context 'with missing filename' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and ignores class with missing name' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app.rb' => { 1 => 2, 2 => 0 } }) end end context 'with invalid line information' do let(:classes_xml) do <<~EOF EOF end it 'raises an error' do expect { parse_report }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError) end end end end context 'when there is no ' do let(:sources_xml) { '' } it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'when there is an empty ' do let(:sources_xml) { '' } it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'when there is a ' do context 'and has a single source with a pattern for Go projects' do let(:project_path) { 'local/go' } # Make sure we're not making false positives let(:sources_xml) do <<~EOF /usr/local/go/src EOF end it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'and has multiple sources with a pattern for Go projects' do let(:project_path) { 'local/go' } # Make sure we're not making false positives let(:sources_xml) do <<~EOF /usr/local/go/src /go/src EOF end it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'and has a single source but already is at the project root path' do let(:sources_xml) do <<~EOF builds/#{project_path} EOF end it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'and has multiple sources but already are at the project root path' do let(:sources_xml) do <<~EOF builds/#{project_path}/ builds/somewhere/#{project_path} EOF end it_behaves_like 'ignoring sources, project_path, and worktree_paths' end context 'and has a single source that is not at the project root path' do let(:sources_xml) do <<~EOF builds/#{project_path}/app EOF end context 'when there is no ' do let(:classes_xml) { '' } it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'when there is a single ' do context 'with no lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'with a single line but the filename cannot be determined based on extracted source and worktree paths' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'with a single line' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2 } }) end end context 'with multiple lines and methods info' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) end end end context 'when there are multiple ' do context 'with the same filename but the filename cannot be determined based on extracted source and worktree paths' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'without a parent package' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns coverage information with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) end end context 'with the same filename and different lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with merged coverage, and with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0, 6 => 1, 7 => 1 } }) end end context 'with the same filename and lines' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with summed-up coverage, and with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 3, 2 => 1 } }) end end context 'with missing filename' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and ignores class with missing name' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) end end context 'with filename that cannot be determined based on extracted source and worktree paths' do let(:classes_xml) do <<~EOF EOF end it 'parses XML and ignores class with undetermined filename' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app/user.rb' => { 1 => 2, 2 => 0 } }) end end context 'with invalid line information' do let(:classes_xml) do <<~EOF EOF end it 'raises an error' do expect { parse_report }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidLineInformationError) end end end end context 'and has multiple sources that are not at the project root path' do let(:sources_xml) do <<~EOF builds/#{project_path}/app1/ builds/#{project_path}/app2/ EOF end context 'and a class filename is available under multiple extracted sources' do let(:paths) { ['app1/user.rb', 'app2/user.rb'] } let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns the files with the filename relative to project root' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app1/user.rb' => { 1 => 2 }, 'app2/user.rb' => { 2 => 3 } }) end end context 'and a class filename is available under one of the extracted sources' do let(:paths) { ['app1/member.rb', 'app2/user.rb', 'app2/pet.rb'] } let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns a single file with the filename relative to project root using the extracted source where it is first found under' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'app2/user.rb' => { 1 => 2 } }) end end context 'and a class filename is not found under any of the extracted sources' do let(:paths) { ['app1/member.rb', 'app2/pet.rb'] } let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end context 'and a class filename is not found under any of the extracted sources within the iteratable limit' do let(:paths) { ['app2/user.rb'] } let(:classes_xml) do <<~EOF EOF end before do stub_const("#{described_class}::MAX_SOURCES", 1) end it 'parses XML and returns empty coverage' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({}) end end end end shared_examples_for 'non-smart parsing' do let(:sources_xml) do <<~EOF builds/foo/bar/app EOF end let(:classes_xml) do <<~EOF EOF end it 'parses XML and returns filenames unchanged just as how they are found in the class node' do expect { parse_report }.not_to raise_error expect(coverage_report.files).to eq({ 'user.rb' => { 1 => 2 } }) end end context 'when project_path is not present' do let(:project_path) { nil } let(:paths) { ['app/user.rb'] } it_behaves_like 'non-smart parsing' end context 'when worktree_paths is not present' do let(:project_path) { 'foo/bar' } let(:paths) { nil } it_behaves_like 'non-smart parsing' end end context 'when data is not Cobertura style XML' do let(:cobertura) { { coverage: '12%' }.to_json } it 'raises an error' do expect { parse_report }.to raise_error(Gitlab::Ci::Parsers::Coverage::Cobertura::InvalidXMLError) end end end end