seeing_is_believing 3.3.0 → 4.0.1

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 (46) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +39 -0
  3. data/{Readme.md → README.md} +9 -12
  4. data/Rakefile +12 -23
  5. data/appveyor.yml +3 -1
  6. data/features/flags.feature +251 -1
  7. data/features/regression.feature +166 -23
  8. data/features/xmpfilter-style.feature +1 -0
  9. data/lib/seeing_is_believing.rb +5 -2
  10. data/lib/seeing_is_believing/binary.rb +6 -1
  11. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +1 -1
  12. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +8 -5
  13. data/lib/seeing_is_believing/binary/comment_lines.rb +2 -2
  14. data/lib/seeing_is_believing/binary/commentable_lines.rb +7 -7
  15. data/lib/seeing_is_believing/binary/config.rb +21 -4
  16. data/lib/seeing_is_believing/binary/engine.rb +58 -1
  17. data/lib/seeing_is_believing/binary/format_comment.rb +9 -6
  18. data/lib/seeing_is_believing/binary/remove_annotations.rb +1 -1
  19. data/lib/seeing_is_believing/code.rb +5 -5
  20. data/lib/seeing_is_believing/compatibility.rb +28 -0
  21. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +52 -71
  22. data/lib/seeing_is_believing/event_stream/consumer.rb +26 -28
  23. data/lib/seeing_is_believing/event_stream/events.rb +7 -0
  24. data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +3 -3
  25. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +2 -1
  26. data/lib/seeing_is_believing/event_stream/producer.rb +29 -5
  27. data/lib/seeing_is_believing/safe.rb +8 -7
  28. data/lib/seeing_is_believing/swap_files.rb +90 -0
  29. data/lib/seeing_is_believing/the_matrix.rb +17 -4
  30. data/lib/seeing_is_believing/version.rb +1 -1
  31. data/lib/seeing_is_believing/wrap_expressions.rb +27 -13
  32. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +4 -1
  33. data/seeing_is_believing.gemspec +4 -6
  34. data/spec/binary/config_spec.rb +63 -35
  35. data/spec/binary/engine_spec.rb +11 -1
  36. data/spec/binary/format_comment_spec.rb +5 -1
  37. data/spec/evaluate_by_moving_files_spec.rb +35 -18
  38. data/spec/event_stream_spec.rb +15 -0
  39. data/spec/seeing_is_believing_spec.rb +148 -26
  40. data/spec/sib_spec_helpers/version.rb +17 -0
  41. data/spec/spec_helper.rb +2 -18
  42. data/spec/wrap_expressions_spec.rb +9 -2
  43. metadata +31 -31
  44. data/.travis.yml +0 -10
  45. data/lib/seeing_is_believing/customize_pp.rb +0 -5
  46. data/spec/binary/options_spec.rb +0 -0
@@ -4,6 +4,10 @@ require 'seeing_is_believing/event_stream/events'
4
4
  require 'seeing_is_believing/error'
5
5
  require 'thread'
6
6
 
7
+ # Polyfill String#scrub on Ruby 2.0.0
8
+ require 'seeing_is_believing/compatibility'
9
+ using SeeingIsBelieving::Compatibility
10
+
7
11
  class SeeingIsBelieving
8
12
  module EventStream
9
13
  class Consumer
@@ -60,15 +64,7 @@ class SeeingIsBelieving
60
64
  rescue EncodingError
61
65
  str = str.force_encoding(Encoding::UTF_8)
62
66
  end
63
- return str.scrub('�') if str.respond_to? :scrub
64
- # basically reimplement scrub, b/c it's not implemented on 2.0.0
65
- str.each_char.inject("") do |new_str, char|
66
- if char.valid_encoding?
67
- new_str << char
68
- else
69
- new_str << '�'
70
- end
71
- end
67
+ str.scrub('�')
72
68
  end
73
69
 
74
70
  def initialize(streams)
@@ -169,51 +165,53 @@ class SeeingIsBelieving
169
165
  end
170
166
  end
171
167
 
172
- def extract_token(line)
168
+ def shift_token(line)
173
169
  event_name = line[/[^ ]+/]
174
170
  line.sub!(/^\s*[^ ]+\s*/, '')
175
171
  event_name
176
172
  end
177
173
 
178
174
  # For a consideration of many different ways of passing the message, see 5633064
179
- def extract_string(line)
180
- str = Marshal.load extract_token(line).unpack('m0').first
175
+ def shift_string(line)
176
+ str = Marshal.load shift_token(line).unpack('m0').first
181
177
  Consumer.fix_encoding(str)
182
178
  end
183
179
 
184
180
  def event_for(original_line)
185
181
  line = original_line.chomp
186
- event_name = extract_token(line).intern
182
+ event_name = shift_token(line).intern
187
183
  case event_name
188
184
  when :result
189
- line_number = extract_token(line).to_i
190
- type = extract_token(line).intern
191
- inspected = extract_string(line)
185
+ line_number = shift_token(line).to_i
186
+ type = shift_token(line).intern
187
+ inspected = shift_string(line)
192
188
  Events::LineResult.new(type: type, line_number: line_number, inspected: inspected)
193
189
  when :maxed_result
194
- line_number = extract_token(line).to_i
195
- type = extract_token(line).intern
190
+ line_number = shift_token(line).to_i
191
+ type = shift_token(line).intern
196
192
  Events::ResultsTruncated.new(type: type, line_number: line_number)
197
193
  when :exception
198
194
  Events::Exception.new \
199
- line_number: extract_token(line).to_i,
200
- class_name: extract_string(line),
201
- message: extract_string(line),
202
- backtrace: extract_token(line).to_i.times.map { extract_string line }
195
+ line_number: shift_token(line).to_i,
196
+ class_name: shift_string(line),
197
+ message: shift_string(line),
198
+ backtrace: shift_token(line).to_i.times.map { shift_string line }
203
199
  when :max_line_captures
204
- token = extract_token(line)
200
+ token = shift_token(line)
205
201
  value = token =~ /infinity/i ? Float::INFINITY : token.to_i
206
202
  Events::MaxLineCaptures.new(value: value)
207
203
  when :num_lines
208
- Events::NumLines.new(value: extract_token(line).to_i)
204
+ Events::NumLines.new(value: shift_token(line).to_i)
209
205
  when :sib_version
210
- Events::SiBVersion.new(value: extract_string(line))
206
+ Events::SiBVersion.new(value: shift_string(line))
211
207
  when :ruby_version
212
- Events::RubyVersion.new(value: extract_string(line))
208
+ Events::RubyVersion.new(value: shift_string(line))
213
209
  when :filename
214
- Events::Filename.new(value: extract_string(line))
210
+ Events::Filename.new(value: shift_string(line))
211
+ when :file_loaded
212
+ Events::FileLoaded.new
215
213
  when :exec
216
- Events::Exec.new(args: extract_string(line))
214
+ Events::Exec.new(args: shift_string(line))
217
215
  else
218
216
  raise UnknownEvent, original_line.inspect
219
217
  end
@@ -58,6 +58,13 @@ class SeeingIsBelieving
58
58
  attributes :value
59
59
  end
60
60
 
61
+ # For knowing when you can perform certain tasks (eg moving the file back)
62
+ class FileLoaded < Event
63
+ def self.event_name
64
+ :file_loaded
65
+ end
66
+ end
67
+
61
68
  # Number of lines in the program.
62
69
  class NumLines < Event
63
70
  def self.event_name
@@ -7,8 +7,8 @@ class SeeingIsBelieving
7
7
  attr_reader :exitstatus
8
8
  attr_reader :timeout_seconds
9
9
 
10
- def initialize(next_observer)
11
- @next_observer = next_observer
10
+ def initialize(next_handler)
11
+ @next_handler = next_handler
12
12
  end
13
13
 
14
14
  def call(event)
@@ -18,7 +18,7 @@ class SeeingIsBelieving
18
18
  when Events::Timeout
19
19
  @timeout_seconds = event.seconds
20
20
  end
21
- @next_observer.call(event)
21
+ @next_handler.call(event)
22
22
  end
23
23
  end
24
24
  end
@@ -29,7 +29,8 @@ class SeeingIsBelieving
29
29
  Finished,
30
30
  StdoutClosed,
31
31
  StderrClosed,
32
- EventStreamClosed
32
+ EventStreamClosed,
33
+ FileLoaded
33
34
  # no op
34
35
  else raise "Unknown event: #{event.inspect}"
35
36
  end
@@ -7,6 +7,17 @@ using SeeingIsBelieving::Safe
7
7
  class SeeingIsBelieving
8
8
  module EventStream
9
9
  class Producer
10
+
11
+ # Guarding against hostile users (e.g. me) that do ridiculous things like blowing away these constants
12
+ old_w, $-w = $-w, nil # Ruby warns about accessing deprecated constants
13
+ Object.constants.each do |name|
14
+ next if name == :SortedSet # Removed in 3.0, but apparently the constant still exists, it just explodes if you reference it
15
+ const_set name, Object.const_get(name)
16
+ end
17
+ $-w = old_w
18
+
19
+ ErrnoEPIPE = Errno::EPIPE # not actually tested, but we can see it is referenced below
20
+
10
21
  module NullQueue
11
22
  extend self
12
23
  Queue.instance_methods(false).each do |name|
@@ -40,6 +51,10 @@ class SeeingIsBelieving
40
51
  queue << "max_line_captures #{max_line_captures}"
41
52
  end
42
53
 
54
+ def file_loaded
55
+ queue << "file_loaded"
56
+ end
57
+
43
58
  StackErrors = [SystemStackError]
44
59
  StackErrors << Java::JavaLang::StackOverflowError if defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java'
45
60
  def record_result(type, line_number, value)
@@ -49,9 +64,13 @@ class SeeingIsBelieving
49
64
  if count < max_line_captures
50
65
  begin
51
66
  if block_given?
52
- inspected = yield(value).to_str
67
+ inspected = yield(value)
53
68
  else
54
- inspected = value.inspect.to_str
69
+ inspected = value.inspect
70
+ end
71
+ unless String === inspected
72
+ inspected = inspected.to_str
73
+ raise unless String === inspected
55
74
  end
56
75
  rescue *StackErrors
57
76
  # this is necessary because SystemStackError won't show the backtrace of the method we tried to call
@@ -76,9 +95,11 @@ class SeeingIsBelieving
76
95
  # records the exception, returns the exitstatus for that exception
77
96
  def record_exception(line_number, exception)
78
97
  return exception.status if SystemExit === exception # TODO === is not in the list
79
- if !line_number && filename
80
- begin line_number = exception.backtrace.grep(/#{filename.to_s}/).first[/:\d+/][1..-1].to_i
81
- rescue NoMethodError
98
+ unless line_number
99
+ if filename
100
+ begin line_number = exception.backtrace.grep(/#{filename.to_s}/).first[/:\d+/][1..-1].to_i
101
+ rescue NoMethodError
102
+ end
82
103
  end
83
104
  end
84
105
  line_number ||= -1
@@ -118,6 +139,9 @@ class SeeingIsBelieving
118
139
  # for a consideration of many different ways of doing this, see 5633064
119
140
  def to_string_token(string)
120
141
  [Marshal.dump(string.to_s)].pack('m0')
142
+ rescue TypeError => err
143
+ raise unless err.message =~ /singleton can't be dumped/
144
+ to_string_token string.to_s.dup
121
145
  end
122
146
 
123
147
  def build_producer_thread(resultstream)
@@ -1,6 +1,13 @@
1
1
  # require this before anything else, b/c it expects the world to be sane when it is loaded
2
2
  class SeeingIsBelieving
3
3
  module Safe
4
+
5
+ # Subclasses must refine before superclasses in older Rubies, otherwise
6
+ # it finds the superclass method and behaves unexpectedly.
7
+ refine String.singleton_class do
8
+ alias === ===
9
+ end
10
+
4
11
  refine Class do
5
12
  alias === ===
6
13
  end
@@ -43,13 +50,7 @@ class SeeingIsBelieving
43
50
  alias to_str to_str
44
51
  end
45
52
 
46
- # in 2.4 we should use Integer instead, but it's not obvious to me how
47
- # to detect this. eg defined?(Fixnum) returns "constant". Accessing it
48
- # leads to a warning, but SiB turns warnings off so you don't see it.
49
- # So.... for now, it incidentally doesn't do anything annoying, but would
50
- # be good to figure out something better (eg if we ever wanted to allow the
51
- # user to decide whether warnings should display or not)
52
- refine Fixnum do
53
+ refine Integer do
53
54
  alias to_s to_s
54
55
  alias next next
55
56
  alias < <
@@ -0,0 +1,90 @@
1
+ require 'seeing_is_believing/error'
2
+ require 'seeing_is_believing/hard_core_ensure'
3
+
4
+ class SeeingIsBelieving
5
+ class SwapFiles
6
+ # Might honeslty make more sense to break this out into 2 different classes.
7
+ # We've got to do some confusing state accounting since there are really 2
8
+ # algorithms here:
9
+ #
10
+ # if the file exists:
11
+ # make sure there isn't a backup (could cause the user to lose their file)
12
+ # back the file up
13
+ # write the rewritten code to the file
14
+ # if we are told to show the user program
15
+ # move the backup over the top of the rewritten file
16
+ # do nothing in the ensure block
17
+ # else
18
+ # in the ensure block: move the backup over the top of the rewritten file
19
+ # if the file DNE:
20
+ # write the rewritten code to the file
21
+ # if we are told to show the user program
22
+ # write the user program to the file
23
+ # delete the file in the ensure block
24
+ def self.call(*args, &block)
25
+ new(*args, &block).call
26
+ end
27
+
28
+ def initialize(file_path, backup_path, user_program, rewritten_program, &block)
29
+ self.file_path = file_path
30
+ self.block = block
31
+ self.backup_path = backup_path
32
+ self.user_program = user_program
33
+ self.rewritten_program = rewritten_program
34
+ end
35
+
36
+ def call
37
+ HardCoreEnsure.call \
38
+ code: -> {
39
+ File.exist? backup_path and
40
+ raise TempFileAlreadyExists.new(file_path, backup_path)
41
+
42
+ @has_file = File.exist? file_path
43
+
44
+ if @has_file
45
+ File.rename file_path, backup_path
46
+ @needs_restore = true
47
+ end
48
+
49
+ save_file rewritten_program
50
+
51
+ block.call self
52
+ },
53
+ ensure: -> {
54
+ set_back_to_initial_conditions
55
+ }
56
+ end
57
+
58
+ def show_user_program
59
+ if @needs_restore
60
+ restore
61
+ else
62
+ save_file user_program
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ attr_accessor :block, :file_path, :backup_path, :rewritten_program, :user_program
69
+
70
+ def restore
71
+ File.rename(backup_path, file_path)
72
+ @needs_restore = false
73
+ end
74
+
75
+
76
+ def save_file(program)
77
+ File.open file_path, 'w', external_encoding: "utf-8" do |f|
78
+ f.write program.to_s
79
+ end
80
+ end
81
+
82
+ def set_back_to_initial_conditions
83
+ if @needs_restore
84
+ restore
85
+ elsif !@has_file
86
+ File.delete(file_path)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -48,23 +48,32 @@ Kernel.module_eval do
48
48
  private
49
49
 
50
50
  define_method :warn do |*args, &block|
51
- $stderr.puts *args
51
+ $stderr.puts(*args)
52
52
  end
53
53
 
54
+ alias :exec :exec # disable warning
54
55
  define_method :exec do |*args, &block|
55
56
  $SiB.record_exec(args)
56
57
  finish.call
57
58
  real_exec.call(*args, &block)
58
59
  end
59
60
 
61
+ alias :exit! :exit! # disable warning
60
62
  define_method :exit! do |status=false|
61
63
  finish.call
62
64
  real_exit_bang.call(status)
63
65
  end
64
66
 
67
+ alias :fork :fork
65
68
  define_method :fork, &fork_defn
66
69
  end
67
70
 
71
+ module Process
72
+ class << self
73
+ alias :fork :fork
74
+ end
75
+ end
76
+
68
77
  Kernel.define_singleton_method :fork, &fork_defn
69
78
  Process.define_singleton_method :fork, &fork_defn
70
79
 
@@ -74,10 +83,14 @@ symbol_to_s = Symbol.instance_method(:to_s)
74
83
  exception_message = Exception.instance_method(:message)
75
84
  exception_backtrace = Exception.instance_method(:backtrace)
76
85
 
86
+ # Guarding against hostile users (e.g. me) that do ridiculous things like blowing away these constants
87
+ exception_class = Exception
88
+ symbol_class = Symbol
89
+
77
90
  at_exit do
78
- Exception.class_eval { define_method :message, exception_message }
79
- Exception.class_eval { define_method :backtrace, exception_backtrace }
80
- Symbol.class_eval { define_method :to_s, symbol_to_s }
91
+ exception_class.class_eval { define_method :message, exception_message }
92
+ exception_class.class_eval { define_method :backtrace, exception_backtrace }
93
+ symbol_class.class_eval { define_method :to_s, symbol_to_s }
81
94
  exitstatus = ($! ? $SiB.record_exception(nil, $!) : 0)
82
95
  finish.call
83
96
  real_exit_bang.call(exitstatus) # clears exceptions so they don't print to stderr and change the processes actual exit status (we recorded what it should be)
@@ -1,3 +1,3 @@
1
1
  class SeeingIsBelieving
2
- VERSION = '3.3.0'
2
+ VERSION = '4.0.1'
3
3
  end
@@ -25,25 +25,25 @@ class SeeingIsBelieving
25
25
 
26
26
  wrappings = wrappings().sort_by(&:first)
27
27
 
28
- wrappings.each do |line_num, (range, last_col, meta)|
28
+ wrappings.each do |line_num, (range, _last_col, meta)|
29
29
  case meta
30
30
  when :total_fucking_failure
31
31
  rewriter.replace range, '.....TOTAL FUCKING FAILURE!.....'
32
32
  when :match_current_line
33
- rewriter.insert_before_multi range, '~' # Regexp#~
33
+ rewriter.insert_before range, '~' # Regexp#~
34
34
  end
35
35
  end
36
36
 
37
- wrappings.each do |line_num, (range, last_col, meta)|
38
- rewriter.insert_before_multi range, before_each.call(line_num)
37
+ wrappings.each do |line_num, (range, _last_col, _meta)|
38
+ rewriter.insert_before range, before_each.call(line_num)
39
39
  end
40
40
 
41
- wrappings.each do |line_num, (range, last_col, meta)|
42
- rewriter.insert_after_multi range, after_each.call(line_num)
41
+ wrappings.each do |line_num, (range, _last_col, _meta)|
42
+ rewriter.insert_after range, after_each.call(line_num)
43
43
  end
44
44
 
45
- rewriter.insert_before_multi root_range, before_all.call
46
- rewriter.insert_after_multi root_range, after_all_text
45
+ rewriter.insert_before root_range, before_all.call
46
+ rewriter.insert_after root_range, after_all_text
47
47
  rewriter.process
48
48
  end
49
49
  end
@@ -53,8 +53,13 @@ class SeeingIsBelieving
53
53
  attr_accessor :before_all, :after_all, :before_each, :after_each
54
54
  attr_accessor :code, :wrappings
55
55
 
56
- def buffer() code.buffer end
57
- def rewriter() code.rewriter end
56
+ def buffer
57
+ code.buffer
58
+ end
59
+
60
+ def rewriter
61
+ code.rewriter
62
+ end
58
63
 
59
64
  def root_range
60
65
  code.root.location.expression
@@ -107,7 +112,10 @@ class SeeingIsBelieving
107
112
  when :defs, :module
108
113
  add_to_wrappings ast
109
114
  add_children ast, true
110
- when :rescue, :ensure, :return, :break, :next, :splat, :kwsplat
115
+ when :ensure, :return, :break, :next, :splat, :kwsplat
116
+ add_children ast
117
+ when :rescue
118
+ add_to_wrappings ast if inline_rescue? ast
111
119
  add_children ast
112
120
  when :class
113
121
  name, * = ast.children
@@ -135,8 +143,8 @@ class SeeingIsBelieving
135
143
  add_to_wrappings ast
136
144
  the_begin = ast.location.begin
137
145
  if !the_begin
138
- # uhhhh, idk, nothing seems to hit this branch (at least in its spec)
139
- # maybe this is 'a = 1,2,3'? (I'd try it out, but have to call the folks)
146
+ # a = 1,2,3
147
+ add_children ast
140
148
  elsif the_begin.source !~ /\A%/
141
149
  # normal array
142
150
  add_children ast
@@ -247,5 +255,11 @@ class SeeingIsBelieving
247
255
  add_children ast
248
256
  end
249
257
  end
258
+
259
+ def inline_rescue?(ast)
260
+ primary_code, rescue_body, _else_body = ast.children
261
+ return false unless primary_code
262
+ primary_code.loc.expression.last_line == rescue_body.loc.expression.first_line
263
+ end
250
264
  end
251
265
  end