Commit adc4918e authored by Tiago Botelho's avatar Tiago Botelho
Browse files

Adapt cycle analytics spec helper and cycle analytics usage data spec

parent 93e054e700ba
......@@ -7,8 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description
expose :median, as: :value do |stage|
if stage.median && !(stage.median.nil? || stage.median.zero?)
distance_of_time_in_words(stage.median)
end
# median returns a BatchLoader instance which we first have to unwrap by using to_i
!stage.median.to_i.zero? ? distance_of_time_in_words(stage.median) : nil
end
end
---
title: Include cycle time in usage ping data
merge_request: 16973
author:
type: added
......@@ -15,7 +15,7 @@ def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(Array(project_ids)))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests
......
......@@ -31,11 +31,15 @@ def median
interval_query = Arel::Nodes::As.new(cte_table,
subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
if project_ids.size == 1
if project_ids.one?
loader.call(@project.id, median_datetime(cte_table, interval_query, name))
else
median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
begin
median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
rescue NotSupportedError
{}
end
end
end
......
......@@ -2,6 +2,8 @@
module Gitlab
module Database
module Median
NotSupportedError = Class.new(StandardError)
def median_datetime(arel_table, query_so_far, column_sym)
extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
end
......@@ -23,11 +25,11 @@ def extract_median(results)
end
def extract_medians(results)
return {} if Gitlab::Database.mysql?
median_values = results.compact.first.values
results.compact.first.values.map do |id, median|
[id.to_i, median&.to_f]
end.to_h
median_values.each_with_object({}) do |(id, median), hash|
hash[id.to_i] = median&.to_f
end
end
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
......@@ -113,6 +115,8 @@ def median_queries(arel_table, query_so_far, column_sym, partition_column = nil)
if Gitlab::Database.postgresql?
pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
elsif Gitlab::Database.mysql?
raise NotSupportedError, "partition_column is not supported for MySQL" if partition_column
mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
end
end
......@@ -159,12 +163,10 @@ def rank_rows(arel_table, column_sym, partition_column)
end
def median_projections(table, column_sym, partition_column)
if partition_column
[table[partition_column],
average([extract_epoch(table[column_sym])], "median")]
else
[average([extract_epoch(table[column_sym])], "median")]
end
projections = []
projections << table[partition_column] if partition_column
projections << average([extract_epoch(table[column_sym])], "median")
projections
end
def extract_epoch(arel_attribute)
......
......@@ -27,7 +27,7 @@
milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
end
it 'is false' do
......
......@@ -6,7 +6,7 @@
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
context 'as an allowed user' do
......@@ -42,7 +42,7 @@
project.add_master(user)
@build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master
deploy_master(user, project)
sign_in(user)
visit project_cycle_analytics_path(project)
......@@ -118,7 +118,7 @@
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master
deploy_master(user, project)
sign_in(guest)
visit project_cycle_analytics_path(project)
......
......@@ -41,7 +41,7 @@ def setup_events(count:)
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
create_merge_request_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
end
end
end
......@@ -236,8 +236,8 @@
pipeline.run!
pipeline.succeed!
merge_merge_requests_closing_issue(context)
deploy_master
merge_merge_requests_closing_issue(user, project, context)
deploy_master(user, project)
end
it 'has the name' do
......@@ -294,8 +294,8 @@
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
merge_merge_requests_closing_issue(context)
deploy_master
merge_merge_requests_closing_issue(user, project, context)
deploy_master(user, project)
end
it 'has the total time' do
......@@ -334,7 +334,7 @@
def setup(context)
milestone = create(:milestone, project: project)
context.update(milestone: milestone)
mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}")
mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
......
require 'spec_helper'
describe Gitlab::CycleAnalytics::UsageData do
let(:project) { create(:project, :repository) }
let(:user) { create(:user, :admin) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
subject { described_class.new([project]) }
describe '#to_json' do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
Timecop.freeze do
user = create(:user, :admin)
projects = create_list(:project, 2, :repository)
projects.each_with_index do |project, time|
issue = create(:issue, project: project, created_at: (time + 1).hour.ago)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
milestone = create(:milestone, project: project)
mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project, environment: 'staging')
deploy_master(user, project)
end
end
end
it 'returns the aggregated usage data of every selected project' do
result = subject.to_json
avg_cycle_analytics = result[:avg_cycle_analytics]
shared_examples 'a valid usage data result' do
it 'returns the aggregated usage data of every selected project' do
result = subject.to_json
expect(result).to have_key(:avg_cycle_analytics)
CycleAnalytics::STAGES.each do |stage|
expect(result[:avg_cycle_analytics]).to have_key(stage)
stage_values = result[:avg_cycle_analytics][stage]
expected_values = expect_values_per_stage[stage]
expect(result).to have_key(:avg_cycle_analytics)
CycleAnalytics::STAGES.each do |stage_name|
stage_values = avg_cycle_analytics[stage_name]
expected_values.each_pair do |op, value|
expect(stage_values).to have_key(op)
expect(avg_cycle_analytics).to have_key(stage_name)
expect(stage_values).to have_key(:average)
expect(stage_values).to have_key(:sd)
expect(stage_values).to have_key(:missing)
if op == :missing
expect(stage_values[op]).to eq(value)
else
# delta is used because of git timings that Timecop does not stub
expect(stage_values[op].to_i).to be_within(5).of(value.to_i)
end
end
end
end
end
context 'when using postgresql', :postgresql do
let(:expect_values_per_stage) do
{
issue: {
average: 5400,
sd: 2545,
missing: 0
},
plan: {
average: 2,
sd: 2,
missing: 0
},
code: {
average: nil,
sd: 0,
missing: 2
},
test: {
average: nil,
sd: 0,
missing: 2
},
review: {
average: 0,
sd: 0,
missing: 0
},
staging: {
average: 0,
sd: 0,
missing: 0
},
production: {
average: 5400,
sd: 2545,
missing: 0
}
}
end
it_behaves_like 'a valid usage data result'
end
context 'when using mysql', :mysql do
let(:expect_values_per_stage) do
{
issue: {
average: nil,
sd: 0,
missing: 2
},
plan: {
average: nil,
sd: 0,
missing: 2
},
code: {
average: nil,
sd: 0,
missing: 2
},
test: {
average: nil,
sd: 0,
missing: 2
},
review: {
average: nil,
sd: 0,
missing: 2
},
staging: {
average: nil,
sd: 0,
missing: 2
},
production: {
average: nil,
sd: 0,
missing: 2
}
}
end
it_behaves_like 'a valid usage data result'
end
end
end
require 'spec_helper'
describe Gitlab::Database::Median do
describe '#extract_medians' do
context 'when using MySQL' do
it 'returns an empty hash' do
values = [["1", "1000"]]
let(:dummy_class) do
Class.new do
include Gitlab::Database::Median
end
end
allow(Gitlab::Database).to receive(:mysql?).and_return(true)
subject(:median) { dummy_class.new }
expect(described_class.new.extract_median(values)).eq({})
end
describe '#median_datetimes' do
it 'raises NotSupportedError', :mysql do
expect { median.median_datetimes(nil, nil, nil, :project_id) }.to raise_error(dummy_class::NotSupportedError, "partition_column is not supported for MySQL")
end
end
end
......@@ -18,11 +18,11 @@
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.deploy_master
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
context.deploy_master(context.user, context.project)
end)
context "when a regular merge request (that doesn't close the issue) is created" do
......@@ -30,10 +30,10 @@
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
create_merge_request_closing_issue(issue, message: "Closes nothing")
create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
merge_merge_requests_closing_issue(issue)
deploy_master
merge_merge_requests_closing_issue(user, project, issue)
deploy_master(user, project)
expect(subject[:code].median).to be_nil
end
......@@ -50,10 +50,10 @@
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular merge request (that doesn't close the issue) is created" do
......@@ -61,9 +61,9 @@
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
create_merge_request_closing_issue(issue, message: "Closes nothing")
create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
merge_merge_requests_closing_issue(issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:code].median).to be_nil
end
......
......@@ -26,8 +26,8 @@
end]],
post_fn: -> (context, data) do
if data[:issue].persisted?
context.create_merge_request_closing_issue(data[:issue].reload)
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue].reload)
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end
end)
......@@ -37,8 +37,8 @@
issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id])
create_merge_request_closing_issue(issue)
merge_merge_requests_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
......
......@@ -29,8 +29,8 @@
context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
end]],
post_fn: -> (context, data) do
context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular label (instead of a list label) is added to the issue" do
......@@ -41,8 +41,8 @@
issue.update(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name)
create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue)
create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
......
......@@ -13,11 +13,11 @@
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
before_end_fn: lambda do |context, data|
context.create_merge_request_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end,
end_time_conditions:
[["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
[["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
......@@ -29,14 +29,14 @@
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master
deploy_master(user, project)
expect(subject[:production].median).to be_nil
end
......@@ -45,9 +45,9 @@
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(environment: 'staging')
deploy_master(user, project, environment: 'staging')
expect(subject[:production].median).to be_nil
end
......
......@@ -13,11 +13,11 @@
data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
start_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
context.create_merge_request_closing_issue(data[:issue])
context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: nil)
......
......@@ -13,15 +13,15 @@
phase: :staging,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
{ issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
{ issue: issue, merge_request: context.create_merge_request_closing_issue(context.user, context.project, issue) }
end,
start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue])
context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do
context.deploy_master
context.deploy_master(context.user, context.project)
end],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
......@@ -34,14 +34,14 @@
branch_name: 'master')
context.project.repository.commit(sha)
context.deploy_master
context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master
deploy_master(user, project)
expect(subject[:staging].median).to be_nil
end
......@@ -50,9 +50,9 @@
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue)
merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(environment: 'staging')
deploy_master(user, project, environment: 'staging')
expect(subject[:staging].median).to be_nil
end
......
......@@ -12,26 +12,26 @@
phase: :test,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
merge_request = context.create_merge_request_closing_issue(issue)
merge_request = context.create_merge_request_closing_issue(context.user, context.project, issue)
pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request)
{ pipeline: pipeline, issue: issue }
end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],