Commit 1e7f1a0a authored by Matija Čupić's avatar Matija Čupić Committed by Kamil Trzciński

Implement user generated metrics reports

Add metrics report type to JobArtifact constants

Add sample metrics fixture

Add factories for artifacts with metrics reports

Adds EE spec factories for CI models that can have metrics reports.

Add scopes for CI models with metrics reports

Adds scopes with metrics reports for Pipeline, Builds and JobArtifacts.

Add metrics_reports Premium feature

Implement has_metrics_reports in Pipelines and MRs

Implements EE::Ci::Pipeline#has_metrics_reports? and
EE::MergeRequest#has_metrics_reports?.

Implement metrics CI Report and Parser

Implements Parser and Report types for metrics reports.

Expose metrics reports to the Merge Request

Implements EE::Ci::Build#collect_metrics_reports! and
EE::Ci::Pipeline#metrics_report and exposes metrics report for
consumption by the Merge Request.

Implement metrics reports comparer

Implements a class that compares two metrics reports and selects new,
existing and removed metrics.

Implement Metric report serializer

Implements a serializer for metrics reports to be used in the metrics
reports comparison service.

Implement metrics reports comparison service

Implements Ci::CompareMetricsReportsService that compares two
metrics reports and serializes the result for frontend consumption.

Expose metrics reports comparison in Merge Request

Add Merge Requests controller endpoint for metrics

Implements a Merge Request controller endpoint for querying metrics
reports.
parent f8687a06
......@@ -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