Commit 745f1f8d authored by Douwe Maan's avatar Douwe Maan

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

Refactored 1st iteration of cycle analytics and stages in general (EE version)

See merge request !1064
parents 1b59c006 745e0f87
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
......
......@@ -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
......
......@@ -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
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 permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
def stats
@stats ||= stats_per_stage
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 no_stats?
stats.all? { |hash| hash[:value].nil? }
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
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])
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def review
@fetcher.calculate_metric(:review,
MergeRequest.arel_table[:created_at],
MergeRequest::Metrics.arel_table[:merged_at])
def [](stage_name)
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
end
def staging
@fetcher.calculate_metric(:staging,
MergeRequest::Metrics.arel_table[:merged_at],
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
end
private
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
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 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)
......
module Gitlab
module CycleAnalytics
class MetricsFetcher
module BaseQuery
include MetricsTables
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))
private
median_datetime(cte_table, interval_query, name)
def base_query
@base_query ||= stage_query
end
# Join table with a row for every <issue,merge_request> 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
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(@from))
query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
where(issue_table[:created_at].gteq(@options[:from]))
# Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin).
......@@ -48,11 +24,6 @@ module Gitlab
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
......
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 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
......
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 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],
......
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 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
......
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 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
......
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 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
......
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
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")])
......
require 'spec_helper'
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 'has the default order' do
expect(event.order).to eq(event.start_time_attrs)
it 'has a default order' do
expect(event.order).not_to be_nil
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 '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 '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
......
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'
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
......
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'
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
require 'spec_helper'
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 'has the default order' do
expect(event.order).to eq(event.start_time_attrs)
it 'has a default order' do
expect(event.order).not_to be_nil
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 '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 'does not have the default order' do
expect(event.order).not_to eq(event.start_time_attrs)
it 'has a default order' do
expect(event.order).not_to be_nil
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
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
......
......@@ -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
......@@ -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
......@@ -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
......@@ -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
......@@ -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
......@@ -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
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 @@
# 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
......
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