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 +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
|