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
@@ -5,44 +5,14 @@ describe 'CycleAnalytics#issue', models: true do
Loading
@@ -5,44 +5,14 @@ describe 'CycleAnalytics#issue', models: true do
let(:from_date) { 10.days.ago } let(:from_date) { 10.days.ago }
subject { CycleAnalytics.new(project, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
   
context "when a milestone is added to the issue" do generate_cycle_analytics_spec(phase: :issue,
it "calculates the median of available durations (between issue creation and milestone addition)" do data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
time_differences = Array.new(5) do start_time_conditions: [["issue created", -> (context, data) { data[:issue].save }]],
start_time = Time.now end_time_conditions: [["issue associated with a milestone", -> (context, data) { data[:issue].update(milestone: context.create(:milestone, project: context.project)) if data[:issue].persisted? }],
end_time = rand(1..10).days.from_now ["list label added to issue", -> (context, data) { data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) if data[:issue].persisted? }]])
milestone = create(:milestone, project: project) context "when a regular label (instead of a list label) is added to the issue" do
issue = Timecop.freeze(start_time) { create(:issue, project: project) } it "returns nil" do
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
5.times do 5.times do
regular_label = create(:label) regular_label = create(:label)
issue = create(:issue, project: project) issue = create(:issue, project: project)
Loading
@@ -53,39 +23,14 @@ describe 'CycleAnalytics#issue', models: true do
Loading
@@ -53,39 +23,14 @@ describe 'CycleAnalytics#issue', models: true do
end end
end end
   
context "when a milestone and list-label are both added to the issue" do context "when the issue belongs to a different project" do
it "calculates the median of available durations (between issue creation and milestone addition)" do it 'returns nil' do
start_time = Time.now other_project = create(:project)
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])
   
expect(subject.issue).to be_nil 5.times do
end issue = create(:issue, project: other_project)
issue.update(milestone: create(:milestone, project: other_project))
it "excludes issues created before the 'from' date" do end
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])
   
expect(subject.issue).to be_nil expect(subject.issue).to be_nil
end end
Loading
Loading
Loading
@@ -6,126 +6,38 @@ describe 'CycleAnalytics#plan', feature: true do
Loading
@@ -6,126 +6,38 @@ describe 'CycleAnalytics#plan', feature: true do
let(:user) { create(:user, :admin) } let(:user) { create(:user, :admin) }
subject { CycleAnalytics.new(project, from: from_date) } subject { CycleAnalytics.new(project, from: from_date) }
   
def create_commit_referencing_issue(issue, time: Time.now) def create_commit_referencing_issue(issue)
sha = Timecop.freeze(time) { project.repository.commit_file(user, FFaker::Product.brand, "content", "Commit for ##{issue.iid}", "master", false) } sha = project.repository.commit_file(user, FFaker::Product.brand, "content", "Commit for ##{issue.iid}", "master", false)
commit = project.repository.commit(sha) commit = project.repository.commit(sha)
commit.create_cross_references! commit.create_cross_references!
end end
   
context "when a milestone is added to the issue" do generate_cycle_analytics_spec(phase: :plan,
context "when the issue is mentioned in a commit" do data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
it "calculates the median of available durations between the two" do start_time_conditions: [["issue associated with a milestone", -> (context, data) { data[:issue].update(milestone: context.create(:milestone, project: context.project)) }],
time_differences = Array.new(5) do ["list label added to issue", -> (context, data) { data[:issue].update(label_ids: [context.create(:label, lists: [context.create(:list)]).id]) }]],
start_time = Time.now end_time_conditions: [["issue mentioned in a commit", -> (context, data) { context.create_commit_referencing_issue(data[:issue]) }]])
end_time = rand(1..10).days.from_now
   
milestone = create(:milestone, project: project) context "when a regular label (instead of a list label) is added to the issue" do
issue = create(:issue, project: project) 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) } expect(subject.issue).to be_nil
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
end end
end end
   
context "when a milestone and list-label are both added to the issue" do it "does not include issues from other projects" do
context "when the issue is mentioned in a commit" do other_project = create(:project)
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]
   
# Use `be_within` to account for time lost between Rails invoking CLI git list_label = create(:label, lists: [create(:list)])
# and the commit being created, which Timecop can't freeze. issue = create(:issue, project: other_project)
expect(subject.plan).to be_within(2).of(median_time_difference) issue.update(milestone: create(:milestone))
end issue.update(label_ids: [list_label.id])
create_commit_referencing_issue(issue)
   
it "does not include issues from other projects" do expect(subject.issue).to be_nil
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
end end
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