diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 2aaf8f2b451..52e06f4945a 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,6 +1,10 @@ module CycleAnalyticsParams extend ActiveSupport::Concern + def options(params) + @options ||= { from: start_date(params), current_user: current_user } + end + def start_date(params) params[:start_date] == '30' ? 30.days.ago : 90.days.ago end diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 13b3eec761f..b69d46f2c41 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -9,56 +9,52 @@ module Projects before_action :authorize_read_merge_request!, only: [:code, :review] def issue - render_events(events.issue_events) + render_events(cycle_analytics[:issue].events) end def plan - render_events(events.plan_events) + render_events(cycle_analytics[:plan].events) end def code - render_events(events.code_events) + render_events(cycle_analytics[:code].events) end def test - options[:branch] = events_params[:branch_name] + options(events_params)[:branch] = events_params[:branch_name] - render_events(events.test_events) + render_events(cycle_analytics[:test].events) end def review - render_events(events.review_events) + render_events(cycle_analytics[:review].events) end def staging - render_events(events.staging_events) + render_events(cycle_analytics[:staging].events) end def production - render_events(events.production_events) + render_events(cycle_analytics[:production].events) end private - - def render_events(events_list) + + def render_events(events) respond_to do |format| format.html - format.json { render json: { events: events_list } } + format.json { render json: { events: events } } end end - def events - @events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) - end - - def options - @options ||= { from: start_date(events_params), current_user: current_user } + def cycle_analytics + @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params)) end def events_params return {} unless params[:events].present? - params[:events].slice(:start_date, :branch_name) + params[:events].permit(:start_date, :branch_name) end end end diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index ac639ef015b..88ac3ad046b 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController before_action :authorize_read_cycle_analytics! def show - @cycle_analytics = ::CycleAnalytics.new(@project, current_user, from: start_date(cycle_analytics_params)) + @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params)) - stats_values, cycle_analytics_json = generate_cycle_analytics_data - - @cycle_analytics_no_data = stats_values.blank? + @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| format.html @@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController def cycle_analytics_params return {} unless params[:cycle_analytics].present? - { start_date: params[:cycle_analytics][:start_date] } + params[:cycle_analytics].permit(:start_date) end - def generate_cycle_analytics_data - stats_values = [] - - cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], - [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], - [:code, "Code", "Related Merge Requests", "Time spent coding"], - [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"], - [:review, "Review", "Relative Merged Requests", "The time taken to review the code"], - [:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"], - [:production, "Production", "Related Issues", "The total time taken from idea to production"]] - - stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)| - value = @cycle_analytics.send(stage_method).presence - - stats_values << value.abs if value - - stats << { - title: stage_text, - description: stage_description, - legend: stage_legend, - value: value && !value.zero? ? distance_of_time_in_words(value) : nil - } - - stats - end - - issues = @cycle_analytics.summary.new_issues - commits = @cycle_analytics.summary.commits - deploys = @cycle_analytics.summary.deploys - - summary = [ - { title: "New Issue".pluralize(issues), value: issues }, - { title: "Commit".pluralize(commits), value: commits }, - { title: "Deploy".pluralize(deploys), value: deploys } - ] - - cycle_analytics_hash = { summary: summary, - stats: stats, - permissions: @cycle_analytics.permissions(user: current_user) + def cycle_analytics_json + { + summary: @cycle_analytics.summary, + stats: @cycle_analytics.stats, + permissions: @cycle_analytics.permissions(user: current_user) } - - [stats_values, cycle_analytics_hash] end end diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb index ba4ee6fcf9d..d2e626c22e8 100644 --- a/app/models/cycle_analytics.rb +++ b/app/models/cycle_analytics.rb @@ -1,62 +1,38 @@ class CycleAnalytics STAGES = %i[issue plan code test review staging production].freeze - def initialize(project, current_user, from:) + def initialize(project, options) @project = project - @current_user = current_user - @from = from - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil) + @options = options end def summary - @summary ||= Summary.new(@project, @current_user, from: @from) + @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project, + from: @options[:from], + current_user: @options[:current_user]).data + end + + def stats + @stats ||= stats_per_stage + end + + def no_stats? + stats.all? { |hash| hash[:value].nil? } end def permissions(user:) Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) end - def issue - @fetcher.calculate_metric(:issue, - Issue.arel_table[:created_at], - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]]) + def [](stage_name) + Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options) end - def plan - @fetcher.calculate_metric(:plan, - [Issue::Metrics.arel_table[:first_associated_with_milestone_at], - Issue::Metrics.arel_table[:first_added_to_board_at]], - Issue::Metrics.arel_table[:first_mentioned_in_commit_at]) - end + private - def code - @fetcher.calculate_metric(:code, - Issue::Metrics.arel_table[:first_mentioned_in_commit_at], - MergeRequest.arel_table[:created_at]) - end - - def test - @fetcher.calculate_metric(:test, - MergeRequest::Metrics.arel_table[:latest_build_started_at], - MergeRequest::Metrics.arel_table[:latest_build_finished_at]) - end - - def review - @fetcher.calculate_metric(:review, - MergeRequest.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:merged_at]) - end - - def staging - @fetcher.calculate_metric(:staging, - MergeRequest::Metrics.arel_table[:merged_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) - end - - def production - @fetcher.calculate_metric(:production, - Issue.arel_table[:created_at], - MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) + def stats_per_stage + STAGES.map do |stage_name| + self[stage_name].as_json + end end end diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb index c9910d8cd09..e69de29bb2d 100644 --- a/app/models/cycle_analytics/summary.rb +++ b/app/models/cycle_analytics/summary.rb @@ -1,43 +0,0 @@ -class CycleAnalytics - class Summary - def initialize(project, current_user, from:) - @project = project - @current_user = current_user - @from = from - end - - def new_issues - IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count - end - - def commits - ref = @project.default_branch.presence - count_commits_for(ref) - end - - def deploys - @project.deployments.where("created_at > ?", @from).count - end - - private - - # Don't use the `Gitlab::Git::Repository#log` method, because it enforces - # a limit. Since we need a commit count, we _can't_ enforce a limit, so - # the easiest way forward is to replicate the relevant portions of the - # `log` function here. - def count_commits_for(ref) - return unless ref - - repository = @project.repository.raw_repository - sha = @project.repository.commit(ref).sha - - cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log) - cmd << '--format=%H' - cmd << "--after=#{@from.iso8601}" - cmd << sha - - raw_output = IO.popen(cmd) { |io| io.read } - raw_output.lines.count - end - end -end diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb new file mode 100644 index 00000000000..a559d0850c4 --- /dev/null +++ b/app/serializers/analytics_stage_entity.rb @@ -0,0 +1,10 @@ +class AnalyticsStageEntity < Grape::Entity + include EntityDateHelper + + expose :title + expose :description + + expose :median, as: :value do |stage| + stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil + end +end diff --git a/app/serializers/analytics_stage_serializer.rb b/app/serializers/analytics_stage_serializer.rb new file mode 100644 index 00000000000..613cf6874d8 --- /dev/null +++ b/app/serializers/analytics_stage_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsStageSerializer < BaseSerializer + entity AnalyticsStageEntity +end diff --git a/app/serializers/analytics_summary_entity.rb b/app/serializers/analytics_summary_entity.rb new file mode 100644 index 00000000000..91803ec07f5 --- /dev/null +++ b/app/serializers/analytics_summary_entity.rb @@ -0,0 +1,7 @@ +class AnalyticsSummaryEntity < Grape::Entity + expose :value, safe: true + + expose :title do |object| + object.title.pluralize(object.value) + end +end diff --git a/app/serializers/analytics_summary_serializer.rb b/app/serializers/analytics_summary_serializer.rb new file mode 100644 index 00000000000..c87a24aa47c --- /dev/null +++ b/app/serializers/analytics_summary_serializer.rb @@ -0,0 +1,3 @@ +class AnalyticsSummarySerializer < BaseSerializer + entity AnalyticsSummaryEntity +end diff --git a/lib/gitlab/cycle_analytics/base_event.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb similarity index 59% rename from lib/gitlab/cycle_analytics/base_event.rb rename to lib/gitlab/cycle_analytics/base_event_fetcher.rb index 53a148ad703..0d8791d396b 100644 --- a/lib/gitlab/cycle_analytics/base_event.rb +++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb @@ -1,13 +1,13 @@ module Gitlab module CycleAnalytics - class BaseEvent - include MetricsTables + class BaseEventFetcher + include BaseQuery - attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query + attr_reader :projections, :query, :stage, :order - def initialize(project:, options:) - @query = EventsQuery.new(project: project, options: options) + def initialize(project:, stage:, options:) @project = project + @stage = stage @options = options end @@ -19,10 +19,8 @@ module Gitlab end.compact end - def custom_query(_base_query); end - def order - @order || @start_time_attrs + @order || default_order end private @@ -34,7 +32,17 @@ module Gitlab end def event_result - @event_result ||= @query.execute(self).to_a + @event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a + end + + def events_query + diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs]) + + base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc) + end + + def default_order + [@options[:start_time_attrs]].flatten.first end def serialize(_event) diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb new file mode 100644 index 00000000000..d560dca45c8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_query.rb @@ -0,0 +1,31 @@ +module Gitlab + module CycleAnalytics + module BaseQuery + include MetricsTables + include Gitlab::Database::Median + include Gitlab::Database::DateTime + + private + + def base_query + @base_query ||= stage_query + end + + def stage_query + 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])). + where(issue_table[:project_id].eq(@project.id)). + where(issue_table[:deleted_at].eq(nil)). + where(issue_table[:created_at].gteq(@options[:from])) + + # 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 +end diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb new file mode 100644 index 00000000000..74bbcdcb3dd --- /dev/null +++ b/lib/gitlab/cycle_analytics/base_stage.rb @@ -0,0 +1,54 @@ +module Gitlab + module CycleAnalytics + class BaseStage + include BaseQuery + + def initialize(project:, options:) + @project = project + @options = options + end + + def events + event_fetcher.fetch + end + + def as_json + AnalyticsStageSerializer.new.represent(self).as_json + end + + def title + name.to_s.capitalize + end + + def median + cte_table = Arel::Table.new("cte_table_for_#{name}") + + # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). + # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). + # We compute the (end_time - start_time) interval, and give it an alias based on the current + # cycle analytics stage. + interval_query = Arel::Nodes::As.new( + cte_table, + subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s)) + + median_datetime(cte_table, interval_query, name) + end + + def name + raise NotImplementedError.new("Expected #{self.name} to implement name") + end + + private + + def event_fetcher + @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project, + stage: name, + options: event_options) + end + + def event_options + @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/review_event.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb similarity index 76% rename from lib/gitlab/cycle_analytics/review_event.rb rename to lib/gitlab/cycle_analytics/code_event_fetcher.rb index b394a02cc52..5245b9ca8fc 100644 --- a/lib/gitlab/cycle_analytics/review_event.rb +++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb @@ -1,22 +1,22 @@ module Gitlab module CycleAnalytics - class ReviewEvent < BaseEvent + class CodeEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :review - @start_time_attrs = mr_table[:created_at] - @end_time_attrs = mr_metrics_table[:merged_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] + @order = mr_table[:created_at] super(*args) end + private + def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/code_stage.rb b/lib/gitlab/cycle_analytics/code_stage.rb new file mode 100644 index 00000000000..d1bc2055ba8 --- /dev/null +++ b/lib/gitlab/cycle_analytics/code_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class CodeStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_table[:created_at] + end + + def name + :code + end + + def description + "Time until first merge request" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/event_fetcher.rb b/lib/gitlab/cycle_analytics/event_fetcher.rb new file mode 100644 index 00000000000..50e126cf00b --- /dev/null +++ b/lib/gitlab/cycle_analytics/event_fetcher.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module EventFetcher + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/events.rb b/lib/gitlab/cycle_analytics/events.rb deleted file mode 100644 index 2d703d76cbb..00000000000 --- a/lib/gitlab/cycle_analytics/events.rb +++ /dev/null @@ -1,38 +0,0 @@ -module Gitlab - module CycleAnalytics - class Events - def initialize(project:, options:) - @project = project - @options = options - end - - def issue_events - IssueEvent.new(project: @project, options: @options).fetch - end - - def plan_events - PlanEvent.new(project: @project, options: @options).fetch - end - - def code_events - CodeEvent.new(project: @project, options: @options).fetch - end - - def test_events - TestEvent.new(project: @project, options: @options).fetch - end - - def review_events - ReviewEvent.new(project: @project, options: @options).fetch - end - - def staging_events - StagingEvent.new(project: @project, options: @options).fetch - end - - def production_events - ProductionEvent.new(project: @project, options: @options).fetch - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/events_query.rb b/lib/gitlab/cycle_analytics/events_query.rb deleted file mode 100644 index 2418832ccc2..00000000000 --- a/lib/gitlab/cycle_analytics/events_query.rb +++ /dev/null @@ -1,37 +0,0 @@ -module Gitlab - module CycleAnalytics - class EventsQuery - attr_reader :project - - def initialize(project:, options: {}) - @project = project - @from = options[:from] - @branch = options[:branch] - @fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch) - end - - def execute(stage_class) - @stage_class = stage_class - - ActiveRecord::Base.connection.exec_query(query.to_sql) - end - - private - - def query - base_query = @fetcher.base_query_for(@stage_class.stage) - diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs) - - @stage_class.custom_query(base_query) - - base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc) - end - - def extract_epoch(arel_attribute) - return arel_attribute unless Gitlab::Database.postgresql? - - Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))}) - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/issue_event.rb b/lib/gitlab/cycle_analytics/issue_event.rb deleted file mode 100644 index 705b7e5ce24..00000000000 --- a/lib/gitlab/cycle_analytics/issue_event.rb +++ /dev/null @@ -1,27 +0,0 @@ -module Gitlab - module CycleAnalytics - class IssueEvent < BaseEvent - include IssueAllowed - - def initialize(*args) - @stage = :issue - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at], - issue_metrics_table[:first_added_to_board_at]] - @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).as_json - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/production_event.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb similarity index 72% rename from lib/gitlab/cycle_analytics/production_event.rb rename to lib/gitlab/cycle_analytics/issue_event_fetcher.rb index 4868c3c6237..0d8da99455e 100644 --- a/lib/gitlab/cycle_analytics/production_event.rb +++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb @@ -1,12 +1,9 @@ module Gitlab module CycleAnalytics - class ProductionEvent < BaseEvent + class IssueEventFetcher < BaseEventFetcher include IssueAllowed def initialize(*args) - @stage = :production - @start_time_attrs = issue_table[:created_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [issue_table[:title], issue_table[:iid], issue_table[:id], diff --git a/lib/gitlab/cycle_analytics/issue_stage.rb b/lib/gitlab/cycle_analytics/issue_stage.rb new file mode 100644 index 00000000000..d2068fbc38f --- /dev/null +++ b/lib/gitlab/cycle_analytics/issue_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class IssueStage < BaseStage + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def name + :issue + end + + def description + "Time before an issue gets scheduled" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/metrics_fetcher.rb b/lib/gitlab/cycle_analytics/metrics_fetcher.rb deleted file mode 100644 index b71e8735e27..00000000000 --- a/lib/gitlab/cycle_analytics/metrics_fetcher.rb +++ /dev/null @@ -1,60 +0,0 @@ -module Gitlab - module CycleAnalytics - class MetricsFetcher - include Gitlab::Database::Median - include Gitlab::Database::DateTime - include MetricsTables - - DEPLOYMENT_METRIC_STAGES = %i[production staging] - - def initialize(project:, from:, branch:) - @project = project - @project = project - @from = from - @branch = branch - end - - def calculate_metric(name, start_time_attrs, end_time_attrs) - cte_table = Arel::Table.new("cte_table_for_#{name}") - - # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time). - # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time). - # We compute the (end_time - start_time) interval, and give it an alias based on the current - # cycle analytics stage. - interval_query = Arel::Nodes::As.new( - cte_table, - subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s)) - - median_datetime(cte_table, interval_query, name) - end - - # Join table with a row for every pair (where the merge request - # closes the given issue) with issue and merge request metrics included. The metrics - # are loaded with an inner join, so issues / merge requests without metrics are - # automatically excluded. - def base_query_for(name) - # Load issues - 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])). - where(issue_table[:project_id].eq(@project.id)). - where(issue_table[:deleted_at].eq(nil)). - where(issue_table[:created_at].gteq(@from)) - - query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch - - # 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])) - - if DEPLOYMENT_METRIC_STAGES.include?(name) - # Limit to merge requests that have been deployed to production after `@from` - query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) - end - - query - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/plan_event.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb similarity index 78% rename from lib/gitlab/cycle_analytics/plan_event.rb rename to lib/gitlab/cycle_analytics/plan_event_fetcher.rb index 7c3f0e9989f..88a8710dbe6 100644 --- a/lib/gitlab/cycle_analytics/plan_event.rb +++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb @@ -1,19 +1,17 @@ module Gitlab module CycleAnalytics - class PlanEvent < BaseEvent + class PlanEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :plan - @start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at] - @end_time_attrs = [issue_metrics_table[:first_added_to_board_at], - issue_metrics_table[:first_mentioned_in_commit_at]] @projections = [mr_diff_table[:st_commits].as('commits'), issue_metrics_table[:first_mentioned_in_commit_at]] super(*args) end - def custom_query(base_query) + def events_query base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/plan_stage.rb b/lib/gitlab/cycle_analytics/plan_stage.rb new file mode 100644 index 00000000000..3b4dfc6a30e --- /dev/null +++ b/lib/gitlab/cycle_analytics/plan_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class PlanStage < BaseStage + def start_time_attrs + @start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at], + issue_metrics_table[:first_added_to_board_at]] + end + + def end_time_attrs + @end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at] + end + + def name + :plan + end + + def description + "Time before an issue starts implementation" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb new file mode 100644 index 00000000000..0fa2e87f673 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class ProductionEventFetcher < IssueEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb new file mode 100644 index 00000000000..d693443bfa4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_helper.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module ProductionHelper + def stage_query + super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from])) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/production_stage.rb b/lib/gitlab/cycle_analytics/production_stage.rb new file mode 100644 index 00000000000..2a6bcc80116 --- /dev/null +++ b/lib/gitlab/cycle_analytics/production_stage.rb @@ -0,0 +1,28 @@ +module Gitlab + module CycleAnalytics + class ProductionStage < BaseStage + include ProductionHelper + + def start_time_attrs + @start_time_attrs ||= issue_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :production + end + + def description + "From issue creation until deploy to production" + end + + def query + # Limit to merge requests that have been deployed to production after `@from` + query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from)) + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/code_event.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb similarity index 69% rename from lib/gitlab/cycle_analytics/code_event.rb rename to lib/gitlab/cycle_analytics/review_event_fetcher.rb index 2afdf0b8518..4df0bd06393 100644 --- a/lib/gitlab/cycle_analytics/code_event.rb +++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb @@ -1,25 +1,19 @@ module Gitlab module CycleAnalytics - class CodeEvent < BaseEvent + class ReviewEventFetcher < BaseEventFetcher include MergeRequestAllowed def initialize(*args) - @stage = :code - @start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at] - @end_time_attrs = mr_table[:created_at] @projections = [mr_table[:title], mr_table[:iid], mr_table[:id], mr_table[:created_at], mr_table[:state], mr_table[:author_id]] - @order = mr_table[:created_at] super(*args) end - private - def serialize(event) AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json end diff --git a/lib/gitlab/cycle_analytics/review_stage.rb b/lib/gitlab/cycle_analytics/review_stage.rb new file mode 100644 index 00000000000..fbaa3010d81 --- /dev/null +++ b/lib/gitlab/cycle_analytics/review_stage.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + class ReviewStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_table[:created_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:merged_at] + end + + def name + :review + end + + def description + "Time between merge request creation and merge/close" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage.rb b/lib/gitlab/cycle_analytics/stage.rb new file mode 100644 index 00000000000..28e0455df59 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage.rb @@ -0,0 +1,9 @@ +module Gitlab + module CycleAnalytics + module Stage + def self.[](stage_name) + CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage") + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/stage_summary.rb b/lib/gitlab/cycle_analytics/stage_summary.rb new file mode 100644 index 00000000000..b34baf5b081 --- /dev/null +++ b/lib/gitlab/cycle_analytics/stage_summary.rb @@ -0,0 +1,23 @@ +module Gitlab + module CycleAnalytics + class StageSummary + def initialize(project, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def data + [serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)), + serialize(Summary::Commit.new(project: @project, from: @from)), + serialize(Summary::Deploy.new(project: @project, from: @from))] + end + + private + + def serialize(summary_object) + AnalyticsSummarySerializer.new.represent(summary_object).as_json + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/staging_event.rb b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb similarity index 70% rename from lib/gitlab/cycle_analytics/staging_event.rb rename to lib/gitlab/cycle_analytics/staging_event_fetcher.rb index a1f30b716f6..a34731a5fcd 100644 --- a/lib/gitlab/cycle_analytics/staging_event.rb +++ b/lib/gitlab/cycle_analytics/staging_event_fetcher.rb @@ -1,10 +1,7 @@ module Gitlab module CycleAnalytics - class StagingEvent < BaseEvent + class StagingEventFetcher < BaseEventFetcher def initialize(*args) - @stage = :staging - @start_time_attrs = mr_metrics_table[:merged_at] - @end_time_attrs = mr_metrics_table[:first_deployed_to_production_at] @projections = [build_table[:id]] @order = build_table[:created_at] @@ -17,8 +14,10 @@ module Gitlab super end - def custom_query(base_query) + def events_query base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id])) + + super end private diff --git a/lib/gitlab/cycle_analytics/staging_stage.rb b/lib/gitlab/cycle_analytics/staging_stage.rb new file mode 100644 index 00000000000..945909a4d62 --- /dev/null +++ b/lib/gitlab/cycle_analytics/staging_stage.rb @@ -0,0 +1,22 @@ +module Gitlab + module CycleAnalytics + class StagingStage < BaseStage + include ProductionHelper + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:merged_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at] + end + + def name + :staging + end + + def description + "From merge request merge until deploy to production" + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/base.rb b/lib/gitlab/cycle_analytics/summary/base.rb new file mode 100644 index 00000000000..43fa3795e5c --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/base.rb @@ -0,0 +1,20 @@ +module Gitlab + module CycleAnalytics + module Summary + class Base + def initialize(project:, from:) + @project = project + @from = from + end + + def title + self.class.name.demodulize + end + + def value + raise NotImplementedError.new("Expected #{self.name} to implement value") + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/commit.rb b/lib/gitlab/cycle_analytics/summary/commit.rb new file mode 100644 index 00000000000..7b8faa4d854 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/commit.rb @@ -0,0 +1,39 @@ +module Gitlab + module CycleAnalytics + module Summary + class Commit < Base + def value + @value ||= count_commits + end + + private + + # Don't use the `Gitlab::Git::Repository#log` method, because it enforces + # a limit. Since we need a commit count, we _can't_ enforce a limit, so + # the easiest way forward is to replicate the relevant portions of the + # `log` function here. + def count_commits + return unless ref + + repository = @project.repository.raw_repository + sha = @project.repository.commit(ref).sha + + cmd = %W(git --git-dir=#{repository.path} log) + cmd << '--format=%H' + cmd << "--after=#{@from.iso8601}" + cmd << sha + + output, status = Gitlab::Popen.popen(cmd) + + raise IOError, output unless status.zero? + + output.lines.count + end + + def ref + @ref ||= @project.default_branch.presence + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/deploy.rb b/lib/gitlab/cycle_analytics/summary/deploy.rb new file mode 100644 index 00000000000..06032e9200e --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/deploy.rb @@ -0,0 +1,11 @@ +module Gitlab + module CycleAnalytics + module Summary + class Deploy < Base + def value + @value ||= @project.deployments.where("created_at > ?", @from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/summary/issue.rb b/lib/gitlab/cycle_analytics/summary/issue.rb new file mode 100644 index 00000000000..008468f24b9 --- /dev/null +++ b/lib/gitlab/cycle_analytics/summary/issue.rb @@ -0,0 +1,21 @@ +module Gitlab + module CycleAnalytics + module Summary + class Issue < Base + def initialize(project:, from:, current_user:) + @project = project + @from = from + @current_user = current_user + end + + def title + 'New Issue' + end + + def value + @value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count + end + end + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_event.rb b/lib/gitlab/cycle_analytics/test_event.rb deleted file mode 100644 index d553d0b5aec..00000000000 --- a/lib/gitlab/cycle_analytics/test_event.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Gitlab - module CycleAnalytics - class TestEvent < StagingEvent - def initialize(*args) - super(*args) - - @stage = :test - @start_time_attrs = mr_metrics_table[:latest_build_started_at] - @end_time_attrs = mr_metrics_table[:latest_build_finished_at] - end - end - end -end diff --git a/lib/gitlab/cycle_analytics/test_event_fetcher.rb b/lib/gitlab/cycle_analytics/test_event_fetcher.rb new file mode 100644 index 00000000000..a2589c6601a --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_event_fetcher.rb @@ -0,0 +1,6 @@ +module Gitlab + module CycleAnalytics + class TestEventFetcher < StagingEventFetcher + end + end +end diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb new file mode 100644 index 00000000000..0079d56e0e4 --- /dev/null +++ b/lib/gitlab/cycle_analytics/test_stage.rb @@ -0,0 +1,29 @@ +module Gitlab + module CycleAnalytics + class TestStage < BaseStage + def start_time_attrs + @start_time_attrs ||= mr_metrics_table[:latest_build_started_at] + end + + def end_time_attrs + @end_time_attrs ||= mr_metrics_table[:latest_build_finished_at] + end + + def name + :test + end + + def description + "Total test time for all commits/merges" + end + + def stage_query + if @options[:branch] + super.where(build_table[:ref].eq(@options[:branch])) + else + super + end + end + end + end +end diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb index 1444d25ebc7..08607c27c09 100644 --- a/lib/gitlab/database/median.rb +++ b/lib/gitlab/database/median.rb @@ -103,6 +103,11 @@ module Gitlab Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) end + def extract_diff_epoch(diff) + return diff unless Gitlab::Database.postgresql? + + Arel.sql(%Q{EXTRACT(EPOCH FROM (#{diff.to_sql}))}) + end # Need to cast '0' to an INTERVAL before we can check if the interval is positive def zero_interval Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) diff --git a/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb new file mode 100644 index 00000000000..0267e8c2f69 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::CodeEventFetcher do + let(:stage_name) { :code } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb b/spec/lib/gitlab/cycle_analytics/code_event_spec.rb deleted file mode 100644 index 43f42d1bde8..00000000000 --- a/spec/lib/gitlab/cycle_analytics/code_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::CodeEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb new file mode 100644 index 00000000000..e8fc67acf05 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/code_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::CodeStage do + let(:stage_name) { :code } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb index 6062e7af4f5..9d2ba481919 100644 --- a/spec/lib/gitlab/cycle_analytics/events_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' -describe Gitlab::CycleAnalytics::Events do +describe 'cycle analytics events' do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } - subject { described_class.new(project: project, options: { from: from_date, current_user: user }) } + let(:events) do + CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events + end before do allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) @@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do end describe '#issue_events' do + let(:stage) { :issue } + it 'has the total time' do - expect(subject.issue_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.issue_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.issue_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.issue_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.issue_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.issue_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end describe '#plan_events' do + let(:stage) { :plan } + it 'has a title' do - expect(subject.plan_events.first[:title]).not_to be_nil + expect(events.first[:title]).not_to be_nil end it 'has a sha short ID' do - expect(subject.plan_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the URL' do - expect(subject.plan_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the total time' do - expect(subject.plan_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.plan_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.plan_events.first[:author][:name]).not_to be_nil + expect(events.first[:author][:name]).not_to be_nil end end describe '#code_events' do + let(:stage) { :code } + before do create_commit_referencing_issue(context) end it 'has the total time' do - expect(subject.code_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.code_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.code_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.code_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.code_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#test_events' do + let(:stage) { :test } + let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.test_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.test_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.test_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.test_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.test_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.test_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.test_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.test_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.test_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end end describe '#review_events' do + let(:stage) { :review } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } it 'has the total time' do - expect(subject.review_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.review_events.first[:title]).to eq('Awesome merge_request') + expect(events.first[:title]).to eq('Awesome merge_request') end it 'has an iid' do - expect(subject.review_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has the URL' do - expect(subject.review_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has a state' do - expect(subject.review_events.first[:state]).not_to be_nil + expect(events.first[:state]).not_to be_nil end it 'has a created_at timestamp' do - expect(subject.review_events.first[:created_at]).not_to be_nil + expect(events.first[:created_at]).not_to be_nil end it "has the author's URL" do - expect(subject.review_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#staging_events' do + let(:stage) { :staging } let(:merge_request) { MergeRequest.first } let!(:pipeline) do create(:ci_pipeline, @@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the name' do - expect(subject.staging_events.first[:name]).not_to be_nil + expect(events.first[:name]).not_to be_nil end it 'has the ID' do - expect(subject.staging_events.first[:id]).not_to be_nil + expect(events.first[:id]).not_to be_nil end it 'has the URL' do - expect(subject.staging_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has the branch name' do - expect(subject.staging_events.first[:branch]).not_to be_nil + expect(events.first[:branch]).not_to be_nil end it 'has the branch URL' do - expect(subject.staging_events.first[:branch][:url]).not_to be_nil + expect(events.first[:branch][:url]).not_to be_nil end it 'has the short SHA' do - expect(subject.staging_events.first[:short_sha]).not_to be_nil + expect(events.first[:short_sha]).not_to be_nil end it 'has the commit URL' do - expect(subject.staging_events.first[:commit_url]).not_to be_nil + expect(events.first[:commit_url]).not_to be_nil end it 'has the date' do - expect(subject.staging_events.first[:date]).not_to be_nil + expect(events.first[:date]).not_to be_nil end it 'has the total time' do - expect(subject.staging_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it "has the author's URL" do - expect(subject.staging_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name) + expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name) end end describe '#production_events' do + let(:stage) { :production } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } before do @@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do end it 'has the total time' do - expect(subject.production_events.first[:total_time]).not_to be_empty + expect(events.first[:total_time]).not_to be_empty end it 'has a title' do - expect(subject.production_events.first[:title]).to eq(context.title) + expect(events.first[:title]).to eq(context.title) end it 'has the URL' do - expect(subject.production_events.first[:url]).not_to be_nil + expect(events.first[:url]).not_to be_nil end it 'has an iid' do - expect(subject.production_events.first[:iid]).to eq(context.iid.to_s) + expect(events.first[:iid]).to eq(context.iid.to_s) end it 'has a created_at timestamp' do - expect(subject.production_events.first[:created_at]).to end_with('ago') + expect(events.first[:created_at]).to end_with('ago') end it "has the author's URL" do - expect(subject.production_events.first[:author][:web_url]).not_to be_nil + expect(events.first[:author][:web_url]).not_to be_nil end it "has the author's avatar URL" do - expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil + expect(events.first[:author][:avatar_url]).not_to be_nil end it "has the author's name" do - expect(subject.production_events.first[:author][:name]).to eq(context.author.name) + expect(events.first[:author][:name]).to eq(context.author.name) end end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb new file mode 100644 index 00000000000..fd9fa2fee49 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::IssueEventFetcher do + let(:stage_name) { :issue } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb b/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb deleted file mode 100644 index 1c5c308da7d..00000000000 --- a/spec/lib/gitlab/cycle_analytics/issue_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::IssueEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - 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 new file mode 100644 index 00000000000..3127f01989d --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::IssueStage do + let(:stage_name) { :issue } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb similarity index 53% rename from spec/lib/gitlab/cycle_analytics/plan_event_spec.rb rename to spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb index 4a5604115ec..2e5dc5b5547 100644 --- a/spec/lib/gitlab/cycle_analytics/plan_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/plan_event_fetcher_spec.rb @@ -1,15 +1,13 @@ require 'spec_helper' require 'lib/gitlab/cycle_analytics/shared_event_spec' -describe Gitlab::CycleAnalytics::PlanEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end +describe Gitlab::CycleAnalytics::PlanEventFetcher do + let(:stage_name) { :plan } + it_behaves_like 'default query config' do context 'no commits' do it 'does not blow up if there are no commits' do - allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}]) + allow(event).to receive(:event_result).and_return([{}]) expect { event.fetch }.not_to raise_error end diff --git a/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb new file mode 100644 index 00000000000..4c715921ad6 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::PlanStage do + let(:stage_name) { :plan } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb new file mode 100644 index 00000000000..74001181305 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ProductionEventFetcher do + let(:stage_name) { :production } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb b/spec/lib/gitlab/cycle_analytics/production_event_spec.rb deleted file mode 100644 index ac17e3b4287..00000000000 --- a/spec/lib/gitlab/cycle_analytics/production_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ProductionEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb new file mode 100644 index 00000000000..916684b81eb --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/production_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ProductionStage do + let(:stage_name) { :production } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb new file mode 100644 index 00000000000..4f67c95ed4c --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_event_fetcher_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::ReviewEventFetcher do + let(:stage_name) { :review } + + it_behaves_like 'default query config' +end diff --git a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb b/spec/lib/gitlab/cycle_analytics/review_event_spec.rb deleted file mode 100644 index 1ff53aa0227..00000000000 --- a/spec/lib/gitlab/cycle_analytics/review_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::ReviewEvent do - it_behaves_like 'default query config' do - it 'has the default order' do - expect(event.order).to eq(event.start_time_attrs) - 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 new file mode 100644 index 00000000000..1412c8dfa08 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/review_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::ReviewStage do + let(:stage_name) { :review } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb index 7019e4c3351..9c5e57342e9 100644 --- a/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/shared_event_spec.rb @@ -1,20 +1,13 @@ require 'spec_helper' shared_examples 'default query config' do - let(:event) { described_class.new(project: double, options: {}) } - - it 'has the start attributes' do - expect(event.start_time_attrs).not_to be_nil - end + let(:project) { create(:empty_project) } + let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) } it 'has the stage attribute' do expect(event.stage).not_to be_nil end - it 'has the end attributes' do - expect(event.end_time_attrs).not_to be_nil - end - it 'has the projection attributes' do expect(event.projections).not_to be_nil end diff --git a/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb new file mode 100644 index 00000000000..08425acbfc8 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +shared_examples 'base stage' do + let(:stage) { described_class.new(project: double, options: {}) } + + before do + allow(stage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'has the median data value' do + expect(stage.as_json[:value]).not_to be_nil + end + + it 'has the median data stage' do + expect(stage.as_json[:title]).not_to be_nil + end + + it 'has the median data description' do + expect(stage.as_json[:description]).not_to be_nil + end + + it 'has the title' do + expect(stage.title).to eq(stage_name.to_s.capitalize) + end + + it 'has the events' do + expect(stage.events).not_to be_nil + end +end diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb similarity index 76% rename from spec/models/cycle_analytics/summary_spec.rb rename to spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb index 725bc68b25f..fb6b6c4a8d2 100644 --- a/spec/models/cycle_analytics/summary_spec.rb +++ b/spec/lib/gitlab/cycle_analytics/stage_summary_spec.rb @@ -1,23 +1,23 @@ require 'spec_helper' -describe CycleAnalytics::Summary, models: true do +describe Gitlab::CycleAnalytics::StageSummary, models: true do let(:project) { create(:project) } - let(:from) { Time.now } + let(:from) { 1.day.ago } let(:user) { create(:user, :admin) } - subject { described_class.new(project, user, from: from) } + subject { described_class.new(project, from: Time.now, current_user: user).data } describe "#new_issues" do it "finds the number of issues created after the 'from date'" do Timecop.freeze(5.days.ago) { create(:issue, project: project) } Timecop.freeze(5.days.from_now) { create(:issue, project: project) } - expect(subject.new_issues).to eq(1) + expect(subject.first[:value]).to eq(1) end it "doesn't find issues from other projects" do Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } - expect(subject.new_issues).to eq(0) + expect(subject.first[:value]).to eq(0) end end @@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') } Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') } - expect(subject.commits).to eq(1) + expect(subject.second[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } - expect(subject.commits).to eq(0) + expect(subject.second[:value]).to eq(0) end it "finds a large (> 100) snumber of commits if present" do Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) } - expect(subject.commits).to eq(100) + expect(subject.second[:value]).to eq(100) end end @@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do Timecop.freeze(5.days.ago) { create(:deployment, project: project) } Timecop.freeze(5.days.from_now) { create(:deployment, project: project) } - expect(subject.deploys).to eq(1) + expect(subject.third[:value]).to eq(1) end it "doesn't find commits from other projects" do Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } - expect(subject.deploys).to eq(0) + expect(subject.third[:value]).to eq(0) end end end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb new file mode 100644 index 00000000000..bbc82496340 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::StagingEventFetcher do + let(:stage_name) { :staging } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb deleted file mode 100644 index 4862d4765f2..00000000000 --- a/spec/lib/gitlab/cycle_analytics/staging_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::StagingEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - end - end -end diff --git a/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb new file mode 100644 index 00000000000..8154b3ac701 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::StagingStage do + let(:stage_name) { :staging } + + it_behaves_like 'base stage' +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb new file mode 100644 index 00000000000..6639fa54e0e --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_event_spec' + +describe Gitlab::CycleAnalytics::TestEventFetcher do + let(:stage_name) { :test } + + it_behaves_like 'default query config' do + it 'has a default order' do + expect(event.order).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb b/spec/lib/gitlab/cycle_analytics/test_event_spec.rb deleted file mode 100644 index e249db69fc6..00000000000 --- a/spec/lib/gitlab/cycle_analytics/test_event_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' -require 'lib/gitlab/cycle_analytics/shared_event_spec' - -describe Gitlab::CycleAnalytics::TestEvent do - it_behaves_like 'default query config' do - it 'does not have the default order' do - expect(event.order).not_to eq(event.start_time_attrs) - 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 new file mode 100644 index 00000000000..eacde22cd56 --- /dev/null +++ b/spec/lib/gitlab/cycle_analytics/test_stage_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' +require 'lib/gitlab/cycle_analytics/shared_stage_spec' + +describe Gitlab::CycleAnalytics::TestStage do + let(:stage_name) { :test } + + it_behaves_like 'base stage' +end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 7771785ead3..70f985afefb 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } context 'with deployment' do generate_cycle_analytics_spec( @@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) context.deploy_master @@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do deploy_master end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end @@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do -> (context, data) do context.create_commit_referencing_issue(data[:issue]) end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(data[:issue]) - end]], + end_time_conditions: [["merge request that closes issue is created", + -> (context, data) do + context.create_merge_request_closing_issue(data[:issue]) + end]], post_fn: -> (context, data) do context.merge_merge_requests_closing_issue(data[:issue]) end) @@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.code).to be_nil + expect(subject[:code].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5ed3d37f2fb..e4b6a8f4518 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :issue, @@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do merge_merge_requests_closing_issue(issue) end - expect(subject.issue).to be_nil + expect(subject[:issue].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 baf3e3241a1..dc5b04852d6 100644 --- a/spec/models/cycle_analytics/plan_spec.rb +++ b/spec/models/cycle_analytics/plan_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :plan, @@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do create_merge_request_closing_issue(issue, source_branch: branch_name) merge_merge_requests_closing_issue(issue) - expect(subject.issue).to be_nil + expect(subject[:issue].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 21b9c6e7150..5e99188f318 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :production, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do deploy_master(environment: 'staging') end - expect(subject.production).to be_nil + expect(subject[:production].median).to be_nil end end end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 158621d59a4..45baa5f7006 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :review, @@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) end - expect(subject.review).to be_nil + expect(subject[:review].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 dad653964b7..77625aad580 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :staging, @@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master end - expect(subject.staging).to be_nil + expect(subject[:staging].median).to be_nil end end @@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do deploy_master(environment: 'staging') end - expect(subject.staging).to be_nil + expect(subject[:staging].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 2313724e8f3..27a117d2d76 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do let(:project) { create(:project) } let(:from_date) { 10.days.ago } let(:user) { create(:user, :admin) } - subject { CycleAnalytics.new(project, user, from: from_date) } + subject { CycleAnalytics.new(project, from: from_date) } generate_cycle_analytics_spec( phase: :test, @@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do pipeline.succeed! end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end @@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do merge_merge_requests_closing_issue(issue) end - expect(subject.test).to be_nil + expect(subject[:test].median).to be_nil end end end diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb new file mode 100644 index 00000000000..f9951826683 --- /dev/null +++ b/spec/serializers/analytics_stage_serializer_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe AnalyticsStageSerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) } + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12) + allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({}) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :description, :value) + end +end diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb new file mode 100644 index 00000000000..7a84c8b0b40 --- /dev/null +++ b/spec/serializers/analytics_summary_serializer_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe AnalyticsSummarySerializer do + let(:serializer) do + described_class + .new.represent(resource) + end + + let(:json) { serializer.as_json } + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:resource) do + Gitlab::CycleAnalytics::Summary::Issue.new(project: double, + from: 1.day.ago, + current_user: user) + end + + before do + allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12) + end + + it 'it generates payload for single object' do + expect(json).to be_kind_of Hash + end + + it 'contains important elements of AnalyticsStage' do + expect(json).to include(:title, :value) + end +end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 8e19a6c92e2..35b40d73191 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -2,7 +2,6 @@ # Note: The ABC size is large here because we have a method generating test cases with # multiple nested contexts. This shouldn't count as a violation. - module CycleAnalyticsHelpers module TestGeneration # Generate the most common set of specs that all cycle analytics phases need to have. @@ -51,7 +50,7 @@ module CycleAnalyticsHelpers end median_time_difference = time_differences.sort[2] - expect(subject.send(phase)).to be_within(5).of(median_time_difference) + expect(subject[phase].median).to be_within(5).of(median_time_difference) end context "when the data belongs to another project" do @@ -83,7 +82,7 @@ module CycleAnalyticsHelpers # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end @@ -106,7 +105,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -126,7 +125,7 @@ module CycleAnalyticsHelpers Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -145,7 +144,7 @@ module CycleAnalyticsHelpers post_fn[self, data] if post_fn end - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end @@ -153,7 +152,7 @@ module CycleAnalyticsHelpers context "when none of the start / end conditions are matched" do it "returns nil" do - expect(subject.send(phase)).to be_nil + expect(subject[phase].median).to be_nil end end end