seeing_is_believing 3.0.0.beta.5 → 3.0.0.beta.6

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