test-prof 0.1.0.beta1
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 +7 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +103 -0
- data/assets/flamegraph.demo.html +173 -0
- data/assets/flamegraph.template.html +196 -0
- data/assets/src/d3-tip.js +352 -0
- data/assets/src/d3-tip.min.js +1 -0
- data/assets/src/d3.flameGraph.css +92 -0
- data/assets/src/d3.flameGraph.js +459 -0
- data/assets/src/d3.flameGraph.min.css +1 -0
- data/assets/src/d3.flameGraph.min.js +1 -0
- data/assets/src/d3.v4.min.js +8 -0
- data/guides/any_fixture.md +60 -0
- data/guides/before_all.md +98 -0
- data/guides/event_prof.md +97 -0
- data/guides/factory_doctor.md +64 -0
- data/guides/factory_prof.md +85 -0
- data/guides/rspec_stamp.md +53 -0
- data/guides/rubocop.md +48 -0
- data/guides/ruby_prof.md +61 -0
- data/guides/stack_prof.md +43 -0
- data/lib/test-prof.rb +3 -0
- data/lib/test_prof/any_fixture.rb +67 -0
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +140 -0
- data/lib/test_prof/event_prof/custom_events/factory_create.rb +51 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +48 -0
- data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +38 -0
- data/lib/test_prof/event_prof/custom_events.rb +5 -0
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +16 -0
- data/lib/test_prof/event_prof/minitest.rb +3 -0
- data/lib/test_prof/event_prof/rspec.rb +94 -0
- data/lib/test_prof/event_prof.rb +177 -0
- data/lib/test_prof/ext/float_duration.rb +14 -0
- data/lib/test_prof/factory_doctor/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_doctor/minitest.rb +3 -0
- data/lib/test_prof/factory_doctor/rspec.rb +96 -0
- data/lib/test_prof/factory_doctor.rb +133 -0
- data/lib/test_prof/factory_prof/factory_girl_patch.rb +12 -0
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +71 -0
- data/lib/test_prof/factory_prof/printers/simple.rb +28 -0
- data/lib/test_prof/factory_prof.rb +140 -0
- data/lib/test_prof/logging.rb +25 -0
- data/lib/test_prof/recipes/rspec/any_fixture.rb +21 -0
- data/lib/test_prof/recipes/rspec/before_all.rb +23 -0
- data/lib/test_prof/rspec_stamp/parser.rb +103 -0
- data/lib/test_prof/rspec_stamp/rspec.rb +91 -0
- data/lib/test_prof/rspec_stamp.rb +135 -0
- data/lib/test_prof/rubocop.rb +3 -0
- data/lib/test_prof/ruby_prof/rspec.rb +13 -0
- data/lib/test_prof/ruby_prof.rb +194 -0
- data/lib/test_prof/stack_prof/rspec.rb +13 -0
- data/lib/test_prof/stack_prof.rb +120 -0
- data/lib/test_prof/version.rb +5 -0
- data/lib/test_prof.rb +108 -0
- metadata +227 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ripper"
|
4
|
+
|
5
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
6
|
+
|
7
|
+
module TestProf
|
8
|
+
module RSpecStamp
|
9
|
+
# Parse examples headers
|
10
|
+
module Parser
|
11
|
+
# Contains the result of parsing
|
12
|
+
class Result
|
13
|
+
attr_accessor :fname, :desc
|
14
|
+
attr_reader :tags, :htags
|
15
|
+
|
16
|
+
def add_tag(v)
|
17
|
+
@tags ||= []
|
18
|
+
@tags << v
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_htag(k, v)
|
22
|
+
@htags ||= []
|
23
|
+
@htags << [k, v]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def parse(code)
|
29
|
+
sexp = Ripper.sexp(code)
|
30
|
+
return unless sexp
|
31
|
+
|
32
|
+
# sexp has the following format:
|
33
|
+
# [:program,
|
34
|
+
# [
|
35
|
+
# [
|
36
|
+
# :command,
|
37
|
+
# [:@ident, "it", [1, 0]],
|
38
|
+
# [:args_add_block, [ ... ]]
|
39
|
+
# ]
|
40
|
+
# ]
|
41
|
+
# ]
|
42
|
+
#
|
43
|
+
# or
|
44
|
+
#
|
45
|
+
# [:program,
|
46
|
+
# [
|
47
|
+
# [
|
48
|
+
# :vcall,
|
49
|
+
# [:@ident, "it", [1, 0]]
|
50
|
+
# ]
|
51
|
+
# ]
|
52
|
+
# ]
|
53
|
+
res = Result.new
|
54
|
+
|
55
|
+
fcall = sexp[1][0][1]
|
56
|
+
fcall = fcall[1] if fcall.first == :fcall
|
57
|
+
res.fname = fcall[1]
|
58
|
+
|
59
|
+
args_block = sexp[1][0][2]
|
60
|
+
|
61
|
+
return res if args_block.nil?
|
62
|
+
|
63
|
+
args_block = args_block[1] if args_block.first == :arg_paren
|
64
|
+
|
65
|
+
args = args_block[1]
|
66
|
+
|
67
|
+
if args.first.first == :string_literal
|
68
|
+
res.desc = parse_literal(args.shift)
|
69
|
+
end
|
70
|
+
|
71
|
+
parse_arg(res, args.shift) until args.empty?
|
72
|
+
|
73
|
+
res
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def parse_arg(res, arg)
|
79
|
+
if arg.first == :symbol_literal
|
80
|
+
res.add_tag parse_literal(arg)
|
81
|
+
elsif arg.first == :bare_assoc_hash
|
82
|
+
parse_hash(res, arg[1])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def parse_hash(res, hash_arg)
|
87
|
+
hash_arg.each do |(_, label, val)|
|
88
|
+
res.add_htag label[1][0..-2].to_sym, parse_literal(val)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Expr of the form:
|
93
|
+
# [:string_literal, [:string_content, [:@tstring_content, "is", [1, 4]]]]
|
94
|
+
def parse_literal(expr)
|
95
|
+
val = expr[1][1][1]
|
96
|
+
val = val.to_sym if expr[0] == :symbol_literal ||
|
97
|
+
expr[0] == :assoc_new
|
98
|
+
val
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module RSpecStamp
|
5
|
+
class RSpecListener # :nodoc:
|
6
|
+
include Logging
|
7
|
+
|
8
|
+
NOTIFICATIONS = %i[
|
9
|
+
example_failed
|
10
|
+
].freeze
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@failed = 0
|
14
|
+
@ignored = 0
|
15
|
+
@total = 0
|
16
|
+
@examples = Hash.new { |h, k| h[k] = [] }
|
17
|
+
end
|
18
|
+
|
19
|
+
def example_failed(notification)
|
20
|
+
return if notification.example.pending?
|
21
|
+
|
22
|
+
location = notification.example.metadata[:location]
|
23
|
+
|
24
|
+
file, line = location.split(":")
|
25
|
+
|
26
|
+
@examples[file] << line.to_i
|
27
|
+
end
|
28
|
+
|
29
|
+
def stamp!
|
30
|
+
@examples.each do |file, lines|
|
31
|
+
stamp_file(file, lines.uniq)
|
32
|
+
end
|
33
|
+
|
34
|
+
msgs = []
|
35
|
+
|
36
|
+
msgs <<
|
37
|
+
<<~MSG
|
38
|
+
RSpec Stamp results
|
39
|
+
|
40
|
+
Total patches: #{@total}
|
41
|
+
Total files: #{@examples.keys.size}
|
42
|
+
|
43
|
+
Failed patches: #{@failed}
|
44
|
+
Ignored files: #{@ignored}
|
45
|
+
MSG
|
46
|
+
|
47
|
+
log :info, msgs.join
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def stamp_file(file, lines)
|
53
|
+
@total += lines.size
|
54
|
+
return if ignored?(file)
|
55
|
+
|
56
|
+
log :info, "(dry-run) Patching #{file}" if dry_run?
|
57
|
+
|
58
|
+
code = File.readlines(file)
|
59
|
+
|
60
|
+
@failed += RSpecStamp.apply_tags(code, lines, RSpecStamp.config.tags)
|
61
|
+
|
62
|
+
File.write(file, code.join) unless dry_run?
|
63
|
+
end
|
64
|
+
|
65
|
+
def ignored?(file)
|
66
|
+
ignored = RSpecStamp.config.ignore_files.find do |pattern|
|
67
|
+
file =~ pattern
|
68
|
+
end
|
69
|
+
|
70
|
+
return unless ignored
|
71
|
+
log :warn, "Ignore stamping file: #{file}"
|
72
|
+
@ignored += 1
|
73
|
+
end
|
74
|
+
|
75
|
+
def dry_run?
|
76
|
+
RSpecStamp.config.dry_run?
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Register EventProf listener
|
83
|
+
TestProf.activate('RSTAMP') do
|
84
|
+
RSpec.configure do |config|
|
85
|
+
listener = TestProf::RSpecStamp::RSpecListener.new
|
86
|
+
|
87
|
+
config.reporter.register_listener(listener, *TestProf::RSpecStamp::RSpecListener::NOTIFICATIONS)
|
88
|
+
|
89
|
+
config.after(:suite) { listener.stamp! }
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/logging"
|
4
|
+
require "test_prof/rspec_stamp/parser"
|
5
|
+
|
6
|
+
module TestProf
|
7
|
+
# Mark RSpec examples with provided tags
|
8
|
+
module RSpecStamp
|
9
|
+
EXAMPLE_RXP = /(\s*)(\w+\s*(?:.*)\s*)(do|{)/
|
10
|
+
|
11
|
+
# RSpecStamp configuration
|
12
|
+
class Configuration
|
13
|
+
attr_accessor :ignore_files, :dry_run, :tags
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@ignore_files = [%r{spec/support}]
|
17
|
+
@dry_run = ENV['RSTAMP_DRY_RUN'] == '1'
|
18
|
+
self.tags = ENV['RSTAMP']
|
19
|
+
end
|
20
|
+
|
21
|
+
def dry_run?
|
22
|
+
@dry_run == true
|
23
|
+
end
|
24
|
+
|
25
|
+
def tags=(val)
|
26
|
+
@tags = if val.is_a?(String)
|
27
|
+
parse_tags(val)
|
28
|
+
else
|
29
|
+
val
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def parse_tags(str)
|
36
|
+
str.split(/\s*,\s*/).each_with_object([]) do |tag, acc|
|
37
|
+
k, v = tag.split(":")
|
38
|
+
acc << if v.nil?
|
39
|
+
k.to_sym
|
40
|
+
else
|
41
|
+
Hash[k.to_sym, v.to_sym]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class << self
|
48
|
+
include TestProf::Logging
|
49
|
+
|
50
|
+
def config
|
51
|
+
@config ||= Configuration.new
|
52
|
+
end
|
53
|
+
|
54
|
+
def configure
|
55
|
+
yield config
|
56
|
+
end
|
57
|
+
|
58
|
+
# Accepts source code (as array of lines),
|
59
|
+
# line numbers (of example to apply tags)
|
60
|
+
# and an array of tags.
|
61
|
+
def apply_tags(code, lines, tags)
|
62
|
+
failed = 0
|
63
|
+
|
64
|
+
lines.each do |line|
|
65
|
+
unless stamp_example(code[line - 1], tags)
|
66
|
+
failed += 1
|
67
|
+
log :warn, "Failed to stamp: #{code[line - 1]}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
failed
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# rubocop: disable Metrics/CyclomaticComplexity
|
76
|
+
# rubocop: disable Metrics/PerceivedComplexity
|
77
|
+
def stamp_example(example, tags)
|
78
|
+
matches = example.match(EXAMPLE_RXP)
|
79
|
+
return false unless matches
|
80
|
+
|
81
|
+
code = matches[2]
|
82
|
+
block = matches[3]
|
83
|
+
|
84
|
+
parsed = Parser.parse(code)
|
85
|
+
return false unless parsed
|
86
|
+
|
87
|
+
parsed.desc ||= 'works'
|
88
|
+
|
89
|
+
tags.each do |t|
|
90
|
+
if t.is_a?(Hash)
|
91
|
+
t.keys.each { |k| parsed.add_htag(k, t[k]) }
|
92
|
+
else
|
93
|
+
parsed.add_tag(t)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
need_parens = block == "{"
|
98
|
+
|
99
|
+
tags_str = parsed.tags.map { |t| t.is_a?(Symbol) ? ":#{t}" : t }.join(", ") unless
|
100
|
+
parsed.tags.nil?
|
101
|
+
|
102
|
+
unless parsed.htags.nil?
|
103
|
+
htags_str = parsed.htags.map do |(k, v)|
|
104
|
+
vstr = v.is_a?(Symbol) ? ":#{v}" : quote(v)
|
105
|
+
|
106
|
+
"#{k}: #{vstr}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
replacement = "\\1#{parsed.fname}#{need_parens ? '(' : ' '}"\
|
111
|
+
"#{[quote(parsed.desc), tags_str, htags_str].compact.join(', ')}"\
|
112
|
+
"#{need_parens ? ') ' : ' '}\\3"
|
113
|
+
|
114
|
+
if config.dry_run?
|
115
|
+
log :info, "Patched: #{example.sub(EXAMPLE_RXP, replacement)}"
|
116
|
+
else
|
117
|
+
example.sub!(EXAMPLE_RXP, replacement)
|
118
|
+
end
|
119
|
+
true
|
120
|
+
end
|
121
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|
122
|
+
# rubocop: enable Metrics/PerceivedComplexity
|
123
|
+
|
124
|
+
def quote(str)
|
125
|
+
if str.include?("'")
|
126
|
+
"\"#{str}\""
|
127
|
+
else
|
128
|
+
"'#{str}'"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
require "test_prof/rspec_stamp/rspec" if defined?(RSpec)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Shared example for RSpec to profile specific examples with RubyProf
|
4
|
+
RSpec.shared_context "ruby-prof", rprof: true do
|
5
|
+
prepend_before do
|
6
|
+
@ruby_prof_report = TestProf::RubyProf.profile
|
7
|
+
end
|
8
|
+
|
9
|
+
append_after do |ex|
|
10
|
+
next unless @ruby_prof_report
|
11
|
+
@ruby_prof_report.dump ex.full_description.parameterize
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,194 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
# RubyProf wrapper.
|
5
|
+
#
|
6
|
+
# Has 2 modes: global and per-example.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# # To activate global profiling you can use env variable
|
11
|
+
# TEST_RUBY_PROF=1 rspec ...
|
12
|
+
#
|
13
|
+
# # or in your code
|
14
|
+
# TestProf::RubyProf.run
|
15
|
+
#
|
16
|
+
# To profile a specific examples add :rprof tag to it:
|
17
|
+
#
|
18
|
+
# it "is doing heavy stuff", :rprof do
|
19
|
+
# ...
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
module RubyProf
|
23
|
+
# RubyProf configuration
|
24
|
+
class Configuration
|
25
|
+
# Default list of methods to exclude from profile.
|
26
|
+
# Contains a lot of RSpec stuff.
|
27
|
+
ELIMINATE_METHODS = [
|
28
|
+
/instance_exec/,
|
29
|
+
/ExampleGroup>?#run/,
|
30
|
+
/Procsy/,
|
31
|
+
/AroundHook#execute_with/,
|
32
|
+
/HookCollections/,
|
33
|
+
/Array#(map|each)/
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
PRINTERS = {
|
37
|
+
'flat' => 'FlatPrinter',
|
38
|
+
'flat_wln' => 'FlatWithLineNumbers',
|
39
|
+
'graph' => 'GraphPrinter',
|
40
|
+
'graph_html' => 'GraphHtmlPrinter',
|
41
|
+
'dot' => 'DotPrinter',
|
42
|
+
'.' => 'DotPrinter',
|
43
|
+
'call_stack' => 'CallStackPrinter',
|
44
|
+
'call_tree' => 'CallTreePrinter'
|
45
|
+
}.freeze
|
46
|
+
|
47
|
+
attr_accessor :printer, :mode, :min_percent,
|
48
|
+
:include_threads, :eliminate_methods
|
49
|
+
|
50
|
+
def initialize
|
51
|
+
@printer = :call_stack
|
52
|
+
@mode = :wall
|
53
|
+
@min_percent = 1
|
54
|
+
@include_threads = false
|
55
|
+
@eliminate_methods = ELIMINATE_METHODS
|
56
|
+
end
|
57
|
+
|
58
|
+
def include_threads?
|
59
|
+
include_threads == true
|
60
|
+
end
|
61
|
+
|
62
|
+
def eliminate_methods?
|
63
|
+
!eliminate_methods.nil? &&
|
64
|
+
!eliminate_methods.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns an array of printer type (ID) and class.
|
68
|
+
# Takes ENV variable TEST_RUBY_PROF_PRINTER into account.
|
69
|
+
def resolve_printer
|
70
|
+
type = ENV['TEST_RUBY_PROF_PRINTER'] || printer
|
71
|
+
|
72
|
+
return ['custom', type] if type.is_a?(Module)
|
73
|
+
|
74
|
+
type = type.to_s
|
75
|
+
|
76
|
+
raise ArgumentError, "Unknown printer: #{type}" unless
|
77
|
+
PRINTERS.key?(type)
|
78
|
+
|
79
|
+
[type, ::RubyProf.const_get(PRINTERS[type])]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Wrapper over RubyProf profiler and printer
|
84
|
+
class Report
|
85
|
+
include TestProf::Logging
|
86
|
+
|
87
|
+
def initialize(profiler)
|
88
|
+
@profiler = profiler
|
89
|
+
end
|
90
|
+
|
91
|
+
# Stop profiling and generate the report
|
92
|
+
# using provided name.
|
93
|
+
def dump(name)
|
94
|
+
result = @profiler.stop
|
95
|
+
|
96
|
+
if config.eliminate_methods?
|
97
|
+
result.eliminate_methods!(config.eliminate_methods)
|
98
|
+
end
|
99
|
+
|
100
|
+
printer_type, printer_class = config.resolve_printer
|
101
|
+
path = build_path name, printer_type
|
102
|
+
|
103
|
+
File.open(path, 'w') do |f|
|
104
|
+
printer_class.new(result).print(f, min_percent: config.min_percent)
|
105
|
+
end
|
106
|
+
|
107
|
+
log :info, "RubyProf report generated: #{path}"
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def build_path(name, printer)
|
113
|
+
TestProf.artefact_path(
|
114
|
+
"ruby-prof-report-#{printer}-#{config.mode}-#{name}.html"
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
def config
|
119
|
+
RubyProf.config
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
include Logging
|
125
|
+
|
126
|
+
def config
|
127
|
+
@config ||= Configuration.new
|
128
|
+
end
|
129
|
+
|
130
|
+
def configure
|
131
|
+
yield config
|
132
|
+
end
|
133
|
+
|
134
|
+
# Run RubyProf and automatically dump
|
135
|
+
# a report when the process exits.
|
136
|
+
#
|
137
|
+
# Use this method to profile the whole run.
|
138
|
+
def run
|
139
|
+
report = profile
|
140
|
+
|
141
|
+
return unless report
|
142
|
+
|
143
|
+
@locked = true
|
144
|
+
|
145
|
+
log :info, "RubyProf enabled"
|
146
|
+
|
147
|
+
at_exit { report.dump("total") }
|
148
|
+
end
|
149
|
+
|
150
|
+
def profile
|
151
|
+
return if locked?
|
152
|
+
return unless init_ruby_prof
|
153
|
+
|
154
|
+
options = {
|
155
|
+
merge_fibers: true
|
156
|
+
}
|
157
|
+
|
158
|
+
options[:include_threads] = [Thread.current] unless
|
159
|
+
config.include_threads?
|
160
|
+
|
161
|
+
profiler = ::RubyProf::Profile.new(options)
|
162
|
+
profiler.start
|
163
|
+
|
164
|
+
Report.new(profiler)
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def locked?
|
170
|
+
@locked == true
|
171
|
+
end
|
172
|
+
|
173
|
+
def init_ruby_prof
|
174
|
+
return @initialized if instance_variable_defined?(:@initialized)
|
175
|
+
ENV["RUBY_PROF_MEASURE_MODE"] = config.mode.to_s
|
176
|
+
@initialized = TestProf.require(
|
177
|
+
'ruby-prof',
|
178
|
+
<<~MSG
|
179
|
+
Please, install 'ruby-prof' first:
|
180
|
+
# Gemfile
|
181
|
+
gem 'ruby-prof', require: false
|
182
|
+
MSG
|
183
|
+
)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
require "test_prof/ruby_prof/rspec" if defined?(RSpec)
|
190
|
+
|
191
|
+
# Hook to run RubyProf globally
|
192
|
+
TestProf.activate('TEST_RUBY_PROF') do
|
193
|
+
TestProf::RubyProf.run
|
194
|
+
end
|