rspec_telemetry 0.3.0 → 0.5.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a39ae80facd19b25cfcbb59d34bde9ad51d47cb815d1ea5bfec9b3a344cead99
4
- data.tar.gz: 031a509323010facb690d5cedbc53eb3e318087cbb69439832426b24673efa98
3
+ metadata.gz: 796f640dcbe14855620f29d78972456705d29af7025b3e01171e9f4d76dfca04
4
+ data.tar.gz: 271548c28826a191510187593eaef599771f964c16b3061c7e6d6751e4dbd7fd
5
5
  SHA512:
6
- metadata.gz: 8158f6494ea9a691a3c52c236320cfffd6b32986258ab578a87dc708940c800a5f7fde9342187240fb254158d24bed74edf7d39963856c39488ce468f78f898a
7
- data.tar.gz: 6d962956bde3f0a320aec11562d584380da45c9c6372a589c1e12cfc5d6361965327624cdfbd6eea08cc6333e7688530095cb91964cce9c8204e2ff2dd3ed611
6
+ metadata.gz: 960b867f5af3ed3a290d637422dee0436ec10b72b7554a9bb348329cd7ba561dbeaca388148cb41f39c1e616708f570d8c2466855f6321cbd1f9bfbf4861e464
7
+ data.tar.gz: 1ddef07a9486a993f55073db7a717e185dba269cacc4f379dc43e117b42b35edebb09a07d81a2be9d652a07eb2208fced3e13faff4f3e70a82a78a71780c577b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.5.0] - 2026-07-16
6
+
7
+ ### Added
8
+ - `RSPEC_TELEMETRY_OUTPUT` environment variable to redirect the NDJSON output
9
+ path per run (e.g. to keep before/after measurements side by side) without
10
+ editing `spec_helper.rb`. It takes precedence over the `TEST_ENV_NUMBER`
11
+ worker suffix.
12
+
13
+ ### Changed
14
+ - Require `tui_tui ~> 0.4`. The interactive viewer now uses the namespaced
15
+ `TuiTui::Widget` / `TuiTui::Modal` widgets and requests a full repaint via
16
+ `TuiTui::Command::Invalidate` instead of the removed `redraw?` hook.
17
+
18
+ ## [0.4.0] - 2026-06-30
19
+
20
+ ### Added
21
+ - `rspec-telemetry-compare --by-factory` to combine strategies (create/build)
22
+ and compare by factory name only. Works together with `--all-depths`.
23
+ - `rspec-telemetry-compare` now prints a `TOTAL` row summing all factories.
24
+
25
+ ### Changed
26
+ - `rspec-telemetry-compare` now reuses `FactoryAggregation` and keys rows by
27
+ `factory:strategy`, so create and build of the same factory are compared
28
+ separately.
29
+
3
30
  ## [0.3.0] - 2026-06-25
4
31
 
5
32
  ### Added
@@ -30,6 +57,9 @@
30
57
 
31
58
  First public release.
32
59
 
33
- [Unreleased]: https://github.com/takahashim/rspec_telemetry/compare/v0.2.0...HEAD
60
+ [Unreleased]: https://github.com/takahashim/rspec_telemetry/compare/v0.5.0...HEAD
61
+ [0.5.0]: https://github.com/takahashim/rspec_telemetry/compare/v0.4.0...v0.5.0
62
+ [0.4.0]: https://github.com/takahashim/rspec_telemetry/compare/v0.3.0...v0.4.0
63
+ [0.3.0]: https://github.com/takahashim/rspec_telemetry/compare/v0.2.0...v0.3.0
34
64
  [0.2.0]: https://github.com/takahashim/rspec_telemetry/compare/v0.1.0...v0.2.0
35
65
  [0.1.0]: https://github.com/takahashim/rspec_telemetry/releases/tag/v0.1.0
data/README.md CHANGED
@@ -52,6 +52,15 @@ If you want to specify the formatter explicitly in `.rspec`:
52
52
  > To disable automatic formatter registration, set the environment variable
53
53
  > `RSPEC_TELEMETRY_NO_AUTOLOAD=1`.
54
54
 
55
+ ### Redirecting the output file
56
+
57
+ Each run overwrites `tmp/rspec_telemetry.ndjson`. Set `RSPEC_TELEMETRY_OUTPUT`
58
+ to redirect a single run elsewhere (e.g. to keep before/after results):
59
+
60
+ ```bash
61
+ RSPEC_TELEMETRY_OUTPUT=tmp/rt-before.ndjson bundle exec rspec
62
+ ```
63
+
55
64
  ## Configuration
56
65
 
57
66
  ```ruby
@@ -120,6 +129,10 @@ $ bundle exec rspec-telemetry-compare \
120
129
  tmp/rspec_telemetry.after.ndjson
121
130
  ```
122
131
 
132
+ Factories are grouped by `factory:strategy`, so `user:create` and `user:build`
133
+ are compared separately. Use `--by-factory` to combine strategies and compare by
134
+ factory name only (e.g. one `user` row).
135
+
123
136
  By default, only root factory events (`depth == 0`) are counted, and their
124
137
  inclusive `duration_ms` is compared.
125
138
 
@@ -131,6 +144,8 @@ counting while showing the actual number and cost of associated factories.
131
144
  rspec-telemetry-compare --sort count BEFORE AFTER
132
145
  rspec-telemetry-compare --sort factory BEFORE AFTER
133
146
  rspec-telemetry-compare --all-depths BEFORE AFTER
147
+ rspec-telemetry-compare --by-factory BEFORE AFTER # combine create/build
148
+ rspec-telemetry-compare --all-depths --by-factory BEFORE AFTER
134
149
  ```
135
150
 
136
151
  ## TUI viewer: `rspec-telemetry-viewer`
@@ -3,6 +3,7 @@
3
3
  require "optparse"
4
4
 
5
5
  require_relative "factory_comparison"
6
+ require_relative "formatting"
6
7
 
7
8
  module RSpecTelemetry
8
9
  class CompareCLI
@@ -10,7 +11,7 @@ module RSpecTelemetry
10
11
  @argv = argv
11
12
  @out = out
12
13
  @err = err
13
- @options = {all_depths: false, sort: "duration"}
14
+ @options = {all_depths: false, by_factory: false, sort: "duration"}
14
15
  end
15
16
 
16
17
  def run
@@ -20,9 +21,13 @@ module RSpecTelemetry
20
21
  return 1
21
22
  end
22
23
 
23
- comparison = FactoryComparison.new(paths[0], paths[1], all_depths: @options[:all_depths])
24
+ comparison = FactoryComparison.new(
25
+ paths[0], paths[1],
26
+ all_depths: @options[:all_depths],
27
+ by_factory: @options[:by_factory]
28
+ )
24
29
  rows = sort_rows(comparison.rows)
25
- @out.puts(render(rows, duration_label: comparison.all_depths ? "Self(ms)" : "Total(ms)"))
30
+ @out.puts(render(rows, duration_label: comparison.duration_label, label_heading: label_heading))
26
31
  0
27
32
  rescue Errno::ENOENT => e
28
33
  @err.puts("File not found: #{e.message}")
@@ -40,6 +45,9 @@ module RSpecTelemetry
40
45
  options.on("--all-depths", "Include nested FactoryBot events") do
41
46
  @options[:all_depths] = true
42
47
  end
48
+ options.on("--by-factory", "Combine strategies (create/build) per factory") do
49
+ @options[:by_factory] = true
50
+ end
43
51
  options.on(
44
52
  "--sort KEY",
45
53
  %w[duration count factory],
@@ -59,17 +67,21 @@ module RSpecTelemetry
59
67
  def sort_rows(rows)
60
68
  case @options[:sort]
61
69
  when "count"
62
- rows.sort_by { |row| [-row.count_diff.abs, row.factory] }
70
+ rows.sort_by { |row| [-row.count_diff.abs, row.label] }
63
71
  when "factory"
64
- rows.sort_by(&:factory)
72
+ rows.sort_by(&:label)
65
73
  else
66
- rows.sort_by { |row| [-row.duration_diff_ms.abs, row.factory] }
74
+ rows.sort_by { |row| [-row.duration_diff_ms.abs, row.label] }
67
75
  end
68
76
  end
69
77
 
70
- def render(rows, duration_label:)
78
+ def label_heading
79
+ @options[:by_factory] ? "Factory" : "Factory:Strategy"
80
+ end
81
+
82
+ def render(rows, duration_label:, label_heading:)
71
83
  headings = [
72
- "Factory",
84
+ label_heading,
73
85
  "Before",
74
86
  "After",
75
87
  "Diff",
@@ -79,51 +91,61 @@ module RSpecTelemetry
79
91
  "Diff(ms)",
80
92
  "Change"
81
93
  ]
82
- body = rows.map do |row|
83
- [
84
- row.factory,
85
- row.before_count.to_s,
86
- row.after_count.to_s,
87
- signed_integer(row.count_diff),
88
- percent(row.count_change_percent),
89
- decimal(row.before_duration_ms),
90
- decimal(row.after_duration_ms),
91
- signed_decimal(row.duration_diff_ms),
92
- percent(row.duration_change_percent)
93
- ]
94
- end
94
+ body = rows.map { |row| columns_for(row) }
95
+ total = columns_for(totals_row(rows))
95
96
 
96
97
  widths = headings.each_index.map do |index|
97
- ([headings[index]] + body.map { |columns| columns[index] }).map(&:length).max
98
+ ([headings[index]] + (body + [total]).map { |columns| columns[index] }).map(&:length).max
98
99
  end
99
100
 
100
101
  lines = []
101
102
  lines << format_row(headings, widths)
102
- lines << widths.map { |width| "-" * width }.join("-+-")
103
+ lines << separator(widths)
103
104
  body.each { |columns| lines << format_row(columns, widths) }
105
+ lines << separator(widths)
106
+ lines << format_row(total, widths)
104
107
  lines.join("\n")
105
108
  end
106
109
 
107
- def format_row(columns, widths)
108
- columns.each_with_index.map do |value, index|
109
- index.zero? ? value.ljust(widths[index]) : value.rjust(widths[index])
110
- end.join(" | ")
110
+ def columns_for(row)
111
+ [
112
+ row.label,
113
+ row.before_count.to_s,
114
+ row.after_count.to_s,
115
+ Formatting.signed_integer(row.count_diff),
116
+ percent(row.count_change_percent),
117
+ Formatting.fixed(row.before_duration_ms),
118
+ Formatting.fixed(row.after_duration_ms),
119
+ Formatting.signed_fixed(row.duration_diff_ms),
120
+ percent(row.duration_change_percent)
121
+ ]
111
122
  end
112
123
 
113
- def signed_integer(value)
114
- format("%+d", value)
124
+ # Reuse Row so the total's diff/percent math matches every other line.
125
+ def totals_row(rows)
126
+ FactoryComparison::Row.new(
127
+ label: "TOTAL",
128
+ before_count: rows.sum(&:before_count),
129
+ after_count: rows.sum(&:after_count),
130
+ before_duration_ms: rows.sum(&:before_duration_ms),
131
+ after_duration_ms: rows.sum(&:after_duration_ms)
132
+ )
115
133
  end
116
134
 
117
- def decimal(value)
118
- format("%.1f", value)
135
+ def separator(widths)
136
+ widths.map { |width| "-" * width }.join("-+-")
119
137
  end
120
138
 
121
- def signed_decimal(value)
122
- format("%+.1f", value)
139
+ def format_row(columns, widths)
140
+ columns.each_with_index.map do |value, index|
141
+ index.zero? ? value.ljust(widths[index]) : value.rjust(widths[index])
142
+ end.join(" | ")
123
143
  end
124
144
 
145
+ # "-" marks a missing baseline (before count/duration was zero), which is a
146
+ # table rule rather than number formatting, so it stays here.
125
147
  def percent(value)
126
- value ? format("%+.1f%%", value) : "-"
148
+ value ? Formatting.signed_percent(value) : "-"
127
149
  end
128
150
  end
129
151
  end
@@ -29,6 +29,11 @@ module RSpecTelemetry
29
29
  end
30
30
 
31
31
  def self.default_output_path
32
+ # An explicit path wins so a single run can be redirected without editing
33
+ # spec_helper (e.g. to keep before/after measurements side by side).
34
+ explicit = ENV["RSPEC_TELEMETRY_OUTPUT"]
35
+ return explicit if explicit && !explicit.empty?
36
+
32
37
  suffix = ENV["TEST_ENV_NUMBER"]
33
38
  if suffix && !suffix.empty?
34
39
  "tmp/rspec_telemetry.#{suffix}.ndjson"
@@ -1,16 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "formatting"
4
+
3
5
  module RSpecTelemetry
4
6
  class ConsoleReport
5
7
  module Helpers
6
8
  module_function
7
9
 
8
- def fmt(ms)
9
- ms = ms.to_f
10
- ms >= 1000 ? format("%.2fs", ms / 1000.0) : format("%.1fms", ms)
11
- end
10
+ def fmt(ms) = Formatting.duration(ms)
12
11
 
13
- def pct(ratio) = format("%.1f%%", ratio * 100)
12
+ def pct(ratio) = Formatting.percent(ratio * 100)
14
13
 
15
14
  def truncate(str, len) = str.length > len ? "#{str[0, len - 1]}…" : str
16
15
 
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "ndjson"
4
+ require_relative "factory_aggregation"
4
5
 
5
6
  module RSpecTelemetry
6
7
  class FactoryComparison
7
- FactoryStat = Struct.new(:count, :duration_ms)
8
8
  Row = Struct.new(
9
- :factory,
9
+ :label,
10
10
  :before_count,
11
11
  :after_count,
12
12
  :before_duration_ms,
@@ -38,47 +38,75 @@ module RSpecTelemetry
38
38
  end
39
39
  end
40
40
 
41
- attr_reader :before_path, :after_path, :all_depths
41
+ attr_reader :before_path, :after_path, :all_depths, :by_factory
42
42
 
43
- def initialize(before_path, after_path, all_depths: false)
43
+ def initialize(before_path, after_path, all_depths: false, by_factory: false)
44
44
  @before_path = before_path
45
45
  @after_path = after_path
46
46
  @all_depths = all_depths
47
+ @by_factory = by_factory
48
+ end
49
+
50
+ def duration_label
51
+ all_depths ? "Self(ms)" : "Total(ms)"
47
52
  end
48
53
 
49
54
  def rows
50
55
  before = aggregate(before_path)
51
56
  after = aggregate(after_path)
52
57
 
53
- (before.keys | after.keys).sort.map do |factory|
54
- before_stat = before.fetch(factory, empty_stat)
55
- after_stat = after.fetch(factory, empty_stat)
58
+ (before.keys | after.keys).sort.map do |key|
59
+ before_stat = before[key]
60
+ after_stat = after[key]
56
61
 
57
62
  Row.new(
58
- factory: factory,
59
- before_count: before_stat.count,
60
- after_count: after_stat.count,
61
- before_duration_ms: before_stat.duration_ms,
62
- after_duration_ms: after_stat.duration_ms
63
+ label: key,
64
+ before_count: before_stat&.count || 0,
65
+ after_count: after_stat&.count || 0,
66
+ before_duration_ms: duration_for(before_stat),
67
+ after_duration_ms: duration_for(after_stat)
63
68
  )
64
69
  end
65
70
  end
66
71
 
67
72
  private
68
73
 
74
+ # Reuse the shared accumulator so counts, durations, and the factory:strategy
75
+ # granularity stay identical to the live summary, CLI report, and viewer.
76
+ # create and build are kept as separate keys (e.g. "user:create") unless
77
+ # by_factory rolls every strategy up under the bare factory name.
69
78
  def aggregate(path)
70
- stats = Hash.new { |hash, factory| hash[factory] = FactoryStat.new(0, 0.0) }
79
+ acc = FactoryAggregation::Accumulator.new
71
80
 
72
81
  File.foreach(path) do |line|
73
82
  event = Ndjson.parse(line)
74
83
  next unless factory_event?(event)
75
84
 
76
- stat = stats[event["factory"].to_s]
77
- stat.count += 1
78
- stat.duration_ms += duration_ms(event)
85
+ acc.add(
86
+ factory: event["factory"],
87
+ strategy: event["strategy"],
88
+ duration_ms: event["duration_ms"],
89
+ self_duration_ms: event["self_duration_ms"]
90
+ )
79
91
  end
80
92
 
81
- stats
93
+ stats = by_factory ? merge_by_factory(acc.stats) : acc.stats
94
+ stats.to_h { |stat| [stat.key, stat] }
95
+ end
96
+
97
+ # Collapse create/build/etc. into a single row keyed by the factory name.
98
+ def merge_by_factory(stats)
99
+ stats.group_by(&:factory).map do |factory, group|
100
+ FactoryAggregation::Stat.new(
101
+ key: factory,
102
+ factory: factory,
103
+ strategy: nil,
104
+ count: group.sum(&:count),
105
+ total_ms: group.sum(&:total_ms),
106
+ self_total_ms: group.sum(&:self_total_ms),
107
+ max_ms: group.map(&:max_ms).max
108
+ )
109
+ end
82
110
  end
83
111
 
84
112
  def factory_event?(event)
@@ -89,13 +117,12 @@ module RSpecTelemetry
89
117
  event["depth"].to_i.zero?
90
118
  end
91
119
 
92
- def duration_ms(event)
93
- key = all_depths ? "self_duration_ms" : "duration_ms"
94
- event[key].to_f
95
- end
120
+ # Default mode compares inclusive time on root events; --all-depths compares
121
+ # self time so nested children are not double-counted.
122
+ def duration_for(stat)
123
+ return 0.0 unless stat
96
124
 
97
- def empty_stat
98
- FactoryStat.new(0, 0.0)
125
+ all_depths ? stat.self_total_ms : stat.total_ms
99
126
  end
100
127
  end
101
128
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTelemetry
4
+ # Single source of truth for how telemetry numbers are rendered, so the decimal
5
+ # places, sign convention, and ms/s threshold stay consistent across the console
6
+ # report, the comparison table, and the live summary.
7
+ module Formatting
8
+ module_function
9
+
10
+ # Human-friendly duration: seconds past 1000ms, otherwise milliseconds.
11
+ def duration(ms)
12
+ ms = ms.to_f
13
+ ms >= 1000 ? format("%.2fs", ms / 1000.0) : format("%.1fms", ms)
14
+ end
15
+
16
+ def fixed(value) = format("%.1f", value.to_f)
17
+
18
+ def signed_fixed(value) = format("%+.1f", value.to_f)
19
+
20
+ def signed_integer(value) = format("%+d", value.to_i)
21
+
22
+ def percent(value) = format("%.1f%%", value.to_f)
23
+
24
+ def signed_percent(value) = format("%+.1f%%", value.to_f)
25
+ end
26
+ end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "time"
4
4
 
5
+ require_relative "summary"
6
+
5
7
  module RSpecTelemetry
6
8
  class Recorder
7
9
  # FactoryBot notifications read this to attach themselves to the active example.
@@ -43,7 +45,6 @@ module RSpecTelemetry
43
45
  def finish
44
46
  return unless @started
45
47
 
46
- SummaryPrinter.print(@summary, @config) if @config.print_summary
47
48
  @writer.close
48
49
  @started = false
49
50
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "factory_aggregation"
4
+ require_relative "formatting"
4
5
 
5
6
  module RSpecTelemetry
6
7
  class Summary
@@ -102,10 +103,10 @@ module RSpecTelemetry
102
103
  tops.each_with_index do |f, i|
103
104
  lines << "#{i + 1}. #{f.key}"
104
105
  lines << " count: #{f.count}"
105
- lines << " self_total: #{round(f.self_total_ms)}ms"
106
- lines << " total: #{round(f.total_ms)}ms"
107
- lines << " avg: #{round(f.avg_ms)}ms"
108
- lines << " max: #{round(f.max_ms)}ms"
106
+ lines << " self_total: #{Formatting.fixed(f.self_total_ms)}ms"
107
+ lines << " total: #{Formatting.fixed(f.total_ms)}ms"
108
+ lines << " avg: #{Formatting.fixed(f.avg_ms)}ms"
109
+ lines << " max: #{Formatting.fixed(f.max_ms)}ms"
109
110
  lines << ""
110
111
  end
111
112
 
@@ -119,16 +120,12 @@ module RSpecTelemetry
119
120
  lines = ["Slow examples:", ""]
120
121
  slow.each_with_index do |e, i|
121
122
  lines << "#{i + 1}. #{e.example_id}"
122
- lines << " duration: #{round(e.duration_ms)}ms"
123
- lines << " factory_bot_total: #{round(e.factory_bot_total_ms)}ms (#{e.factory_bot_count} calls)"
123
+ lines << " duration: #{Formatting.fixed(e.duration_ms)}ms"
124
+ lines << " factory_bot_total: #{Formatting.fixed(e.factory_bot_total_ms)}ms (#{e.factory_bot_count} calls)"
124
125
  lines << ""
125
126
  end
126
127
 
127
128
  lines
128
129
  end
129
-
130
- def round(value)
131
- value.to_f.round(1)
132
- end
133
130
  end
134
131
  end
@@ -58,7 +58,7 @@ module RSpecTelemetry
58
58
  @follow_ctl = FollowController.new(source: source, active: follow)
59
59
  @source_view = SourceView.new(source_root: source_root, base_dir: base_dir)
60
60
  @renderer = AppRenderer.new
61
- @list = TuiTui::ScrollList.new
61
+ @list = TuiTui::Widget::ScrollList.new
62
62
  @detail_scroll = 0
63
63
  @focus_ring = TuiTui::FocusRing.new(:timeline, :detail)
64
64
  @modal = nil
@@ -76,11 +76,13 @@ module RSpecTelemetry
76
76
 
77
77
  def wants_tick? = @follow_ctl.wants_tick?
78
78
 
79
- def redraw?(event) = event.is_a?(TuiTui::KeyEvent) && event.key == REDRAW
80
-
81
79
  def update(event)
82
80
  case event
83
81
  when TuiTui::KeyEvent
82
+ # Ctrl-L forces a full repaint; 0.4 requests it via a command rather
83
+ # than the removed redraw? hook.
84
+ return [self, TuiTui::Command::Invalidate.new] if event.key == REDRAW
85
+
84
86
  @modal ? route_modal(event) : handle_key_event(event)
85
87
  when TuiTui::MouseEvent
86
88
  @modal ? route_modal(event) : handle_mouse(event)
@@ -139,7 +141,7 @@ module RSpecTelemetry
139
141
  end
140
142
 
141
143
  def confirm_quit
142
- open_modal(TuiTui::Confirm.new("Quit the trace viewer?", theme: Theme.base)) { |r| :quit if r == :ok }
144
+ open_modal(TuiTui::Modal::Confirm.new("Quit the trace viewer?", theme: Theme.base)) { |r| :quit if r == :ok }
143
145
  end
144
146
 
145
147
  def handle_key_event(event)
@@ -155,7 +157,7 @@ module RSpecTelemetry
155
157
  when "q"
156
158
  confirm_quit
157
159
  when "?"
158
- open_modal(TuiTui::Help.new("Keys", HELP, theme: Theme.base)) { nil }
160
+ open_modal(TuiTui::Modal::Help.new("Keys", HELP, theme: Theme.base)) { nil }
159
161
  when "1"
160
162
  set_view(:timeline)
161
163
  when "2"
@@ -14,7 +14,7 @@ module RSpecTelemetry
14
14
  def length = @lines.length
15
15
 
16
16
  def draw(canvas, rect)
17
- TuiTui::TextView.draw(canvas, rect, top: @scroll, scrollbar: Theme.base, total: @lines.length) do |index|
17
+ TuiTui::Widget::TextView.draw(canvas, rect, top: @scroll, scrollbar: Theme.base, total: @lines.length) do |index|
18
18
  line = @lines[index]
19
19
  next nil if line.nil?
20
20
 
@@ -14,7 +14,7 @@ module RSpecTelemetry
14
14
 
15
15
  def draw(canvas, rect)
16
16
  highlight = @focus ? Theme::SELECT : Theme::SELECT_BLUR
17
- TuiTui::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: Theme.base) do |index, selected|
17
+ TuiTui::Widget::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: Theme.base) do |index, selected|
18
18
  row = @rows[index]
19
19
  style = selected ? highlight : Theme.style(row.style)
20
20
  TuiTui::Line[TuiTui::Span[row.text, style]]
@@ -124,7 +124,7 @@ module RSpecTelemetry
124
124
  end
125
125
 
126
126
  def open_filter(app)
127
- app.open_modal(TuiTui::Prompt.new("Filter:", value: @filter.to_s, theme: Theme.base)) do |result|
127
+ app.open_modal(TuiTui::Modal::Prompt.new("Filter:", value: @filter.to_s, theme: Theme.base)) do |result|
128
128
  apply_filter(app, result[1]) if result.is_a?(Array)
129
129
  end
130
130
  end
@@ -141,7 +141,7 @@ module RSpecTelemetry
141
141
 
142
142
  app
143
143
  .open_modal(
144
- TuiTui::Select.new("Jump to example", rows.map { |i| Label.plain(@visible[i]) }, theme: Theme.base)
144
+ TuiTui::Modal::Select.new("Jump to example", rows.map { |i| Label.plain(@visible[i]) }, theme: Theme.base)
145
145
  ) do |result|
146
146
  app.go_to(rows[result]) if result.is_a?(Integer)
147
147
  end
@@ -48,7 +48,7 @@ module RSpecTelemetry
48
48
 
49
49
  def draw_window(canvas, body)
50
50
  top = window_top(body.rows)
51
- TuiTui::TextView.draw(canvas, body, top: top) do |index|
51
+ TuiTui::Widget::TextView.draw(canvas, body, top: top) do |index|
52
52
  text = @lines[index]
53
53
  next nil if text.nil?
54
54
 
@@ -26,7 +26,7 @@ module RSpecTelemetry
26
26
 
27
27
  file, target = source_location(location)
28
28
  lines = numbered_source(@resolver.lines_for(file), target, file)
29
- TuiTui::Pager.new(
29
+ TuiTui::Modal::Pager.new(
30
30
  "source: #{location}",
31
31
  lines,
32
32
  start: [target - 4, 0].max,
@@ -31,7 +31,7 @@ module RSpecTelemetry
31
31
 
32
32
  def draw(canvas, rect)
33
33
  right = "#{@position} #{@notice || HINTS} "
34
- TuiTui::StatusBar.draw(canvas, rect, left: left_text, right: right, style: Theme::BAR)
34
+ TuiTui::Widget::StatusBar.draw(canvas, rect, left: left_text, right: right, style: Theme::BAR)
35
35
  end
36
36
 
37
37
  private
@@ -24,7 +24,7 @@ module RSpecTelemetry
24
24
 
25
25
  def draw(canvas, rect)
26
26
  highlight = @focus ? Theme::SELECT : Theme::SELECT_BLUR
27
- TuiTui::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: Theme.base) do |index, selected|
27
+ TuiTui::Widget::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: Theme.base) do |index, selected|
28
28
  entry = @entries[index]
29
29
  text = prefix(entry) + Label.plain(entry) + duration_suffix(entry)
30
30
  style = selected ? highlight : Theme.style(Label.category(entry))
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RSpecTelemetry
4
- VERSION = "0.3.0"
4
+ VERSION = "0.5.0"
5
5
  end
@@ -32,7 +32,13 @@ module RSpecTelemetry
32
32
  end
33
33
 
34
34
  def finish!
35
- @recorder&.finish
35
+ active = @recorder
36
+ if active&.started?
37
+ # Printing the end-of-run summary is a reporting concern owned by the
38
+ # lifecycle, not by the Recorder (which only records events).
39
+ SummaryPrinter.print(active.summary, config)
40
+ end
41
+ active&.finish
36
42
  unsubscribe!
37
43
  end
38
44
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec_telemetry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - takahashimm
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - "~>"
31
31
  - !ruby/object:Gem::Version
32
- version: '0.2'
32
+ version: '0.4'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: '0.2'
39
+ version: '0.4'
40
40
  description: Collect RSpec / FactoryBot telemetry as NDJSON to find slow tests.
41
41
  email:
42
42
  - takahashimm@gmail.com
@@ -63,6 +63,7 @@ files:
63
63
  - lib/rspec_telemetry/factory_aggregation.rb
64
64
  - lib/rspec_telemetry/factory_comparison.rb
65
65
  - lib/rspec_telemetry/formatter.rb
66
+ - lib/rspec_telemetry/formatting.rb
66
67
  - lib/rspec_telemetry/ndjson.rb
67
68
  - lib/rspec_telemetry/recorder.rb
68
69
  - lib/rspec_telemetry/subscribers/factory_bot.rb