seeing_is_believing 2.2.0 → 3.0.0.beta.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Changelog.md +33 -0
  4. data/bin/seeing_is_believing +1 -1
  5. data/features/errors.feature +3 -3
  6. data/features/examples.feature +6 -6
  7. data/features/flags.feature +51 -196
  8. data/features/regression.feature +12 -3
  9. data/features/safe.feature +33 -0
  10. data/features/support/env.rb +20 -0
  11. data/features/xmpfilter-style.feature +156 -0
  12. data/lib/seeing_is_believing.rb +17 -35
  13. data/lib/seeing_is_believing/binary.rb +81 -176
  14. data/lib/seeing_is_believing/binary/align_chunk.rb +5 -7
  15. data/lib/seeing_is_believing/binary/align_file.rb +4 -5
  16. data/lib/seeing_is_believing/binary/align_line.rb +4 -4
  17. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +60 -0
  18. data/lib/seeing_is_believing/binary/annotate_every_line.rb +64 -0
  19. data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +133 -0
  20. data/lib/seeing_is_believing/binary/comment_formatter.rb +19 -5
  21. data/lib/seeing_is_believing/binary/comment_lines.rb +1 -1
  22. data/lib/seeing_is_believing/binary/commentable_lines.rb +1 -1
  23. data/lib/seeing_is_believing/binary/interpret_flags.rb +149 -0
  24. data/lib/seeing_is_believing/binary/parse_args.rb +96 -104
  25. data/lib/seeing_is_believing/binary/remove_annotations.rb +95 -0
  26. data/lib/seeing_is_believing/binary/rewrite_comments.rb +8 -30
  27. data/lib/seeing_is_believing/code.rb +99 -0
  28. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +27 -19
  29. data/lib/seeing_is_believing/evaluate_with_eval_in.rb +27 -0
  30. data/lib/seeing_is_believing/event_stream/consumer.rb +111 -0
  31. data/lib/seeing_is_believing/event_stream/events.rb +16 -0
  32. data/lib/seeing_is_believing/event_stream/producer.rb +106 -0
  33. data/lib/seeing_is_believing/event_stream/update_result.rb +21 -0
  34. data/lib/seeing_is_believing/inspect_expressions.rb +24 -0
  35. data/lib/seeing_is_believing/parser_helpers.rb +1 -11
  36. data/lib/seeing_is_believing/result.rb +14 -56
  37. data/lib/seeing_is_believing/the_matrix.rb +14 -14
  38. data/lib/seeing_is_believing/version.rb +1 -1
  39. data/lib/seeing_is_believing/wrap_expressions.rb +32 -9
  40. data/seeing_is_believing.gemspec +7 -7
  41. data/spec/binary/comment_formatter_spec.rb +169 -18
  42. data/spec/binary/comment_lines_spec.rb +1 -1
  43. data/spec/binary/interpret_flags_spec.rb +307 -0
  44. data/spec/binary/parse_args_spec.rb +93 -91
  45. data/spec/binary/{clean_body_spec.rb → remove_annotations_spec.rb} +29 -22
  46. data/spec/binary/rewrite_comments_spec.rb +13 -13
  47. data/spec/code_spec.rb +49 -0
  48. data/spec/debugger_spec.rb +1 -1
  49. data/spec/evaluate_by_moving_files_spec.rb +7 -3
  50. data/spec/event_stream_spec.rb +390 -0
  51. data/spec/hard_core_ensure_spec.rb +1 -1
  52. data/spec/seeing_is_believing_spec.rb +137 -40
  53. data/spec/spec_helper.rb +3 -3
  54. data/spec/wrap_expressions_spec.rb +48 -35
  55. metadata +58 -35
  56. data/lib/seeing_is_believing/binary/add_annotations.rb +0 -144
  57. data/lib/seeing_is_believing/binary/clean_body.rb +0 -95
  58. data/lib/seeing_is_believing/has_exception.rb +0 -27
  59. data/lib/seeing_is_believing/line.rb +0 -90
  60. data/spec/line_spec.rb +0 -86
@@ -0,0 +1,99 @@
1
+ require 'parser/current'
2
+ class SeeingIsBelieving
3
+ class Code
4
+ InlineComment = Struct.new :line_number,
5
+ :whitespace_col,
6
+ :whitespace,
7
+ :text_col,
8
+ :text,
9
+ :full_range,
10
+ :whitespace_range,
11
+ :comment_range
12
+
13
+ # At prsent, it is expected that the syntax is validated before code arrives here
14
+ # or that its validity doesn't matter (e.g. extracting comments)
15
+ def initialize(raw_ruby_code, name="SeeingIsBelieving")
16
+ self.code = raw_ruby_code
17
+ self.name = name
18
+ end
19
+
20
+ def buffer() @buffer ||= (parse && @buffer ) end
21
+ def parser() @parser ||= (parse && @parser ) end
22
+ def rewriter() @rewriter ||= (parse && @rewriter ) end
23
+ def inline_comments() @comments ||= (parse && @inline_comments) end
24
+ def root() @root ||= (parse && @root ) end
25
+
26
+ def range_for(start_index, end_index)
27
+ Parser::Source::Range.new buffer, start_index, end_index
28
+ end
29
+
30
+ private
31
+
32
+ attr_accessor :code, :name
33
+
34
+ def parse
35
+ @buffer = Parser::Source::Buffer.new(name)
36
+ @buffer.source = code
37
+ builder = Parser::Builders::Default.new
38
+ builder.emit_file_line_as_literals = false # should be injectible?
39
+ @parser = Parser::CurrentRuby.new builder
40
+ @rewriter = Parser::Source::Rewriter.new @buffer
41
+
42
+ can_parse_invalid_code(@parser)
43
+
44
+ @root, all_comments, tokens = parser.tokenize(@buffer)
45
+
46
+ @inline_comments = all_comments.select(&:inline?).map { |c| wrap_comment c }
47
+ end
48
+
49
+ def can_parse_invalid_code(parser)
50
+ # THIS IS SO WE CAN EXTRACT COMMENTS FROM INVALID FILES.
51
+
52
+ # We do it by telling Parser's diagnostic to not blow up.
53
+ # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb
54
+
55
+ # However, this probably implies SiB won't work on Rbx/JRuby
56
+ # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L129-134
57
+
58
+ # Ideally we could just do this
59
+ # parser.diagnostics.all_errors_are_fatal = false
60
+ # parser.diagnostics.ignore_warnings = false
61
+
62
+ # But, the parser will still blow up on "fatal" errors (e.g. unterminated string) So we need to actually change it.
63
+ # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/diagnostic/engine.rb#L99
64
+
65
+ # We could make a NullDiagnostics like this:
66
+ # class NullDiagnostics < Parser::Diagnostic::Engine
67
+ # def process(*)
68
+ # # no op
69
+ # end
70
+ # end
71
+
72
+ # But we don't control initialization of the variable, and the value gets passed around, at least into the lexer.
73
+ # https://github.com/whitequark/parser/blob/2d69a1b5f34ef15b3a8330beb036ac4bf4775e29/lib/parser/base.rb#L139
74
+ # and since it's all private, it could change at any time (Parser is very state based),
75
+ # so I think it's just generally safer to mutate that one object, as we do now.
76
+ diagnostics = parser.diagnostics
77
+ def diagnostics.process(*)
78
+ self
79
+ end
80
+ end
81
+
82
+ def wrap_comment(comment)
83
+ last_char = comment.location.expression.begin_pos
84
+ first_char = last_char
85
+ first_char -= 1 while first_char > 0 && code[first_char-1] =~ /[ \t]/
86
+ preceding_whitespace = buffer.source[first_char...last_char]
87
+ preceding_whitespace_range = range_for first_char, last_char
88
+
89
+ InlineComment.new comment.location.line,
90
+ preceding_whitespace_range.column,
91
+ preceding_whitespace,
92
+ comment.location.column,
93
+ comment.text,
94
+ range_for(first_char, comment.location.expression.end_pos),
95
+ preceding_whitespace_range,
96
+ comment.location.expression
97
+ end
98
+ end
99
+ end
@@ -11,15 +11,16 @@
11
11
  # read the wrong file... of course, since we rewrite the file,
12
12
  # its body will be incorrect, anyway.
13
13
 
14
- require 'json'
15
14
  require 'open3'
16
15
  require 'timeout'
17
16
  require 'stringio'
18
- require 'fileutils'
17
+ require 'fileutils' # DELETE?
19
18
  require 'seeing_is_believing/error'
20
19
  require 'seeing_is_believing/result'
21
20
  require 'seeing_is_believing/debugger'
22
21
  require 'seeing_is_believing/hard_core_ensure'
22
+ require 'seeing_is_believing/event_stream/consumer'
23
+ require 'seeing_is_believing/event_stream/update_result'
23
24
 
24
25
  class SeeingIsBelieving
25
26
  class EvaluateByMovingFiles
@@ -28,14 +29,13 @@ class SeeingIsBelieving
28
29
  new(*args).call
29
30
  end
30
31
 
31
- attr_accessor :program, :filename, :input_stream, :matrix_filename, :require_flags, :load_path_flags, :encoding, :timeout, :ruby_executable, :debugger
32
+ attr_accessor :program, :filename, :input_stream, :require_flags, :load_path_flags, :encoding, :timeout, :ruby_executable, :debugger, :result
32
33
 
33
34
  def initialize(program, filename, options={})
34
35
  self.program = program
35
36
  self.filename = filename
36
37
  self.input_stream = options.fetch :input_stream, StringIO.new('')
37
- self.matrix_filename = options[:matrix_filename] || 'seeing_is_believing/the_matrix'
38
- self.require_flags = options.fetch(:require, []).map { |filename| ['-r', filename] }.flatten
38
+ self.require_flags = options.fetch(:require, ['seeing_is_believing/the_matrix']).map { |filename| ['-r', filename] }.flatten
39
39
  self.load_path_flags = options.fetch(:load_path, []).map { |dir| ['-I', dir] }.flatten
40
40
  self.encoding = options.fetch :encoding, nil
41
41
  self.timeout = options[:timeout]
@@ -51,7 +51,8 @@ class SeeingIsBelieving
51
51
  write_program_to_file
52
52
  begin
53
53
  evaluate_file
54
- deserialize_result.tap { |result| fail if result.bug_in_sib? }
54
+ fail if result.bug_in_sib?
55
+ result
55
56
  rescue Exception => error
56
57
  error = wrap_error error if error_implies_bug_in_sib? error
57
58
  raise error
@@ -102,17 +103,27 @@ class SeeingIsBelieving
102
103
 
103
104
  def evaluate_file
104
105
  Open3.popen3 ENV, *popen_args do |process_stdin, process_stdout, process_stderr, thread|
105
- out_reader = Thread.new { process_stdout.read }
106
- err_reader = Thread.new { process_stderr.read }
107
- Thread.new do
106
+ # send stdin
107
+ Thread.new {
108
108
  input_stream.each_char { |char| process_stdin.write char }
109
109
  process_stdin.close
110
+ }
111
+
112
+ # consume events
113
+ self.result = Result.new
114
+ event_consumer = Thread.new do
115
+ EventStream::Consumer.new(process_stdout)
116
+ .each { |event| EventStream::UpdateResult.call result, event }
110
117
  end
118
+
119
+ # process stderr
120
+ err_reader = Thread.new { process_stderr.read }
121
+
111
122
  begin
112
123
  Timeout::timeout timeout do
113
- self.stdout = out_reader.value
114
124
  self.stderr = err_reader.value
115
125
  self.exitstatus = thread.value
126
+ event_consumer.join
116
127
  end
117
128
  rescue Timeout::Error
118
129
  Process.kill "TERM", thread.pid
@@ -126,7 +137,6 @@ class SeeingIsBelieving
126
137
  '-W0', # no warnings (b/c I hijack STDOUT/STDERR)
127
138
  *(encoding ? ["-K#{encoding}"] : []), # allow the encoding to be set
128
139
  '-I', File.expand_path('../..', __FILE__), # add lib to the load path
129
- '-r', matrix_filename, # hijack the environment so it can be recorded
130
140
  *load_path_flags, # users can inject dirs to be added to the load path
131
141
  *require_flags, # users can inject files to be required
132
142
  filename]
@@ -136,16 +146,14 @@ class SeeingIsBelieving
136
146
  raise "Exitstatus: #{exitstatus.inspect},\nError: #{stderr.inspect}"
137
147
  end
138
148
 
139
- def deserialize_result
140
- Result.from_primitive JSON.load stdout
141
- end
142
-
143
149
  def wrap_error(error)
144
150
  debugger.context "Program could not be evaluated" do
145
- "Program: #{program.inspect.chomp}\n\n"\
146
- "Stdout: #{stdout.inspect.chomp}\n\n"\
147
- "Stderr: #{stderr.inspect.chomp}\n\n"\
148
- "Status: #{exitstatus.inspect.chomp}\n"
151
+ "Program: #{program.inspect.chomp}\n\n"\
152
+ "Stderr: #{stderr.inspect.chomp}\n\n"\
153
+ "Status: #{exitstatus.inspect.chomp}\n\n"\
154
+ "Result: #{result.inspect.chomp}\n\n"\
155
+ "Actual Error: #{error.inspect.chomp}\n"+
156
+ error.backtrace.map { |sf| " #{sf}\n" }.join("")
149
157
  end
150
158
  BugInSib.new error
151
159
  end
@@ -0,0 +1,27 @@
1
+ require 'eval_in'
2
+ require 'stringio'
3
+
4
+ class SeeingIsBelieving
5
+ class EvaluateWithEvalIn
6
+ def self.call(*args)
7
+ new(*args).call
8
+ end
9
+
10
+ attr_accessor :program, :filename, :input_stream, :matrix_filename, :require_flags, :load_path_flags, :encoding, :timeout, :ruby_executable, :debugger
11
+
12
+ def initialize(program, filename, options={})
13
+ self.program = program
14
+ # self.input_stream = options.fetch :input_stream, StringIO.new('')
15
+ # self.timeout = options[:timeout]
16
+ # self.debugger = options.fetch :debugger, Debugger.new(stream: nil)
17
+ end
18
+
19
+ def call
20
+ eval_in_result = ::EvalIn.call(program, language: 'ruby/mri-2.1')
21
+ result = Result.new
22
+ EventStream::Consumer.new(StringIO.new eval_in_result.output)
23
+ .each { |event| EventStream::UpdateResult.call result, event }
24
+ result
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,111 @@
1
+ require 'seeing_is_believing/event_stream/events'
2
+ require 'seeing_is_believing/error'
3
+ class SeeingIsBelieving
4
+ module EventStream
5
+ class Consumer
6
+ NoMoreInput = Class.new SeeingIsBelievingError
7
+ WtfWhoClosedMyShit = Class.new SeeingIsBelievingError
8
+
9
+ def initialize(readstream)
10
+ @readstream = readstream
11
+ end
12
+
13
+ def call(n=1)
14
+ events = n.times.map do
15
+ raise NoMoreInput if finished?
16
+ line = @readstream.gets
17
+ raise NoMoreInput if line.nil?
18
+ event = event_for line
19
+ @finished = true if Events::Finish === event
20
+ event
21
+ end
22
+ n == 1 ? events.first : events
23
+ rescue IOError
24
+ @finished = true
25
+ raise WtfWhoClosedMyShit, "Our end of the pipe was closed!"
26
+ rescue NoMoreInput
27
+ @finished = true
28
+ raise
29
+ end
30
+
31
+ def each
32
+ return to_enum :each unless block_given?
33
+ loop do
34
+ event = call
35
+ yield event unless Events::Finish === event
36
+ end
37
+ rescue NoMoreInput
38
+ return nil
39
+ end
40
+
41
+ def finished?
42
+ @finished
43
+ end
44
+
45
+ private
46
+
47
+ def extract_token(line)
48
+ event_name = line[/[^ ]+/]
49
+ line.sub! /^\s*[^ ]+\s*/, ''
50
+ event_name
51
+ end
52
+
53
+ # for a consideration of many different ways of doing this, see 5633064
54
+ def extract_string(line)
55
+ Marshal.load extract_token(line).unpack('m0').first
56
+ end
57
+
58
+ def tokenize(line)
59
+ line.split(' ')
60
+ end
61
+
62
+ def event_for(line)
63
+ line.chomp!
64
+ event_name = extract_token(line).intern
65
+ case event_name
66
+ when :result
67
+ line_number = extract_token(line).to_i
68
+ type = extract_token(line).intern
69
+ inspected = extract_string(line)
70
+ Events::LineResult.new(type, line_number, inspected)
71
+ when :maxed_result
72
+ line_number = extract_token(line).to_i
73
+ type = extract_token(line).intern
74
+ Events::UnrecordedResult.new(type, line_number)
75
+ when :exception
76
+ Events::Exception.new(-1, '', '', []).tap do |exception|
77
+ loop do
78
+ line = @readstream.gets.chomp
79
+ case extract_token(line).intern
80
+ when :line_number then exception.line_number = extract_token(line).to_i
81
+ when :class_name then exception.class_name = extract_string(line)
82
+ when :message then exception.message = extract_string(line)
83
+ when :backtrace then exception.backtrace << extract_string(line)
84
+ when :end then break
85
+ end
86
+ end
87
+ end
88
+ when :stdout
89
+ Events::Stdout.new(extract_string line)
90
+ when :stderr
91
+ Events::Stderr.new(extract_string line)
92
+ when :bug_in_sib
93
+ Events::BugInSiB.new(extract_token(line) == 'true')
94
+ when :max_line_captures
95
+ token = extract_token(line)
96
+ value = token =~ /infinity/i ? Float::INFINITY : token.to_i
97
+ Events::MaxLineCaptures.new(value)
98
+ when :exitstatus
99
+ # TODO: Will this fuck it up if you run `exit true`?
100
+ Events::Exitstatus.new(extract_token(line).to_i)
101
+ when :finish
102
+ Events::Finish.new
103
+ when :num_lines
104
+ Events::NumLines.new(extract_token(line).to_i)
105
+ else
106
+ raise "IDK what #{event_name.inspect} is!"
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,16 @@
1
+ class SeeingIsBelieving
2
+ module EventStream
3
+ module Events
4
+ LineResult = Struct.new(:type, :line_number, :inspected)
5
+ UnrecordedResult = Struct.new(:type, :line_number)
6
+ Stdout = Struct.new(:value)
7
+ Stderr = Struct.new(:value)
8
+ BugInSiB = Struct.new(:value)
9
+ MaxLineCaptures = Struct.new(:value)
10
+ NumLines = Struct.new(:value)
11
+ Exitstatus = Struct.new(:value)
12
+ Exception = Struct.new(:line_number, :class_name, :message, :backtrace)
13
+ Finish = Class.new
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,106 @@
1
+ require 'seeing_is_believing/event_stream/events'
2
+ class SeeingIsBelieving
3
+ module EventStream
4
+ require 'thread'
5
+ class Producer
6
+ attr_accessor :exitstatus, :bug_in_sib, :max_line_captures, :num_lines
7
+
8
+ def initialize(resultstream)
9
+ self.exitstatus = 0
10
+ self.bug_in_sib = false
11
+ self.max_line_captures = Float::INFINITY
12
+ self.num_lines = 0
13
+ self.recorded_results = []
14
+ self.queue = Thread::Queue.new
15
+ self.producer_thread = Thread.new do
16
+ finish = "finish"
17
+ begin
18
+ loop do
19
+ to_publish = queue.shift
20
+ if to_publish == finish
21
+ resultstream << "finish\n"
22
+ break
23
+ else
24
+ resultstream << (to_publish << "\n")
25
+ end
26
+ end
27
+ rescue IOError, Errno::EPIPE
28
+ loop { break if queue.shift == finish }
29
+ end
30
+ end
31
+ end
32
+
33
+ def bug_in_sib=(bool)
34
+ @bug_in_sib = (bool ? true : false)
35
+ end
36
+
37
+ # for a consideration of many different ways of doing this, see 5633064
38
+ def to_string_token(string)
39
+ [Marshal.dump(string.to_s)].pack('m0')
40
+ end
41
+
42
+ StackErrors = [SystemStackError]
43
+ StackErrors << Java::JavaLang::StackOverflowError if defined?(RUBY_PLATFORM) && RUBY_PLATFORM == 'java'
44
+ def record_result(type, line_number, value)
45
+ self.num_lines = line_number if num_lines < line_number
46
+ counts = recorded_results[line_number] ||= Hash.new(0)
47
+ count = counts[type]
48
+ recorded_results[line_number][type] = count.next
49
+ if count < max_line_captures
50
+ begin
51
+ if block_given?
52
+ inspected = yield(value).to_str
53
+ else
54
+ inspected = value.inspect.to_str
55
+ end
56
+ rescue *StackErrors
57
+ # this is necessary because SystemStackError won't show the backtrace of the method we tried to call
58
+ # which means there won't be anything showing the user where this came from
59
+ # so we need to re-raise the error to get a backtrace that shows where we came from
60
+ # otherwise it looks like the bug is in SiB and not the user's program, see https://github.com/JoshCheek/seeing_is_believing/issues/37
61
+ raise SystemStackError, "Calling inspect blew the stack (is it recursive w/o a base case?)"
62
+ rescue Exception
63
+ inspected = "#<no inspect available>"
64
+ end
65
+ queue << "result #{line_number} #{type} #{to_string_token inspected}"
66
+ elsif count == max_line_captures
67
+ queue << "maxed_result #{line_number} #{type}"
68
+ end
69
+ value
70
+ end
71
+
72
+ def record_exception(line_number, exception)
73
+ self.num_lines = line_number if num_lines < line_number
74
+ queue << "exception"
75
+ queue << " line_number #{line_number}"
76
+ queue << " class_name #{to_string_token exception.class.name}"
77
+ queue << " message #{to_string_token exception.message}"
78
+ exception.backtrace.each { |line|
79
+ queue << " backtrace #{to_string_token line}"
80
+ }
81
+ queue << "end"
82
+ end
83
+
84
+ def record_stdout(stdout)
85
+ queue << "stdout #{to_string_token stdout}"
86
+ end
87
+
88
+ def record_stderr(stderr)
89
+ queue << "stderr #{to_string_token stderr}"
90
+ end
91
+
92
+ def finish!
93
+ queue << "bug_in_sib #{bug_in_sib}"
94
+ queue << "max_line_captures #{max_line_captures}"
95
+ queue << "num_lines #{num_lines}"
96
+ queue << "exitstatus #{exitstatus}"
97
+ queue << "finish".freeze
98
+ producer_thread.join
99
+ end
100
+
101
+ private
102
+
103
+ attr_accessor :resultstream, :queue, :producer_thread, :recorded_results
104
+ end
105
+ end
106
+ end