rspec_telemetry 0.3.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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +35 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +193 -0
  5. data/examples/sample.ndjson +15 -0
  6. data/exe/rspec-telemetry +7 -0
  7. data/exe/rspec-telemetry-compare +6 -0
  8. data/exe/rspec-telemetry-viewer +67 -0
  9. data/lib/rspec_telemetry/analyzer.rb +170 -0
  10. data/lib/rspec_telemetry/cli.rb +71 -0
  11. data/lib/rspec_telemetry/compare_cli.rb +129 -0
  12. data/lib/rspec_telemetry/config.rb +40 -0
  13. data/lib/rspec_telemetry/console_report.rb +124 -0
  14. data/lib/rspec_telemetry/factory_aggregation.rb +50 -0
  15. data/lib/rspec_telemetry/factory_comparison.rb +101 -0
  16. data/lib/rspec_telemetry/formatter.rb +91 -0
  17. data/lib/rspec_telemetry/ndjson.rb +24 -0
  18. data/lib/rspec_telemetry/recorder.rb +75 -0
  19. data/lib/rspec_telemetry/subscribers/factory_bot.rb +88 -0
  20. data/lib/rspec_telemetry/summary.rb +134 -0
  21. data/lib/rspec_telemetry/trace/viewer/app.rb +269 -0
  22. data/lib/rspec_telemetry/trace/viewer/app_renderer.rb +88 -0
  23. data/lib/rspec_telemetry/trace/viewer/detail_lines.rb +75 -0
  24. data/lib/rspec_telemetry/trace/viewer/detail_pane.rb +28 -0
  25. data/lib/rspec_telemetry/trace/viewer/document.rb +198 -0
  26. data/lib/rspec_telemetry/trace/viewer/follow_controller.rb +51 -0
  27. data/lib/rspec_telemetry/trace/viewer/format.rb +23 -0
  28. data/lib/rspec_telemetry/trace/viewer/label.rb +84 -0
  29. data/lib/rspec_telemetry/trace/viewer/layout.rb +100 -0
  30. data/lib/rspec_telemetry/trace/viewer/pane_resizer.rb +99 -0
  31. data/lib/rspec_telemetry/trace/viewer/report_pane.rb +26 -0
  32. data/lib/rspec_telemetry/trace/viewer/report_view.rb +86 -0
  33. data/lib/rspec_telemetry/trace/viewer/screen/ranked_screen.rb +66 -0
  34. data/lib/rspec_telemetry/trace/viewer/screen/timeline_screen.rb +180 -0
  35. data/lib/rspec_telemetry/trace/viewer/source.rb +52 -0
  36. data/lib/rspec_telemetry/trace/viewer/source_pane.rb +70 -0
  37. data/lib/rspec_telemetry/trace/viewer/source_resolver.rb +56 -0
  38. data/lib/rspec_telemetry/trace/viewer/source_view.rb +63 -0
  39. data/lib/rspec_telemetry/trace/viewer/status_line.rb +50 -0
  40. data/lib/rspec_telemetry/trace/viewer/text_report.rb +49 -0
  41. data/lib/rspec_telemetry/trace/viewer/theme.rb +30 -0
  42. data/lib/rspec_telemetry/trace/viewer/time_bar.rb +46 -0
  43. data/lib/rspec_telemetry/trace/viewer/timeline_pane.rb +53 -0
  44. data/lib/rspec_telemetry/trace/viewer/version.rb +9 -0
  45. data/lib/rspec_telemetry/trace/viewer.rb +31 -0
  46. data/lib/rspec_telemetry/version.rb +5 -0
  47. data/lib/rspec_telemetry/writer.rb +59 -0
  48. data/lib/rspec_telemetry.rb +102 -0
  49. metadata +122 -0
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "theme"
4
+ require_relative "label"
5
+ require_relative "format"
6
+
7
+ module RSpecTelemetry
8
+ module Trace
9
+ module Viewer
10
+ class TimelinePane
11
+ EXPANDED = "- "
12
+ COLLAPSED = "+ "
13
+ NO_CHILDREN = " "
14
+ EVENT_INDENT = " "
15
+
16
+ def initialize(entries, list:, focus:, collapsed: nil, childful: nil, durations: nil)
17
+ @entries = entries
18
+ @list = list
19
+ @focus = focus
20
+ @collapsed = collapsed || []
21
+ @childful = childful || []
22
+ @durations = durations || {}
23
+ end
24
+
25
+ def draw(canvas, rect)
26
+ highlight = @focus ? Theme::SELECT : Theme::SELECT_BLUR
27
+ TuiTui::List.new(@list).draw(canvas, rect, highlight: highlight, scrollbar: Theme.base) do |index, selected|
28
+ entry = @entries[index]
29
+ text = prefix(entry) + Label.plain(entry) + duration_suffix(entry)
30
+ style = selected ? highlight : Theme.style(Label.category(entry))
31
+ TuiTui::Line[TuiTui::Span[text, style]]
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def prefix(entry)
38
+ return EVENT_INDENT unless entry.is_a?(Document::Action)
39
+ return NO_CHILDREN unless @childful.include?(entry.seq)
40
+
41
+ @collapsed.include?(entry.seq) ? COLLAPSED : EXPANDED
42
+ end
43
+
44
+ def duration_suffix(entry)
45
+ return "" unless entry.is_a?(Document::Action)
46
+
47
+ formatted = Format.ms(@durations[entry.seq])
48
+ formatted ? " (#{formatted})" : ""
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTelemetry
4
+ module Trace
5
+ module Viewer
6
+ VERSION = "0.1.0"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "viewer/version"
4
+ require_relative "viewer/document"
5
+ require_relative "viewer/format"
6
+ require_relative "viewer/theme"
7
+ require_relative "viewer/label"
8
+ require_relative "viewer/text_report"
9
+ require_relative "viewer/timeline_pane"
10
+ require_relative "viewer/detail_lines"
11
+ require_relative "viewer/detail_pane"
12
+ require_relative "viewer/status_line"
13
+ require_relative "viewer/time_bar"
14
+ require_relative "viewer/source_pane"
15
+ require_relative "viewer/source"
16
+ require_relative "viewer/source_resolver"
17
+ require_relative "viewer/source_view"
18
+ require_relative "viewer/layout"
19
+ require_relative "viewer/report_view"
20
+ require_relative "viewer/report_pane"
21
+ require_relative "viewer/app_renderer"
22
+ require_relative "viewer/screen/timeline_screen"
23
+ require_relative "viewer/screen/ranked_screen"
24
+ require_relative "viewer/app"
25
+
26
+ module RSpecTelemetry
27
+ module Trace
28
+ module Viewer
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RSpecTelemetry
4
+ VERSION = "0.3.0"
5
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "fileutils"
5
+
6
+ module RSpecTelemetry
7
+ class Writer
8
+ def initialize(output_path, flush_each: false)
9
+ @output_path = output_path
10
+ @flush_each = flush_each
11
+ @mutex = Mutex.new
12
+ @io = nil
13
+ end
14
+
15
+ def open
16
+ FileUtils.mkdir_p(File.dirname(@output_path))
17
+ # Each run gets a fresh stream; appending would create misleading time gaps.
18
+ @io = File.open(@output_path, "w")
19
+ rescue => e
20
+ warn_failure("open", e)
21
+ @io = nil
22
+ end
23
+
24
+ def write(event)
25
+ return unless @io
26
+
27
+ @mutex.synchronize do
28
+ @io.puts(JSON.generate(event))
29
+ @io.flush if @flush_each
30
+ end
31
+
32
+ rescue => e
33
+ warn_failure("write", e)
34
+ end
35
+
36
+ def flush
37
+ @mutex.synchronize { @io&.flush }
38
+ rescue => e
39
+ warn_failure("flush", e)
40
+ end
41
+
42
+ def close
43
+ @mutex.synchronize do
44
+ @io&.flush
45
+ @io&.close
46
+ @io = nil
47
+ end
48
+
49
+ rescue => e
50
+ warn_failure("close", e)
51
+ end
52
+
53
+ private
54
+
55
+ def warn_failure(action, error)
56
+ warn("[rspec-telemetry] failed to #{action} event: #{error.class}: #{error.message}")
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "rspec_telemetry/version"
4
+ require_relative "rspec_telemetry/config"
5
+ require_relative "rspec_telemetry/writer"
6
+ require_relative "rspec_telemetry/summary"
7
+ require_relative "rspec_telemetry/recorder"
8
+
9
+ module RSpecTelemetry
10
+ class << self
11
+ def config
12
+ @config ||= Config.new
13
+ end
14
+
15
+ def configure
16
+ yield config if block_given?
17
+ config
18
+ end
19
+
20
+ def recorder
21
+ @recorder ||= Recorder.new(config)
22
+ end
23
+
24
+ def start!
25
+ return unless config.enabled
26
+
27
+ recorder.start
28
+ return unless recorder.started?
29
+
30
+ subscribe!
31
+ recorder
32
+ end
33
+
34
+ def finish!
35
+ @recorder&.finish
36
+ unsubscribe!
37
+ end
38
+
39
+ def reset!
40
+ unsubscribe!
41
+ @config = nil
42
+ @recorder = nil
43
+ @warned = nil
44
+ end
45
+
46
+ def safely(context)
47
+ yield
48
+ # Telemetry must never break the user's RSpec run, even on non-StandardError failures.
49
+ rescue Exception => e # rubocop:disable Lint/RescueException
50
+ warn_once(context, e)
51
+ nil
52
+ end
53
+
54
+ private
55
+
56
+ def warn_once(context, error)
57
+ @warned ||= {}
58
+ return if @warned[context]
59
+
60
+ @warned[context] = true
61
+ warn(
62
+ "[rspec-telemetry] #{context} で例外を無視しました(以後同種は抑制): " \
63
+ "#{error.class}: #{error.message}"
64
+ )
65
+ end
66
+
67
+ def subscribe!
68
+ return unless config.capture_factory_bot
69
+ return if @factory_bot_subscriber
70
+
71
+ subscriber = build_factory_bot_subscriber
72
+ return unless subscriber
73
+
74
+ @factory_bot_subscriber = subscriber
75
+ @factory_bot_subscriber.subscribe
76
+ end
77
+
78
+ # activesupport is an optional dependency: it is only needed for FactoryBot
79
+ # tracking, which relies on ActiveSupport::Notifications. FactoryBot itself
80
+ # pulls in activesupport, so when it is absent there are no factory events to
81
+ # capture and we silently skip the subscription.
82
+ def build_factory_bot_subscriber
83
+ require_relative "rspec_telemetry/subscribers/factory_bot"
84
+ Subscribers::FactoryBot.new(recorder)
85
+ rescue LoadError
86
+ nil
87
+ end
88
+
89
+ def unsubscribe!
90
+ @factory_bot_subscriber&.unsubscribe
91
+ @factory_bot_subscriber = nil
92
+ end
93
+ end
94
+ end
95
+
96
+ if defined?(RSpec) && RSpec.respond_to?(:configure) && !ENV.key?("RSPEC_TELEMETRY_NO_AUTOLOAD")
97
+ require_relative "rspec_telemetry/formatter"
98
+
99
+ RSpec.configure do |config|
100
+ config.add_formatter(RSpecTelemetry::Formatter)
101
+ end
102
+ end
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rspec_telemetry
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - takahashimm
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rspec-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: tui_tui
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.2'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.2'
40
+ description: Collect RSpec / FactoryBot telemetry as NDJSON to find slow tests.
41
+ email:
42
+ - takahashimm@gmail.com
43
+ executables:
44
+ - rspec-telemetry
45
+ - rspec-telemetry-compare
46
+ - rspec-telemetry-viewer
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - CHANGELOG.md
51
+ - LICENSE.txt
52
+ - README.md
53
+ - examples/sample.ndjson
54
+ - exe/rspec-telemetry
55
+ - exe/rspec-telemetry-compare
56
+ - exe/rspec-telemetry-viewer
57
+ - lib/rspec_telemetry.rb
58
+ - lib/rspec_telemetry/analyzer.rb
59
+ - lib/rspec_telemetry/cli.rb
60
+ - lib/rspec_telemetry/compare_cli.rb
61
+ - lib/rspec_telemetry/config.rb
62
+ - lib/rspec_telemetry/console_report.rb
63
+ - lib/rspec_telemetry/factory_aggregation.rb
64
+ - lib/rspec_telemetry/factory_comparison.rb
65
+ - lib/rspec_telemetry/formatter.rb
66
+ - lib/rspec_telemetry/ndjson.rb
67
+ - lib/rspec_telemetry/recorder.rb
68
+ - lib/rspec_telemetry/subscribers/factory_bot.rb
69
+ - lib/rspec_telemetry/summary.rb
70
+ - lib/rspec_telemetry/trace/viewer.rb
71
+ - lib/rspec_telemetry/trace/viewer/app.rb
72
+ - lib/rspec_telemetry/trace/viewer/app_renderer.rb
73
+ - lib/rspec_telemetry/trace/viewer/detail_lines.rb
74
+ - lib/rspec_telemetry/trace/viewer/detail_pane.rb
75
+ - lib/rspec_telemetry/trace/viewer/document.rb
76
+ - lib/rspec_telemetry/trace/viewer/follow_controller.rb
77
+ - lib/rspec_telemetry/trace/viewer/format.rb
78
+ - lib/rspec_telemetry/trace/viewer/label.rb
79
+ - lib/rspec_telemetry/trace/viewer/layout.rb
80
+ - lib/rspec_telemetry/trace/viewer/pane_resizer.rb
81
+ - lib/rspec_telemetry/trace/viewer/report_pane.rb
82
+ - lib/rspec_telemetry/trace/viewer/report_view.rb
83
+ - lib/rspec_telemetry/trace/viewer/screen/ranked_screen.rb
84
+ - lib/rspec_telemetry/trace/viewer/screen/timeline_screen.rb
85
+ - lib/rspec_telemetry/trace/viewer/source.rb
86
+ - lib/rspec_telemetry/trace/viewer/source_pane.rb
87
+ - lib/rspec_telemetry/trace/viewer/source_resolver.rb
88
+ - lib/rspec_telemetry/trace/viewer/source_view.rb
89
+ - lib/rspec_telemetry/trace/viewer/status_line.rb
90
+ - lib/rspec_telemetry/trace/viewer/text_report.rb
91
+ - lib/rspec_telemetry/trace/viewer/theme.rb
92
+ - lib/rspec_telemetry/trace/viewer/time_bar.rb
93
+ - lib/rspec_telemetry/trace/viewer/timeline_pane.rb
94
+ - lib/rspec_telemetry/trace/viewer/version.rb
95
+ - lib/rspec_telemetry/version.rb
96
+ - lib/rspec_telemetry/writer.rb
97
+ homepage: https://github.com/takahashim/rspec_telemetry
98
+ licenses:
99
+ - MIT
100
+ metadata:
101
+ allowed_push_host: https://rubygems.org
102
+ homepage_uri: https://github.com/takahashim/rspec_telemetry
103
+ source_code_uri: https://github.com/takahashim/rspec_telemetry
104
+ changelog_uri: https://github.com/takahashim/rspec_telemetry
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '3.2'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubygems_version: 3.6.9
120
+ specification_version: 4
121
+ summary: Collect RSpec / FactoryBot telemetry as NDJSON to find slow tests.
122
+ test_files: []