Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
0b30959da0
commit
fb7cc53653
21 changed files with 1165 additions and 890 deletions
|
@ -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>
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
147
lib/gitlab/ci/parsers/coverage/dom_parser.rb
Normal file
147
lib/gitlab/ci/parsers/coverage/dom_parser.rb
Normal 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
|
110
lib/gitlab/ci/parsers/coverage/sax_document.rb
Normal file
110
lib/gitlab/ci/parsers/coverage/sax_document.rb
Normal 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
|
|
@ -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', () => {
|
||||
|
|
|
@ -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
|
||||
|
|
10
spec/lib/gitlab/ci/parsers/coverage/dom_parser_spec.rb
Normal file
10
spec/lib/gitlab/ci/parsers/coverage/dom_parser_spec.rb
Normal 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
|
10
spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
Normal file
10
spec/lib/gitlab/ci/parsers/coverage/sax_document_spec.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue