power_assert 0.4.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ warn 'power_assert/colorize are experimental'
2
+
3
+ require 'power_assert/configuration'
4
+
5
+ PowerAssert.configure do |c|
6
+ c.lazy_inspection = true
7
+ c._colorize_message = true
8
+ c._use_pp = true
9
+ end
@@ -1,7 +1,7 @@
1
1
  module PowerAssert
2
2
  class << self
3
3
  def configuration
4
- @configuration ||= Configuration[false, false, true]
4
+ @configuration ||= Configuration[false, false, true, false, false]
5
5
  end
6
6
 
7
7
  def configure
@@ -12,13 +12,35 @@ module PowerAssert
12
12
  SUPPORT_ALIAS_METHOD = TracePoint.public_method_defined?(:callee_id)
13
13
  private_constant :SUPPORT_ALIAS_METHOD
14
14
 
15
- class Configuration < Struct.new(:lazy_inspection, :_trace_alias_method, :_redefinition)
15
+ class Configuration < Struct.new(:lazy_inspection, :_trace_alias_method, :_redefinition, :_colorize_message, :_use_pp)
16
16
  def _trace_alias_method=(bool)
17
17
  super
18
18
  if SUPPORT_ALIAS_METHOD
19
- warn '_trace_alias_method option is obsolete. You no longer have to set it.'
19
+ warn 'power_assert: _trace_alias_method option is obsolete. You no longer have to set it.'
20
20
  end
21
21
  end
22
+
23
+ def _colorize_message=(bool)
24
+ if bool
25
+ require 'pry'
26
+ end
27
+ super
28
+ end
29
+
30
+ def lazy_inspection=(bool)
31
+ unless bool
32
+ raise 'lazy_inspection option must be enabled when using pp' if _use_pp
33
+ end
34
+ super
35
+ end
36
+
37
+ def _use_pp=(bool)
38
+ if bool
39
+ raise 'lazy_inspection option must be enabled when using pp' unless lazy_inspection
40
+ require 'pp'
41
+ end
42
+ super
43
+ end
22
44
  end
23
45
  private_constant :Configuration
24
46
  end
@@ -0,0 +1,201 @@
1
+ require 'power_assert/configuration'
2
+ require 'power_assert/enable_tracepoint_events'
3
+ require 'power_assert/inspector'
4
+ require 'power_assert/parser'
5
+
6
+ module PowerAssert
7
+ class Context
8
+ Value = Struct.new(:name, :value, :column)
9
+
10
+ attr_reader :message_proc
11
+
12
+ def initialize(base_caller_length)
13
+ @fired = false
14
+ @target_thread = Thread.current
15
+ method_id_set = nil
16
+ return_values = []
17
+ trace_alias_method = PowerAssert.configuration._trace_alias_method
18
+ @trace_return = TracePoint.new(:return, :c_return) do |tp|
19
+ method_id_set ||= @parser.method_id_set
20
+ method_id = SUPPORT_ALIAS_METHOD ? tp.callee_id :
21
+ trace_alias_method && tp.event == :return ? tp.binding.eval('::Kernel.__callee__') :
22
+ tp.method_id
23
+ next if ! method_id_set[method_id]
24
+ next if tp.event == :c_return and
25
+ not (@parser.lineno == tp.lineno and @parser.path == tp.path)
26
+ next unless tp.binding # workaround for ruby 2.2
27
+ locs = PowerAssert.app_caller_locations
28
+ diff = locs.length - base_caller_length
29
+ if (tp.event == :c_return && diff == 1 || tp.event == :return && diff <= 2) and Thread.current == @target_thread
30
+ idx = -(base_caller_length + 1)
31
+ if @parser.path == locs[idx].path and @parser.lineno == locs[idx].lineno
32
+ val = PowerAssert.configuration.lazy_inspection ?
33
+ tp.return_value :
34
+ InspectedValue.new(SafeInspectable.new(tp.return_value).inspect)
35
+ return_values << Value[method_id.to_s, val, nil]
36
+ end
37
+ end
38
+ end
39
+ @message_proc = -> {
40
+ raise RuntimeError, 'call #yield or #enable at first' unless fired?
41
+ @message ||= build_assertion_message(@parser.line, @parser.idents, @parser.binding, return_values).freeze
42
+ }
43
+ end
44
+
45
+ def message
46
+ @message_proc.()
47
+ end
48
+
49
+ private
50
+
51
+ def fired?
52
+ @fired
53
+ end
54
+
55
+ def build_assertion_message(line, idents, proc_binding, return_values)
56
+ if PowerAssert.configuration._colorize_message
57
+ line = Pry::Code.new(line).highlighted
58
+ end
59
+
60
+ path = detect_path(idents, return_values)
61
+ return line unless path
62
+
63
+ delete_unidentified_calls(return_values, path)
64
+ methods, refs = path.partition {|i| i.type == :method }
65
+ return_values.zip(methods) do |i, j|
66
+ unless i.name == j.name
67
+ warn "power_assert: [BUG] Failed to get column: #{i.name}"
68
+ return line
69
+ end
70
+ i.column = j.column
71
+ end
72
+ ref_values = refs.map {|i| Value[i.name, proc_binding.eval(i.name), i.column] }
73
+ vals = (return_values + ref_values).find_all(&:column).sort_by(&:column).reverse
74
+ return line if vals.empty?
75
+
76
+ fmt = (0..vals[0].column).map {|i| vals.find {|v| v.column == i } ? "%<#{i}>s" : ' ' }.join
77
+ lines = []
78
+ lines << line.chomp
79
+ lines << sprintf(fmt, vals.each_with_object({}) {|v, h| h[v.column.to_s.to_sym] = '|' }).chomp
80
+ vals.each do |i|
81
+ inspected_val = SafeInspectable.new(Formatter.new(i.value, i.column)).inspect
82
+ inspected_val.each_line do |l|
83
+ map_to = vals.each_with_object({}) do |j, h|
84
+ h[j.column.to_s.to_sym] = [l, '|', ' '][i.column <=> j.column]
85
+ end
86
+ lines << encoding_safe_rstrip(sprintf(fmt, map_to))
87
+ end
88
+ end
89
+ lines.join("\n")
90
+ end
91
+
92
+ def detect_path(idents, return_values)
93
+ return @parser.call_paths.flatten.uniq if @parser.method_id_set.empty?
94
+ all_paths = @parser.call_paths
95
+ return_value_names = return_values.map(&:name)
96
+ uniq_calls = uniq_calls(all_paths)
97
+ uniq_call = return_value_names.find {|i| uniq_calls.include?(i) }
98
+ detected_paths = all_paths.find_all do |path|
99
+ method_names = path.find_all {|ident| ident.type == :method }.map(&:name)
100
+ break [path] if uniq_call and method_names.include?(uniq_call)
101
+ return_value_names == method_names
102
+ end
103
+ return nil unless detected_paths.length == 1
104
+ detected_paths[0]
105
+ end
106
+
107
+ def uniq_calls(paths)
108
+ all_calls = enum_count_by(paths.map {|path| path.find_all {|ident| ident.type == :method }.map(&:name).uniq }.flatten) {|i| i }
109
+ all_calls.find_all {|_, call_count| call_count == 1 }.map {|name, _| name }
110
+ end
111
+
112
+ def delete_unidentified_calls(return_values, path)
113
+ return_value_num_of_calls = enum_count_by(return_values, &:name)
114
+ path_num_of_calls = enum_count_by(path.find_all {|ident| ident.type == :method }, &:name)
115
+ identified_calls = return_value_num_of_calls.find_all {|name, num| path_num_of_calls[name] == num }.map(&:first)
116
+ return_values.delete_if {|val| ! identified_calls.include?(val.name) }
117
+ path.delete_if {|ident| ident.type == :method and ! identified_calls.include?(ident.name) }
118
+ end
119
+
120
+ def enum_count_by(enum, &blk)
121
+ Hash[enum.group_by(&blk).map{|k, v| [k, v.length] }]
122
+ end
123
+
124
+ def encoding_safe_rstrip(str)
125
+ str.rstrip
126
+ rescue ArgumentError, Encoding::CompatibilityError
127
+ enc = str.encoding
128
+ if enc.ascii_compatible?
129
+ str.b.rstrip.force_encoding(enc)
130
+ else
131
+ str
132
+ end
133
+ end
134
+ end
135
+ private_constant :Context
136
+
137
+ class BlockContext < Context
138
+ def initialize(assertion_proc_or_source, assertion_method, source_binding)
139
+ super(0)
140
+ if assertion_proc_or_source.respond_to?(:to_proc)
141
+ @assertion_proc = assertion_proc_or_source.to_proc
142
+ line = nil
143
+ else
144
+ @assertion_proc = source_binding.eval "Proc.new {#{assertion_proc_or_source}}"
145
+ line = assertion_proc_or_source
146
+ end
147
+ @parser = Parser::DUMMY
148
+ @trace_call = TracePoint.new(:call, :c_call) do |tp|
149
+ if PowerAssert.app_context? and Thread.current == @target_thread
150
+ @trace_call.disable
151
+ locs = PowerAssert.app_caller_locations
152
+ path = locs.last.path
153
+ lineno = locs.last.lineno
154
+ line ||= open(path).each_line.drop(lineno - 1).first
155
+ @parser = Parser.new(line, path, lineno, @assertion_proc.binding, assertion_method.to_s)
156
+ end
157
+ end
158
+ end
159
+
160
+ def yield
161
+ @fired = true
162
+ invoke_yield(&@assertion_proc)
163
+ end
164
+
165
+ private
166
+
167
+ def invoke_yield
168
+ @trace_return.enable do
169
+ @trace_call.enable do
170
+ yield
171
+ end
172
+ end
173
+ end
174
+ end
175
+ private_constant :BlockContext
176
+
177
+ class TraceContext < Context
178
+ def initialize(binding)
179
+ target_frame, *base = PowerAssert.app_caller_locations
180
+ super(base.length)
181
+ path = target_frame.path
182
+ lineno = target_frame.lineno
183
+ line = open(path).each_line.drop(lineno - 1).first
184
+ @parser = Parser.new(line, path, lineno, binding)
185
+ end
186
+
187
+ def enable
188
+ @fired = true
189
+ @trace_return.enable
190
+ end
191
+
192
+ def disable
193
+ @trace_return.disable
194
+ end
195
+
196
+ def enabled?
197
+ @trace_return.enabled?
198
+ end
199
+ end
200
+ private_constant :TraceContext
201
+ end
@@ -1,7 +1,12 @@
1
1
  require 'power_assert/configuration'
2
2
 
3
- if defined? RubyVM
3
+ if defined?(RubyVM)
4
4
  if PowerAssert.configuration._redefinition
5
+ if RUBY_VERSION == '2.3.2'
6
+ warn 'power_assert: It is strongly recommended that you use Ruby 2.3.3 or later which fixes regression on 2.3.2.'
7
+ warn 'power_assert: See https://www.ruby-lang.org/en/news/2016/11/21/ruby-2-3-3-released/ for more details.'
8
+ end
9
+
5
10
  verbose = $VERBOSE
6
11
  begin
7
12
  $VERBOSE = nil
@@ -0,0 +1,61 @@
1
+ require 'power_assert/configuration'
2
+
3
+ module PowerAssert
4
+ class InspectedValue
5
+ def initialize(value)
6
+ @value = value
7
+ end
8
+
9
+ def inspect
10
+ @value
11
+ end
12
+ end
13
+ private_constant :InspectedValue
14
+
15
+ class SafeInspectable
16
+ def initialize(value)
17
+ @value = value
18
+ end
19
+
20
+ def inspect
21
+ inspected = @value.inspect
22
+ if Encoding.compatible?(Encoding.default_external, inspected)
23
+ inspected
24
+ else
25
+ begin
26
+ "#{inspected.encode(Encoding.default_external)}(#{inspected.encoding})"
27
+ rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError
28
+ inspected.force_encoding(Encoding.default_external)
29
+ end
30
+ end
31
+ rescue => e
32
+ "InspectionFailure: #{e.class}: #{e.message.each_line.first}"
33
+ end
34
+ end
35
+ private_constant :SafeInspectable
36
+
37
+ class Formatter
38
+ def initialize(value, indent)
39
+ @value = value
40
+ @indent = indent
41
+ end
42
+
43
+ def inspect
44
+ if PowerAssert.configuration._colorize_message
45
+ if PowerAssert.configuration._use_pp
46
+ width = [Pry::Terminal.width! - 1 - @indent, 10].max
47
+ Pry::ColorPrinter.pp(@value, '', width)
48
+ else
49
+ Pry::Code.new(@value.inspect).highlighted
50
+ end
51
+ else
52
+ if PowerAssert.configuration._use_pp
53
+ PP.pp(@value, '')
54
+ else
55
+ @value.inspect
56
+ end
57
+ end
58
+ end
59
+ end
60
+ private_constant :Formatter
61
+ end
@@ -0,0 +1,239 @@
1
+ require 'ripper'
2
+
3
+ module PowerAssert
4
+ class Parser
5
+ Ident = Struct.new(:type, :name, :column)
6
+
7
+ attr_reader :line, :path, :lineno, :binding
8
+
9
+ def initialize(line, path, lineno, binding, assertion_method_name = nil)
10
+ @line = line
11
+ @line_for_parsing = valid_syntax?(line) ? line : slice_expression(line)
12
+ @path = path
13
+ @lineno = lineno
14
+ @binding = binding
15
+ @proc_local_variables = binding.eval('local_variables').map(&:to_s)
16
+ @assertion_method_name = assertion_method_name
17
+ end
18
+
19
+ def idents
20
+ @idents ||= extract_idents(Ripper.sexp(@line_for_parsing))
21
+ end
22
+
23
+ def call_paths
24
+ collect_paths(idents).uniq
25
+ end
26
+
27
+ def method_id_set
28
+ methods = idents.flatten.find_all {|i| i.type == :method }
29
+ @method_id_set ||= methods.map(&:name).map(&:to_sym).each_with_object({}) {|i, h| h[i] = true }
30
+ end
31
+
32
+ private
33
+
34
+ def valid_syntax?(str)
35
+ return true unless defined?(RubyVM)
36
+ begin
37
+ verbose, $VERBOSE = $VERBOSE, nil
38
+ RubyVM::InstructionSequence.compile(str)
39
+ true
40
+ rescue SyntaxError
41
+ false
42
+ ensure
43
+ $VERBOSE = verbose
44
+ end
45
+ end
46
+
47
+ def slice_expression(str)
48
+ str = str.chomp
49
+ str.sub!(/\A\s*(?:if|unless|elsif|case|while|until) /) {|i| ' ' * i.length }
50
+ str.sub!(/\A\s*(?:\}|\]|end)?\./) {|i| ' ' * i.length }
51
+ str.sub!(/[\{\.\\]\z/, '')
52
+ str.sub!(/(?:&&|\|\|)\z/, '')
53
+ str.sub!(/ (?:do|and|or)\z/, '')
54
+ str
55
+ end
56
+
57
+ class Branch < Array
58
+ end
59
+
60
+ AND_OR_OPS = %i(and or && ||)
61
+
62
+ #
63
+ # Returns idents as graph structure.
64
+ #
65
+ # +--c--b--+
66
+ # extract_idents(Ripper.sexp('a&.b(c).d')) #=> a--+ +--d
67
+ # +--------+
68
+ #
69
+ def extract_idents(sexp)
70
+ tag, * = sexp
71
+ case tag
72
+ when :arg_paren, :assoc_splat, :fcall, :hash, :method_add_block, :string_literal, :return
73
+ extract_idents(sexp[1])
74
+ when :assign, :massign
75
+ extract_idents(sexp[2])
76
+ when :opassign
77
+ _, _, (_, op_name, (_, op_column)), s0 = sexp
78
+ extract_idents(s0) + [Ident[:method, op_name.sub(/=\z/, ''), op_column]]
79
+ when :assoclist_from_args, :bare_assoc_hash, :dyna_symbol, :paren, :string_embexpr,
80
+ :regexp_literal, :xstring_literal
81
+ sexp[1].flat_map {|s| extract_idents(s) }
82
+ when :command
83
+ [sexp[2], sexp[1]].flat_map {|s| extract_idents(s) }
84
+ when :assoc_new, :dot2, :dot3, :string_content
85
+ sexp[1..-1].flat_map {|s| extract_idents(s) }
86
+ when :unary
87
+ handle_columnless_ident([], sexp[1], extract_idents(sexp[2]))
88
+ when :binary
89
+ op = sexp[2]
90
+ if AND_OR_OPS.include?(op)
91
+ extract_idents(sexp[1]) + [Branch[extract_idents(sexp[3]), []]]
92
+ else
93
+ handle_columnless_ident(extract_idents(sexp[1]), op, extract_idents(sexp[3]))
94
+ end
95
+ when :call
96
+ with_safe_op = sexp[2] == :"&."
97
+ if sexp[3] == :call
98
+ handle_columnless_ident(extract_idents(sexp[1]), :call, [], with_safe_op)
99
+ else
100
+ extract_idents(sexp[1]) + (with_safe_op ? [Branch[extract_idents(sexp[3]), []]] : extract_idents(sexp[3]))
101
+ end
102
+ when :array
103
+ sexp[1] ? sexp[1].flat_map {|s| extract_idents(s) } : []
104
+ when :command_call
105
+ [sexp[1], sexp[4], sexp[3]].flat_map {|s| extract_idents(s) }
106
+ when :aref
107
+ handle_columnless_ident(extract_idents(sexp[1]), :[], extract_idents(sexp[2]))
108
+ when :method_add_arg
109
+ idents = extract_idents(sexp[1])
110
+ if idents.empty?
111
+ # idents may be empty(e.g. ->{}.())
112
+ extract_idents(sexp[2])
113
+ else
114
+ if idents[-1].kind_of?(Branch) and idents[-1][1].empty?
115
+ # Safe navigation operator is used. See :call clause also.
116
+ idents[0..-2] + [Branch[extract_idents(sexp[2]) + idents[-1][0], []]]
117
+ else
118
+ idents[0..-2] + extract_idents(sexp[2]) + [idents[-1]]
119
+ end
120
+ end
121
+ when :args_add_block
122
+ _, (tag, ss0, *ss1), _ = sexp
123
+ if tag == :args_add_star
124
+ (ss0 + ss1).flat_map {|s| extract_idents(s) }
125
+ else
126
+ sexp[1].flat_map {|s| extract_idents(s) }
127
+ end
128
+ when :vcall
129
+ _, (tag, name, (_, column)) = sexp
130
+ if tag == :@ident
131
+ [Ident[@proc_local_variables.include?(name) ? :ref : :method, name, column]]
132
+ else
133
+ []
134
+ end
135
+ when :program
136
+ _, ((tag0, (tag1, (tag2, (tag3, mname, _)), _), (tag4, _, ss))) = sexp
137
+ if tag0 == :method_add_block and tag1 == :method_add_arg and tag2 == :fcall and
138
+ (tag3 == :@ident or tag3 == :@const) and mname == @assertion_method_name and (tag4 == :brace_block or tag4 == :do_block)
139
+ ss.flat_map {|s| extract_idents(s) }
140
+ else
141
+ _, (s, *) = sexp
142
+ extract_idents(s)
143
+ end
144
+ when :ifop
145
+ _, s0, s1, s2 = sexp
146
+ [*extract_idents(s0), Branch[extract_idents(s1), extract_idents(s2)]]
147
+ when :if_mod, :unless_mod
148
+ _, s0, s1 = sexp
149
+ [*extract_idents(s0), Branch[extract_idents(s1), []]]
150
+ when :var_ref, :var_field
151
+ _, (tag, ref_name, (_, column)) = sexp
152
+ case tag
153
+ when :@kw
154
+ if ref_name == 'self'
155
+ [Ident[:ref, 'self', column]]
156
+ else
157
+ []
158
+ end
159
+ when :@ident, :@const, :@cvar, :@ivar, :@gvar
160
+ [Ident[:ref, ref_name, column]]
161
+ else
162
+ []
163
+ end
164
+ when :@ident, :@const, :@op
165
+ _, method_name, (_, column) = sexp
166
+ [Ident[:method, method_name, column]]
167
+ else
168
+ []
169
+ end
170
+ end
171
+
172
+ def str_indices(str, re, offset, limit)
173
+ idx = str.index(re, offset)
174
+ if idx and idx <= limit
175
+ [idx, *str_indices(str, re, idx + 1, limit)]
176
+ else
177
+ []
178
+ end
179
+ end
180
+
181
+ MID2SRCTXT = {
182
+ :[] => '[',
183
+ :+@ => '+',
184
+ :-@ => '-',
185
+ :call => '('
186
+ }
187
+
188
+ def handle_columnless_ident(left_idents, mid, right_idents, with_safe_op = false)
189
+ left_max = left_idents.flatten.max_by(&:column)
190
+ right_min = right_idents.flatten.min_by(&:column)
191
+ bg = left_max ? left_max.column + left_max.name.length : 0
192
+ ed = right_min ? right_min.column - 1 : @line_for_parsing.length - 1
193
+ mname = mid.to_s
194
+ srctxt = MID2SRCTXT[mid] || mname
195
+ re = /
196
+ #{'\b' if /\A\w/ =~ srctxt}
197
+ #{Regexp.escape(srctxt)}
198
+ #{'\b' if /\w\z/ =~ srctxt}
199
+ /x
200
+ indices = str_indices(@line_for_parsing, re, bg, ed)
201
+ if indices.length == 1 or !(right_idents.empty? and left_idents.empty?)
202
+ ident = Ident[:method, mname, right_idents.empty? ? indices.first : indices.last]
203
+ left_idents + right_idents + (with_safe_op ? [Branch[[ident], []]] : [ident])
204
+ else
205
+ left_idents + right_idents
206
+ end
207
+ end
208
+
209
+ def collect_paths(idents, prefixes = [[]], index = 0)
210
+ if index < idents.length
211
+ node = idents[index]
212
+ if node.kind_of?(Branch)
213
+ prefixes = node.flat_map {|n| collect_paths(n, prefixes, 0) }
214
+ else
215
+ prefixes = prefixes.empty? ? [[node]] : prefixes.map {|prefix| prefix + [node] }
216
+ end
217
+ collect_paths(idents, prefixes, index + 1)
218
+ else
219
+ prefixes
220
+ end
221
+ end
222
+
223
+ class DummyParser < Parser
224
+ def initialize
225
+ super('', nil, nil, TOPLEVEL_BINDING)
226
+ end
227
+
228
+ def idents
229
+ []
230
+ end
231
+
232
+ def call_paths
233
+ []
234
+ end
235
+ end
236
+ DUMMY = DummyParser.new
237
+ end
238
+ private_constant :Parser
239
+ end