greenletters 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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