seeing_is_believing 3.1.1 → 3.2.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +1 -0
  3. data/Rakefile +43 -16
  4. data/Readme.md +27 -242
  5. data/appveyor.yml +29 -0
  6. data/bin/seeing_is_believing +2 -1
  7. data/features/deprecated-flags.feature +19 -0
  8. data/features/errors.feature +57 -0
  9. data/features/examples.feature +4 -2
  10. data/features/flags.feature +35 -2
  11. data/features/regression.feature +107 -10
  12. data/features/support/env.rb +61 -2
  13. data/features/xmpfilter-style.feature +3 -2
  14. data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +11 -9
  15. data/lib/seeing_is_believing/binary/annotate_every_line.rb +9 -8
  16. data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +9 -8
  17. data/lib/seeing_is_believing/binary/config.rb +19 -3
  18. data/lib/seeing_is_believing/binary/engine.rb +5 -10
  19. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +60 -45
  20. data/lib/seeing_is_believing/event_stream/consumer.rb +6 -1
  21. data/lib/seeing_is_believing/event_stream/handlers/debug.rb +5 -1
  22. data/lib/seeing_is_believing/event_stream/producer.rb +1 -1
  23. data/lib/seeing_is_believing/hard_core_ensure.rb +6 -0
  24. data/lib/seeing_is_believing/result.rb +26 -14
  25. data/lib/seeing_is_believing/safe.rb +6 -1
  26. data/lib/seeing_is_believing/the_matrix.rb +16 -4
  27. data/lib/seeing_is_believing/version.rb +1 -1
  28. data/lib/seeing_is_believing/wrap_expressions.rb +1 -1
  29. data/lib/seeing_is_believing.rb +7 -10
  30. data/seeing_is_believing.gemspec +9 -8
  31. data/spec/binary/config_spec.rb +65 -4
  32. data/spec/binary/engine_spec.rb +1 -1
  33. data/spec/evaluate_by_moving_files_spec.rb +31 -5
  34. data/spec/event_stream_spec.rb +14 -6
  35. data/spec/hard_core_ensure_spec.rb +70 -44
  36. data/spec/seeing_is_believing_spec.rb +136 -42
  37. data/spec/spec_helper.rb +8 -0
  38. data/spec/wrap_expressions_spec.rb +15 -0
  39. metadata +21 -6
@@ -240,7 +240,7 @@ Feature:
240
240
 
241
241
 
242
242
  Scenario: Repeated invocations
243
- When I run "echo 'puts 1' | seeing_is_believing | seeing_is_believing"
243
+ When I run the pipeline "echo puts 1" | "seeing_is_believing" | "seeing_is_believing"
244
244
  Then stdout is:
245
245
  """
246
246
  puts 1 # => nil
@@ -322,9 +322,10 @@ Feature:
322
322
  Scenario: A program overriding stdout/stderr
323
323
  Given the file "black_hole.rb":
324
324
  """
325
- File.open '/dev/null', 'w' do |black_hole|
326
- STDERR = $stderr = black_hole
327
- STDOUT = $stdout = black_hole
325
+ require 'rubygems'
326
+ File.open IO::NULL, 'w' do |black_hole|
327
+ STDERR = $stderr = black_hole; nil
328
+ STDOUT = $stdout = black_hole; nil
328
329
  puts "You won't see this, it goes into the black hole"
329
330
  system %q(ruby -e '$stdout.puts "stdout gets past it b/c of dumb ruby bug"')
330
331
  system %q(ruby -e '$stderr.puts "stderr gets past it b/c of dumb ruby bug"')
@@ -335,9 +336,10 @@ Feature:
335
336
  And the exit status is 0
336
337
  And stdout is:
337
338
  """
338
- File.open '/dev/null', 'w' do |black_hole| # => File
339
- STDERR = $stderr = black_hole # => #<File:/dev/null>
340
- STDOUT = $stdout = black_hole # => #<File:/dev/null>
339
+ require 'rubygems' # => false
340
+ File.open IO::NULL, 'w' do |black_hole| # => File
341
+ STDERR = $stderr = black_hole; nil # => nil
342
+ STDOUT = $stdout = black_hole; nil # => nil
341
343
  puts "You won't see this, it goes into the black hole" # => nil
342
344
  system %q(ruby -e '$stdout.puts "stdout gets past it b/c of dumb ruby bug"') # => true
343
345
  system %q(ruby -e '$stderr.puts "stderr gets past it b/c of dumb ruby bug"') # => true
@@ -420,7 +422,7 @@ Feature:
420
422
  And stdout includes "zomg"
421
423
 
422
424
 
423
- Scenario: Comments with makers elsewhere in them
425
+ Scenario: Comments with markers elsewhere in them
424
426
  Given the file "comments_with_markers_elsewhere.rb":
425
427
  """
426
428
  # a # => a
@@ -463,9 +465,13 @@ Feature:
463
465
  """
464
466
 
465
467
 
466
- # not going to get too detailed on what it prints, b/c that message seems pretty fragile,
468
+ # See this issue for the issue we're testing for: https://github.com/JoshCheek/seeing_is_believing/issues/46
469
+ # See this issue for why we turn it off on 2.4: https://github.com/flori/json/issues/309
470
+ #
471
+ # Not going to get too detailed on what it prints, b/c that message seems pretty fragile,
467
472
  # but just generally that it doesn't fkn blow up
468
- Scenario: https://github.com/JoshCheek/seeing_is_believing/issues/46
473
+ @not-2.4.0
474
+ Scenario: Old JSON bug
469
475
  Given the file "json_and_encodings.rb":
470
476
  """
471
477
  # encoding: utf-8
@@ -482,6 +488,27 @@ Feature:
482
488
  """
483
489
 
484
490
 
491
+ # https://github.com/JoshCheek/seeing_is_believing/wiki/Encodings
492
+ # https://github.com/JoshCheek/seeing_is_believing/issues/109
493
+ Scenario: Assumes utf-8 for files regardless of what Ruby thinks
494
+ Given the environment variable "LANG" is set to ''
495
+ And the file "utf8_file_without_magic_comment.rb" "縧 = 1"
496
+ When I run "seeing_is_believing utf8_file_without_magic_comment.rb"
497
+ Then stderr is empty
498
+ And the exit status is 0
499
+ And stdout is "縧 = 1 # => 1"
500
+
501
+
502
+ # https://github.com/JoshCheek/seeing_is_believing/issues/109
503
+ Scenario: Assumes utf-8 for files regardless of what Ruby thinks
504
+ Given the environment variable "LANG" is set to ''
505
+ Given the stdin content "縧 = 1"
506
+ When I run "seeing_is_believing utf8_file_without_magic_comment.rb"
507
+ Then stderr is empty
508
+ And the exit status is 0
509
+ And stdout is "縧 = 1 # => 1"
510
+
511
+
485
512
  Scenario: Correctly identify end of file
486
513
  Given the file "fake_data_segment.rb":
487
514
  """
@@ -656,3 +683,73 @@ Feature:
656
683
  And stdout includes 'Zomg # => lolol'
657
684
  And stdout includes '# ~> ZeroDivisionError'
658
685
  And stdout includes '# ~> divided by 0'
686
+
687
+
688
+ Scenario: All objects have an object id (Issue #91)
689
+ Given the file "object_ids.rb":
690
+ """
691
+ ObjectSpace.each_object { |o| o.object_id || p(obj: o) }#
692
+ """
693
+ When I run "seeing_is_believing object_ids.rb"
694
+ Then stderr is empty
695
+ And stdout is:
696
+ """
697
+ ObjectSpace.each_object { |o| o.object_id || p(obj: o) }#
698
+ """
699
+
700
+
701
+ Scenario: Does not blow up when the program closes its stdin/stdout/stderr
702
+ Given the stdin content "input"
703
+ And the file "closed_pipes.rb":
704
+ """
705
+ [$stdin, $stdout, $stderr].each &:close#
706
+ """
707
+ When I run "seeing_is_believing closed_pipes.rb"
708
+ Then stderr is empty
709
+ And the exit status is 0
710
+ And stdout is:
711
+ """
712
+ [$stdin, $stdout, $stderr].each &:close#
713
+ """
714
+
715
+
716
+ Scenario: Overriding Symbol#inspect
717
+ Given the file "overriding_symbol_inspect.rb":
718
+ """
719
+ :abc # =>
720
+ class Symbol
721
+ def inspect
722
+ "overridden"
723
+ end
724
+ end
725
+ :abc # =>
726
+ """
727
+ When I run "seeing_is_believing overriding_symbol_inspect.rb -x"
728
+ Then stderr is empty
729
+ And the exit status is 0
730
+ Then stdout is:
731
+ """
732
+ :abc # => :abc
733
+ class Symbol
734
+ def inspect
735
+ "overridden"
736
+ end
737
+ end
738
+ :abc # => overridden
739
+ """
740
+
741
+
742
+ Scenario: SiB running SiB
743
+ Given the file "sib_running_sib.rb":
744
+ """
745
+ require 'seeing_is_believing'
746
+ SeeingIsBelieving.call("1+1").result[1][0]
747
+ """
748
+ When I run "seeing_is_believing sib_running_sib.rb"
749
+ Then stderr is empty
750
+ And the exit status is 0
751
+ And stdout is:
752
+ """
753
+ require 'seeing_is_believing' # => true
754
+ SeeingIsBelieving.call("1+1").result[1][0] # => "2"
755
+ """
@@ -1,6 +1,49 @@
1
1
  require_relative '../../lib/seeing_is_believing/version'
2
2
 
3
3
  require 'haiti'
4
+ # A lot of the stuff in this file should get moved into haiti
5
+
6
+ module Haiti
7
+ module CommandLineHelpers
8
+ # overwriting this method while trying to get windows support working,
9
+ # it looks like the underlying shell is treating the commands differently
10
+ # probably on Unix it invoked `sh` and did shady shit with splitting a string into an array on whitespace
11
+ # and on Windows (powershell?) it expected an actual array of strings
12
+ require 'shellwords'
13
+ def execute(command_string, stdin_data, env_vars)
14
+ stdin_data ||= ''
15
+ env_vars ||= {}
16
+ in_proving_grounds do
17
+ with_bin_in_path do
18
+ Invocation.new *Open3.capture3(env_vars, command_string, stdin_data: stdin_data)
19
+ end
20
+ end
21
+ end
22
+
23
+ def execute_pipeline(command_strings, stdin_data, env_vars)
24
+ stdin_data ||= ''
25
+ env_vars ||= {}
26
+ in_proving_grounds do
27
+ with_bin_in_path do
28
+ ioin, ioout, pids = Open3.pipeline_rw *command_strings.map { |cmd| [env_vars, cmd] }
29
+ ioin.print stdin_data
30
+ ioin.close
31
+ stderr = "" # uh... how do I record it for real?
32
+ Invocation.new ioout.read, stderr, pids.last.value
33
+ end
34
+ end
35
+ end
36
+
37
+ def with_bin_in_path
38
+ original_path = ENV['PATH']
39
+ dirs = ENV["PATH"].split(File::PATH_SEPARATOR)
40
+ ENV['PATH'] = [config.bin_dir, *dirs].join(File::PATH_SEPARATOR)
41
+ yield
42
+ ensure
43
+ ENV['PATH'] = original_path
44
+ end
45
+ end
46
+ end
4
47
 
5
48
  module SiBHelpers
6
49
  def method_result(name)
@@ -18,8 +61,9 @@ end
18
61
  World SiBHelpers
19
62
 
20
63
  Haiti.configure do |config|
21
- config.proving_grounds_dir = File.expand_path '../../../proving_grounds', __FILE__
22
- config.bin_dir = File.expand_path '../../../bin', __FILE__
64
+ lib_root = File.join __dir__, '..', '..'
65
+ config.proving_grounds_dir = File.expand_path 'proving_grounds', lib_root
66
+ config.bin_dir = File.expand_path 'bin', lib_root
23
67
  end
24
68
 
25
69
 
@@ -41,3 +85,18 @@ end
41
85
  Given %q(the file '$filename' '$body') do |filename, body|
42
86
  Haiti::CommandLineHelpers.write_file filename, eval_curlies(body)
43
87
  end
88
+
89
+ Given /^I run the pipeline "([^"]*)"(?: *\| *"([^"]*)")*$/ do |*commands|
90
+ @last_executed = Haiti::CommandLineHelpers.execute_pipeline(
91
+ commands,
92
+ @stdin_data,
93
+ @env_vars_to_set
94
+ )
95
+ end
96
+
97
+ Given(/^the binary file "([^"]*)" "([^"]*)"$/) do |filename, body|
98
+ Haiti::CommandLineHelpers.in_proving_grounds do
99
+ FileUtils.mkdir_p File.dirname filename
100
+ File.open(filename, 'wb') { |file| file.write body }
101
+ end
102
+ end
@@ -90,13 +90,14 @@ Feature: Xmpfilter style
90
90
  # "0123456789\n"
91
91
  """
92
92
 
93
+
93
94
  Scenario: --xmpfilter-style overrides previous multiline results
94
95
  Given the file "xmpfilter-prev-line2.rb":
95
96
  """
96
97
  {foo: 42, bar: {baz: 1, buz: 2, fuz: 3}, wibble: {magic_word: "xyzzy"}}
97
98
  # =>
98
99
  """
99
- When I run "seeing_is_believing --xmpfilter-style xmpfilter-prev-line2.rb | seeing_is_believing --xmpfilter-style"
100
+ When I run the pipeline "seeing_is_believing --xmpfilter-style xmpfilter-prev-line2.rb" | "seeing_is_believing --xmpfilter-style"
100
101
  Then stdout is:
101
102
  """
102
103
  {foo: 42, bar: {baz: 1, buz: 2, fuz: 3}, wibble: {magic_word: "xyzzy"}}
@@ -272,7 +273,7 @@ Feature: Xmpfilter style
272
273
  # :wibble=>{:magic_word=>"xyzzy"}}
273
274
  end
274
275
  """
275
- When I run "seeing_is_believing -x mutltiline_output_repeatedly_invoked.rb | seeing_is_believing -x"
276
+ When I run the pipeline "seeing_is_believing -x mutltiline_output_repeatedly_invoked.rb" | "seeing_is_believing -x"
276
277
  Then stdout is:
277
278
  """
278
279
  3.times do
@@ -37,15 +37,17 @@ class SeeingIsBelieving
37
37
  def exception_output_for(results, options)
38
38
  return '' unless results.has_exception?
39
39
  exception_marker = options[:markers][:exception][:prefix]
40
- exception = results.exception
41
- output = "\n"
42
- output << FormatComment.new(0, exception_marker, exception.class_name, options).call << "\n"
43
- exception.message.each_line do |line|
44
- output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
45
- end
46
- output << exception_marker.sub(/\s+$/, '') << "\n"
47
- exception.backtrace.each do |line|
48
- output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
40
+ output = ""
41
+ results.exceptions.each do |exception|
42
+ output << "\n"
43
+ output << FormatComment.new(0, exception_marker, exception.class_name, options).call << "\n"
44
+ exception.message.each_line do |line|
45
+ output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
46
+ end
47
+ output << exception_marker.sub(/\s+$/, '') << "\n"
48
+ exception.backtrace.each do |line|
49
+ output << FormatComment.new(0, exception_marker, line.chomp, options).call << "\n"
50
+ end
49
51
  end
50
52
  output
51
53
  end
@@ -18,23 +18,24 @@ class SeeingIsBelieving
18
18
  @new_body ||= begin
19
19
  require 'seeing_is_believing/binary/comment_lines'
20
20
  require 'seeing_is_believing/binary/format_comment'
21
- exception_text = @options[:markers][:exception][:prefix]
22
- value_text = @options[:markers][:value][:prefix]
21
+ exception_prefix = @options[:markers][:exception][:prefix]
22
+ value_prefix = @options[:markers][:value][:prefix]
23
+ exceptions = Hash.[] @results.exceptions.map { |e| [e.line_number, e] }
23
24
 
24
25
  alignment_strategy = @options[:alignment_strategy].new(@body)
25
- exception_lineno = @results.has_exception? ? @results.exception.line_number : -1
26
26
  new_body = CommentLines.call @body do |line, line_number|
27
- options = @options.merge pad_to: alignment_strategy.line_length_for(line_number)
28
- if exception_lineno == line_number
29
- result = sprintf "%s: %s", @results.exception.class_name, @results.exception.message.gsub("\n", '\n')
30
- FormatComment.call(line.size, exception_text, result, options)
27
+ exception = exceptions[line_number]
28
+ options = @options.merge pad_to: alignment_strategy.line_length_for(line_number)
29
+ if exception
30
+ result = sprintf "%s: %s", exception.class_name, exception.message.gsub("\n", '\n')
31
+ FormatComment.call(line.size, exception_prefix, result, options)
31
32
  elsif @results[line_number].any?
32
33
  if @options[:interline_align]
33
34
  result = @interline_align.call line_number, @results[line_number].map { |result| result.gsub "\n", '\n' }
34
35
  else
35
36
  result = @results[line_number].map { |result| result.gsub "\n", '\n' }.join(', ')
36
37
  end
37
- FormatComment.call(line.size, value_text, result, options)
38
+ FormatComment.call(line.size, value_prefix, result, options)
38
39
  else
39
40
  ''
40
41
  end
@@ -94,23 +94,24 @@ class SeeingIsBelieving
94
94
  @new_body ||= begin
95
95
  require 'seeing_is_believing/binary/rewrite_comments'
96
96
  require 'seeing_is_believing/binary/format_comment'
97
- include_lines = []
97
+ include_lines = []
98
+ exception_results = {}
98
99
 
99
- if @results.has_exception?
100
- exception_result = sprintf "%s: %s", @results.exception.class_name, @results.exception.message.gsub("\n", '\n')
101
- exception_lineno = @results.exception.line_number
102
- include_lines << exception_lineno
100
+ @results.exceptions.each do |exception|
101
+ exception_results[exception.line_number] =
102
+ sprintf "%s: %s", exception.class_name, exception.message.gsub("\n", '\n')
103
+ include_lines << exception.line_number
103
104
  end
104
105
 
105
106
  _, pp_map = self.class.map_markers_to_linenos(@body, @options[:markers])
106
107
  new_body = RewriteComments.call @body, include_lines: include_lines do |comment|
107
- exception_on_line = exception_lineno == comment.line_number
108
+ exception_result = exception_results[comment.line_number]
108
109
  annotate_this_line = comment.text[value_regex]
109
110
  pp_annotation = annotate_this_line && comment.whitespace_col.zero?
110
111
  normal_annotation = annotate_this_line && !pp_annotation
111
- if exception_on_line && annotate_this_line
112
+ if exception_result && annotate_this_line
112
113
  [comment.whitespace, FormatComment.call(comment.text_col, value_prefix, exception_result, @options)]
113
- elsif exception_on_line
114
+ elsif exception_result
114
115
  whitespace = comment.whitespace
115
116
  whitespace = " " if whitespace.empty?
116
117
  [whitespace, FormatComment.call(0, exception_prefix, exception_result, @options)]
@@ -79,6 +79,11 @@ class SeeingIsBelieving
79
79
  self.deprecations << DeprecatedArgMessage.new(explanation: explanation, args: args)
80
80
  end
81
81
 
82
+ encodings_are_deprecated = lambda do |*args|
83
+ saw_deprecated.call "The ability to set encodings is deprecated. If you need this, details are at https://github.com/JoshCheek/seeing_is_believing/wiki/Encodings",
84
+ *args
85
+ end
86
+
82
87
  next_arg = lambda do |flagname, argtype, &on_success|
83
88
  arg = args.shift
84
89
  arg ? on_success.call(arg) :
@@ -204,17 +209,26 @@ class SeeingIsBelieving
204
209
 
205
210
  when /\A-K(.+)/
206
211
  self.lib_options.encoding = $1
212
+ encodings_are_deprecated.call arg
207
213
 
208
214
  when '-K', '--encoding'
209
215
  next_arg.call arg, "an encoding" do |encoding|
210
216
  self.lib_options.encoding = encoding
217
+ encodings_are_deprecated.call arg, encoding
211
218
  end
212
219
 
213
220
  when /^(-.|--.*)$/
214
221
  unknown_flags << arg
215
222
 
216
223
  when /^-[^-]/
217
- args.unshift *arg.scan(/[^-]\+?/).map { |flag| "-#{flag}" }
224
+ needs_arg = 'adDeIKnrst'
225
+ arg.scan(/[#{needs_arg}].*|h\+|[^-]\+?/).reverse.each do |flag|
226
+ if flag =~ /([#{needs_arg}])(.+)/
227
+ args.unshift "-#{$1}", $2
228
+ else
229
+ args.unshift "-#{flag}"
230
+ end
231
+ end
218
232
 
219
233
  else
220
234
  filenames << arg
@@ -249,6 +263,8 @@ class SeeingIsBelieving
249
263
  lib_options.event_handler = EventStream::Handlers::StreamJsonEvents.new(stdout)
250
264
  end
251
265
 
266
+ stdin.set_encoding 'utf-8'
267
+
252
268
  case debug
253
269
  when String
254
270
  debug_file = File.open(debug, 'a').tap { |f| f.sync = true }
@@ -262,7 +278,8 @@ class SeeingIsBelieving
262
278
  add_error("Cannot give a program body and a filename to get the program body from.")
263
279
  elsif filename && file_class.exist?(filename)
264
280
  self.lib_options.stdin = stdin
265
- self.body = file_class.read filename
281
+ # TODO: this doesn't seem like a safe assumption
282
+ self.body = file_class.read filename, external_encoding: "utf-8"
266
283
  elsif filename
267
284
  add_error("#{filename} does not exist!")
268
285
  elsif body
@@ -316,7 +333,6 @@ Options:
316
333
  -I, --load-path dir # a dir that should be added to the $LOAD_PATH
317
334
  -r, --require file # additional files to be required before running the program
318
335
  -e, --program program-body # pass the program body to execute as an argument
319
- -K, --encoding encoding # sets file encoding, equivalent to Ruby's -Kx (see `man ruby` for valid values)
320
336
  -a, --as filename # run the program as if it was the specified filename
321
337
  -c, --clean # remove annotations from previous runs of seeing_is_believing
322
338
  -g, --debug # print debugging information
@@ -36,16 +36,11 @@ class SeeingIsBelieving
36
36
  end
37
37
 
38
38
  def evaluate!
39
- @evaluated || begin
40
- SeeingIsBelieving.call normalized_cleaned_body,
41
- config.lib_options.merge(event_handler: record_exit_events)
42
- @timed_out = false
43
- @evaluated = true
44
- end
45
- rescue Timeout::Error
46
- @timed_out = true
47
- @evaluated = true
48
- ensure return self unless $! # idk, maybe too tricky, but was really annoying putting it in three places
39
+ @evaluated ||= !!SeeingIsBelieving.call(
40
+ normalized_cleaned_body,
41
+ config.lib_options.merge(event_handler: record_exit_events)
42
+ )
43
+ self
49
44
  end
50
45
 
51
46
  def timed_out?
@@ -12,12 +12,14 @@
12
12
  # its body will be incorrect, anyway.
13
13
 
14
14
  require 'rbconfig'
15
- require 'timeout'
15
+ require 'socket'
16
16
  require 'seeing_is_believing/error'
17
17
  require 'seeing_is_believing/result'
18
18
  require 'seeing_is_believing/debugger'
19
19
  require 'seeing_is_believing/hard_core_ensure'
20
20
  require 'seeing_is_believing/event_stream/consumer'
21
+ require "childprocess"
22
+ ChildProcess.posix_spawn = true # forking locks up for some reason when we run SiB inside of SiB
21
23
 
22
24
  class SeeingIsBelieving
23
25
  class EvaluateByMovingFiles
@@ -31,7 +33,7 @@ class SeeingIsBelieving
31
33
  options = options.dup
32
34
  self.program = program
33
35
  self.filename = filename
34
- self.encoding = options.delete(:encoding)
36
+ self.encoding = options.delete(:encoding) || "u"
35
37
  self.timeout_seconds = options.delete(:timeout_seconds) || 0 # 0 is the new infinity
36
38
  self.provided_input = options.delete(:provided_input) || String.new
37
39
  self.event_handler = options.delete(:event_handler) || raise(ArgumentError, "must provide an :event_handler")
@@ -83,49 +85,47 @@ class SeeingIsBelieving
83
85
  end
84
86
 
85
87
  def write_program_to_file
86
- File.open(filename, 'w') { |f| f.write program.to_s }
88
+ File.open(filename, 'w', external_encoding: "utf-8") { |f| f.write program.to_s }
87
89
  end
88
90
 
89
- # have to basically copy a bunch of Open3 code into here b/c keywords don't work right when the keys are not symbols
90
- # https://github.com/ruby/ruby/pull/808 my PR
91
- # https://bugs.ruby-lang.org/issues/10699 they opened an issue
92
- # https://bugs.ruby-lang.org/issues/10118 weird feature vs bug conversation
91
+
93
92
  def evaluate_file
93
+ event_server = TCPServer.new(0) # dynamically allocates an available port
94
+
94
95
  # setup streams
95
- eventstream, child_eventstream = IO.pipe
96
- stdout, child_stdout = IO.pipe
97
- stderr, child_stderr = IO.pipe
98
- child_stdin, stdin = IO.pipe
96
+ stdout, child_stdout = IO.pipe("utf-8")
97
+ stderr, child_stderr = IO.pipe("utf-8")
99
98
 
100
99
  # setup environment variables
101
100
  env = ENV.to_hash.merge 'SIB_VARIABLES.MARSHAL.B64' =>
102
101
  [Marshal.dump(
103
- event_stream_fd: 4,
102
+ event_stream_port: event_server.addr[1],
104
103
  max_line_captures: max_line_captures,
105
104
  num_lines: program.lines.count,
106
105
  filename: filename
107
106
  )].pack('m0')
108
107
 
109
- # evaluate the code in a child process
110
- opts = {
111
- 4 => child_eventstream,
112
- in: child_stdin,
113
- out: child_stdout,
114
- err: child_stderr,
115
- pgroup: true, # run it in its own process group so we can SIGINT the whole group
116
- }
117
- child_pid = Kernel.spawn(env, *popen_args, opts)
118
- child_pgid = Process.getpgid(child_pid)
119
-
120
- # close child streams b/c they won't emit EOF
121
- # until both child and parent references are closed
122
- close_streams(child_eventstream, child_stdout, child_stderr, child_stdin)
123
- stdin.sync = true
108
+ child = ChildProcess.build(*popen_args)
109
+ child.leader = true
110
+ child.duplex = true
111
+ child.environment.merge!(env)
112
+ child.io.stdout = child_stdout
113
+ child.io.stderr = child_stderr
114
+
115
+ child.start
116
+
117
+ # close child streams b/c they won't emit EOF if parent still has an open reference
118
+ close_streams(child_stdout, child_stderr)
119
+ child.io.stdin.binmode
120
+ child.io.stdin.sync = true
121
+
122
+ # Start receiving events from the child
123
+ eventstream = event_server.accept
124
124
 
125
125
  # send stdin (char at a time b/c input could come from a stream)
126
126
  Thread.new do
127
- provided_input.each_char { |char| stdin.write char }
128
- stdin.close
127
+ provided_input.each_char { |char| child.io.stdin.write char }
128
+ child.io.stdin.close
129
129
  end
130
130
 
131
131
  # set up the event consumer
@@ -133,37 +133,52 @@ class SeeingIsBelieving
133
133
  consumer_thread = Thread.new { consumer.each { |e| event_handler.call e } }
134
134
 
135
135
  # wait for completion
136
- Timeout.timeout timeout_seconds do
137
- Process.wait child_pid
138
- consumer.process_exitstatus($?.exitstatus)
139
- consumer_thread.join
136
+ if timeout_seconds == 0
137
+ child.wait
138
+ else
139
+ child.poll_for_exit(timeout_seconds)
140
140
  end
141
- rescue Timeout::Error
142
- consumer.process_timeout timeout_seconds
143
- ensure
144
- allow_error(Errno::ESRCH) { Process.kill "-INT", child_pgid } # negative makes it apply to the group
145
- allow_error(Errno::ECHILD) { Process.wait child_pid } # I can't tell if this actually works, or just creates enough of a delay for the OS to finish cleaning up the thread
141
+ consumer.process_exitstatus(child.exit_code)
146
142
  consumer_thread.join
147
- close_streams(stdin, stdout, stderr, eventstream)
143
+ rescue ChildProcess::TimeoutError
144
+ consumer.process_timeout(timeout_seconds)
145
+ child.stop
146
+ consumer_thread.join
147
+ ensure
148
+ # On Windows, we need to call stop if there is an error since it interrupted
149
+ # the previos waiting/polling. If we don't call stop, in that situation, it will
150
+ # 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)
156
+ end
157
+ cleanup_run(child)
158
+ close_streams(stdout, stderr, eventstream, event_server)
148
159
  end
149
160
 
150
161
  def popen_args
151
162
  [RbConfig.ruby,
152
163
  '-W0', # no warnings (b/c I hijack STDOUT/STDERR)
153
164
  *(encoding ? ["-K#{encoding}"] : []), # allow the encoding to be set
154
- '-I', File.expand_path('../..', __FILE__), # add lib to the load path
165
+ '-I', File.realpath('..', __dir__), # add lib to the load path
155
166
  *load_path_flags, # users can inject dirs to be added to the load path
156
167
  *require_flags, # users can inject files to be required
157
168
  filename]
158
169
  end
159
170
 
160
- def allow_error(error)
161
- yield
162
- rescue error
163
- end
164
-
165
171
  def close_streams(*streams)
166
172
  streams.each { |io| io.close unless io.closed? }
167
173
  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
168
183
  end
169
184
  end
@@ -7,6 +7,10 @@ require 'thread'
7
7
  class SeeingIsBelieving
8
8
  module EventStream
9
9
  class Consumer
10
+ # Contemplated doing FinishCriteria in binary, but the cost of doing it with an array
11
+ # like this is negligible and it has the nice advantage that the elements in the array
12
+ # are named # so if I ever look at it, I don't have to tranlsate a number to figure out
13
+ # the names https://gist.github.com/JoshCheek/10deb07277b6c85efc7b5e65c007785d
10
14
  class FinishCriteria
11
15
  EventThreadFinished = Module.new
12
16
  StdoutThreadFinished = Module.new
@@ -57,7 +61,7 @@ class SeeingIsBelieving
57
61
  str = str.force_encoding(Encoding::UTF_8)
58
62
  end
59
63
  return str.scrub('�') if str.respond_to? :scrub
60
- # basically reimplement scrub, b/c it's not implemented on 1.9.3
64
+ # basically reimplement scrub, b/c it's not implemented on 2.0.0
61
65
  str.each_char.inject("") do |new_str, char|
62
66
  if char.valid_encoding?
63
67
  new_str << char
@@ -124,6 +128,7 @@ class SeeingIsBelieving
124
128
  # from within the same thread as the consumer, because if it
125
129
  # blocks, who will remove items from the queue?
126
130
  def process_exitstatus(status)
131
+ status ||= 1 # see #100
127
132
  queue << lambda {
128
133
  queue << Events::Exitstatus.new(value: status)
129
134
  finish_criteria.received_exitstatus!