jirametrics 2.3 → 2.4pre2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e7264aee468cd14d42fa1c7ba8f737177ba99a78b3f7250d46771f687d4d619
4
- data.tar.gz: da813f37b2a6047d6ab844f483008ecdb8c579786bd7bba7f2c01f038a259949
3
+ metadata.gz: dd626d730d591c7cc7ec2a9245fa395bc1d8d7fc22d658b6663dce28aa40f351
4
+ data.tar.gz: b0437b25f7def75b2bd2f0fd889aead1104b65ec53e339f48b8f2e535dc84d96
5
5
  SHA512:
6
- metadata.gz: c234046627a2c92729c346f44cf78471e74545e01ce3a9810c6c22ed659f6479a0146303ac7fb0652512feafcf8077025577d2a4c039dddf2fd70d6899360adb
7
- data.tar.gz: 81f24c30b8d56f8811ed298ca17cebcee6cb32afdbea5cfd12332587aabaa2701b023dc620b651e111226bf66bac9285f84a893549c00c45bd0eb992694a2c64
6
+ metadata.gz: 3c8c7f1ce346a98f989641fe4cb962d4f3fd04eab690b861e296154564f2d145cfa705fbf8d590ec004a4b02f707799fb2c21eef5cddc7e4e00bcc090730bca7
7
+ data.tar.gz: 596c0b5559ae4a64d46e95f38fb13e0fdec5bc8c315195ea9a45d69abd26841d4d4aa28cee1fe46b769b596810129aaf79e9fb1052ec3827aaa89c092a5687e4
@@ -15,12 +15,12 @@ class BlockedStalledChange
15
15
  @time = time
16
16
  end
17
17
 
18
- def blocked? = @flag || blocked_by_status? || @blocking_issue_keys
19
- def stalled? = @stalled_days || stalled_by_status?
18
+ def blocked? = !!(@flag || blocked_by_status? || @blocking_issue_keys)
19
+ def stalled? = !!(@stalled_days || stalled_by_status?)
20
20
  def active? = !blocked? && !stalled?
21
21
 
22
- def blocked_by_status? = @status && @status_is_blocking
23
- def stalled_by_status? = @status && !@status_is_blocking
22
+ def blocked_by_status? = !!(@status && @status_is_blocking)
23
+ def stalled_by_status? = !!(@status && !@status_is_blocking)
24
24
 
25
25
  def reasons
26
26
  result = []
@@ -35,4 +35,24 @@ class BlockedStalledChange
35
35
  end
36
36
  result.join(', ')
37
37
  end
38
+
39
+ def as_symbol
40
+ if blocked?
41
+ :blocked
42
+ elsif stalled?
43
+ :stalled
44
+ else
45
+ :active
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ text = +"BlockedStalledChange(time: '#{@time}', "
51
+ if active?
52
+ text << 'Active'
53
+ else
54
+ text << reasons
55
+ end
56
+ text << ')'
57
+ end
38
58
  end
@@ -5,7 +5,6 @@ class ChangeItem
5
5
  attr_accessor :value, :old_value, :time
6
6
 
7
7
  def initialize raw:, time:, author:, artificial: false
8
- # raw will only ever be nil in a test and in that case field and value should be passed in
9
8
  @raw = raw
10
9
  @time = time
11
10
  raise "Time must be an object of type Time in the correct timezone: #{@time}" if @time.is_a? String
@@ -36,16 +35,17 @@ class ChangeItem
36
35
  def link? = (field == 'Link')
37
36
 
38
37
  def to_s
39
- message = "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: \"#{@time}\""
40
- message += ', artificial' if artificial?
41
- message += ')'
38
+ message = +''
39
+ message << "ChangeItem(field: #{field.inspect}, value: #{value.inspect}, time: #{time_to_s(@time).inspect}"
40
+ message << ', artificial' if artificial?
41
+ message << ')'
42
42
  message
43
43
  end
44
44
 
45
45
  def inspect = to_s
46
46
 
47
47
  def == other
48
- field.eql?(other.field) && value.eql?(other.value) && time.to_s.eql?(other.time.to_s)
48
+ field.eql?(other.field) && value.eql?(other.value) && time_to_s(time).eql?(time_to_s(other.time))
49
49
  end
50
50
 
51
51
  def current_status_matches *status_names_or_ids
@@ -77,4 +77,12 @@ class ChangeItem
77
77
  end
78
78
  end
79
79
  end
80
+
81
+ private
82
+
83
+ def time_to_s time
84
+ # MRI and JRuby return different strings for to_s() so we have to explicitly provide a full
85
+ # format so that tests work under both environments.
86
+ time.strftime '%Y-%m-%d %H:%M:%S %z'
87
+ end
80
88
  end
@@ -34,6 +34,10 @@ class ColumnsConfig
34
34
  @columns << [:string, label, proc]
35
35
  end
36
36
 
37
+ def integer label, proc
38
+ @columns << [:integer, label, proc]
39
+ end
40
+
37
41
  def column_entry_times board_id: nil
38
42
  @file_config.project_config.find_board_by_id(board_id).visible_columns.each do |column|
39
43
  date column.name, first_time_in_status(*column.status_ids)
@@ -38,25 +38,12 @@ class DailyWipByBlockedStalledChart < DailyWipChart
38
38
  HTML
39
39
  end
40
40
 
41
- def key_blocked_stalled_change issue:, date:, end_time:
42
- stalled_change = nil
43
- blocked_change = nil
44
-
45
- issue.blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
46
- blocked_change = change if change.blocked?
47
- stalled_change = change if change.stalled?
48
- end
49
-
50
- return blocked_change if blocked_change
51
- return stalled_change if stalled_change
52
-
53
- nil
54
- end
55
-
56
41
  def default_grouping_rules issue:, rules:
57
42
  started = issue.board.cycletime.started_time(issue)
58
43
  stopped_date = issue.board.cycletime.stopped_time(issue)&.to_date
59
- change = key_blocked_stalled_change issue: issue, date: rules.current_date, end_time: time_range.end
44
+
45
+ date = rules.current_date
46
+ change = issue.blocked_stalled_by_date(date_range: date..date, chart_end_time: time_range.end)[date]
60
47
 
61
48
  stopped_today = stopped_date == rules.current_date
62
49
 
@@ -71,7 +71,10 @@ class DataQualityReport < ChartBase
71
71
 
72
72
  # Return a format that's easier to assert against
73
73
  def testable_entries
74
- @entries.collect { |entry| [entry.started.to_s, entry.stopped.to_s, entry.issue] }
74
+ format = '%Y-%m-%d %H:%M:%S %z'
75
+ @entries.collect do |entry|
76
+ [entry.started&.strftime(format) || '', entry.stopped&.strftime(format) || '', entry.issue]
77
+ end
75
78
  end
76
79
 
77
80
  def entries_with_problems
@@ -5,7 +5,7 @@ require 'fileutils'
5
5
  class Object
6
6
  def deprecated message:, date:
7
7
  text = +''
8
- text << "Deprecated(#{date}):"
8
+ text << "Deprecated(#{date}): "
9
9
  text << message
10
10
  text << "\n-> Called from #{caller(1..1).first}"
11
11
  warn text
@@ -13,7 +13,8 @@ class Object
13
13
  end
14
14
 
15
15
  class Exporter
16
- attr_reader :project_configs, :file_system
16
+ attr_reader :project_configs
17
+ attr_accessor :file_system
17
18
 
18
19
  def self.configure &block
19
20
  logfile_name = 'jirametrics.log'
@@ -99,7 +100,10 @@ class Exporter
99
100
  end
100
101
 
101
102
  def jira_config filename = nil
102
- @jira_config = file_system.load_json(filename) unless filename.nil?
103
+ if filename
104
+ @jira_config = file_system.load_json(filename)
105
+ @jira_config['url'] = $1 if @jira_config['url'] =~ /^(.+)\/+$/
106
+ end
103
107
  @jira_config
104
108
  end
105
109
 
@@ -66,7 +66,9 @@ class FileConfig
66
66
  # is that all empty values in the first column should be at the bottom.
67
67
  def sort_output all_lines
68
68
  all_lines.sort do |a, b|
69
- if a[0].nil?
69
+ if a[0] == b[0]
70
+ a[1..] <=> b[1..]
71
+ elsif a[0].nil?
70
72
  1
71
73
  elsif b[0].nil?
72
74
  -1
@@ -110,6 +112,10 @@ class FileConfig
110
112
  object.to_s
111
113
  end
112
114
 
115
+ def to_integer object
116
+ object.to_i
117
+ end
118
+
113
119
  def file_suffix suffix = nil
114
120
  @file_suffix = suffix unless suffix.nil?
115
121
  @file_suffix
@@ -1,6 +1,7 @@
1
1
  <html>
2
2
  <head>
3
3
  <meta charset="UTF-8">
4
+ <link rel="icon" type="image/png" href="https://github.com/mikebowler/jirametrics/blob/main/favicon.png?raw=true" />
4
5
  <script src="https://cdn.jsdelivr.net/npm/moment@2.29.1/moment.js"></script>
5
6
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
6
7
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-moment@^1"></script>
@@ -186,29 +186,62 @@ class Issue
186
186
  end
187
187
 
188
188
  def blocked_on_date? date, end_time:
189
- blocked_stalled_changes_on_date(date: date, end_time: end_time) do |change|
190
- return true if change.blocked?
191
- end
192
- false
193
- end
189
+ (blocked_stalled_by_date date_range: date..date, chart_end_time: end_time)[date].blocked?
190
+ end
191
+
192
+ # For any day in the day range...
193
+ # If the issue was blocked at any point in this day, the whole day is blocked.
194
+ # If the issue was active at any point in this day, the whole day is active
195
+ # If the day was stalled for the entire day then it's stalled
196
+ # If there was no activity at all on this day then the last change from the previous day carries over
197
+ def blocked_stalled_by_date date_range:, chart_end_time:, settings: nil
198
+ results = {}
199
+ current_date = nil
200
+ blocked_stalled_changes = blocked_stalled_changes(end_time: chart_end_time, settings: settings)
201
+ blocked_stalled_changes.each do |change|
202
+ current_date = change.time.to_date
203
+
204
+ winning_change, _last_change = results[current_date]
205
+ if winning_change.nil? ||
206
+ change.blocked? ||
207
+ (change.active? && (winning_change.active? || winning_change.stalled?)) ||
208
+ (change.stalled? && winning_change.stalled?)
209
+
210
+ winning_change = change
211
+ end
194
212
 
195
- def blocked_stalled_changes_on_date date:, end_time:
196
- next_day_start_time = (date + 1).to_time
197
- this_day_start_time = date.to_time
213
+ results[current_date] = [winning_change, change]
214
+ end
198
215
 
199
- # changes_affecting_date = []
200
- previous_change_time = nil
201
- blocked_stalled_changes(end_time: end_time).each do |change|
202
- if previous_change_time.nil?
203
- previous_change_time = change.time
204
- next
216
+ last_populated_date = nil
217
+ (results.keys.min..results.keys.max).each do |date|
218
+ if results.key? date
219
+ last_populated_date = date
220
+ else
221
+ _winner, last = results[last_populated_date]
222
+ results[date] = [last, last]
205
223
  end
206
-
207
- yield change if previous_change_time < next_day_start_time && change.time >= this_day_start_time
208
224
  end
225
+ results = results.transform_values(&:first)
226
+
227
+ # The requested date range may span outside the actual changes we find in the changelog
228
+ date_of_first_change = blocked_stalled_changes[0].time.to_date
229
+ date_of_last_change = blocked_stalled_changes[-1].time.to_date
230
+ date_range.each do |date|
231
+ results[date] = blocked_stalled_changes[0] if date < date_of_first_change
232
+ results[date] = blocked_stalled_changes[-1] if date > date_of_last_change
233
+ end
234
+
235
+ # To make the code simpler, we've been accumulating data for every date. Now remove anything
236
+ # that isn't in the requested date_range
237
+ results.select! { |date, _value| date_range.include? date }
238
+
239
+ results
209
240
  end
210
241
 
211
- def blocked_stalled_changes end_time:, settings: @board.project_config.settings
242
+ def blocked_stalled_changes end_time:, settings: nil
243
+ settings ||= @board.project_config.settings
244
+
212
245
  blocked_statuses = settings['blocked_statuses']
213
246
  stalled_statuses = settings['stalled_statuses']
214
247
  unless blocked_statuses.is_a?(Array) && stalled_statuses.is_a?(Array)
@@ -222,7 +255,7 @@ class Issue
222
255
  blocking_issue_keys = []
223
256
 
224
257
  result = []
225
- previous_was_active = true
258
+ previous_was_active = false # Must start as false so that the creation will insert an :active
226
259
  previous_change_time = created
227
260
 
228
261
  blocking_status = nil
@@ -231,6 +264,7 @@ class Issue
231
264
  # This mock change is to force the writing of one last entry at the end of the time range.
232
265
  # By doing this, we're able to eliminate a lot of duplicated code in charts.
233
266
  mock_change = ChangeItem.new time: end_time, author: '', artificial: true, raw: { 'field' => '' }
267
+
234
268
  (changes + [mock_change]).each do |change|
235
269
  previous_was_active = false if check_for_stalled(
236
270
  change_time: change.time,
@@ -292,6 +326,7 @@ class Issue
292
326
  stalled_days: result[-1].stalled_days
293
327
  )
294
328
  end
329
+
295
330
  result
296
331
  end
297
332
 
@@ -301,6 +336,9 @@ class Issue
301
336
  # The most common case will be nothing to split so quick escape.
302
337
  return false if (change_time - previous_change_time).to_i < stalled_threshold_seconds
303
338
 
339
+ # If the last identified change was blocked then it doesn't matter now long we've waited, we're still blocked.
340
+ return false if blocking_stalled_changes[-1]&.blocked?
341
+
304
342
  list = [previous_change_time..change_time]
305
343
  all_subtask_activity_times.each do |time|
306
344
  matching_range = list.find { |range| time >= range.begin && time <= range.end }
@@ -457,7 +495,7 @@ class Issue
457
495
  value = change.value
458
496
  old_value = change.old_value
459
497
 
460
- message = " [change] #{change.time} [#{change.field}] "
498
+ message = " [change] #{change.time.strftime '%Y-%m-%d %H:%M:%S %z'} [#{change.field}] "
461
499
  message << "#{compact_text(old_value).inspect} -> " unless old_value.nil? || old_value.empty?
462
500
  message << compact_text(value).inspect
463
501
  message << " (#{change.author})"
@@ -511,7 +549,10 @@ class Issue
511
549
  # It's common that a resolved will happen at the same time as a status change.
512
550
  # Put them in a defined order so tests can be deterministic.
513
551
  compare = a.time <=> b.time
514
- compare = 1 if compare.zero? && a.resolution?
552
+ if compare.zero?
553
+ compare = 1 if a.resolution?
554
+ compare = -1 if b.resolution?
555
+ end
515
556
  compare
516
557
  end
517
558
  end
@@ -48,15 +48,17 @@ class JiraGateway
48
48
  end
49
49
 
50
50
  def make_curl_command url:
51
- command = 'curl'
52
- command += ' -s'
53
- command += ' -k' if @ignore_ssl_errors
54
- command += " --cookie #{@cookies.inspect}" unless @cookies.empty?
55
- command += " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
56
- command += " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
57
- command += ' --request GET'
58
- command += ' --header "Accept: application/json"'
59
- command += " --url \"#{url}\""
51
+ command = +''
52
+ command << 'curl'
53
+ command << ' -L' # follow redirects
54
+ command << ' -s' # silent
55
+ command << ' -k' if @ignore_ssl_errors # insecure
56
+ command << " --cookie #{@cookies.inspect}" unless @cookies.empty?
57
+ command << " --user #{@jira_email}:#{@jira_api_token}" if @jira_api_token
58
+ command << " -H \"Authorization: Bearer #{@jira_personal_access_token}\"" if @jira_personal_access_token
59
+ command << ' --request GET'
60
+ command << ' --header "Accept: application/json"'
61
+ command << " --url \"#{url}\""
60
62
  command
61
63
  end
62
64
  end
@@ -249,7 +249,7 @@ class ProjectConfig
249
249
 
250
250
  def to_time string, end_of_day: false
251
251
  time = end_of_day ? '23:59:59' : '00:00:00'
252
- string = "#{string}T#{time}#{@timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
252
+ string = "#{string}T#{time}#{exporter.timezone_offset}" if string.match?(/^\d{4}-\d{2}-\d{2}$/)
253
253
  Time.parse string
254
254
  end
255
255
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.3'
4
+ version: 2.4pre2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-06-03 00:00:00.000000000 Z
11
+ date: 2024-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: random-word
@@ -153,7 +153,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
153
153
  - !ruby/object:Gem::Version
154
154
  version: '0'
155
155
  requirements: []
156
- rubygems_version: 3.5.11
156
+ rubygems_version: 3.5.15
157
157
  signing_key:
158
158
  specification_version: 4
159
159
  summary: Extract Jira metrics