jirametrics 3.3.1 → 3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/lib/jirametrics/aging_work_bar_chart.rb +2 -4
  3. data/lib/jirametrics/aging_work_in_progress_chart.rb +26 -26
  4. data/lib/jirametrics/atlassian_document_format.rb +22 -14
  5. data/lib/jirametrics/board_movement_calculator.rb +0 -23
  6. data/lib/jirametrics/change_item.rb +4 -8
  7. data/lib/jirametrics/chart_base.rb +44 -15
  8. data/lib/jirametrics/chart_format.rb +15 -0
  9. data/lib/jirametrics/daily_view.rb +6 -7
  10. data/lib/jirametrics/daily_wip_by_parent_chart.rb +1 -1
  11. data/lib/jirametrics/data_quality_report.rb +3 -3
  12. data/lib/jirametrics/dependency_chart.rb +3 -5
  13. data/lib/jirametrics/estimate_accuracy_chart.rb +37 -8
  14. data/lib/jirametrics/expedited_chart.rb +5 -6
  15. data/lib/jirametrics/exporter.rb +24 -0
  16. data/lib/jirametrics/flow_efficiency_scatterplot.rb +3 -5
  17. data/lib/jirametrics/html/aging_work_in_progress_chart.erb +10 -7
  18. data/lib/jirametrics/html/estimate_accuracy_chart.erb +34 -1
  19. data/lib/jirametrics/html/index.css +10 -9
  20. data/lib/jirametrics/html/index.js +13 -0
  21. data/lib/jirametrics/html/time_based_scatterplot.erb +2 -1
  22. data/lib/jirametrics/issue.rb +5 -7
  23. data/lib/jirametrics/issue_printer.rb +5 -3
  24. data/lib/jirametrics/jira_gateway.rb +5 -7
  25. data/lib/jirametrics/sprint_burndown.rb +2 -2
  26. data/lib/jirametrics/testing/mock_board.rb +34 -0
  27. data/lib/jirametrics/testing/mock_change_item.rb +112 -0
  28. data/lib/jirametrics/testing/mock_cycle_time_config.rb +73 -0
  29. data/lib/jirametrics/testing/mock_issue.rb +108 -0
  30. data/lib/jirametrics/testing.rb +88 -0
  31. data/lib/jirametrics/time_based_histogram.rb +2 -4
  32. data/lib/jirametrics/time_based_scatterplot.rb +2 -4
  33. data/lib/jirametrics/user.rb +16 -2
  34. data/lib/jirametrics/wip_by_column_chart.rb +1 -2
  35. data/lib/jirametrics.rb +6 -1
  36. metadata +7 -1
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Support for writing tests against jirametrics from outside this repo, for anyone building their
4
+ # own charts or other extensions.
5
+ #
6
+ # Require this and you get the whole library loaded along with it, so a client test file needs
7
+ # nothing else:
8
+ #
9
+ # require 'jirametrics/testing'
10
+ #
11
+ # What is supported is exactly what this module holds: the Mock* classes below, plus the methods
12
+ # to_time, to_date and empty_config_block. The require above loads the rest of the library too,
13
+ # and that is a side effect of loading rather than a promise about any of it.
14
+ #
15
+ # The methods, and only the methods, arrive through include. Constants do not, because constant
16
+ # lookup in an example block runs through the block's lexical scope rather than the ancestors of
17
+ # the class it runs against:
18
+ #
19
+ # RSpec.configure { |config| config.include JiraMetrics::Testing }
20
+ #
21
+ # to_time '2024-01-01' # works, include supplies it
22
+ # JiraMetrics::Testing::MockIssue.empty(...) # classes are named in full
23
+ #
24
+ # Add your own alias if the full name grates. That is your namespace to spend, so we don't spend
25
+ # it for you:
26
+ #
27
+ # MockIssue = JiraMetrics::Testing::MockIssue
28
+ #
29
+ # The require of 'jirametrics' below is not optional. This file nests inside the JiraMetrics class,
30
+ # and reopening it before Thor has defined it raises a superclass mismatch.
31
+ require 'jirametrics'
32
+ require 'require_all'
33
+ require_rel '.'
34
+
35
+ class JiraMetrics
36
+ module Testing
37
+ # Accepts the date formats a test is likely to write, rather than only what Time.parse takes.
38
+ # A bare date means midnight, and a missing offset means UTC:
39
+ #
40
+ # to_time '2024-01-01' => 2024-01-01 00:00:00 +0000
41
+ # to_time '2024-01-01T12:34:56' => 2024-01-01 12:34:56 +0000
42
+ # to_time '2024-01-01T12:34:56.789' => 2024-01-01 12:34:56.789 +0000
43
+ # to_time '2024-01-01T12:34:56 +10:00' => 2024-01-01 12:34:56 +1000
44
+ TIME_PATTERN = /
45
+ ^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})
46
+ (?<remainder>T(?<hour>\d{2}):(?<minute>\d{2}):(?<second>\d{2})(?<fraction>\.\d+)?
47
+ \s*(?<offset>[+-]\d{2}:?\d{2})?)?$
48
+ /x
49
+ private_constant :TIME_PATTERN
50
+
51
+ extend self
52
+
53
+ def to_time input
54
+ return input unless input.is_a? String
55
+
56
+ matches = input.match TIME_PATTERN
57
+ raise "Can't parse string: #{input.inspect}" unless matches
58
+
59
+ Time.parse format_matched_time(matches)
60
+ end
61
+
62
+ # The companion to to_time, for the many places a test wants a Date rather than a Time. Passes a
63
+ # Date straight through for the same reason to_time passes a Time through.
64
+ def to_date input
65
+ input.is_a?(Date) ? input : Date.parse(input)
66
+ end
67
+
68
+ # Every chart is constructed with a configuration block, so a test that only wants to exercise
69
+ # the chart itself still has to supply one. This is that block, doing nothing:
70
+ #
71
+ # chart = MyCustomChart.new empty_config_block
72
+ # chart.issues = [issue]
73
+ def empty_config_block = ->(_) {}
74
+
75
+ private
76
+
77
+ # Every optional part gets a default, which is what makes this long rather than interesting.
78
+ def format_matched_time matches
79
+ format(
80
+ '%<year>04d-%<month>02d-%<day>02dT-%<hour>02d:%<minute>02d:%<second>02d%<fraction>s%<offset>s',
81
+ year: matches[:year].to_i, month: matches[:month].to_i, day: matches[:day].to_i,
82
+ hour: (matches[:hour] || 0).to_i, minute: (matches[:minute] || 0).to_i,
83
+ second: (matches[:second] || 0).to_i,
84
+ fraction: matches[:fraction] || '', offset: matches[:offset] || '+0000'
85
+ )
86
+ end
87
+ end
88
+ end
@@ -13,6 +13,7 @@ class TimeBasedHistogram < TimeBasedChart
13
13
  def initialize
14
14
  super
15
15
 
16
+ no_data_text '<%= render_header %><div>No data matched the selected criteria. Nothing to show.</div>'
16
17
  percentiles [50, 85, 98]
17
18
  @show_stats = true
18
19
  end
@@ -77,10 +78,7 @@ class TimeBasedHistogram < TimeBasedChart
77
78
  )
78
79
  end
79
80
 
80
- if data_sets.empty?
81
- return "<h1 class='foldable'>#{@header_text}</h1>" \
82
- '<div>No data matched the selected criteria. Nothing to show.</div>'
83
- end
81
+ return render_no_data if data_sets.empty?
84
82
 
85
83
  wrap_and_render(binding, __FILE__)
86
84
  end
@@ -22,6 +22,7 @@ class TimeBasedScatterplot < TimeBasedChart
22
22
  def initialize
23
23
  super
24
24
 
25
+ no_data_text '<%= render_header %><div>No data matched the selected criteria. Nothing to show.</div>'
25
26
  @percentage_lines = []
26
27
  @highest_y_value = 0
27
28
  @percentiles = [85]
@@ -56,10 +57,7 @@ class TimeBasedScatterplot < TimeBasedChart
56
57
  }
57
58
  end
58
59
 
59
- if data_sets.empty?
60
- return "<h1 class='foldable'>#{@header_text}</h1>" \
61
- '<div>No data matched the selected criteria. Nothing to show.</div>'
62
- end
60
+ return render_no_data if data_sets.empty?
63
61
 
64
62
  wrap_and_render(binding, __FILE__)
65
63
  end
@@ -1,12 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class User
4
+ RENDERED_AVATAR_SIZE = '16x16'
5
+
6
+ # Jira omits the user entirely when there isn't one, so callers get nil rather than a User that
7
+ # raises on every question.
8
+ def self.from_raw raw
9
+ raw && new(raw: raw)
10
+ end
11
+
4
12
  def initialize raw:
5
13
  @raw = raw
6
14
  end
7
15
 
8
16
  def account_id = @raw['accountId']
9
- def avatar_url = @raw['avatarUrls']['16x16']
10
17
  def active? = @raw['active']
11
- def display_name = @raw['displayName']
18
+ def email_address = @raw['emailAddress']
19
+
20
+ # Nil when there's no name to show, in either spelling. Callers want different things in that
21
+ # case, so the default belongs at the call site. 'name' is what older Jira called displayName,
22
+ # and newer versions of Cloud don't return it.
23
+ def display_name = @raw['displayName'] || @raw['name']
24
+
25
+ def avatar_url = @raw['avatarUrls']&.[](RENDERED_AVATAR_SIZE)
12
26
  end
@@ -10,7 +10,7 @@ class WipByColumnChart < ChartBase
10
10
  # Long only because of the inline description_text heredoc and one-time setup; splitting wouldn't help.
11
11
  def initialize block # rubocop:disable Metrics/MethodLength
12
12
  super()
13
- header_text 'WIP by column'
13
+ header_text 'WIP by column on board: <%= current_board.name %>'
14
14
  description_text <<-HTML
15
15
  <p>
16
16
  This chart shows how much time each board column has spent at different WIP (Work in Progress) levels.
@@ -58,7 +58,6 @@ class WipByColumnChart < ChartBase
58
58
  end
59
59
 
60
60
  def run
61
- @header_text += " on board: #{current_board.name}"
62
61
  stats = column_stats
63
62
  @column_names = stats.collect(&:name)
64
63
  @wip_data = stats.collect do |stat|
data/lib/jirametrics.rb CHANGED
@@ -157,7 +157,12 @@ class JiraMetrics < Thor
157
157
  exit 1
158
158
  end
159
159
 
160
- require_rel 'jirametrics'
160
+ # The testing surface has no place in a production run, and the examples are opt-in: a config
161
+ # that wants standard_project requires it by name. Matching on the prefix also excludes
162
+ # testing.rb, which has to go with its directory because it require_rel's the whole tree.
163
+ not_autoloaded = %w[testing examples].collect { |name| File.join __dir__, 'jirametrics', name }
164
+ require_all(Dir[File.join(__dir__, 'jirametrics', '**', '*.rb')]
165
+ .reject { |file| not_autoloaded.any? { |prefix| file.start_with? prefix } })
161
166
  # Set only after the require above, so Exporter is defined. The config file we load below calls
162
167
  # Exporter.configure, which opens this log; the MCP server passes its own name so it doesn't
163
168
  # truncate jirametrics.log.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jirametrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: '3.4'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Bowler
@@ -122,6 +122,7 @@ files:
122
122
  - lib/jirametrics/cfd_data_builder.rb
123
123
  - lib/jirametrics/change_item.rb
124
124
  - lib/jirametrics/chart_base.rb
125
+ - lib/jirametrics/chart_format.rb
125
126
  - lib/jirametrics/color_palette.rb
126
127
  - lib/jirametrics/columns_config.rb
127
128
  - lib/jirametrics/css_variable.rb
@@ -201,6 +202,11 @@ files:
201
202
  - lib/jirametrics/status.rb
202
203
  - lib/jirametrics/status_collection.rb
203
204
  - lib/jirametrics/stitcher.rb
205
+ - lib/jirametrics/testing.rb
206
+ - lib/jirametrics/testing/mock_board.rb
207
+ - lib/jirametrics/testing/mock_change_item.rb
208
+ - lib/jirametrics/testing/mock_cycle_time_config.rb
209
+ - lib/jirametrics/testing/mock_issue.rb
204
210
  - lib/jirametrics/throughput_by_completed_resolution_chart.rb
205
211
  - lib/jirametrics/throughput_chart.rb
206
212
  - lib/jirametrics/time_based_chart.rb