Commit 27a207fc authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'mc/feature/custom-metrics' into 'master'

Add 'Metrics' report type to merge requests

See merge request gitlab-org/gitlab-ee!10452
parents f8687a06 1e7f1a0a
......@@ -98,20 +98,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def test_reports
result = @merge_request.compare_test_reports
case result[:status]
when :parsing
Gitlab::PollingInterval.set_header(response, interval: 3000)
render json: '', status: :no_content
when :parsed
render json: result[:data].to_json, status: :ok
when :error
render json: { status_reason: result[:status_reason] }, status: :bad_request
else
render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end
reports_response(@merge_request.compare_test_reports)
end
def edit
......@@ -353,6 +340,21 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
end
def reports_response(report_comparison)
case report_comparison[:status]
when :parsing
::Gitlab::PollingInterval.set_header(response, interval: 3000)
render json: '', status: :no_content
when :parsed
render json: report_comparison[:data].to_json, status: :ok
when :error
render json: { status_reason: report_comparison[:status_reason] }, status: :bad_request
else
render json: { status_reason: 'Unknown error' }, status: :internal_server_error
end
end
end
Projects::MergeRequestsController.prepend(EE::Projects::MergeRequestsController)
......@@ -104,8 +104,8 @@ module Ci
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
scope :with_test_reports, ->() do
with_existing_job_artifacts(Ci::JobArtifact.test_reports)
scope :with_reports, ->(reports_scope) do
with_existing_job_artifacts(reports_scope)
.eager_load_job_artifacts
end
......
......@@ -21,7 +21,8 @@ module Ci
container_scanning: 'gl-container-scanning-report.json',
dast: 'gl-dast-report.json',
license_management: 'gl-license-management-report.json',
performance: 'performance.json'
performance: 'performance.json',
metrics: 'metrics.txt'
}.freeze
TYPE_AND_FORMAT_PAIRS = {
......@@ -29,6 +30,7 @@ module Ci
metadata: :gzip,
trace: :raw,
junit: :gzip,
metrics: :gzip,
# All these file formats use `raw` as we need to store them uncompressed
# for Frontend to fetch the files and do analysis
......@@ -88,7 +90,8 @@ module Ci
dast: 8, ## EE-specific
codequality: 9, ## EE-specific
license_management: 10, ## EE-specific
performance: 11 ## EE-specific
performance: 11, ## EE-specific
metrics: 12 ## EE-specific
}
enum file_format: {
......
......@@ -210,6 +210,10 @@ module Ci
where(source: branch_pipeline_sources).where(ref: ref, tag: false)
end
scope :with_reports, -> (reports_scope) do
where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
end
# Returns the pipelines in descending order (= newest first), optionally
# limited to a number of references.
#
......@@ -689,13 +693,13 @@ module Ci
@latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
end
def has_test_reports?
complete? && builds.latest.with_test_reports.any?
def has_reports?(reports_scope)
complete? && builds.latest.with_reports(reports_scope).exists?
end
def test_reports
Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
builds.latest.with_test_reports.each do |build|
builds.latest.with_reports(Ci::JobArtifact.test_reports).each do |build|
build.collect_test_reports!(test_reports)
end
end
......
......@@ -1154,7 +1154,7 @@ class MergeRequest < ApplicationRecord
end
def has_test_reports?
actual_head_pipeline&.has_test_reports?
actual_head_pipeline&.has_reports?(Ci::JobArtifact.test_reports)
end
def predefined_variables
......
......@@ -42,6 +42,14 @@ module EE
render_approvals_json
end
def metrics_reports
unless ::Feature.enabled?(:metrics_reports, project)
return render json: '', status: :bad_request
end
reports_response(merge_request.compare_metrics_reports)
end
protected
# rubocop:disable Gitlab/ModuleWithInstanceVariables
......
......@@ -23,16 +23,6 @@ module EE
has_many :sourced_pipelines,
class_name: ::Ci::Sources::Pipeline,
foreign_key: :source_job_id
scope :with_security_reports, -> do
with_existing_job_artifacts(::Ci::JobArtifact.security_reports)
.eager_load_job_artifacts
end
scope :with_license_management_reports, -> do
with_existing_job_artifacts(::Ci::JobArtifact.license_management_reports)
.eager_load_job_artifacts
end
end
def shared_runners_minutes_limit_enabled?
......@@ -90,6 +80,16 @@ module EE
license_management_report
end
def collect_metrics_reports!(metrics_report)
each_report(::Ci::JobArtifact::METRICS_REPORT_FILE_TYPES) do |file_type, blob|
next unless project.feature_available?(:metrics_reports)
::Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, metrics_report)
end
metrics_report
end
private
def name_in?(names)
......
......@@ -13,6 +13,7 @@ module EE
SECURITY_REPORT_FILE_TYPES = %w[sast dependency_scanning container_scanning dast].freeze
LICENSE_MANAGEMENT_REPORT_FILE_TYPES = %w[license_management].freeze
METRICS_REPORT_FILE_TYPES = %w[metrics].freeze
scope :not_expired, -> { where('expire_at IS NULL OR expire_at > ?', Time.current) }
scope :geo_syncable, -> { with_files_stored_locally.not_expired }
......@@ -25,6 +26,10 @@ module EE
scope :license_management_reports, -> do
with_file_types(LICENSE_MANAGEMENT_REPORT_FILE_TYPES)
end
scope :metrics_reports, -> do
with_file_types(METRICS_REPORT_FILE_TYPES)
end
end
def log_geo_deleted_event
......
......@@ -33,11 +33,6 @@ module EE
joins(:artifacts).where(ci_builds: { name: %w[sast dependency_scanning sast:container container_scanning dast] })
end
# The new `reports:` syntax reports
scope :with_security_reports, -> do
where('EXISTS (?)', ::Ci::Build.latest.with_security_reports.where('ci_pipelines.id=ci_builds.commit_id').select(1))
end
scope :with_vulnerabilities, -> do
where('EXISTS (?)', ::Vulnerabilities::OccurrencePipeline.where('ci_pipelines.id=vulnerability_occurrence_pipelines.pipeline_id').select(1))
end
......@@ -51,7 +46,8 @@ module EE
container_scanning: %i[container_scanning sast_container],
dast: %i[dast],
performance: %i[merge_request_performance_metrics],
license_management: %i[license_management]
license_management: %i[license_management],
metrics: %i[metrics_reports]
}.freeze
# Deprecated, to be removed in 12.0
......@@ -91,7 +87,7 @@ module EE
state_machine :status do
after_transition any => ::Ci::Pipeline::COMPLETED_STATUSES.map(&:to_sym) do |pipeline|
next unless pipeline.has_security_reports? && pipeline.default_branch?
next unless pipeline.has_reports?(::Ci::JobArtifact.security_reports) && pipeline.default_branch?
pipeline.run_after_commit do
StoreSecurityReportsWorker.perform_async(pipeline.id)
......@@ -149,30 +145,30 @@ module EE
any_report_artifact_for_type(:license_management)
end
def has_security_reports?
complete? && builds.latest.with_security_reports.any?
end
def security_reports
::Gitlab::Ci::Reports::Security::Reports.new.tap do |security_reports|
builds.latest.with_security_reports.each do |build|
builds.latest.with_reports(::Ci::JobArtifact.security_reports).each do |build|
build.collect_security_reports!(security_reports)
end
end
end
def has_license_management_reports?
complete? && builds.latest.with_license_management_reports.any?
end
def license_management_report
::Gitlab::Ci::Reports::LicenseManagement::Report.new.tap do |license_management_report|
builds.latest.with_license_management_reports.each do |build|
builds.latest.with_reports(::Ci::JobArtifact.license_management_reports).each do |build|
build.collect_license_management_reports!(license_management_report)
end
end
end
def metrics_report
::Gitlab::Ci::Reports::Metrics::Report.new.tap do |metrics_report|
builds.latest.with_reports(::Ci::JobArtifact.metrics_reports).each do |build|
build.collect_metrics_reports!(metrics_report)
end
end
end
##
# Check if it's a merge request pipeline with the HEAD of source and target branches
# TODO: Make `Ci::Pipeline#latest?` compatible with merge request pipelines and remove this method.
......
......@@ -122,7 +122,7 @@ module EE
end
def has_license_management_reports?
actual_head_pipeline&.has_license_management_reports?
actual_head_pipeline&.has_reports?(::Ci::JobArtifact.license_management_reports)
end
def compare_license_management_reports
......@@ -133,6 +133,18 @@ module EE
compare_reports(::Ci::CompareLicenseManagementReportsService)
end
def has_metrics_reports?
actual_head_pipeline&.has_reports?(::Ci::JobArtifact.metrics_reports)
end
def compare_metrics_reports
unless has_metrics_reports?
return { status: :error, status_reason: 'This merge request does not have metrics reports' }
end
compare_reports(::Ci::CompareMetricsReportsService)
end
def sync_code_owners_with_approvers
return if merged?
......
......@@ -145,7 +145,7 @@ module EE
end
def latest_pipeline_with_security_reports
ci_pipelines.newest_first(ref: default_branch).with_security_reports.first ||
ci_pipelines.newest_first(ref: default_branch).with_reports(::Ci::JobArtifact.security_reports).first ||
ci_pipelines.newest_first(ref: default_branch).with_legacy_security_reports.first
end
......
......@@ -78,6 +78,7 @@ class License < ApplicationRecord
design_management
operations_dashboard
dependency_proxy
metrics_reports
]
EEP_FEATURES.freeze
......
......@@ -84,6 +84,10 @@ module EE
end
end
expose :metrics_reports_path, if: -> (mr, _) { mr.has_metrics_reports? } do |merge_request|
metrics_reports_project_merge_request_path(merge_request.project, merge_request, format: :json)
end
expose :sast_container, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:container_scanning) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:container_scanning)
......
# frozen_string_literal: true
class MetricsReportMetricEntity < Grape::Entity
expose :name
expose :value
expose :previous_value, if: -> (metric, _) { metric.previous_value != metric.value }
end
# frozen_string_literal: true
class MetricsReportsComparerEntity < Grape::Entity
expose :new_metrics, using: MetricsReportMetricEntity
expose :existing_metrics, using: MetricsReportMetricEntity
expose :removed_metrics, using: MetricsReportMetricEntity
end
# frozen_string_literal: true
class MetricsReportsComparerSerializer < BaseSerializer
entity MetricsReportsComparerEntity
end
# frozen_string_literal: true
module Ci
class CompareMetricsReportsService < ::Ci::CompareReportsBaseService
def comparer_class
Gitlab::Ci::Reports::Metrics::ReportsComparer
end
def serializer_class
MetricsReportsComparerSerializer
end
def get_report(pipeline)
pipeline&.metrics_report
end
end
end
---
title: Add 'Metrics' job artifact report type.
merge_request: 10452
author:
type: added
......@@ -34,6 +34,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
resources :merge_requests, only: [], constraints: { id: /\d+/ } do
member do
get :metrics_reports
end
end
resource :insights, only: [:show] do
collection do
post :query
......
......@@ -13,7 +13,8 @@ module EE
dependency_scanning: ::Gitlab::Ci::Parsers::Security::DependencyScanning,
container_scanning: ::Gitlab::Ci::Parsers::Security::ContainerScanning,
dast: ::Gitlab::Ci::Parsers::Security::Dast,
sast: ::Gitlab::Ci::Parsers::Security::Sast
sast: ::Gitlab::Ci::Parsers::Security::Sast,
metrics: ::Gitlab::Ci::Parsers::Metrics::Generic
})
end
end
......
# frozen_string_literal: true
module Gitlab
module Ci
module Parsers
module Metrics
class Generic
MetricsParserError = Class.new(::Gitlab::Ci::Parsers::ParserError)
def parse!(string_data, metrics_report)
string_data.each_line.lazy.reject(&:blank?).each { |line| parse_line(line, metrics_report) }
end
private
def parse_line(line, metrics_report)
name, *metric_values = line.gsub(/#.*$/, '').shellsplit
return if name.blank? || metric_values.empty?
metrics_report.add_metric(name, metric_values.first)
rescue => e
Gitlab::Sentry.track_exception(e)
raise MetricsParserError, "Metrics parsing failed"
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Metrics
class Report
attr_reader :metrics
def initialize
@metrics = {}
end
def add_metric(key, value)
@metrics[key] = value
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Ci
module Reports
module Metrics
class ReportsComparer
include Gitlab::Utils::StrongMemoize
attr_reader :base_report, :head_report
ComparedMetric = Struct.new(:name, :value, :previous_value)
def initialize(base_report, head_report)
@base_report = base_report || ::Gitlab::Ci::Reports::Metrics::Report.new
@head_report = head_report
end
def new_metrics
strong_memoize(:new_metrics) do
head_report.metrics.map do |key, value|
ComparedMetric.new(key, value) unless base_report.metrics.include?(key)
end.compact
end
end
def existing_metrics
strong_memoize(:existing_metrics) do
base_report.metrics.map do |key, value|
new_value = head_report.metrics[key]
ComparedMetric.new(key, new_value, value) if new_value
end.compact
end
end
def removed_metrics
strong_memoize(:removed_metrics) do
base_report.metrics.map do |key, value|
ComparedMetric.new(key, value) unless head_report.metrics.include?(key)
end.compact
end
end
end
end
end
end
end
......@@ -401,4 +401,104 @@ describe Projects::MergeRequestsController do
it_behaves_like 'approvals'
end
end
describe 'GET #metrics_reports' do
let(:params) do
{
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid
}
end
subject { get :metrics_reports, params: params, format: :json }
context 'when feature is enabled' do
let(:merge_request) { create(:ee_merge_request, :with_metrics_reports, source_project: project, author: create(:user)) }
before do
allow_any_instance_of(::MergeRequest).to receive(:compare_reports)
.with(::Ci::CompareMetricsReportsService).and_return(comparison_status)
end
context 'when comparison is being processed' do
let(:comparison_status) { { status: :parsing } }
it 'sends polling interval' do
expect(::Gitlab::PollingInterval).to receive(:set_header)
subject
end
it 'returns 204 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:no_content)
end
end
context 'when comparison is done' do
let(:comparison_status) { { status: :parsed, data: { summary: 1 } } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 200 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to eq({ 'summary' => 1 })
end
end
context 'when user created corrupted test reports' do
let(:comparison_status) { { status: :error, status_reason: 'Failed to parse test reports' } }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 400 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to eq({ 'status_reason' => 'Failed to parse test reports' })
end
end
context 'when something went wrong on our system' do
let(:comparison_status) { {} }
it 'does not send polling interval' do
expect(::Gitlab::PollingInterval).not_to receive(:set_header)
subject
end
it 'returns 500 HTTP status' do
subject
expect(response).to have_gitlab_http_status(:internal_server_error)
expect(json_response).to eq({ 'status_reason' => 'Unknown error' })
end
end
end
context 'when feature is not enabled' do
before do
stub_feature_flags(metrics_reports: false)
end
it 'returns no content' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
......@@ -27,17 +27,29 @@ FactoryBot.define do
end
end
end
end
trait :license_management_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :license_management_feature_branch, job: build)
trait :metrics do
after(:build) do |build|
build.job_artifacts << build(:ee_ci_job_artifact, :metrics, job: build)
end
end
trait :metrics_alternate do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :metrics_alternate, job: build)
end
end
end
trait :corrupted_license_management_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_license_management_report, job: build)
trait :license_management_feature_branch do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :license_management_feature_branch, job: build)
end
end
trait :corrupted_license_management_report do
after(:build) do |build|
build.job_artifacts << create(:ee_ci_job_artifact, :corrupted_license_management_report, job: build)
end
end
end
end
......@@ -131,5 +131,25 @@ FactoryBot.define do
Rails.root.join('spec/fixtures/security-reports/master/gl-dast-report.json'), 'text/plain')
end
end
trait :metrics do
file_format :gzip
file_type :metrics
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/metrics.txt.gz'), 'application/x-gzip')
end
end
trait :metrics_alternate do
file_format :gzip
file_type :metrics
after(:build) do |artifact, _|
artifact.file = fixture_file_upload(
Rails.root.join('ee/spec/fixtures/alternate_metrics.txt.gz'), 'application/x-gzip')
end
end
end
end
......@@ -30,5 +30,21 @@ FactoryBot.define do
pipeline.builds << build(:ee_ci_build, :corrupted_license_management_report, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_metrics_report do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :metrics, pipeline: pipeline, project: pipeline.project)
end
end
trait :with_metrics_alternate_report do
status :success
after(:build) do |pipeline, evaluator|
pipeline.builds << build(:ee_ci_build, :metrics_alternate, pipeline: pipeline, project: pipeline.project)
end
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :ci_reports_metrics_report, class: ::Gitlab::Ci::Reports::Metrics::Report do
trait :base_metrics do
after(:build) do |report, _|
report.add_metric('metric_name', 'metric_value')
report.add_metric('second_metric_name', 'metric_value')
end
end
trait :head_metrics do
after(:build) do |report, _|
report.add_metric('metric_name', 'metric_value')
report.add_metric('extra_metric_name', 'metric_value')
end
end
end
end
......@@ -38,5 +38,17 @@ FactoryBot.define do
sha: merge_request.diff_head_sha)
end
end
trait :with_metrics_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
:ee_ci_pipeline,
:success,
:with_metrics_report,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
end
end
end
first_metric 12.47
second_metric hello
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Parsers::Metrics::Generic do
describe '#parse!' do
subject { described_class.new.parse!(data, report) }
let(:report) { Gitlab::Ci::Reports::Metrics::Report.new }
context 'when data is sample metrics report' do
let(:data) { File.read(Rails.root.join('ee/spec/fixtures/metrics.txt')) }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'parses all metrics' do
expect { subject }.to change { report.metrics.count }.from(0).to(2)
end
end
context 'when string data has comments' do
let(:data) { '# metric_name metric_value' }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'does not parse comments' do
expect { subject }.not_to change { report.metrics.count }.from(0)
end
end
context 'when string data has metrics with labels' do
let(:data) { 'metric_name{label_name="label value"} metric_value' }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'parses the metric with labels' do
expect { subject }.to change { report.metrics.count }.from(0).to(1)
end
it 'stores the labels with the metric name' do
subject
expect(report.metrics['metric_name{label_name=label value}']).to eq('metric_value')
end
end
context 'when string data has metrics with multiple values' do
let(:data) { 'metric_name metric_value metric_second_value' }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'parses the metric with multiple values' do
expect { subject }.to change { report.metrics.count }.from(0).to(1)
end
it 'stores only the first metric value' do
subject
expect(report.metrics['metric_name']).to eq('metric_value')
end
end
context 'when string data has an incomplete metric' do
context 'when the incomplete metric does not have a value' do
let(:data) { 'just_the_name' }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'does not parse the metric' do
expect { subject }.not_to change { report.metrics.count }.from(0)
end
end
context 'when the incomplete metric is an empty line' do
let(:data) { '' }
it 'parses without error' do
expect { subject }.not_to raise_error
end
it 'does not parse the metric' do
expect { subject }.not_to change { report.metrics.count }.from(0)
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Metrics::Report do
let(:report) { described_class.new }
describe '#add_metric' do
let(:key) { 'metric_name' }
let(:value) { 'metric_value' }
subject { report.add_metric(key, value) }
it 'stores given metric' do
subject
expect(report.metrics.count).to eq(1)
end
it 'correctly stores metric params' do
subject
expect(report.metrics[key]).to eq(value)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::Ci::Reports::Metrics::ReportsComparer do
let(:first_report) { build :ci_reports_metrics_report, :base_metrics }
let(:second_report) { build :ci_reports_metrics_report, :head_metrics }
let(:report_comparer) { described_class.new(first_report, second_report) }
describe '#new_metrics' do
subject { report_comparer.new_metrics }
it 'reports new metrics' do
expect(subject.count).to eq 1
expect(subject.first.name).to eq 'extra_metric_name'
end
end
describe '#existing_metrics' do
subject { report_comparer.existing_metrics }
it 'reports existing metrics' do
expect(subject.count).to eq 1
expect(subject.first.name).to eq 'metric_name'
end
context 'when existing metric changes' do
before do
second_report.add_metric('metric_name', 'new_metric_value')
end
it 'sets previous value' do
expect(subject.first.previous_value).to eq 'metric_value'
expect(subject.first.value).to eq 'new_metric_value'
end
end
end
describe '#removed_metrics' do
subject { report_comparer.removed_metrics }
it 'reports removed metrics' do
expect(subject.count).to eq 1
expect(subject.first.name).to eq 'second_metric_name'
end
end
end
......@@ -119,40 +119,6 @@ describe Ci::Build do
end
end
describe '.with_security_reports' do
subject { described_class.with_security_reports }
context 'when build has a security report' do
let!(:build) { create(:ee_ci_build, :success, :sast) }
it 'selects the build' do
is_expected.to eq([build])
end
end
context 'when build does not have security reports' do
let!(:build) { create(:ci_build, :success, :trace_artifact) }
it 'does not select the build' do
is_expected.to be_empty
end
end
context 'when there are multiple builds with security reports' do
let!(:builds) { create_list(:ee_ci_build, 5, :success, :sast) }
it 'does not execute a query for selecting job artifacts one by one' do
recorded = ActiveRecord::QueryRecorder.new do
subject.each do |build|
build.job_artifacts.map { |a| a.file.exists? }
end
end
expect(recorded.count).to eq(2)
end
end
end
describe '#collect_security_reports!' do
let(:security_reports) { ::Gitlab::Ci::Reports::Security::Reports.new }
......@@ -325,4 +291,38 @@ describe Ci::Build do
end
end
end
describe '#collect_metrics_reports!' do
subject { job.collect_metrics_reports!(metrics_report) }
let(:metrics_report) { Gitlab::Ci::Reports::Metrics::Report.new }
context 'when there is a metrics report' do
before do
create(:ee_ci_job_artifact, :metrics, job: job, project: job.project)
end
context 'when license has metrics_reports' do
before do
stub_licensed_features(metrics_reports: true)
end
it 'parses blobs and add the results to the report' do
expect { subject }.to change { metrics_report.metrics.count }.from(0).to(2)
end
end
context 'when license does not have metrics_reports' do
before do
stub_licensed_features(license_management: false)
end
it 'does not parse metrics report' do
subject
expect(metrics_report.metrics.count).to eq(0)
end
end
end
end
end
......@@ -166,7 +166,9 @@ describe Ci::Pipeline do
subject { pipeline.legacy_report_artifact_for_file_type(file_type) }
described_class::REPORT_LICENSED_FEATURES.each do |file_type, licensed_features|
described_class::LEGACY_REPORT_FORMATS.each do |file_type, _|
licensed_features = described_class::REPORT_LICENSED_FEATURES[file_type]
context "for file_type: #{file_type}" do
let(:file_type) { file_type }
let(:expected) { OpenStruct.new(build: build, path: artifact_path) }
......@@ -182,48 +184,6 @@ describe Ci::Pipeline do
end
end
describe '#has_security_reports?' do
subject { pipeline.has_security_reports? }
context 'when pipeline has builds with security reports' do
before do
create(:ee_ci_build, :sast, pipeline: pipeline, project: project)
end
context 'when pipeline status is running' do
let(:pipeline) { create(:ci_pipeline, :running, project: project) }
it { is_expected.to be_falsey }
end
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline does not have builds with security reports' do
before do
create(:ci_build, :artifacts, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
context 'when retried build has security reports' do
before do
create(:ee_ci_build, :retried, :sast, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
end
describe '#security_reports' do
subject { pipeline.security_reports }
......@@ -326,52 +286,6 @@ describe Ci::Pipeline do
end
end
describe '#has_license_management_reports?' do
subject { pipeline.has_license_management_reports? }
before do
stub_licensed_features(license_management: true)
end
context 'when pipeline has builds with license_management reports' do
before do
create(:ee_ci_build, :license_management, pipeline: pipeline, project: project)
end
context 'when pipeline status is running' do
let(:pipeline) { create(:ci_pipeline, :running, project: project) }
it { is_expected.to be_falsey }
end
context 'when pipeline status is success' do
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline does not have builds with license_management reports' do
before do
create(:ci_build, :artifacts, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
context 'when retried build has license management reports' do
before do
create(:ee_ci_build, :retried, :license_management, pipeline: pipeline, project: project)
end
let(:pipeline) { create(:ci_pipeline, :success, project: project) }
it { is_expected.to be_falsey }
end
end
describe '#license_management_reports' do
subject { pipeline.license_management_report }
......@@ -410,6 +324,40 @@ describe Ci::Pipeline do
end
end
describe '#metrics_report' do
subject { pipeline.metrics_report }
before do
stub_licensed_features(metrics_reports: true)
end
context 'when pipeline has multiple builds with metrics reports' do
before do
create(:ee_ci_build, :success, :metrics, pipeline: pipeline, project: project)
end
it 'returns a metrics report with collected data' do
expect(subject.metrics.count).to eq(2)
end
end
context 'when pipeline has multiple builds with metrics reports that are retried' do
before do
create_list(:ee_ci_build, 2, :retried, :success, :metrics, pipeline: pipeline, project: project)
end
it 'does not take retried builds into account' do
expect(subject.metrics).to be_empty
end
end
context 'when pipeline does not have any builds with metrics reports' do
it 'returns an empty metrics report' do
expect(subject.metrics).to be_empty
end
end
end
describe 'upstream status interactions' do
context 'when a pipeline has an upstream status' do
context 'when an upstream status is a bridge' do
......
......@@ -12,4 +12,20 @@ describe EE::Ci::JobArtifact do
it { is_expected.to eq([artifact]) }
end
end
describe '.metrics_reports' do
subject { Ci::JobArtifact.metrics_reports }
context 'when there is a metrics report' do
let!(:artifact) { create(:ee_ci_job_artifact, :metrics) }
it { is_expected.to eq([artifact]) }
end
context 'when there is no metrics reports' do
let!(:artifact) { create(:ee_ci_job_artifact, :trace) }
it { is_expected.to be_empty }
end
end
end
......@@ -527,6 +527,27 @@ describe MergeRequest do
end
end
describe '#has_metrics_reports?' do
subject { merge_request.has_metrics_reports? }
let(:project) { create(:project, :repository) }
before do
stub_licensed_features(metrics_reports: true)
end
context 'when head pipeline has metrics reports' do
let(:merge_request) { create(:ee_merge_request, :with_metrics_reports, source_project: project) }
it { is_expected.to be_truthy }
end
context 'when head pipeline does not have license management reports' do
let(:merge_request) { create(:ee_merge_request, source_project: project) }
it { is_expected.to be_falsey }
end
end
describe '#compare_license_management_reports' do
subject { merge_request.compare_license_management_reports }
......@@ -600,6 +621,79 @@ describe MergeRequest do
end
end
describe '#compare_metrics_reports' do
subject { merge_request.compare_metrics_reports }
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:base_pipeline) do
create(:ee_ci_pipeline,
:with_metrics_report,
project: project,
ref: merge_request.target_branch,
sha: merge_request.diff_base_sha)
end
before do
merge_request.update!(head_pipeline_id: head_pipeline.id)
end
context 'when head pipeline has metrics reports' do
let!(:head_pipeline) do
create(:ee_ci_pipeline,
:with_metrics_report,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
context 'when reactive cache worker is parsing asynchronously' do
it 'returns status' do
expect(subject[:status]).to eq(:parsing)
end
end
context 'when reactive cache worker is inline' do
before do
synchronous_reactive_cache(merge_request)
end
it 'returns status and data' do
expect_any_instance_of(Ci::CompareMetricsReportsService)
.to receive(:execute).with(base_pipeline, head_pipeline).and_call_original
subject
end
context 'when cached results is not latest' do
before do
allow_any_instance_of(Ci::CompareMetricsReportsService)
.to receive(:latest?).and_return(false)
end
it 'raises and InvalidateReactiveCache error' do
expect { subject }.to raise_error(ReactiveCaching::InvalidateReactiveCache)
end
end
end
end
context 'when head pipeline does not have metrics reports' do
let!(:head_pipeline) do
create(:ci_pipeline,
project: project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha)
end
it 'returns status and error message' do
expect(subject[:status]).to eq(:error)
expect(subject[:status_reason]).to eq('This merge request does not have metrics reports')
end
end
end
describe '#mergeable_with_quick_action?' do
def create_pipeline(status)
pipeline = create(:ci_pipeline_with_one_job,
......
# frozen_string_literal: true
require 'spec_helper'
describe MetricsReportMetricEntity do
let(:metric) { ::Gitlab::Ci::Reports::Metrics::ReportsComparer::ComparedMetric.new('metric_name', 'metric_value') }
let(:entity) { described_class.new(metric) }
describe '#as_json' do
subject { entity.as_json }
it 'contains the correct metric' do
expect(subject[:name]).to eq('metric_name')
expect(subject[:value]).to eq('metric_value')
end
context 'when the metric did not change' do
before do
metric.previous_value = metric.value
end
it 'does not expose previous_value' do
expect(subject).not_to include(:previous_value)
end
end
context 'when the metric changed' do
before do
metric.previous_value = 'previous_metric_value'
end
it 'exposes the previous_value' do
expect(subject).to include(:previous_value)
expect(subject[:previous_value]).to eq('previous_metric_value')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe MetricsReportsComparerEntity do
let(:base_report) { build(:ci_reports_metrics_report, :base_metrics) }
let(:head_report) { build(:ci_reports_metrics_report, :head_metrics) }
let(:comparer) { Gitlab::Ci::Reports::Metrics::ReportsComparer.new(base_report, head_report) }
let(:entity) { described_class.new(comparer) }
describe '#as_json' do
subject { entity.as_json }
it 'contains the new metrics' do
expect(subject).to have_key(:new_metrics)
expect(subject[:new_metrics][0][:name]).to eq('extra_metric_name')
end
it 'contains existing metrics' do
expect(subject).to have_key(:existing_metrics)
expect(subject[:existing_metrics].count).to be(1)
end
it 'contains removed metrics' do
expect(subject).to have_key(:removed_metrics)
expect(subject[:removed_metrics][0][:name]).to eq('second_metric_name')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::CompareMetricsReportsService do
set(:project) { create(:project, :repository) }
let(:service) { described_class.new(project) }
before do
stub_licensed_features(metrics_reports: true)
end
describe '#execute' do
subject { service.execute(base_pipeline, head_pipeline) }
context 'when head pipeline has metrics reports' do
let!(:base_pipeline) { nil }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_metrics_report, project: project) }
it 'reports new metrics' do
expect(subject[:status]).to eq(:parsed)
expect(subject[:data]['new_metrics'].count).to eq(2)
end
end
context 'when base and head pipelines have metrics reports' do
let!(:base_pipeline) { create(:ee_ci_pipeline, :with_metrics_report, project: project) }
let!(:head_pipeline) { create(:ee_ci_pipeline, :with_metrics_alternate_report, project: project) }
it 'reports status as parsed' do
expect(subject[:status]).to eq(:parsed)
end
it 'reports new licenses' do
expect(subject[:data]['new_metrics'].count).to eq(1)
expect(subject[:data]['new_metrics']).to include(a_hash_including('name' => 'third_metric'))
end
it 'reports existing metrics' do
expect(subject[:data]['existing_metrics'].count).to eq(1)
expect(subject[:data]['existing_metrics']).to include(a_hash_including('name' => 'first_metric'))
end
it 'reports removed metrics' do
expect(subject[:data]['removed_metrics'].count).to eq(1)
expect(subject[:data]['removed_metrics']).to include(a_hash_including('name' => 'second_metric'))
end
end
end
end
......@@ -11,7 +11,7 @@ module Gitlab
include ::Gitlab::Config::Entry::Validatable
include ::Gitlab::Config::Entry::Attributable
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management].freeze
ALLOWED_KEYS = %i[junit codequality sast dependency_scanning container_scanning dast performance license_management metrics].freeze
attributes ALLOWED_KEYS
......@@ -28,6 +28,7 @@ module Gitlab
validates :dast, array_of_strings_or_string: true
validates :performance, array_of_strings_or_string: true
validates :license_management, array_of_strings_or_string: true
validates :metrics, array_of_strings_or_string: true
end
end
......
......@@ -166,8 +166,8 @@ describe Ci::Build do
end
end
describe '.with_test_reports' do
subject { described_class.with_test_reports }
describe '.with_reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
context 'when build has a test report' do
let!(:build) { create(:ci_build, :success, :test_reports) }
......
......@@ -426,6 +426,26 @@ describe Ci::Pipeline, :mailer do
end
end
describe '.with_reports' do
subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
context 'when pipeline has a test report' do
let!(:pipeline_with_report) { create(:ci_pipeline, :with_test_reports) }
it 'selects the pipeline' do
is_expected.to eq([pipeline_with_report])
end
end
context 'when pipeline does not have metrics reports' do
let!(:pipeline_without_report) { create(:ci_empty_pipeline) }
it 'does not select the pipeline' do
is_expected.to be_empty
end
end
end
describe '.merge_request_event' do
subject { described_class.merge_request_event }
......@@ -2729,8 +2749,8 @@ describe Ci::Pipeline, :mailer do
end
end
describe '#has_test_reports?' do
subject { pipeline.has_test_reports? }
describe '#has_reports?' do
subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }
context 'when pipeline has builds with test reports' do
before do
......
......@@ -30,7 +30,7 @@ describe Ci::RetryBuildService do
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_performance
job_artifacts_codequality scheduled_at].freeze
job_artifacts_codequality job_artifacts_metrics scheduled_at].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment