seeing_is_believing 3.0.0.beta.4 → 3.0.0.beta.5

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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -8
  3. data/Rakefile +1 -1
  4. data/Readme.md +65 -25
  5. data/bin/seeing_is_believing +1 -0
  6. data/docs/sib-streaming.gif +0 -0
  7. data/features/deprecated-flags.feature +62 -2
  8. data/features/errors.feature +12 -7
  9. data/features/examples.feature +143 -4
  10. data/features/flags.feature +89 -29
  11. data/features/regression.feature +58 -14
  12. data/features/support/env.rb +4 -0
  13. data/features/xmpfilter-style.feature +181 -36
  14. data/lib/seeing_is_believing.rb +44 -33
  15. data/lib/seeing_is_believing/binary.rb +31 -88
  16. data/lib/seeing_is_believing/binary/align_chunk.rb +30 -11
  17. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +10 -16
  18. data/lib/seeing_is_believing/binary/annotate_every_line.rb +5 -25
  19. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +136 -0
  20. data/lib/seeing_is_believing/binary/comment_lines.rb +8 -10
  21. data/lib/seeing_is_believing/binary/commentable_lines.rb +20 -26
  22. data/lib/seeing_is_believing/binary/config.rb +392 -0
  23. data/lib/seeing_is_believing/binary/data_structures.rb +57 -0
  24. data/lib/seeing_is_believing/binary/engine.rb +104 -0
  25. data/lib/seeing_is_believing/binary/{comment_formatter.rb → format_comment.rb} +6 -6
  26. data/lib/seeing_is_believing/binary/remove_annotations.rb +29 -28
  27. data/lib/seeing_is_believing/binary/rewrite_comments.rb +42 -43
  28. data/lib/seeing_is_believing/code.rb +105 -49
  29. data/lib/seeing_is_believing/debugger.rb +6 -5
  30. data/lib/seeing_is_believing/error.rb +6 -17
  31. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +78 -129
  32. data/lib/seeing_is_believing/event_stream/consumer.rb +114 -64
  33. data/lib/seeing_is_believing/event_stream/events.rb +169 -11
  34. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +57 -0
  35. data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +18 -0
  36. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +45 -0
  37. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +39 -0
  38. data/lib/seeing_is_believing/event_stream/producer.rb +25 -24
  39. data/lib/seeing_is_believing/hash_struct.rb +206 -0
  40. data/lib/seeing_is_believing/result.rb +20 -3
  41. data/lib/seeing_is_believing/the_matrix.rb +20 -12
  42. data/lib/seeing_is_believing/version.rb +1 -1
  43. data/lib/seeing_is_believing/wrap_expressions.rb +55 -115
  44. data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +14 -0
  45. data/seeing_is_believing.gemspec +1 -1
  46. data/spec/binary/alignment_specs.rb +27 -0
  47. data/spec/binary/comment_lines_spec.rb +3 -2
  48. data/spec/binary/config_spec.rb +657 -0
  49. data/spec/binary/engine_spec.rb +97 -0
  50. data/spec/binary/{comment_formatter_spec.rb → format_comment_spec.rb} +2 -2
  51. data/spec/binary/marker_spec.rb +71 -0
  52. data/spec/binary/options_spec.rb +0 -0
  53. data/spec/binary/remove_annotations_spec.rb +31 -18
  54. data/spec/binary/rewrite_comments_spec.rb +26 -11
  55. data/spec/code_spec.rb +190 -6
  56. data/spec/debugger_spec.rb +4 -0
  57. data/spec/evaluate_by_moving_files_spec.rb +38 -20
  58. data/spec/event_stream_spec.rb +265 -116
  59. data/spec/hash_struct_spec.rb +514 -0
  60. data/spec/seeing_is_believing_spec.rb +108 -46
  61. data/spec/spec_helper.rb +9 -0
  62. data/spec/wrap_expressions_spec.rb +207 -172
  63. metadata +30 -18
  64. data/docs/for-presentations +0 -33
  65. data/lib/seeing_is_believing/binary/annotate_xmpfilter_style.rb +0 -128
  66. data/lib/seeing_is_believing/binary/interpret_flags.rb +0 -156
  67. data/lib/seeing_is_believing/binary/parse_args.rb +0 -263
  68. data/lib/seeing_is_believing/event_stream/update_result.rb +0 -24
  69. data/lib/seeing_is_believing/inspect_expressions.rb +0 -21
  70. data/lib/seeing_is_believing/parser_helpers.rb +0 -82
  71. data/spec/binary/interpret_flags_spec.rb +0 -332
  72. data/spec/binary/parse_args_spec.rb +0 -415
@@ -1,58 +1,69 @@
1
- require 'stringio'
2
1
  require 'tmpdir'
3
2
 
4
3
  require 'seeing_is_believing/result'
5
4
  require 'seeing_is_believing/version'
6
5
  require 'seeing_is_believing/debugger'
7
- require 'seeing_is_believing/inspect_expressions'
6
+ require 'seeing_is_believing/wrap_expressions_with_inspect'
7
+ require 'seeing_is_believing/hash_struct'
8
8
  require 'seeing_is_believing/evaluate_by_moving_files'
9
+ require 'seeing_is_believing/event_stream/handlers/debug'
10
+ require 'seeing_is_believing/event_stream/handlers/update_result'
9
11
 
10
12
  class SeeingIsBelieving
11
- BLANK_REGEX = /\A\s*\Z/
13
+ class Options < HashStruct
14
+ predicate(:event_handler) { EventStream::Handlers::UpdateResult.new Result.new }
15
+ attribute(:filename) { nil }
16
+ attribute(:encoding) { nil }
17
+ attribute(:stdin) { "" }
18
+ attribute(:require_files) { ['seeing_is_believing/the_matrix'] }
19
+ attribute(:load_path_dirs) { [File.expand_path('..', __FILE__)] }
20
+ attribute(:timeout_seconds) { 0 }
21
+ attribute(:debugger) { Debugger::Null }
22
+ attribute(:max_line_captures) { Float::INFINITY }
23
+ attribute(:rewrite_code) { WrapExpressionsWithInspect }
24
+ end
12
25
 
13
26
  def self.call(*args)
14
27
  new(*args).call
15
28
  end
16
29
 
30
+ attr_reader :options
17
31
  def initialize(program, options={})
18
- @program = program
19
- @filename = options[:filename]
20
- @stdin = to_stream options.fetch(:stdin, '')
21
- @require = options.fetch :require, ['seeing_is_believing/the_matrix']
22
- @load_path = options.fetch :load_path, []
23
- @encoding = options.fetch :encoding, nil
24
- @timeout = options[:timeout]
25
- @debugger = options.fetch :debugger, Debugger.new(stream: nil)
26
- @number_of_captures = options.fetch :number_of_captures, Float::INFINITY
27
- @evaluator = options.fetch :evaluator, EvaluateByMovingFiles
28
- @record_expressions = options.fetch :record_expressions, InspectExpressions # TODO: rename to wrap_expressions
32
+ @program = program
33
+ @program += "\n" unless @program.end_with? "\n"
34
+ @options = Options.new options
29
35
  end
30
36
 
31
37
  def call
32
38
  @memoized_result ||= Dir.mktmpdir("seeing_is_believing_temp_dir") { |dir|
33
- filename = @filename || File.join(dir, 'program.rb')
34
- new_program = @record_expressions.call "#{@program.chomp}\n", filename, @number_of_captures
35
- @debugger.context("TRANSLATED PROGRAM") { new_program }
36
-
37
- result = @evaluator.call new_program,
38
- filename,
39
- input_stream: @stdin,
40
- require: @require,
41
- load_path: @load_path,
42
- encoding: @encoding,
43
- timeout: @timeout,
44
- debugger: @debugger
45
-
46
- @debugger.context("RESULT") { result.inspect }
47
-
48
- result
39
+ filename = options.filename || File.join(dir, 'program.rb')
40
+ new_program = options.rewrite_code.call @program
41
+
42
+ options.debugger.context("REWRITTEN PROGRAM") { new_program }
43
+
44
+ EvaluateByMovingFiles.call \
45
+ new_program,
46
+ filename,
47
+ event_handler: debugging_handler,
48
+ provided_input: options.stdin,
49
+ require_files: options.require_files,
50
+ load_path_dirs: options.load_path_dirs,
51
+ encoding: options.encoding,
52
+ timeout_seconds: options.timeout_seconds,
53
+ max_line_captures: options.max_line_captures
54
+
55
+ options.event_handler
49
56
  }
50
57
  end
51
58
 
52
59
  private
53
60
 
54
- def to_stream(string_or_stream)
55
- return string_or_stream if string_or_stream.respond_to? :gets
56
- StringIO.new string_or_stream
61
+ # Even though the debugger can be disabled,
62
+ # Handlers::Debug is somewhat expensive, and there could be tens of millions of calls
63
+ # e.g. https://github.com/JoshCheek/seeing_is_believing/issues/12
64
+ # so just skip it in this case
65
+ def debugging_handler
66
+ return options.event_handler unless options.debugger.enabled?
67
+ EventStream::Handlers::Debug.new options.debugger, options.event_handler
57
68
  end
58
69
  end
@@ -1,125 +1,68 @@
1
1
  require 'seeing_is_believing'
2
- require 'seeing_is_believing/binary/parse_args'
3
- require 'seeing_is_believing/binary/interpret_flags'
4
- require 'seeing_is_believing/binary/remove_annotations'
2
+ require 'seeing_is_believing/binary/config'
3
+ require 'seeing_is_believing/binary/engine'
5
4
 
6
5
  class SeeingIsBelieving
7
6
  module Binary
8
7
  SUCCESS_STATUS = 0
9
- DISPLAYABLE_ERROR_STATUS = 1 # e.g. there was an error, but the output is legit (we can display exceptions)
10
- NONDISPLAYABLE_ERROR_STATUS = 2 # e.g. an error like incorrect invocation or syntax that can't be displayed in the input program
8
+ DISPLAYABLE_ERROR_STATUS = 1 # e.g. user code raises an exception (we can display this in the output)
9
+ NONDISPLAYABLE_ERROR_STATUS = 2 # e.g. SiB was invoked incorrectly
11
10
 
12
11
  def self.call(argv, stdin, stdout, stderr)
13
- flags = ParseArgs.call(argv)
14
- options = InterpretFlags.new(flags, stdin, stdout)
12
+ config = Config.new.parse_args(argv).finalize(stdin, stdout, stderr, File)
13
+ engine = Engine.new config
15
14
 
16
- if options.errors.any?
17
- stderr.puts options.errors.join("\n")
18
- return NONDISPLAYABLE_ERROR_STATUS
19
- end
20
-
21
- if options.print_help? # TODO: Should this be first?
22
- stdout.puts options.help_screen
15
+ if config.print_help?
16
+ stdout.puts config.help_screen
23
17
  return SUCCESS_STATUS
24
18
  end
25
19
 
26
- if options.print_version?
20
+ if config.print_version?
27
21
  stdout.puts SeeingIsBelieving::VERSION
28
22
  return SUCCESS_STATUS
29
23
  end
30
24
 
31
- if options.provided_filename_dne?
32
- stderr.puts "#{options.filename} does not exist!"
25
+ if config.errors.any?
26
+ stderr.puts *config.errors, *config.deprecations
33
27
  return NONDISPLAYABLE_ERROR_STATUS
34
28
  end
35
29
 
36
- if options.print_cleaned?
37
- stdout.print RemoveAnnotations.call(options.prepared_body, true, options.marker_regexes)
30
+ if config.print_cleaned?
31
+ stdout.print engine.cleaned_body
38
32
  return SUCCESS_STATUS
39
33
  end
40
34
 
41
- syntax_error_notice = syntax_error_notice_for(options.body)
42
- if syntax_error_notice
43
- stderr.puts syntax_error_notice
44
- return NONDISPLAYABLE_ERROR_STATUS
45
- end
46
-
47
- results, program_timedout, unexpected_exception =
48
- evaluate_program(options.prepared_body, options.lib_options)
49
-
50
- if program_timedout
51
- stderr.puts "Timeout Error after #{options.timeout} seconds!"
35
+ if engine.syntax_error?
36
+ stderr.puts engine.syntax_error
52
37
  return NONDISPLAYABLE_ERROR_STATUS
53
38
  end
54
39
 
55
- if unexpected_exception.kind_of? BugInSib
56
- stderr.puts unexpected_exception.message
57
- return NONDISPLAYABLE_ERROR_STATUS
58
- end
40
+ engine.evaluate!
59
41
 
60
- if unexpected_exception
61
- stderr.puts unexpected_exception.class,
62
- unexpected_exception.message,
63
- "",
64
- unexpected_exception.backtrace
42
+ if engine.timed_out?
43
+ stderr.puts "Timeout Error after #{config.timeout_seconds} seconds!"
65
44
  return NONDISPLAYABLE_ERROR_STATUS
66
45
  end
67
46
 
68
- if options.result_as_json?
47
+ if config.result_as_json?
69
48
  require 'json'
70
- stdout.puts JSON.dump(result_as_data_structure(results))
49
+ stdout.puts JSON.dump(engine.result.as_json)
71
50
  return SUCCESS_STATUS
51
+ elsif config.print_event_stream?
52
+ # no op, the event stream handler has been printing it all along
53
+ elsif config.debug?
54
+ config.debugger.context("OUTPUT") { engine.annotated_body }
55
+ else
56
+ stdout.print engine.annotated_body
72
57
  end
73
58
 
74
- # TODO: Annoying debugger stuff from annotators can move up to here
75
- # or maybe debugging goes to stderr, and we still print this anyway?
76
- stdout.print options.annotator.call(options.prepared_body,
77
- results,
78
- options.annotator_options)
79
-
80
- if options.inherit_exit_status?
81
- results.exitstatus
82
- elsif results.has_exception? && results.exitstatus != 0 # e.g. `exit 0` raises SystemExit but isn't an error
83
- DISPLAYABLE_ERROR_STATUS
84
- else
59
+ if config.inherit_exitstatus?
60
+ engine.exitstatus
61
+ elsif engine.exitstatus.zero?
85
62
  SUCCESS_STATUS
63
+ else
64
+ DISPLAYABLE_ERROR_STATUS
86
65
  end
87
66
  end
88
-
89
- private
90
-
91
- def self.syntax_error_notice_for(body)
92
- out, err, syntax_status = Open3.capture3 RbConfig.ruby, '-c', stdin_data: body
93
- return err unless syntax_status.success?
94
-
95
- # The stdin_data may still be getting written when the pipe closes
96
- # This is because Ruby will stop reading from stdin if everything left is in the DATA segment, and the data segment is not referenced.
97
- # In this case, the Syntax is fine
98
- # https://bugs.ruby-lang.org/issues/9583
99
- rescue Errno::EPIPE
100
- return nil
101
- end
102
-
103
- def self.evaluate_program(body, options)
104
- return SeeingIsBelieving.call(body, options), nil, nil
105
- rescue Timeout::Error
106
- return nil, true, nil
107
- rescue Exception
108
- return nil, false, $!
109
- end
110
-
111
- def self.result_as_data_structure(results)
112
- exception = results.has_exception? && { line_number_in_this_file: results.exception.line_number,
113
- class_name: results.exception.class_name,
114
- message: results.exception.message,
115
- backtrace: results.exception.backtrace
116
- }
117
- { stdout: results.stdout,
118
- stderr: results.stderr,
119
- exit_status: results.exitstatus,
120
- exception: exception,
121
- lines: results.each.with_object(Hash.new).with_index(1) { |(result, hash), line_number| hash[line_number] = result },
122
- }
123
- end
124
67
  end
125
68
  end
@@ -18,19 +18,38 @@ class SeeingIsBelieving
18
18
 
19
19
  def line_lengths
20
20
  @line_lengths ||= begin
21
- line_num_to_indexes = CommentableLines.new(body).call # {line_number => [index_in_file, index_in_col]}
22
- Hash[line_num_to_indexes
23
- .keys
24
- .sort
25
- .slice_before { |line_number| line_num_to_indexes[line_number].last.zero? }
26
- .map { |slice|
27
- max_chunk_length = 2 + slice.map { |line_num| line_num_to_indexes[line_num].last }.max
28
- slice.map { |line_number| [line_number, max_chunk_length] }
29
- }
30
- .flatten(1)
31
- ]
21
+ # sheesh, I need like Hash#map_values or something
22
+ line_nums_to_cols = Hash.[] \
23
+ CommentableLines.call(body)
24
+ .map { |line_num, (file_index, col_index)|
25
+ [line_num, col_index-amount_of_preceding_whitespace(file_index)]
26
+ }
27
+
28
+ Hash.[] \
29
+ line_nums_to_cols
30
+ .keys
31
+ .sort
32
+ .slice_before { |line_number| line_nums_to_cols[line_number].zero? }
33
+ .flat_map { |slice|
34
+ max_chunk_length = 2 + slice.map { |line_num| line_nums_to_cols[line_num] }.max
35
+ slice.map { |line_number| [line_number, max_chunk_length] }
36
+ }
37
+ end
38
+ end
39
+
40
+ def trim_trailing_whitespace(line_nums_to_indexes)
41
+ line_nums_to_indexes.each do |num, indexes|
42
+ index_in_file = indexes[0]
43
+ num_to_trim = amount_of_preceding_whitespace(index_in_file)
44
+ indexes.map! { |index| index - num_to_trim }
32
45
  end
33
46
  end
47
+
48
+ def amount_of_preceding_whitespace(index_of_trailing_newline)
49
+ index = index_of_trailing_newline - 1
50
+ index -= 1 while 0 <= index && body[index] !~ /[\S\n]/
51
+ index_of_trailing_newline - index - 1
52
+ end
34
53
  end
35
54
  end
36
55
  end
@@ -1,32 +1,26 @@
1
1
  require 'seeing_is_believing/binary' # defines the markers
2
- require 'seeing_is_believing/binary/comment_formatter'
2
+ require 'seeing_is_believing/binary/format_comment'
3
3
 
4
4
  class SeeingIsBelieving
5
5
  module Binary
6
6
  module AnnotateEndOfFile
7
7
  extend self
8
8
 
9
- # TODO: Switch options to markers
10
9
  def add_stdout_stderr_and_exceptions_to(new_body, results, options)
11
10
  output = stdout_ouptut_for(results, options) <<
12
11
  stderr_ouptut_for(results, options) <<
13
12
  exception_output_for(results, options)
14
13
 
15
- # this technically could find an __END__ in a string or whatever
16
- # going to just ignore that, though
17
- if new_body[/^__END__$/]
18
- new_body.sub! "\n__END__", "\n#{output}__END__"
19
- else
20
- new_body << "\n" unless new_body.end_with? "\n"
21
- new_body << output
22
- end
14
+ code = Code.new(new_body)
15
+ code.rewriter.insert_after code.body_range, output
16
+ new_body.replace code.rewriter.process
23
17
  end
24
18
 
25
19
  def stdout_ouptut_for(results, options)
26
20
  return '' unless results.has_stdout?
27
21
  output = "\n"
28
22
  results.stdout.each_line do |line|
29
- output << CommentFormatter.call(0, options[:markers][:stdout], line.chomp, options) << "\n"
23
+ output << FormatComment.call(0, options[:markers][:stdout][:prefix], line.chomp, options) << "\n"
30
24
  end
31
25
  output
32
26
  end
@@ -35,23 +29,23 @@ class SeeingIsBelieving
35
29
  return '' unless results.has_stderr?
36
30
  output = "\n"
37
31
  results.stderr.each_line do |line|
38
- output << CommentFormatter.call(0, options[:markers][:stderr], line.chomp, options) << "\n"
32
+ output << FormatComment.call(0, options[:markers][:stderr][:prefix], line.chomp, options) << "\n"
39
33
  end
40
34
  output
41
35
  end
42
36
 
43
37
  def exception_output_for(results, options)
44
38
  return '' unless results.has_exception?
45
- exception_marker = options[:markers][:exception]
39
+ exception_marker = options[:markers][:exception][:prefix]
46
40
  exception = results.exception
47
41
  output = "\n"
48
- output << CommentFormatter.new(0, exception_marker, exception.class_name, options).call << "\n"
42
+ output << FormatComment.new(0, exception_marker, exception.class_name, options).call << "\n"
49
43
  exception.message.each_line do |line|
50
- output << CommentFormatter.new(0, exception_marker, line.chomp, options).call << "\n"
44
+ output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
51
45
  end
52
46
  output << exception_marker.sub(/\s+$/, '') << "\n"
53
47
  exception.backtrace.each do |line|
54
- output << CommentFormatter.new(0, exception_marker, line.chomp, options).call << "\n"
48
+ output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
55
49
  end
56
50
  output
57
51
  end
@@ -1,16 +1,6 @@
1
1
  class SeeingIsBelieving
2
2
  module Binary
3
3
  class AnnotateEveryLine
4
- def self.prepare_body(uncleaned_body, marker_regexes)
5
- require 'seeing_is_believing/binary/remove_annotations'
6
- RemoveAnnotations.call uncleaned_body, true, marker_regexes
7
- end
8
-
9
- def self.expression_wrapper(markers, marker_regexes)
10
- require 'seeing_is_believing/inspect_expressions'
11
- InspectExpressions
12
- end
13
-
14
4
  def self.call(body, results, options)
15
5
  new(body, results, options).call
16
6
  end
@@ -24,7 +14,9 @@ class SeeingIsBelieving
24
14
  def call
25
15
  @new_body ||= begin
26
16
  require 'seeing_is_believing/binary/comment_lines'
27
- require 'seeing_is_believing/binary/comment_formatter'
17
+ require 'seeing_is_believing/binary/format_comment'
18
+ exception_text = @options[:markers][:exception][:prefix]
19
+ value_text = @options[:markers][:value][:prefix]
28
20
 
29
21
  alignment_strategy = @options[:alignment_strategy].new(@body)
30
22
  exception_lineno = @results.has_exception? ? @results.exception.line_number : -1
@@ -32,10 +24,10 @@ class SeeingIsBelieving
32
24
  options = @options.merge pad_to: alignment_strategy.line_length_for(line_number)
33
25
  if exception_lineno == line_number
34
26
  result = sprintf "%s: %s", @results.exception.class_name, @results.exception.message.gsub("\n", '\n')
35
- CommentFormatter.call(line.size, exception_marker, result, options)
27
+ FormatComment.call(line.size, exception_text, result, options)
36
28
  elsif @results[line_number].any?
37
29
  result = @results[line_number].map { |result| result.gsub "\n", '\n' }.join(', ')
38
- CommentFormatter.call(line.size, value_marker, result, options)
30
+ FormatComment.call(line.size, value_text, result, options)
39
31
  else
40
32
  ''
41
33
  end
@@ -44,21 +36,9 @@ class SeeingIsBelieving
44
36
  require 'seeing_is_believing/binary/annotate_end_of_file'
45
37
  AnnotateEndOfFile.add_stdout_stderr_and_exceptions_to new_body, @results, @options
46
38
 
47
- # What's w/ this debugger? maybe this should move higher?
48
- @options.fetch(:debugger).context "OUTPUT"
49
39
  new_body
50
40
  end
51
41
  end
52
-
53
- private
54
-
55
- def value_marker
56
- @value_marker ||= @options.fetch(:markers).fetch(:value)
57
- end
58
-
59
- def exception_marker
60
- @xnextline_marker ||= @options.fetch(:markers).fetch(:exception)
61
- end
62
42
  end
63
43
  end
64
44
  end
@@ -0,0 +1,136 @@
1
+ # encoding: utf-8
2
+ require 'seeing_is_believing/code'
3
+
4
+
5
+ # *sigh* need to find a way to join the annotators.
6
+ # They are sinful ugly, kinda hard to work with,
7
+ # and absurdly duplicated.
8
+
9
+ class SeeingIsBelieving
10
+ module Binary
11
+ # Based on the behaviour of xmpfilger (a binary in the rcodetools gem)
12
+ # See https://github.com/JoshCheek/seeing_is_believing/issues/44 for more details
13
+ class AnnotateMarkedLines
14
+ def self.code_rewriter(markers)
15
+ lambda do |program|
16
+ inspect_linenos = []
17
+ pp_linenos = []
18
+ value_regex = markers[:value][:regex]
19
+ Code.new(program).inline_comments.each do |c|
20
+ next unless c.text[value_regex]
21
+ c.whitespace_col == 0 ? pp_linenos << c.line_number - 1
22
+ : inspect_linenos << c.line_number
23
+ end
24
+
25
+ should_inspect = false
26
+ should_pp = false
27
+ WrapExpressions.call \
28
+ program,
29
+ before_each: -> line_number {
30
+ should_inspect = inspect_linenos.include? line_number
31
+ should_pp = pp_linenos.include? line_number
32
+ should_inspect || should_pp ? '(' : ''
33
+ },
34
+ after_each: -> line_number {
35
+ # 74 b/c pretty print_defaults to 79 (guessing 80 chars with 1 reserved for newline), and
36
+ # 79 - "# => ".length # => 4
37
+ inspect = "$SiB.record_result :inspect, #{line_number}, v"
38
+ pp = "$SiB.record_result(:pp, #{line_number}, v) { PP.pp v, '', 74 }"
39
+
40
+ if should_inspect && should_pp then ").tap { |v| #{inspect}; #{pp} }"
41
+ elsif should_inspect then ").tap { |v| #{inspect} }"
42
+ elsif should_pp then ").tap { |v| #{pp} }"
43
+ else ""
44
+ end
45
+ }
46
+ end
47
+ end
48
+
49
+ def self.call(body, results, options)
50
+ new(body, results, options).call
51
+ end
52
+
53
+ def initialize(body, results, options={})
54
+ @options = options
55
+ @body = body
56
+ @results = results
57
+ end
58
+
59
+ # seems like maybe this should respect the alignment strategy (not what xmpfilter does, but there are other ways I'd like to deviate anyway)
60
+ # and we should just add a new alignment strategy for default xmpfilter style
61
+ def call
62
+ @new_body ||= begin
63
+ require 'seeing_is_believing/binary/rewrite_comments'
64
+ require 'seeing_is_believing/binary/format_comment'
65
+ include_lines = []
66
+
67
+ if @results.has_exception?
68
+ exception_result = sprintf "%s: %s", @results.exception.class_name, @results.exception.message.gsub("\n", '\n')
69
+ exception_lineno = @results.exception.line_number
70
+ include_lines << exception_lineno
71
+ end
72
+
73
+ new_body = RewriteComments.call @body, include_lines: include_lines do |comment|
74
+ exception_on_line = exception_lineno == comment.line_number
75
+ annotate_this_line = comment.text[value_regex]
76
+ pp_annotation = annotate_this_line && comment.whitespace_col.zero?
77
+ normal_annotation = annotate_this_line && !pp_annotation
78
+ if exception_on_line && annotate_this_line
79
+ [comment.whitespace, FormatComment.call(comment.text_col, value_prefix, exception_result, @options)]
80
+ elsif exception_on_line
81
+ whitespace = comment.whitespace
82
+ whitespace = " " if whitespace.empty?
83
+ [whitespace, FormatComment.call(0, exception_prefix, exception_result, @options)]
84
+ elsif normal_annotation
85
+ result = @results[comment.line_number].map { |result| result.gsub "\n", '\n' }.join(', ')
86
+ [comment.whitespace, FormatComment.call(comment.text_col, value_prefix, result, @options)]
87
+ elsif pp_annotation
88
+ result = @results[comment.line_number-1, :pp].map { |result| result.chomp }.join("\n,") # ["1\n2", "1\n2", ...
89
+ swap_leading_whitespace_in_multiline_comment(result)
90
+ comment_lines = result.each_line.map.with_index do |comment_line, result_offest|
91
+ if result_offest == 0
92
+ FormatComment.call(comment.whitespace_col, value_prefix, comment_line.chomp, @options)
93
+ else
94
+ leading_whitespace = " " * comment.text_col
95
+ leading_whitespace << FormatComment.call(comment.whitespace_col, nextline_prefix, comment_line.chomp, @options)
96
+ end
97
+ end
98
+ comment_lines = [value_prefix.rstrip] if comment_lines.empty?
99
+ [comment.whitespace, comment_lines.join("\n")]
100
+ else
101
+ [comment.whitespace, comment.text]
102
+ end
103
+ end
104
+
105
+ require 'seeing_is_believing/binary/annotate_end_of_file'
106
+ AnnotateEndOfFile.add_stdout_stderr_and_exceptions_to new_body, @results, @options
107
+
108
+ new_body
109
+ end
110
+ end
111
+
112
+ def value_prefix
113
+ @value_prefix ||= @options[:markers][:value][:prefix]
114
+ end
115
+
116
+ def nextline_prefix
117
+ @nextline_prefix ||= ('#' + ' '*value_prefix.length.pred)
118
+ end
119
+
120
+ def exception_prefix
121
+ @exception_prefix ||= @options[:markers][:exception][:prefix]
122
+ end
123
+
124
+ def value_regex
125
+ @value_regex ||= @options[:markers][:value][:regex]
126
+ end
127
+
128
+ def swap_leading_whitespace_in_multiline_comment(comment)
129
+ return if comment.scan("\n").size < 2
130
+ return if comment[0] =~ /\S/
131
+ nonbreaking_space = " "
132
+ comment[0] = nonbreaking_space
133
+ end
134
+ end
135
+ end
136
+ end