power_assert 0.4.1 → 1.0.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.
@@ -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