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.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +39 -0
- data/{Readme.md → README.md} +9 -12
- data/Rakefile +12 -23
- data/appveyor.yml +3 -1
- data/features/flags.feature +251 -1
- data/features/regression.feature +166 -23
- data/features/xmpfilter-style.feature +1 -0
- data/lib/seeing_is_believing.rb +5 -2
- data/lib/seeing_is_believing/binary.rb +6 -1
- data/lib/seeing_is_believing/binary/annotate_end_of_file.rb +1 -1
- data/lib/seeing_is_believing/binary/annotate_marked_lines.rb +8 -5
- data/lib/seeing_is_believing/binary/comment_lines.rb +2 -2
- data/lib/seeing_is_believing/binary/commentable_lines.rb +7 -7
- data/lib/seeing_is_believing/binary/config.rb +21 -4
- data/lib/seeing_is_believing/binary/engine.rb +58 -1
- data/lib/seeing_is_believing/binary/format_comment.rb +9 -6
- data/lib/seeing_is_believing/binary/remove_annotations.rb +1 -1
- data/lib/seeing_is_believing/code.rb +5 -5
- data/lib/seeing_is_believing/compatibility.rb +28 -0
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +52 -71
- data/lib/seeing_is_believing/event_stream/consumer.rb +26 -28
- data/lib/seeing_is_believing/event_stream/events.rb +7 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +3 -3
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +2 -1
- data/lib/seeing_is_believing/event_stream/producer.rb +29 -5
- data/lib/seeing_is_believing/safe.rb +8 -7
- data/lib/seeing_is_believing/swap_files.rb +90 -0
- data/lib/seeing_is_believing/the_matrix.rb +17 -4
- data/lib/seeing_is_believing/version.rb +1 -1
- data/lib/seeing_is_believing/wrap_expressions.rb +27 -13
- data/lib/seeing_is_believing/wrap_expressions_with_inspect.rb +4 -1
- data/seeing_is_believing.gemspec +4 -6
- data/spec/binary/config_spec.rb +63 -35
- data/spec/binary/engine_spec.rb +11 -1
- data/spec/binary/format_comment_spec.rb +5 -1
- data/spec/evaluate_by_moving_files_spec.rb +35 -18
- data/spec/event_stream_spec.rb +15 -0
- data/spec/seeing_is_believing_spec.rb +148 -26
- data/spec/sib_spec_helpers/version.rb +17 -0
- data/spec/spec_helper.rb +2 -18
- data/spec/wrap_expressions_spec.rb +9 -2
- metadata +31 -31
- data/.travis.yml +0 -10
- data/lib/seeing_is_believing/customize_pp.rb +0 -5
- data/spec/binary/options_spec.rb +0 -0
data/lib/seeing_is_believing.rb
CHANGED
@@ -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
|
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.
|
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
|
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
|
-
|
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,
|
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.
|
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,
|
57
|
+
.select { |line_number, (index_of_newline, _col)|
|
58
58
|
code[index_of_newline-1] == '\\'
|
59
59
|
}
|
60
|
-
.reject { |line_number, (index_of_newline,
|
60
|
+
.reject { |line_number, (index_of_newline, _col)|
|
61
61
|
ors_indexes.include? index_of_newline
|
62
62
|
}
|
63
|
-
.each { |line_number, (
|
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,
|
77
|
-
.each { |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,
|
86
|
-
.each { |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
|
-
|
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
|
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
|
-
|
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.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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,
|
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::
|
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,
|
95
|
-
.map { |_, (
|
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, (
|
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
|
16
|
+
require "childprocess"
|
17
|
+
|
17
18
|
require 'seeing_is_believing/result'
|
18
19
|
require 'seeing_is_believing/debugger'
|
19
|
-
require 'seeing_is_believing/
|
20
|
+
require 'seeing_is_believing/swap_files'
|
20
21
|
require 'seeing_is_believing/event_stream/consumer'
|
21
|
-
require
|
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 :
|
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
|
-
|
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.
|
35
|
-
self.
|
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) || []).
|
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
|
-
|
48
|
-
|
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
|
-
|
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:
|
105
|
-
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
|
-
|
128
|
-
|
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
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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
|