Commit 0d1adc9f authored by Sean McGivern's avatar Sean McGivern

Merge branch 'adjust-cycle-analytics-to-group-level' into 'master'

Adjust cycle analytics to group level

See merge request gitlab-org/gitlab-ce!30391
parents f4ce99c5 196dfd12
# frozen_string_literal: true
module CycleAnalytics
class GroupLevel
include LevelBase
attr_reader :options, :group
def initialize(group:, options:)
@group = group
@options = options.merge(group: group)
end
def summary
@summary ||= ::Gitlab::CycleAnalytics::GroupStageSummary.new(group,
from: options[:from],
current_user: options[:current_user]).data
end
def permissions(*)
STAGES.each_with_object({}) do |stage, obj|
obj[stage] = true
end
end
def stats
@stats ||= STAGES.map do |stage_name|
self[stage_name].as_json(serializer: GroupAnalyticsStageSerializer)
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
module CycleAnalytics module CycleAnalytics
class Base module LevelBase
STAGES = %i[issue plan code test review staging production].freeze STAGES = %i[issue plan code test review staging production].freeze
def all_medians_by_stage def all_medians_by_stage
STAGES.each_with_object({}) do |stage_name, medians_per_stage| STAGES.each_with_object({}) do |stage_name, medians_per_stage|
medians_per_stage[stage_name] = self[stage_name].median medians_per_stage[stage_name] = self[stage_name].project_median
end end
end end
...@@ -21,7 +21,7 @@ module CycleAnalytics ...@@ -21,7 +21,7 @@ module CycleAnalytics
end end
def [](stage_name) def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) Gitlab::CycleAnalytics::Stage[stage_name].new(options: options)
end end
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
module CycleAnalytics module CycleAnalytics
class ProjectLevel < Base class ProjectLevel
include LevelBase
attr_reader :project, :options attr_reader :project, :options
def initialize(project, options:) def initialize(project, options:)
@project = project @project = project
@options = options @options = options.merge(project: project)
end end
def summary def summary
......
...@@ -20,12 +20,12 @@ class AnalyticsIssueEntity < Grape::Entity ...@@ -20,12 +20,12 @@ class AnalyticsIssueEntity < Grape::Entity
end end
expose :url do |object| expose :url do |object|
url_to(:namespace_project_issue, id: object[:iid].to_s) url_to(:namespace_project_issue, object)
end end
private private
def url_to(route, id) def url_to(route, object)
public_send("#{route}_url", request.project.namespace, request.project, id) # rubocop:disable GitlabSecurity/PublicSend public_send("#{route}_url", object[:path], object[:name], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend
end end
end end
...@@ -4,6 +4,6 @@ class AnalyticsMergeRequestEntity < AnalyticsIssueEntity ...@@ -4,6 +4,6 @@ class AnalyticsMergeRequestEntity < AnalyticsIssueEntity
expose :state expose :state
expose :url do |object| expose :url do |object|
url_to(:namespace_project_merge_request, id: object[:iid].to_s) url_to(:namespace_project_merge_request, object)
end end
end end
...@@ -8,9 +8,9 @@ class AnalyticsStageEntity < Grape::Entity ...@@ -8,9 +8,9 @@ class AnalyticsStageEntity < Grape::Entity
expose :legend expose :legend
expose :description expose :description
expose :median, as: :value do |stage| expose :project_median, as: :value do |stage|
# median returns a BatchLoader instance which we first have to unwrap by using to_f # median returns a BatchLoader instance which we first have to unwrap by using to_f
# we use to_f to make sure results below 1 are presented to the end-user # we use to_f to make sure results below 1 are presented to the end-user
stage.median.to_f.nonzero? ? distance_of_time_in_words(stage.median) : nil stage.project_median.to_f.nonzero? ? distance_of_time_in_words(stage.project_median) : nil
end end
end end
# frozen_string_literal: true
class GroupAnalyticsStageEntity < Grape::Entity
include EntityDateHelper
expose :title
expose :name
expose :legend
expose :description
expose :group_median, as: :value do |stage|
# group_median returns a BatchLoader instance which we first have to unwrap by using to_f
# we use to_f to make sure results below 1 are presented to the end-user
stage.group_median.to_f.nonzero? ? distance_of_time_in_words(stage.group_median) : nil
end
end
# frozen_string_literal: true
class GroupAnalyticsStageSerializer < BaseSerializer
entity GroupAnalyticsStageEntity
end
...@@ -5,12 +5,11 @@ module Gitlab ...@@ -5,12 +5,11 @@ module Gitlab
class BaseEventFetcher class BaseEventFetcher
include BaseQuery include BaseQuery
attr_reader :projections, :query, :stage, :order, :project, :options attr_reader :projections, :query, :stage, :order, :options
MAX_EVENTS = 50 MAX_EVENTS = 50
def initialize(project: nil, stage:, options:) def initialize(stage:, options:)
@project = project
@stage = stage @stage = stage
@options = options @options = options
end end
...@@ -68,11 +67,23 @@ module Gitlab ...@@ -68,11 +67,23 @@ module Gitlab
end end
def allowed_ids_source def allowed_ids_source
{ project_id: project.id } group ? { group_id: group.id, include_subgroups: true } : { project_id: project.id }
end
def serialization_context
{}
end end
def projects def projects
[project] group ? Project.inside_path(group.full_path) : [project]
end
def group
@group ||= options.fetch(:group, nil)
end
def project
@project ||= options.fetch(:project, nil)
end end
end end
end end
......
...@@ -16,17 +16,25 @@ module Gitlab ...@@ -16,17 +16,25 @@ module Gitlab
def stage_query(project_ids) 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]))
.join(projects_table).on(issue_table[:project_id].eq(projects_table[:id]))
.join(routes_table).on(projects_table[:namespace_id].eq(routes_table[:source_id]))
.project(issue_table[:project_id].as("project_id")) .project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids)) .where(issue_table[:project_id].in(project_ids))
.where(routes_table[:source_type].eq('Namespace'))
.where(issue_table[:created_at].gteq(options[:from])) .where(issue_table[:created_at].gteq(options[:from]))
# Load merge_requests # Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin)
query = load_merge_requests(query)
query
end
def load_merge_requests(query)
query.join(mr_table, Arel::Nodes::OuterJoin)
.on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])) .on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id]))
.join(mr_metrics_table) .join(mr_metrics_table)
.on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) .on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
query
end end
end end
end end
......
...@@ -5,10 +5,9 @@ module Gitlab ...@@ -5,10 +5,9 @@ module Gitlab
class BaseStage class BaseStage
include BaseQuery include BaseQuery
attr_reader :project, :options attr_reader :options
def initialize(project: nil, options:) def initialize(options:)
@project = project
@options = options @options = options
end end
...@@ -24,7 +23,7 @@ module Gitlab ...@@ -24,7 +23,7 @@ module Gitlab
raise NotImplementedError.new("Expected #{self.name} to implement title") raise NotImplementedError.new("Expected #{self.name} to implement title")
end end
def median def project_median
return if project.nil? return if project.nil?
BatchLoader.for(project.id).batch(key: name) do |project_ids, loader| BatchLoader.for(project.id).batch(key: name) do |project_ids, loader|
...@@ -42,6 +41,10 @@ module Gitlab ...@@ -42,6 +41,10 @@ module Gitlab
end end
end end
def group_median
median_query(projects.map(&:id))
end
def median_query(project_ids) def median_query(project_ids)
# 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).
...@@ -67,8 +70,7 @@ module Gitlab ...@@ -67,8 +70,7 @@ module Gitlab
private private
def event_fetcher def event_fetcher
@event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: project, @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(stage: name,
stage: name,
options: event_options) options: event_options)
end end
...@@ -77,7 +79,15 @@ module Gitlab ...@@ -77,7 +79,15 @@ module Gitlab
end end
def projects def projects
[project] group ? Project.inside_path(group.full_path) : [project]
end
def group
@group ||= options.fetch(:group, nil)
end
def project
@project ||= options.fetch(:project, nil)
end end
end end
end end
......
...@@ -11,7 +11,9 @@ module Gitlab ...@@ -11,7 +11,9 @@ module Gitlab
mr_table[:id], mr_table[:id],
mr_table[:created_at], mr_table[:created_at],
mr_table[:state], mr_table[:state],
mr_table[:author_id]] mr_table[:author_id],
projects_table[:name],
routes_table[:path]]
@order = mr_table[:created_at] @order = mr_table[:created_at]
super(*args) super(*args)
...@@ -20,7 +22,7 @@ module Gitlab ...@@ -20,7 +22,7 @@ module Gitlab
private private
def serialize(event) def serialize(event)
AnalyticsMergeRequestSerializer.new(project: project).represent(event) AnalyticsMergeRequestSerializer.new(serialization_context).represent(event)
end end
def allowed_ids_finder_class def allowed_ids_finder_class
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
class GroupStageSummary
def initialize(group, from:, current_user:)
@group = group
@from = from
@current_user = current_user
end
def data
[serialize(Summary::Group::Issue.new(group: @group, from: @from, current_user: @current_user)),
serialize(Summary::Group::Deploy.new(group: @group, from: @from))]
end
private
def serialize(summary_object)
AnalyticsSummarySerializer.new.represent(summary_object)
end
end
end
end
...@@ -10,7 +10,9 @@ module Gitlab ...@@ -10,7 +10,9 @@ module Gitlab
issue_table[:iid], issue_table[:iid],
issue_table[:id], issue_table[:id],
issue_table[:created_at], issue_table[:created_at],
issue_table[:author_id]] issue_table[:author_id],
projects_table[:name],
routes_table[:path]]
super(*args) super(*args)
end end
...@@ -18,7 +20,7 @@ module Gitlab ...@@ -18,7 +20,7 @@ module Gitlab
private private
def serialize(event) def serialize(event)
AnalyticsIssueSerializer.new(project: project).represent(event) AnalyticsIssueSerializer.new(serialization_context).represent(event)
end end
def allowed_ids_finder_class def allowed_ids_finder_class
......
...@@ -5,8 +5,11 @@ module Gitlab ...@@ -5,8 +5,11 @@ module Gitlab
module IssueHelper module IssueHelper
def stage_query(project_ids) def stage_query(project_ids)
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.join(projects_table).on(issue_table[:project_id].eq(projects_table[:id]))
.join(routes_table).on(projects_table[:namespace_id].eq(routes_table[:source_id]))
.project(issue_table[:project_id].as("project_id")) .project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids)) .where(issue_table[:project_id].in(project_ids))
.where(routes_table[:source_type].eq('Namespace'))
.where(issue_table[:created_at].gteq(options[:from])) .where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil))) .where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
......
...@@ -35,6 +35,14 @@ module Gitlab ...@@ -35,6 +35,14 @@ module Gitlab
User.arel_table User.arel_table
end end
def projects_table
Project.arel_table
end
def routes_table
Route.arel_table
end
def build_table def build_table
::CommitStatus.arel_table ::CommitStatus.arel_table
end end
......
...@@ -23,7 +23,7 @@ module Gitlab ...@@ -23,7 +23,7 @@ module Gitlab
end end
def get def get
::CycleAnalytics::Base::STAGES.each do |stage| ::CycleAnalytics::LevelBase::STAGES.each do |stage|
@stage_permission_hash[stage] = authorized_stage?(stage) @stage_permission_hash[stage] = authorized_stage?(stage)
end end
......
...@@ -10,7 +10,9 @@ module Gitlab ...@@ -10,7 +10,9 @@ module Gitlab
issue_table[:iid], issue_table[:iid],
issue_table[:id], issue_table[:id],
issue_table[:created_at], issue_table[:created_at],
issue_table[:author_id]] issue_table[:author_id],
projects_table[:name],
routes_table[:path]]
super(*args) super(*args)
end end
...@@ -18,7 +20,7 @@ module Gitlab ...@@ -18,7 +20,7 @@ module Gitlab
private private
def serialize(event) def serialize(event)
AnalyticsIssueSerializer.new(project: project).represent(event) AnalyticsIssueSerializer.new(serialization_context).represent(event)
end end
def allowed_ids_finder_class def allowed_ids_finder_class
......
...@@ -5,14 +5,21 @@ module Gitlab ...@@ -5,14 +5,21 @@ module Gitlab
module PlanHelper module PlanHelper
def stage_query(project_ids) def stage_query(project_ids)
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.join(projects_table).on(issue_table[:project_id].eq(projects_table[:id]))
.join(routes_table).on(projects_table[:namespace_id].eq(routes_table[:source_id]))
.project(issue_table[:project_id].as("project_id")) .project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids)) .where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(options[:from])) .where(routes_table[:source_type].eq('Namespace'))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil))) query = add_conditions_to_query(query)
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
query query
end end
def add_conditions_to_query(query)
query.where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
end
end end
end end
end end
...@@ -10,7 +10,9 @@ module Gitlab ...@@ -10,7 +10,9 @@ module Gitlab
issue_table[:iid], issue_table[:iid],
issue_table[:id], issue_table[:id],
issue_table[:created_at], issue_table[:created_at],
issue_table[:author_id]] issue_table[:author_id],
projects_table[:name],
routes_table[:path]]
super(*args) super(*args)
end end
...@@ -18,7 +20,7 @@ module Gitlab ...@@ -18,7 +20,7 @@ module Gitlab
private private
def serialize(event) def serialize(event)
AnalyticsIssueSerializer.new(project: project).represent(event) AnalyticsIssueSerializer.new(serialization_context).represent(event)
end end
def allowed_ids_finder_class def allowed_ids_finder_class
......
...@@ -11,7 +11,9 @@ module Gitlab ...@@ -11,7 +11,9 @@ module Gitlab
mr_table[:id], mr_table[:id],
mr_table[:created_at], mr_table[:created_at],
mr_table[:state], mr_table[:state],
mr_table[:author_id]] mr_table[:author_id],
projects_table[:name],
routes_table[:path]]
super(*args) super(*args)
end end
...@@ -19,7 +21,7 @@ module Gitlab ...@@ -19,7 +21,7 @@ module Gitlab
private private
def serialize(event) def serialize(event)
AnalyticsMergeRequestSerializer.new(project: project).represent(event) AnalyticsMergeRequestSerializer.new(serialization_context).represent(event)
end end
def allowed_ids_finder_class def allowed_ids_finder_class
......
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module Summary
module Group
class Base
def initialize(group:, from:)
@group = group
@from = from
end
def title
raise NotImplementedError.new("Expected #{self.name} to implement title")
end
def value
raise NotImplementedError.new("Expected #{self.name} to implement value")
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module Summary
module Group
class Deploy < Group::Base
def title
n_('Deploy', 'Deploys', value)
end
def value
@value ||= Deployment.joins(:project)
.where(projects: { id: projects })
.where("deployments.created_at > ?", @from)
.success
.count
end
private
def projects
Project.inside_path(@group.full_path).ids
end
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module CycleAnalytics
module Summary
module Group
class Issue < Group::Base
def initialize(group:, from:, current_user:)
@group = group
@from = from
@current_user = current_user
end
def title
n_('New Issue', 'New Issues', value)
end
def value
@value ||= IssuesFinder.new(@current_user, group_id: @group.id, include_subgroups: true, created_after: @from).execute.count
end
end
end
end
end
end
...@@ -9,12 +9,12 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do ...@@ -9,12 +9,12 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
let(:options) do let(:options) do
{ start_time_attrs: start_time_attrs, { start_time_attrs: start_time_attrs,
end_time_attrs: end_time_attrs, end_time_attrs: end_time_attrs,
from: 30.days.ago } from: 30.days.ago,
project: project }
end end
subject do subject do
described_class.new(project: project, described_class.new(stage: :issue,
stage: :issue,
options: options).fetch options: options).fetch
end end
......
...@@ -5,40 +5,114 @@ describe Gitlab::CycleAnalytics::CodeStage do ...@@ -5,40 +5,114 @@ describe Gitlab::CycleAnalytics::CodeStage do
let(:stage_name) { :code } let(:stage_name) { :code }
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, source_project: project, created_at: 15.minutes.ago) } let(:mr_1) { create(:merge_request, source_project: project, created_at: 15.minutes.ago) }
let!(:mr_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') } let(:mr_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) }
before do before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago) issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago)
issue_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) issue_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
issue_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago) issue_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B')
create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1) create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_1)
create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2) create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2)
end end
it_behaves_like 'base stage' it_behaves_like 'base stage'
describe '#median' do describe '#project_median' do
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
describe '#events' do describe '#events' do
subject { stage.events }
it 'exposes merge requests that closes issues' do it 'exposes merge requests that closes issues' do
result = stage.events expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title)
end
end
context 'when group is given' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_2) { create(:project, group: group) }
let(:project_3) { create(:project, group: group) }
let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) }
let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) }
let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) }
let(:mr_2_1) { create(:merge_request, source_project: project_2, created_at: 15.minutes.ago) }
let(:mr_2_2) { create(:merge_request, source_project: project_3, created_at: 10.minutes.ago, source_branch: 'A') }
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) }
before do
group.add_owner(user)
issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago)
issue_2_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
issue_2_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
create(:merge_requests_closing_issues, merge_request: mr_2_1, issue: issue_2_1)
create(:merge_requests_closing_issues, merge_request: mr_2_2, issue: issue_2_2)
end
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
expect(result.count).to eq(2) describe '#events' do
expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title)
end
end
context 'when subgroup is given' do
let(:subgroup) { create(:group, parent: group) }
let(:project_4) { create(:project, group: subgroup) }
let(:project_5) { create(:project, group: subgroup) }
let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) }
let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
let(:mr_3_1) { create(:merge_request, source_project: project_4, created_at: 15.minutes.ago) }
let(:mr_3_2) { create(:merge_request, source_project: project_5, created_at: 10.minutes.ago, source_branch: 'A') }
before do
issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 45.minutes.ago)
issue_3_2.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
issue_3_3.metrics.update!(first_added_to_board_at: 60.minutes.ago, first_mentioned_in_commit_at: 40.minutes.ago)
create(:merge_requests_closing_issues, merge_request: mr_3_1, issue: issue_3_1)
create(:merge_requests_closing_issues, merge_request: mr_3_2, issue: issue_3_2)
end
describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(4)
expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title, mr_3_1.title, mr_3_2.title)
end
it 'exposes merge requests that close issues with full path for subgroup' do
expect(subject.count).to eq(4)
expect(subject.find { |event| event[:title] == mr_3_1.title }[:url]).to include("#{subgroup.full_path}")
end
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::CycleAnalytics::GroupStageSummary do
let(:group) { create(:group) }
let(:project) { create(:project, :repository, namespace: group) }
let(:project_2) { create(:project, :repository, namespace: group) }
let(:from) { 1.day.ago }
let(:user) { create(:user, :admin) }
subject { described_class.new(group, from: Time.now, current_user: user).data }
describe "#new_issues" do
context 'with from date' do
before do
Timecop.freeze(5.days.ago) { create(:issue, project: project) }
Timecop.freeze(5.days.ago) { create(:issue, project: project_2) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project_2) }
end
it "finds the number of issues created after it" do
expect(subject.first[:value]).to eq(2)
end
context 'with subgroups' do
before do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: create(:group, parent: group))) }
end
it "finds issues from them" do
expect(subject.first[:value]).to eq(3)
end
end
end
context 'with other projects' do
before do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project, namespace: create(:group))) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { create(:issue, project: project_2) }
end
it "doesn't find issues from them" do
expect(subject.first[:value]).to eq(2)
end
end
end
describe "#deploys" do
context 'with from date' do
before do
Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project) }
Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project) }
Timecop.freeze(5.days.ago) { create(:deployment, :success, project: project_2) }
Timecop.freeze(5.days.from_now) { create(:deployment, :success, project: project_2) }
end
it "finds the number of deploys made created after it" do
expect(subject.second[:value]).to eq(2)
end
context 'with subgroups' do
before do
Timecop.freeze(5.days.from_now) do
create(:deployment, :success, project: create(:project, :repository, namespace: create(:group, parent: group)))
end
end
it "finds deploys from them" do
expect(subject.second[:value]).to eq(3)
end
end
end
context 'with other projects' do
before do
Timecop.freeze(5.days.from_now) do
create(:deployment, :success, project: create(:project, :repository, namespace: create(:group)))
end
end
it "doesn't find deploys from them" do
expect(subject.second[:value]).to eq(0)
end
end
end
end
...@@ -4,11 +4,11 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' ...@@ -4,11 +4,11 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::IssueStage do describe Gitlab::CycleAnalytics::IssueStage do
let(:stage_name) { :issue } let(:stage_name) { :issue }
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) } let(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) } let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
before do before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago ) issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago )
...@@ -24,7 +24,7 @@ describe Gitlab::CycleAnalytics::IssueStage do ...@@ -24,7 +24,7 @@ describe Gitlab::CycleAnalytics::IssueStage do
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
...@@ -36,4 +36,67 @@ describe Gitlab::CycleAnalytics::IssueStage do ...@@ -36,4 +36,67 @@ describe Gitlab::CycleAnalytics::IssueStage do
expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title, issue_3.title) expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title, issue_3.title)
end end
end end
context 'when group is given' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_2) { create(:project, group: group) }
let(:project_3) { create(:project, group: group) }
let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) }
let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) }
let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) }
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) }
before do
group.add_owner(user)
issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago)
issue_2_2.metrics.update!(first_added_to_board_at: 30.minutes.ago)
end
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title)
end
end
context 'when subgroup is given' do
let(:subgroup) { create(:group, parent: group) }
let(:project_4) { create(:project, group: subgroup) }
let(:project_5) { create(:project, group: subgroup) }
let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) }
let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
before do
issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago)
issue_3_2.metrics.update!(first_added_to_board_at: 30.minutes.ago)
end
describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(4)
expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title, issue_3_1.title, issue_3_2.title)
end
it 'exposes merge requests that close issues with full path for subgroup' do
expect(subject.count).to eq(4)
expect(subject.find { |event| event[:title] == issue_3_1.title }[:url]).to include("#{subgroup.full_path}")
end
end
end
end
end end
...@@ -8,7 +8,7 @@ describe Gitlab::CycleAnalytics::PlanStage do ...@@ -8,7 +8,7 @@ describe Gitlab::CycleAnalytics::PlanStage do
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) } let!(:issue_3) { create(:issue, project: project, created_at: 30.minutes.ago) }
let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) } let!(:issue_without_milestone) { create(:issue, project: project, created_at: 1.minute.ago) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
before do before do
issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago) issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago)
...@@ -18,22 +18,88 @@ describe Gitlab::CycleAnalytics::PlanStage do ...@@ -18,22 +18,88 @@ describe Gitlab::CycleAnalytics::PlanStage do
it_behaves_like 'base stage' it_behaves_like 'base stage'
describe '#median' do describe '#project_median' do
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
describe '#events' do describe '#events' do
subject { stage.events }
it 'exposes issues with metrics' do it 'exposes issues with metrics' do
result = stage.events expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title)
end
end
context 'when group is given' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_2) { create(:project, group: group) }
let(:project_3) { create(:project, group: group) }
let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) }
let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) }
let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) }
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) }
before do
group.add_owner(user)
issue_2_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago)
issue_2_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago)
issue_2_3.metrics.update!(first_added_to_board_at: 15.minutes.ago)
end
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
expect(result.count).to eq(2) describe '#events' do
expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title) subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title)
end
end
context 'when subgroup is given' do
let(:subgroup) { create(:group, parent: group) }
let(:project_4) { create(:project, group: subgroup) }
let(:project_5) { create(:project, group: subgroup) }
let(:issue_3_1) { create(:issue, project: project_4, created_at: 90.minutes.ago) }
let(:issue_3_2) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
let(:issue_3_3) { create(:issue, project: project_5, created_at: 60.minutes.ago) }
before do
issue_3_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago)
issue_3_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago)
issue_3_3.metrics.update!(first_added_to_board_at: 15.minutes.ago)
end
describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(4)
expect(subject.map { |event| event[:title] }).to contain_exactly(issue_2_1.title, issue_2_2.title, issue_3_1.title, issue_3_2.title)
end
it 'exposes merge requests that close issues with full path for subgroup' do
expect(subject.count).to eq(4)
expect(subject.find { |event| event[:title] == issue_3_1.title }[:url]).to include("#{subgroup.full_path}")
end
end
end end
end end
end end
...@@ -4,14 +4,14 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' ...@@ -4,14 +4,14 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::ReviewStage do describe Gitlab::CycleAnalytics::ReviewStage do
let(:stage_name) { :review } let(:stage_name) { :review }
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) } let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
let!(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') } let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
let!(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') } let!(:mr_4) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'C') }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
before do before do
mr_1.metrics.update!(merged_at: 30.minutes.ago) mr_1.metrics.update!(merged_at: 30.minutes.ago)
...@@ -24,22 +24,66 @@ describe Gitlab::CycleAnalytics::ReviewStage do ...@@ -24,22 +24,66 @@ describe Gitlab::CycleAnalytics::ReviewStage do
it_behaves_like 'base stage' it_behaves_like 'base stage'
describe '#median' do describe '#project_median' do
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
describe '#events' do describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do it 'exposes merge requests that close issues' do
result = stage.events expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title)
end
end
context 'when group is given' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_2) { create(:project, group: group) }
let(:project_3) { create(:project, group: group) }
let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) }
let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) }
let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) }
let(:mr_2_1) { create(:merge_request, :closed, source_project: project_2, created_at: 60.minutes.ago) }
let(:mr_2_2) { create(:merge_request, :closed, source_project: project_3, created_at: 40.minutes.ago, source_branch: 'A') }
let(:mr_2_3) { create(:merge_request, source_project: project_2, created_at: 10.minutes.ago, source_branch: 'B') }
let!(:mr_2_4) { create(:merge_request, source_project: project_3, created_at: 10.minutes.ago, source_branch: 'C') }
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) }
before do
group.add_owner(user)
mr_2_1.metrics.update!(merged_at: 30.minutes.ago)
mr_2_2.metrics.update!(merged_at: 10.minutes.ago)
create(:merge_requests_closing_issues, merge_request: mr_2_1, issue: issue_2_1)
create(:merge_requests_closing_issues, merge_request: mr_2_2, issue: issue_2_2)
create(:merge_requests_closing_issues, merge_request: mr_2_3, issue: issue_2_3)
end
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
subject { stage.events }
expect(result.count).to eq(2) it 'exposes merge requests that close issues' do
expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) expect(subject.count).to eq(2)
expect(subject.map { |event| event[:title] }).to contain_exactly(mr_2_1.title, mr_2_2.title)
end
end end
end end
end end
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
shared_examples 'default query config' do shared_examples 'default query config' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } let(:event) { described_class.new(stage: stage_name, options: { from: 1.day.ago, project: project }) }
it 'has the stage attribute' do it 'has the stage attribute' do
expect(event.stage).not_to be_nil expect(event.stage).not_to be_nil
......
...@@ -3,10 +3,10 @@ require 'spec_helper' ...@@ -3,10 +3,10 @@ require 'spec_helper'
shared_examples 'base stage' do shared_examples 'base stage' do
ISSUES_MEDIAN = 30.minutes.to_i ISSUES_MEDIAN = 30.minutes.to_i
let(:stage) { described_class.new(project: double, options: {}) } let(:stage) { described_class.new(options: { project: double }) }
before do before do
allow(stage).to receive(:median).and_return(1.12) allow(stage).to receive(:project_median).and_return(1.12)
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
end end
......
...@@ -5,16 +5,16 @@ describe Gitlab::CycleAnalytics::StagingStage do ...@@ -5,16 +5,16 @@ describe Gitlab::CycleAnalytics::StagingStage do
let(:stage_name) { :staging } let(:stage_name) { :staging }
let(:project) { create(:project) } let(:project) { create(:project) }
let!(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) } let(:issue_1) { create(:issue, project: project, created_at: 90.minutes.ago) }
let!(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_2) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) } let(:issue_3) { create(:issue, project: project, created_at: 60.minutes.ago) }
let!(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) } let(:mr_1) { create(:merge_request, :closed, source_project: project, created_at: 60.minutes.ago) }
let!(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') } let(:mr_2) { create(:merge_request, :closed, source_project: project, created_at: 40.minutes.ago, source_branch: 'A') }
let!(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') } let(:mr_3) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'B') }
let(:build_1) { create(:ci_build, project: project) } let(:build_1) { create(:ci_build, project: project) }
let(:build_2) { create(:ci_build, project: project) } let(:build_2) { create(:ci_build, project: project) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
before do before do
mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id) mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id)
...@@ -28,22 +28,68 @@ describe Gitlab::CycleAnalytics::StagingStage do ...@@ -28,22 +28,68 @@ describe Gitlab::CycleAnalytics::StagingStage do
it_behaves_like 'base stage' it_behaves_like 'base stage'
describe '#median' do describe '#project_median' do
around do |example| around do |example|
Timecop.freeze { example.run } Timecop.freeze { example.run }
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
describe '#events' do describe '#events' do
subject { stage.events }
it 'exposes builds connected to merge request' do it 'exposes builds connected to merge request' do
result = stage.events expect(subject.count).to eq(2)
expect(subject.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name)
end
end
expect(result.count).to eq(2) context 'when group is given' do
expect(result.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name) let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project_2) { create(:project, group: group) }
let(:project_3) { create(:project, group: group) }
let(:issue_2_1) { create(:issue, project: project_2, created_at: 90.minutes.ago) }
let(:issue_2_2) { create(:issue, project: project_3, created_at: 60.minutes.ago) }
let(:issue_2_3) { create(:issue, project: project_2, created_at: 60.minutes.ago) }
let(:mr_1) { create(:merge_request, :closed, source_project: project_2, created_at: 60.minutes.ago) }
let(:mr_2) { create(:merge_request, :closed, source_project: project_3, created_at: 40.minutes.ago, source_branch: 'A') }
let(:mr_3) { create(:merge_request, source_project: project_2, created_at: 10.minutes.ago, source_branch: 'B') }
let(:build_1) { create(:ci_build, project: project_2) }
let(:build_2) { create(:ci_build, project: project_3) }
let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: user, group: group }) }
before do
group.add_owner(user)
mr_1.metrics.update!(merged_at: 80.minutes.ago, first_deployed_to_production_at: 50.minutes.ago, pipeline_id: build_1.commit_id)
mr_2.metrics.update!(merged_at: 60.minutes.ago, first_deployed_to_production_at: 30.minutes.ago, pipeline_id: build_2.commit_id)
mr_3.metrics.update!(merged_at: 10.minutes.ago, first_deployed_to_production_at: 3.days.ago, pipeline_id: create(:ci_build, project: project_2).commit_id)
create(:merge_requests_closing_issues, merge_request: mr_1, issue: issue_2_1)
create(:merge_requests_closing_issues, merge_request: mr_2, issue: issue_2_2)
create(:merge_requests_closing_issues, merge_request: mr_3, issue: issue_2_3)
end
describe '#group_median' do
around do |example|
Timecop.freeze { example.run }
end
it 'counts median from issues with metrics' do
expect(stage.group_median).to eq(ISSUES_MEDIAN)
end
end
describe '#events' do
subject { stage.events }
it 'exposes merge requests that close issues' do
expect(subject.count).to eq(2)
expect(subject.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name)
end
end end
end end
end end
...@@ -4,7 +4,7 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' ...@@ -4,7 +4,7 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec'
describe Gitlab::CycleAnalytics::TestStage do describe Gitlab::CycleAnalytics::TestStage do
let(:stage_name) { :test } let(:stage_name) { :test }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:stage) { described_class.new(project: project, options: { from: 2.days.ago, current_user: project.creator }) } let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) }
it_behaves_like 'base stage' it_behaves_like 'base stage'
...@@ -36,7 +36,7 @@ describe Gitlab::CycleAnalytics::TestStage do ...@@ -36,7 +36,7 @@ describe Gitlab::CycleAnalytics::TestStage do
end end
it 'counts median from issues with metrics' do it 'counts median from issues with metrics' do
expect(stage.median).to eq(ISSUES_MEDIAN) expect(stage.project_median).to eq(ISSUES_MEDIAN)
end end
end end
end end
...@@ -34,7 +34,7 @@ describe Gitlab::CycleAnalytics::UsageData do ...@@ -34,7 +34,7 @@ describe Gitlab::CycleAnalytics::UsageData do
expect(result).to have_key(:avg_cycle_analytics) expect(result).to have_key(:avg_cycle_analytics)
CycleAnalytics::Base::STAGES.each do |stage| CycleAnalytics::LevelBase::STAGES.each do |stage|
expect(result[:avg_cycle_analytics]).to have_key(stage) expect(result[:avg_cycle_analytics]).to have_key(stage)
stage_values = result[:avg_cycle_analytics][stage] stage_values = result[:avg_cycle_analytics][stage]
......
...@@ -38,7 +38,7 @@ describe 'CycleAnalytics#code' do ...@@ -38,7 +38,7 @@ describe 'CycleAnalytics#code' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
deploy_master(user, project) deploy_master(user, project)
expect(subject[:code].median).to be_nil expect(subject[:code].project_median).to be_nil
end end
end end
end end
...@@ -68,7 +68,7 @@ describe 'CycleAnalytics#code' do ...@@ -68,7 +68,7 @@ describe 'CycleAnalytics#code' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:code].median).to be_nil expect(subject[:code].project_median).to be_nil
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe CycleAnalytics::GroupLevel do
let(:group) { create(:group)}
let(:project) { create(:project, :repository, namespace: group) }
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(group: group, options: { from: from_date, current_user: user }) }
describe '#permissions' do
it 'returns true for all stages' do
expect(subject.permissions.values.uniq).to eq([true])
end
end
describe '#stats' 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 medians for each stage for a specific group' do
expect(subject.no_stats?).to eq(false)
end
end
describe '#summary' do
before do
create_cycle(user, project, issue, mr, milestone, pipeline)
deploy_master(user, project)
end
it 'returns medians for each stage for a specific group' do
expect(subject.summary.map { |summary| summary[:value] }).to contain_exactly(1, 1)
end
end
end
...@@ -43,7 +43,7 @@ describe 'CycleAnalytics#issue' do ...@@ -43,7 +43,7 @@ describe 'CycleAnalytics#issue' do
create_merge_request_closing_issue(user, project, issue) create_merge_request_closing_issue(user, project, issue)
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil expect(subject[:issue].project_median).to be_nil
end end
end end
end end
...@@ -47,7 +47,7 @@ describe 'CycleAnalytics#plan' do ...@@ -47,7 +47,7 @@ describe 'CycleAnalytics#plan' do
create_merge_request_closing_issue(user, project, issue, source_branch: branch_name) create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil expect(subject[:issue].project_median).to be_nil
end end
end end
end end
...@@ -41,7 +41,7 @@ describe 'CycleAnalytics#production' do ...@@ -41,7 +41,7 @@ describe 'CycleAnalytics#production' do
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project) deploy_master(user, project)
expect(subject[:production].median).to be_nil expect(subject[:production].project_median).to be_nil
end end
end end
...@@ -52,7 +52,7 @@ describe 'CycleAnalytics#production' do ...@@ -52,7 +52,7 @@ describe 'CycleAnalytics#production' do
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project, environment: 'staging') deploy_master(user, project, environment: 'staging')
expect(subject[:production].median).to be_nil expect(subject[:production].project_median).to be_nil
end end
end end
end end
...@@ -23,7 +23,7 @@ describe CycleAnalytics::ProjectLevel do ...@@ -23,7 +23,7 @@ describe CycleAnalytics::ProjectLevel do
it 'returns every median for each stage for a specific project' do it 'returns every median for each stage for a specific project' do
values = described_class::STAGES.each_with_object({}) do |stage_name, hsh| values = described_class::STAGES.each_with_object({}) do |stage_name, hsh|
hsh[stage_name] = subject[stage_name].median.presence hsh[stage_name] = subject[stage_name].project_median.presence
end end
expect(subject.all_medians_by_stage).to eq(values) expect(subject.all_medians_by_stage).to eq(values)
......
...@@ -28,7 +28,7 @@ describe 'CycleAnalytics#review' do ...@@ -28,7 +28,7 @@ describe 'CycleAnalytics#review' do
it "returns nil" do it "returns nil" do
MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
expect(subject[:review].median).to be_nil expect(subject[:review].project_median).to be_nil
end end
end end
end end
...@@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging' do ...@@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging' do
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project) deploy_master(user, project)
expect(subject[:staging].median).to be_nil expect(subject[:staging].project_median).to be_nil
end end
end end
...@@ -56,7 +56,7 @@ describe 'CycleAnalytics#staging' do ...@@ -56,7 +56,7 @@ describe 'CycleAnalytics#staging' do
MergeRequests::MergeService.new(project, user).execute(merge_request) MergeRequests::MergeService.new(project, user).execute(merge_request)
deploy_master(user, project, environment: 'staging') deploy_master(user, project, environment: 'staging')
expect(subject[:staging].median).to be_nil expect(subject[:staging].project_median).to be_nil
end end
end end
end end
...@@ -36,7 +36,7 @@ describe 'CycleAnalytics#test' do ...@@ -36,7 +36,7 @@ describe 'CycleAnalytics#test' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].project_median).to be_nil
end end
end end
...@@ -47,7 +47,7 @@ describe 'CycleAnalytics#test' do ...@@ -47,7 +47,7 @@ describe 'CycleAnalytics#test' do
pipeline.run! pipeline.run!
pipeline.succeed! pipeline.succeed!
expect(subject[:test].median).to be_nil expect(subject[:test].project_median).to be_nil
end end
end end
...@@ -62,7 +62,7 @@ describe 'CycleAnalytics#test' do ...@@ -62,7 +62,7 @@ describe 'CycleAnalytics#test' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].project_median).to be_nil
end end
end end
...@@ -77,7 +77,7 @@ describe 'CycleAnalytics#test' do ...@@ -77,7 +77,7 @@ describe 'CycleAnalytics#test' do
merge_merge_requests_closing_issue(user, project, issue) merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil expect(subject[:test].project_median).to be_nil
end end
end end
end end
...@@ -9,12 +9,14 @@ describe AnalyticsIssueEntity do ...@@ -9,12 +9,14 @@ describe AnalyticsIssueEntity do
iid: "1", iid: "1",
id: "1", id: "1",
created_at: "2016-11-12 15:04:02.948604", created_at: "2016-11-12 15:04:02.948604",
author: user author: user,
name: project.name,
path: project.namespace
} }
end end
let(:project) { create(:project) } let(:project) { create(:project) }
let(:request) { EntityRequest.new(project: project, entity: :merge_request) } let(:request) { EntityRequest.new(entity: :merge_request) }
let(:entity) do let(:entity) do
described_class.new(entity_hash, request: request, project: project) described_class.new(entity_hash, request: request, project: project)
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe AnalyticsIssueSerializer do describe AnalyticsIssueSerializer do
subject do subject do
described_class described_class
.new(project: project, entity: :merge_request) .new(entity: :merge_request)
.represent(resource) .represent(resource)
end end
...@@ -16,7 +16,9 @@ describe AnalyticsIssueSerializer do ...@@ -16,7 +16,9 @@ describe AnalyticsIssueSerializer do
iid: "1", iid: "1",
id: "1", id: "1",
created_at: "2016-11-12 15:04:02.948604", created_at: "2016-11-12 15:04:02.948604",
author: user author: user,
name: project.name,
path: project.namespace
} }
end end
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe AnalyticsMergeRequestSerializer do describe AnalyticsMergeRequestSerializer do
subject do subject do
described_class described_class
.new(project: project, entity: :merge_request) .new(entity: :merge_request)
.represent(resource) .represent(resource)
end end
...@@ -17,7 +17,9 @@ describe AnalyticsMergeRequestSerializer do ...@@ -17,7 +17,9 @@ describe AnalyticsMergeRequestSerializer do
id: "1", id: "1",
state: 'open', state: 'open',
created_at: "2016-11-12 15:04:02.948604", created_at: "2016-11-12 15:04:02.948604",
author: user author: user,
name: project.name,
path: project.namespace
} }
end end
......
...@@ -6,11 +6,11 @@ describe AnalyticsStageSerializer do ...@@ -6,11 +6,11 @@ describe AnalyticsStageSerializer do
end end
let(:resource) do let(:resource) do
Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) Gitlab::CycleAnalytics::CodeStage.new(options: { project: double })
end end
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:project_median).and_return(1.12)
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
end end
...@@ -24,7 +24,7 @@ describe AnalyticsStageSerializer do ...@@ -24,7 +24,7 @@ describe AnalyticsStageSerializer do
context 'when median is equal 0' do context 'when median is equal 0' do
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(0) allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:project_median).and_return(0)
end end
it 'sets the value to nil' do it 'sets the value to nil' do
...@@ -34,7 +34,7 @@ describe AnalyticsStageSerializer do ...@@ -34,7 +34,7 @@ describe AnalyticsStageSerializer do
context 'when median is below 1' do context 'when median is below 1' do
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(0.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:project_median).and_return(0.12)
end end
it 'sets the value to equal to median' do it 'sets the value to equal to median' do
...@@ -44,7 +44,7 @@ describe AnalyticsStageSerializer do ...@@ -44,7 +44,7 @@ describe AnalyticsStageSerializer do
context 'when median is above 1' do context 'when median is above 1' do
before do before do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(60.12) allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:project_median).and_return(60.12)
end end
it 'sets the value to equal to median' do it 'sets the value to equal to median' do
......
...@@ -50,7 +50,7 @@ module CycleAnalyticsHelpers ...@@ -50,7 +50,7 @@ module CycleAnalyticsHelpers
end end
median_time_difference = time_differences.sort[2] median_time_difference = time_differences.sort[2]
expect(subject[phase].median).to be_within(5).of(median_time_difference) expect(subject[phase].project_median).to be_within(5).of(median_time_difference)
end end
context "when the data belongs to another project" do context "when the data belongs to another project" do
...@@ -80,7 +80,7 @@ module CycleAnalyticsHelpers ...@@ -80,7 +80,7 @@ module CycleAnalyticsHelpers
# Turn off the stub before checking assertions # Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original allow(self).to receive(:project).and_call_original
expect(subject[phase].median).to be_nil expect(subject[phase].project_median).to be_nil
end end
end end
...@@ -103,7 +103,7 @@ module CycleAnalyticsHelpers ...@@ -103,7 +103,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
expect(subject[phase].median).to be_nil expect(subject[phase].project_median).to be_nil
end end
end end
end end
...@@ -121,7 +121,7 @@ module CycleAnalyticsHelpers ...@@ -121,7 +121,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
expect(subject[phase].median).to be_nil expect(subject[phase].project_median).to be_nil
end end
end end
end end
...@@ -138,7 +138,7 @@ module CycleAnalyticsHelpers ...@@ -138,7 +138,7 @@ module CycleAnalyticsHelpers
post_fn[self, data] if post_fn post_fn[self, data] if post_fn
expect(subject[phase].median).to be_nil expect(subject[phase].project_median).to be_nil
end end
end end
end end
...@@ -146,7 +146,7 @@ module CycleAnalyticsHelpers ...@@ -146,7 +146,7 @@ module CycleAnalyticsHelpers
context "when none of the start / end conditions are matched" do context "when none of the start / end conditions are matched" do
it "returns nil" do it "returns nil" do
expect(subject[phase].median).to be_nil expect(subject[phase].project_median).to be_nil
end end
end end
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