Commit 008120f8 authored by Douwe Maan's avatar Douwe Maan

Merge branch '41777-include-cycle-time-in-usage-ping' into 'master'

Resolve "Include cycle time in usage ping"

Closes #41777

See merge request gitlab-org/gitlab-ce!16793
parents ccb080d9 522f4b2c
...@@ -6,6 +6,12 @@ class CycleAnalytics ...@@ -6,6 +6,12 @@ class CycleAnalytics
@options = options @options = options
end end
def all_medians_per_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median
end
end
def summary def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from], from: @options[:from],
......
...@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity ...@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description expose :description
expose :median, as: :value do |stage| expose :median, as: :value do |stage|
stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil # 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
end end
---
title: Include cycle time in usage ping data
merge_request: 16973
author:
type: added
...@@ -8,13 +8,14 @@ module Gitlab ...@@ -8,13 +8,14 @@ module Gitlab
private private
def base_query def base_query
@base_query ||= stage_query @base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
def stage_query def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) 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])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables .project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests # Load merge_requests
......
...@@ -21,17 +21,28 @@ module Gitlab ...@@ -21,17 +21,28 @@ module Gitlab
end end
def median def median
cte_table = Arel::Table.new("cte_table_for_#{name}") BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
cte_table = Arel::Table.new("cte_table_for_#{name}")
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
# We compute the (end_time - start_time) interval, and give it an alias based on the current # We compute the (end_time - start_time) interval, and give it an alias based on the current
# cycle analytics stage. # cycle analytics stage.
interval_query = Arel::Nodes::As.new( interval_query = Arel::Nodes::As.new(cte_table,
cte_table, subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
median_datetime(cte_table, interval_query, name) if project_ids.one?
loader.call(@project.id, median_datetime(cte_table, interval_query, name))
else
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
end end
def name def name
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
module ProductionHelper module ProductionHelper
def stage_query def stage_query(project_ids)
super super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at] .where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end end
......
...@@ -25,11 +25,11 @@ module Gitlab ...@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges") _("Total test time for all commits/merges")
end end
def stage_query def stage_query(project_ids)
if @options[:branch] if @options[:branch]
super.where(build_table[:ref].eq(@options[:branch])) super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else else
super super(project_ids)
end end
end end
end end
......
module Gitlab
module CycleAnalytics
class UsageData
PROJECTS_LIMIT = 10
attr_reader :projects, :options
def initialize
@projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
@options = { from: 7.days.ago }
end
def to_json
total = 0
values =
medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
calculations = stage_values(medians)
total += calculations.values.compact.sum
hsh[stage_name] = calculations
end
values[:total] = total
{ avg_cycle_analytics: values }
end
private
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
end
end
def stage_values(medians)
medians = medians.map(&:presence).compact
average = calc_average(medians)
{
average: average,
sd: standard_deviation(medians, average),
missing: projects.length - medians.length
}
end
def calc_average(values)
return if values.empty?
(values.sum / values.length).to_i
end
def standard_deviation(values, average)
Math.sqrt(sample_variance(values, average)).to_i
end
def sample_variance(values, average)
return 0 if values.length <= 1
sum = values.inject(0) do |acc, val|
acc + (val - average)**2
end
sum / (values.length - 1)
end
end
end
end
...@@ -2,18 +2,14 @@ ...@@ -2,18 +2,14 @@
module Gitlab module Gitlab
module Database module Database
module Median module Median
NotSupportedError = Class.new(StandardError)
def median_datetime(arel_table, query_so_far, column_sym) def median_datetime(arel_table, query_so_far, column_sym)
median_queries = extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
if Gitlab::Database.postgresql? end
pg_median_datetime_sql(arel_table, query_so_far, column_sym)
elsif Gitlab::Database.mysql? def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
mysql_median_datetime_sql(arel_table, query_so_far, column_sym) extract_medians(execute_queries(arel_table, query_so_far, column_sym, partition_column)).presence
end
results = Array.wrap(median_queries).map do |query|
ActiveRecord::Base.connection.execute(query)
end
extract_median(results).presence
end end
def extract_median(results) def extract_median(results)
...@@ -21,13 +17,21 @@ module Gitlab ...@@ -21,13 +17,21 @@ module Gitlab
if Gitlab::Database.postgresql? if Gitlab::Database.postgresql?
result = result.first.presence result = result.first.presence
median = result['median'] if result
median.to_f if median result['median']&.to_f if result
elsif Gitlab::Database.mysql? elsif Gitlab::Database.mysql?
result.to_a.flatten.first result.to_a.flatten.first
end end
end end
def extract_medians(results)
median_values = results.compact.first.values
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) def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
query = arel_table query = arel_table
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name)) .from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
...@@ -53,7 +57,7 @@ module Gitlab ...@@ -53,7 +57,7 @@ module Gitlab
] ]
end end
def pg_median_datetime_sql(arel_table, query_so_far, column_sym) def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column = nil)
# Create a CTE with the column we're operating on, row number (after sorting by the column # Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows # we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count` # of the CTE. For example, if we're looking to find the median of the `projects.star_count`
...@@ -64,41 +68,107 @@ module Gitlab ...@@ -64,41 +68,107 @@ module Gitlab
# 5 | 1 | 3 # 5 | 1 | 3
# 9 | 2 | 3 # 9 | 2 | 3
# 15 | 3 | 3 # 15 | 3 | 3
#
# If a partition column is used we will do the same operation but for separate partitions,
# when that happens the CTE might look like this:
#
# project_id | star_count | row_id | ct
# ------------+------------+--------+----
# 1 | 5 | 1 | 2
# 1 | 9 | 2 | 2
# 2 | 10 | 1 | 3
# 2 | 15 | 2 | 3
# 2 | 20 | 3 | 3
cte_table = Arel::Table.new("ordered_records") cte_table = Arel::Table.new("ordered_records")
cte = Arel::Nodes::As.new( cte = Arel::Nodes::As.new(
cte_table, cte_table,
arel_table arel_table.project(*rank_rows(arel_table, column_sym, partition_column)).
.project(
arel_table[column_sym].as(column_sym.to_s),
Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
arel_table.project("COUNT(1)").as('ct')).
# Disallow negative values # Disallow negative values
where(arel_table[column_sym].gteq(zero_interval))) where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished # From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the # by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value. # selected rows, and this is the median value.
cte_table.project(average([extract_epoch(cte_table[column_sym])], "median")) result =
.where( cte_table
Arel::Nodes::Between.new( .project(*median_projections(cte_table, column_sym, partition_column))
cte_table[:row_id], .where(
Arel::Nodes::And.new( Arel::Nodes::Between.new(
[(cte_table[:ct] / Arel.sql('2.0')), cte_table[:row_id],
(cte_table[:ct] / Arel.sql('2.0') + 1)] Arel::Nodes::And.new(
[(cte_table[:ct] / Arel.sql('2.0')),
(cte_table[:ct] / Arel.sql('2.0') + 1)]
)
) )
) )
) .with(query_so_far, cte)
.with(query_so_far, cte)
.to_sql result.group(cte_table[partition_column]).order(cte_table[partition_column]) if partition_column
result.to_sql
end end
private private
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
def execute_queries(arel_table, query_so_far, column_sym, partition_column = nil)
queries = median_queries(arel_table, query_so_far, column_sym, partition_column)
Array.wrap(queries).map { |query| ActiveRecord::Base.connection.execute(query) }
end
def average(args, as) def average(args, as)
Arel::Nodes::NamedFunction.new("AVG", args, as) Arel::Nodes::NamedFunction.new("AVG", args, as)
end end
def rank_rows(arel_table, column_sym, partition_column)
column_row = arel_table[column_sym].as(column_sym.to_s)
if partition_column
partition_row = arel_table[partition_column]
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('rank', []),
Arel::Nodes::Window.new.partition(arel_table[partition_column])
.order(arel_table[column_sym])
).as('row_id')
count = arel_table.from(arel_table.alias)
.project('COUNT(*)')
.where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
.as('ct')
[partition_row, column_row, row_id, count]
else
row_id =
Arel::Nodes::Over.new(
Arel::Nodes::NamedFunction.new('row_number', []),
Arel::Nodes::Window.new.order(arel_table[column_sym])
).as('row_id')
count = arel_table.project("COUNT(1)").as('ct')
[column_row, row_id, count]
end
end
def median_projections(table, column_sym, partition_column)
projections = []
projections << table[partition_column] if partition_column
projections << average([extract_epoch(table[column_sym])], "median")
projections
end
def extract_epoch(arel_attribute) def extract_epoch(arel_attribute)
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end end
......
...@@ -9,6 +9,7 @@ module Gitlab ...@@ -9,6 +9,7 @@ module Gitlab
license_usage_data.merge(system_usage_data) license_usage_data.merge(system_usage_data)
.merge(features_usage_data) .merge(features_usage_data)
.merge(components_usage_data) .merge(components_usage_data)
.merge(cycle_analytics_usage_data)
end end
def to_json(force_refresh: false) def to_json(force_refresh: false)
...@@ -71,6 +72,10 @@ module Gitlab ...@@ -71,6 +72,10 @@ module Gitlab
} }
end end
def cycle_analytics_usage_data
Gitlab::CycleAnalytics::UsageData.new.to_json
end
def features_usage_data def features_usage_data
features_usage_data_ce features_usage_data_ce
end end
......
...@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do ...@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do
milestone = create(:milestone, project: project, created_at: 5.days.ago) milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone) issue.update(milestone: milestone)
create_merge_request_closing_issue(issue) create_merge_request_closing_issue(user, project, issue)
end end
it 'is false' do it 'is false' do
......
...@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do ...@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } 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) } 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 context 'as an allowed user' do
...@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do ...@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
project.add_master(user) project.add_master(user)
create_cycle @build = create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master deploy_master(user, project)
sign_in(user) sign_in(user)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
...@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do ...@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do
project.add_guest(guest) project.add_guest(guest)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue]) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master deploy_master(user, project)
sign_in(guest) sign_in(guest)
visit project_cycle_analytics_path(project) visit project_cycle_analytics_path(project)
...@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do ...@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do
expect(find('.stage-events')).to have_content("!#{mr.iid}") expect(find('.stage-events')).to have_content("!#{mr.iid}")
end end
def create_cycle
issue.update(milestone: milestone)
pipeline.run
@build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
def click_stage(stage_name) def click_stage(stage_name)
find('.stage-nav li', text: stage_name).click find('.stage-nav li', text: stage_name).click
wait_for_requests wait_for_requests
......
...@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do ...@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
issue.update(milestone: milestone) issue.update(milestone: milestone)
create_merge_request_closing_issue(issue) create_merge_request_closing_issue(user, project, issue)
end end
end end
end end
...@@ -236,8 +236,8 @@ describe 'cycle analytics events' do ...@@ -236,8 +236,8 @@ describe 'cycle analytics events' do
pipeline.run! pipeline.run!
pipeline.succeed! pipeline.succeed!
merge_merge_requests_closing_issue(context) merge_merge_requests_closing_issue(user, project, context)
deploy_master deploy_master(user, project)
end end
it 'has the name' do it 'has the name' do
...@@ -294,8 +294,8 @@ describe 'cycle analytics events' do ...@@ -294,8 +294,8 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do before do
merge_merge_requests_closing_issue(context) merge_merge_requests_closing_issue(user, project, context)
deploy_master deploy_master(user, project)
end end
it 'has the total time' do it 'has the total time' do
...@@ -334,7 +334,7 @@ describe 'cycle analytics events' do ...@@ -334,7 +334,7 @@ describe 'cycle analytics events' do
def setup(context) def setup(context)
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
context.update(milestone: milestone) 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) ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end end
......
require 'spec_helper'
describe Gitlab::CycleAnalytics::UsageData do
describe '#to_json' do
before do
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(user, project, environment: 'staging')
deploy_master(user, project)
end
end
end
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]
expected_values.each_pair do |op, value|
expect(stage_values).to have_key(op)
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
let(:dummy_class) do
Class.new do
include Gitlab::Database::Median
end
end
subject(:median) { dummy_class.new }
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
...@@ -36,6 +36,7 @@ describe Gitlab::UsageData do ...@@ -36,6 +36,7 @@ describe Gitlab::UsageData do
gitlab_shared_runners gitlab_shared_runners
git git
database database
avg_cycle_analytics
)) ))
end end
......
...@@ -18,11 +18,11 @@ describe 'CycleAnalytics#code' do ...@@ -18,11 +18,11 @@ describe 'CycleAnalytics#code' do
end]], end]],
end_time_conditions: [["merge request that closes issue is created", end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do -> (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]],
post_fn: -> (context, data) do 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])
context.deploy_master context.deploy_master(context.user, context.project)
end) end)
context "when a regular merge request (that doesn't close the issue) is created" do context "when a regular merge request (that doesn't close the issue) is created" do
...@@ -30,10 +30,10 @@ describe 'CycleAnalytics#code' do ...@@ -30,10 +30,10 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project) issue = create(:issue, project: project)
create_commit_referencing_issue(issue) 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)
deploy_master deploy_master(user, project)
expect(subject[:code].median).to be_nil expect(subject[:code].median).to be_nil
end end
...@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code' do ...@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code' do
end]], end]],
end_time_conditions: [["merge request that closes issue is created", end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do -> (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]],
post_fn: -> (context, data) do 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) end)
context "when a regular merge request (that doesn't close the issue) is created" do context "when a regular merge request (that doesn't close the issue) is created" do
...@@ -61,9 +61,9 @@ describe 'CycleAnalytics#code' do ...@@ -61,9 +61,9 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project) issue = create(:issue, project: project)
create_commit_referencing_issue(issue) 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 expect(subject[:code].median).to be_nil
end end
......
...@@ -26,8 +26,8 @@ describe 'CycleAnalytics#issue' do ...@@ -26,8 +26,8 @@ describe 'CycleAnalytics#issue' do
end]], end]],
post_fn: -> (context, data) do post_fn: -> (context, data) do
if data[:issue].persisted? if data[:issue].persisted?
context.create_merge_request_closing_issue(data[:issue].reload) context.create_merge_request_closing_issue(context.user, context.project, data[:issue].reload)
context.merge_merge_requests_closing_issue(data[:issue]) context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end end
end) end)
...@@ -37,8 +37,8 @@ describe 'CycleAnalytics#issue' do ...@@ -37,8 +37,8 @@ describe 'CycleAnalytics#issue' do
issue = create(:issue, project: project) issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id]) issue.update(label_ids: [regular_label.id])
create_merge_request_closing_issue(issue) create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil expect(subject[:issue].median).to be_nil
end end
......
...@@ -29,8 +29,8 @@ describe 'CycleAnalytics#plan' do ...@@ -29,8 +29,8 @@ describe 'CycleAnalytics#plan' do
context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name]) context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
end]], end]],
post_fn: -> (context, data) do post_fn: -> (context, data) do
context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name]) context.create_merge_request_closing_issue(context.user, context.project, data[:issue], source_branch: data[:branch_name])
context.merge_merge_requests_closing_issue(data[:issue]) context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end) end)
context "when a regular label (instead of a list label) is added to the issue" do context "when a regular label (instead of a list label) is added to the issue" do
...@@ -41,8 +41,8 @@ describe 'CycleAnalytics#plan' do ...@@ -41,8 +41,8 @@ describe 'CycleAnalytics#plan' do
issue.update(label_ids: [label.id]) issue.update(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name) create_commit_referencing_issue(issue, branch_name: branch_name)
create_merge_request_closing_issue(issue, source_branch: branch_name) create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil expect(subject[:issue].median).to be_nil
end end
......
...@@ -13,11 +13,11 @@ describe 'CycleAnalytics#production' do ...@@ -13,11 +13,11 @@ describe 'CycleAnalytics#production' do
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]], start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
before_end_fn: lambda do |context, data| before_end_fn: lambda do |context, data|
context.create_merge_request_closing_issue(data[:issue]) context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
context.merge_merge_requests_closing_issue(data[:issue]) context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end, end,
end_time_conditions: 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)", ["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data| lambda do |context, data|
# Make other changes on master # Make other changes on master
...@@ -29,14 +29,14 @@ describe 'CycleAnalytics#production' do ...@@ -29,14 +29,14 @@ describe 'CycleAnalytics#production' do
branch_name: 'master') branch_name: 'master')
context.project.repository.commit(sha) context.project.repository.commit(sha)
context.deploy_master context.deploy_master(context.user, context.project)
end]]) end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do it "returns nil" do
merge_request = create(:merge_request) merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master deploy_master(user, project)
expect(subject[:production].median).to be_nil expect(subject[:production].median).to be_nil
end end
...@@ -45,9 +45,9 @@ describe 'CycleAnalytics#production' do ...@@ -45,9 +45,9 @@ describe 'CycleAnalytics#production' do
context "when the deployment happens to a non-production environment" do context "when the deployment happens to a non-production environment" do
it "returns nil" do it "returns nil" do
issue = create(:issue, project: project) 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) 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 expect(subject[:production].median).to be_nil
end end
......
...@@ -13,11 +13,11 @@ describe 'CycleAnalytics#review' do ...@@ -13,11 +13,11 @@ describe 'CycleAnalytics#review' do
data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
start_time_conditions: [["merge request that closes issue is created", start_time_conditions: [["merge request that closes issue is created",
-> (context, data) do -> (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]],
end_time_conditions: [["merge request that closes issue is merged", end_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do -> (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]],
post_fn: nil) post_fn: nil)
......
...@@ -13,15 +13,15 @@ describe 'CycleAnalytics#staging' do ...@@ -13,15 +13,15 @@ describe 'CycleAnalytics#staging' do
phase: :staging, phase: :staging,
data_fn: lambda do |context| data_fn: lambda do |context|
issue = context.create(:issue, project: context.project) 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, end,
start_time_conditions: [["merge request that closes issue is merged", start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do -> (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]],
end_time_conditions: [["merge request that closes issue is deployed to production", end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do -> (context, data) do
context.deploy_master context.deploy_master(context.user, context.project)
end], end],
["production deploy happens after merge request is merged (along with other changes)", ["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data| lambda do |context, data|
...@@ -34,14 +34,14 @@ describe 'CycleAnalytics#staging' do ...@@ -34,14 +34,14 @@ describe 'CycleAnalytics#staging' do
branch_name: 'master') branch_name: 'master')
context.project.repository.commit(sha) context.project.repository.commit(sha)
context.deploy_master context.deploy_master(context.user, context.project)
end]]) end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do it "returns nil" do
merge_request = create(:merge_request) merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master deploy_master(user, project)
expect(subject[:staging].median).to be_nil expect(subject[:staging].median).to be_nil
end end
...@@ -50,9 +50,9 @@ describe 'CycleAnalytics#staging' do ...@@ -50,9 +50,9 @@ describe 'CycleAnalytics#staging' do
context "when the deployment happens to a non-production environment" do context "when the deployment happens to a non-production environment" do
it "returns nil" do it "returns nil" do
issue = create(:issue, project: project) 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) 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 expect(subject[:staging].median).to be_nil
end end
......
...@@ -12,26 +12,26 @@ describe 'CycleAnalytics#test' do ...@@ -12,26 +12,26 @@ describe 'CycleAnalytics#test' do
phase: :test, phase: :test,
data_fn: lambda do |context| data_fn: lambda do |context|
issue = context.create(:issue, project: context.project) 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 = 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 } { pipeline: pipeline, issue: issue }
end, end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]], start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]], end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
post_fn: -> (context, data) do 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) end)
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do it "returns nil" do
issue = create(:issue, project: project) issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue) merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run! pipeline.run!
pipeline.succeed! pipeline.succeed!
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].median).to be_nil
end end
...@@ -51,13 +51,13 @@ describe 'CycleAnalytics#test' do ...@@ -51,13 +51,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is dropped (failed)" do context "when the pipeline is dropped (failed)" do
it "returns nil" do it "returns nil" do
issue = create(:issue, project: project) issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue) merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run! pipeline.run!
pipeline.drop! pipeline.drop!
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].median).to be_nil
end end
...@@ -66,13 +66,13 @@ describe 'CycleAnalytics#test' do ...@@ -66,13 +66,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is cancelled" do context "when the pipeline is cancelled" do
it "returns nil" do it "returns nil" do
issue = create(:issue, project: project) issue = create(:issue, project: project)
merge_request = create_merge_request_closing_issue(issue) merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run! pipeline.run!
pipeline.cancel! pipeline.cancel!
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].median).to be_nil
end end
......
require 'spec_helper'
describe CycleAnalytics do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
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(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) }
subject { described_class.new(project, from: from_date) }
describe '#all_medians_per_stage' do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
end
it 'returns every median for each stage for a specific project' do
values = described_class::STAGES.each_with_object({}) do |stage_name, hsh|
hsh[stage_name] = subject[stage_name].median.presence
end
expect(subject.all_medians_per_stage).to eq(values)
end
end
end
...@@ -15,7 +15,7 @@ describe 'cycle analytics events' do ...@@ -15,7 +15,7 @@ describe 'cycle analytics events' do
end end
end end
deploy_master deploy_master(user, project)
login_as(user) login_as(user)
end end
...@@ -119,7 +119,7 @@ describe 'cycle analytics events' do ...@@ -119,7 +119,7 @@ describe 'cycle analytics events' do
def create_cycle def create_cycle
milestone = create(:milestone, project: project) milestone = create(:milestone, project: project)
issue.update(milestone: milestone) issue.update(milestone: milestone)
mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") 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) pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
pipeline.run pipeline.run
...@@ -127,7 +127,7 @@ describe 'cycle analytics events' do ...@@ -127,7 +127,7 @@ describe 'cycle analytics events' do
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user) create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash) ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end end
......
...@@ -186,6 +186,10 @@ RSpec.configure do |config| ...@@ -186,6 +186,10 @@ RSpec.configure do |config|
example.run if Gitlab::Database.postgresql? example.run if Gitlab::Database.postgresql?
end end
config.around(:each, :mysql) do |example|
example.run if Gitlab::Database.mysql?
end
# This makes sure the `ApplicationController#can?` method is stubbed with the # This makes sure the `ApplicationController#can?` method is stubbed with the
# original implementation for all view specs. # original implementation for all view specs.
config.before(:each, type: :view) do config.before(:each, type: :view) do
......
...@@ -26,7 +26,19 @@ module CycleAnalyticsHelpers ...@@ -26,7 +26,19 @@ module CycleAnalyticsHelpers
ref: 'refs/heads/master').execute ref: 'refs/heads/master').execute
end end
def create_merge_request_closing_issue(issue, message: nil, source_branch: nil, commit_message: 'commit message') def create_cycle(user, project, issue, mr, milestone, pipeline)
issue.update(milestone: milestone)
pipeline.run
ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
ci_build
end
def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
if !source_branch || project.repository.commit(source_branch).blank? if !source_branch || project.repository.commit(source_branch).blank?
source_branch = generate(:branch) source_branch = generate(:branch)
project.repository.add_branch(user, source_branch, 'master') project.repository.add_branch(user, source_branch, 'master')
...@@ -52,19 +64,19 @@ module CycleAnalyticsHelpers ...@@ -52,19 +64,19 @@ module CycleAnalyticsHelpers
mr mr
end end
def merge_merge_requests_closing_issue(issue) def merge_merge_requests_closing_issue(user, project, issue)
merge_requests = issue.closed_by_merge_requests(user) merge_requests = issue.closed_by_merge_requests(user)
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) } merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end end
def deploy_master(environment: 'production') def deploy_master(user, project, environment: 'production')
dummy_job = dummy_job =
case environment case environment
when 'production' when 'production'
dummy_production_job dummy_production_job(user, project)
when 'staging' when 'staging'
dummy_staging_job dummy_staging_job(user, project)
else else
raise ArgumentError raise ArgumentError
end end
...@@ -72,25 +84,24 @@ module CycleAnalyticsHelpers ...@@ -72,25 +84,24 @@ module CycleAnalyticsHelpers
CreateDeploymentService.new(dummy_job).execute CreateDeploymentService.new(dummy_job).execute
end end
def dummy_production_job def dummy_production_job(user, project)
@dummy_job ||= new_dummy_job('production') new_dummy_job(user, project, 'production')
end end
def dummy_staging_job def dummy_staging_job(user, project)
@dummy_job ||= new_dummy_job('staging') new_dummy_job(user, project, 'staging')
end end
def dummy_pipeline def dummy_pipeline(project)
@dummy_pipeline ||= Ci::Pipeline.new(
Ci::Pipeline.new( sha: project.repository.commit('master').sha,
sha: project.repository.commit('master').sha, ref: 'master',
ref: 'master', source: :push,
source: :push, project: project,
project: project, protected: false)
protected: false)
end end
def new_dummy_job(environment) def new_dummy_job(user, project, environment)
project.environments.find_or_create_by(name: environment) project.environments.find_or_create_by(name: environment)
Ci::Build.new( Ci::Build.new(
...@@ -101,7 +112,7 @@ module CycleAnalyticsHelpers ...@@ -101,7 +112,7 @@ module CycleAnalyticsHelpers
tag: false, tag: false,
name: 'dummy', name: 'dummy',
stage: 'dummy', stage: 'dummy',
pipeline: dummy_pipeline, pipeline: dummy_pipeline(project),
protected: false) protected: false)
end end
......
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