expectr 1.1.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/expectr/version.rb +1 -1
- data/lib/expectr.rb +150 -87
- data/test/test_core.rb +3 -3
- data/test/test_interaction.rb +11 -11
- data/test/test_signals.rb +7 -6
- metadata +2 -2
data/lib/expectr/version.rb
CHANGED
data/lib/expectr.rb
CHANGED
@@ -30,9 +30,15 @@ require 'expectr/version'
|
|
30
30
|
# # Do other stuff
|
31
31
|
# end
|
32
32
|
class Expectr
|
33
|
+
DEFAULT_TIMEOUT = 30
|
34
|
+
DEFAULT_FLUSH_BUFFER = true
|
35
|
+
DEFAULT_BUFFER_SIZE = 8192
|
36
|
+
DEFAULT_CONSTRAIN = false
|
37
|
+
DEFAULT_FORCE_MATCH = false
|
38
|
+
|
33
39
|
# Public: Gets/sets the number of seconds a call to Expectr#expect may last
|
34
40
|
attr_accessor :timeout
|
35
|
-
# Public: Gets/sets whether to flush program output to
|
41
|
+
# Public: Gets/sets whether to flush program output to $stdout
|
36
42
|
attr_accessor :flush_buffer
|
37
43
|
# Public: Gets/sets the number of bytes to use for the internal buffer
|
38
44
|
attr_accessor :buffer_size
|
@@ -69,52 +75,21 @@ class Expectr
|
|
69
75
|
# set to false, preventing further matches until more
|
70
76
|
# output is generated otherwise. (default: false)
|
71
77
|
def initialize(cmd, args={})
|
72
|
-
|
73
|
-
|
74
|
-
end
|
75
|
-
|
76
|
-
cmd = cmd.path if cmd.kind_of? File
|
77
|
-
|
78
|
-
@buffer = ''.encode("UTF-8")
|
79
|
-
@discard = ''.encode("UTF-8")
|
80
|
-
|
81
|
-
@timeout = args[:timeout] || 30
|
82
|
-
@flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer]
|
83
|
-
@buffer_size = args[:buffer_size] || 8192
|
84
|
-
@constrain = args[:constrain] || false
|
85
|
-
@force_match = args[:force_match] || false
|
78
|
+
cmd = cmd.path if cmd.kind_of?(File)
|
79
|
+
raise ArgumentError, "String or File expected" unless cmd.kind_of?(String)
|
86
80
|
|
81
|
+
parse_options(args)
|
82
|
+
@buffer = ''
|
83
|
+
@discard = ''
|
87
84
|
@out_mutex = Mutex.new
|
88
85
|
@out_update = false
|
89
86
|
@interact = false
|
90
87
|
|
91
88
|
@stdout,@stdin,@pid = PTY.spawn(cmd)
|
92
|
-
@stdout.winsize =
|
89
|
+
@stdout.winsize = $stdout.winsize if $stdout.tty?
|
93
90
|
|
94
91
|
Thread.new do
|
95
|
-
|
96
|
-
unless select([@stdout], nil, nil, @timeout).nil?
|
97
|
-
buf = ''.encode("UTF-8")
|
98
|
-
|
99
|
-
begin
|
100
|
-
@stdout.sysread(@buffer_size, buf)
|
101
|
-
rescue Errno::EIO #Application went away.
|
102
|
-
@pid = 0
|
103
|
-
break
|
104
|
-
end
|
105
|
-
|
106
|
-
force_utf8(buf) unless buf.valid_encoding?
|
107
|
-
print_buffer(buf)
|
108
|
-
|
109
|
-
@out_mutex.synchronize do
|
110
|
-
@buffer << buf
|
111
|
-
if @buffer.length > @buffer_size && @constrain
|
112
|
-
@buffer = @buffer[-@buffer_size..-1]
|
113
|
-
end
|
114
|
-
@out_update = true
|
115
|
-
end
|
116
|
-
end
|
117
|
-
end
|
92
|
+
process_output
|
118
93
|
end
|
119
94
|
|
120
95
|
Thread.new do
|
@@ -139,41 +114,19 @@ class Expectr
|
|
139
114
|
|
140
115
|
blocking = args[:blocking] || false
|
141
116
|
@flush_buffer = args[:flush_buffer].nil? ? true : args[:flush_buffer]
|
142
|
-
@interact = true
|
143
|
-
|
144
|
-
# Save our old tty settings and set up our new environment
|
145
|
-
old_tty = `stty -g`
|
146
|
-
`stty -icanon min 1 time 0 -echo`
|
147
117
|
|
148
|
-
# SIGINT should be sent along to the process.
|
149
|
-
old_int_trap = trap 'INT' do
|
150
|
-
send "\C-c"
|
151
|
-
end
|
152
|
-
|
153
|
-
# SIGTSTP should be sent along to the process as well.
|
154
|
-
old_tstp_trap = trap 'TSTP' do
|
155
|
-
send "\C-z"
|
156
|
-
end
|
157
|
-
|
158
|
-
# SIGWINCH should trigger an update to the child process
|
159
|
-
old_winch_trap = trap 'WINCH' do
|
160
|
-
@stdout.winsize = STDOUT.winsize
|
161
|
-
end
|
162
|
-
|
163
118
|
interact = Thread.new do
|
164
|
-
|
119
|
+
env = prepare_interact_environment
|
120
|
+
input = ''
|
121
|
+
|
165
122
|
while @pid > 0 && @interact
|
166
|
-
if select([
|
167
|
-
c =
|
123
|
+
if select([$stdin], nil, nil, 1)
|
124
|
+
c = $stdin.getc.chr
|
168
125
|
send c unless c.nil?
|
169
126
|
end
|
170
127
|
end
|
171
128
|
|
172
|
-
|
173
|
-
trap 'TSTP', old_tstp_trap
|
174
|
-
trap 'WINCH', old_winch_trap
|
175
|
-
`stty #{old_tty}`
|
176
|
-
@interact = false
|
129
|
+
restore_environment(env)
|
177
130
|
end
|
178
131
|
|
179
132
|
blocking ? interact.join : interact
|
@@ -256,27 +209,15 @@ class Expectr
|
|
256
209
|
# Raises Timeout::Error if a match isn't found in time, unless recoverable
|
257
210
|
def expect(pattern, recoverable = false)
|
258
211
|
match = nil
|
259
|
-
@out_update
|
260
|
-
|
261
|
-
|
262
|
-
when String
|
263
|
-
pattern = Regexp.new(Regexp.quote(pattern))
|
264
|
-
when Regexp
|
265
|
-
else
|
212
|
+
@out_update ||= @force_match
|
213
|
+
pattern = Regexp.new(Regexp.quote(pattern)) if pattern.kind_of?(String)
|
214
|
+
unless pattern.kind_of?(Regexp)
|
266
215
|
raise TypeError, "Pattern class should be String or Regexp"
|
267
216
|
end
|
268
217
|
|
269
218
|
begin
|
270
219
|
Timeout::timeout(@timeout) do
|
271
|
-
|
272
|
-
if @out_update
|
273
|
-
@out_mutex.synchronize do
|
274
|
-
match = pattern.match @buffer
|
275
|
-
@out_update = false
|
276
|
-
end
|
277
|
-
end
|
278
|
-
sleep 0.1
|
279
|
-
end
|
220
|
+
match = check_match(pattern)
|
280
221
|
end
|
281
222
|
|
282
223
|
@out_mutex.synchronize do
|
@@ -296,7 +237,7 @@ class Expectr
|
|
296
237
|
# Returns nothing.
|
297
238
|
def clear_buffer!
|
298
239
|
@out_mutex.synchronize do
|
299
|
-
@buffer = ''
|
240
|
+
@buffer = ''
|
300
241
|
@out_update = false
|
301
242
|
end
|
302
243
|
end
|
@@ -308,14 +249,14 @@ class Expectr
|
|
308
249
|
@stdout.winsize
|
309
250
|
end
|
310
251
|
|
311
|
-
# Internal: Print buffer to
|
252
|
+
# Internal: Print buffer to $stdout if @flush_buffer is true
|
312
253
|
#
|
313
|
-
# buf - String to be printed to
|
254
|
+
# buf - String to be printed to $stdout
|
314
255
|
#
|
315
256
|
# Returns nothing.
|
316
257
|
def print_buffer(buf)
|
317
258
|
print buf if @flush_buffer
|
318
|
-
|
259
|
+
$stdout.flush unless $stdout.sync
|
319
260
|
end
|
320
261
|
|
321
262
|
# Internal: Encode a String twice to force UTF-8 encoding, dropping
|
@@ -325,6 +266,128 @@ class Expectr
|
|
325
266
|
#
|
326
267
|
# Returns the encoded String.
|
327
268
|
def force_utf8(buf)
|
269
|
+
return buf if buf.valid_encoding?
|
328
270
|
buf.force_encoding('ISO-8859-1').encode('UTF-8', 'UTF-8', replace: nil)
|
329
271
|
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
# Internal: Determine values of instance options and set instance variables
|
276
|
+
# appropriately, allowing for default values where nothing is passed.
|
277
|
+
#
|
278
|
+
# args - A Hash used to specify options for the new object (default: {}):
|
279
|
+
# :timeout - Number of seconds that a call to Expectr#expect has
|
280
|
+
# to complete.
|
281
|
+
# :flush_buffer - Whether to flush output of the process to the
|
282
|
+
# console.
|
283
|
+
# :buffer_size - Number of bytes to attempt to read from sub-process
|
284
|
+
# at a time. If :constrain is true, this will be the
|
285
|
+
# maximum size of the internal buffer as well.
|
286
|
+
# :constrain - Whether to constrain the internal buffer from the
|
287
|
+
# sub-process to :buffer_size.
|
288
|
+
# :force_match - Whether to always attempt to match against the
|
289
|
+
# internal buffer on a call to Expectr#expect. This
|
290
|
+
# is relevant following a failed call to
|
291
|
+
# Expectr#expect, which will leave the update status
|
292
|
+
# set to false, preventing further matches until more
|
293
|
+
# output is generated otherwise.
|
294
|
+
#
|
295
|
+
# Returns nothing.
|
296
|
+
def parse_options(args)
|
297
|
+
@timeout = args[:timeout] || DEFAULT_TIMEOUT
|
298
|
+
@buffer_size = args[:buffer_size] || DEFAULT_BUFFER_SIZE
|
299
|
+
@constrain = args[:constrain] || DEFAULT_CONSTRAIN
|
300
|
+
@force_match = args[:force_match] || DEFAULT_FORCE_MATCH
|
301
|
+
@flush_buffer = args[:flush_buffer]
|
302
|
+
@flush_buffer = DEFAULT_FLUSH_BUFFER if @flush_buffer.nil?
|
303
|
+
end
|
304
|
+
|
305
|
+
# Internal: Read from the process's stdout. Force UTF-8 encoding, append to
|
306
|
+
# the internal buffer, and print to $stdout if appropriate.
|
307
|
+
#
|
308
|
+
# Returns nothing.
|
309
|
+
def process_output
|
310
|
+
while @pid > 0
|
311
|
+
unless select([@stdout], nil, nil, @timeout).nil?
|
312
|
+
buf = ''
|
313
|
+
|
314
|
+
begin
|
315
|
+
@stdout.sysread(@buffer_size, buf)
|
316
|
+
rescue Errno::EIO #Application went away.
|
317
|
+
@pid = 0
|
318
|
+
return
|
319
|
+
end
|
320
|
+
|
321
|
+
print_buffer(force_utf8(buf))
|
322
|
+
|
323
|
+
@out_mutex.synchronize do
|
324
|
+
@buffer << buf
|
325
|
+
if @constrain && @buffer.length > @buffer_size
|
326
|
+
@buffer = @buffer[-@buffer_size..-1]
|
327
|
+
end
|
328
|
+
@out_update = true
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
# Internal: Prepare environment for interact mode, saving original
|
335
|
+
# environment parameters.
|
336
|
+
#
|
337
|
+
# Returns a Hash object with two keys: :tty containing original tty
|
338
|
+
# information and :sig containing signal handlers which have been replaced.
|
339
|
+
def prepare_interact_environment
|
340
|
+
env = {sig: {}}
|
341
|
+
# Save our old tty settings and set up our new environment
|
342
|
+
env[:tty] = `stty -g`
|
343
|
+
`stty -icanon min 1 time 0 -echo`
|
344
|
+
|
345
|
+
# SIGINT should be sent along to the process.
|
346
|
+
env[:sig]['INT'] = trap 'INT' do
|
347
|
+
send "\C-c"
|
348
|
+
end
|
349
|
+
|
350
|
+
# SIGTSTP should be sent along to the process as well.
|
351
|
+
env[:sig]['TSTP'] = trap 'TSTP' do
|
352
|
+
send "\C-z"
|
353
|
+
end
|
354
|
+
|
355
|
+
# SIGWINCH should trigger an update to the child process
|
356
|
+
env[:sig]['WINCH'] = trap 'WINCH' do
|
357
|
+
@stdout.winsize = $stdout.winsize
|
358
|
+
end
|
359
|
+
|
360
|
+
@interact = true
|
361
|
+
env
|
362
|
+
end
|
363
|
+
|
364
|
+
# Internal: Restore environment post interact mode from saved parameters.
|
365
|
+
#
|
366
|
+
# Returns nothing.
|
367
|
+
def restore_environment(env)
|
368
|
+
env[:sig].each_key do |sig|
|
369
|
+
trap sig, env[:sig][sig]
|
370
|
+
end
|
371
|
+
`stty #{env[:tty]}`
|
372
|
+
@interact = false
|
373
|
+
end
|
374
|
+
|
375
|
+
# Internal: Check for a match against a given pattern until a match is found.
|
376
|
+
# This method should be wrapped in a Timeout block or otherwise have some
|
377
|
+
# mechanism to break out of the loop.
|
378
|
+
#
|
379
|
+
# Returns a MatchData object containing the match found.
|
380
|
+
def check_match(pattern)
|
381
|
+
match = nil
|
382
|
+
while match.nil?
|
383
|
+
if @out_update
|
384
|
+
@out_mutex.synchronize do
|
385
|
+
match = pattern.match(@buffer)
|
386
|
+
@out_update = false
|
387
|
+
end
|
388
|
+
end
|
389
|
+
sleep 0.1
|
390
|
+
end
|
391
|
+
match
|
392
|
+
end
|
330
393
|
end
|
data/test/test_core.rb
CHANGED
@@ -4,7 +4,7 @@ class CoreTests < Test::Unit::TestCase
|
|
4
4
|
# For the purpose of testing, we will assume we are working within a POSIX
|
5
5
|
# environment.
|
6
6
|
def setup
|
7
|
-
@exp = Expectr.new("ls /dev", flush_buffer: false, timeout: 1,
|
7
|
+
@exp = Expectr.new("ls /dev && sleep 5", flush_buffer: false, timeout: 1,
|
8
8
|
buffer_size: 4096)
|
9
9
|
end
|
10
10
|
|
@@ -33,7 +33,7 @@ class CoreTests < Test::Unit::TestCase
|
|
33
33
|
|
34
34
|
def test_clear_buffer
|
35
35
|
sleep 1
|
36
|
-
assert_not_equal(@exp.buffer
|
36
|
+
assert_not_equal('', @exp.buffer)
|
37
37
|
@exp.clear_buffer!
|
38
38
|
assert_equal('', @exp.buffer)
|
39
39
|
end
|
@@ -45,6 +45,6 @@ class CoreTests < Test::Unit::TestCase
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def test_pid_set
|
48
|
-
|
48
|
+
assert_not_equal(0, @exp.pid)
|
49
49
|
end
|
50
50
|
end
|
data/test/test_interaction.rb
CHANGED
@@ -38,26 +38,26 @@ class InteractionTest < Test::Unit::TestCase
|
|
38
38
|
end
|
39
39
|
|
40
40
|
def test_winsize_set
|
41
|
-
assert_not_equal
|
41
|
+
assert_not_equal([0, 0], @exp.winsize)
|
42
42
|
end
|
43
43
|
|
44
44
|
def test_interact_sets_appropriate_flags
|
45
45
|
[
|
46
46
|
Thread.new {
|
47
|
-
assert_equal
|
47
|
+
assert_equal(false, @exp.interact?)
|
48
48
|
|
49
49
|
sleep 0.5
|
50
50
|
@exp.interact!.join
|
51
51
|
},
|
52
52
|
Thread.new {
|
53
53
|
sleep 1
|
54
|
-
assert_equal
|
55
|
-
assert_equal
|
54
|
+
assert_equal(true, @exp.flush_buffer)
|
55
|
+
assert_equal(true, @exp.interact?)
|
56
56
|
|
57
57
|
@exp.flush_buffer = false
|
58
58
|
@exp.send("quit\n")
|
59
59
|
}
|
60
|
-
].each {|x| x.join}
|
60
|
+
].each { |x| x.join }
|
61
61
|
end
|
62
62
|
|
63
63
|
def test_interact_mode
|
@@ -72,9 +72,9 @@ class InteractionTest < Test::Unit::TestCase
|
|
72
72
|
@exp.send("300+21\n")
|
73
73
|
@exp.send("quit\n")
|
74
74
|
}
|
75
|
-
].each {|x| x.join}
|
75
|
+
].each { |x| x.join }
|
76
76
|
|
77
|
-
assert_not_nil
|
77
|
+
assert_not_nil(@exp.expect(/321/))
|
78
78
|
end
|
79
79
|
|
80
80
|
def test_leaving_interact_mode
|
@@ -87,10 +87,10 @@ class InteractionTest < Test::Unit::TestCase
|
|
87
87
|
sleep 1
|
88
88
|
@exp.flush_buffer = false
|
89
89
|
assert_nothing_raised { @exp.leave! }
|
90
|
-
assert_equal
|
90
|
+
assert_equal(false, @exp.interact?)
|
91
91
|
@exp.send("quit\n")
|
92
92
|
}
|
93
|
-
].each {|x| x.join}
|
93
|
+
].each { |x| x.join }
|
94
94
|
end
|
95
95
|
|
96
96
|
def test_blocking_interact_mode
|
@@ -105,8 +105,8 @@ class InteractionTest < Test::Unit::TestCase
|
|
105
105
|
@exp.send("300+21\n")
|
106
106
|
@exp.send("quit\n")
|
107
107
|
}
|
108
|
-
].each {|x| x.join}
|
108
|
+
].each { |x| x.join }
|
109
109
|
|
110
|
-
assert_not_nil
|
110
|
+
assert_not_nil(@exp.expect(/321/))
|
111
111
|
end
|
112
112
|
end
|
data/test/test_signals.rb
CHANGED
@@ -14,22 +14,23 @@ class SignalHandlerTest < Test::Unit::TestCase
|
|
14
14
|
},
|
15
15
|
Thread.new {
|
16
16
|
sleep 1
|
17
|
-
@exp.flush_buffer=false
|
17
|
+
@exp.flush_buffer = false
|
18
18
|
assert_nothing_raised do
|
19
|
-
$stdout.winsize = [10,10]
|
19
|
+
$stdout.winsize = [10, 10]
|
20
20
|
end
|
21
21
|
sleep 0.1
|
22
|
-
assert_equal
|
22
|
+
assert_equal([10, 10], @exp.winsize)
|
23
23
|
@exp.puts("quit")
|
24
24
|
}
|
25
|
-
].each {|x| x.join}
|
25
|
+
].each { |x| x.join }
|
26
26
|
|
27
27
|
$stdout.winsize = winsize
|
28
28
|
end
|
29
29
|
|
30
30
|
def test_kill_process
|
31
|
-
assert_equal
|
32
|
-
|
31
|
+
assert_equal(true, @exp.kill!)
|
32
|
+
sleep 0.5
|
33
|
+
assert_equal(0, @exp.pid)
|
33
34
|
assert_raises(Expectr::ProcessError) { @exp.send("test\n") }
|
34
35
|
end
|
35
36
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: expectr
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.
|
4
|
+
version: 1.1.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-05-03 00:00:00.000000000 Z
|
13
13
|
dependencies: []
|
14
14
|
description: Expectr is an interface to the functionality of Expect in Ruby
|
15
15
|
email: chris@chriswuest.com
|