seeing_is_believing 3.0.0.beta.5 → 3.0.0.beta.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +15 -0
- data/.rspec +4 -0
- data/.travis.yml +1 -1
- data/Rakefile +28 -18
- data/Readme.md +22 -13
- data/bin/seeing_is_believing +0 -1
- data/features/errors.feature +3 -3
- data/features/examples.feature +65 -0
- data/features/flags.feature +19 -0
- data/lib/seeing_is_believing/binary.rb +1 -1
- data/lib/seeing_is_believing/binary/align_chunk.rb +0 -8
- data/lib/seeing_is_believing/binary/commentable_lines.rb +7 -12
- data/lib/seeing_is_believing/binary/config.rb +18 -6
- data/lib/seeing_is_believing/binary/engine.rb +11 -6
- data/lib/seeing_is_believing/binary/remove_annotations.rb +17 -19
- data/lib/seeing_is_believing/error.rb +2 -2
- data/lib/seeing_is_believing/evaluate_by_moving_files.rb +8 -6
- data/lib/seeing_is_believing/event_stream/consumer.rb +55 -24
- data/lib/seeing_is_believing/event_stream/events.rb +11 -0
- data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
- data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +0 -22
- data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +1 -0
- data/lib/seeing_is_believing/result.rb +5 -15
- data/lib/seeing_is_believing/version.rb +1 -1
- data/seeing_is_believing.gemspec +3 -2
- data/spec/binary/config_spec.rb +25 -8
- data/spec/binary/engine_spec.rb +12 -5
- data/spec/evaluate_by_moving_files_spec.rb +17 -4
- data/spec/event_stream_spec.rb +45 -10
- data/spec/seeing_is_believing_spec.rb +29 -4
- data/spec/wrap_expressions_spec.rb +6 -1
- metadata +23 -10
- data/Changelog.md +0 -33
- data/lib/seeing_is_believing/event_stream/handlers/record_exitstatus.rb +0 -18
@@ -68,33 +68,31 @@ class SeeingIsBelieving
|
|
68
68
|
def annotation_chunks_in(code)
|
69
69
|
code
|
70
70
|
.inline_comments
|
71
|
-
.map { |comment|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
.
|
79
|
-
.
|
71
|
+
.map { |comment| # associate each annotation to its comment
|
72
|
+
annotation = comment.text[value_regex] ||
|
73
|
+
comment.text[exception_regex] ||
|
74
|
+
comment.text[stdout_regex] ||
|
75
|
+
comment.text[stderr_regex]
|
76
|
+
[annotation, comment]
|
77
|
+
}
|
78
|
+
.slice_before { |annotation, comment| annotation } # annotations begin chunks
|
79
|
+
.select { |(annotation, start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
|
80
|
+
.map { |(annotation, start), *rest| # end the chunk if the comment doesn't meet nextline criteria
|
80
81
|
nextline_comments = []
|
81
|
-
prev
|
82
|
+
prev = start
|
82
83
|
rest.each { |_, potential_nextline|
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
sequential = (prev.line_number.next == potential_nextline.line_number)
|
85
|
+
vertically_aligned = start.text_col == potential_nextline.text_col
|
86
|
+
only_preceded_by_whitespace = potential_nextline.whitespace_col.zero?
|
87
|
+
indention_matches_annotation = annotation.length <= potential_nextline.text[/#\s*/].length
|
88
|
+
break unless sequential && vertically_aligned && only_preceded_by_whitespace && indention_matches_annotation
|
88
89
|
nextline_comments << potential_nextline
|
90
|
+
prev = potential_nextline
|
89
91
|
}
|
90
92
|
[start, nextline_comments]
|
91
93
|
}
|
92
94
|
end
|
93
95
|
|
94
|
-
def value_prefix
|
95
|
-
@value_prefix ||= markers.fetch(:value).fetch(:prefix)
|
96
|
-
end
|
97
|
-
|
98
96
|
def value_regex
|
99
97
|
@value_regex ||= markers.fetch(:value).fetch(:regex)
|
100
98
|
end
|
@@ -33,7 +33,7 @@ class SeeingIsBelieving
|
|
33
33
|
self.encoding = options.delete(:encoding)
|
34
34
|
self.timeout_seconds = options.delete(:timeout_seconds) || 0 # 0 is the new infinity
|
35
35
|
self.provided_input = options.delete(:provided_input) || String.new
|
36
|
-
self.event_handler = options.delete(:event_handler) || raise("must provide an
|
36
|
+
self.event_handler = options.delete(:event_handler) || raise(ArgumentError, "must provide an :event_handler")
|
37
37
|
self.load_path_flags = (options.delete(:load_path_dirs) || []).map { |dir| ['-I', dir] }.flatten
|
38
38
|
self.require_flags = (options.delete(:require_files) || ['seeing_is_believing/the_matrix']).map { |filename| ['-r', filename] }.flatten
|
39
39
|
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)
|
@@ -110,7 +110,8 @@ class SeeingIsBelieving
|
|
110
110
|
child_eventstream => child_eventstream }
|
111
111
|
child = Process.detach Kernel.spawn(env, *popen_args, opts)
|
112
112
|
|
113
|
-
# close b/c
|
113
|
+
# close child streams b/c they won't emit EOF
|
114
|
+
# until both child and parent references are closed
|
114
115
|
child_eventstream.close
|
115
116
|
child_stdout.close
|
116
117
|
child_stderr.close
|
@@ -123,8 +124,8 @@ class SeeingIsBelieving
|
|
123
124
|
stdin.close
|
124
125
|
}
|
125
126
|
|
126
|
-
#
|
127
|
-
consumer
|
127
|
+
# set up the event consumer
|
128
|
+
consumer = EventStream::Consumer.new(events: eventstream, stdout: stdout, stderr: stderr)
|
128
129
|
consumer_thread = Thread.new { consumer.each { |e| event_handler.call e } }
|
129
130
|
|
130
131
|
# wait for completion
|
@@ -134,8 +135,9 @@ class SeeingIsBelieving
|
|
134
135
|
consumer_thread.join
|
135
136
|
end
|
136
137
|
rescue Timeout::Error
|
137
|
-
Process.kill "
|
138
|
-
|
138
|
+
Process.kill "KILL", child.pid
|
139
|
+
consumer.process_timeout timeout_seconds
|
140
|
+
consumer_thread.join # finish consuming events
|
139
141
|
ensure
|
140
142
|
[stdin, stdout, stderr, eventstream].each { |io| io.close unless io.closed? }
|
141
143
|
end
|
@@ -8,32 +8,56 @@ class SeeingIsBelieving
|
|
8
8
|
module EventStream
|
9
9
|
class Consumer
|
10
10
|
class FinishCriteria
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
].freeze.each do |name|
|
17
|
-
define_method name do
|
18
|
-
@unmet_criteria.delete name
|
19
|
-
@satisfied = @unmet_criteria.empty?
|
20
|
-
end
|
21
|
-
end
|
11
|
+
EventThreadFinished = Module.new
|
12
|
+
StdoutThreadFinished = Module.new
|
13
|
+
StderrThreadFinished = Module.new
|
14
|
+
ProcessExited = Module.new
|
15
|
+
|
22
16
|
def initialize
|
23
|
-
@
|
24
|
-
|
17
|
+
@unmet_criteria = [
|
18
|
+
EventThreadFinished,
|
19
|
+
StdoutThreadFinished,
|
20
|
+
StderrThreadFinished,
|
21
|
+
ProcessExited,
|
22
|
+
]
|
25
23
|
end
|
24
|
+
|
25
|
+
# finish criteria are satisfied,
|
26
|
+
# we can stop processing events
|
26
27
|
def satisfied?
|
27
|
-
@
|
28
|
+
@unmet_criteria.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def event_thread_finished!
|
32
|
+
@unmet_criteria.delete EventThreadFinished
|
33
|
+
end
|
34
|
+
|
35
|
+
def stdout_thread_finished!
|
36
|
+
@unmet_criteria.delete StdoutThreadFinished
|
37
|
+
end
|
38
|
+
|
39
|
+
def stderr_thread_finished!
|
40
|
+
@unmet_criteria.delete StderrThreadFinished
|
41
|
+
end
|
42
|
+
|
43
|
+
def received_exitstatus!
|
44
|
+
@unmet_criteria.delete ProcessExited
|
45
|
+
end
|
46
|
+
|
47
|
+
def received_timeout!
|
48
|
+
@unmet_criteria.delete ProcessExited
|
28
49
|
end
|
29
50
|
end
|
30
51
|
|
31
52
|
# https://github.com/JoshCheek/seeing_is_believing/issues/46
|
32
53
|
def self.fix_encoding(str)
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
54
|
+
begin
|
55
|
+
str.encode! Encoding::UTF_8
|
56
|
+
rescue EncodingError
|
57
|
+
str = str.force_encoding(Encoding::UTF_8)
|
58
|
+
end
|
59
|
+
return str.scrub('�') if str.respond_to? :scrub
|
60
|
+
# basically reimplement scrub, b/c it's not implemented on 1.9.3
|
37
61
|
str.each_char.inject("") do |new_str, char|
|
38
62
|
if char.valid_encoding?
|
39
63
|
new_str << char
|
@@ -44,6 +68,7 @@ class SeeingIsBelieving
|
|
44
68
|
end
|
45
69
|
|
46
70
|
def initialize(streams)
|
71
|
+
@finished = false
|
47
72
|
self.finish_criteria = FinishCriteria.new
|
48
73
|
self.queue = Queue.new
|
49
74
|
event_stream = streams.fetch :events
|
@@ -94,16 +119,23 @@ class SeeingIsBelieving
|
|
94
119
|
yield call 1 until @finished
|
95
120
|
end
|
96
121
|
|
97
|
-
# NOTE: Note it's probably a bad plan to call
|
122
|
+
# NOTE: Note it's probably a bad plan to call these methods
|
98
123
|
# from within the same thread as the consumer, because if it
|
99
124
|
# blocks, who will remove items from the queue?
|
100
125
|
def process_exitstatus(status)
|
101
|
-
queue << Events::Exitstatus.new(value: status)
|
102
126
|
queue << lambda {
|
103
|
-
|
127
|
+
queue << Events::Exitstatus.new(value: status)
|
128
|
+
finish_criteria.received_exitstatus!
|
129
|
+
}
|
130
|
+
end
|
131
|
+
def process_timeout(seconds)
|
132
|
+
queue << lambda {
|
133
|
+
queue << Events::Timeout.new(seconds: seconds)
|
134
|
+
finish_criteria.received_timeout!
|
104
135
|
}
|
105
136
|
end
|
106
137
|
|
138
|
+
|
107
139
|
private
|
108
140
|
|
109
141
|
attr_accessor :queue, :finish_criteria
|
@@ -115,9 +147,8 @@ class SeeingIsBelieving
|
|
115
147
|
event_for element
|
116
148
|
when Proc
|
117
149
|
element.call
|
118
|
-
|
150
|
+
finish_criteria.satisfied? &&
|
119
151
|
queue << Events::Finished.new
|
120
|
-
end
|
121
152
|
next_event
|
122
153
|
when Events::Finished
|
123
154
|
@finished = true
|
@@ -125,7 +156,7 @@ class SeeingIsBelieving
|
|
125
156
|
when Event
|
126
157
|
element
|
127
158
|
else
|
128
|
-
|
159
|
+
raise SeeingIsBelieving::UnknownEvent, "WAT IS THIS?: #{element.inspect}"
|
129
160
|
end
|
130
161
|
end
|
131
162
|
|
@@ -92,6 +92,17 @@ class SeeingIsBelieving
|
|
92
92
|
attributes :value
|
93
93
|
end
|
94
94
|
|
95
|
+
# The process timed out
|
96
|
+
# note that you will probably not receive an exitstatus
|
97
|
+
# if this occurs. Though it's hypothetically possible...
|
98
|
+
# this is all asynchronous.
|
99
|
+
class Timeout < Event
|
100
|
+
def self.event_name
|
101
|
+
:timeout
|
102
|
+
end
|
103
|
+
attributes :seconds
|
104
|
+
end
|
105
|
+
|
95
106
|
# Emitted when the process invokes exec.
|
96
107
|
# Note that this could be a child process,
|
97
108
|
# so it does not necessarily mean there won't be any more line results
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'seeing_is_believing/event_stream/events'
|
2
|
+
|
3
|
+
class SeeingIsBelieving
|
4
|
+
module EventStream
|
5
|
+
module Handlers
|
6
|
+
class RecordExitEvents
|
7
|
+
attr_reader :exitstatus
|
8
|
+
attr_reader :timeout_seconds
|
9
|
+
|
10
|
+
def initialize(next_observer)
|
11
|
+
@next_observer = next_observer
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(event)
|
15
|
+
case event
|
16
|
+
when Events::Exitstatus
|
17
|
+
@exitstatus = event.value
|
18
|
+
when Events::Timeout
|
19
|
+
@timeout_seconds = event.seconds
|
20
|
+
end
|
21
|
+
@next_observer.call(event)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -13,32 +13,10 @@ class SeeingIsBelieving
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def call(event)
|
16
|
-
write_event event
|
17
|
-
record_outcome event
|
18
|
-
end
|
19
|
-
|
20
|
-
def has_exception?
|
21
|
-
true
|
22
|
-
end
|
23
|
-
|
24
|
-
def exitstatus
|
25
|
-
@exitstatus
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def write_event(event)
|
31
16
|
@stream << JSON.dump(event.as_json)
|
32
17
|
@stream << "\n"
|
33
18
|
@stream.flush if @flush
|
34
19
|
end
|
35
|
-
|
36
|
-
def record_outcome(event)
|
37
|
-
case event
|
38
|
-
when Events::Exception then @has_exception = true
|
39
|
-
when Events::Exitstatus then @exitstatus = event.value
|
40
|
-
end
|
41
|
-
end
|
42
20
|
end
|
43
21
|
end
|
44
22
|
end
|
@@ -24,6 +24,7 @@ class SeeingIsBelieving
|
|
24
24
|
when SiBVersion then result.sib_version = event.value
|
25
25
|
when RubyVersion then result.ruby_version = event.value
|
26
26
|
when Filename then result.filename = event.value
|
27
|
+
when Timeout then result.timeout_seconds = event.seconds
|
27
28
|
when Exec,
|
28
29
|
Finished,
|
29
30
|
StdoutClosed,
|
@@ -3,7 +3,7 @@ class SeeingIsBelieving
|
|
3
3
|
include Enumerable
|
4
4
|
RecordedException = Struct.new :line_number, :class_name, :message, :backtrace
|
5
5
|
|
6
|
-
attr_accessor :stdout, :stderr, :exitstatus, :max_line_captures, :exception, :num_lines, :sib_version, :ruby_version, :filename
|
6
|
+
attr_accessor :stdout, :stderr, :exitstatus, :max_line_captures, :exception, :num_lines, :sib_version, :ruby_version, :filename, :timeout_seconds
|
7
7
|
|
8
8
|
def initialize
|
9
9
|
self.stdout = ''
|
@@ -20,6 +20,10 @@ class SeeingIsBelieving
|
|
20
20
|
stderr && !stderr.empty?
|
21
21
|
end
|
22
22
|
|
23
|
+
def timeout?
|
24
|
+
!!timeout_seconds
|
25
|
+
end
|
26
|
+
|
23
27
|
def record_result(type, line_number, value)
|
24
28
|
results_for(line_number, type) << value
|
25
29
|
value
|
@@ -38,20 +42,6 @@ class SeeingIsBelieving
|
|
38
42
|
(1..num_lines).each { |line_number| block.call self[line_number] }
|
39
43
|
end
|
40
44
|
|
41
|
-
def inspect
|
42
|
-
results
|
43
|
-
variables = instance_variables.map do |name|
|
44
|
-
value = instance_variable_get(name)
|
45
|
-
inspected = if name.to_s == '@results'
|
46
|
-
"{#{value.sort_by(&:first).map { |k, v| "#{k.inspect}=>#{v.inspect}"}.join(",\n ")}}"
|
47
|
-
else
|
48
|
-
value.inspect
|
49
|
-
end
|
50
|
-
"#{name}=#{inspected}"
|
51
|
-
end
|
52
|
-
"#<SIB::Result #{variables.join "\n "}>"
|
53
|
-
end
|
54
|
-
|
55
45
|
def max_line_captures
|
56
46
|
@max_line_captures || Float::INFINITY
|
57
47
|
end
|
data/seeing_is_believing.gemspec
CHANGED
@@ -19,11 +19,12 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = ["lib"]
|
21
21
|
|
22
|
-
s.add_dependency "parser", ">= 2.2.0.
|
22
|
+
s.add_dependency "parser", ">= 2.2.0.3", "< 3.0"
|
23
23
|
|
24
|
+
s.add_development_dependency "what_weve_got_here_is_an_error_to_communicate"
|
24
25
|
s.add_development_dependency "haiti", ">= 0.1", "< 0.3"
|
25
26
|
s.add_development_dependency "rake", "~> 10.0"
|
26
|
-
s.add_development_dependency "rspec", "~> 3.
|
27
|
+
s.add_development_dependency "rspec", "~> 3.2"
|
27
28
|
s.add_development_dependency "cucumber", "~> 1.2"
|
28
29
|
s.add_development_dependency "ichannel", "~> 5.1"
|
29
30
|
|
data/spec/binary/config_spec.rb
CHANGED
@@ -8,9 +8,9 @@ RSpec.describe SeeingIsBelieving::Binary::Config do
|
|
8
8
|
config.errors.find do |error|
|
9
9
|
case error_assertion
|
10
10
|
when Regexp
|
11
|
-
error_assertion =~ error.
|
11
|
+
error_assertion =~ error.to_s
|
12
12
|
else
|
13
|
-
error.
|
13
|
+
error.to_s.include? error_assertion
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -37,6 +37,7 @@ RSpec.describe SeeingIsBelieving::Binary::Config do
|
|
37
37
|
deprecated = deprecated_args.first
|
38
38
|
expect(deprecated.args).to eq [flag, *args]
|
39
39
|
expect(deprecated.explanation).to be_a_kind_of String
|
40
|
+
expect(deprecated.to_s).to include "Deprecated"
|
40
41
|
end
|
41
42
|
|
42
43
|
shared_examples 'it requires a positive int argument' do |flags|
|
@@ -103,11 +104,27 @@ RSpec.describe SeeingIsBelieving::Binary::Config do
|
|
103
104
|
assert_same_flat_opts ['-jgh+'], ['-j', '-g', '-h+']
|
104
105
|
end
|
105
106
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
describe 'ignore_unknown_flags?' do
|
108
|
+
it 'is false by default' do
|
109
|
+
expect(parse([]).ignore_unknown_flags?).to eq false
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'is set to true when it sees the --ignore-unknown-options flag' do
|
113
|
+
expect(parse(['--ignore-unknown-flags']).ignore_unknown_flags?).to eq true
|
114
|
+
end
|
115
|
+
|
116
|
+
specify 'when false, unknown flags set an error' do
|
117
|
+
expect(parse(['--xyz' ])).to have_error '--xyz is not a flag'
|
118
|
+
expect(parse(['-y' ])).to have_error '-y is not a flag'
|
119
|
+
expect(parse(['-y', 'b'])).to have_error '-y is not a flag'
|
120
|
+
expect(parse(['-+h' ])).to have_error '-+ is not a flag'
|
121
|
+
end
|
122
|
+
|
123
|
+
specify 'when true, unknown flags do not set an error' do
|
124
|
+
expect(parse(['--zomg'])).to have_error /zomg/
|
125
|
+
expect(parse(['--ignore-unknown-flags', '--zomg'])).to_not have_error /zomg/
|
126
|
+
expect(parse(['--zomg', '--ignore-unknown-flags'])).to_not have_error /zomg/
|
127
|
+
end
|
111
128
|
end
|
112
129
|
|
113
130
|
describe 'filename and lib_options.filename' do
|
@@ -421,7 +438,7 @@ RSpec.describe SeeingIsBelieving::Binary::Config do
|
|
421
438
|
end
|
422
439
|
end
|
423
440
|
|
424
|
-
describe 'remove_value_prefixes?'
|
441
|
+
describe 'remove_value_prefixes?' do
|
425
442
|
it 'defaults to true' do
|
426
443
|
expect(parse([]).remove_value_prefixes?).to eq true
|
427
444
|
end
|
data/spec/binary/engine_spec.rb
CHANGED
@@ -36,6 +36,7 @@ class SeeingIsBelieving
|
|
36
36
|
allow_any_instance_of(Code::Syntax).to receive(:line_number).and_return 123
|
37
37
|
expect(invalid_engine.syntax_error)
|
38
38
|
.to eq SyntaxErrorMessage.new(line_number: 123, filename: "filename.rb", explanation: "ERR!!")
|
39
|
+
expect(invalid_engine.syntax_error.to_s).to include "ERR!!"
|
39
40
|
end
|
40
41
|
end
|
41
42
|
|
@@ -50,10 +51,11 @@ class SeeingIsBelieving
|
|
50
51
|
end
|
51
52
|
|
52
53
|
context 'before evaluating it raises if asked for' do
|
53
|
-
specify('result')
|
54
|
-
specify('exitstatus')
|
55
|
-
specify('timed_out?')
|
56
|
-
specify('
|
54
|
+
specify('result') { assert_must_evaluate :result }
|
55
|
+
specify('exitstatus') { assert_must_evaluate :exitstatus }
|
56
|
+
specify('timed_out?') { assert_must_evaluate :timed_out? }
|
57
|
+
specify('timeout_seconds') { assert_must_evaluate :timeout_seconds }
|
58
|
+
specify('annotated_body') { assert_must_evaluate :annotated_body }
|
57
59
|
end
|
58
60
|
|
59
61
|
context 'after evaluating' do
|
@@ -68,11 +70,16 @@ class SeeingIsBelieving
|
|
68
70
|
expect(engine.exitstatus).to eq 88
|
69
71
|
end
|
70
72
|
|
71
|
-
specify 'timed_out? is true if
|
73
|
+
specify 'timed_out? is true if a Timeout event was emitted' do
|
72
74
|
expect(call('', timeout: 1).evaluate!.timed_out?).to eq false
|
73
75
|
expect(call('sleep 1', timeout: 0.01).evaluate!.timed_out?).to eq true
|
74
76
|
end
|
75
77
|
|
78
|
+
specify 'timeout_seconds is nil, or the timeout duration' do
|
79
|
+
expect(call('', timeout: 1).evaluate!.timeout_seconds).to eq nil
|
80
|
+
expect(call('sleep 1', timeout: 0.01).evaluate!.timeout_seconds).to eq 0.01
|
81
|
+
end
|
82
|
+
|
76
83
|
context 'annotated_body' do
|
77
84
|
it 'is the body after being run through the annotator' do
|
78
85
|
expect(call("1").evaluate!.annotated_body).to eq "1 # => 1"
|