Commit 9022a33d authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'milestone-burnup-charts-graphql' into 'master'

Add milestone burnup charts to GraphQL

See merge request gitlab-org/gitlab!38672
parents 9c95852d 66143fd6
......@@ -60,3 +60,5 @@ module Types
end
end
end
Types::MilestoneType.prepend_if_ee('::EE::Types::MilestoneType')
......@@ -1427,6 +1427,36 @@ type Branch {
name: String!
}
"""
Represents the total number of issues and their weights for a particular day.
"""
type BurnupChartDailyTotals {
"""
Number of closed issues as of this day
"""
completedCount: Int!
"""
Total weight of closed issues as of this day
"""
completedWeight: Int!
"""
Date for burnup totals
"""
date: ISO8601Date!
"""
Number of issues as of this day
"""
scopeCount: Int!
"""
Total weight of issues as of this day
"""
scopeWeight: Int!
}
type CiGroup {
"""
Jobs in group
......@@ -9549,6 +9579,11 @@ type MetricsDashboardAnnotationEdge {
Represents a milestone.
"""
type Milestone {
"""
Daily scope and completed totals for burnup charts
"""
burnupTimeSeries: [BurnupChartDailyTotals!]
"""
Timestamp of milestone creation
"""
......
......@@ -3851,6 +3851,109 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"description": "Represents the total number of issues and their weights for a particular day.",
"fields": [
{
"name": "completedCount",
"description": "Number of closed issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "completedWeight",
"description": "Total weight of closed issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "date",
"description": "Date for burnup totals",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ISO8601Date",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopeCount",
"description": "Number of issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "scopeWeight",
"description": "Total weight of issues as of this day",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "CiGroup",
......@@ -26709,6 +26812,28 @@
"name": "Milestone",
"description": "Represents a milestone.",
"fields": [
{
"name": "burnupTimeSeries",
"description": "Daily scope and completed totals for burnup charts",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "BurnupChartDailyTotals",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of milestone creation",
......@@ -245,6 +245,18 @@ Autogenerated return type of BoardListUpdateLimitMetrics
| `commit` | Commit | Commit for the branch |
| `name` | String! | Name of the branch |
## BurnupChartDailyTotals
Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| --- | ---- | ---------- |
| `completedCount` | Int! | Number of closed issues as of this day |
| `completedWeight` | Int! | Total weight of closed issues as of this day |
| `date` | ISO8601Date! | Date for burnup totals |
| `scopeCount` | Int! | Number of issues as of this day |
| `scopeWeight` | Int! | Total weight of issues as of this day |
## CiGroup
| Name | Type | Description |
......@@ -1477,6 +1489,7 @@ Represents a milestone.
| Name | Type | Description |
| --- | ---- | ---------- |
| `burnupTimeSeries` | BurnupChartDailyTotals! => Array | Daily scope and completed totals for burnup charts |
| `createdAt` | Time! | Timestamp of milestone creation |
| `description` | String | Description of the milestone |
| `dueDate` | Time | Timestamp of the milestone due date |
......
# frozen_string_literal: true
module EE
module Types
module MilestoneType
extend ActiveSupport::Concern
prepended do
field :burnup_time_series, [::Types::BurnupChartDailyTotalsType], null: true,
resolver: ::Resolvers::MilestoneBurnupTimeSeriesResolver,
description: 'Daily scope and completed totals for burnup charts',
complexity: 175
end
end
end
end
# frozen_string_literal: true
module Resolvers
class MilestoneBurnupTimeSeriesResolver < BaseResolver
type [Types::BurnupChartDailyTotalsType], null: true
alias_method :milestone, :synchronized_object
def resolve(*args)
return [] unless milestone.burnup_charts_available?
response = Milestones::BurnupChartService.new(milestone).execute
raise GraphQL::ExecutionError, response.message if response.error?
response.payload
end
end
end
# frozen_string_literal: true
module Types
# rubocop: disable Graphql/AuthorizeTypes
class BurnupChartDailyTotalsType < BaseObject
graphql_name 'BurnupChartDailyTotals'
description 'Represents the total number of issues and their weights for a particular day.'
field :date, GraphQL::Types::ISO8601Date, null: false,
description: 'Date for burnup totals'
field :scope_count, GraphQL::INT_TYPE, null: false,
description: 'Number of issues as of this day'
field :scope_weight, GraphQL::INT_TYPE, null: false,
description: 'Total weight of issues as of this day'
field :completed_count, GraphQL::INT_TYPE, null: false,
description: 'Number of closed issues as of this day'
field :completed_weight, GraphQL::INT_TYPE, null: false,
description: 'Total weight of closed issues as of this day'
end
end
......@@ -20,6 +20,10 @@ module EE
resource_parent&.feature_available?(feature_name) && supports_weight?
end
def supports_burnup_charts?
resource_parent&.feature_available?(:milestone_charts) && supports_weight?
end
def burnup_charts_available?
::Feature.enabled?(:burnup_charts, resource_parent)
end
......
......@@ -28,6 +28,7 @@ class License < ApplicationRecord
ldap_group_sync
member_lock
merge_request_approvers
milestone_charts
multiple_issue_assignees
multiple_ldap_servers
multiple_merge_request_assignees
......
......@@ -12,20 +12,18 @@ class Milestones::BurnupChartService
EVENT_COUNT_LIMIT = 50_000
TooManyEventsError = Class.new(StandardError)
def initialize(milestone)
raise ArgumentError, 'Milestone must have a start and due date' if milestone.start_date.blank? || milestone.due_date.blank?
@milestone = milestone
end
def execute
return ServiceResponse.error(message: _('Milestone does not support burnup charts')) unless milestone.supports_burnup_charts?
return ServiceResponse.error(message: _('Milestone must have a start and due date')) if milestone.start_date.blank? || milestone.due_date.blank?
return ServiceResponse.error(message: _('Burnup chart could not be generated due to too many events')) if resource_events.num_tuples > EVENT_COUNT_LIMIT
@issue_states = {}
@chart_data = []
raise TooManyEventsError if resource_events.num_tuples > EVENT_COUNT_LIMIT
resource_events.each do |event|
case event['event_type']
when 'milestone'
......@@ -37,7 +35,7 @@ class Milestones::BurnupChartService
end
end
chart_data
ServiceResponse.success(payload: chart_data)
end
private
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Milestone'] do
it { expect(described_class).to have_graphql_field(:burnup_time_series) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::MilestoneBurnupTimeSeriesResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let_it_be(:issues) { create_list(:issue, 2, project: project) }
before_all do
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-10')
end
before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
subject { resolve(described_class, obj: milestone) }
context 'when the feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false)
end
it 'returns empty data' do
expect(subject).to be_empty
end
end
context 'when the feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true)
end
it 'returns burnup chart data' do
expect(subject).to eq([
{
date: Date.parse('2020-01-05'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-10'),
scope_count: 2,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
context 'when the service returns an error' do
before do
stub_const('Milestones::BurnupChartService::EVENT_COUNT_LIMIT', 1)
end
it 'raises a GraphQL exception' do
expect { subject }.to raise_error(GraphQL::ExecutionError, 'Burnup chart could not be generated due to too many events')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['BurnupChartDailyTotals'] do
it { expect(described_class.graphql_name).to eq('BurnupChartDailyTotals') }
it 'has specific fields' do
expect(described_class).to have_graphql_fields(
:date,
:scope_count,
:scope_weight,
:completed_count,
:completed_weight
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Querying a Milestone' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let(:query) do
graphql_query_for('milestone', { id: milestone.to_global_id.to_s }, fields)
end
subject { graphql_data['milestone'] }
before_all do
project.add_guest(current_user)
end
context 'burnupTimeSeries' do
let(:fields) do
<<~FIELDS
burnupTimeSeries {
date
scopeCount
scopeWeight
completedCount
completedWeight
}
FIELDS
end
let_it_be(:issue) { create(:issue, project: project) }
before_all do
create(:resource_milestone_event, issue: issue, milestone: milestone, action: :add, created_at: '2020-01-05')
end
context 'when feature flag is enabled' do
before do
stub_feature_flags(burnup_charts: true)
end
context 'with insufficient license' do
before do
stub_licensed_features(milestone_charts: false)
end
it 'returns an error' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to include(a_hash_including('message' => 'Milestone does not support burnup charts'))
end
end
context 'with correct license' do
before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
it 'returns burnup chart data' do
post_graphql(query, current_user: current_user)
expect(subject).to eq({
'burnupTimeSeries' => [
{
'date' => '2020-01-05',
'scopeCount' => 1,
'scopeWeight' => 0,
'completedCount' => 0,
'completedWeight' => 0
}
]
})
end
end
end
context 'when feature flag is disabled' do
before do
stub_feature_flags(burnup_charts: false)
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
it 'returns empty results' do
post_graphql(query, current_user: current_user)
expect(subject).to eq({ 'burnupTimeSeries' => [] })
end
end
end
end
......@@ -4,249 +4,274 @@ require 'spec_helper'
RSpec.describe Milestones::BurnupChartService do
let_it_be(:project) { create(:project) }
let_it_be(:milestone) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let_it_be(:milestone, reload: true) { create(:milestone, project: project, start_date: '2020-01-01', due_date: '2020-01-15') }
let_it_be(:issues) { create_list(:issue, 5, project: project) }
let(:chart_data) { described_class.new(milestone).execute }
let(:response) { described_class.new(milestone).execute }
it 'raises an error when milestone does not have a start and due date' do
milestone = build(:milestone, project: project)
context 'when license is not available' do
before do
stub_licensed_features(milestone_charts: false)
end
expect { described_class.new(milestone) }.to raise_error('Milestone must have a start and due date')
it 'returns an error message' do
expect(response.error?).to eq(true)
expect(response.message).to eq('Milestone does not support burnup charts')
end
end
it 'raises an error when the number of events exceeds the limit' do
stub_const('Milestones::BurnupChartService::EVENT_COUNT_LIMIT', 1)
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
expect { chart_data }.to raise_error(described_class::TooManyEventsError)
end
it 'aggregates events before the start date to the start date' do
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2019-12-18')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2019-12-18')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2019-12-18')
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2019-12-25')
create(:resource_milestone_event, issue: issues[3], milestone: milestone, action: :add, created_at: '2019-12-17')
create(:resource_weight_event, issue: issues[3], weight: 4, created_at: '2019-12-18')
create(:resource_state_event, issue: issues[3], state: :closed, created_at: '2019-12-26')
expect(chart_data).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 4,
scope_weight: 10,
completed_count: 2,
completed_weight: 7
}
])
end
it 'updates counts and weight when the milestone is added or removed' do
# Add milestone to an open issue with no weight.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
# Ignore duplicate add event.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
# Add milestone to an open issue with weight 2 on the same day. This should increment the scope totals for the same day.
create(:resource_weight_event, issue: issues[1], weight: 2, created_at: '2020-01-01')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 05:00')
# Add milestone to already closed issue with weight 3. This should increment both the scope and completed totals.
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2020-01-01')
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2020-01-05')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2020-01-06')
# Remove milestone from the 2nd open issue. This should decrement the scope totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :remove, created_at: '2020-01-07')
# Remove milestone from the closed issue. This should decrement both the scope and completed totals.
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :remove, created_at: '2020-01-08')
# Adding a different milestone should not affect the data.
create(:resource_milestone_event, issue: issues[3], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-08')
# Adding the milestone after the due date should not affect the data.
create(:resource_milestone_event, issue: issues[4], milestone: milestone, action: :add, created_at: '2020-01-30')
# Removing the milestone after the due date should not affect the data.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :remove, created_at: '2020-01-30')
expect(chart_data).to eq([
{
date: Date.parse('2020-01-05'),
scope_count: 2,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-06'),
scope_count: 3,
scope_weight: 5,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-07'),
scope_count: 2,
scope_weight: 3,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-08'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
it 'updates the completed counts when issue state is changed' do
# Close an issue assigned to the milestone with weight 2. This should increment the completed totals.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01 01:00')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-01 02:00')
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-02')
# Closing an issue that is already closed should be ignored.
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-03')
# Re-opening the issue should decrement the completed totals.
create(:resource_state_event, issue: issues[0], state: :reopened, created_at: '2020-01-04')
# Closing and re-opening an issue on the same day should not change the totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 01:00')
create(:resource_weight_event, issue: issues[1], weight: 3, created_at: '2020-01-05 02:00')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-06 05:00')
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-06 08:00')
# Re-opening an issue that is already open should be ignored.
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-07')
# Closing a re-opened issue should increment the completed totals.
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-08')
# Changing state when the milestone is already removed should not affect the data.
create(:resource_milestone_event, issue: issues[1], action: :remove, created_at: '2020-01-09')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-10')
expect(chart_data).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-02'),
scope_count: 1,
scope_weight: 2,
completed_count: 1,
completed_weight: 2
},
{
date: Date.parse('2020-01-04'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-05'),
scope_count: 2,
scope_weight: 5,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-06'),
scope_count: 2,
scope_weight: 5,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-08'),
scope_count: 2,
scope_weight: 5,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-09'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
}
])
end
it 'updates the weight totals when issue weight is changed' do
# Issue starts out with no weight and should increment once the weight is changed to 2.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-02')
# A closed issue is added and weight is set to 5 and should add to the weight totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-03 01:00')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-03 02:00')
create(:resource_weight_event, issue: issues[1], weight: 5, created_at: '2020-01-03 03:00')
# Lowering the weight of the 2nd issue should decrement the weight totals.
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2020-01-04')
# After the first issue is assigned to another milestone, weight changes shouldn't affect the data.
create(:resource_milestone_event, issue: issues[0], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-05')
create(:resource_weight_event, issue: issues[0], weight: 10, created_at: '2020-01-06')
expect(chart_data).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-02'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-03'),
scope_count: 2,
scope_weight: 7,
completed_count: 1,
completed_weight: 5
},
{
date: Date.parse('2020-01-04'),
scope_count: 2,
scope_weight: 3,
completed_count: 1,
completed_weight: 1
},
{
date: Date.parse('2020-01-05'),
scope_count: 1,
scope_weight: 1,
completed_count: 1,
completed_weight: 1
}
])
context 'when license is available' do
before do
stub_licensed_features(milestone_charts: true, issue_weights: true)
end
context 'when milestone does not have a start and due date' do
let(:milestone) { build(:milestone, project: project) }
it 'returns an error message' do
expect(response.error?).to eq(true)
expect(response.message).to eq('Milestone must have a start and due date')
end
end
it 'returns an error when the number of events exceeds the limit' do
stub_const('Milestones::BurnupChartService::EVENT_COUNT_LIMIT', 1)
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
expect(response.error?).to eq(true)
expect(response.message).to eq('Burnup chart could not be generated due to too many events')
end
it 'aggregates events before the start date to the start date' do
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2019-12-15')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2019-12-18')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2019-12-18')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2019-12-16')
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2019-12-18')
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2019-12-25')
create(:resource_milestone_event, issue: issues[3], milestone: milestone, action: :add, created_at: '2019-12-17')
create(:resource_weight_event, issue: issues[3], weight: 4, created_at: '2019-12-18')
create(:resource_state_event, issue: issues[3], state: :closed, created_at: '2019-12-26')
expect(response.success?).to eq(true)
expect(response.payload).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 4,
scope_weight: 10,
completed_count: 2,
completed_weight: 7
}
])
end
it 'updates counts and weight when the milestone is added or removed' do
# Add milestone to an open issue with no weight.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
# Ignore duplicate add event.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-05 03:00')
# Add milestone to an open issue with weight 2 on the same day. This should increment the scope totals for the same day.
create(:resource_weight_event, issue: issues[1], weight: 2, created_at: '2020-01-01')
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 05:00')
# Add milestone to already closed issue with weight 3. This should increment both the scope and completed totals.
create(:resource_weight_event, issue: issues[2], weight: 3, created_at: '2020-01-01')
create(:resource_state_event, issue: issues[2], state: :closed, created_at: '2020-01-05')
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :add, created_at: '2020-01-06')
# Remove milestone from the 2nd open issue. This should decrement the scope totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :remove, created_at: '2020-01-07')
# Remove milestone from the closed issue. This should decrement both the scope and completed totals.
create(:resource_milestone_event, issue: issues[2], milestone: milestone, action: :remove, created_at: '2020-01-08')
# Adding a different milestone should not affect the data.
create(:resource_milestone_event, issue: issues[3], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-08')
# Adding the milestone after the due date should not affect the data.
create(:resource_milestone_event, issue: issues[4], milestone: milestone, action: :add, created_at: '2020-01-30')
# Removing the milestone after the due date should not affect the data.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :remove, created_at: '2020-01-30')
expect(response.success?).to eq(true)
expect(response.payload).to eq([
{
date: Date.parse('2020-01-05'),
scope_count: 2,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-06'),
scope_count: 3,
scope_weight: 5,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-07'),
scope_count: 2,
scope_weight: 3,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-08'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
}
])
end
it 'updates the completed counts when issue state is changed' do
# Close an issue assigned to the milestone with weight 2. This should increment the completed totals.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01 01:00')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-01 02:00')
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-02')
# Closing an issue that is already closed should be ignored.
create(:resource_state_event, issue: issues[0], state: :closed, created_at: '2020-01-03')
# Re-opening the issue should decrement the completed totals.
create(:resource_state_event, issue: issues[0], state: :reopened, created_at: '2020-01-04')
# Closing and re-opening an issue on the same day should not change the totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-05 01:00')
create(:resource_weight_event, issue: issues[1], weight: 3, created_at: '2020-01-05 02:00')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-06 05:00')
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-06 08:00')
# Re-opening an issue that is already open should be ignored.
create(:resource_state_event, issue: issues[1], state: :reopened, created_at: '2020-01-07')
# Closing a re-opened issue should increment the completed totals.
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-08')
# Changing state when the milestone is already removed should not affect the data.
create(:resource_milestone_event, issue: issues[1], action: :remove, created_at: '2020-01-09')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-10')
expect(response.success?).to eq(true)
expect(response.payload).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-02'),
scope_count: 1,
scope_weight: 2,
completed_count: 1,
completed_weight: 2
},
{
date: Date.parse('2020-01-04'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-05'),
scope_count: 2,
scope_weight: 5,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-06'),
scope_count: 2,
scope_weight: 5,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-08'),
scope_count: 2,
scope_weight: 5,
completed_count: 1,
completed_weight: 3
},
{
date: Date.parse('2020-01-09'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
}
])
end
it 'updates the weight totals when issue weight is changed' do
# Issue starts out with no weight and should increment once the weight is changed to 2.
create(:resource_milestone_event, issue: issues[0], milestone: milestone, action: :add, created_at: '2020-01-01')
create(:resource_weight_event, issue: issues[0], weight: 2, created_at: '2020-01-02')
# A closed issue is added and weight is set to 5 and should add to the weight totals.
create(:resource_milestone_event, issue: issues[1], milestone: milestone, action: :add, created_at: '2020-01-03 01:00')
create(:resource_state_event, issue: issues[1], state: :closed, created_at: '2020-01-03 02:00')
create(:resource_weight_event, issue: issues[1], weight: 5, created_at: '2020-01-03 03:00')
# Lowering the weight of the 2nd issue should decrement the weight totals.
create(:resource_weight_event, issue: issues[1], weight: 1, created_at: '2020-01-04')
# After the first issue is assigned to another milestone, weight changes shouldn't affect the data.
create(:resource_milestone_event, issue: issues[0], milestone: create(:milestone, project: project), action: :add, created_at: '2020-01-05')
create(:resource_weight_event, issue: issues[0], weight: 10, created_at: '2020-01-06')
expect(response.success?).to eq(true)
expect(response.payload).to eq([
{
date: Date.parse('2020-01-01'),
scope_count: 1,
scope_weight: 0,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-02'),
scope_count: 1,
scope_weight: 2,
completed_count: 0,
completed_weight: 0
},
{
date: Date.parse('2020-01-03'),
scope_count: 2,
scope_weight: 7,
completed_count: 1,
completed_weight: 5
},
{
date: Date.parse('2020-01-04'),
scope_count: 2,
scope_weight: 3,
completed_count: 1,
completed_weight: 1
},
{
date: Date.parse('2020-01-05'),
scope_count: 1,
scope_weight: 1,
completed_count: 1,
completed_weight: 1
}
])
end
end
end
......@@ -4190,6 +4190,9 @@ msgstr ""
msgid "Burnup chart"
msgstr ""
msgid "Burnup chart could not be generated due to too many events"
msgstr ""
msgid "Business"
msgstr ""
......@@ -15705,12 +15708,18 @@ msgid_plural "Milestones"
msgstr[0] ""
msgstr[1] ""
msgid "Milestone does not support burnup charts"
msgstr ""
msgid "Milestone lists not available with your current license"
msgstr ""
msgid "Milestone lists show all issues from the selected milestone."
msgstr ""
msgid "Milestone must have a start and due date"
msgstr ""
msgid "MilestoneSidebar|Closed:"
msgstr ""
......
......@@ -15,7 +15,7 @@ RSpec.describe GitlabSchema.types['Milestone'] do
stats
]
expect(described_class).to have_graphql_fields(*expected_fields)
expect(described_class).to have_graphql_fields(*expected_fields).at_least
end
describe 'stats field' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment