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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +15 -0
  3. data/.rspec +4 -0
  4. data/.travis.yml +1 -1
  5. data/Rakefile +28 -18
  6. data/Readme.md +22 -13
  7. data/bin/seeing_is_believing +0 -1
  8. data/features/errors.feature +3 -3
  9. data/features/examples.feature +65 -0
  10. data/features/flags.feature +19 -0
  11. data/lib/seeing_is_believing/binary.rb +1 -1
  12. data/lib/seeing_is_believing/binary/align_chunk.rb +0 -8
  13. data/lib/seeing_is_believing/binary/commentable_lines.rb +7 -12
  14. data/lib/seeing_is_believing/binary/config.rb +18 -6
  15. data/lib/seeing_is_believing/binary/engine.rb +11 -6
  16. data/lib/seeing_is_believing/binary/remove_annotations.rb +17 -19
  17. data/lib/seeing_is_believing/error.rb +2 -2
  18. data/lib/seeing_is_believing/evaluate_by_moving_files.rb +8 -6
  19. data/lib/seeing_is_believing/event_stream/consumer.rb +55 -24
  20. data/lib/seeing_is_believing/event_stream/events.rb +11 -0
  21. data/lib/seeing_is_believing/event_stream/handlers/record_exit_events.rb +26 -0
  22. data/lib/seeing_is_believing/event_stream/handlers/stream_json_events.rb +0 -22
  23. data/lib/seeing_is_believing/event_stream/handlers/update_result.rb +1 -0
  24. data/lib/seeing_is_believing/result.rb +5 -15
  25. data/lib/seeing_is_believing/version.rb +1 -1
  26. data/seeing_is_believing.gemspec +3 -2
  27. data/spec/binary/config_spec.rb +25 -8
  28. data/spec/binary/engine_spec.rb +12 -5
  29. data/spec/evaluate_by_moving_files_spec.rb +17 -4
  30. data/spec/event_stream_spec.rb +45 -10
  31. data/spec/seeing_is_believing_spec.rb +29 -4
  32. data/spec/wrap_expressions_spec.rb +6 -1
  33. metadata +23 -10
  34. data/Changelog.md +0 -33
  35. 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| [ (comment.text[value_regex] || # associates each comment to its annotation
72
- comment.text[exception_regex] ||
73
- comment.text[stdout_regex] ||
74
- comment.text[stderr_regex]
75
- ),
76
- comment]}
77
- .slice_before { |annotation, comment| annotation } # annotations begin chunks
78
- .select { |(annotation, start), *| annotation } # discard chunks not beginning with an annotation (probably can only happens on first comment)
79
- .map { |(annotation, start), *rest| # end the chunk if the comment doesn't meet nextline criteria
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 = start
82
+ prev = start
82
83
  rest.each { |_, potential_nextline|
83
- break unless prev.line_number.next == potential_nextline.line_number &&
84
- start.text_col == potential_nextline.text_col &&
85
- potential_nextline.whitespace_col.zero? &&
86
- annotation.length <= potential_nextline.text[/#\s*/].length
87
- prev = potential_nextline
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
@@ -12,6 +12,6 @@ class SeeingIsBelieving
12
12
  end
13
13
 
14
14
  # EventStream
15
- NoMoreEvents = Class.new SeeingIsBelievingError
16
- UnknownEvent = Class.new SeeingIsBelievingError
15
+ NoMoreEvents = Class.new SeeingIsBelievingError
16
+ UnknownEvent = Class.new SeeingIsBelievingError
17
17
  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 event handler")
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 we won't get EOF until all fds are closed
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
- # consume events
127
- consumer = EventStream::Consumer.new(events: eventstream, stdout: stdout, stderr: stderr)
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 "TERM", child.pid
138
- raise
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
- CRITERIA = [
12
- :event_thread_finished!,
13
- :stdout_thread_finished!,
14
- :stderr_thread_finished!,
15
- :process_exited!,
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
- @satisfied = false
24
- @unmet_criteria = CRITERIA.dup
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
- @satisfied
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
- str.encode! Encoding::UTF_8
34
- rescue EncodingError
35
- str = str.force_encoding(Encoding::UTF_8)
36
- return str.scrub('�') if str.respond_to? :scrub # b/c it's not implemented on 1.9.3
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 this method
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
- finish_criteria.process_exited!
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
- if finish_criteria.satisfied?
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
- p "WAT: #{element.inspect}"
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
@@ -1,3 +1,3 @@
1
1
  class SeeingIsBelieving
2
- VERSION = '3.0.0.beta.5'
2
+ VERSION = '3.0.0.beta.6'
3
3
  end
@@ -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.2", "< 3.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.0"
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
 
@@ -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.explanation
11
+ error_assertion =~ error.to_s
12
12
  else
13
- error.explanation.include? error_assertion
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
- specify 'unknown options set an error' do
107
- expect(parse(['--xyz' ])).to have_error '--xyz is not an option'
108
- expect(parse(['-y' ])).to have_error '-y is not an option'
109
- expect(parse(['-y', 'b'])).to have_error '-y is not an option'
110
- expect(parse(['-+h' ])).to have_error '-+ is not an option'
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?', t:true do
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
@@ -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') { assert_must_evaluate :result }
54
- specify('exitstatus') { assert_must_evaluate :exitstatus }
55
- specify('timed_out?') { assert_must_evaluate :timed_out? }
56
- specify('annotated_body') { assert_must_evaluate :annotated_body }
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 the program raised a Timeout::Error' do
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"