seeing_is_believing 2.2.0 → 3.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
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