test-prof 0.2.1 → 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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -1
  3. data/README.md +12 -2
  4. data/guides/event_prof.md +12 -0
  5. data/guides/factory_doctor.md +10 -0
  6. data/guides/let_it_be.md +96 -0
  7. data/guides/rspec_dissect.md +58 -0
  8. data/guides/ruby_prof.md +5 -5
  9. data/guides/stack_prof.md +3 -3
  10. data/lib/test_prof/cops/rspec/aggregate_failures.rb +2 -2
  11. data/lib/test_prof/event_prof/custom_events/factory_create.rb +5 -1
  12. data/lib/test_prof/event_prof/custom_events/sidekiq_inline.rb +5 -1
  13. data/lib/test_prof/event_prof/custom_events/sidekiq_jobs.rb +5 -1
  14. data/lib/test_prof/event_prof/rspec.rb +44 -5
  15. data/lib/test_prof/event_prof.rb +38 -25
  16. data/lib/test_prof/ext/array_bsearch_index.rb +15 -0
  17. data/lib/test_prof/ext/float_duration.rb +0 -1
  18. data/lib/test_prof/ext/string_strip_heredoc.rb +15 -0
  19. data/lib/test_prof/ext/string_truncate.rb +19 -0
  20. data/lib/test_prof/factory_doctor/rspec.rb +38 -2
  21. data/lib/test_prof/factory_doctor.rb +12 -4
  22. data/lib/test_prof/factory_prof/printers/simple.rb +4 -1
  23. data/lib/test_prof/recipes/rspec/any_fixture.rb +1 -1
  24. data/lib/test_prof/recipes/rspec/before_all.rb +8 -0
  25. data/lib/test_prof/recipes/rspec/let_it_be.rb +80 -0
  26. data/lib/test_prof/rspec_dissect/rspec.rb +157 -0
  27. data/lib/test_prof/rspec_dissect.rb +112 -0
  28. data/lib/test_prof/rspec_stamp/parser.rb +58 -7
  29. data/lib/test_prof/rspec_stamp/rspec.rb +10 -34
  30. data/lib/test_prof/rspec_stamp.rb +55 -6
  31. data/lib/test_prof/ruby_prof.rb +19 -4
  32. data/lib/test_prof/stack_prof.rb +19 -5
  33. data/lib/test_prof/tag_prof/rspec.rb +5 -3
  34. data/lib/test_prof/utils/sized_ordered_set.rb +65 -0
  35. data/lib/test_prof/utils.rb +18 -0
  36. data/lib/test_prof/version.rb +1 -1
  37. data/lib/test_prof.rb +15 -2
  38. metadata +15 -75
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Ruby 2.3 #bsearch_index method (for usage with older Rubies)
5
+ # Straighforward and non-optimal implementation,
6
+ # just for compatiblity
7
+ module ArrayBSearchIndex
8
+ refine Array do
9
+ def bsearch_index(&block)
10
+ el = bsearch(&block)
11
+ index(el)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Add #duration method to floats
4
3
  module TestProf
5
4
  # Extend Float with #duration method
6
5
  module FloatDuration
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Add #strip_heredoc method to use instead of
5
+ # squiggly docs (to support older Rubies)
6
+ module StringStripHeredoc
7
+ refine String do
8
+ def strip_heredoc
9
+ min = scan(/^[ \t]*(?=\S)/).min
10
+ indent = min ? min.size : 0
11
+ gsub(/^[ \t]{#{indent}}/, '')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestProf
4
+ # Extend String with #truncate method
5
+ module StringTruncate
6
+ refine String do
7
+ # Truncate to the specified limit
8
+ # by replacing middle part with dots
9
+ def truncate(limit = 30)
10
+ return self unless size > limit
11
+
12
+ head = ((limit - 3) / 2)
13
+ tail = head + 3 - limit
14
+
15
+ "#{self[0..(head - 1)]}...#{self[tail..-1]}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,14 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_strip_heredoc"
4
5
 
5
6
  module TestProf
6
7
  module FactoryDoctor
7
8
  class RSpecListener # :nodoc:
8
9
  include Logging
9
10
  using FloatDuration
11
+ using StringStripHeredoc
10
12
 
11
- SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'
13
+ SUCCESS_MESSAGE = 'FactoryDoctor says: "Looks good to me!"'.freeze
12
14
 
13
15
  NOTIFICATIONS = %i[
14
16
  example_started
@@ -49,7 +51,7 @@ module TestProf
49
51
  msgs = []
50
52
 
51
53
  msgs <<
52
- <<~MSG
54
+ <<-MSG.strip_heredoc
53
55
  FactoryDoctor report
54
56
 
55
57
  Total (potentially) bad examples: #{@count}
@@ -68,6 +70,40 @@ module TestProf
68
70
  end
69
71
 
70
72
  log :info, msgs.join
73
+
74
+ stamp! if FactoryDoctor.stamp?
75
+ end
76
+
77
+ def stamp!
78
+ stamper = RSpecStamp::Stamper.new
79
+
80
+ examples = Hash.new { |h, k| h[k] = [] }
81
+
82
+ @example_groups.each_value do |bad_examples|
83
+ bad_examples.each do |example|
84
+ file, line = example.metadata[:location].split(":")
85
+ examples[file] << line.to_i
86
+ end
87
+ end
88
+
89
+ examples.each do |file, lines|
90
+ stamper.stamp_file(file, lines.uniq)
91
+ end
92
+
93
+ msgs = []
94
+
95
+ msgs <<
96
+ <<-MSG.strip_heredoc
97
+ RSpec Stamp results
98
+
99
+ Total patches: #{stamper.total}
100
+ Total files: #{examples.keys.size}
101
+
102
+ Failed patches: #{stamper.failed}
103
+ Ignored files: #{stamper.ignored}
104
+ MSG
105
+
106
+ log :info, msgs.join
71
107
  end
72
108
 
73
109
  private
@@ -16,7 +16,7 @@ module TestProf
16
16
  end
17
17
 
18
18
  def bad?
19
- count.positive? && queries_count.zero?
19
+ count > 0 && queries_count.zero?
20
20
  end
21
21
  end
22
22
 
@@ -54,6 +54,14 @@ module TestProf
54
54
  defined?(::FactoryGirl)
55
55
 
56
56
  subscribe!
57
+
58
+ @stamp = ENV['FDOC_STAMP']
59
+
60
+ RSpecStamp.config.tags = @stamp if stamp?
61
+ end
62
+
63
+ def stamp?
64
+ !@stamp.nil?
57
65
  end
58
66
 
59
67
  def start
@@ -82,14 +90,14 @@ module TestProf
82
90
  return yield if ignore? || !running? || (strategy != :create)
83
91
 
84
92
  begin
85
- ts = Time.now if @depth.zero?
93
+ ts = TestProf.now if @depth.zero?
86
94
  @depth += 1
87
95
  @count += 1
88
96
  yield
89
97
  ensure
90
98
  @depth -= 1
91
99
 
92
- @time += (Time.now - ts) if @depth.zero?
100
+ @time += (TestProf.now - ts) if @depth.zero?
93
101
  end
94
102
  end
95
103
 
@@ -111,7 +119,7 @@ module TestProf
111
119
  end
112
120
 
113
121
  def within_factory?
114
- @depth.positive?
122
+ @depth > 0
115
123
  end
116
124
 
117
125
  def ignore?
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "test_prof/ext/string_strip_heredoc"
4
+
3
5
  module TestProf::FactoryProf
4
6
  module Printers
5
7
  module Simple # :nodoc: all
6
8
  class << self
7
9
  include TestProf::Logging
10
+ using TestProf::StringStripHeredoc
8
11
 
9
12
  def dump(result)
10
13
  msgs = []
11
14
 
12
15
  msgs <<
13
- <<~MSG
16
+ <<-MSG.strip_heredoc
14
17
  Factories usage
15
18
 
16
19
  total top-level name
@@ -12,7 +12,7 @@ RSpec.shared_context "any_fixture:clean", with_clean_fixture: true do
12
12
 
13
13
  def open_transaction?
14
14
  pool = ActiveRecord::Base.connection_pool
15
- pool.active_connection? && pool.connection.open_transactions.positive?
15
+ pool.active_connection? && pool.connection.open_transactions > 0
16
16
  end
17
17
  end
18
18
 
@@ -6,6 +6,10 @@ module TestProf
6
6
  def before_all(&block)
7
7
  raise ArgumentError, "Block is required!" unless block_given?
8
8
 
9
+ return if within_before_all?
10
+
11
+ @__before_all_activated__ = true
12
+
9
13
  before(:all) do
10
14
  ActiveRecord::Base.connection.begin_transaction(joinable: false)
11
15
  instance_eval(&block)
@@ -15,6 +19,10 @@ module TestProf
15
19
  ActiveRecord::Base.connection.rollback_transaction
16
20
  end
17
21
  end
22
+
23
+ def within_before_all?
24
+ instance_variable_defined?(:@__before_all_activated__)
25
+ end
18
26
  end
19
27
  end
20
28
 
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "./before_all"
4
+
5
+ module TestProf
6
+ # Just like `let`, but persist the result for the whole group.
7
+ # NOTE: Experimental and magical, for more control use `before_all`.
8
+ module LetItBe
9
+ class << self
10
+ def module_for(group)
11
+ modules.fetch(group) do
12
+ Module.new.tap { |mod| group.prepend(mod) }
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def modules
19
+ @modules ||= {}
20
+ end
21
+ end
22
+ # Use uniq prefix for instance variables to avoid collisions
23
+ # We want to use the power of Ruby's unicode support)
24
+ # And we love cats!)
25
+ PREFIX = RUBY_ENGINE == 'jruby' ? "@__jruby_is_not_cat_friendly__".freeze : "@😸".freeze
26
+
27
+ def let_it_be(identifier, **options, &block)
28
+ initializer = proc do
29
+ instance_variable_set(:"#{PREFIX}#{identifier}", instance_exec(&block))
30
+ end
31
+
32
+ if within_before_all?
33
+ before(:all, &initializer)
34
+ else
35
+ before_all(&initializer)
36
+ end
37
+
38
+ define_let_it_be_methods(identifier, **options)
39
+ end
40
+
41
+ def define_let_it_be_methods(identifier, reload: false, refind: false)
42
+ let_accessor = -> { instance_variable_get(:"#{PREFIX}#{identifier}") }
43
+
44
+ if reload
45
+ let_accessor = lambda do
46
+ record = instance_variable_get(:"#{PREFIX}#{identifier}")
47
+ next unless record.is_a?(::ActiveRecord::Base)
48
+ record.reload
49
+ end
50
+ end
51
+
52
+ if refind
53
+ let_accessor = lambda do
54
+ record = instance_variable_get(:"#{PREFIX}#{identifier}")
55
+ next unless record.is_a?(::ActiveRecord::Base)
56
+
57
+ record.class.find(record.send(record.class.primary_key))
58
+ end
59
+ end
60
+
61
+ LetItBe.module_for(self).module_eval do
62
+ define_method(identifier) do
63
+ # Trying to detect the context (couldn't find other way so far)
64
+ if @__inspect_output =~ /\(:context\)/
65
+ instance_variable_get(:"#{PREFIX}#{identifier}")
66
+ else
67
+ # Fallback to let definition
68
+ super()
69
+ end
70
+ end
71
+ end
72
+
73
+ let(identifier, &let_accessor)
74
+ end
75
+ end
76
+ end
77
+
78
+ RSpec.configure do |config|
79
+ config.extend TestProf::LetItBe
80
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/ext/float_duration"
4
+ require "test_prof/ext/string_truncate"
5
+ require "test_prof/utils/sized_ordered_set"
6
+ require "test_prof/ext/string_strip_heredoc"
7
+
8
+ module TestProf
9
+ module RSpecDissect
10
+ class Listener # :nodoc:
11
+ include Logging
12
+ using FloatDuration
13
+ using StringTruncate
14
+ using StringStripHeredoc
15
+
16
+ NOTIFICATIONS = %i[
17
+ example_finished
18
+ example_group_finished
19
+ ].freeze
20
+
21
+ def initialize
22
+ @before_results = Utils::SizedOrderedSet.new(
23
+ top_count, sort_by: :before
24
+ )
25
+ @memo_results = Utils::SizedOrderedSet.new(
26
+ top_count, sort_by: :memo
27
+ )
28
+ @examples_count = 0
29
+ @examples_time = 0.0
30
+ @total_examples_time = 0.0
31
+ end
32
+
33
+ def example_finished(notification)
34
+ @examples_count += 1
35
+ @examples_time += notification.example.execution_result.run_time
36
+ end
37
+
38
+ def example_group_finished(notification)
39
+ return unless notification.group.top_level?
40
+
41
+ data = {}
42
+ data[:total] = @examples_time
43
+ data[:count] = @examples_count
44
+ data[:before] = RSpecDissect.before_time
45
+ data[:memo] = RSpecDissect.memo_time
46
+ data[:desc] = notification.group.top_level_description
47
+ data[:loc] = notification.group.metadata[:location]
48
+
49
+ @before_results << data
50
+ @memo_results << data
51
+
52
+ @total_examples_time += @examples_time
53
+ @examples_count = 0
54
+ @examples_time = 0.0
55
+
56
+ RSpecDissect.reset!
57
+ end
58
+
59
+ def print
60
+ msgs = []
61
+
62
+ msgs <<
63
+ <<-MSG.strip_heredoc
64
+ RSpecDissect report
65
+
66
+ Total time: #{@total_examples_time.duration}
67
+ Total `before(:each)` time: #{RSpecDissect.total_before_time.duration}
68
+ Total `let` time: #{RSpecDissect.total_memo_time.duration}
69
+
70
+ MSG
71
+
72
+ msgs <<
73
+ <<-MSG.strip_heredoc
74
+ Top #{top_count} slowest suites (by `before(:each)` time):
75
+
76
+ MSG
77
+
78
+ @before_results.each do |group|
79
+ msgs <<
80
+ <<-GROUP.strip_heredoc
81
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:before].duration} of #{group[:total].duration} (#{group[:count]})
82
+ GROUP
83
+ end
84
+
85
+ msgs <<
86
+ <<-MSG.strip_heredoc
87
+
88
+ Top #{top_count} slowest suites (by `let` time):
89
+
90
+ MSG
91
+
92
+ @memo_results.each do |group|
93
+ msgs <<
94
+ <<-GROUP.strip_heredoc
95
+ #{group[:desc].truncate} (#{group[:loc]}) – #{group[:memo].duration} of #{group[:total].duration} (#{group[:count]})
96
+ GROUP
97
+ end
98
+
99
+ log :info, msgs.join
100
+
101
+ stamp! if RSpecDissect.config.stamp?
102
+ end
103
+
104
+ def stamp!
105
+ stamper = RSpecStamp::Stamper.new
106
+
107
+ examples = Hash.new { |h, k| h[k] = [] }
108
+
109
+ (@before_results.to_a + @memo_results.to_a)
110
+ .map { |obj| obj[:loc] }.each do |location|
111
+ file, line = location.split(":")
112
+ examples[file] << line.to_i
113
+ end
114
+
115
+ examples.each do |file, lines|
116
+ stamper.stamp_file(file, lines.uniq)
117
+ end
118
+
119
+ msgs = []
120
+
121
+ msgs <<
122
+ <<-MSG.strip_heredoc
123
+ RSpec Stamp results
124
+
125
+ Total patches: #{stamper.total}
126
+ Total files: #{examples.keys.size}
127
+
128
+ Failed patches: #{stamper.failed}
129
+ Ignored files: #{stamper.ignored}
130
+ MSG
131
+
132
+ log :info, msgs.join
133
+ end
134
+
135
+ private
136
+
137
+ def top_count
138
+ RSpecDissect.config.top_count
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ # Register RSpecDissect listener
145
+ TestProf.activate('RD_PROF') do
146
+ RSpec.configure do |config|
147
+ listener = TestProf::RSpecDissect::Listener.new
148
+
149
+ config.before(:suite) do
150
+ config.reporter.register_listener(
151
+ listener, *TestProf::RSpecDissect::Listener::NOTIFICATIONS
152
+ )
153
+ end
154
+
155
+ config.after(:suite) { listener.print }
156
+ end
157
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_prof/rspec_stamp"
4
+ require "test_prof/logging"
5
+
6
+ module TestProf
7
+ # RSpecDissect tracks how much time do you spend in `before` hooks
8
+ # and memoization helpers (i.e. `let`) in your tests.
9
+ module RSpecDissect
10
+ module ExampleInstrumentation # :nodoc:
11
+ def run_before_example(*)
12
+ RSpecDissect.track(:before) { super }
13
+ end
14
+ end
15
+
16
+ module MemoizedInstrumentation # :nodoc:
17
+ def fetch_or_store(*)
18
+ res = nil
19
+ Thread.current[:_rspec_dissect_memo_depth] ||= 0
20
+ Thread.current[:_rspec_dissect_memo_depth] += 1
21
+ begin
22
+ res = if Thread.current[:_rspec_dissect_memo_depth] == 1
23
+ RSpecDissect.track(:memo) { super }
24
+ else
25
+ super
26
+ end
27
+ ensure
28
+ Thread.current[:_rspec_dissect_memo_depth] -= 1
29
+ end
30
+ res
31
+ end
32
+ end
33
+
34
+ # RSpecDisect configuration
35
+ class Configuration
36
+ attr_accessor :top_count
37
+
38
+ def initialize
39
+ @top_count = (ENV['RD_PROF_TOP'] || 5).to_i
40
+ @stamp = ENV['RD_PROF_STAMP']
41
+
42
+ RSpecStamp.config.tags = @stamp if stamp?
43
+ end
44
+
45
+ def stamp?
46
+ !@stamp.nil?
47
+ end
48
+ end
49
+
50
+ METRICS = %w[before memo].freeze
51
+
52
+ class << self
53
+ include Logging
54
+
55
+ def config
56
+ @config ||= Configuration.new
57
+ end
58
+
59
+ def configure
60
+ yield config
61
+ end
62
+
63
+ def init
64
+ RSpec::Core::Example.prepend(ExampleInstrumentation)
65
+ RSpec::Core::MemoizedHelpers::ThreadsafeMemoized.prepend(MemoizedInstrumentation)
66
+ RSpec::Core::MemoizedHelpers::NonThreadSafeMemoized.prepend(MemoizedInstrumentation)
67
+
68
+ @data = {}
69
+
70
+ METRICS.each do |type|
71
+ @data["total_#{type}"] = 0.0
72
+ end
73
+
74
+ reset!
75
+
76
+ log :info, "RSpecDissect enabled"
77
+ end
78
+
79
+ def track(type)
80
+ start = TestProf.now
81
+ res = yield
82
+ delta = (TestProf.now - start)
83
+ type = type.to_s
84
+ @data[type] += delta
85
+ @data["total_#{type}"] += delta
86
+ res
87
+ end
88
+
89
+ def reset!
90
+ METRICS.each do |type|
91
+ @data[type.to_s] = 0.0
92
+ end
93
+ end
94
+
95
+ METRICS.each do |type|
96
+ define_method("#{type}_time") do
97
+ @data[type.to_s]
98
+ end
99
+
100
+ define_method("total_#{type}_time") do
101
+ @data["total_#{type}"]
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ require "test_prof/rspec_dissect/rspec" if defined?(RSpec::Core)
109
+
110
+ TestProf.activate('RD_PROF') do
111
+ TestProf::RSpecDissect.init
112
+ end
@@ -2,15 +2,13 @@
2
2
 
3
3
  require "ripper"
4
4
 
5
- # rubocop: disable Metrics/CyclomaticComplexity
6
-
7
5
  module TestProf
8
6
  module RSpecStamp
9
7
  # Parse examples headers
10
8
  module Parser
11
9
  # Contains the result of parsing
12
10
  class Result
13
- attr_accessor :fname, :desc
11
+ attr_accessor :fname, :desc, :desc_const
14
12
  attr_reader :tags, :htags
15
13
 
16
14
  def add_tag(v)
@@ -22,9 +20,16 @@ module TestProf
22
20
  @htags ||= []
23
21
  @htags << [k, v]
24
22
  end
23
+
24
+ def remove_tag(tag)
25
+ @tags.delete(tag) if @tags
26
+ @htags.delete_if { |(k, _v)| k == tag } if @htags
27
+ end
25
28
  end
26
29
 
27
30
  class << self
31
+ # rubocop: disable Metrics/CyclomaticComplexity
32
+ # rubocop: disable Metrics/PerceivedComplexity
28
33
  def parse(code)
29
34
  sexp = Ripper.sexp(code)
30
35
  return unless sexp
@@ -53,11 +58,17 @@ module TestProf
53
58
  res = Result.new
54
59
 
55
60
  fcall = sexp[1][0][1]
56
- fcall = fcall[1] if fcall.first == :fcall
57
- res.fname = fcall[1]
58
-
59
61
  args_block = sexp[1][0][2]
60
62
 
63
+ if fcall.first == :fcall
64
+ fcall = fcall[1]
65
+ elsif fcall.first == :var_ref
66
+ res.fname = [parse_const(fcall), sexp[1][0][3][1]].join(".")
67
+ args_block = sexp[1][0][4]
68
+ end
69
+
70
+ res.fname ||= fcall[1]
71
+
61
72
  return res if args_block.nil?
62
73
 
63
74
  args_block = args_block[1] if args_block.first == :arg_paren
@@ -66,12 +77,16 @@ module TestProf
66
77
 
67
78
  if args.first.first == :string_literal
68
79
  res.desc = parse_literal(args.shift)
80
+ elsif args.first.first == :var_ref || args.first.first == :const_path_ref
81
+ res.desc_const = parse_const(args.shift)
69
82
  end
70
83
 
71
84
  parse_arg(res, args.shift) until args.empty?
72
85
 
73
86
  res
74
87
  end
88
+ # rubocop: enable Metrics/CyclomaticComplexity
89
+ # rubocop: enable Metrics/PerceivedComplexity
75
90
 
76
91
  private
77
92
 
@@ -85,7 +100,24 @@ module TestProf
85
100
 
86
101
  def parse_hash(res, hash_arg)
87
102
  hash_arg.each do |(_, label, val)|
88
- res.add_htag label[1][0..-2].to_sym, parse_literal(val)
103
+ res.add_htag label[1][0..-2].to_sym, parse_value(val)
104
+ end
105
+ end
106
+
107
+ # Expr of the form:
108
+ # bool - [:var_ref, [:@kw, "true", [1, 24]]]
109
+ # string - [:string_literal, [:string_content, [...]]]
110
+ # int - [:@int, "3", [1, 52]]]]
111
+ def parse_value(expr)
112
+ case expr.first
113
+ when :var_ref
114
+ expr[1][1] == "true"
115
+ when :@int
116
+ expr[1].to_i
117
+ when :@float
118
+ expr[1].to_f
119
+ else
120
+ parse_literal(expr)
89
121
  end
90
122
  end
91
123
 
@@ -97,6 +129,25 @@ module TestProf
97
129
  expr[0] == :assoc_new
98
130
  val
99
131
  end
132
+
133
+ # Expr of the form:
134
+ # [:var_ref, [:@const, "User", [1, 9]]]
135
+ #
136
+ # or
137
+ #
138
+ # [:const_path_ref, [:const_path_ref, [:var_ref,
139
+ # [:@const, "User", [1, 17]]],
140
+ # [:@const, "Guest", [1, 23]]],
141
+ # [:@const, "Collection", [1, 30]]
142
+ def parse_const(expr)
143
+ if expr.first == :var_ref
144
+ expr[1][1]
145
+ elsif expr.first == :@const
146
+ expr[1]
147
+ elsif expr.first == :const_path_ref
148
+ expr[1..-1].map(&method(:parse_const)).join("::")
149
+ end
150
+ end
100
151
  end
101
152
  end
102
153
  end