test-prof 1.2.2 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
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