greenletters 0.0.1 → 0.1.0

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.
data/README.org CHANGED
@@ -10,15 +10,22 @@
10
10
 
11
11
  * Synopsis
12
12
 
13
- #+begin_src ruby
13
+ *See introductory blog post [[http://avdi.org/devblog/2010/07/19/greenletters-painless-automation-and-testing-for-command-line-applications/][HERE]].*
14
+
15
+ #+BEGIN_SRC ruby
14
16
  require 'greenletters'
17
+
15
18
  adv = Greenletters::Process.new("adventure", :transcript => $stdout)
19
+
20
+ # Install a handler which may be triggered at any time
16
21
  adv.on(:output, /welcome to adventure/i) do |process, match_data|
17
22
  adv << "no\n"
18
23
  end
19
24
 
20
25
  puts "Starting adventure..."
21
26
  adv.start!
27
+
28
+ # Wait for the specified pattern before proceeding
22
29
  adv.wait_for(:output, /you are standing at the end of a road/i)
23
30
  adv << "east\n"
24
31
  adv.wait_for(:output, /inside a building/i)
@@ -27,7 +34,7 @@
27
34
  adv << "yes\n"
28
35
  adv.wait_for(:exit)
29
36
  puts "Adventure has exited."
30
- #+end_src
37
+ #+END_SRC
31
38
 
32
39
  Or, in Cucumber format:
33
40
 
@@ -68,7 +75,7 @@ Or, in Cucumber format:
68
75
  * How
69
76
  Greenletters uses the pty.rb library under the covers to create a UNIX
70
77
  pseudoterminal under Ruby's control. Of course, this means that it is
71
- *NIX-only; Windows users need not apply.
78
+ MacOSX/Linux/*NIX-only; Windows users need not apply.
72
79
 
73
80
  The advantage of using a PTY is that *any* output - inclding output written to
74
81
  the console instead of STDOUT/STDERR - will be captured by Greenletters.
data/lib/greenletters.rb CHANGED
@@ -123,6 +123,7 @@ module Greenletters
123
123
  attr_accessor :exclusive
124
124
  attr_accessor :logger
125
125
  attr_accessor :interruption
126
+ attr_reader :options
126
127
 
127
128
  alias_method :exclusive?, :exclusive
128
129
 
@@ -131,6 +132,7 @@ module Greenletters
131
132
  @exclusive = options.fetch(:exclusive) { false }
132
133
  @logger = ::Logger.new($stdout)
133
134
  @interruption = :none
135
+ @options = options
134
136
  end
135
137
 
136
138
  def call(process)
@@ -142,6 +144,7 @@ module Greenletters
142
144
  class OutputTrigger < Trigger
143
145
  def initialize(pattern=//, options={}, &block)
144
146
  super(options, &block)
147
+ options[:operation] ||= :all
145
148
  @pattern = pattern
146
149
  end
147
150
 
@@ -150,6 +153,13 @@ module Greenletters
150
153
  end
151
154
 
152
155
  def call(process)
156
+ case @pattern
157
+ when Array then match_multiple(process)
158
+ else match_one(process)
159
+ end
160
+ end
161
+
162
+ def match_one(process)
153
163
  scanner = process.output_buffer
154
164
  @logger.debug "matching #{@pattern.inspect} against #{scanner.rest.inspect}"
155
165
  if scanner.scan_until(@pattern)
@@ -160,16 +170,66 @@ module Greenletters
160
170
  false
161
171
  end
162
172
  end
173
+
174
+ def match_multiple(process)
175
+ op = options[:operation]
176
+ raise "Invalid operation #{op.inspect}" unless [:any, :all].include?(op)
177
+ scanner = process.output_buffer
178
+ @logger.debug "matching #{op} of multiple patterns against #{scanner.rest.inspect}"
179
+ starting_pos = scanner.pos
180
+ ending_pos = starting_pos
181
+ result = @pattern.send("#{op}?") {|pattern|
182
+ scanner.pos = starting_pos
183
+ if (char_count = scanner.skip_until(pattern))
184
+ ending_pos = [ending_pos, starting_pos + char_count].max
185
+ end
186
+ }
187
+ if result
188
+ scanner.pos = ending_pos
189
+ true
190
+ else
191
+ scanner.pos = starting_pos
192
+ false
193
+ end
194
+ end
195
+ end
196
+
197
+ class BytesTrigger < Trigger
198
+ attr_reader :num_bytes
199
+
200
+ def initialize(num_bytes, options={}, &block)
201
+ super(options, &block)
202
+ @num_bytes = num_bytes
203
+ end
204
+
205
+ def to_s
206
+ "#{num_bytes} bytes of output"
207
+ end
208
+
209
+ def call(process)
210
+ @logger.debug "checking if #{num_bytes} byes have been received"
211
+ process.rest_size >= num_bytes
212
+ end
163
213
  end
164
214
 
165
215
  class TimeoutTrigger < Trigger
216
+ attr_reader :expiration_time
217
+
218
+ def initialize(expiration_time=Time.now+1.0, options={}, &block)
219
+ super(options, &block)
220
+ @expiration_time = case expiration_time
221
+ when Time then expiration_time
222
+ when Numeric then Time.now + expiration_time
223
+ end
224
+ end
225
+
166
226
  def to_s
167
- "timeout"
227
+ "timeout at #{expiration_time}"
168
228
  end
169
229
 
170
230
  def call(process)
171
231
  @block.call(process, process.blocker)
172
- true
232
+ process.time >= expiration_time
173
233
  end
174
234
  end
175
235
 
@@ -207,7 +267,8 @@ module Greenletters
207
267
  end
208
268
 
209
269
  class Process
210
- END_MARKER = '__GREENLETTERS_PROCESS_ENDED__'
270
+ END_MARKER = '__GREENLETTERS_PROCESS_ENDED__'
271
+ DEFAULT_LOG_LEVEL = ::Logger::WARN
211
272
 
212
273
  # Shamelessly stolen from Rake
213
274
  RUBY_EXT =
@@ -230,8 +291,7 @@ module Greenletters
230
291
  attr_reader :cwd # Working directory for the command
231
292
 
232
293
  def_delegators :input_buffer, :puts, :write, :print, :printf, :<<
233
- def_delegators :output_buffer, :read, :readpartial, :read_nonblock, :gets,
234
- :getline
294
+ def_delegators :output_buffer, :rest, :rest_size, :check_until
235
295
  def_delegators :blocker, :interruption, :interruption=
236
296
 
237
297
  def initialize(*args)
@@ -245,7 +305,7 @@ module Greenletters
245
305
  @cwd = options.fetch(:cwd) {Dir.pwd}
246
306
  @logger = options.fetch(:logger) {
247
307
  l = ::Logger.new($stdout)
248
- l.level = ::Logger::WARN
308
+ l.level = DEFAULT_LOG_LEVEL
249
309
  l
250
310
  }
251
311
  @state = :not_started
@@ -261,7 +321,7 @@ module Greenletters
261
321
  end
262
322
 
263
323
  def on(event, *args, &block)
264
- t = add_trigger(event, *args, &block)
324
+ t = add_nonblocking_trigger(event, *args, &block)
265
325
  end
266
326
 
267
327
  def wait_for(event, *args, &block)
@@ -274,6 +334,12 @@ module Greenletters
274
334
  raise
275
335
  end
276
336
 
337
+ def add_nonblocking_trigger(event, *args, &block)
338
+ t = add_trigger(event, *args, &block)
339
+ catchup_trigger!(t)
340
+ t
341
+ end
342
+
277
343
  def add_trigger(event, *args, &block)
278
344
  t = Trigger(event, *args, &block)
279
345
  t.logger = @logger
@@ -296,6 +362,7 @@ module Greenletters
296
362
  t.time_to_live = 1
297
363
  @logger.debug "waiting for #{t}"
298
364
  self.blocker = t
365
+ catchup_trigger!(t)
299
366
  t
300
367
  end
301
368
 
@@ -350,6 +417,10 @@ module Greenletters
350
417
  @state == :ended
351
418
  end
352
419
 
420
+ def time
421
+ Time.now
422
+ end
423
+
353
424
  private
354
425
 
355
426
  attr_reader :triggers
@@ -372,9 +443,10 @@ module Greenletters
372
443
  input_handles = input_buffer.string.empty? ? [] : [@input]
373
444
  output_handles = [@output]
374
445
  error_handles = [@input, @output]
375
- @logger.debug "select() on #{[output_handles, input_handles, error_handles].inspect}"
446
+ timeout = shortest_timeout
447
+ @logger.debug "select() on #{[output_handles, input_handles, error_handles, timeout].inspect}"
376
448
  ready_handles = IO.select(
377
- output_handles, input_handles, error_handles, 1.0)
449
+ output_handles, input_handles, error_handles, timeout)
378
450
  if ready_handles.nil?
379
451
  process_timeout
380
452
  else
@@ -402,8 +474,10 @@ module Greenletters
402
474
  @history << data
403
475
  @logger.debug format_input_for_log(data)
404
476
  @logger.debug "read #{data.size} bytes"
477
+ handle_triggers(:bytes)
405
478
  handle_triggers(:output)
406
479
  flush_triggers!(OutputTrigger) if ended?
480
+ flush_triggers!(BytesTrigger) if ended?
407
481
  # flush_output_buffer! unless ended?
408
482
  end
409
483
 
@@ -463,29 +537,35 @@ module Greenletters
463
537
  matches = 0
464
538
  triggers.grep(klass).each do |t|
465
539
  @logger.debug "checking #{event} against #{t}"
466
- if t.call(self) # match
540
+ check_trigger(t) do
467
541
  matches += 1
468
- @logger.debug "match trigger #{t}"
469
- if blocker.equal?(t)
470
- unblock!
471
- end
472
- if t.time_to_live
473
- if t.time_to_live > 1
474
- t.time_to_live -= 1
475
- @logger.debug "trigger ttl reduced to #{t.time_to_live}"
476
- else
477
- triggers.delete(t)
478
- @logger.debug "trigger removed"
479
- end
480
- end
481
542
  break if t.exclusive?
482
- else
483
- @logger.debug "no match"
484
543
  end
485
544
  end
486
545
  matches > 0
487
546
  end
488
547
 
548
+ def check_trigger(trigger)
549
+ if trigger.call(self) # match
550
+ @logger.debug "match trigger #{trigger}"
551
+ if blocker.equal?(trigger)
552
+ unblock!
553
+ end
554
+ if trigger.time_to_live
555
+ if trigger.time_to_live > 1
556
+ trigger.time_to_live -= 1
557
+ @logger.debug "trigger ttl reduced to #{trigger.time_to_live}"
558
+ else
559
+ triggers.delete(trigger)
560
+ @logger.debug "trigger removed"
561
+ end
562
+ end
563
+ yield if block_given?
564
+ else
565
+ @logger.debug "no match"
566
+ end
567
+ end
568
+
489
569
  def handle_end_marker
490
570
  return false if ended?
491
571
  @logger.debug "end marker found"
@@ -551,6 +631,10 @@ module Greenletters
551
631
  end
552
632
  end
553
633
 
634
+ def catchup_trigger!(trigger)
635
+ check_trigger(trigger)
636
+ end
637
+
554
638
  def format_output_for_log(text)
555
639
  "\n" + text.split("\n").map{|l| ">> #{l}"}.join("\n")
556
640
  end
@@ -559,6 +643,14 @@ module Greenletters
559
643
  "\n" + text.split("\n").map{|l| "<< #{l}"}.join("\n")
560
644
  end
561
645
 
646
+ def shortest_timeout
647
+ result = triggers.grep(TimeoutTrigger).map{|t|
648
+ t.expiration_time - Time.now
649
+ }.min
650
+ if result.nil? then result = 1.0 end
651
+ if result < 0 then result = 0 end
652
+ result
653
+ end
562
654
  end
563
655
  end
564
656
 
@@ -48,6 +48,26 @@ When /^I execute the process(?: "([^\"]*)")?$/ do |name|
48
48
  @greenletters_process_table[name].start!
49
49
  end
50
50
 
51
+ When /^I wait for (\d+) bytes from the process(?: "([^\"]*)")?$/ do
52
+ |byte_length, name|
53
+ name ||= "default"
54
+ byte_length = byte_length.to_i
55
+ @greenletters_process_table[name].wait_for(:bytes, byte_length)
56
+ end
57
+
58
+ When /^I wait ([0-9.]+) seconds for output from the process(?: "([^\"]*)")?$/ do
59
+ |seconds, name|
60
+ name ||= "default"
61
+ seconds = seconds.to_i
62
+ @greenletters_process_table[name].wait_for(:timeout, seconds)
63
+ end
64
+
65
+ When /^I discard earlier outputs from the process(?: "([^\"]*)")?$/ do
66
+ |name|
67
+ name ||= "default"
68
+ @greenletters_process_table[name].flush_output_buffer!
69
+ end
70
+
51
71
  Then /^I should see the following output(?: from process "([^\"]*)")?:$/ do
52
72
  |name, pattern|
53
73
  name ||= "default"
@@ -55,6 +75,26 @@ Then /^I should see the following output(?: from process "([^\"]*)")?:$/ do
55
75
  @greenletters_process_table[name].wait_for(:output, pattern)
56
76
  end
57
77
 
78
+ Then /^I should see all the following outputs(?: from process "([^\"]*)")?:$/ do
79
+ |name, table|
80
+
81
+ name ||= "default"
82
+ patterns = table.hashes.map { |hash|
83
+ greenletters_massage_pattern(hash['text'])
84
+ }
85
+ @greenletters_process_table[name].wait_for(:output, patterns, :operation => :all)
86
+ end
87
+
88
+
89
+ # Note: you may want to wait for output to be buffered before executing this
90
+ # step. E.g. "When I wait on process for 1024 bytes or 0.1 seconds"
91
+ Then /^I should not see the following output(?: from process "([^\"]*)")?:$/ do
92
+ |name, pattern|
93
+ name ||= "default"
94
+ pattern = greenletters_massage_pattern(pattern)
95
+ @greenletters_process_table[name].check_until(pattern).should be_nil
96
+ end
97
+
58
98
  When /^I enter "([^\"]*)"(?: into process "([^\"]*)")?$/ do
59
99
  |input, name|
60
100
  name ||= "default"
data/script/console CHANGED
@@ -2,4 +2,7 @@
2
2
  $:.unshift(File.expand_path("../lib", File.dirname(__FILE__)))
3
3
  require 'greenletters'
4
4
  require 'irb'
5
+
6
+ GLP = Greenletters::Process
7
+ GLP.const_set(:DEFAULT_LOG_LEVEL, ::Logger::DEBUG)
5
8
  IRB.start
data/version.txt CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.1.0
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 0
8
7
  - 1
9
- version: 0.0.1
8
+ - 0
9
+ version: 0.1.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Avdi Grimm
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-07-19 00:00:00 -04:00
17
+ date: 2010-07-24 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency