seeing_is_believing 3.3.0 → 4.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -72,6 +72,7 @@ Feature: Xmpfilter style
72
72
  # :wibble=>{:magic_word=>"xyzzy"}}
73
73
  """
74
74
 
75
+ @not-2.3
75
76
  Scenario: --xmpfilter-style, when displayed on the next line, prints the string across multiple lines
76
77
  Given the file "xmpfilter-prev-line-is-multiline-string.rb":
77
78
  """
@@ -12,6 +12,7 @@ require 'seeing_is_believing/event_stream/handlers/update_result'
12
12
  class SeeingIsBelieving
13
13
  class Options < HashStruct
14
14
  predicate(:event_handler) { EventStream::Handlers::UpdateResult.new Result.new }
15
+ predicate(:local_cwd) { false }
15
16
  attribute(:filename) { nil }
16
17
  attribute(:encoding) { nil }
17
18
  attribute(:stdin) { "" }
@@ -42,15 +43,17 @@ class SeeingIsBelieving
42
43
  options.debugger.context("REWRITTEN PROGRAM") { new_program }
43
44
 
44
45
  EvaluateByMovingFiles.call \
45
- new_program,
46
46
  filename,
47
+ @program,
48
+ new_program,
47
49
  event_handler: event_handler(options.debugger, options.event_handler),
48
50
  provided_input: options.stdin,
49
51
  require_files: options.require_files,
50
52
  load_path_dirs: options.load_path_dirs,
51
53
  encoding: options.encoding,
52
54
  timeout_seconds: options.timeout_seconds,
53
- max_line_captures: options.max_line_captures
55
+ max_line_captures: options.max_line_captures,
56
+ local_cwd: options.local_cwd?
54
57
 
55
58
  options.event_handler
56
59
  }
@@ -23,7 +23,7 @@ class SeeingIsBelieving
23
23
  end
24
24
 
25
25
  if config.errors.any?
26
- stderr.puts *config.errors, *config.deprecations
26
+ stderr.puts(*config.errors, *config.deprecations)
27
27
  return NONDISPLAYABLE_ERROR_STATUS
28
28
  end
29
29
 
@@ -32,6 +32,11 @@ class SeeingIsBelieving
32
32
  return SUCCESS_STATUS
33
33
  end
34
34
 
35
+ if config.toggle_mark?
36
+ stdout.print engine.toggled_mark
37
+ return SUCCESS_STATUS
38
+ end
39
+
35
40
  if engine.syntax_error?
36
41
  stderr.puts engine.syntax_error
37
42
  return NONDISPLAYABLE_ERROR_STATUS
@@ -12,7 +12,7 @@ class SeeingIsBelieving
12
12
  exception_output_for(results, options)
13
13
 
14
14
  code = Code.new(new_body)
15
- code.rewriter.insert_after_multi code.body_range, output
15
+ code.rewriter.insert_after code.body_range, output
16
16
  new_body.replace code.rewriter.process
17
17
  end
18
18
 
@@ -7,7 +7,7 @@ require 'seeing_is_believing/code'
7
7
 
8
8
  class SeeingIsBelieving
9
9
  module Binary
10
- # Based on the behaviour of xmpfilger (a binary in the rcodetools gem)
10
+ # Based on the behaviour of xmpfilter (a binary in the rcodetools gem)
11
11
  # See https://github.com/JoshCheek/seeing_is_believing/issues/44 for more details
12
12
  class AnnotateMarkedLines
13
13
  def self.map_markers_to_linenos(program, markers)
@@ -46,9 +46,8 @@ class SeeingIsBelieving
46
46
  should_pp = false
47
47
  WrapExpressions.call \
48
48
  program,
49
+ before_all: -> { "BEGIN { $SiB.file_loaded };" },
49
50
  before_each: -> line_number {
50
- # 74 b/c pretty print_defaults to 79 (guessing 80 chars with 1 reserved for newline), and
51
- # 79 - "# => ".length # => 4
52
51
  inspect = "$SiB.record_result(:inspect, #{line_number}, ("
53
52
  pp = "$SiB.record_result(:pp, #{line_number}, ("
54
53
 
@@ -62,7 +61,11 @@ class SeeingIsBelieving
62
61
  end
63
62
  },
64
63
  after_each: -> line_number {
65
- inspect = "))"
64
+ # 74 b/c pretty print_defaults to 79 (guessing 80 chars with 1 reserved for newline), and
65
+ # 79 - "# => ".length # => 4
66
+ # ALSO: This should be configurable, b/c otherwise you have to go into the guts of `pp`
67
+ # https://gist.github.com/JoshCheek/6472c8f334ae493f4ab1f7865e2470e5
68
+ inspect = ")) { |v| v.inspect }"
66
69
  pp = ")) { |v| PP.pp v, '', 74 }"
67
70
 
68
71
  should_inspect = inspect_linenos.include? line_number
@@ -111,7 +114,7 @@ class SeeingIsBelieving
111
114
  normal_annotation = annotate_this_line && !pp_annotation
112
115
  if exception_result && annotate_this_line
113
116
  [comment.whitespace, FormatComment.call(comment.text_col, value_prefix, exception_result, @options)]
114
- elsif exception_result
117
+ elsif exception_result && comment.text.empty?
115
118
  whitespace = comment.whitespace
116
119
  whitespace = " " if whitespace.empty?
117
120
  [whitespace, FormatComment.call(0, exception_prefix, exception_result, @options)]
@@ -17,12 +17,12 @@ class SeeingIsBelieving
17
17
  def call
18
18
  @call ||= begin
19
19
  commentable_lines = CommentableLines.new raw_code
20
- commentable_lines.call.each do |line_number, (index_of_newline, col)|
20
+ commentable_lines.call.each do |line_number, (index_of_newline, _col)|
21
21
  first_index = last_index = index_of_newline
22
22
  first_index -= 1 while first_index > 0 && raw_code[first_index-1] != "\n"
23
23
  comment_text = commenter.call raw_code[first_index...last_index], line_number
24
24
  range = Parser::Source::Range.new(commentable_lines.buffer, first_index, last_index)
25
- commentable_lines.rewriter.insert_after_multi range, comment_text
25
+ commentable_lines.rewriter.insert_after range, comment_text
26
26
  end
27
27
  commentable_lines.rewriter.process
28
28
  end
@@ -54,13 +54,13 @@ class SeeingIsBelieving
54
54
  def remove_lines_whose_newline_is_escaped(line_num_to_location)
55
55
  ors_indexes = code_obj.indexes_of_ors_at_eol
56
56
  line_num_to_location
57
- .select { |line_number, (index_of_newline, col)|
57
+ .select { |line_number, (index_of_newline, _col)|
58
58
  code[index_of_newline-1] == '\\'
59
59
  }
60
- .reject { |line_number, (index_of_newline, col)|
60
+ .reject { |line_number, (index_of_newline, _col)|
61
61
  ors_indexes.include? index_of_newline
62
62
  }
63
- .each { |line_number, (index_of_newline, col)|
63
+ .each { |line_number, (_index_of_newline, _col)|
64
64
  line_num_to_location.delete line_number
65
65
  }
66
66
  end
@@ -73,8 +73,8 @@ class SeeingIsBelieving
73
73
  begin_pos = comment.location.expression.begin_pos
74
74
  end_pos = comment.location.expression.end_pos
75
75
  range = begin_pos...end_pos
76
- line_num_to_location.select { |line_number, (index_of_newline, col)| range.include? index_of_newline }
77
- .each { |line_number, (index_of_newline, col)| line_num_to_location.delete line_number }
76
+ line_num_to_location.select { |line_number, (index_of_newline, _col)| range.include? index_of_newline }
77
+ .each { |line_number, (_index_of_newline, _col)| line_num_to_location.delete line_number }
78
78
  end
79
79
  end
80
80
  end
@@ -82,8 +82,8 @@ class SeeingIsBelieving
82
82
  def remove_lines_inside_of_strings_and_things(line_num_to_location, ast)
83
83
  invalid_boundaries = ranges_of_atomic_expressions ast, []
84
84
  invalid_boundaries.each do |invalid_boundary|
85
- line_num_to_location.select { |line_number, (index_of_newline, col)| invalid_boundary.include? index_of_newline }
86
- .each { |line_number, (index_of_newline, col)| line_num_to_location.delete line_number }
85
+ line_num_to_location.select { |line_number, (index_of_newline, _col)| invalid_boundary.include? index_of_newline }
86
+ .each { |line_number, (_index_of_newline, _col)| line_num_to_location.delete line_number }
87
87
  end
88
88
  end
89
89
 
@@ -30,6 +30,7 @@ class SeeingIsBelieving
30
30
  predicate(:remove_value_prefixes) { true }
31
31
  predicate(:debug) { false }
32
32
  predicate(:ignore_unknown_flags) { false }
33
+ predicate(:toggle_mark) { nil }
33
34
  attribute(:body) { nil }
34
35
  attribute(:filename) { nil }
35
36
  attribute(:errors) { [] }
@@ -107,7 +108,11 @@ class SeeingIsBelieving
107
108
  self.lib_options.rewrite_code = AnnotateMarkedLines.code_rewriter(markers)
108
109
  self.remove_value_prefixes = false
109
110
  self.lib_options.require_files << 'pp'
110
- self.lib_options.require_files << 'seeing_is_believing/customize_pp'
111
+
112
+ when '--toggle-mark'
113
+ extract_positive_int_for.call arg do |n|
114
+ self.toggle_mark = n
115
+ end
111
116
 
112
117
  when '-i', '--inherit-exitstatus', '--inherit-exit-status'
113
118
  self.inherit_exitstatus = true
@@ -181,6 +186,9 @@ class SeeingIsBelieving
181
186
  as = filename
182
187
  end
183
188
 
189
+ when '--local-cwd'
190
+ self.lib_options.local_cwd = true
191
+
184
192
  when '-s', '--alignment-strategy'
185
193
  strategies = {'file' => AlignFile, 'chunk' => AlignChunk, 'line' => AlignLine}
186
194
  strategy_names = strategies.keys.inspect
@@ -249,6 +257,13 @@ class SeeingIsBelieving
249
257
  print_event_stream? && (result_as_json? || annotator == AnnotateMarkedLines) &&
250
258
  add_error("can only have one output format, --stream is not compatible with --json, -x, and --xmpfilter-style")
251
259
 
260
+ toggle_mark? && print_event_stream? &&
261
+ add_error("--toggle-mark and --stream are mutually exclusive")
262
+
263
+ toggle_mark? && result_as_json? &&
264
+ add_error("--toggle-mark and --json are mutually exclusive")
265
+
266
+
252
267
  self.filename = filenames.first
253
268
  self.lib_options.filename = as || filename
254
269
  self.lib_options.debugger = debugger
@@ -297,8 +312,7 @@ class SeeingIsBelieving
297
312
  end
298
313
 
299
314
  def Binary.help_screen(markers)
300
- value = markers[:value][:prefix]
301
- stdout = markers[:stdout][:prefix]
315
+ value = markers[:value][:prefix]
302
316
 
303
317
  <<FLAGS
304
318
  Usage: seeing_is_believing [options] [filename]
@@ -326,7 +340,7 @@ Options:
326
340
  file => the entire file is at the same alignment
327
341
  line => each line is at its own alignment
328
342
  --[no-]-interline-align # align results on adjacent lines when they have the same number of results
329
- defautls to --align
343
+ defaults to --align
330
344
  -t, --timeout-seconds s # how long to evaluate the source file before timing out
331
345
  0 means it will never timeout (this is the default)
332
346
  accepts floating point values (e.g. 0.5 would timeout after half a second)
@@ -334,9 +348,12 @@ Options:
334
348
  -r, --require file # additional files to be required before running the program
335
349
  -e, --program program-body # pass the program body to execute as an argument
336
350
  -a, --as filename # run the program as if it was the specified filename
351
+ --local-cwd # run the program from the file's directory instead of the calling program's CWD
337
352
  -c, --clean # remove annotations from previous runs of seeing_is_believing
338
353
  -g, --debug # print debugging information
354
+ --debug-to FILE # print debugging information to FILE
339
355
  -x, --xmpfilter-style # annotate marked lines instead of every line
356
+ --toggle-mark n # add / remove annotation on line n
340
357
  -j, --json # print results in json format (i.e. so another program can consume them)
341
358
  -i, --inherit-exitstatus # exit with the exit status of the program being evaluated
342
359
  --stream # a JSON stream of every event ias it is seen (such as recording a line)
@@ -18,12 +18,69 @@ class SeeingIsBelieving
18
18
 
19
19
  def cleaned_body
20
20
  @cleaned_body ||= if missing_newline?
21
- normalized_cleaned_body.chomp!
21
+ normalized_cleaned_body.chomp
22
22
  else
23
23
  normalized_cleaned_body
24
24
  end
25
25
  end
26
26
 
27
+
28
+ require 'seeing_is_believing/binary/rewrite_comments'
29
+ require 'seeing_is_believing/binary/format_comment'
30
+ module ToggleMark
31
+ def self.call(options)
32
+ options = options.dup
33
+ body = options.delete :body
34
+ line = options.delete :line
35
+ markers = options.delete :markers
36
+ alignment_strategy = options.delete :alignment_strategy
37
+
38
+ marker_regexes = markers.values.map(&:regex)
39
+ RewriteComments.call body, include_lines: [line] do |comment|
40
+ if line == comment.line_number && marker_regexes.any? { |r| r =~ comment.text }
41
+ new_comment = ''
42
+ elsif line == comment.line_number && comment.text.empty?
43
+ new_comment = FormatComment.call(
44
+ comment.whitespace_col,
45
+ markers.value.prefix,
46
+ '',
47
+ options.merge(
48
+ pad_to: alignment_strategy.line_length_for(comment.line_number)
49
+ ),
50
+ )
51
+ elsif line == comment.line_number
52
+ new_comment = comment.whitespace + comment.text
53
+ elsif match = markers.value.regex.match(comment.text)
54
+ new_comment = FormatComment.call(
55
+ comment.whitespace_col,
56
+ markers.value.prefix,
57
+ match.post_match,
58
+ options.merge(
59
+ pad_to: alignment_strategy.line_length_for(comment.line_number)
60
+ )
61
+ )
62
+ else
63
+ new_comment = comment.whitespace + comment.text
64
+ end
65
+ [new_comment[/^\s*/], new_comment.lstrip]
66
+ end
67
+ end
68
+ end
69
+
70
+ def toggled_mark
71
+ body = config.body
72
+ body += "\n" if missing_newline?
73
+ toggled = ToggleMark.call(
74
+ body: body,
75
+ line: config.toggle_mark,
76
+ markers: config.markers,
77
+ alignment_strategy: config.annotator_options.alignment_strategy.new(normalized_cleaned_body),
78
+ options: config.annotator_options.to_h,
79
+ )
80
+ toggled.chomp! if missing_newline?
81
+ toggled
82
+ end
83
+
27
84
  def syntax_error?
28
85
  code.syntax.invalid?
29
86
  end
@@ -1,3 +1,7 @@
1
+ # Polyfill String#scrub on Ruby 2.0.0
2
+ require 'seeing_is_believing/compatibility'
3
+ using SeeingIsBelieving::Compatibility
4
+
1
5
  class SeeingIsBelieving
2
6
  module Binary
3
7
  # not sure I like this name, it formats comments that show results
@@ -64,12 +68,11 @@ class SeeingIsBelieving
64
68
  end
65
69
 
66
70
  def escape_non_printable(str, omissions)
67
- str.each_char
68
- .map { |char|
69
- next char if 0x20 <= char.ord # above this has a printable representation
70
- next char if omissions.include? char
71
- char.inspect[1...-1]
72
- }.join('')
71
+ str.scrub { |c| c.inspect[1...-1] }
72
+ .gsub(/[\u0000-\u0020]/) { |char|
73
+ next char if omissions.include? char
74
+ char.inspect[1...-1]
75
+ }
73
76
  end
74
77
  end
75
78
  end
@@ -76,7 +76,7 @@ class SeeingIsBelieving
76
76
  [annotation, comment]
77
77
  }
78
78
  .slice_before { |annotation, comment| annotation } # annotations begin chunks
79
- .select { |(annotation, start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
79
+ .select { |(annotation, _start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
80
80
  .map { |(annotation, start), *rest| # end the chunk if the comment doesn't meet nextline criteria
81
81
  nextline_comments = []
82
82
  prev = start
@@ -25,7 +25,7 @@ class SeeingIsBelieving
25
25
  @raw = raw_code
26
26
  @buffer = Parser::Source::Buffer.new(name||"SeeingIsBelieving")
27
27
  @buffer.source = raw
28
- @rewriter = Parser::Source::Rewriter.new buffer
28
+ @rewriter = Parser::Source::TreeRewriter.new buffer
29
29
  builder = Parser::Builders::Default.new.tap { |b| b.emit_file_line_as_literals = false }
30
30
  @parser = Parser::CurrentRuby.new builder
31
31
  @syntax = Syntax.new
@@ -91,8 +91,8 @@ class SeeingIsBelieving
91
91
  def indexes_of_ors_at_eol
92
92
  Set.new(
93
93
  @tokens.select { |type, *| type == :tGVAR }
94
- .select { |_, (var, range)| var == '$\\'.freeze }
95
- .map { |_, (var, range)| range.end_pos }
94
+ .select { |_, (var, _range)| var == '$\\'.freeze }
95
+ .map { |_, (_var, range)| range.end_pos }
96
96
  )
97
97
  end
98
98
 
@@ -106,10 +106,10 @@ class SeeingIsBelieving
106
106
 
107
107
  def body_range_from_tokens(tokens)
108
108
  return range_for(0, 0) if raw.start_with? "__END__\n"
109
- (name, (_, range)) = tokens.max_by { |name, (data, range)| range.end_pos } ||
109
+ (name, (_, range)) = tokens.max_by { |name, (_data, range)| range.end_pos } ||
110
110
  [nil, [nil, range_for(0, 1)]]
111
111
  end_pos = range.end_pos
112
- end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END #
112
+ end_pos += 1 if name == :tCOMMENT || name == :tSTRING_END || name == :tSEMI
113
113
  range_for 0, end_pos
114
114
  end
115
115
 
@@ -0,0 +1,28 @@
1
+ class SeeingIsBelieving
2
+ module Compatibility
3
+ end
4
+ end
5
+
6
+ # Ruby 2.0.0 is soooooo painful >.<
7
+ # want to stop supporting this so bad!!
8
+ is_v2_0 = !String.instance_methods.include?(:scrub)
9
+
10
+ is_v2_0 && begin
11
+ old_verbose, $VERBOSE = $VERBOSE, nil
12
+ module SeeingIsBelieving::Compatibility
13
+ refine String do
14
+ def scrub(char=nil, &block)
15
+ char && block = lambda { |c| char }
16
+ each_char.inject("") do |new_str, char|
17
+ if char.valid_encoding?
18
+ new_str << char
19
+ else
20
+ new_str << block.call(char)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ ensure
27
+ $VERBOSE = old_verbose
28
+ end
@@ -13,12 +13,14 @@
13
13
 
14
14
  require 'rbconfig'
15
15
  require 'socket'
16
- require 'seeing_is_believing/error'
16
+ require "childprocess"
17
+
17
18
  require 'seeing_is_believing/result'
18
19
  require 'seeing_is_believing/debugger'
19
- require 'seeing_is_believing/hard_core_ensure'
20
+ require 'seeing_is_believing/swap_files'
20
21
  require 'seeing_is_believing/event_stream/consumer'
21
- require "childprocess"
22
+ require 'seeing_is_believing/event_stream/events'
23
+
22
24
  ChildProcess.posix_spawn = true # forking locks up for some reason when we run SiB inside of SiB
23
25
 
24
26
  class SeeingIsBelieving
@@ -27,69 +29,40 @@ class SeeingIsBelieving
27
29
  new(*args).call
28
30
  end
29
31
 
30
- attr_accessor :program, :filename, :provided_input, :require_flags, :load_path_flags, :encoding, :timeout_seconds, :debugger, :event_handler, :max_line_captures
32
+ attr_accessor :user_program, :rewritten_program, :provided_input, :require_flags, :load_path_flags, :encoding, :timeout_seconds, :debugger, :event_handler, :max_line_captures
31
33
 
32
- def initialize(program, filename, options={})
34
+ attr_accessor :file_directory, :file_path, :local_cwd, :relative_filename, :backup_path
35
+
36
+ def initialize(file_path, user_program, rewritten_program, options={})
33
37
  options = options.dup
34
- self.program = program
35
- self.filename = filename
38
+ self.user_program = user_program
39
+ self.rewritten_program = rewritten_program
36
40
  self.encoding = options.delete(:encoding) || "u"
37
41
  self.timeout_seconds = options.delete(:timeout_seconds) || 0 # 0 is the new infinity
38
42
  self.provided_input = options.delete(:provided_input) || String.new
39
43
  self.event_handler = options.delete(:event_handler) || raise(ArgumentError, "must provide an :event_handler")
40
- self.load_path_flags = (options.delete(:load_path_dirs) || []).map { |dir| ['-I', dir] }.flatten
44
+ self.load_path_flags = (options.delete(:load_path_dirs) || []).flat_map { |dir| ['-I', dir] }
41
45
  self.require_flags = (options.delete(:require_files) || ['seeing_is_believing/the_matrix']).map { |filename| ['-r', filename] }.flatten
42
46
  self.max_line_captures = (options.delete(:max_line_captures) || Float::INFINITY) # (optimization: child stops producing results at this number, even though it might make more sense for the consumer to stop emitting them)
47
+ self.local_cwd = options.delete(:local_cwd) || false
48
+ self.file_path = file_path
49
+ self.file_directory = File.dirname file_path
50
+ file_name = File.basename file_path
51
+ self.relative_filename = local_cwd ? file_name : file_path
52
+ self.backup_path = File.join file_directory, "seeing_is_believing_backup.#{file_name}"
53
+
43
54
  options.any? && raise(ArgumentError, "Unknown options: #{options.inspect}")
44
55
  end
45
56
 
46
57
  def call
47
- HardCoreEnsure.call \
48
- code: -> {
49
- we_will_not_overwrite_existing_backup_file!
50
- backup_existing_file
51
- write_program_to_file
52
- evaluate_file
53
- },
54
- ensure: -> {
55
- set_back_to_initial_conditions
56
- }
57
- end
58
-
59
- def file_directory
60
- File.dirname filename
61
- end
62
-
63
- def backup_filename
64
- File.join file_directory, "seeing_is_believing_backup.#{File.basename filename}"
65
- end
66
-
67
- private
68
-
69
- def we_will_not_overwrite_existing_backup_file!
70
- raise TempFileAlreadyExists.new(filename, backup_filename) if File.exist? backup_filename
71
- end
72
-
73
- def backup_existing_file
74
- return unless File.exist? filename
75
- File.rename filename, backup_filename
76
- @was_backed_up = true
77
- end
78
-
79
- def set_back_to_initial_conditions
80
- if @was_backed_up
81
- File.rename(backup_filename, filename)
82
- else
83
- File.delete(filename)
58
+ SwapFiles.call file_path, backup_path, user_program, rewritten_program do |swap_files|
59
+ evaluate_file swap_files
84
60
  end
85
61
  end
86
62
 
87
- def write_program_to_file
88
- File.open(filename, 'w', external_encoding: "utf-8") { |f| f.write program.to_s }
89
- end
90
-
63
+ private
91
64
 
92
- def evaluate_file
65
+ def evaluate_file(swap_files)
93
66
  event_server = TCPServer.new(0) # dynamically allocates an available port
94
67
 
95
68
  # setup streams
@@ -101,11 +74,12 @@ class SeeingIsBelieving
101
74
  [Marshal.dump(
102
75
  event_stream_port: event_server.addr[1],
103
76
  max_line_captures: max_line_captures,
104
- num_lines: program.lines.count,
105
- filename: filename
77
+ num_lines: user_program.lines.count,
78
+ filename: relative_filename,
106
79
  )].pack('m0')
107
80
 
108
81
  child = ChildProcess.build(*popen_args)
82
+ child.cwd = file_directory if local_cwd
109
83
  child.leader = true
110
84
  child.duplex = true
111
85
  child.environment.merge!(env)
@@ -124,13 +98,23 @@ class SeeingIsBelieving
124
98
 
125
99
  # send stdin (char at a time b/c input could come from a stream)
126
100
  Thread.new do
127
- provided_input.each_char { |char| child.io.stdin.write char }
128
- child.io.stdin.close
101
+ begin
102
+ provided_input.each_char { |char| child.io.stdin.write char }
103
+ rescue
104
+ # don't explode if child closes IO
105
+ ensure
106
+ child.io.stdin.close
107
+ end
129
108
  end
130
109
 
131
110
  # set up the event consumer
132
111
  consumer = EventStream::Consumer.new(events: eventstream, stdout: stdout, stderr: stderr)
133
- consumer_thread = Thread.new { consumer.each { |e| event_handler.call e } }
112
+ consumer_thread = Thread.new do
113
+ consumer.each do |e|
114
+ swap_files.show_user_program if e.is_a? SeeingIsBelieving::EventStream::Events::FileLoaded
115
+ event_handler.call e
116
+ end
117
+ end
134
118
 
135
119
  # wait for completion
136
120
  if timeout_seconds == 0
@@ -148,13 +132,19 @@ class SeeingIsBelieving
148
132
  # On Windows, we need to call stop if there is an error since it interrupted
149
133
  # the previos waiting/polling. If we don't call stop, in that situation, it will
150
134
  # leave orphan processes. On Unix, we need to always call stop or it may leave orphans
151
- if ChildProcess.unix?
152
- child.stop
153
- elsif $!
154
- child.stop
155
- consumer.process_exitstatus(child.exit_code)
135
+ begin
136
+ if ChildProcess.unix?
137
+ child.stop
138
+ elsif $!
139
+ child.stop
140
+ consumer.process_exitstatus(child.exit_code)
141
+ end
142
+ child.alive? && child.stop
143
+ rescue ChildProcess::Error
144
+ # On AppVeyor, I keep getting errors
145
+ # The handle is invalid: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/22
146
+ # Access is denied: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/24
156
147
  end
157
- cleanup_run(child)
158
148
  close_streams(stdout, stderr, eventstream, event_server)
159
149
  end
160
150
 
@@ -165,20 +155,11 @@ class SeeingIsBelieving
165
155
  '-I', File.realpath('..', __dir__), # add lib to the load path
166
156
  *load_path_flags, # users can inject dirs to be added to the load path
167
157
  *require_flags, # users can inject files to be required
168
- filename]
158
+ relative_filename]
169
159
  end
170
160
 
171
161
  def close_streams(*streams)
172
162
  streams.each { |io| io.close unless io.closed? }
173
163
  end
174
-
175
- # On AppVeyor, I keep getting errors
176
- # The handle is invalid: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/22
177
- # Access is denied: https://ci.appveyor.com/project/JoshCheek/seeing-is-believing/build/24
178
- def cleanup_run(child, *streams)
179
- child.alive? && child.stop
180
- rescue ChildProcess::Error
181
- # noop
182
- end
183
164
  end
184
165
  end