diff options
32 files changed, 379 insertions, 164 deletions
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue deleted file mode 100644 index 6c256fa6736..00000000000 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.vue +++ /dev/null @@ -1,59 +0,0 @@ -<script> -import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; -import iconCommit from '../svg/icon_commit.svg'; -import limitWarning from './limit_warning_component.vue'; -import totalTime from './total_time_component.vue'; - -export default { - components: { - userAvatarImage, - totalTime, - limitWarning, - }, - props: { - items: { - type: Array, - default: () => [], - }, - stage: { - type: Object, - default: () => ({}), - }, - }, - computed: { - iconCommit() { - return iconCommit; - }, - }, -}; -</script> -<template> - <div> - <div class="events-description"> - {{ stage.description }} - <limit-warning :count="items.length" /> - </div> - <ul class="stage-event-list"> - <li v-for="(commit, i) in items" :key="i" class="stage-event-item"> - <div class="item-details item-conmmit-component"> - <!-- FIXME: Pass an alt attribute here for accessibility --> - <user-avatar-image :img-src="commit.author.avatarUrl" /> - <h5 class="item-title commit-title"> - <a :href="commit.commitUrl"> {{ commit.title }} </a> - </h5> - <span> - {{ s__('FirstPushedBy|First') }} <span class="commit-icon" v-html="iconCommit"> </span> - <a :href="commit.commitUrl" class="commit-hash-link commit-sha">{{ - commit.shortSha - }}</a> - {{ s__('FirstPushedBy|pushed by') }} - <a :href="commit.author.webUrl" class="commit-author-link"> - {{ commit.author.name }} - </a> - </span> - </div> - <div class="item-time"><total-time :time="commit.totalTime" /></div> - </li> - </ul> - </div> -</template> diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 3f0a9f2602c..b56e08175cc 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -5,7 +5,6 @@ import Flash from '../flash'; import Translate from '../vue_shared/translate'; import banner from './components/banner.vue'; import stageCodeComponent from './components/stage_code_component.vue'; -import stagePlanComponent from './components/stage_plan_component.vue'; import stageComponent from './components/stage_component.vue'; import stageReviewComponent from './components/stage_review_component.vue'; import stageStagingComponent from './components/stage_staging_component.vue'; @@ -26,7 +25,7 @@ export default () => { components: { banner, 'stage-issue-component': stageComponent, - 'stage-plan-component': stagePlanComponent, + 'stage-plan-component': stageComponent, 'stage-code-component': stageCodeComponent, 'stage-test-component': stageTestComponent, 'stage-review-component': stageReviewComponent, diff --git a/changelogs/unreleased/30138-display-cycle-analytics-issue-logic-fixes.yml b/changelogs/unreleased/30138-display-cycle-analytics-issue-logic-fixes.yml new file mode 100644 index 00000000000..574995f20fa --- /dev/null +++ b/changelogs/unreleased/30138-display-cycle-analytics-issue-logic-fixes.yml @@ -0,0 +1,5 @@ +--- +title: Change logic behind cycle analytics +merge_request: 29018 +author: +type: changed diff --git a/lib/gitlab/cycle_analytics/builds_event_helper.rb b/lib/gitlab/cycle_analytics/builds_event_helper.rb new file mode 100644 index 00000000000..0d6f32fdc6f --- /dev/null +++ b/lib/gitlab/cycle_analytics/builds_event_helper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module BuildsEventHelper + def initialize(*args) + @projections = [build_table[:id]] + @order = build_table[:created_at] + + super(*args) + end + + def fetch + Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build) + + super + end + + def events_query + base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + + super + end + + private + + def allowed_ids + nil + end + + def serialize(event) + AnalyticsBuildSerializer.new.represent(event['build']) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb index 591db3c35e6..6c348f1862d 100644 --- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class CodeEventFetcher < BaseEventFetcher + include CodeHelper + def initialize(*args) @projections = [mr_table[:title], mr_table[:iid], diff --git a/lib/gitlab/cycle_analytics/code_helper.rb b/lib/gitlab/cycle_analytics/code_helper.rb new file mode 100644 index 00000000000..8f28bdd2502 --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module CodeHelper + def stage_query(project_ids) + super(project_ids).where(mr_table[:created_at].gteq(issue_metrics_table[:first_mentioned_in_commit_at])) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb index 2e5f9ef5a40..89a6430221c 100644 --- a/lib/gitlab/cycle_analytics/code_stage.rb +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class CodeStage < BaseStage + include CodeHelper + def start_time_attrs @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] end diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 30c6ead8968..8a870f2e2a3 100644 --- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class IssueEventFetcher < BaseEventFetcher + include IssueHelper + def initialize(*args) @projections = [issue_table[:title], issue_table[:iid], diff --git a/lib/gitlab/cycle_analytics/issue_helper.rb b/lib/gitlab/cycle_analytics/issue_helper.rb new file mode 100644 index 00000000000..c9266341378 --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module IssueHelper + def stage_query(project_ids) + query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) + .project(issue_table[:project_id].as("project_id")) + .where(issue_table[:project_id].in(project_ids)) + .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables + .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 + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb index 4eae2da512c..738cb3eba03 100644 --- a/lib/gitlab/cycle_analytics/issue_stage.rb +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class IssueStage < BaseStage + include IssueHelper + def start_time_attrs @start_time_attrs ||= issue_table[:created_at] end diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb index aeca9d00156..d924f956dcd 100644 --- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -3,60 +3,26 @@ module Gitlab module CycleAnalytics class PlanEventFetcher < BaseEventFetcher + include PlanHelper + def initialize(*args) - @projections = [mr_diff_table[:id], - issue_metrics_table[:first_mentioned_in_commit_at]] + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] super(*args) end - def events_query - base_query - .join(mr_diff_table) - .on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) - - super - end - private - def allowed_ids - nil - end - - def merge_request_diff_commits - @merge_request_diff_commits ||= - MergeRequestDiffCommit - .where(merge_request_diff_id: event_result.map { |event| event['id'] }) - .group_by(&:merge_request_diff_id) - end - def serialize(event) - commit = first_time_reference_commit(event) - - return unless commit - - serialize_commit(event, commit, query) - end - - def first_time_reference_commit(event) - return unless event && merge_request_diff_commits - - commits = merge_request_diff_commits[event['id'].to_i] - - return if commits.blank? - - commits.find do |commit| - next unless commit[:committed_date] && event['first_mentioned_in_commit_at'] - - commit[:committed_date].to_i == DateTime.parse(event['first_mentioned_in_commit_at'].to_s).to_i - end + AnalyticsIssueSerializer.new(project: @project).represent(event) end - def serialize_commit(event, commit, query) - commit = Commit.from_hash(commit.to_hash, @project) - - AnalyticsCommitSerializer.new(project: @project, total_time: event['total_time']).represent(commit) + def allowed_ids_finder_class + IssuesFinder end end end diff --git a/lib/gitlab/cycle_analytics/plan_helper.rb b/lib/gitlab/cycle_analytics/plan_helper.rb new file mode 100644 index 00000000000..30fc2ce6d40 --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module PlanHelper + def stage_query(project_ids) + query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])) + .project(issue_table[:project_id].as("project_id")) + .where(issue_table[:project_id].in(project_ids)) + .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables + .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)) + + query + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb index 513e4575be0..0b27d114f52 100644 --- a/lib/gitlab/cycle_analytics/plan_stage.rb +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class PlanStage < BaseStage + include PlanHelper + def start_time_attrs @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], issue_metrics_table[:first_added_to_board_at]] @@ -21,7 +23,7 @@ module Gitlab end def legend - _("Related Commits") + _("Related Issues") end def description diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb index 6681cb42c90..6bcbe0412a9 100644 --- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -2,7 +2,28 @@ module Gitlab module CycleAnalytics - class ProductionEventFetcher < IssueEventFetcher + class ProductionEventFetcher < BaseEventFetcher + include ProductionHelper + + def initialize(*args) + @projections = [issue_table[:title], + issue_table[:iid], + issue_table[:id], + issue_table[:created_at], + issue_table[:author_id]] + + super(*args) + end + + private + + def serialize(event) + AnalyticsIssueSerializer.new(project: @project).represent(event) + end + + def allowed_ids_finder_class + IssuesFinder + end end end end diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb index de100295281..b6354b5ffad 100644 --- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class ReviewEventFetcher < BaseEventFetcher + include ReviewHelper + def initialize(*args) @projections = [mr_table[:title], mr_table[:iid], diff --git a/lib/gitlab/cycle_analytics/review_helper.rb b/lib/gitlab/cycle_analytics/review_helper.rb new file mode 100644 index 00000000000..c53249652b5 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module ReviewHelper + def stage_query(project_ids) + super(project_ids).where(mr_metrics_table[:merged_at].not_eq(nil)) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb index 294b656bc55..e9df8cd5a05 100644 --- a/lib/gitlab/cycle_analytics/review_stage.rb +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class ReviewStage < BaseStage + include ReviewHelper + def start_time_attrs @start_time_attrs ||= mr_table[:created_at] end diff --git a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb index 70ce82383b3..1454a1a33eb 100644 --- a/lib/gitlab/cycle_analytics/staging_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -3,34 +3,8 @@ module Gitlab module CycleAnalytics class StagingEventFetcher < BaseEventFetcher - def initialize(*args) - @projections = [build_table[:id]] - @order = build_table[:created_at] - - super(*args) - end - - def fetch - Updater.update!(event_result, from: 'id', to: 'build', klass: ::Ci::Build) - - super - end - - def events_query - base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) - - super - end - - private - - def allowed_ids - nil - end - - def serialize(event) - AnalyticsBuildSerializer.new.represent(event['build']) - end + include ProductionHelper + include BuildsEventHelper end end end diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb index dbc2414ff66..e03627c6cd1 100644 --- a/lib/gitlab/cycle_analytics/staging_stage.rb +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -4,6 +4,7 @@ module Gitlab module CycleAnalytics class StagingStage < BaseStage include ProductionHelper + def start_time_attrs @start_time_attrs ||= mr_metrics_table[:merged_at] end diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb index 4d5ea5b7c34..2fa44b1b364 100644 --- a/lib/gitlab/cycle_analytics/test_event_fetcher.rb +++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb @@ -2,7 +2,9 @@ module Gitlab module CycleAnalytics - class TestEventFetcher < StagingEventFetcher + class TestEventFetcher < BaseEventFetcher + include TestHelper + include BuildsEventHelper end end end diff --git a/lib/gitlab/cycle_analytics/test_helper.rb b/lib/gitlab/cycle_analytics/test_helper.rb new file mode 100644 index 00000000000..32fca7fa898 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_helper.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Gitlab + module CycleAnalytics + module TestHelper + def stage_query(project_ids) + if branch + super(project_ids).where(build_table[:ref].eq(branch)) + else + super(project_ids) + end + end + + private + + def branch + @branch ||= @options[:branch] # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb index c31b664148b..4787a906c07 100644 --- a/lib/gitlab/cycle_analytics/test_stage.rb +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -3,6 +3,8 @@ module Gitlab module CycleAnalytics class TestStage < BaseStage + include TestHelper + def start_time_attrs @start_time_attrs ||= mr_metrics_table[:latest_build_started_at] end @@ -26,14 +28,6 @@ module Gitlab def description _("Total test time for all commits/merges") end - - def stage_query(project_ids) - if @options[:branch] - super(project_ids).where(build_table[:ref].eq(@options[:branch])) - else - super(project_ids) - end - end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 53a1d1c5466..186c4cb2dba 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4500,12 +4500,6 @@ msgstr "" msgid "First day of the week" msgstr "" -msgid "FirstPushedBy|First" -msgstr "" - -msgid "FirstPushedBy|pushed by" -msgstr "" - msgid "FlowdockService|Flowdock Git source token" msgstr "" @@ -8313,9 +8307,6 @@ msgstr "" msgid "Registry" msgstr "" -msgid "Related Commits" -msgstr "" - msgid "Related Deployed Jobs" msgstr "" diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index 48edc764a8e..4108a0f370d 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -58,7 +58,7 @@ describe 'Cycle Analytics', :js do expect_issue_to_be_present click_stage('Plan') - expect(find('.stage-events')).to have_content(mr.commits.last.title) + expect_issue_to_be_present click_stage('Code') expect_merge_request_to_be_present diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb index e8fc67acf05..c738cc49c1f 100644 --- a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -4,5 +4,41 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' 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 }) } + + 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_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 + around do |example| + Timecop.freeze { example.run } + end + + it 'counts median from issues with metrics' do + expect(stage.median).to eq(ISSUES_MEDIAN) + end + end + + describe '#events' do + it 'exposes merge requests that closes issues' do + result = stage.events + + expect(result.count).to eq(2) + expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) + end + end end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 397dd4e5d2c..f8b103c0fab 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -53,20 +53,28 @@ describe 'cycle analytics events' do describe '#plan_events' do let(:stage) { :plan } - it 'has a title' do - expect(events.first[:title]).not_to be_nil + before do + create_commit_referencing_issue(context) end - it 'has a sha short ID' do - expect(events.first[:short_sha]).not_to be_nil + it 'has the total time' do + expect(events.first[:total_time]).not_to be_empty + end + + it 'has a title' do + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(events.first[:commit_url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end - it 'has the total time' do - expect(events.first[:total_time]).not_to be_empty + it 'has an iid' do + expect(events.first[:iid]).to eq(context.iid.to_s) + end + + it 'has a created_at timestamp' do + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do @@ -78,12 +86,13 @@ describe 'cycle analytics events' do end it "has the author's name" do - expect(events.first[:author][:name]).not_to be_nil + expect(events.first[:author][:name]).to eq(context.author.name) end end describe '#code_events' do let(:stage) { :code } + let!(:merge_request) { MergeRequest.first } before do create_commit_referencing_issue(context) @@ -122,6 +131,7 @@ describe 'cycle analytics events' do let(:stage) { :test } let(:merge_request) { MergeRequest.first } + let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let!(:pipeline) do create(:ci_pipeline, @@ -137,6 +147,7 @@ describe 'cycle analytics events' do pipeline.run! pipeline.succeed! + merge_merge_requests_closing_issue(user, project, context) end it 'has the name' do @@ -180,6 +191,10 @@ describe 'cycle analytics events' do let(:stage) { :review } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } + before do + merge_merge_requests_closing_issue(user, project, context) + end + it 'has the total time' do expect(events.first[:total_time]).not_to be_empty end diff --git a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb index 3127f01989d..3b6af9cbaed 100644 --- a/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -3,6 +3,37 @@ 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_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 }) } + + before do + issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago ) + issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago) + issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) + end it_behaves_like 'base stage' + + describe '#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) + end + end + + describe '#events' do + it 'exposes issues with metrics' do + result = stage.events + + expect(result.count).to eq(3) + expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title, issue_3.title) + 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 4c715921ad6..506a8160412 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -3,6 +3,37 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' describe Gitlab::CycleAnalytics::PlanStage do let(:stage_name) { :plan } + 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_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 }) } + + before do + issue_1.metrics.update!(first_associated_with_milestone_at: 60.minutes.ago, first_mentioned_in_commit_at: 10.minutes.ago) + issue_2.metrics.update!(first_added_to_board_at: 30.minutes.ago, first_mentioned_in_commit_at: 20.minutes.ago) + issue_3.metrics.update!(first_added_to_board_at: 15.minutes.ago) + end it_behaves_like 'base stage' + + describe '#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) + end + end + + describe '#events' do + it 'exposes issues with metrics' do + result = stage.events + + expect(result.count).to eq(2) + expect(result.map { |event| event[:title] }).to contain_exactly(issue_1.title, issue_2.title) + 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 1412c8dfa08..f072a9644e8 100644 --- a/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -3,6 +3,43 @@ 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!(: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 }) } + + before do + mr_1.metrics.update!(merged_at: 30.minutes.ago) + mr_2.metrics.update!(merged_at: 10.minutes.ago) + + 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_3, issue: issue_3) + end it_behaves_like 'base stage' + + describe '#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) + end + end + + describe '#events' do + it 'exposes merge requests that close issues' do + result = stage.events + + expect(result.count).to eq(2) + expect(result.map { |event| event[:title] }).to contain_exactly(mr_1.title, mr_2.title) + end + end end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb index 08425acbfc8..1a4b572cc11 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' shared_examples 'base stage' do + ISSUES_MEDIAN = 30.minutes.to_i + let(:stage) { described_class.new(project: double, options: {}) } before do diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb index 8154b3ac701..17d5fbb9733 100644 --- a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -4,5 +4,46 @@ require 'lib/gitlab/cycle_analytics/shared_stage_spec' 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(: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 }) } + + 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_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).commit_id) + + 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_3, issue: issue_3) + end + it_behaves_like 'base stage' + + describe '#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) + end + end + + describe '#events' do + it 'exposes builds connected to merge request' do + result = stage.events + + expect(result.count).to eq(2) + expect(result.map { |event| event[:name] }).to contain_exactly(build_1.name, build_2.name) + end + end end diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index 49412b628b3..25390f8a23e 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -32,10 +32,10 @@ describe 'cycle analytics events' do it 'lists the plan events' do get project_cycle_analytics_plan_path(project, format: :json) - first_mr_short_sha = project.merge_requests.sort_by_attribute(:created_asc).first.commits.first.short_id + first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s expect(json_response['events']).not_to be_empty - expect(json_response['events'].first['short_sha']).to eq(first_mr_short_sha) + expect(json_response['events'].first['iid']).to eq(first_issue_iid) end it 'lists the code events' do |