test-prof 1.2.2 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13127b82bf827a56e15c8d060ff5a8e5be791e16deea31e899d88ecd7b76bb38
4
- data.tar.gz: 1ae017bc058b3017355c75fd871c17aad9ec3649f7b982af062d606b93d9fe2c
3
+ metadata.gz: '0678bc968a267ebeece00e12338f6408683140428ecdd041ece64c70e6bb91c0'
4
+ data.tar.gz: 3ab5bc88b7b5dea557847f05a16d7fc3e4c6c9212115cd16984a56b7dd7b1737
5
5
  SHA512:
6
- metadata.gz: b272b01aca1fb3bf9328fcd97fc148f5f329acf66650337071568aff6e20e1f3f95c066295df4777a7bd401976879bdddb3ff43fb2ce5093bca261fe65c07a94
7
- data.tar.gz: 309644b36e328286ea52705196daa99e8af2f5237cf27f33ffa0091961f642eb29da73bcf48cb05e51896e3bfe9d709a1c08cbc1cfcd9abae1b429674a98fb2b
6
+ metadata.gz: 5b32e7e689cef27e72ce5f7e65ca0463bae7b83dfcaf77eba17aa0cc9bae1b74d6223b6b141e805debb7640b74b75be34bf238a8f41cb3efaf0386929935d814
7
+ data.tar.gz: 34200774bc30c39acc48109fbc5031fd8f48abb5f7175799a1e1bbfd7650a11a441c9ba63638bf186d78ef5cd15b7b33e64782c781a7f28bd43e43cb6d1e5588
data/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 1.3.0 (2023-11-21)
6
+
7
+ - Add Vernier integration. ([@palkan][])
8
+
9
+ - StackProf uses JSON format by default. ([@palkan][])
10
+
11
+ - MemoryProf ia added. ([@Vankiru][])
12
+
13
+ ## 1.2.3 (2023-09-11)
14
+
15
+ - Minor fixes and dependencies upgrades.
16
+
5
17
  ## 1.2.2 (2023-06-27)
6
18
 
7
19
  - Ignore inaccessible connection pools in `before_all`. ([@bf4][])
@@ -156,7 +168,7 @@ And for every test run see the overall factories usage:
156
168
 
157
169
  ## 1.0.0.rc2 (2021-01-06)
158
170
 
159
- - Make Rails fixtures accesible in `before_all`. ([@palkan][])
171
+ - Make Rails fixtures accessible in `before_all`. ([@palkan][])
160
172
 
161
173
  You can load and access fixtures when explicitly enabling them via `before_all(setup_fixtures: true, &block)`.
162
174
 
@@ -217,7 +229,7 @@ end
217
229
 
218
230
  See more in [#181](https://github.com/test-prof/test-prof/issues/181).
219
231
 
220
- - Adds the ability to define stackprof's interval sampling by using `TEST_STACK_PROF_INTERVAL` env variable ([@LynxEyes][])
232
+ - Adds the ability to define stackprof interval sampling by using `TEST_STACK_PROF_INTERVAL` env variable ([@LynxEyes][])
221
233
 
222
234
  Now you can use `$ TEST_STACK_PROF=1 TEST_STACK_PROF_INTERVAL=10000 rspec` to define a custom interval (in microseconds).
223
235
 
@@ -363,3 +375,4 @@ See [changelog](https://github.com/test-prof/test-prof/blob/v0.8.0/CHANGELOG.md)
363
375
  [@cbarton]: https://github.com/cbarton
364
376
  [@peret]: https://github.com/peret
365
377
  [@bf4]: https://github.com/bf4
378
+ [@Vankiru]: https://github.com/Vankiru
data/README.md CHANGED
@@ -93,7 +93,7 @@ Supported RSpec version (for RSpec features only): >= 3.5.0 (for older RSpec ver
93
93
 
94
94
  Check out our [docs][].
95
95
 
96
- ## What's next?
96
+ ## What's next
97
97
 
98
98
  Have an idea? [Propose](https://github.com/test-prof/test-prof/issues/new) a feature request!
99
99
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "test_prof/event_prof/minitest"
4
4
  require "test_prof/factory_doctor/minitest"
5
+ require "test_prof/memory_prof/minitest"
5
6
 
6
7
  module Minitest # :nodoc:
7
8
  module TestProf # :nodoc:
@@ -13,6 +14,8 @@ module Minitest # :nodoc:
13
14
  opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
14
15
  opts[:fdoc] = true if ENV["FDOC"]
15
16
  opts[:sample] = true if ENV["SAMPLE"] || ENV["SAMPLE_GROUPS"]
17
+ opts[:mem_prof_mode] = ENV["TEST_MEM_PROF"] if ENV["TEST_MEM_PROF"]
18
+ opts[:mem_prof_top_count] = ENV["TEST_MEM_PROF_COUNT"] if ENV["TEST_MEM_PROF_COUNT"]
16
19
  end
17
20
  end
18
21
  end
@@ -33,6 +36,12 @@ module Minitest # :nodoc:
33
36
  opts.on "--factory-doctor", TrueClass, "Enable Factory Doctor for your examples" do |flag|
34
37
  options[:fdoc] = flag
35
38
  end
39
+ opts.on "--mem-prof=MODE", "Enable MemoryProf for your examples" do |flag|
40
+ options[:mem_prof_mode] = flag
41
+ end
42
+ opts.on "--mem-prof-top-count=N", "Limits MemoryProf results with N groups/examples" do |flag|
43
+ options[:mem_prof_top_count] = flag
44
+ end
36
45
  end
37
46
 
38
47
  def self.plugin_test_prof_init(options)
@@ -40,6 +49,7 @@ module Minitest # :nodoc:
40
49
 
41
50
  reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
42
51
  reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
52
+ reporter << TestProf::MemoryProfReporter.new(options[:io], options) if options[:mem_prof_mode]
43
53
 
44
54
  ::TestProf::MinitestSample.call if options[:sample]
45
55
  end
@@ -18,7 +18,7 @@ module TestProf
18
18
  end
19
19
 
20
20
  def compile_sql(sql, binds)
21
- sql.gsub(/\?/) { binds.shift.gsub("\n", "' || char(10) || '") }
21
+ sql.gsub("?") { binds.shift.gsub("\n", "' || char(10) || '") }
22
22
  end
23
23
 
24
24
  def import(path)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
  require "test_prof/ext/float_duration"
5
5
  require "test_prof/any_fixture/dump"
6
6
 
@@ -5,10 +5,12 @@ module TestProf
5
5
  module Adapters
6
6
  # ActiveRecord adapter for `before_all`
7
7
  module ActiveRecord
8
+ POOL_ARGS = ((::ActiveRecord::VERSION::MAJOR > 6) ? [:writing] : []).freeze
9
+
8
10
  class << self
9
11
  def all_connections
10
12
  @all_connections ||= if ::ActiveRecord::Base.respond_to? :connects_to
11
- ::ActiveRecord::Base.connection_handler.connection_pool_list.filter_map { |pool|
13
+ ::ActiveRecord::Base.connection_handler.connection_pool_list(*POOL_ARGS).filter_map { |pool|
12
14
  begin
13
15
  pool.connection
14
16
  rescue *pool_connection_errors => error
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
 
5
5
  module TestProf
6
6
  # `before_all` helper configuration
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "logger"
5
+
6
+ require "test_prof/logging"
7
+ require "test_prof/utils"
8
+
9
+ # Ruby applications tests profiling tools.
10
+ #
11
+ # Contains tools to analyze factories usage, integrate with Ruby profilers,
12
+ # profile your examples using ActiveSupport notifications (if any) and
13
+ # statically analyze your code with custom RuboCop cops.
14
+ #
15
+ # Example usage:
16
+ #
17
+ # require 'test_prof'
18
+ #
19
+ # # Activate a tool by providing environment variable, e.g.
20
+ # TEST_RUBY_PROF=1 rspec ...
21
+ #
22
+ # # or manually in your code
23
+ # TestProf::RubyProf.run
24
+ #
25
+ # See other modules for more examples.
26
+ module TestProf
27
+ class << self
28
+ include Logging
29
+
30
+ def config
31
+ @config ||= Configuration.new
32
+ end
33
+
34
+ def configure
35
+ yield config
36
+ end
37
+
38
+ # Returns true if we're inside RSpec
39
+ def rspec?
40
+ defined?(RSpec::Core)
41
+ end
42
+
43
+ # Returns true if we're inside Minitest
44
+ def minitest?
45
+ defined?(Minitest)
46
+ end
47
+
48
+ # Returns true if Spring is used and not disabled
49
+ def spring?
50
+ # See https://github.com/rails/spring/blob/577cf01f232bb6dbd0ade7df2df2ac209697e741/lib/spring/binstub.rb
51
+ disabled = ENV["DISABLE_SPRING"]
52
+ defined?(::Spring::Application) && (disabled.nil? || disabled.empty? || disabled == "0")
53
+ end
54
+
55
+ # Returns the current process time
56
+ def now
57
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
58
+ end
59
+
60
+ # Require gem and shows a custom
61
+ # message if it fails to load
62
+ def require(gem_name, msg = nil)
63
+ Kernel.require gem_name
64
+ block_given? ? yield : true
65
+ rescue LoadError
66
+ log(:error, msg) if msg
67
+ false
68
+ end
69
+
70
+ # Run block only if provided env var is present and
71
+ # equal to the provided value (if any).
72
+ # Contains workaround for applications using Spring.
73
+ def activate(env_var, val = nil)
74
+ if spring?
75
+ notify_spring_detected
76
+ ::Spring.after_fork do
77
+ activate!(env_var, val) do
78
+ notify_spring_activate env_var
79
+ yield
80
+ end
81
+ end
82
+ else
83
+ activate!(env_var, val) { yield }
84
+ end
85
+ end
86
+
87
+ # Return absolute path to asset
88
+ def asset_path(filename)
89
+ ::File.expand_path(filename, ::File.join(::File.dirname(__FILE__), "..", "..", "assets"))
90
+ end
91
+
92
+ # Return a path to store artifact
93
+ def artifact_path(filename)
94
+ create_artifact_dir
95
+
96
+ with_timestamps(
97
+ ::File.join(
98
+ config.output_dir,
99
+ with_report_suffix(
100
+ filename
101
+ )
102
+ )
103
+ )
104
+ end
105
+
106
+ def create_artifact_dir
107
+ FileUtils.mkdir_p(config.output_dir)[0]
108
+ end
109
+
110
+ private
111
+
112
+ def activate!(env_var, val)
113
+ yield if ENV[env_var] && (val.nil? || val === ENV[env_var])
114
+ end
115
+
116
+ def with_timestamps(path)
117
+ return path unless config.timestamps?
118
+ timestamps = "-#{now.to_i}"
119
+ "#{path.sub(/\.\w+$/, "")}#{timestamps}#{::File.extname(path)}"
120
+ end
121
+
122
+ def with_report_suffix(path)
123
+ return path if config.report_suffix.nil?
124
+
125
+ "#{path.sub(/\.\w+$/, "")}-#{config.report_suffix}#{::File.extname(path)}"
126
+ end
127
+
128
+ def notify_spring_detected
129
+ return if instance_variable_defined?(:@spring_notified)
130
+ log :info, "Spring detected"
131
+ @spring_notified = true
132
+ end
133
+
134
+ def notify_spring_activate(env_var)
135
+ log :info, "Activating #{env_var} with `Spring.after_fork`"
136
+ end
137
+ end
138
+
139
+ # TestProf configuration
140
+ class Configuration
141
+ attr_accessor :output, # IO to write logs
142
+ :color, # Whether to colorize output or not
143
+ :output_dir, # Directory to store artifacts
144
+ :timestamps, # Whether to use timestamped names for artifacts,
145
+ :report_suffix # Custom suffix for reports/artifacts
146
+
147
+ def initialize
148
+ @output = $stdout
149
+ @color = true
150
+ @output_dir = "tmp/test_prof"
151
+ @timestamps = false
152
+ @report_suffix = ENV["TEST_PROF_REPORT"]
153
+ end
154
+
155
+ def color?
156
+ color == true && output.is_a?(IO) && output.tty?
157
+ end
158
+
159
+ def timestamps?
160
+ timestamps == true
161
+ end
162
+
163
+ def logger
164
+ @logger ||= Logger.new(output, formatter: Logging::Formatter.new)
165
+ end
166
+ end
167
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
  require "test_prof/factory_bot"
5
5
  require "test_prof/factory_all_stub/factory_bot_patch"
6
6
 
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "test_prof"
3
+ require "test_prof/core"
4
4
  require "test_prof/factory_bot"
5
5
  require "test_prof/factory_default/factory_bot_patch"
6
6
  require "test_prof/ext/float_duration"
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest/base_reporter"
4
+
5
+ module Minitest
6
+ module TestProf
7
+ class MemoryProfReporter < BaseReporter # :nodoc:
8
+ attr_reader :tracker, :printer, :current_example
9
+
10
+ def initialize(io = $stdout, options = {})
11
+ super
12
+
13
+ configure_profiler(options)
14
+
15
+ @tracker = ::TestProf::MemoryProf.tracker
16
+ @printer = ::TestProf::MemoryProf.printer(tracker)
17
+
18
+ @current_example = nil
19
+ end
20
+
21
+ def prerecord(group, example)
22
+ set_current_example(group, example)
23
+ tracker.example_started(current_example)
24
+ end
25
+
26
+ def record(example)
27
+ tracker.example_finished(current_example)
28
+ end
29
+
30
+ def start
31
+ tracker.start
32
+ end
33
+
34
+ def report
35
+ tracker.finish
36
+ printer.print
37
+ end
38
+
39
+ private
40
+
41
+ def set_current_example(group, example)
42
+ @current_example = {
43
+ name: example.gsub(/^test_(?:\d+_)?/, ""),
44
+ location: location_with_line_number(group, example)
45
+ }
46
+ end
47
+
48
+ def configure_profiler(options)
49
+ ::TestProf::MemoryProf.configure do |config|
50
+ config.mode = options[:mem_prof_mode]
51
+ config.top_count = options[:mem_prof_top_count] if options[:mem_prof_top_count]
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module MemoryProf
5
+ class Printer
6
+ module NumberToHuman
7
+ BASE = 1024
8
+ UNITS = %w[B KB MB GB TB PB EB ZB]
9
+
10
+ class << self
11
+ def convert(number)
12
+ exponent = exponent(number)
13
+ human_size = number.to_f / (BASE**exponent)
14
+
15
+ "#{round(human_size)}#{UNITS[exponent]}"
16
+ end
17
+
18
+ private
19
+
20
+ def exponent(number)
21
+ return 0 unless number.positive?
22
+
23
+ max = UNITS.size - 1
24
+
25
+ exponent = (Math.log(number) / Math.log(BASE)).to_i
26
+ (exponent > max) ? max : exponent
27
+ end
28
+
29
+ def round(number)
30
+ if integer?(number)
31
+ number.round
32
+ else
33
+ number.round(2)
34
+ end
35
+ end
36
+
37
+ def integer?(number)
38
+ number.round == number
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/memory_prof/printer/number_to_human"
4
+ require "test_prof/ext/string_truncate"
5
+
6
+ module TestProf
7
+ module MemoryProf
8
+ class Printer
9
+ include Logging
10
+ using StringTruncate
11
+
12
+ def initialize(tracker)
13
+ @tracker = tracker
14
+ end
15
+
16
+ def print
17
+ messages = [
18
+ "MemoryProf results\n\n",
19
+ print_total,
20
+ print_block("groups", tracker.groups),
21
+ print_block("examples", tracker.examples)
22
+ ]
23
+
24
+ log :info, messages.join
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :tracker
30
+
31
+ def print_block(name, items)
32
+ return if items.empty?
33
+
34
+ <<~GROUP
35
+ Top #{tracker.top_count} #{name} (by #{mode}):
36
+
37
+ #{print_items(items)}
38
+ GROUP
39
+ end
40
+
41
+ def print_items(items)
42
+ messages =
43
+ items.map do |item|
44
+ <<~ITEM
45
+ #{item[:name].truncate(30)} (#{item[:location]}) – +#{memory_amount(item)} (#{memory_percentage(item)}%)
46
+ ITEM
47
+ end
48
+
49
+ messages.join
50
+ end
51
+
52
+ def memory_percentage(item)
53
+ (100.0 * item[:memory] / tracker.total_memory).round(2)
54
+ end
55
+
56
+ def number_to_human(value)
57
+ NumberToHuman.convert(value)
58
+ end
59
+ end
60
+
61
+ class AllocPrinter < Printer
62
+ private
63
+
64
+ def mode
65
+ "allocations"
66
+ end
67
+
68
+ def print_total
69
+ "Total allocations: #{tracker.total_memory}\n\n"
70
+ end
71
+
72
+ def memory_amount(item)
73
+ item[:memory]
74
+ end
75
+ end
76
+
77
+ class RssPrinter < Printer
78
+ private
79
+
80
+ def mode
81
+ "RSS"
82
+ end
83
+
84
+ def print_total
85
+ "Final RSS: #{number_to_human(tracker.total_memory)}\n\n"
86
+ end
87
+
88
+ def memory_amount(item)
89
+ number_to_human(item[:memory])
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ module MemoryProf
5
+ class RSpecListener
6
+ NOTIFICATIONS = %i[
7
+ example_started
8
+ example_finished
9
+ example_group_started
10
+ example_group_finished
11
+ ].freeze
12
+
13
+ attr_reader :tracker, :printer
14
+
15
+ def initialize
16
+ @tracker = MemoryProf.tracker
17
+ @printer = MemoryProf.printer(tracker)
18
+
19
+ @tracker.start
20
+ end
21
+
22
+ def example_started(notification)
23
+ tracker.example_started(example(notification))
24
+ end
25
+
26
+ def example_finished(notification)
27
+ tracker.example_finished(example(notification))
28
+ end
29
+
30
+ def example_group_started(notification)
31
+ tracker.group_started(group(notification))
32
+ end
33
+
34
+ def example_group_finished(notification)
35
+ tracker.group_finished(group(notification))
36
+ end
37
+
38
+ def report
39
+ tracker.finish
40
+ printer.print
41
+ end
42
+
43
+ private
44
+
45
+ def example(notification)
46
+ {
47
+ name: notification.example.description,
48
+ location: notification.example.metadata[:location]
49
+ }
50
+ end
51
+
52
+ def group(notification)
53
+ {
54
+ name: notification.group.description,
55
+ location: notification.group.metadata[:location]
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ TestProf.activate("TEST_MEM_PROF") do
63
+ RSpec.configure do |config|
64
+ listener = nil
65
+
66
+ config.before(:suite) do
67
+ listener = TestProf::MemoryProf::RSpecListener.new
68
+
69
+ config.reporter.register_listener(
70
+ listener, *TestProf::MemoryProf::RSpecListener::NOTIFICATIONS
71
+ )
72
+ end
73
+
74
+ config.after(:suite) { listener&.report }
75
+ end
76
+ end