seeing_is_believing 3.3.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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