expectr 1.1.0 → 1.1.1
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/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
|