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

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