Merge branch 'fix/refactor-cycle-analytics-stages' into 'master'
Refactor cycle analytics stages (1st iteration) See merge request !7647
This commit is contained in:
commit
1cc6d206b5
74 changed files with 832 additions and 543 deletions
|
@ -1,6 +1,10 @@
|
|||
module CycleAnalyticsParams
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def options(params)
|
||||
@options ||= { from: start_date(params), current_user: current_user }
|
||||
end
|
||||
|
||||
def start_date(params)
|
||||
params[:start_date] == '30' ? 30.days.ago : 90.days.ago
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,62 +1,38 @@
|
|||
class CycleAnalytics
|
||||
STAGES = %i[issue plan code test review staging production].freeze
|
||||
|
||||
def initialize(project, current_user, from:)
|
||||
def initialize(project, options)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@from = from
|
||||
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: from, branch: nil)
|
||||
@options = options
|
||||
end
|
||||
|
||||
def summary
|
||||
@summary ||= Summary.new(@project, @current_user, from: @from)
|
||||
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
|
||||
from: @options[:from],
|
||||
current_user: @options[:current_user]).data
|
||||
end
|
||||
|
||||
def stats
|
||||
@stats ||= stats_per_stage
|
||||
end
|
||||
|
||||
def no_stats?
|
||||
stats.all? { |hash| hash[:value].nil? }
|
||||
end
|
||||
|
||||
def permissions(user:)
|
||||
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
|
||||
end
|
||||
|
||||
def issue
|
||||
@fetcher.calculate_metric(:issue,
|
||||
Issue.arel_table[:created_at],
|
||||
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
|
||||
Issue::Metrics.arel_table[:first_added_to_board_at]])
|
||||
def [](stage_name)
|
||||
Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
|
||||
end
|
||||
|
||||
def plan
|
||||
@fetcher.calculate_metric(:plan,
|
||||
[Issue::Metrics.arel_table[:first_associated_with_milestone_at],
|
||||
Issue::Metrics.arel_table[:first_added_to_board_at]],
|
||||
Issue::Metrics.arel_table[:first_mentioned_in_commit_at])
|
||||
end
|
||||
private
|
||||
|
||||
def code
|
||||
@fetcher.calculate_metric(:code,
|
||||
Issue::Metrics.arel_table[:first_mentioned_in_commit_at],
|
||||
MergeRequest.arel_table[:created_at])
|
||||
end
|
||||
|
||||
def test
|
||||
@fetcher.calculate_metric(:test,
|
||||
MergeRequest::Metrics.arel_table[:latest_build_started_at],
|
||||
MergeRequest::Metrics.arel_table[:latest_build_finished_at])
|
||||
end
|
||||
|
||||
def review
|
||||
@fetcher.calculate_metric(:review,
|
||||
MergeRequest.arel_table[:created_at],
|
||||
MergeRequest::Metrics.arel_table[:merged_at])
|
||||
end
|
||||
|
||||
def staging
|
||||
@fetcher.calculate_metric(:staging,
|
||||
MergeRequest::Metrics.arel_table[:merged_at],
|
||||
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
|
||||
end
|
||||
|
||||
def production
|
||||
@fetcher.calculate_metric(:production,
|
||||
Issue.arel_table[:created_at],
|
||||
MergeRequest::Metrics.arel_table[:first_deployed_to_production_at])
|
||||
def stats_per_stage
|
||||
STAGES.map do |stage_name|
|
||||
self[stage_name].as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
class CycleAnalytics
|
||||
class Summary
|
||||
def initialize(project, current_user, from:)
|
||||
@project = project
|
||||
@current_user = current_user
|
||||
@from = from
|
||||
end
|
||||
|
||||
def new_issues
|
||||
IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
|
||||
end
|
||||
|
||||
def commits
|
||||
ref = @project.default_branch.presence
|
||||
count_commits_for(ref)
|
||||
end
|
||||
|
||||
def deploys
|
||||
@project.deployments.where("created_at > ?", @from).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
|
||||
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
|
||||
# the easiest way forward is to replicate the relevant portions of the
|
||||
# `log` function here.
|
||||
def count_commits_for(ref)
|
||||
return unless ref
|
||||
|
||||
repository = @project.repository.raw_repository
|
||||
sha = @project.repository.commit(ref).sha
|
||||
|
||||
cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log)
|
||||
cmd << '--format=%H'
|
||||
cmd << "--after=#{@from.iso8601}"
|
||||
cmd << sha
|
||||
|
||||
raw_output = IO.popen(cmd) { |io| io.read }
|
||||
raw_output.lines.count
|
||||
end
|
||||
end
|
||||
end
|
10
app/serializers/analytics_stage_entity.rb
Normal file
10
app/serializers/analytics_stage_entity.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
class AnalyticsStageEntity < Grape::Entity
|
||||
include EntityDateHelper
|
||||
|
||||
expose :title
|
||||
expose :description
|
||||
|
||||
expose :median, as: :value do |stage|
|
||||
stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
|
||||
end
|
||||
end
|
3
app/serializers/analytics_stage_serializer.rb
Normal file
3
app/serializers/analytics_stage_serializer.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class AnalyticsStageSerializer < BaseSerializer
|
||||
entity AnalyticsStageEntity
|
||||
end
|
7
app/serializers/analytics_summary_entity.rb
Normal file
7
app/serializers/analytics_summary_entity.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
class AnalyticsSummaryEntity < Grape::Entity
|
||||
expose :value, safe: true
|
||||
|
||||
expose :title do |object|
|
||||
object.title.pluralize(object.value)
|
||||
end
|
||||
end
|
3
app/serializers/analytics_summary_serializer.rb
Normal file
3
app/serializers/analytics_summary_serializer.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class AnalyticsSummarySerializer < BaseSerializer
|
||||
entity AnalyticsSummaryEntity
|
||||
end
|
|
@ -1,13 +1,13 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class BaseEvent
|
||||
include MetricsTables
|
||||
class BaseEventFetcher
|
||||
include BaseQuery
|
||||
|
||||
attr_reader :stage, :start_time_attrs, :end_time_attrs, :projections, :query
|
||||
attr_reader :projections, :query, :stage, :order
|
||||
|
||||
def initialize(project:, options:)
|
||||
@query = EventsQuery.new(project: project, options: options)
|
||||
def initialize(project:, stage:, options:)
|
||||
@project = project
|
||||
@stage = stage
|
||||
@options = options
|
||||
end
|
||||
|
||||
|
@ -19,10 +19,8 @@ module Gitlab
|
|||
end.compact
|
||||
end
|
||||
|
||||
def custom_query(_base_query); end
|
||||
|
||||
def order
|
||||
@order || @start_time_attrs
|
||||
@order || default_order
|
||||
end
|
||||
|
||||
private
|
||||
|
@ -34,7 +32,17 @@ module Gitlab
|
|||
end
|
||||
|
||||
def event_result
|
||||
@event_result ||= @query.execute(self).to_a
|
||||
@event_result ||= ActiveRecord::Base.connection.exec_query(events_query.to_sql).to_a
|
||||
end
|
||||
|
||||
def events_query
|
||||
diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
|
||||
|
||||
base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc)
|
||||
end
|
||||
|
||||
def default_order
|
||||
[@options[:start_time_attrs]].flatten.first
|
||||
end
|
||||
|
||||
def serialize(_event)
|
31
lib/gitlab/cycle_analytics/base_query.rb
Normal file
31
lib/gitlab/cycle_analytics/base_query.rb
Normal file
|
@ -0,0 +1,31 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module BaseQuery
|
||||
include MetricsTables
|
||||
include Gitlab::Database::Median
|
||||
include Gitlab::Database::DateTime
|
||||
|
||||
private
|
||||
|
||||
def base_query
|
||||
@base_query ||= stage_query
|
||||
end
|
||||
|
||||
def stage_query
|
||||
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
|
||||
join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
|
||||
where(issue_table[:project_id].eq(@project.id)).
|
||||
where(issue_table[:deleted_at].eq(nil)).
|
||||
where(issue_table[:created_at].gteq(@options[:from]))
|
||||
|
||||
# Load merge_requests
|
||||
query = query.join(mr_table, Arel::Nodes::OuterJoin).
|
||||
on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
|
||||
join(mr_metrics_table).
|
||||
on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
|
||||
|
||||
query
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
54
lib/gitlab/cycle_analytics/base_stage.rb
Normal file
54
lib/gitlab/cycle_analytics/base_stage.rb
Normal file
|
@ -0,0 +1,54 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class BaseStage
|
||||
include BaseQuery
|
||||
|
||||
def initialize(project:, options:)
|
||||
@project = project
|
||||
@options = options
|
||||
end
|
||||
|
||||
def events
|
||||
event_fetcher.fetch
|
||||
end
|
||||
|
||||
def as_json
|
||||
AnalyticsStageSerializer.new.represent(self).as_json
|
||||
end
|
||||
|
||||
def title
|
||||
name.to_s.capitalize
|
||||
end
|
||||
|
||||
def median
|
||||
cte_table = Arel::Table.new("cte_table_for_#{name}")
|
||||
|
||||
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
|
||||
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
|
||||
# We compute the (end_time - start_time) interval, and give it an alias based on the current
|
||||
# cycle analytics stage.
|
||||
interval_query = Arel::Nodes::As.new(
|
||||
cte_table,
|
||||
subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
|
||||
|
||||
median_datetime(cte_table, interval_query, name)
|
||||
end
|
||||
|
||||
def name
|
||||
raise NotImplementedError.new("Expected #{self.name} to implement name")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def event_fetcher
|
||||
@event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
|
||||
stage: name,
|
||||
options: event_options)
|
||||
end
|
||||
|
||||
def event_options
|
||||
@options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,22 +1,22 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class ReviewEvent < BaseEvent
|
||||
class CodeEventFetcher < BaseEventFetcher
|
||||
include MergeRequestAllowed
|
||||
|
||||
def initialize(*args)
|
||||
@stage = :review
|
||||
@start_time_attrs = mr_table[:created_at]
|
||||
@end_time_attrs = mr_metrics_table[:merged_at]
|
||||
@projections = [mr_table[:title],
|
||||
mr_table[:iid],
|
||||
mr_table[:id],
|
||||
mr_table[:created_at],
|
||||
mr_table[:state],
|
||||
mr_table[:author_id]]
|
||||
@order = mr_table[:created_at]
|
||||
|
||||
super(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize(event)
|
||||
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
|
||||
end
|
21
lib/gitlab/cycle_analytics/code_stage.rb
Normal file
21
lib/gitlab/cycle_analytics/code_stage.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class CodeStage < BaseStage
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= mr_table[:created_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:code
|
||||
end
|
||||
|
||||
def description
|
||||
"Time until first merge request"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
lib/gitlab/cycle_analytics/event_fetcher.rb
Normal file
9
lib/gitlab/cycle_analytics/event_fetcher.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module EventFetcher
|
||||
def self.[](stage_name)
|
||||
CycleAnalytics.const_get("#{stage_name.to_s.camelize}EventFetcher")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +0,0 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class Events
|
||||
def initialize(project:, options:)
|
||||
@project = project
|
||||
@options = options
|
||||
end
|
||||
|
||||
def issue_events
|
||||
IssueEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def plan_events
|
||||
PlanEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def code_events
|
||||
CodeEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def test_events
|
||||
TestEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def review_events
|
||||
ReviewEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def staging_events
|
||||
StagingEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
|
||||
def production_events
|
||||
ProductionEvent.new(project: @project, options: @options).fetch
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,37 +0,0 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class EventsQuery
|
||||
attr_reader :project
|
||||
|
||||
def initialize(project:, options: {})
|
||||
@project = project
|
||||
@from = options[:from]
|
||||
@branch = options[:branch]
|
||||
@fetcher = Gitlab::CycleAnalytics::MetricsFetcher.new(project: project, from: @from, branch: @branch)
|
||||
end
|
||||
|
||||
def execute(stage_class)
|
||||
@stage_class = stage_class
|
||||
|
||||
ActiveRecord::Base.connection.exec_query(query.to_sql)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def query
|
||||
base_query = @fetcher.base_query_for(@stage_class.stage)
|
||||
diff_fn = @fetcher.subtract_datetimes_diff(base_query, @stage_class.start_time_attrs, @stage_class.end_time_attrs)
|
||||
|
||||
@stage_class.custom_query(base_query)
|
||||
|
||||
base_query.project(extract_epoch(diff_fn).as('total_time'), *@stage_class.projections).order(@stage_class.order.desc)
|
||||
end
|
||||
|
||||
def extract_epoch(arel_attribute)
|
||||
return arel_attribute unless Gitlab::Database.postgresql?
|
||||
|
||||
Arel.sql(%Q{EXTRACT(EPOCH FROM (#{arel_attribute.to_sql}))})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,27 +0,0 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class IssueEvent < BaseEvent
|
||||
include IssueAllowed
|
||||
|
||||
def initialize(*args)
|
||||
@stage = :issue
|
||||
@start_time_attrs = issue_table[:created_at]
|
||||
@end_time_attrs = [issue_metrics_table[:first_associated_with_milestone_at],
|
||||
issue_metrics_table[:first_added_to_board_at]]
|
||||
@projections = [issue_table[:title],
|
||||
issue_table[:iid],
|
||||
issue_table[:id],
|
||||
issue_table[:created_at],
|
||||
issue_table[:author_id]]
|
||||
|
||||
super(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize(event)
|
||||
AnalyticsIssueSerializer.new(project: @project).represent(event).as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,12 +1,9 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class ProductionEvent < BaseEvent
|
||||
class IssueEventFetcher < BaseEventFetcher
|
||||
include IssueAllowed
|
||||
|
||||
def initialize(*args)
|
||||
@stage = :production
|
||||
@start_time_attrs = issue_table[:created_at]
|
||||
@end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
|
||||
@projections = [issue_table[:title],
|
||||
issue_table[:iid],
|
||||
issue_table[:id],
|
22
lib/gitlab/cycle_analytics/issue_stage.rb
Normal file
22
lib/gitlab/cycle_analytics/issue_stage.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class IssueStage < BaseStage
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= issue_table[:created_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
|
||||
issue_metrics_table[:first_added_to_board_at]]
|
||||
end
|
||||
|
||||
def name
|
||||
:issue
|
||||
end
|
||||
|
||||
def description
|
||||
"Time before an issue gets scheduled"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,60 +0,0 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class MetricsFetcher
|
||||
include Gitlab::Database::Median
|
||||
include Gitlab::Database::DateTime
|
||||
include MetricsTables
|
||||
|
||||
DEPLOYMENT_METRIC_STAGES = %i[production staging]
|
||||
|
||||
def initialize(project:, from:, branch:)
|
||||
@project = project
|
||||
@project = project
|
||||
@from = from
|
||||
@branch = branch
|
||||
end
|
||||
|
||||
def calculate_metric(name, start_time_attrs, end_time_attrs)
|
||||
cte_table = Arel::Table.new("cte_table_for_#{name}")
|
||||
|
||||
# Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
|
||||
# Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
|
||||
# We compute the (end_time - start_time) interval, and give it an alias based on the current
|
||||
# cycle analytics stage.
|
||||
interval_query = Arel::Nodes::As.new(
|
||||
cte_table,
|
||||
subtract_datetimes(base_query_for(name), start_time_attrs, end_time_attrs, name.to_s))
|
||||
|
||||
median_datetime(cte_table, interval_query, name)
|
||||
end
|
||||
|
||||
# Join table with a row for every <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
|
||||
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id])).
|
||||
join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id])).
|
||||
where(issue_table[:project_id].eq(@project.id)).
|
||||
where(issue_table[:deleted_at].eq(nil)).
|
||||
where(issue_table[:created_at].gteq(@from))
|
||||
|
||||
query = query.where(build_table[:ref].eq(@branch)) if name == :test && @branch
|
||||
|
||||
# Load merge_requests
|
||||
query = query.join(mr_table, Arel::Nodes::OuterJoin).
|
||||
on(mr_table[:id].eq(mr_closing_issues_table[:merge_request_id])).
|
||||
join(mr_metrics_table).
|
||||
on(mr_table[:id].eq(mr_metrics_table[:merge_request_id]))
|
||||
|
||||
if DEPLOYMENT_METRIC_STAGES.include?(name)
|
||||
# Limit to merge requests that have been deployed to production after `@from`
|
||||
query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
|
||||
end
|
||||
|
||||
query
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,19 +1,17 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class PlanEvent < BaseEvent
|
||||
class PlanEventFetcher < BaseEventFetcher
|
||||
def initialize(*args)
|
||||
@stage = :plan
|
||||
@start_time_attrs = issue_metrics_table[:first_associated_with_milestone_at]
|
||||
@end_time_attrs = [issue_metrics_table[:first_added_to_board_at],
|
||||
issue_metrics_table[:first_mentioned_in_commit_at]]
|
||||
@projections = [mr_diff_table[:st_commits].as('commits'),
|
||||
issue_metrics_table[:first_mentioned_in_commit_at]]
|
||||
|
||||
super(*args)
|
||||
end
|
||||
|
||||
def custom_query(base_query)
|
||||
def events_query
|
||||
base_query.join(mr_diff_table).on(mr_diff_table[:merge_request_id].eq(mr_table[:id]))
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
22
lib/gitlab/cycle_analytics/plan_stage.rb
Normal file
22
lib/gitlab/cycle_analytics/plan_stage.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class PlanStage < BaseStage
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= [issue_metrics_table[:first_associated_with_milestone_at],
|
||||
issue_metrics_table[:first_added_to_board_at]]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= issue_metrics_table[:first_mentioned_in_commit_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:plan
|
||||
end
|
||||
|
||||
def description
|
||||
"Time before an issue starts implementation"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
6
lib/gitlab/cycle_analytics/production_event_fetcher.rb
Normal file
6
lib/gitlab/cycle_analytics/production_event_fetcher.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class ProductionEventFetcher < IssueEventFetcher
|
||||
end
|
||||
end
|
||||
end
|
9
lib/gitlab/cycle_analytics/production_helper.rb
Normal file
9
lib/gitlab/cycle_analytics/production_helper.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module ProductionHelper
|
||||
def stage_query
|
||||
super.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@options[:from]))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
28
lib/gitlab/cycle_analytics/production_stage.rb
Normal file
28
lib/gitlab/cycle_analytics/production_stage.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class ProductionStage < BaseStage
|
||||
include ProductionHelper
|
||||
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= issue_table[:created_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:production
|
||||
end
|
||||
|
||||
def description
|
||||
"From issue creation until deploy to production"
|
||||
end
|
||||
|
||||
def query
|
||||
# Limit to merge requests that have been deployed to production after `@from`
|
||||
query.where(mr_metrics_table[:first_deployed_to_production_at].gteq(@from))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,25 +1,19 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class CodeEvent < BaseEvent
|
||||
class ReviewEventFetcher < BaseEventFetcher
|
||||
include MergeRequestAllowed
|
||||
|
||||
def initialize(*args)
|
||||
@stage = :code
|
||||
@start_time_attrs = issue_metrics_table[:first_mentioned_in_commit_at]
|
||||
@end_time_attrs = mr_table[:created_at]
|
||||
@projections = [mr_table[:title],
|
||||
mr_table[:iid],
|
||||
mr_table[:id],
|
||||
mr_table[:created_at],
|
||||
mr_table[:state],
|
||||
mr_table[:author_id]]
|
||||
@order = mr_table[:created_at]
|
||||
|
||||
super(*args)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize(event)
|
||||
AnalyticsMergeRequestSerializer.new(project: @project).represent(event).as_json
|
||||
end
|
21
lib/gitlab/cycle_analytics/review_stage.rb
Normal file
21
lib/gitlab/cycle_analytics/review_stage.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class ReviewStage < BaseStage
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= mr_table[:created_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= mr_metrics_table[:merged_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:review
|
||||
end
|
||||
|
||||
def description
|
||||
"Time between merge request creation and merge/close"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
9
lib/gitlab/cycle_analytics/stage.rb
Normal file
9
lib/gitlab/cycle_analytics/stage.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Stage
|
||||
def self.[](stage_name)
|
||||
CycleAnalytics.const_get("#{stage_name.to_s.camelize}Stage")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
23
lib/gitlab/cycle_analytics/stage_summary.rb
Normal file
23
lib/gitlab/cycle_analytics/stage_summary.rb
Normal file
|
@ -0,0 +1,23 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class StageSummary
|
||||
def initialize(project, from:, current_user:)
|
||||
@project = project
|
||||
@from = from
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def data
|
||||
[serialize(Summary::Issue.new(project: @project, from: @from, current_user: @current_user)),
|
||||
serialize(Summary::Commit.new(project: @project, from: @from)),
|
||||
serialize(Summary::Deploy.new(project: @project, from: @from))]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def serialize(summary_object)
|
||||
AnalyticsSummarySerializer.new.represent(summary_object).as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +1,7 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class StagingEvent < BaseEvent
|
||||
class StagingEventFetcher < BaseEventFetcher
|
||||
def initialize(*args)
|
||||
@stage = :staging
|
||||
@start_time_attrs = mr_metrics_table[:merged_at]
|
||||
@end_time_attrs = mr_metrics_table[:first_deployed_to_production_at]
|
||||
@projections = [build_table[:id]]
|
||||
@order = build_table[:created_at]
|
||||
|
||||
|
@ -17,8 +14,10 @@ module Gitlab
|
|||
super
|
||||
end
|
||||
|
||||
def custom_query(base_query)
|
||||
def events_query
|
||||
base_query.join(build_table).on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
private
|
22
lib/gitlab/cycle_analytics/staging_stage.rb
Normal file
22
lib/gitlab/cycle_analytics/staging_stage.rb
Normal file
|
@ -0,0 +1,22 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class StagingStage < BaseStage
|
||||
include ProductionHelper
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= mr_metrics_table[:merged_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= mr_metrics_table[:first_deployed_to_production_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:staging
|
||||
end
|
||||
|
||||
def description
|
||||
"From merge request merge until deploy to production"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
20
lib/gitlab/cycle_analytics/summary/base.rb
Normal file
20
lib/gitlab/cycle_analytics/summary/base.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Summary
|
||||
class Base
|
||||
def initialize(project:, from:)
|
||||
@project = project
|
||||
@from = from
|
||||
end
|
||||
|
||||
def title
|
||||
self.class.name.demodulize
|
||||
end
|
||||
|
||||
def value
|
||||
raise NotImplementedError.new("Expected #{self.name} to implement value")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
39
lib/gitlab/cycle_analytics/summary/commit.rb
Normal file
39
lib/gitlab/cycle_analytics/summary/commit.rb
Normal file
|
@ -0,0 +1,39 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Summary
|
||||
class Commit < Base
|
||||
def value
|
||||
@value ||= count_commits
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Don't use the `Gitlab::Git::Repository#log` method, because it enforces
|
||||
# a limit. Since we need a commit count, we _can't_ enforce a limit, so
|
||||
# the easiest way forward is to replicate the relevant portions of the
|
||||
# `log` function here.
|
||||
def count_commits
|
||||
return unless ref
|
||||
|
||||
repository = @project.repository.raw_repository
|
||||
sha = @project.repository.commit(ref).sha
|
||||
|
||||
cmd = %W(git --git-dir=#{repository.path} log)
|
||||
cmd << '--format=%H'
|
||||
cmd << "--after=#{@from.iso8601}"
|
||||
cmd << sha
|
||||
|
||||
output, status = Gitlab::Popen.popen(cmd)
|
||||
|
||||
raise IOError, output unless status.zero?
|
||||
|
||||
output.lines.count
|
||||
end
|
||||
|
||||
def ref
|
||||
@ref ||= @project.default_branch.presence
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
11
lib/gitlab/cycle_analytics/summary/deploy.rb
Normal file
11
lib/gitlab/cycle_analytics/summary/deploy.rb
Normal file
|
@ -0,0 +1,11 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Summary
|
||||
class Deploy < Base
|
||||
def value
|
||||
@value ||= @project.deployments.where("created_at > ?", @from).count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
21
lib/gitlab/cycle_analytics/summary/issue.rb
Normal file
21
lib/gitlab/cycle_analytics/summary/issue.rb
Normal file
|
@ -0,0 +1,21 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
module Summary
|
||||
class Issue < Base
|
||||
def initialize(project:, from:, current_user:)
|
||||
@project = project
|
||||
@from = from
|
||||
@current_user = current_user
|
||||
end
|
||||
|
||||
def title
|
||||
'New Issue'
|
||||
end
|
||||
|
||||
def value
|
||||
@value ||= IssuesFinder.new(@current_user, project_id: @project.id).execute.created_after(@from).count
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,13 +0,0 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class TestEvent < StagingEvent
|
||||
def initialize(*args)
|
||||
super(*args)
|
||||
|
||||
@stage = :test
|
||||
@start_time_attrs = mr_metrics_table[:latest_build_started_at]
|
||||
@end_time_attrs = mr_metrics_table[:latest_build_finished_at]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
6
lib/gitlab/cycle_analytics/test_event_fetcher.rb
Normal file
6
lib/gitlab/cycle_analytics/test_event_fetcher.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class TestEventFetcher < StagingEventFetcher
|
||||
end
|
||||
end
|
||||
end
|
29
lib/gitlab/cycle_analytics/test_stage.rb
Normal file
29
lib/gitlab/cycle_analytics/test_stage.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
module Gitlab
|
||||
module CycleAnalytics
|
||||
class TestStage < BaseStage
|
||||
def start_time_attrs
|
||||
@start_time_attrs ||= mr_metrics_table[:latest_build_started_at]
|
||||
end
|
||||
|
||||
def end_time_attrs
|
||||
@end_time_attrs ||= mr_metrics_table[:latest_build_finished_at]
|
||||
end
|
||||
|
||||
def name
|
||||
:test
|
||||
end
|
||||
|
||||
def description
|
||||
"Total test time for all commits/merges"
|
||||
end
|
||||
|
||||
def stage_query
|
||||
if @options[:branch]
|
||||
super.where(build_table[:ref].eq(@options[:branch]))
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -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")])
|
||||
|
|
12
spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
Normal file
12
spec/lib/gitlab/cycle_analytics/code_event_fetcher_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::CodeEventFetcher do
|
||||
let(:stage_name) { :code }
|
||||
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has a default order' do
|
||||
expect(event.order).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::CodeEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'does not have the default order' do
|
||||
expect(event.order).not_to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/code_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::CodeStage do
|
||||
let(:stage_name) { :code }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -1,12 +1,14 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::CycleAnalytics::Events do
|
||||
describe 'cycle analytics events' do
|
||||
let(:project) { create(:project) }
|
||||
let(:from_date) { 10.days.ago }
|
||||
let(:user) { create(:user, :admin) }
|
||||
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
|
||||
|
||||
subject { described_class.new(project: project, options: { from: from_date, current_user: user }) }
|
||||
let(:events) do
|
||||
CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([context])
|
||||
|
@ -15,104 +17,112 @@ describe Gitlab::CycleAnalytics::Events do
|
|||
end
|
||||
|
||||
describe '#issue_events' do
|
||||
let(:stage) { :issue }
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.issue_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it 'has a title' do
|
||||
expect(subject.issue_events.first[:title]).to eq(context.title)
|
||||
expect(events.first[:title]).to eq(context.title)
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.issue_events.first[:url]).not_to be_nil
|
||||
expect(events.first[:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has an iid' do
|
||||
expect(subject.issue_events.first[:iid]).to eq(context.iid.to_s)
|
||||
expect(events.first[:iid]).to eq(context.iid.to_s)
|
||||
end
|
||||
|
||||
it 'has a created_at timestamp' do
|
||||
expect(subject.issue_events.first[:created_at]).to end_with('ago')
|
||||
expect(events.first[:created_at]).to end_with('ago')
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.issue_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.issue_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.issue_events.first[:author][:name]).to eq(context.author.name)
|
||||
expect(events.first[:author][:name]).to eq(context.author.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plan_events' do
|
||||
let(:stage) { :plan }
|
||||
|
||||
it 'has a title' do
|
||||
expect(subject.plan_events.first[:title]).not_to be_nil
|
||||
expect(events.first[:title]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has a sha short ID' do
|
||||
expect(subject.plan_events.first[:short_sha]).not_to be_nil
|
||||
expect(events.first[:short_sha]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.plan_events.first[:commit_url]).not_to be_nil
|
||||
expect(events.first[:commit_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.plan_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.plan_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.plan_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.plan_events.first[:author][:name]).not_to be_nil
|
||||
expect(events.first[:author][:name]).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#code_events' do
|
||||
let(:stage) { :code }
|
||||
|
||||
before do
|
||||
create_commit_referencing_issue(context)
|
||||
end
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.code_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it 'has a title' do
|
||||
expect(subject.code_events.first[:title]).to eq('Awesome merge_request')
|
||||
expect(events.first[:title]).to eq('Awesome merge_request')
|
||||
end
|
||||
|
||||
it 'has an iid' do
|
||||
expect(subject.code_events.first[:iid]).to eq(context.iid.to_s)
|
||||
expect(events.first[:iid]).to eq(context.iid.to_s)
|
||||
end
|
||||
|
||||
it 'has a created_at timestamp' do
|
||||
expect(subject.code_events.first[:created_at]).to end_with('ago')
|
||||
expect(events.first[:created_at]).to end_with('ago')
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.code_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.code_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.code_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#test_events' do
|
||||
let(:stage) { :test }
|
||||
|
||||
let(:merge_request) { MergeRequest.first }
|
||||
let!(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
|
@ -130,83 +140,85 @@ describe Gitlab::CycleAnalytics::Events do
|
|||
end
|
||||
|
||||
it 'has the name' do
|
||||
expect(subject.test_events.first[:name]).not_to be_nil
|
||||
expect(events.first[:name]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the ID' do
|
||||
expect(subject.test_events.first[:id]).not_to be_nil
|
||||
expect(events.first[:id]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.test_events.first[:url]).not_to be_nil
|
||||
expect(events.first[:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the branch name' do
|
||||
expect(subject.test_events.first[:branch]).not_to be_nil
|
||||
expect(events.first[:branch]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the branch URL' do
|
||||
expect(subject.test_events.first[:branch][:url]).not_to be_nil
|
||||
expect(events.first[:branch][:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the short SHA' do
|
||||
expect(subject.test_events.first[:short_sha]).not_to be_nil
|
||||
expect(events.first[:short_sha]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the commit URL' do
|
||||
expect(subject.test_events.first[:commit_url]).not_to be_nil
|
||||
expect(events.first[:commit_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the date' do
|
||||
expect(subject.test_events.first[:date]).not_to be_nil
|
||||
expect(events.first[:date]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.test_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe '#review_events' do
|
||||
let(:stage) { :review }
|
||||
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.review_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it 'has a title' do
|
||||
expect(subject.review_events.first[:title]).to eq('Awesome merge_request')
|
||||
expect(events.first[:title]).to eq('Awesome merge_request')
|
||||
end
|
||||
|
||||
it 'has an iid' do
|
||||
expect(subject.review_events.first[:iid]).to eq(context.iid.to_s)
|
||||
expect(events.first[:iid]).to eq(context.iid.to_s)
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.review_events.first[:url]).not_to be_nil
|
||||
expect(events.first[:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has a state' do
|
||||
expect(subject.review_events.first[:state]).not_to be_nil
|
||||
expect(events.first[:state]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has a created_at timestamp' do
|
||||
expect(subject.review_events.first[:created_at]).not_to be_nil
|
||||
expect(events.first[:created_at]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.review_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.review_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.review_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#staging_events' do
|
||||
let(:stage) { :staging }
|
||||
let(:merge_request) { MergeRequest.first }
|
||||
let!(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
|
@ -227,55 +239,56 @@ describe Gitlab::CycleAnalytics::Events do
|
|||
end
|
||||
|
||||
it 'has the name' do
|
||||
expect(subject.staging_events.first[:name]).not_to be_nil
|
||||
expect(events.first[:name]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the ID' do
|
||||
expect(subject.staging_events.first[:id]).not_to be_nil
|
||||
expect(events.first[:id]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.staging_events.first[:url]).not_to be_nil
|
||||
expect(events.first[:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the branch name' do
|
||||
expect(subject.staging_events.first[:branch]).not_to be_nil
|
||||
expect(events.first[:branch]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the branch URL' do
|
||||
expect(subject.staging_events.first[:branch][:url]).not_to be_nil
|
||||
expect(events.first[:branch][:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the short SHA' do
|
||||
expect(subject.staging_events.first[:short_sha]).not_to be_nil
|
||||
expect(events.first[:short_sha]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the commit URL' do
|
||||
expect(subject.staging_events.first[:commit_url]).not_to be_nil
|
||||
expect(events.first[:commit_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the date' do
|
||||
expect(subject.staging_events.first[:date]).not_to be_nil
|
||||
expect(events.first[:date]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.staging_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.staging_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.staging_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.staging_events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
expect(events.first[:author][:name]).to eq(MergeRequest.first.author.name)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#production_events' do
|
||||
let(:stage) { :production }
|
||||
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
|
||||
|
||||
before do
|
||||
|
@ -284,35 +297,35 @@ describe Gitlab::CycleAnalytics::Events do
|
|||
end
|
||||
|
||||
it 'has the total time' do
|
||||
expect(subject.production_events.first[:total_time]).not_to be_empty
|
||||
expect(events.first[:total_time]).not_to be_empty
|
||||
end
|
||||
|
||||
it 'has a title' do
|
||||
expect(subject.production_events.first[:title]).to eq(context.title)
|
||||
expect(events.first[:title]).to eq(context.title)
|
||||
end
|
||||
|
||||
it 'has the URL' do
|
||||
expect(subject.production_events.first[:url]).not_to be_nil
|
||||
expect(events.first[:url]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has an iid' do
|
||||
expect(subject.production_events.first[:iid]).to eq(context.iid.to_s)
|
||||
expect(events.first[:iid]).to eq(context.iid.to_s)
|
||||
end
|
||||
|
||||
it 'has a created_at timestamp' do
|
||||
expect(subject.production_events.first[:created_at]).to end_with('ago')
|
||||
expect(events.first[:created_at]).to end_with('ago')
|
||||
end
|
||||
|
||||
it "has the author's URL" do
|
||||
expect(subject.production_events.first[:author][:web_url]).not_to be_nil
|
||||
expect(events.first[:author][:web_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's avatar URL" do
|
||||
expect(subject.production_events.first[:author][:avatar_url]).not_to be_nil
|
||||
expect(events.first[:author][:avatar_url]).not_to be_nil
|
||||
end
|
||||
|
||||
it "has the author's name" do
|
||||
expect(subject.production_events.first[:author][:name]).to eq(context.author.name)
|
||||
expect(events.first[:author][:name]).to eq(context.author.name)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::IssueEventFetcher do
|
||||
let(:stage_name) { :issue }
|
||||
|
||||
it_behaves_like 'default query config'
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::IssueEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has the default order' do
|
||||
expect(event.order).to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/issue_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::IssueStage do
|
||||
let(:stage_name) { :issue }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -1,15 +1,13 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::PlanEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has the default order' do
|
||||
expect(event.order).to eq(event.start_time_attrs)
|
||||
end
|
||||
describe Gitlab::CycleAnalytics::PlanEventFetcher do
|
||||
let(:stage_name) { :plan }
|
||||
|
||||
it_behaves_like 'default query config' do
|
||||
context 'no commits' do
|
||||
it 'does not blow up if there are no commits' do
|
||||
allow_any_instance_of(Gitlab::CycleAnalytics::EventsQuery).to receive(:execute).and_return([{}])
|
||||
allow(event).to receive(:event_result).and_return([{}])
|
||||
|
||||
expect { event.fetch }.not_to raise_error
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/plan_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::PlanStage do
|
||||
let(:stage_name) { :plan }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ProductionEventFetcher do
|
||||
let(:stage_name) { :production }
|
||||
|
||||
it_behaves_like 'default query config'
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ProductionEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has the default order' do
|
||||
expect(event.order).to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/production_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ProductionStage do
|
||||
let(:stage_name) { :production }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ReviewEventFetcher do
|
||||
let(:stage_name) { :review }
|
||||
|
||||
it_behaves_like 'default query config'
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ReviewEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has the default order' do
|
||||
expect(event.order).to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/review_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::ReviewStage do
|
||||
let(:stage_name) { :review }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -1,20 +1,13 @@
|
|||
require 'spec_helper'
|
||||
|
||||
shared_examples 'default query config' do
|
||||
let(:event) { described_class.new(project: double, options: {}) }
|
||||
|
||||
it 'has the start attributes' do
|
||||
expect(event.start_time_attrs).not_to be_nil
|
||||
end
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:event) { described_class.new(project: project, stage: stage_name, options: { from: 1.day.ago }) }
|
||||
|
||||
it 'has the stage attribute' do
|
||||
expect(event.stage).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the end attributes' do
|
||||
expect(event.end_time_attrs).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the projection attributes' do
|
||||
expect(event.projections).not_to be_nil
|
||||
end
|
||||
|
|
30
spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
Normal file
30
spec/lib/gitlab/cycle_analytics/shared_stage_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
require 'spec_helper'
|
||||
|
||||
shared_examples 'base stage' do
|
||||
let(:stage) { described_class.new(project: double, options: {}) }
|
||||
|
||||
before do
|
||||
allow(stage).to receive(:median).and_return(1.12)
|
||||
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
|
||||
end
|
||||
|
||||
it 'has the median data value' do
|
||||
expect(stage.as_json[:value]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the median data stage' do
|
||||
expect(stage.as_json[:title]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the median data description' do
|
||||
expect(stage.as_json[:description]).not_to be_nil
|
||||
end
|
||||
|
||||
it 'has the title' do
|
||||
expect(stage.title).to eq(stage_name.to_s.capitalize)
|
||||
end
|
||||
|
||||
it 'has the events' do
|
||||
expect(stage.events).not_to be_nil
|
||||
end
|
||||
end
|
|
@ -1,23 +1,23 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe CycleAnalytics::Summary, models: true do
|
||||
describe Gitlab::CycleAnalytics::StageSummary, models: true do
|
||||
let(:project) { create(:project) }
|
||||
let(:from) { Time.now }
|
||||
let(:from) { 1.day.ago }
|
||||
let(:user) { create(:user, :admin) }
|
||||
subject { described_class.new(project, user, from: from) }
|
||||
subject { described_class.new(project, from: Time.now, current_user: user).data }
|
||||
|
||||
describe "#new_issues" do
|
||||
it "finds the number of issues created after the 'from date'" do
|
||||
Timecop.freeze(5.days.ago) { create(:issue, project: project) }
|
||||
Timecop.freeze(5.days.from_now) { create(:issue, project: project) }
|
||||
|
||||
expect(subject.new_issues).to eq(1)
|
||||
expect(subject.first[:value]).to eq(1)
|
||||
end
|
||||
|
||||
it "doesn't find issues from other projects" do
|
||||
Timecop.freeze(5.days.from_now) { create(:issue, project: create(:project)) }
|
||||
|
||||
expect(subject.new_issues).to eq(0)
|
||||
expect(subject.first[:value]).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -26,19 +26,19 @@ describe CycleAnalytics::Summary, models: true do
|
|||
Timecop.freeze(5.days.ago) { create_commit("Test message", project, user, 'master') }
|
||||
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master') }
|
||||
|
||||
expect(subject.commits).to eq(1)
|
||||
expect(subject.second[:value]).to eq(1)
|
||||
end
|
||||
|
||||
it "doesn't find commits from other projects" do
|
||||
Timecop.freeze(5.days.from_now) { create_commit("Test message", create(:project), user, 'master') }
|
||||
|
||||
expect(subject.commits).to eq(0)
|
||||
expect(subject.second[:value]).to eq(0)
|
||||
end
|
||||
|
||||
it "finds a large (> 100) snumber of commits if present" do
|
||||
Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
|
||||
|
||||
expect(subject.commits).to eq(100)
|
||||
expect(subject.second[:value]).to eq(100)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -47,13 +47,13 @@ describe CycleAnalytics::Summary, models: true do
|
|||
Timecop.freeze(5.days.ago) { create(:deployment, project: project) }
|
||||
Timecop.freeze(5.days.from_now) { create(:deployment, project: project) }
|
||||
|
||||
expect(subject.deploys).to eq(1)
|
||||
expect(subject.third[:value]).to eq(1)
|
||||
end
|
||||
|
||||
it "doesn't find commits from other projects" do
|
||||
Timecop.freeze(5.days.from_now) { create(:deployment, project: create(:project)) }
|
||||
|
||||
expect(subject.deploys).to eq(0)
|
||||
expect(subject.third[:value]).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::StagingEventFetcher do
|
||||
let(:stage_name) { :staging }
|
||||
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has a default order' do
|
||||
expect(event.order).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::StagingEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'does not have the default order' do
|
||||
expect(event.order).not_to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/staging_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::StagingStage do
|
||||
let(:stage_name) { :staging }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
12
spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
Normal file
12
spec/lib/gitlab/cycle_analytics/test_event_fetcher_spec.rb
Normal file
|
@ -0,0 +1,12 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::TestEventFetcher do
|
||||
let(:stage_name) { :test }
|
||||
|
||||
it_behaves_like 'default query config' do
|
||||
it 'has a default order' do
|
||||
expect(event.order).not_to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,10 +0,0 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_event_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::TestEvent do
|
||||
it_behaves_like 'default query config' do
|
||||
it 'does not have the default order' do
|
||||
expect(event.order).not_to eq(event.start_time_attrs)
|
||||
end
|
||||
end
|
||||
end
|
8
spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
Normal file
8
spec/lib/gitlab/cycle_analytics/test_stage_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
require 'spec_helper'
|
||||
require 'lib/gitlab/cycle_analytics/shared_stage_spec'
|
||||
|
||||
describe Gitlab::CycleAnalytics::TestStage do
|
||||
let(:stage_name) { :test }
|
||||
|
||||
it_behaves_like 'base stage'
|
||||
end
|
|
@ -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
|
||||
|
|
24
spec/serializers/analytics_stage_serializer_spec.rb
Normal file
24
spec/serializers/analytics_stage_serializer_spec.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AnalyticsStageSerializer do
|
||||
let(:serializer) do
|
||||
described_class
|
||||
.new.represent(resource)
|
||||
end
|
||||
|
||||
let(:json) { serializer.as_json }
|
||||
let(:resource) { Gitlab::CycleAnalytics::CodeStage.new(project: double, options: {}) }
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(1.12)
|
||||
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
|
||||
end
|
||||
|
||||
it 'it generates payload for single object' do
|
||||
expect(json).to be_kind_of Hash
|
||||
end
|
||||
|
||||
it 'contains important elements of AnalyticsStage' do
|
||||
expect(json).to include(:title, :description, :value)
|
||||
end
|
||||
end
|
29
spec/serializers/analytics_summary_serializer_spec.rb
Normal file
29
spec/serializers/analytics_summary_serializer_spec.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe AnalyticsSummarySerializer do
|
||||
let(:serializer) do
|
||||
described_class
|
||||
.new.represent(resource)
|
||||
end
|
||||
|
||||
let(:json) { serializer.as_json }
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:resource) do
|
||||
Gitlab::CycleAnalytics::Summary::Issue.new(project: double,
|
||||
from: 1.day.ago,
|
||||
current_user: user)
|
||||
end
|
||||
|
||||
before do
|
||||
allow_any_instance_of(Gitlab::CycleAnalytics::Summary::Issue).to receive(:value).and_return(1.12)
|
||||
end
|
||||
|
||||
it 'it generates payload for single object' do
|
||||
expect(json).to be_kind_of Hash
|
||||
end
|
||||
|
||||
it 'contains important elements of AnalyticsStage' do
|
||||
expect(json).to include(:title, :value)
|
||||
end
|
||||
end
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue