diff options
49 files changed, 772 insertions, 127 deletions
diff --git a/app/models/cycle_analytics/group_level.rb b/app/models/cycle_analytics/group_level.rb new file mode 100644 index 00000000000..508cde0ca00 --- /dev/null +++ b/app/models/cycle_analytics/group_level.rb @@ -0,0 +1,31 @@ +# 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 diff --git a/app/models/cycle_analytics/base.rb b/app/models/cycle_analytics/level_base.rb index d7b28cd1b67..543349ebf8f 100644 --- a/app/models/cycle_analytics/base.rb +++ b/app/models/cycle_analytics/level_base.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module CycleAnalytics - class Base + module LevelBase STAGES = %i[issue plan code test review staging production].freeze def all_medians_by_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 @@ -21,7 +21,7 @@ module CycleAnalytics end def [](stage_name) - Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) + Gitlab::CycleAnalytics::Stage[stage_name].new(options: options) end end end diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb index 22631cc7d41..4aa426c58a1 100644 --- a/app/models/cycle_analytics/project_level.rb +++ b/app/models/cycle_analytics/project_level.rb @@ -1,12 +1,13 @@ # frozen_string_literal: true module CycleAnalytics - class ProjectLevel < Base + class ProjectLevel + include LevelBase attr_reader :project, :options def initialize(project, options:) @project = project - @options = options + @options = options.merge(project: project) end def summary diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb index ab15bd0ac7a..29d4a6ae1d0 100644 --- a/app/serializers/analytics_issue_entity.rb +++ b/app/serializers/analytics_issue_entity.rb @@ -20,12 +20,12 @@ class AnalyticsIssueEntity < Grape::Entity end expose :url do |object| - url_to(:namespace_project_issue, id: object[:iid].to_s) + url_to(:namespace_project_issue, object) end private - def url_to(route, id) - public_send("#{route}_url", request.project.namespace, request.project, id) # rubocop:disable GitlabSecurity/PublicSend + def url_to(route, object) + public_send("#{route}_url", object[:path], object[:name], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend end end diff --git a/app/serializers/analytics_merge_request_entity.rb b/app/serializers/analytics_merge_request_entity.rb index b7134da9461..21d7eeb81b0 100644 --- a/app/serializers/analytics_merge_request_entity.rb +++ b/app/serializers/analytics_merge_request_entity.rb @@ -4,6 +4,6 @@ class AnalyticsMergeRequestEntity < AnalyticsIssueEntity expose :state expose :url do |object| - url_to(:namespace_project_merge_request, id: object[:iid].to_s) + url_to(:namespace_project_merge_request, object) end end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb index 8bc6da5aeeb..eb38b90fb18 100644 --- a/app/serializers/analytics_stage_entity.rb +++ b/app/serializers/analytics_stage_entity.rb @@ -8,9 +8,9 @@ class AnalyticsStageEntity < Grape::Entity expose :legend 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 # 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 diff --git a/app/serializers/group_analytics_stage_entity.rb b/app/serializers/group_analytics_stage_entity.rb new file mode 100644 index 00000000000..81be20e7dd8 --- /dev/null +++ b/app/serializers/group_analytics_stage_entity.rb @@ -0,0 +1,16 @@ +# 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 diff --git a/app/serializers/group_analytics_stage_serializer.rb b/app/serializers/group_analytics_stage_serializer.rb new file mode 100644 index 00000000000..ec448dea602 --- /dev/null +++ b/app/serializers/group_analytics_stage_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class GroupAnalyticsStageSerializer < BaseSerializer + entity GroupAnalyticsStageEntity +end diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb index 0cacef5b278..96aa799e864 100644 --- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -5,12 +5,11 @@ module Gitlab class BaseEventFetcher include BaseQuery - attr_reader :projections, :query, :stage, :order, :project, :options + attr_reader :projections, :query, :stage, :order, :options MAX_EVENTS = 50 - def initialize(project: nil, stage:, options:) - @project = project + def initialize(stage:, options:) @stage = stage @options = options end @@ -68,11 +67,23 @@ module Gitlab end 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 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 diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb index 39fc1759cfc..9c98c0bfbf2 100644 --- a/lib/gitlab/cycle_analytics/base_query.rb +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -16,17 +16,25 @@ module Gitlab def stage_query(project_ids) query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])) .join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) + .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")) .where(issue_table[:project_id].in(project_ids)) + .where(routes_table[:source_type].eq('Namespace')) .where(issue_table[:created_at].gteq(options[:from])) # 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])) .join(mr_metrics_table) .on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) - - query end end end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb index 98b86e54340..678a891e941 100644 --- a/lib/gitlab/cycle_analytics/base_stage.rb +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -5,10 +5,9 @@ module Gitlab class BaseStage include BaseQuery - attr_reader :project, :options + attr_reader :options - def initialize(project: nil, options:) - @project = project + def initialize(options:) @options = options end @@ -24,7 +23,7 @@ module Gitlab raise NotImplementedError.new("Expected #{self.name} to implement title") end - def median + def project_median return if project.nil? BatchLoader.for(project.id).batch(key: name) do |project_ids, loader| @@ -42,6 +41,10 @@ module Gitlab end end + def group_median + median_query(projects.map(&:id)) + end + 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). # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). @@ -67,8 +70,7 @@ module Gitlab private def event_fetcher - @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: project, - stage: name, + @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(stage: name, options: event_options) end @@ -77,7 +79,15 @@ module Gitlab end 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 diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index 9e7ca529579..1e4e9b9e02c 100644 --- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -11,7 +11,9 @@ module Gitlab mr_table[:id], mr_table[:created_at], mr_table[:state], - mr_table[:author_id]] + mr_table[:author_id], + projects_table[:name], + routes_table[:path]] @order = mr_table[:created_at] super(*args) @@ -20,7 +22,7 @@ module Gitlab private def serialize(event) - AnalyticsMergeRequestSerializer.new(project: project).represent(event) + AnalyticsMergeRequestSerializer.new(serialization_context).represent(event) end def allowed_ids_finder_class diff --git a/lib/gitlab/cycle_analytics/group_stage_summary.rb b/lib/gitlab/cycle_analytics/group_stage_summary.rb new file mode 100644 index 00000000000..7b5c74e1a1b --- /dev/null +++ b/lib/gitlab/cycle_analytics/group_stage_summary.rb @@ -0,0 +1,24 @@ +# 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 diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index bb3520ae920..2d03e425a6a 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -10,7 +10,9 @@ module Gitlab issue_table[:iid], issue_table[:id], issue_table[:created_at], - issue_table[:author_id]] + issue_table[:author_id], + projects_table[:name], + routes_table[:path]] super(*args) end @@ -18,7 +20,7 @@ module Gitlab private def serialize(event) - AnalyticsIssueSerializer.new(project: project).represent(event) + AnalyticsIssueSerializer.new(serialization_context).represent(event) end def allowed_ids_finder_class diff --git a/lib/gitlab/cycle_analytics/issue_helper.rb b/lib/gitlab/cycle_analytics/issue_helper.rb index ac836b8bf0f..0fc4f1dd41a 100644 --- a/lib/gitlab/cycle_analytics/issue_helper.rb +++ b/lib/gitlab/cycle_analytics/issue_helper.rb @@ -5,8 +5,11 @@ module Gitlab module IssueHelper def stage_query(project_ids) 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")) .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_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil))) diff --git a/lib/gitlab/cycle_analytics/metrics_tables.rb b/lib/gitlab/cycle_analytics/metrics_tables.rb index 3e0302d308d..015f7bfde24 100644 --- a/lib/gitlab/cycle_analytics/metrics_tables.rb +++ b/lib/gitlab/cycle_analytics/metrics_tables.rb @@ -35,6 +35,14 @@ module Gitlab User.arel_table end + def projects_table + Project.arel_table + end + + def routes_table + Route.arel_table + end + def build_table ::CommitStatus.arel_table end diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb index 03ba98b4dfb..55214e6b896 100644 --- a/lib/gitlab/cycle_analytics/permissions.rb +++ b/lib/gitlab/cycle_analytics/permissions.rb @@ -23,7 +23,7 @@ module Gitlab end def get - ::CycleAnalytics::Base::STAGES.each do |stage| + ::CycleAnalytics::LevelBase::STAGES.each do |stage| @stage_permission_hash[stage] = authorized_stage?(stage) end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 49a6b099f34..77cc358daa9 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -10,7 +10,9 @@ module Gitlab issue_table[:iid], issue_table[:id], issue_table[:created_at], - issue_table[:author_id]] + issue_table[:author_id], + projects_table[:name], + routes_table[:path]] super(*args) end @@ -18,7 +20,7 @@ module Gitlab private def serialize(event) - AnalyticsIssueSerializer.new(project: project).represent(event) + AnalyticsIssueSerializer.new(serialization_context).represent(event) end def allowed_ids_finder_class diff --git a/lib/gitlab/cycle_analytics/plan_helper.rb b/lib/gitlab/cycle_analytics/plan_helper.rb index ae578d45ad5..c3f742503a9 100644 --- a/lib/gitlab/cycle_analytics/plan_helper.rb +++ b/lib/gitlab/cycle_analytics/plan_helper.rb @@ -5,14 +5,21 @@ module Gitlab module PlanHelper def stage_query(project_ids) 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")) .where(issue_table[:project_id].in(project_ids)) - .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)) + .where(routes_table[:source_type].eq('Namespace')) + query = add_conditions_to_query(query) query 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 diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb index 949119d69a0..404b2460814 100644 --- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -10,7 +10,9 @@ module Gitlab issue_table[:iid], issue_table[:id], issue_table[:created_at], - issue_table[:author_id]] + issue_table[:author_id], + projects_table[:name], + routes_table[:path]] super(*args) end @@ -18,7 +20,7 @@ module Gitlab private def serialize(event) - AnalyticsIssueSerializer.new(project: project).represent(event) + AnalyticsIssueSerializer.new(serialization_context).represent(event) end def allowed_ids_finder_class diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index d31736e755d..6acd12517fa 100644 --- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -11,7 +11,9 @@ module Gitlab mr_table[:id], mr_table[:created_at], mr_table[:state], - mr_table[:author_id]] + mr_table[:author_id], + projects_table[:name], + routes_table[:path]] super(*args) end @@ -19,7 +21,7 @@ module Gitlab private def serialize(event) - AnalyticsMergeRequestSerializer.new(project: project).represent(event) + AnalyticsMergeRequestSerializer.new(serialization_context).represent(event) end def allowed_ids_finder_class diff --git a/lib/gitlab/cycle_analytics/summary/group/base.rb b/lib/gitlab/cycle_analytics/summary/group/base.rb new file mode 100644 index 00000000000..7f18b61d309 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/group/base.rb @@ -0,0 +1,24 @@ +# 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 diff --git a/lib/gitlab/cycle_analytics/summary/group/deploy.rb b/lib/gitlab/cycle_analytics/summary/group/deploy.rb new file mode 100644 index 00000000000..d8fcd8f2ce4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/group/deploy.rb @@ -0,0 +1,29 @@ +# 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 diff --git a/lib/gitlab/cycle_analytics/summary/group/issue.rb b/lib/gitlab/cycle_analytics/summary/group/issue.rb new file mode 100644 index 00000000000..70073e6d843 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/group/issue.rb @@ -0,0 +1,25 @@ +# 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 diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb index 8b07da11c5d..b7a64adc2ff 100644 --- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb @@ -9,12 +9,12 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do let(:options) do { start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs, - from: 30.days.ago } + from: 30.days.ago, + project: project } end subject do - described_class.new(project: project, - stage: :issue, + described_class.new(stage: :issue, options: options).fetch end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb index c738cc49c1f..933f3c7896e 100644 --- a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -5,40 +5,114 @@ describe Gitlab::CycleAnalytics::CodeStage do let(:stage_name) { :code } let(:project) { create(:project) } - 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_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_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(project: project, options: { from: 2.days.ago, current_user: project.creator }) } + 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_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_2) { create(:merge_request, source_project: project, created_at: 10.minutes.ago, source_branch: 'A') } + let(:stage) { described_class.new(options: { from: 2.days.ago, current_user: project.creator, project: project }) } before do 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_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_2, issue: issue_2) end it_behaves_like 'base stage' - describe '#median' do + describe '#project_median' do around do |example| Timecop.freeze { example.run } end 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 describe '#events' do + subject { stage.events } + 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 + + 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(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 - expect(result.count).to eq(2) - expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) + 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 diff --git a/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb b/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb new file mode 100644 index 00000000000..eea4f33ccb8 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/group_stage_summary_spec.rb @@ -0,0 +1,88 @@ +# 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 diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb index 3b6af9cbaed..ffd0b84cb57 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -4,11 +4,11 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::IssueStage do let(:stage_name) { :issue } let(:project) { create(:project) } - 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_3) { create(:issue, project: project, created_at: 30.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_3) { create(:issue, project: project, created_at: 30.minutes.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 issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago ) @@ -24,7 +24,7 @@ describe Gitlab::CycleAnalytics::IssueStage do end 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 @@ -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) 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 diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb index 506a8160412..3cd1320ca9c 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::CycleAnalytics::PlanStage do 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_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 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 it_behaves_like 'base stage' - describe '#median' do + describe '#project_median' do around do |example| Timecop.freeze { example.run } end 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 describe '#events' do + subject { stage.events } + 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 + + 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, 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 - expect(result.count).to eq(2) - expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title) + 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 diff --git a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb index f072a9644e8..6d14973c711 100644 --- a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -4,14 +4,14 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::ReviewStage do let(:stage_name) { :review } let(:project) { create(:project) } - 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_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_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(: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_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_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_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 mr_1.metrics.update!(merged_at: 30.minutes.ago) @@ -24,22 +24,66 @@ describe Gitlab::CycleAnalytics::ReviewStage do it_behaves_like 'base stage' - describe '#median' do + describe '#project_median' do around do |example| Timecop.freeze { example.run } end 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 describe '#events' do + subject { stage.events } + 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) - expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) + 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 end end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index c22d27f60d6..b001a46001e 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' shared_examples 'default query config' do 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 expect(event.stage).not_to be_nil diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index 1a4b572cc11..c146146723f 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' shared_examples 'base stage' do ISSUES_MEDIAN = 30.minutes.to_i - let(:stage) { described_class.new(project: double, options: {}) } + let(:stage) { described_class.new(options: { project: double }) } 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({}) end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb index 17d5fbb9733..9ca12cc448c 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -5,16 +5,16 @@ describe Gitlab::CycleAnalytics::StagingStage do let(:stage_name) { :staging } let(:project) { create(:project) } - 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_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_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(: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_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_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(:build_1) { 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 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 it_behaves_like 'base stage' - describe '#median' do + describe '#project_median' do around do |example| Timecop.freeze { example.run } end 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 describe '#events' do + subject { stage.events } + 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 + + 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_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 } - expect(result.count).to eq(2) - expect(result.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name) + 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 diff --git a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb index 8633a63849f..41028c44a00 100644 --- a/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -4,7 +4,7 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::TestStage do let(:stage_name) { :test } 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' @@ -36,7 +36,7 @@ describe Gitlab::CycleAnalytics::TestStage do end 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 diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb index 8122e85a981..ad61bdeace7 100644 --- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::CycleAnalytics::UsageData do 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) stage_values = result[:avg_cycle_analytics][stage] diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index db6e70973ae..808659552ff 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -38,7 +38,7 @@ describe 'CycleAnalytics#code' do merge_merge_requests_closing_issue(user, project, issue) deploy_master(user, project) - expect(subject[:code].median).to be_nil + expect(subject[:code].project_median).to be_nil end end end @@ -68,7 +68,7 @@ describe 'CycleAnalytics#code' do 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 diff --git a/spec/models/cycle_analytics/group_level_spec.rb b/spec/models/cycle_analytics/group_level_spec.rb new file mode 100644 index 00000000000..154c1b9c0f8 --- /dev/null +++ b/spec/models/cycle_analytics/group_level_spec.rb @@ -0,0 +1,46 @@ +# 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 diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 4ccbdf29df6..8cdf83b1292 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -43,7 +43,7 @@ describe 'CycleAnalytics#issue' do create_merge_request_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 diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb index c99c38e9cf3..28ad9bd194d 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -47,7 +47,7 @@ describe 'CycleAnalytics#plan' do create_merge_request_closing_issue(user, project, issue, source_branch: branch_name) merge_merge_requests_closing_issue(user, project, issue) - expect(subject[:issue].median).to be_nil + expect(subject[:issue].project_median).to be_nil end end end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index ddd199362d1..613c1786540 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -41,7 +41,7 @@ describe 'CycleAnalytics#production' do MergeRequests::MergeService.new(project, user).execute(merge_request) deploy_master(user, project) - expect(subject[:production].median).to be_nil + expect(subject[:production].project_median).to be_nil end end @@ -52,7 +52,7 @@ describe 'CycleAnalytics#production' do MergeRequests::MergeService.new(project, user).execute(merge_request) deploy_master(user, project, environment: 'staging') - expect(subject[:production].median).to be_nil + expect(subject[:production].project_median).to be_nil end end end diff --git a/spec/models/cycle_analytics/project_level_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb index 77bd0bfeb9c..4de01b1c679 100644 --- a/spec/models/cycle_analytics/project_level_spec.rb +++ b/spec/models/cycle_analytics/project_level_spec.rb @@ -23,7 +23,7 @@ describe CycleAnalytics::ProjectLevel do 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 + hsh[stage_name] = subject[stage_name].project_median.presence end expect(subject.all_medians_by_stage).to eq(values) diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 63c481ed465..ef88fd86340 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -28,7 +28,7 @@ describe 'CycleAnalytics#review' do it "returns nil" do 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 diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index c134b97553f..571792559d8 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging' do MergeRequests::MergeService.new(project, user).execute(merge_request) deploy_master(user, project) - expect(subject[:staging].median).to be_nil + expect(subject[:staging].project_median).to be_nil end end @@ -56,7 +56,7 @@ describe 'CycleAnalytics#staging' do MergeRequests::MergeService.new(project, user).execute(merge_request) deploy_master(user, project, environment: 'staging') - expect(subject[:staging].median).to be_nil + expect(subject[:staging].project_median).to be_nil end end end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index a6ea73b2699..7b3001d2bd8 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -36,7 +36,7 @@ describe 'CycleAnalytics#test' do 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 @@ -47,7 +47,7 @@ describe 'CycleAnalytics#test' do pipeline.run! pipeline.succeed! - expect(subject[:test].median).to be_nil + expect(subject[:test].project_median).to be_nil end end @@ -62,7 +62,7 @@ describe 'CycleAnalytics#test' do 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 @@ -77,7 +77,7 @@ describe 'CycleAnalytics#test' do 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 diff --git a/spec/serializers/analytics_issue_entity_spec.rb b/spec/serializers/analytics_issue_entity_spec.rb index 89588b4df2b..dd5e43a4b62 100644 --- a/spec/serializers/analytics_issue_entity_spec.rb +++ b/spec/serializers/analytics_issue_entity_spec.rb @@ -9,12 +9,14 @@ describe AnalyticsIssueEntity do iid: "1", id: "1", created_at: "2016-11-12 15:04:02.948604", - author: user + author: user, + name: project.name, + path: project.namespace } end let(:project) { create(:project) } - let(:request) { EntityRequest.new(project: project, entity: :merge_request) } + let(:request) { EntityRequest.new(entity: :merge_request) } let(:entity) do described_class.new(entity_hash, request: request, project: project) diff --git a/spec/serializers/analytics_issue_serializer_spec.rb b/spec/serializers/analytics_issue_serializer_spec.rb index 5befc28f4fa..c9ffe1c5dad 100644 --- a/spec/serializers/analytics_issue_serializer_spec.rb +++ b/spec/serializers/analytics_issue_serializer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe AnalyticsIssueSerializer do subject do described_class - .new(project: project, entity: :merge_request) + .new(entity: :merge_request) .represent(resource) end @@ -16,7 +16,9 @@ describe AnalyticsIssueSerializer do iid: "1", id: "1", created_at: "2016-11-12 15:04:02.948604", - author: user + author: user, + name: project.name, + path: project.namespace } end diff --git a/spec/serializers/analytics_merge_request_serializer_spec.rb b/spec/serializers/analytics_merge_request_serializer_spec.rb index 62067cc0ef2..123d7d795ce 100644 --- a/spec/serializers/analytics_merge_request_serializer_spec.rb +++ b/spec/serializers/analytics_merge_request_serializer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe AnalyticsMergeRequestSerializer do subject do described_class - .new(project: project, entity: :merge_request) + .new(entity: :merge_request) .represent(resource) end @@ -17,7 +17,9 @@ describe AnalyticsMergeRequestSerializer do id: "1", state: 'open', created_at: "2016-11-12 15:04:02.948604", - author: user + author: user, + name: project.name, + path: project.namespace } end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb index 5b05c2f2ef3..86a796a2d94 100644 --- a/spec/serializers/analytics_stage_serializer_spec.rb +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -6,11 +6,11 @@ describe AnalyticsStageSerializer do end let(:resource) do - Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) + Gitlab::CycleAnalytics::CodeStage.new(options: { project: double }) end 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({}) end @@ -24,7 +24,7 @@ describe AnalyticsStageSerializer do context 'when median is equal 0' 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 it 'sets the value to nil' do @@ -34,7 +34,7 @@ describe AnalyticsStageSerializer do context 'when median is below 1' 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 it 'sets the value to equal to median' do @@ -44,7 +44,7 @@ describe AnalyticsStageSerializer do context 'when median is above 1' 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 it 'sets the value to equal to median' do diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 19b32c84d81..be1c2bc3046 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -50,7 +50,7 @@ module CycleAnalyticsHelpers end 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 context "when the data belongs to another project" do @@ -80,7 +80,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions 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 @@ -103,7 +103,7 @@ module CycleAnalyticsHelpers 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 @@ -121,7 +121,7 @@ module CycleAnalyticsHelpers 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 @@ -138,7 +138,7 @@ module CycleAnalyticsHelpers 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 @@ -146,7 +146,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject[phase].median).to be_nil + expect(subject[phase].project_median).to be_nil end end end |