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.
@@ -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