Skip to content
Snippets Groups Projects
Commit 98c9d120 authored by Timothy Andrew's avatar Timothy Andrew
Browse files

Refactor cycle analytics specs.

1. Generalise the specs that will be common across all cycle analytics
   phases.

2. Rewrite specs `issue` and `plan` to use this abstracted testing
   strategy.

3. Specs that are specific to a given phase, or unwieldy to test in an
   abstracted manner, are added to each phase's spec.
parent 0f748605
No related branches found
No related tags found
1 merge request!5986Cycle Analytics: first iteration
Pipeline #
Loading
Loading
@@ -5,44 +5,14 @@ describe 'CycleAnalytics#issue', models: true do
let(:from_date) { 10.days.ago }
subject { CycleAnalytics.new(project, from: from_date) }
 
context "when a milestone is added to the issue" do
it "calculates the median of available durations (between issue creation and milestone addition)" do
time_differences = Array.new(5) do
start_time = Time.now
end_time = rand(1..10).days.from_now
milestone = create(:milestone, project: project)
issue = Timecop.freeze(start_time) { create(:issue, project: project) }
Timecop.freeze(end_time) { issue.update(milestone: milestone) }
end_time - start_time
end
median_time_difference = time_differences.sort[2]
expect(subject.issue).to eq(median_time_difference)
end
end
context "when a label is added to the issue" do
context "when the label is a list-label" do
it "calculates the median of available durations (between issue creation and label addition)" do
time_differences = Array.new(5) do
start_time = Time.now
end_time = rand(1..10).days.from_now
list_label = create(:label, lists: [create(:list)])
issue = Timecop.freeze(start_time) { create(:issue, project: project) }
Timecop.freeze(end_time) { issue.update(label_ids: [list_label.id]) }
end_time - start_time
end
median_time_difference = time_differences.sort[2]
expect(subject.issue).to eq(median_time_difference)
end
end
it "does not make a calculation for regular labels" do
generate_cycle_analytics_spec(phase: :issue,
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
end_time_conditions: [["issue associated with a milestone", -> (context, data) { data[:issue].update(milestone: context.create(:milestone, project: context.project)) if data[:issue].persisted? }],
["list label added to issue", -> (context, data) { data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) if data[:issue].persisted? }]])
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
5.times do
regular_label = create(:label)
issue = create(:issue, project: project)
Loading
Loading
@@ -53,39 +23,14 @@ describe 'CycleAnalytics#issue', models: true do
end
end
 
context "when a milestone and list-label are both added to the issue" do
it "calculates the median of available durations (between issue creation and milestone addition)" do
start_time = Time.now
milestone_add_time = rand(1..10).days.from_now
list_label_add_time = rand(1..10).days.from_now
milestone = create(:milestone, project: project)
list_label = create(:label, lists: [create(:list)])
issue = Timecop.freeze(start_time) { create(:issue, project: project) }
Timecop.freeze(milestone_add_time) { issue.update(milestone: milestone) }
Timecop.freeze(list_label_add_time) { issue.update(label_ids: [list_label.id]) }
expect(subject.issue).to eq(milestone_add_time - start_time)
end
it "does not include issues from other projects" do
milestone = create(:milestone, project: project)
list_label = create(:label, lists: [create(:list)])
issue = create(:issue)
issue.update(milestone: milestone)
issue.update(label_ids: [list_label.id])
context "when the issue belongs to a different project" do
it 'returns nil' do
other_project = create(:project)
 
expect(subject.issue).to be_nil
end
it "excludes issues created before the 'from' date" do
before_from_date = from_date - 5.days
milestone = create(:milestone, project: project)
list_label = create(:label, lists: [create(:list)])
issue = Timecop.freeze(before_from_date) { create(:issue, project: project)}
issue.update(milestone: milestone)
issue.update(label_ids: [list_label.id])
5.times do
issue = create(:issue, project: other_project)
issue.update(milestone: create(:milestone, project: other_project))
end
 
expect(subject.issue).to be_nil
end
Loading
Loading
Loading
Loading
@@ -6,126 +6,38 @@ describe 'CycleAnalytics#plan', feature: true do
let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) }
 
def create_commit_referencing_issue(issue, time: Time.now)
sha = Timecop.freeze(time) { project.repository.commit_file(user, FFaker::Product.brand, "content", "Commit for ##{issue.iid}", "master", false) }
def create_commit_referencing_issue(issue)
sha = project.repository.commit_file(user, FFaker::Product.brand, "content", "Commit for ##{issue.iid}", "master", false)
commit = project.repository.commit(sha)
commit.create_cross_references!
end
 
context "when a milestone is added to the issue" do
context "when the issue is mentioned in a commit" do
it "calculates the median of available durations between the two" do
time_differences = Array.new(5) do
start_time = Time.now
end_time = rand(1..10).days.from_now
generate_cycle_analytics_spec(phase: :plan,
data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
start_time_conditions: [["issue associated with a milestone", -> (context, data) { data[:issue].update(milestone: context.create(:milestone, project: context.project)) }],
["list label added to issue", -> (context, data) { data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) }]],
end_time_conditions: [["issue mentioned in a commit", -> (context, data) { context.create_commit_referencing_issue(data[:issue]) }]])
 
milestone = create(:milestone, project: project)
issue = create(:issue, project: project)
context "when a regular label (instead of a list label) is added to the issue" do
it "returns nil" do
label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [label.id])
create_commit_referencing_issue(issue)
 
Timecop.freeze(start_time) { issue.update(milestone: milestone) }
create_commit_referencing_issue(issue, time: end_time)
end_time - start_time
end
median_time_difference = time_differences.sort[2]
# Use `be_within` to account for time lost between Rails invoking CLI git
# and the commit being created, which Timecop can't freeze.
expect(subject.plan).to be_within(2).of(median_time_difference)
end
end
end
context "when a label is added to the issue" do
context "when the issue is mentioned in a commit" do
context "when the label is a list-label" do
it "calculates the median of available durations between the two" do
time_differences = Array.new(5) do
start_time = Time.now
end_time = rand(1..10).days.from_now
issue = create(:issue, project: project)
list_label = create(:label, lists: [create(:list)])
Timecop.freeze(start_time) { issue.update(label_ids: [list_label.id]) }
create_commit_referencing_issue(issue, time: end_time)
end_time - start_time
end
median_time_difference = time_differences.sort[2]
# Use `be_within` to account for time lost between Rails invoking CLI git
# and the commit being created, which Timecop can't freeze.
expect(subject.plan).to be_within(2).of(median_time_difference)
end
end
it "does not make a calculation for regular labels" do
5.times do
regular_label = create(:label)
issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id])
create_commit_referencing_issue(issue)
end
expect(subject.plan).to be_nil
end
expect(subject.issue).to be_nil
end
end
 
context "when a milestone and list-label are both added to the issue" do
context "when the issue is mentioned in a commit" do
it "calculates the median of available durations between the two (using milestone addition as the 'start_time')" do
time_differences = Array.new(5) do
label_addition_time = Time.now
milestone_addition_time = rand(2..12).hours.from_now
end_time = rand(1..10).days.from_now
issue = create(:issue, project: project)
milestone = create(:milestone, project: project)
list_label = create(:label, lists: [create(:list)])
Timecop.freeze(label_addition_time) { issue.update(label_ids: [list_label.id]) }
Timecop.freeze(milestone_addition_time) { issue.update(milestone: milestone) }
create_commit_referencing_issue(issue, time: end_time)
end_time - milestone_addition_time
end
median_time_difference = time_differences.sort[2]
it "does not include issues from other projects" do
other_project = create(:project)
 
# Use `be_within` to account for time lost between Rails invoking CLI git
# and the commit being created, which Timecop can't freeze.
expect(subject.plan).to be_within(2).of(median_time_difference)
end
list_label = create(:label, lists: [create(:list)])
issue = create(:issue, project: other_project)
issue.update(milestone: create(:milestone))
issue.update(label_ids: [list_label.id])
create_commit_referencing_issue(issue)
 
it "does not include issues from other projects" do
other_project = create(:project)
list_label = create(:label, lists: [create(:list)])
issue = create(:issue, project: other_project)
issue.update(milestone: create(:milestone))
issue.update(label_ids: [list_label.id])
create_commit_referencing_issue(issue)
expect(subject.issue).to be_nil
end
it "excludes issues created before the 'from' date" do
before_from_date = from_date - 5.days
milestone = create(:milestone, project: project)
list_label = create(:label, lists: [create(:list)])
issue = Timecop.freeze(before_from_date) { create(:issue, project: project)}
issue.update(milestone: milestone)
issue.update(label_ids: [list_label.id])
create_commit_referencing_issue(issue)
expect(subject.issue).to be_nil
end
end
expect(subject.issue).to be_nil
end
end
# rubocop:disable Metrics/AbcSize
# 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
# Generate the most common set of specs that all cycle analytics phases need to have.
#
# Arguments:
#
# phase: Which phase are we testing? Will call `CycleAnalytics.new.send(phase)` for the final assertion
# data_fn: A function that returns a hash, constituting initial data for the test case
# start_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
# `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
# Each `condition_fn` is expected to implement a case which consitutes the start of the given cycle analytics phase.
# end_time_conditions: An array of `conditions`. Each condition is an tuple of `condition_name` and `condition_fn`. `condition_fn` is called with
# `context` (no lexical scope, so need to do `context.create` for factories, for example) and `data` (from the `data_fn`).
# Each `condition_fn` is expected to implement a case which consitutes the end of the given cycle analytics phase.
def generate_cycle_analytics_spec(phase:, data_fn:, start_time_conditions:, end_time_conditions:)
combinations_of_start_time_conditions = (1..start_time_conditions.size).flat_map { |size| start_time_conditions.combination(size).to_a }
combinations_of_end_time_conditions = (1..end_time_conditions.size).flat_map { |size| end_time_conditions.combination(size).to_a }
scenarios = combinations_of_start_time_conditions.product(combinations_of_end_time_conditions)
scenarios.each do |start_time_conditions, end_time_conditions|
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "finds the median of available durations between the two conditions" do
time_differences = Array.new(5) do
data = data_fn[self]
start_time = Time.now
end_time = rand(1..10).days.from_now
start_time_conditions.each do |condition_name, condition_fn|
Timecop.freeze(start_time) { condition_fn[self, data] }
end
end_time_conditions.each do |condition_name, condition_fn|
Timecop.freeze(end_time) { condition_fn[self, data] }
end
end_time - start_time
end
median_time_difference = time_differences.sort[2]
expect(subject.send(phase)).to be_within(2).of(median_time_difference)
end
end
end
context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
5.times do
data = data_fn[self]
end_time = rand(1..10).days.from_now
end_time_conditions.each_with_index do |(condition_name, condition_fn), index|
Timecop.freeze(end_time + index.days) { condition_fn[self, data] }
end
end
expect(subject.send(phase)).to be_nil
end
end
end
context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do
context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do
it "returns nil" do
5.times do
data = data_fn[self]
start_time = Time.now
start_time_conditions.each do |condition_name, condition_fn|
Timecop.freeze(start_time) { condition_fn[self, data] }
end
end
expect(subject.send(phase)).to be_nil
end
end
end
end
context "when none of the start / end conditions are matched" do
it "returns nil" do
expect(subject.send(phase)).to be_nil
end
end
end
end
RSpec.configure do |config|
config.extend CycleAnalyticsHelpers
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment