Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-02-19 00:19:25 +00:00
parent 0b30959da0
commit fb7cc53653
21 changed files with 1165 additions and 890 deletions

View file

@ -1,5 +1,6 @@
<script>
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@ -44,6 +45,11 @@ export default {
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
title: s__('ReviewApp|Enable Review App'),
},
data() {
const modalInfoCopyId = uniqueId('enable-review-app-copy-string-');
return { modalInfoCopyId };
},
computed: {
modalInfoCopyStr() {
return `deploy_review:
@ -99,14 +105,14 @@ export default {
</gl-sprintf>
</p>
<div class="gl-display-flex align-items-start">
<pre class="gl-w-full" data-testid="enable-review-app-copy-string">
<pre :id="modalInfoCopyId" class="gl-w-full" data-testid="enable-review-app-copy-string">
{{ modalInfoCopyStr }} </pre
>
<modal-copy-button
:title="$options.modalInfo.copyToClipboardText"
:text="$options.modalInfo.copyString"
:modal-id="modalId"
css-classes="border-0"
:target="`#${modalInfoCopyId}`"
/>
</div>
</div>

View file

@ -0,0 +1,8 @@
---
name: use_cobertura_sax_parser
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79866
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352579
milestone: '14.9'
type: development
group: group::memory
default_enabled: false

View file

@ -1,12 +0,0 @@
- name: "GitLab self-monitoring" # The name of the feature to be deprecated
announcement_milestone: "14.8" # The milestone when this feature was first announced as deprecated.
announcement_date: "2022-02-22" # The date of the milestone release when this feature was first announced as deprecated. This should almost always be the 22nd of a month (YYYY-MM-22), unless you did an out of band blog post.
breaking_change: false # If this deprecation is a breaking change, set this value to true
reporter: abellucci # GitLab username of the person reporting the deprecation
body: | # Do not modify this line, instead modify the lines below.
GitLab self-monitoring gives administrators of self-hosted GitLab instances the tools to monitor the health of their instances. This feature is deprecated in GitLab 14.8, but is not scheduled for removal. For more information, see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitlab-self-monitoring).
# The following items are not published on the docs page, but may be used in the future.
stage: Monitor # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
tiers: [Core, Premium, Ultimate] # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/348909 # (optional) This is a link to the deprecation issue in GitLab
documentation_url: https://docs.gitlab.com/ee/administration/monitoring/gitlab_self_monitoring_project/ # (optional) This is a link to the current documentation page

View file

@ -4,14 +4,10 @@ group: Respond
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Self-monitoring project (DEPRECATED) **(FREE SELF)**
# Self-monitoring project **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32351) in GitLab 12.7 [with a flag](../../feature_flags.md) named `self_monitoring_project`. Disabled by default.
> - Generally available in GitLab 12.8. [Feature flag `self_monitoring_project`](https://gitlab.com/gitlab-org/gitlab/-/issues/198511) removed.
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/348909) in GitLab 14.8.
WARNING:
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/348909) in GitLab 14.8.
GitLab provides administrators insights into the health of their GitLab instance.

View file

@ -101,6 +101,7 @@ EE: true
- _Any_ contribution from a community member, no matter how small, **may** have
a changelog entry regardless of these guidelines if the contributor wants one.
- Any [GLEX experiment](experiment_guide/gitlab_experiment.md) changes **should not** have a changelog entry.
- An MR that includes only documentation changes **should not** have a changelog entry.
For more information, see
[how to handle changelog entries with feature flags](feature_flags/index.md#changelog).

View file

@ -266,6 +266,7 @@ requirements.
again.
1. [Performance guidelines](../merge_request_performance_guidelines.md) have been followed.
1. [Secure coding guidelines](https://gitlab.com/gitlab-com/gl-security/security-guidelines) have been followed.
1. [Application and rate limit guidelines](../merge_request_application_and_rate_limit_guidelines.md) have been followed.
1. [Documented](../documentation/index.md) in the `/doc` directory.
1. [Changelog entry added](../changelog.md), if necessary.
1. Reviewed by relevant reviewers, and all concerns are addressed for Availability, Regressions, and Security. Documentation reviews should take place as soon as possible, but they should not block a merge request.

View file

@ -0,0 +1,28 @@
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Application and rate limit guidelines
GitLab, like most large applications, enforces limits within certain features.
The absences of limits can affect security, performance, data, or could even
exhaust the allocated resources for the application.
Every new feature should have safe usage limits included in its implementation.
Limits are applicable for:
- System-level resource pools such as API requests, SSHD connections, database connections, storage, and so on.
- Domain-level objects such as CI minutes, groups, sign-in attempts, and so on.
## When limits are required
1. Limits are required if the absence of the limit matches severity 1 - 3 in the severity definitions for [limit-related bugs](https://about.gitlab.com/handbook/engineering/quality/issue-triage/#limit-related-bugs).
1. [GitLab application limits](../administration/instance_limits.md) documentation must be updated anytime limits are added, removed, or updated.
## Additional reading
- Existing [GitLab application limits](../administration/instance_limits.md)
- Product processes: [introducing application limits](https://about.gitlab.com/handbook/product/product-processes/#introducing-application-limits)
- Development docs: [guide for adding application limits](application_limits.md)

View file

@ -446,49 +446,6 @@ that accepts an upper limit of counting rows.
In some cases it's desired that badge counters are loaded asynchronously.
This can speed up the initial page load and give a better user experience overall.
## Application/misuse limits
Every new feature should have safe usage quotas introduced.
The quota should be optimised to a level that we consider the feature to
be performant and usable for the user, but **not limiting**.
**We want the features to be fully usable for the users.**
**However, we want to ensure that the feature continues to perform well if used at its limit**
**and it doesn't cause availability issues.**
Consider that it's always better to start with some kind of limitation,
instead of later introducing a breaking change that would result in some
workflows breaking.
The intent is to provide a safe usage pattern for the feature,
as our implementation decisions are optimised for the given data set.
Our feature limits should reflect the optimisations that we introduced.
The intent of quotas could be different:
1. We want to provide higher quotas for higher tiers of features:
we want to provide on GitLab.com more capabilities for different tiers,
1. We want to prevent misuse of the feature: someone accidentally creates
10000 deploy tokens, because of a broken API script,
1. We want to prevent abuse of the feature: someone purposely creates
a 10000 pipelines to take advantage of the system.
Examples:
1. Pipeline Schedules: It's very unlikely that user wants to create
more than 50 schedules.
In such cases it's rather expected that this is either misuse
or abuse of the feature. Lack of the upper limit can result
in service degradation as the system tries to process all schedules
assigned the project.
1. GitLab CI/CD includes: We started with the limit of maximum of 50 nested includes.
We understood that performance of the feature was acceptable at that level.
We received a request from the community that the limit is too small.
We had a time to understand the customer requirement, and implement an additional
fail-safe mechanism (time-based one) to increase the limit 100, and if needed increase it
further without negative impact on availability of the feature and GitLab.
## Usage of feature flags
Each feature that has performance critical elements or has a known performance deficiency

View file

@ -900,10 +900,6 @@ To align with this change, API calls to list external status checks will also re
**Planned removal milestone: 15.0 (2022-05-22)**
### GitLab self-monitoring
GitLab self-monitoring gives administrators of self-hosted GitLab instances the tools to monitor the health of their instances. This feature is deprecated in GitLab 14.8, but is not scheduled for removal. For more information, see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitlab-self-monitoring).
### GraphQL ID and GlobalID compatibility
WARNING:

View file

@ -115,6 +115,36 @@ When you add a member to a group, that member is also added to all subgroups.
Permission level is inherited from the group's parent. This model allows access to
subgroups if you have membership in one of its parents.
Subgroup members can:
1. Be [direct members](../../project/members/index.md#add-users-to-a-project) of the subgroup.
1. [Inherit membership](../../project/members/index.md#inherited-membership) of the subgroup from the subgroup's parent group.
1. Be a member of a group that was [shared with the subgroup's top-level group](../index.md#share-a-group-with-another-group).
```mermaid
flowchart RL
subgraph Group A
A(Direct member)
B{{Shared member}}
subgraph Subgroup A
H(1. Direct member)
C{{2. Inherited member}}
D{{Inherited member}}
E{{3. Shared member}}
end
A-->|Direct membership of Group A\nInherited membership of Subgroup A|C
end
subgraph Group C
G(Direct member)
end
subgraph Group B
F(Direct member)
end
F-->|Group B\nshared with\nGroup A|B
B-->|Inherited membership of Subgroup A|D
G-->|Group C shared with Subgroup A|E
```
Jobs for pipelines in subgroups can use [runners](../../../ci/runners/index.md) registered to the parent group(s).
This means secrets configured for the parent group are available to subgroup jobs.

View file

@ -93,6 +93,15 @@ When the pipeline finishes successfully, you can see your new cluster:
- In AWS: from the [EKS console](https://console.aws.amazon.com/eks/home) select **Amazon EKS > Clusters**.
- In GitLab: from your project's sidebar, select **Infrastructure > Kubernetes clusters**.
## Use your cluster
After you provision the cluster, it is connected to GitLab and is ready for deployments. To check the connection:
1. On the left sidebar, select **Infrastructure > Kubernetes clusters**.
1. In the list, view the **Connection status** column.
For more information about the capabilities of the connection, see [the GitLab agent for Kubernetes documentation](../index.md).
## Removing the cluster
A cleanup job is not included in your pipeline by default. To remove all created resources, you

View file

@ -10,6 +10,37 @@ Members are the users and groups who have access to your project.
Each member gets a role, which determines what they can do in the project.
Project members can:
1. Be [direct members](#add-users-to-a-project) of the project.
1. [Inherit membership](#inherited-membership) of the project from the project's group.
1. Be a member of a group that was [shared](share_project_with_groups.md) with the project.
1. Be a member of a group that was [shared with the project's group](../../group/index.md#share-a-group-with-another-group).
```mermaid
flowchart RL
subgraph Group A
A(Direct member)
B{{Shared member}}
subgraph Project A
H(1. Direct member)
C{{2. Inherited member}}
D{{4. Inherited member}}
E{{3. Shared member}}
end
A-->|Direct membership of Group A\nInherited membership of Project A|C
end
subgraph Group C
G(Direct member)
end
subgraph Group B
F(Direct member)
end
F-->|Group B\nshared with\nGroup A|B
B-->|Inherited membership of Project A|D
G-->|Group C shared with Project A|E
```
## Add users to a project
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/247208) in GitLab 13.11 from a form to a modal window [with a flag](../../feature_flags.md). Disabled by default.

View file

@ -8,141 +8,13 @@ module Gitlab
InvalidXMLError = Class.new(Gitlab::Ci::Parsers::ParserError)
InvalidLineInformationError = Class.new(Gitlab::Ci::Parsers::ParserError)
GO_SOURCE_PATTERN = '/usr/local/go/src'
MAX_SOURCES = 100
def parse!(xml_data, coverage_report, project_path: nil, worktree_paths: nil)
root = Hash.from_xml(xml_data)
context = {
project_path: project_path,
paths: worktree_paths&.to_set,
sources: []
}
parse_all(root, coverage_report, context)
rescue Nokogiri::XML::SyntaxError
raise InvalidXMLError, "XML parsing failed"
end
private
def parse_all(root, coverage_report, context)
return unless root.present?
root.each do |key, value|
parse_node(key, value, coverage_report, context)
if Feature.enabled?(:use_cobertura_sax_parser, default_enabled: :yaml)
Nokogiri::XML::SAX::Parser.new(SaxDocument.new(coverage_report, project_path, worktree_paths)).parse(xml_data)
else
DomParser.new.parse(xml_data, coverage_report, project_path, worktree_paths)
end
end
def parse_node(key, value, coverage_report, context)
if key == 'sources' && value && value['source'].present?
parse_sources(value['source'], context)
elsif key == 'package'
Array.wrap(value).each do |item|
parse_package(item, coverage_report, context)
end
elsif key == 'class'
# This means the cobertura XML does not have classes within package nodes.
# This is possible in some cases like in simple JS project structures
# running Jest.
Array.wrap(value).each do |item|
parse_class(item, coverage_report, context)
end
elsif value.is_a?(Hash)
parse_all(value, coverage_report, context)
elsif value.is_a?(Array)
value.each do |item|
parse_all(item, coverage_report, context)
end
end
end
def parse_sources(sources, context)
return unless context[:project_path] && context[:paths]
sources = Array.wrap(sources)
# TODO: Go cobertura has a different format with how their packages
# are included in the filename. So we can't rely on the sources.
# We'll deal with this later.
return if sources.include?(GO_SOURCE_PATTERN)
sources.each do |source|
source = build_source_path(source, context)
context[:sources] << source if source.present?
end
end
def build_source_path(source, context)
# | raw source | extracted |
# |-----------------------------|------------|
# | /builds/foo/test/SampleLib/ | SampleLib/ |
# | /builds/foo/test/something | something |
# | /builds/foo/test/ | nil |
# | /builds/foo/test | nil |
source.split("#{context[:project_path]}/", 2)[1]
end
def parse_package(package, coverage_report, context)
classes = package.dig('classes', 'class')
return unless classes.present?
matched_filenames = Array.wrap(classes).map do |item|
parse_class(item, coverage_report, context)
end
# Remove these filenames from the paths to avoid conflict
# with other packages that may contain the same class filenames
remove_matched_filenames(matched_filenames, context)
end
def remove_matched_filenames(filenames, context)
return unless context[:paths]
filenames.each { |f| context[:paths].delete(f) }
end
def parse_class(file, coverage_report, context)
return unless file["filename"].present? && file["lines"].present?
parsed_lines = parse_lines(file["lines"])
filename = determine_filename(file["filename"], context)
coverage_report.add_file(filename, Hash[parsed_lines]) if filename
filename
end
def parse_lines(lines)
line_array = Array.wrap(lines["line"])
line_array.map do |line|
# Using `Integer()` here to raise exception on invalid values
[Integer(line["number"]), Integer(line["hits"])]
end
rescue StandardError
raise InvalidLineInformationError, "Line information had invalid values"
end
def determine_filename(filename, context)
return filename unless context[:sources].any?
full_filename = nil
context[:sources].each_with_index do |source, index|
break if index >= MAX_SOURCES
break if full_filename = check_source(source, filename, context)
end
full_filename
end
def check_source(source, filename, context)
full_path = File.join(source, filename)
return full_path if context[:paths].include?(full_path)
end
end
end
end

View file

@ -0,0 +1,147 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Coverage
class DomParser
GO_SOURCE_PATTERN = '/usr/local/go/src'
MAX_SOURCES = 100
def parse(xml_data, coverage_report, project_path, worktree_paths)
root = Hash.from_xml(xml_data)
context = {
project_path: project_path,
paths: worktree_paths&.to_set,
sources: []
}
parse_all(root, coverage_report, context)
rescue Nokogiri::XML::SyntaxError
raise Cobertura::InvalidXMLError, "XML parsing failed"
end
private
def parse_all(root, coverage_report, context)
return unless root.present?
root.each do |key, value|
parse_node(key, value, coverage_report, context)
end
end
def parse_node(key, value, coverage_report, context)
if key == 'sources' && value && value['source'].present?
parse_sources(value['source'], context)
elsif key == 'package'
Array.wrap(value).each do |item|
parse_package(item, coverage_report, context)
end
elsif key == 'class'
# This means the cobertura XML does not have classes within package nodes.
# This is possible in some cases like in simple JS project structures
# running Jest.
Array.wrap(value).each do |item|
parse_class(item, coverage_report, context)
end
elsif value.is_a?(Hash)
parse_all(value, coverage_report, context)
elsif value.is_a?(Array)
value.each do |item|
parse_all(item, coverage_report, context)
end
end
end
def parse_sources(sources, context)
return unless context[:project_path] && context[:paths]
sources = Array.wrap(sources)
# TODO: Go cobertura has a different format with how their packages
# are included in the filename. So we can't rely on the sources.
# We'll deal with this later.
return if sources.include?(GO_SOURCE_PATTERN)
sources.each do |source|
source = build_source_path(source, context)
context[:sources] << source if source.present?
end
end
def build_source_path(source, context)
# | raw source | extracted |
# |-----------------------------|------------|
# | /builds/foo/test/SampleLib/ | SampleLib/ |
# | /builds/foo/test/something | something |
# | /builds/foo/test/ | nil |
# | /builds/foo/test | nil |
source.split("#{context[:project_path]}/", 2)[1]
end
def parse_package(package, coverage_report, context)
classes = package.dig('classes', 'class')
return unless classes.present?
matched_filenames = Array.wrap(classes).map do |item|
parse_class(item, coverage_report, context)
end
# Remove these filenames from the paths to avoid conflict
# with other packages that may contain the same class filenames
remove_matched_filenames(matched_filenames, context)
end
def remove_matched_filenames(filenames, context)
return unless context[:paths]
filenames.each { |f| context[:paths].delete(f) }
end
def parse_class(file, coverage_report, context)
return unless file["filename"].present? && file["lines"].present?
parsed_lines = parse_lines(file["lines"])
filename = determine_filename(file["filename"], context)
coverage_report.add_file(filename, Hash[parsed_lines]) if filename
filename
end
def parse_lines(lines)
line_array = Array.wrap(lines["line"])
line_array.map do |line|
# Using `Integer()` here to raise exception on invalid values
[Integer(line["number"]), Integer(line["hits"])]
end
rescue StandardError
raise Cobertura::InvalidLineInformationError, "Line information had invalid values"
end
def determine_filename(filename, context)
return filename unless context[:sources].any?
full_filename = nil
context[:sources].each_with_index do |source, index|
break if index >= MAX_SOURCES
break if full_filename = check_source(source, filename, context)
end
full_filename
end
def check_source(source, filename, context)
full_path = File.join(source, filename)
return full_path if context[:paths].include?(full_path)
end
end
end
end
end
end

View file

@ -0,0 +1,110 @@
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Coverage
class SaxDocument < Nokogiri::XML::SAX::Document
GO_SOURCE_PATTERN = '/usr/local/go/src'
MAX_SOURCES = 100
def initialize(coverage_report, project_path, worktree_paths)
@coverage_report = coverage_report
@project_path = project_path
@paths = worktree_paths&.to_set
@matched_filenames = []
@parsed_lines = []
@sources = []
end
def error(error)
raise Cobertura::InvalidXMLError, "XML parsing failed with error: #{error}"
end
def start_element(node_name, attrs = [])
return unless node_name
self.node_name = node_name
node_attrs = Hash[attrs]
if node_name == 'class' && node_attrs["filename"].present?
self.filename = determine_filename(node_attrs["filename"])
self.matched_filenames << filename if filename
elsif node_name == 'line'
self.parsed_lines << parse_line(node_attrs)
end
end
def characters(node_content)
if node_name == 'source'
parse_source(node_content)
end
end
def end_element(node_name)
if node_name == "package"
remove_matched_filenames
elsif node_name == "class" && filename && parsed_lines.present?
coverage_report.add_file(filename, Hash[parsed_lines])
self.filename = nil
self.parsed_lines = []
end
end
private
attr_accessor :coverage_report, :project_path, :paths, :sources, :node_name, :filename, :parsed_lines, :matched_filenames
def parse_line(line)
[Integer(line["number"]), Integer(line["hits"])]
rescue StandardError
raise Cobertura::InvalidLineInformationError, "Line information had invalid values"
end
def parse_source(node)
return unless project_path && paths && !node.include?(GO_SOURCE_PATTERN)
source = build_source_path(node)
self.sources << source if source.present?
end
def build_source_path(node)
# | raw source | extracted |
# |-----------------------------|------------|
# | /builds/foo/test/SampleLib/ | SampleLib/ |
# | /builds/foo/test/something | something |
# | /builds/foo/test/ | nil |
# | /builds/foo/test | nil |
node.split("#{project_path}/", 2)[1]
end
def remove_matched_filenames
return unless paths
matched_filenames.each { |f| paths.delete(f) }
end
def determine_filename(filename)
return filename unless sources.any?
full_filename = nil
sources.each_with_index do |source, index|
break if index >= MAX_SOURCES
break if full_filename = check_source(source, filename)
end
full_filename
end
def check_source(source, filename)
full_path = File.join(source, filename)
return full_path if paths.include?(full_path)
end
end
end
end
end
end

View file

@ -4,10 +4,17 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EnableReviewAppButton from '~/environments/components/enable_review_app_modal.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
// hardcode uniqueId for determinism
jest.mock('lodash/uniqueId', () => (x) => `${x}77`);
const EXPECTED_COPY_PRE_ID = 'enable-review-app-copy-string-77';
describe('Enable Review App Button', () => {
let wrapper;
let modal;
const findCopyString = () => wrapper.find(`#${EXPECTED_COPY_PRE_ID}`);
afterEach(() => {
wrapper.destroy();
});
@ -30,12 +37,15 @@ describe('Enable Review App Button', () => {
});
it('renders the defaultBranchName copy', () => {
const findCopyString = () => wrapper.findByTestId('enable-review-app-copy-string');
expect(findCopyString().text()).toContain('- main');
});
it('renders the copyToClipboard button', () => {
expect(wrapper.findComponent(ModalCopyButton).exists()).toBe(true);
expect(wrapper.findComponent(ModalCopyButton).props()).toMatchObject({
modalId: 'fake-id',
target: `#${EXPECTED_COPY_PRE_ID}`,
title: 'Copy snippet text',
});
});
it('emits change events from the modal up', () => {

View file

@ -1,700 +1,44 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'spec_helper'
RSpec.describe Gitlab::Ci::Parsers::Coverage::Cobertura do
describe '#parse!' do
subject(:parse_report) { described_class.new.parse!(cobertura, coverage_report, project_path: project_path, worktree_paths: paths) }
let(:xml_data) { double }
let(:coverage_report) { double }
let(:project_path) { double }
let(:paths) { double }
let(:coverage_report) { Gitlab::Ci::Reports::CoverageReports.new }
let(:project_path) { 'foo/bar' }
let(:paths) { ['app/user.rb'] }
subject(:parse_report) { described_class.new.parse!(xml_data, coverage_report, project_path: project_path, worktree_paths: paths) }
let(:cobertura) do
<<~EOF
<coverage>
#{sources_xml}
#{classes_xml}
</coverage>
EOF
end
context 'when use_cobertura_sax_parser feature flag is enabled' do
before do
stub_feature_flags(use_cobertura_sax_parser: true)
context 'when data is Cobertura style XML' do
shared_examples_for 'ignoring sources, project_path, and worktree_paths' do
context 'when there is no <class>' 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 <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes></package></packages>
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 <class>' do
context 'without a package parent' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="foo.rb"><methods/><lines>
<line number="6" hits="1"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="1"/>
<line number="2" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'raises an error' do
expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
end
end
end
end
context 'when there is no <sources>' do
let(:sources_xml) { '' }
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'when there is an empty <sources>' do
let(:sources_xml) { '<sources />' }
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'when there is a <sources>' 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
<sources>
<source>/usr/local/go/src</source>
</sources>
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
<sources>
<source>/usr/local/go/src</source>
<source>/go/src</source>
</sources>
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
<sources>
<source>builds/#{project_path}</source>
</sources>
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
<sources>
<source>builds/#{project_path}/</source>
<source>builds/somewhere/#{project_path}</source>
</sources>
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
<sources>
<source>builds/#{project_path}/app</source>
</sources>
EOF
end
context 'when there is no <class>' 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 <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="member.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes></package></packages>
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 <class>' 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
<packages><package name="app"><classes>
<class filename="member.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="1"/>
<line number="2" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
EOF
end
it 'raises an error' do
expect { parse_report }.to raise_error(described_class::InvalidLineInformationError)
end
end
end
end
context 'and has multiple sources that are not at the project root path' do
let(:sources_xml) do
<<~EOF
<sources>
<source>builds/#{project_path}/app1/</source>
<source>builds/#{project_path}/app2/</source>
</sources>
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
<package name="app1">
<classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes>
</package>
<package name="app2">
<classes>
<class filename="user.rb"><lines>
<line number="2" hits="3"/>
</lines></class>
</classes>
</package>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="record.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<sources>
<source>builds/foo/bar/app</source>
</sources>
EOF
end
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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'
allow_next_instance_of(Nokogiri::XML::SAX::Parser) do |document|
allow(document).to receive(:parse)
end
end
context 'when data is not Cobertura style XML' do
let(:cobertura) { { coverage: '12%' }.to_json }
it 'uses Sax parser' do
expect(Gitlab::Ci::Parsers::Coverage::SaxDocument).to receive(:new)
it 'raises an error' do
expect { parse_report }.to raise_error(described_class::InvalidXMLError)
parse_report
end
end
context 'when use_cobertura_sax_parser feature flag is disabled' do
before do
stub_feature_flags(use_cobertura_sax_parser: false)
allow_next_instance_of(Gitlab::Ci::Parsers::Coverage::DomParser) do |parser|
allow(parser).to receive(:parse)
end
end
it 'uses Dom parser' do
expect(Gitlab::Ci::Parsers::Coverage::DomParser).to receive(:new)
parse_report
end
end
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/shared_examples/lib/gitlab/ci/parsers/coverage/cobertura_xml_shared_examples'
RSpec.describe Gitlab::Ci::Parsers::Coverage::DomParser do
subject(:parse_report) { described_class.new.parse(cobertura, coverage_report, project_path, paths) }
include_examples 'parse cobertura xml'
end

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'support/shared_examples/lib/gitlab/ci/parsers/coverage/cobertura_xml_shared_examples'
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) }
include_examples 'parse cobertura xml'
end

View file

@ -0,0 +1,721 @@
# frozen_string_literal: true
RSpec.shared_examples 'parse cobertura xml' do
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
<coverage>
#{sources_xml}
#{classes_xml}
</coverage>
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 <class>' 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 <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="app.rb"></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages>
<class filename="app.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes></package></packages>
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
<coverage>
<packages><package name="app1"><classes>
<class filename="app1.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
<packages><package name="app2"><classes>
<class filename="app2.rb"><lines>
<line number="11" hits="3"/>
</lines></class>
</classes></package></packages>
</coverage>
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 <class>' do
context 'without a package parent' do
let(:classes_xml) do
<<~EOF
<packages>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="foo.rb"><methods/><lines>
<line number="6" hits="1"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="1"/>
<line number="2" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="app.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="app.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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 <sources>' do
let(:sources_xml) { '' }
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'when there is an empty <sources>' do
let(:sources_xml) { '<sources />' }
it_behaves_like 'ignoring sources, project_path, and worktree_paths'
end
context 'when there is a <sources>' 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
<sources>
<source>/usr/local/go/src</source>
</sources>
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
<sources>
<source>/usr/local/go/src</source>
<source>/go/src</source>
</sources>
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
<sources>
<source>builds/#{project_path}</source>
</sources>
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
<sources>
<source>builds/#{project_path}/</source>
<source>builds/somewhere/#{project_path}</source>
</sources>
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
<sources>
<source>builds/#{project_path}/app</source>
</sources>
EOF
end
context 'when there is no <class>' 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 <class>' do
context 'with no lines' do
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="member.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
</classes></package></packages>
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 <class>' 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
<packages><package name="app"><classes>
<class filename="member.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="1"/>
<line number="2" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="member.rb"><methods/><lines>
<line number="6" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><methods/><lines>
<line number="1" hits="2"/>
<line number="2" hits="0"/>
</lines></class>
<class filename="user.rb"><methods/><lines>
<line null="test" hits="1"/>
<line number="7" hits="1"/>
</lines></class>
</classes></package></packages>
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
<sources>
<source>builds/#{project_path}/app1/</source>
<source>builds/#{project_path}/app2/</source>
</sources>
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
<package name="app1">
<classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes>
</package>
<package name="app2">
<classes>
<class filename="user.rb"><lines>
<line number="2" hits="3"/>
</lines></class>
</classes>
</package>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<packages><package name="app"><classes>
<class filename="record.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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
<sources>
<source>builds/foo/bar/app</source>
</sources>
EOF
end
let(:classes_xml) do
<<~EOF
<packages><package name="app"><classes>
<class filename="user.rb"><lines>
<line number="1" hits="2"/>
</lines></class>
</classes></package></packages>
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

View file

@ -90,7 +90,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
it_behaves_like '.roots'
it 'make recursive queries' do
it 'makes recursive queries' do
expect { described_class.where(id: [nested_group_1]).roots.load }.to make_queries_matching(/WITH RECURSIVE/)
end
end
@ -159,7 +159,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
it_behaves_like '.self_and_ancestors'
it 'make recursive queries' do
it 'makes recursive queries' do
expect { described_class.where(id: [nested_group_1]).self_and_ancestors.load }.to make_queries_matching(/WITH RECURSIVE/)
end
end
@ -204,7 +204,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
it_behaves_like '.self_and_ancestor_ids'
it 'make recursive queries' do
it 'makes recursive queries' do
expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.not_to make_queries_matching(/WITH RECURSIVE/)
end
end
@ -216,7 +216,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
it_behaves_like '.self_and_ancestor_ids'
it 'make recursive queries' do
it 'makes recursive queries' do
expect { described_class.where(id: [nested_group_1]).self_and_ancestor_ids.load }.to make_queries_matching(/WITH RECURSIVE/)
end
end
@ -340,7 +340,7 @@ RSpec.shared_examples 'namespace traversal scopes' do
it_behaves_like '.self_and_hierarchy'
it 'make recursive queries' do
it 'makes recursive queries' do
base_groups = Group.where(id: nested_group_1)
expect { base_groups.self_and_hierarchy.load }.to make_queries_matching(/WITH RECURSIVE/)
end