Commit 1cc6d206 authored by Douwe Maan's avatar Douwe Maan

Merge branch 'fix/refactor-cycle-analytics-stages' into 'master'

Refactor cycle analytics stages (1st iteration)

See merge request !7647
parents cc1e43da 1d775d97
module CycleAnalyticsParams module CycleAnalyticsParams
extend ActiveSupport::Concern extend ActiveSupport::Concern
def options(params)
@options ||= { from: start_date(params), current_user: current_user }
end
def start_date(params) def start_date(params)
params[:start_date] == '30' ? 30.days.ago : 90.days.ago params[:start_date] == '30' ? 30.days.ago : 90.days.ago
end end
......
...@@ -9,56 +9,52 @@ module Projects ...@@ -9,56 +9,52 @@ module Projects
before_action :authorize_read_merge_request!, only: [:code, :review] before_action :authorize_read_merge_request!, only: [:code, :review]
def issue def issue
render_events(events.issue_events) render_events(cycle_analytics[:issue].events)
end end
def plan def plan
render_events(events.plan_events) render_events(cycle_analytics[:plan].events)
end end
def code def code
render_events(events.code_events) render_events(cycle_analytics[:code].events)
end end
def test 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 end
def review def review
render_events(events.review_events) render_events(cycle_analytics[:review].events)
end end
def staging def staging
render_events(events.staging_events) render_events(cycle_analytics[:staging].events)
end end
def production def production
render_events(events.production_events) render_events(cycle_analytics[:production].events)
end end
private private
def render_events(events_list) def render_events(events)
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: { events: events_list } } format.json { render json: { events: events } }
end end
end end
def events def cycle_analytics
@events ||= Gitlab::CycleAnalytics::Events.new(project: project, options: options) @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
end
def options
@options ||= { from: start_date(events_params), current_user: current_user }
end end
def events_params def events_params
return {} unless params[:events].present? return {} unless params[:events].present?
params[:events].slice(:start_date, :branch_name) params[:events].permit(:start_date, :branch_name)
end end
end end
end end
......
...@@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -6,11 +6,9 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics! before_action :authorize_read_cycle_analytics!
def show 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 = @cycle_analytics.no_stats?
@cycle_analytics_no_data = stats_values.blank?
respond_to do |format| respond_to do |format|
format.html format.html
...@@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -23,50 +21,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def cycle_analytics_params def cycle_analytics_params
return {} unless params[:cycle_analytics].present? return {} unless params[:cycle_analytics].present?
{ start_date: params[:cycle_analytics][:start_date] } params[:cycle_analytics].permit(:start_date)
end end
def generate_cycle_analytics_data def cycle_analytics_json
stats_values = [] {
summary: @cycle_analytics.summary,
cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"], stats: @cycle_analytics.stats,
[:plan, "Plan", "Related Commits", "Time before an issue starts implementation"], permissions: @cycle_analytics.permissions(user: current_user)
[: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)
} }
[stats_values, cycle_analytics_hash]
end end
end end
class CycleAnalytics class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, current_user, from:) def initialize(project, options)
@project = project @project = project
@current_user = current_user @options = options
@from = from
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
end end
def summary 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 end
def permissions(user:) def stats
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project) @stats ||= stats_per_stage
end end
def issue def no_stats?
@fetcher.calculate_metric(:issue, stats.all? { |hash| hash[:value].nil? }
Issue.arel_table[:created_at],
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
Issue::Metrics.arel_table[:first_added_to_board_at]])
end end
def plan def permissions(user:)
@fetcher.calculate_metric(:plan, Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
[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
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 end
def review def [](stage_name)
@fetcher.calculate_metric(:review, Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
end end
def staging private
@fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
def production def stats_per_stage
@fetcher.calculate_metric(:production, STAGES.map do |stage_name|
Issue.arel_table[:created_at], self[stage_name].as_json
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at]) end
end end
end end
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
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
class AnalyticsStageSerializer < BaseSerializer
entity AnalyticsStageEntity
end
class AnalyticsSummaryEntity < Grape::Entity
expose :value, safe: true
expose :title do |object|
object.title.pluralize(object.value)
end
end
class AnalyticsSummarySerializer < BaseSerializer
entity AnalyticsSummaryEntity
end
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class BaseEvent class BaseEventFetcher
include MetricsTables include BaseQuery
attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query attr_reader :projections, :query, :stage, :order
def initialize(project:, options:) def initialize(project:, stage:, options:)
@query = EventsQuery.new(project: project, options: options)
@project = project @project = project
@stage = stage
@options = options @options = options
end end
...@@ -19,10 +19,8 @@ module Gitlab ...@@ -19,10 +19,8 @@ module Gitlab
end.compact end.compact
end end
def custom_query(_base_query); end
def order def order
@order || @start_time_attrs @order || default_order
end end
private private
...@@ -34,7 +32,17 @@ module Gitlab ...@@ -34,7 +32,17 @@ module Gitlab
end end
def event_result 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 end
def serialize(_event) def serialize(_event)
......
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class MetricsFetcher module BaseQuery
include MetricsTables
include Gitlab::Database::Median include Gitlab::Database::Median
include Gitlab::Database::DateTime 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). private
# 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) def base_query
@base_query ||= stage_query
end end
# Join table with a row for every <issue,merge_request> pair (where the merge request def stage_query
# 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])). query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])). join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
where(issue_table[:project_id].eq(@project.id)). where(issue_table[:project_id].eq(@project.id)).
where(issue_table[:deleted_at].eq(nil)). where(issue_table[:deleted_at].eq(nil)).
where(issue_table[:created_at].gteq(@from)) where(issue_table[:created_at].gteq(@options[:from]))
query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
# Load merge_requests # Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin). query = query.join(mr_table, Arel::Nodes::OuterJoin).
...@@ -48,11 +24,6 @@ module Gitlab ...@@ -48,11 +24,6 @@ module Gitlab
join(mr_metrics_table). join(mr_metrics_table).
on(mr_table[:id].eq(mr_metrics_table[:merge_request_id])) on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
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 query
end end
end end
......
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
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class ReviewEvent < BaseEvent class CodeEventFetcher < BaseEventFetcher
include MergeRequestAllowed include MergeRequestAllowed
def initialize(*args) def initialize(*args)
@stage = :review
@start_time_attrs = mr_table[:created_at]
@end_time_attrs = mr_metrics_table[:merged_at]
@projections = [mr_table[:title], @projections = [mr_table[:title],
mr_table[:iid], mr_table[:iid],
mr_table[:id], mr_table[:id],
mr_table[:created_at], mr_table[:created_at],
mr_table[:state], mr_table[:state],
mr_table[:author_id]] mr_table[:author_id]]
@order = mr_table[:created_at]
super(*args) super(*args)
end end
private
def serialize(event) def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end end
......
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
module Gitlab
module CycleAnalytics
module EventFetcher
def self.[](stage_name)
CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
end
end
end
end
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
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
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
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class ProductionEvent < BaseEvent class IssueEventFetcher < BaseEventFetcher
include IssueAllowed include IssueAllowed
def initialize(*args) 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], @projections = [issue_table[:title],
issue_table[:iid], issue_table[:iid],
issue_table[:id], issue_table[:id],
......
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
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class PlanEvent < BaseEvent class PlanEventFetcher < BaseEventFetcher
def initialize(*args) 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'), @projections = [mr_diff_table[:st_commits].as('commits'),
issue_metrics_table[:first_mentioned_in_commit_at]] issue_metrics_table[:first_mentioned_in_commit_at]]
super(*args) super(*args)
end 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])) base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
super
end end
private private
......
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
module Gitlab
module CycleAnalytics
class ProductionEventFetcher < IssueEventFetcher
end
end
end
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
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
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class CodeEvent < BaseEvent class ReviewEventFetcher < BaseEventFetcher
include MergeRequestAllowed include MergeRequestAllowed
def initialize(*args) 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], @projections = [mr_table[:title],
mr_table[:iid], mr_table[:iid],
mr_table[:id], mr_table[:id],
mr_table[:created_at], mr_table[:created_at],
mr_table[:state], mr_table[:state],
mr_table[:author_id]] mr_table[:author_id]]
@order = mr_table[:created_at]
super(*args) super(*args)
end end
private
def serialize(event) def serialize(event)
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
end end
......
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
module Gitlab
module CycleAnalytics
module Stage
def self.[](stage_name)
CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
end
end
end
end
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
module Gitlab module Gitlab
module CycleAnalytics module CycleAnalytics
class StagingEvent < BaseEvent class StagingEventFetcher < BaseEventFetcher
def initialize(*args) 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]] @projections = [build_table[:id]]
@order = build_table[:created_at] @order = build_table[:created_at]
...@@ -17,8 +14,10 @@ module Gitlab ...@@ -17,8 +14,10 @@ module Gitlab
super super
end 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])) base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
super
end end
private private
......
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
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
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
module Gitlab
module CycleAnalytics
module Summary
class Deploy < Base
def value
@value ||= @project.deployments.where("created_at > ?", @from).count
end
end
end
end
end
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
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
module Gitlab
module CycleAnalytics
class TestEventFetcher < StagingEventFetcher
end
end
end
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
...@@ -103,6 +103,11 @@ module Gitlab ...@@ -103,6 +103,11 @@ module Gitlab
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")}) Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end end
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 # Need to cast '0' to an INTERVAL before we can check if the interval is positive
def zero_interval def zero_interval
Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")]) Arel::Nodes::NamedFunction.new("CAST", [Arel.sql("'0' AS INTERVAL")])
......
require 'spec_helper' require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec' require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::ProductionEvent do describe Gitlab::CycleAnalytics::CodeEventFetcher do
let(:stage_name) { :code }
it_behaves_like 'default query config' do it_behaves_like 'default query config' do
it 'has the default order' do it 'has a default order' do
expect(event.order).to eq(event.start_time_attrs) expect(event.order).not_to be_nil
end end
end end
end end
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
require 'spec_helper' require 'spec_helper'
describe Gitlab::CycleAnalytics::Events do describe 'cycle analytics events' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } 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 before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context]) allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
...@@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do ...@@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do
end end
describe '#issue_events' do describe '#issue_events' do
let(:stage) { :issue }
it 'has the total time' do 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 end
it 'has a title' do it 'has a title' do
expect(subject.issue_events.first[:title]).to eq(context.title) expect(events.first[:title]).to eq(context.title)
end end
it 'has the URL' do it 'has the URL' do
expect(subject.issue_events.first[:url]).not_to be_nil expect(events.first[:url]).not_to be_nil
end end
it 'has an iid' do 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 end
it 'has a created_at timestamp' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
describe '#plan_events' do describe '#plan_events' do
let(:stage) { :plan }
it 'has a title' do it 'has a title' do
expect(subject.plan_events.first[:title]).not_to be_nil expect(events.first[:title]).not_to be_nil
end end
it 'has a sha short ID' do 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 end
it 'has the URL' do 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 end
it 'has the total time' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
describe '#code_events' do describe '#code_events' do
let(:stage) { :code }
before do before do
create_commit_referencing_issue(context) create_commit_referencing_issue(context)
end end
it 'has the total time' do 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 end
it 'has a title' do 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 end
it 'has an iid' do 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 end
it 'has a created_at timestamp' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
describe '#test_events' do describe '#test_events' do
let(:stage) { :test }
let(:merge_request) { MergeRequest.first } let(:merge_request) { MergeRequest.first }
let!(:pipeline) do let!(:pipeline) do
create(:ci_pipeline, create(:ci_pipeline,
...@@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do ...@@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do
end end
it 'has the name' do it 'has the name' do
expect(subject.test_events.first[:name]).not_to be_nil expect(events.first[:name]).not_to be_nil
end end
it 'has the ID' do it 'has the ID' do
expect(subject.test_events.first[:id]).not_to be_nil expect(events.first[:id]).not_to be_nil
end end
it 'has the URL' do it 'has the URL' do
expect(subject.test_events.first[:url]).not_to be_nil expect(events.first[:url]).not_to be_nil
end end
it 'has the branch name' do 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 end
it 'has the branch URL' do 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 end
it 'has the short SHA' do 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 end
it 'has the commit URL' do 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 end
it 'has the date' do it 'has the date' do
expect(subject.test_events.first[:date]).not_to be_nil expect(events.first[:date]).not_to be_nil
end end
it 'has the total time' do 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
end end
describe '#review_events' do describe '#review_events' do
let(:stage) { :review }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
it 'has the total time' do 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 end
it 'has a title' do 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 end
it 'has an iid' do 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 end
it 'has the URL' do it 'has the URL' do
expect(subject.review_events.first[:url]).not_to be_nil expect(events.first[:url]).not_to be_nil
end end
it 'has a state' do it 'has a state' do
expect(subject.review_events.first[:state]).not_to be_nil expect(events.first[:state]).not_to be_nil
end end
it 'has a created_at timestamp' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
describe '#staging_events' do describe '#staging_events' do
let(:stage) { :staging }
let(:merge_request) { MergeRequest.first } let(:merge_request) { MergeRequest.first }
let!(:pipeline) do let!(:pipeline) do
create(:ci_pipeline, create(:ci_pipeline,
...@@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do ...@@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do
end end
it 'has the name' do it 'has the name' do
expect(subject.staging_events.first[:name]).not_to be_nil expect(events.first[:name]).not_to be_nil
end end
it 'has the ID' do it 'has the ID' do
expect(subject.staging_events.first[:id]).not_to be_nil expect(events.first[:id]).not_to be_nil
end end
it 'has the URL' do it 'has the URL' do
expect(subject.staging_events.first[:url]).not_to be_nil expect(events.first[:url]).not_to be_nil
end end
it 'has the branch name' do 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 end
it 'has the branch URL' do 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 end
it 'has the short SHA' do 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 end
it 'has the commit URL' do 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 end
it 'has the date' do it 'has the date' do
expect(subject.staging_events.first[:date]).not_to be_nil expect(events.first[:date]).not_to be_nil
end end
it 'has the total time' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
describe '#production_events' do describe '#production_events' do
let(:stage) { :production }
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) } let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do before do
...@@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do ...@@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do
end end
it 'has the total time' do 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 end
it 'has a title' do it 'has a title' do
expect(subject.production_events.first[:title]).to eq(context.title) expect(events.first[:title]).to eq(context.title)
end end
it 'has the URL' do it 'has the URL' do
expect(subject.production_events.first[:url]).not_to be_nil expect(events.first[:url]).not_to be_nil
end end
it 'has an iid' do 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 end
it 'has a created_at timestamp' do 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 end
it "has the author's URL" do 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 end
it "has the author's avatar URL" do 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 end
it "has the author's name" do 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
end end
......
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
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
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
require 'spec_helper' require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec' require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::PlanEvent do describe Gitlab::CycleAnalytics::PlanEventFetcher do
it_behaves_like 'default query config' do let(:stage_name) { :plan }
it 'has the default order' do
expect(event.order).to eq(event.start_time_attrs)
end
it_behaves_like 'default query config' do
context 'no commits' do context 'no commits' do
it 'does not blow up if there are 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 expect { event.fetch }.not_to raise_error
end end
......
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
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
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
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
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
require 'spec_helper' require 'spec_helper'
shared_examples 'default query config' do shared_examples 'default query config' do
let(:event) { described_class.new(project: double, options: {}) } let(:project) { create(:empty_project) }
let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) }
it 'has the start attributes' do
expect(event.start_time_attrs).not_to be_nil
end
it 'has the stage attribute' do it 'has the stage attribute' do
expect(event.stage).not_to be_nil expect(event.stage).not_to be_nil
end end
it 'has the end attributes' do
expect(event.end_time_attrs).not_to be_nil
end
it 'has the projection attributes' do it 'has the projection attributes' do
expect(event.projections).not_to be_nil expect(event.projections).not_to be_nil
end end
......
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
require 'spec_helper' require 'spec_helper'
describe CycleAnalytics::Summary, models: true do describe Gitlab::CycleAnalytics::StageSummary, models: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from) { Time.now } let(:from) { 1.day.ago }
let(:user) { create(:user, :admin) } 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 describe "#new_issues" do
it "finds the number of issues created after the 'from date'" 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.ago) { create(:issue, project: project) }
Timecop.freeze(5.days.from_now) { 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 end
it "doesn't find issues from other projects" do it "doesn't find issues from other projects" do
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) } 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
end end
...@@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do ...@@ -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.ago) { create_commit("Test message", project, user, 'master') }
Timecop.freeze(5.days.from_now) { 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 end
it "doesn't find commits from other projects" do it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') } 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 end
it "finds a large (> 100) snumber of commits if present" do 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) } 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
end end
...@@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do ...@@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do
Timecop.freeze(5.days.ago) { create(:deployment, project: project) } Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
Timecop.freeze(5.days.from_now) { 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 end
it "doesn't find commits from other projects" do it "doesn't find commits from other projects" do
Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) } 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 end
end end
require 'spec_helper' require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec' require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::ReviewEvent do describe Gitlab::CycleAnalytics::StagingEventFetcher do
let(:stage_name) { :staging }
it_behaves_like 'default query config' do it_behaves_like 'default query config' do
it 'has the default order' do it 'has a default order' do
expect(event.order).to eq(event.start_time_attrs) expect(event.order).not_to be_nil
end end
end end
end end
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
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
require 'spec_helper' require 'spec_helper'
require 'lib/gitlab/cycle_analytics/shared_event_spec' require 'lib/gitlab/cycle_analytics/shared_event_spec'
describe Gitlab::CycleAnalytics::CodeEvent do describe Gitlab::CycleAnalytics::TestEventFetcher do
let(:stage_name) { :test }
it_behaves_like 'default query config' do it_behaves_like 'default query config' do
it 'does not have the default order' do it 'has a default order' do
expect(event.order).not_to eq(event.start_time_attrs) expect(event.order).not_to be_nil
end end
end end
end end
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
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
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#code', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
context 'with deployment' do context 'with deployment' do
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
...@@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do ...@@ -16,10 +16,10 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do -> (context, data) do
context.create_commit_referencing_issue(data[:issue]) context.create_commit_referencing_issue(data[:issue])
end]], end]],
end_time_conditions: [["merge request that closes issue is created", end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do -> (context, data) do
context.create_merge_request_closing_issue(data[:issue]) context.create_merge_request_closing_issue(data[:issue])
end]], end]],
post_fn: -> (context, data) do post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue]) context.merge_merge_requests_closing_issue(data[:issue])
context.deploy_master context.deploy_master
...@@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do ...@@ -37,7 +37,7 @@ describe 'CycleAnalytics#code', feature: true do
deploy_master deploy_master
end end
expect(subject.code).to be_nil expect(subject[:code].median).to be_nil
end end
end end
end end
...@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do ...@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code', feature: true do
-> (context, data) do -> (context, data) do
context.create_commit_referencing_issue(data[:issue]) context.create_commit_referencing_issue(data[:issue])
end]], end]],
end_time_conditions: [["merge request that closes issue is created", end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do -> (context, data) do
context.create_merge_request_closing_issue(data[:issue]) context.create_merge_request_closing_issue(data[:issue])
end]], end]],
post_fn: -> (context, data) do post_fn: -> (context, data) do
context.merge_merge_requests_closing_issue(data[:issue]) context.merge_merge_requests_closing_issue(data[:issue])
end) end)
...@@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do ...@@ -69,7 +69,7 @@ describe 'CycleAnalytics#code', feature: true do
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
end end
expect(subject.code).to be_nil expect(subject[:code].median).to be_nil
end end
end end
end end
......
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#issue', models: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :issue, phase: :issue,
...@@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do ...@@ -42,7 +42,7 @@ describe 'CycleAnalytics#issue', models: true do
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
end end
expect(subject.issue).to be_nil expect(subject[:issue].median).to be_nil
end end
end end
end end
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#plan', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :plan, phase: :plan,
...@@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do ...@@ -44,7 +44,7 @@ describe 'CycleAnalytics#plan', feature: true do
create_merge_request_closing_issue(issue, source_branch: branch_name) create_merge_request_closing_issue(issue, source_branch: branch_name)
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
expect(subject.issue).to be_nil expect(subject[:issue].median).to be_nil
end end
end end
end end
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#production', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :production, phase: :production,
...@@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do ...@@ -35,7 +35,7 @@ describe 'CycleAnalytics#production', feature: true do
deploy_master deploy_master
end end
expect(subject.production).to be_nil expect(subject[:production].median).to be_nil
end end
end end
...@@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do ...@@ -48,7 +48,7 @@ describe 'CycleAnalytics#production', feature: true do
deploy_master(environment: 'staging') deploy_master(environment: 'staging')
end end
expect(subject.production).to be_nil expect(subject[:production].median).to be_nil
end end
end end
end end
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#review', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :review, phase: :review,
...@@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do ...@@ -27,7 +27,7 @@ describe 'CycleAnalytics#review', feature: true do
MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) MergeRequests::MergeService.new(project, user).execute(create(:merge_request))
end end
expect(subject.review).to be_nil expect(subject[:review].median).to be_nil
end end
end end
end end
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#staging', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :staging, phase: :staging,
...@@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do ...@@ -45,7 +45,7 @@ describe 'CycleAnalytics#staging', feature: true do
deploy_master deploy_master
end end
expect(subject.staging).to be_nil expect(subject[:staging].median).to be_nil
end end
end end
...@@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do ...@@ -58,7 +58,7 @@ describe 'CycleAnalytics#staging', feature: true do
deploy_master(environment: 'staging') deploy_master(environment: 'staging')
end end
expect(subject.staging).to be_nil expect(subject[:staging].median).to be_nil
end end
end end
end end
...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do ...@@ -6,7 +6,7 @@ describe 'CycleAnalytics#test', feature: true do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, user, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
generate_cycle_analytics_spec( generate_cycle_analytics_spec(
phase: :test, phase: :test,
...@@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do ...@@ -35,7 +35,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
end end
expect(subject.test).to be_nil expect(subject[:test].median).to be_nil
end end
end end
...@@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do ...@@ -48,7 +48,7 @@ describe 'CycleAnalytics#test', feature: true do
pipeline.succeed! pipeline.succeed!
end end
expect(subject.test).to be_nil expect(subject[:test].median).to be_nil
end end
end end
...@@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do ...@@ -65,7 +65,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
end end
expect(subject.test).to be_nil expect(subject[:test].median).to be_nil
end end
end end
...@@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do ...@@ -82,7 +82,7 @@ describe 'CycleAnalytics#test', feature: true do
merge_merge_requests_closing_issue(issue) merge_merge_requests_closing_issue(issue)
end end
expect(subject.test).to be_nil expect(subject[:test].median).to be_nil
end end
end end
end end
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
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
...@@ -2,7 +2,6 @@ ...@@ -2,7 +2,6 @@
# Note: The ABC size is large here because we have a method generating test cases with # 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. # multiple nested contexts. This shouldn't count as a violation.
module CycleAnalyticsHelpers module CycleAnalyticsHelpers
module TestGeneration module TestGeneration
# Generate the most common set of specs that all cycle analytics phases need to have. # Generate the most common set of specs that all cycle analytics phases need to have.
...@@ -51,7 +50,7 @@ module CycleAnalyticsHelpers ...@@ -51,7 +50,7 @@ module CycleAnalyticsHelpers
end end
median_time_difference = time_differences.sort[2] 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 end
context "when the data belongs to another project" do context "when the data belongs to another project" do
...@@ -83,7 +82,7 @@ module CycleAnalyticsHelpers ...@@ -83,7 +82,7 @@ module CycleAnalyticsHelpers
# Turn off the stub before checking assertions # Turn off the stub before checking assertions
allow(self).to receive(:project).and_call_original allow(self).to receive(:project).and_call_original
expect(subject.send(phase)).to be_nil expect(subject[phase].median).to be_nil
end end
end end
...@@ -106,7 +105,7 @@ module CycleAnalyticsHelpers ...@@ -106,7 +105,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
expect(subject.send(phase)).to be_nil expect(subject[phase].median).to be_nil
end end
end end
end end
...@@ -126,7 +125,7 @@ module CycleAnalyticsHelpers ...@@ -126,7 +125,7 @@ module CycleAnalyticsHelpers
Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn
end end
expect(subject.send(phase)).to be_nil expect(subject[phase].median).to be_nil
end end
end end
end end
...@@ -145,7 +144,7 @@ module CycleAnalyticsHelpers ...@@ -145,7 +144,7 @@ module CycleAnalyticsHelpers
post_fn[self, data] if post_fn post_fn[self, data] if post_fn
end end
expect(subject.send(phase)).to be_nil expect(subject[phase].median).to be_nil
end end
end end
end end
...@@ -153,7 +152,7 @@ module CycleAnalyticsHelpers ...@@ -153,7 +152,7 @@ module CycleAnalyticsHelpers
context "when none of the start / end conditions are matched" do context "when none of the start / end conditions are matched" do
it "returns nil" do it "returns nil" do
expect(subject.send(phase)).to be_nil expect(subject[phase].median).to be_nil
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment