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 +10 -3
- data/lib/greenletters.rb +117 -25
- data/lib/greenletters/cucumber_steps.rb +40 -0
- data/script/console +3 -0
- data/version.txt +1 -1
- metadata +3 -3
data/README.org
CHANGED
@@ -10,15 +10,22 @@
|
|
10
10
|
|
11
11
|
* Synopsis
|
12
12
|
|
13
|
-
|
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
|
-
#+
|
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
|
-
|
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
|
-
|
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
|
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, :
|
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 =
|
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 =
|
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
|
-
|
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,
|
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
|
-
|
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
data/version.txt
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
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
|
-
|
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-
|
17
|
+
date: 2010-07-24 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|