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 +4 -4
- data/CHANGELOG.md +31 -1
- data/README.md +15 -0
- data/lib/rspec_telemetry/compare_cli.rb +56 -34
- data/lib/rspec_telemetry/config.rb +5 -0
- data/lib/rspec_telemetry/console_report.rb +4 -5
- data/lib/rspec_telemetry/factory_comparison.rb +50 -23
- data/lib/rspec_telemetry/formatting.rb +26 -0
- data/lib/rspec_telemetry/recorder.rb +2 -1
- data/lib/rspec_telemetry/summary.rb +7 -10
- data/lib/rspec_telemetry/trace/viewer/app.rb +7 -5
- data/lib/rspec_telemetry/trace/viewer/detail_pane.rb +1 -1
- data/lib/rspec_telemetry/trace/viewer/report_pane.rb +1 -1
- data/lib/rspec_telemetry/trace/viewer/screen/timeline_screen.rb +2 -2
- data/lib/rspec_telemetry/trace/viewer/source_pane.rb +1 -1
- data/lib/rspec_telemetry/trace/viewer/source_view.rb +1 -1
- data/lib/rspec_telemetry/trace/viewer/status_line.rb +1 -1
- data/lib/rspec_telemetry/trace/viewer/timeline_pane.rb +1 -1
- data/lib/rspec_telemetry/version.rb +1 -1
- data/lib/rspec_telemetry.rb +7 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 796f640dcbe14855620f29d78972456705d29af7025b3e01171e9f4d76dfca04
|
|
4
|
+
data.tar.gz: 271548c28826a191510187593eaef599771f964c16b3061c7e6d6751e4dbd7fd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
70
|
+
rows.sort_by { |row| [-row.count_diff.abs, row.label] }
|
|
63
71
|
when "factory"
|
|
64
|
-
rows.sort_by(&:
|
|
72
|
+
rows.sort_by(&:label)
|
|
65
73
|
else
|
|
66
|
-
rows.sort_by { |row| [-row.duration_diff_ms.abs, row.
|
|
74
|
+
rows.sort_by { |row| [-row.duration_diff_ms.abs, row.label] }
|
|
67
75
|
end
|
|
68
76
|
end
|
|
69
77
|
|
|
70
|
-
def
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
|
118
|
-
|
|
135
|
+
def separator(widths)
|
|
136
|
+
widths.map { |width| "-" * width }.join("-+-")
|
|
119
137
|
end
|
|
120
138
|
|
|
121
|
-
def
|
|
122
|
-
|
|
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 ?
|
|
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) =
|
|
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
|
-
:
|
|
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 |
|
|
54
|
-
before_stat = before
|
|
55
|
-
after_stat = after
|
|
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
|
-
|
|
59
|
-
before_count: before_stat
|
|
60
|
-
after_count: after_stat
|
|
61
|
-
before_duration_ms: before_stat
|
|
62
|
-
after_duration_ms: after_stat
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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: #{
|
|
106
|
-
lines << " total: #{
|
|
107
|
-
lines << " avg: #{
|
|
108
|
-
lines << " max: #{
|
|
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: #{
|
|
123
|
-
lines << " factory_bot_total: #{
|
|
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
|
|
|
@@ -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))
|
data/lib/rspec_telemetry.rb
CHANGED
|
@@ -32,7 +32,13 @@ module RSpecTelemetry
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def finish!
|
|
35
|
-
@recorder
|
|
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.
|
|
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.
|
|
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.
|
|
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
|