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.
- 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
|