expectr 1.1.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
1
  class Expectr
2
- VERSION = '1.1.0'
2
+ VERSION = '1.1.1'
3
3
  end
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 STDOUT
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
- unless cmd.kind_of? String or cmd.kind_of? File
73
- raise ArgumentError, "String or File expected"
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 = STDOUT.winsize
89
+ @stdout.winsize = $stdout.winsize if $stdout.tty?
93
90
 
94
91
  Thread.new do
95
- while @pid > 0
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
- input = ''.encode("UTF-8")
119
+ env = prepare_interact_environment
120
+ input = ''
121
+
165
122
  while @pid > 0 && @interact
166
- if select([STDIN], nil, nil, 1)
167
- c = STDIN.getc.chr
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
- trap 'INT', old_int_trap
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 = true if @force_match
260
-
261
- case pattern
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
- while match.nil?
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 = ''.encode("UTF-8")
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 STDOUT if @flush_buffer is true
252
+ # Internal: Print buffer to $stdout if @flush_buffer is true
312
253
  #
313
- # buf - String to be printed to STDOUT
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
- STDOUT.flush unless STDOUT.sync
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
- assert @exp.pid > 0
48
+ assert_not_equal(0, @exp.pid)
49
49
  end
50
50
  end
@@ -38,26 +38,26 @@ class InteractionTest < Test::Unit::TestCase
38
38
  end
39
39
 
40
40
  def test_winsize_set
41
- assert_not_equal [0, 0], @exp.winsize
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 false, @exp.interact?
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 true, @exp.flush_buffer
55
- assert_equal true, @exp.interact?
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 @exp.expect(/321/)
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 false, @exp.interact?
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 @exp.expect(/321/)
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 [10,10], @exp.winsize
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 true, @exp.kill!
32
- assert_equal 0, @exp.pid
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.0
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-02-18 00:00:00.000000000 Z
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