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.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/Rakefile +43 -16
- data/Readme.md +27 -242
- data/appveyor.yml +29 -0
- data/bin/seeing_is_believing +2 -1
- data/features/deprecated-flags.feature +19 -0
- data/features/errors.feature +57 -0
- data/features/examples.feature +4 -2
- data/features/flags.feature +35 -2
- data/features/regression.feature +107 -10
- data/features/support/env.rb +61 -2
- data/features/xmpfilter-style.feature +3 -2
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +11 -9
- data/lib/seeing_is_believing/binary/annotate_every_line.rb +9 -8
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +9 -8
- data/lib/seeing_is_believing/binary/config.rb +19 -3
- data/lib/seeing_is_believing/binary/engine.rb +5 -10
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +60 -45
- data/lib/seeing_is_believing/event_stream/consumer.rb +6 -1
- data/lib/seeing_is_believing/event_stream/handlers/debug.rb +5 -1
- data/lib/seeing_is_believing/event_stream/producer.rb +1 -1
- data/lib/seeing_is_believing/hard_core_ensure.rb +6 -0
- data/lib/seeing_is_believing/result.rb +26 -14
- data/lib/seeing_is_believing/safe.rb +6 -1
- data/lib/seeing_is_believing/the_matrix.rb +16 -4
- data/lib/seeing_is_believing/version.rb +1 -1
- data/lib/seeing_is_believing/wrap_expressions.rb +1 -1
- data/lib/seeing_is_believing.rb +7 -10
- data/seeing_is_believing.gemspec +9 -8
- data/spec/binary/config_spec.rb +65 -4
- data/spec/binary/engine_spec.rb +1 -1
- data/spec/evaluate_by_moving_files_spec.rb +31 -5
- data/spec/event_stream_spec.rb +14 -6
- data/spec/hard_core_ensure_spec.rb +70 -44
- data/spec/seeing_is_believing_spec.rb +136 -42
- data/spec/spec_helper.rb +8 -0
- data/spec/wrap_expressions_spec.rb +15 -0
- metadata +21 -6
data/features/regression.feature
CHANGED
@@ -240,7 +240,7 @@ Feature:
|
|
240
240
|
|
241
241
|
|
242
242
|
Scenario: Repeated invocations
|
243
|
-
When I run "echo
|
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
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
339
|
-
|
340
|
-
|
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
|
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
|
-
#
|
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
|
-
|
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
|
+
"""
|
data/features/support/env.rb
CHANGED
@@ -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
|
-
|
22
|
-
config.
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
22
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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,
|
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
|
-
|
100
|
-
|
101
|
-
|
102
|
-
include_lines <<
|
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
|
-
|
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
|
112
|
+
if exception_result && annotate_this_line
|
112
113
|
[comment.whitespace, FormatComment.call(comment.text_col, value_prefix, exception_result, @options)]
|
113
|
-
elsif
|
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
|
-
|
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
|
-
|
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
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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 '
|
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
|
-
|
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
|
-
|
96
|
-
|
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
|
-
|
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
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
136
|
+
if timeout_seconds == 0
|
137
|
+
child.wait
|
138
|
+
else
|
139
|
+
child.poll_for_exit(timeout_seconds)
|
140
140
|
end
|
141
|
-
|
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
|
-
|
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.
|
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
|
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!
|