shell_test 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +7 -0
- data/README.rdoc +42 -21
- data/lib/shell_test/file_methods.rb +78 -45
- data/lib/shell_test/shell_methods/agent.rb +15 -17
- data/lib/shell_test/shell_methods/session.rb +47 -64
- data/lib/shell_test/shell_methods/utils.rb +51 -17
- data/lib/shell_test/shell_methods.rb +13 -0
- data/lib/shell_test/version.rb +1 -1
- metadata +5 -5
data/History.rdoc
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.4.0 2011/11/01
|
2
|
+
|
3
|
+
Renames prepare methods in FileMethods to setup_file and setup_dir, and
|
4
|
+
changes numerous class methods for setting up method dir cleanup.
|
5
|
+
|
6
|
+
* Parser now escapes special chars in inline prompts [issue #16]
|
7
|
+
|
1
8
|
== 0.3.0 2011/10/23
|
2
9
|
|
3
10
|
Rewrite and expansion of library to use PTY shell sessions rather than
|
data/README.rdoc
CHANGED
@@ -16,20 +16,44 @@ ShellTest builds on modules that provide specific functionality. The modules
|
|
16
16
|
may be used independently, but by including ShellTest you get them all:
|
17
17
|
|
18
18
|
require 'shell_test/unit'
|
19
|
-
|
19
|
+
|
20
20
|
class ShellTestExample < Test::Unit::TestCase
|
21
21
|
include ShellTest
|
22
|
-
|
22
|
+
|
23
23
|
def test_a_script
|
24
|
-
script =
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
script = setup_file 'script.sh', %{
|
25
|
+
echo goodnight $1
|
26
|
+
}
|
27
|
+
|
28
28
|
assert_script %{
|
29
29
|
$ sh '#{script}' moon
|
30
30
|
goodnight moon
|
31
31
|
}, :exitstatus => 0
|
32
32
|
end
|
33
|
+
|
34
|
+
def test_a_script_that_takes_input
|
35
|
+
script = setup_file 'script.sh', %{
|
36
|
+
stty -echo
|
37
|
+
while true; do
|
38
|
+
printf "Do you wish to continue? [y/n]: "
|
39
|
+
read answer
|
40
|
+
case $answer in
|
41
|
+
y ) printf "\nOk!\n"; break;;
|
42
|
+
n ) printf "\nToo bad.\n"; break;;
|
43
|
+
* ) printf "\nPlease answer y or n.\n";;
|
44
|
+
esac
|
45
|
+
done
|
46
|
+
stty echo
|
47
|
+
}
|
48
|
+
|
49
|
+
assert_script %{
|
50
|
+
$ sh '#{script}'
|
51
|
+
Do you wish to continue? [y/n]: {{hmmm}}
|
52
|
+
Please answer y or n.
|
53
|
+
Do you wish to continue? [y/n]: {{y}}
|
54
|
+
Ok!
|
55
|
+
}
|
56
|
+
end
|
33
57
|
end
|
34
58
|
|
35
59
|
==== {ShellMethods}[link:classes/ShellTest/ShellMethods.html]
|
@@ -81,17 +105,13 @@ from the terminal.
|
|
81
105
|
}
|
82
106
|
end
|
83
107
|
|
84
|
-
def
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
Password:{{cr@pWhatIsIt}}
|
92
|
-
Sorry, try again.
|
93
|
-
sudo: 3 incorrect password attempts
|
94
|
-
}, :max_run_time => 10
|
108
|
+
def test_scripts_where_alternate_prompts_are_needed
|
109
|
+
with_env('PS1' => '% ') do
|
110
|
+
assert_script %{
|
111
|
+
% echo '$ $ $'
|
112
|
+
$ $ $
|
113
|
+
}
|
114
|
+
end
|
95
115
|
end
|
96
116
|
end
|
97
117
|
|
@@ -99,7 +119,8 @@ from the terminal.
|
|
99
119
|
|
100
120
|
Sets up a temporary, test-specific directory for working with files. This
|
101
121
|
approach is better in most cases than using Tempfile because you can flag the
|
102
|
-
directory to be saved on a failure (using ENV['KEEP_OUTPUTS']='true')
|
122
|
+
directory to be saved on a failure (using ENV['KEEP_OUTPUTS']='true') and
|
123
|
+
immediately know where to look for your files.
|
103
124
|
|
104
125
|
By default the directory is guessed based off of the test file and test
|
105
126
|
method. If this example were located in the 'test/file_methods_example.rb'
|
@@ -107,12 +128,12 @@ file, then the directory for the test case would be
|
|
107
128
|
'test/file_methods_example/test_preparation_of_a_test_specific_file'.
|
108
129
|
|
109
130
|
require 'shell_test/unit'
|
110
|
-
|
131
|
+
|
111
132
|
class FileMethodsExample < Test::Unit::TestCase
|
112
133
|
include ShellTest::FileMethods
|
113
134
|
|
114
|
-
def
|
115
|
-
path =
|
135
|
+
def test_setup_of_a_test_specific_file
|
136
|
+
path = setup_file('dir/file.txt') {|io| io << 'content' }
|
116
137
|
assert_equal "content", File.read(path)
|
117
138
|
end
|
118
139
|
end
|
@@ -6,29 +6,47 @@ module ShellTest
|
|
6
6
|
module ClassMethods
|
7
7
|
attr_accessor :class_dir
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
# A registry tracking paths_to_cleanup for the current class.
|
10
|
+
attr_reader :paths_to_cleanup_registry
|
11
|
+
|
12
|
+
# A hash of (method_name, [relative_path]) pairs identifying which
|
13
|
+
# relative paths on each method have been marked on this class or
|
14
|
+
# inherited from ancestors. Entries in paths_to_cleanup should not be
|
15
|
+
# edited directly. Instead use:
|
16
|
+
#
|
17
|
+
# cleanup : turn on cleanup for methods
|
18
|
+
# do_not_cleanup : turn off cleanup for methods
|
19
|
+
# default_paths_to_cleanup : set the default paths to cleanup
|
20
|
+
#
|
21
|
+
# Or if you need very precise editing, use (with the same semantics as
|
22
|
+
# {define/remove/undef}_method):
|
23
|
+
#
|
24
|
+
# define_paths_to_cleanup
|
25
|
+
# remove_paths_to_cleanup
|
26
|
+
# undef_paths_to_cleanup
|
27
|
+
#
|
28
|
+
def paths_to_cleanup
|
29
|
+
@paths_to_cleanup ||= begin
|
30
|
+
paths_to_cleanup = {}
|
14
31
|
|
15
32
|
ancestors.reverse.each do |ancestor|
|
16
33
|
next unless ancestor.kind_of?(ClassMethods)
|
17
|
-
ancestor.
|
34
|
+
ancestor.paths_to_cleanup_registry.each_pair do |key, value|
|
18
35
|
if value.nil?
|
19
|
-
|
36
|
+
paths_to_cleanup.delete(key)
|
20
37
|
else
|
21
|
-
|
38
|
+
paths_to_cleanup[key] = value
|
22
39
|
end
|
23
40
|
end
|
24
41
|
end
|
25
42
|
|
26
|
-
|
43
|
+
paths_to_cleanup
|
27
44
|
end
|
28
45
|
end
|
29
46
|
|
30
|
-
|
31
|
-
|
47
|
+
# Resets paths_to_cleanup such that it will be recalculated.
|
48
|
+
def reset_paths_to_cleanup
|
49
|
+
@paths_to_cleanup = nil
|
32
50
|
end
|
33
51
|
|
34
52
|
protected
|
@@ -48,13 +66,13 @@ module ShellTest
|
|
48
66
|
base.class_dir = Dir.tmpdir
|
49
67
|
end
|
50
68
|
|
51
|
-
base.
|
52
|
-
unless base.instance_variable_defined?(:@
|
53
|
-
base.instance_variable_set(:@
|
69
|
+
base.reset_paths_to_cleanup
|
70
|
+
unless base.instance_variable_defined?(:@paths_to_cleanup_registry)
|
71
|
+
base.instance_variable_set(:@paths_to_cleanup_registry, {})
|
54
72
|
end
|
55
73
|
|
56
|
-
unless base.instance_variable_defined?(:@
|
57
|
-
base.instance_variable_set(:@
|
74
|
+
unless base.instance_variable_defined?(:@default_paths_to_cleanup)
|
75
|
+
base.instance_variable_set(:@default_paths_to_cleanup, ['.'])
|
58
76
|
end
|
59
77
|
|
60
78
|
unless base.instance_variable_defined?(:@cleanup)
|
@@ -67,47 +85,65 @@ module ShellTest
|
|
67
85
|
super
|
68
86
|
end
|
69
87
|
|
70
|
-
|
71
|
-
|
72
|
-
|
88
|
+
# Define the paths_to_cleanup for the specified method. The settings
|
89
|
+
# are inherited, but can be overridden in subclasses.
|
90
|
+
def define_paths_to_cleanup(method_name, relative_paths)
|
91
|
+
reset_paths_to_cleanup
|
92
|
+
paths_to_cleanup_registry[method_name.to_sym] = relative_paths
|
73
93
|
end
|
74
94
|
|
75
|
-
|
76
|
-
|
77
|
-
|
95
|
+
# Remove the paths_to_cleanup for the method as defined on self. The
|
96
|
+
# paths_to_cleanup inherited from ancestors will still be in effect.
|
97
|
+
def remove_paths_to_cleanup(method_name)
|
98
|
+
reset_paths_to_cleanup
|
99
|
+
paths_to_cleanup_registry.delete(method_name.to_sym)
|
78
100
|
end
|
79
101
|
|
80
|
-
|
81
|
-
|
82
|
-
|
102
|
+
# Undefines the paths_to_cleanup for the method, preventing inheritance
|
103
|
+
# from ancestors.
|
104
|
+
def undef_paths_to_cleanup(method_name)
|
105
|
+
reset_paths_to_cleanup
|
106
|
+
paths_to_cleanup_registry[method_name.to_sym] = nil
|
83
107
|
end
|
84
108
|
|
85
|
-
|
86
|
-
|
109
|
+
# Sets the default paths_to_cleanup for subsequent methods.
|
110
|
+
def default_paths_to_cleanup(*relative_paths)
|
111
|
+
@default_paths_to_cleanup = relative_paths
|
87
112
|
end
|
88
113
|
|
114
|
+
# Mark the methods for cleanup using the default_paths_to_cleanup. Call
|
115
|
+
# without method names to mark all subsequent methods for cleanup.
|
89
116
|
def cleanup(*method_names)
|
90
117
|
if method_names.empty?
|
91
118
|
@cleanup = true
|
92
119
|
else
|
93
120
|
method_names.each do |method_name|
|
94
|
-
|
121
|
+
define_paths_to_cleanup method_name, @default_paths_to_cleanup
|
95
122
|
end
|
96
123
|
end
|
97
124
|
end
|
98
125
|
|
99
|
-
|
126
|
+
# Prevent cleanup for the methods. Call without method names to prevent
|
127
|
+
# cleanup for subsequent methods.
|
128
|
+
def do_not_cleanup(*method_names)
|
100
129
|
if method_names.empty?
|
101
130
|
@cleanup = false
|
102
131
|
else
|
103
132
|
method_names.each do |method_name|
|
104
|
-
|
133
|
+
undef_paths_to_cleanup method_name
|
105
134
|
end
|
106
135
|
end
|
107
136
|
end
|
108
137
|
|
138
|
+
# Returns true if the method should be marked for cleanup when added.
|
139
|
+
def mark_for_cleanup?(method_name)
|
140
|
+
@cleanup && !paths_to_cleanup_registry.has_key?(method_name.to_sym) && method_name.to_s.index("test_") == 0
|
141
|
+
end
|
142
|
+
|
143
|
+
# Overridden to ensure methods marked for cleanup are cleaned up.
|
109
144
|
def method_added(sym)
|
110
|
-
|
145
|
+
super
|
146
|
+
if mark_for_cleanup?(sym)
|
111
147
|
cleanup sym
|
112
148
|
end
|
113
149
|
end
|
@@ -208,7 +244,7 @@ module ShellTest
|
|
208
244
|
end
|
209
245
|
|
210
246
|
# Creates a directory under method_dir.
|
211
|
-
def
|
247
|
+
def setup_dir(relative_path)
|
212
248
|
target_dir = path(relative_path)
|
213
249
|
unless File.directory?(target_dir)
|
214
250
|
FileUtils.mkdir_p(target_dir)
|
@@ -216,8 +252,10 @@ module ShellTest
|
|
216
252
|
target_dir
|
217
253
|
end
|
218
254
|
|
219
|
-
|
220
|
-
|
255
|
+
alias prepare_dir setup_dir
|
256
|
+
|
257
|
+
# Same as setup_file but does not outdent content.
|
258
|
+
def _setup_file(relative_path, content=nil, &block)
|
221
259
|
target = path(relative_path)
|
222
260
|
|
223
261
|
if File.exists?(target)
|
@@ -241,16 +279,16 @@ module ShellTest
|
|
241
279
|
# Content provided as a string is outdented (see StringMethods#outdent),
|
242
280
|
# so this syntax is possible:
|
243
281
|
#
|
244
|
-
# path =
|
282
|
+
# path = setup_file 'file', %{
|
245
283
|
# line one
|
246
284
|
# line two
|
247
285
|
# }
|
248
286
|
# File.read(path) # => "line one\nline two\n"
|
249
287
|
#
|
250
288
|
# Returns the absolute path to the new file.
|
251
|
-
def
|
289
|
+
def setup_file(relative_path, content=nil, &block)
|
252
290
|
content = outdent(content) if content
|
253
|
-
|
291
|
+
_setup_file(relative_path, content, &block)
|
254
292
|
end
|
255
293
|
|
256
294
|
# Returns the content of the file under method_dir, if it exists.
|
@@ -272,15 +310,10 @@ module ShellTest
|
|
272
310
|
FileUtils.rm_r(full_path) if File.exists?(full_path)
|
273
311
|
end
|
274
312
|
|
275
|
-
#
|
276
|
-
def cleanup_methods
|
277
|
-
self.class.cleanup_methods
|
278
|
-
end
|
279
|
-
|
280
|
-
# Recursively removes paths specified for cleanup in cleanup_methods.
|
313
|
+
# Recursively removes paths specified for cleanup by paths_to_cleanup.
|
281
314
|
def cleanup
|
282
|
-
if
|
283
|
-
|
315
|
+
if paths = self.class.paths_to_cleanup[method_name.to_sym]
|
316
|
+
paths.each {|path| remove(path) }
|
284
317
|
end
|
285
318
|
end
|
286
319
|
end
|
@@ -20,32 +20,33 @@ module ShellTest
|
|
20
20
|
# timer using `timer.timeout=` and retrieved via `timer.timeout`.
|
21
21
|
attr_reader :timer
|
22
22
|
|
23
|
-
|
24
|
-
attr_reader :prompts
|
25
|
-
|
26
|
-
def initialize(master, slave, timer, prompts={})
|
23
|
+
def initialize(master, slave, timer)
|
27
24
|
@master = master
|
28
25
|
@slave = slave
|
29
26
|
@timer = timer
|
30
|
-
@prompts = prompts
|
31
27
|
end
|
32
28
|
|
33
|
-
def on(
|
34
|
-
output = expect(
|
29
|
+
def on(regexp, input, timeout=nil)
|
30
|
+
output = expect(regexp, timeout)
|
35
31
|
write(input)
|
36
32
|
output
|
37
33
|
end
|
38
34
|
|
39
|
-
# Reads from the slave until the
|
40
|
-
#
|
41
|
-
#
|
35
|
+
# Reads from the slave until the regexp is matched and returns the
|
36
|
+
# resulting string. If regexp is a String, then it is converted into a
|
37
|
+
# regexp assuming it's a literal prompt (ie /^regexp\z/ - where the
|
38
|
+
# regexp string is Regexp escaped). If regexp is nil then expect reads
|
39
|
+
# until the slave eof.
|
42
40
|
#
|
43
41
|
# A timeout may be given. If the slave doesn't produce the expected
|
44
42
|
# string within the timeout then expect raises a ReadError. A ReadError
|
45
|
-
# will be also be raised if the slave eof is reached before the
|
43
|
+
# will be also be raised if the slave eof is reached before the regexp
|
46
44
|
# matches.
|
47
|
-
def expect(
|
48
|
-
regexp
|
45
|
+
def expect(regexp, timeout=nil)
|
46
|
+
if regexp.kind_of?(String)
|
47
|
+
regexp = /#{Regexp.escape(regexp)}\z/
|
48
|
+
end
|
49
|
+
|
49
50
|
timer.timeout = timeout
|
50
51
|
|
51
52
|
buffer = ''
|
@@ -66,10 +67,7 @@ module ShellTest
|
|
66
67
|
begin
|
67
68
|
c = slave.read(1)
|
68
69
|
rescue Errno::EIO
|
69
|
-
#
|
70
|
-
# an EIO error when a terminal disconnect occurs and an EIO
|
71
|
-
# condition occurs - the exact behavior is unspecified but the
|
72
|
-
# meaning is the same... no more data is available, so break.
|
70
|
+
# see notes in Utils#spawn
|
73
71
|
c = nil
|
74
72
|
end
|
75
73
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'shell_test/env_methods'
|
2
1
|
require 'shell_test/shell_methods/agent'
|
3
2
|
require 'shell_test/shell_methods/utils'
|
4
3
|
require 'strscan'
|
@@ -8,24 +7,15 @@ module ShellTest
|
|
8
7
|
|
9
8
|
# Session is an engine for running shell sessions.
|
10
9
|
class Session
|
11
|
-
include EnvMethods
|
12
10
|
include Utils
|
13
11
|
|
14
12
|
DEFAULT_SHELL = '/bin/sh'
|
15
|
-
DEFAULT_PS1 = '$ '
|
16
|
-
DEFAULT_PS2 = '> '
|
17
13
|
DEFAULT_STTY = '-echo -onlcr'
|
18
14
|
DEFAULT_MAX_RUN_TIME = 1
|
19
15
|
|
20
16
|
# The session shell
|
21
17
|
attr_reader :shell
|
22
18
|
|
23
|
-
# The shell PS1
|
24
|
-
attr_reader :ps1
|
25
|
-
|
26
|
-
# The shell PS2
|
27
|
-
attr_reader :ps2
|
28
|
-
|
29
19
|
# Aguments string passed stty on run
|
30
20
|
attr_reader :stty
|
31
21
|
|
@@ -47,18 +37,22 @@ module ShellTest
|
|
47
37
|
|
48
38
|
def initialize(options={})
|
49
39
|
@shell = options[:shell] || DEFAULT_SHELL
|
50
|
-
@ps1 = options[:ps1] || DEFAULT_PS1
|
51
|
-
@ps2 = options[:ps2] || DEFAULT_PS2
|
52
40
|
@stty = options[:stty] || DEFAULT_STTY
|
53
41
|
@timer = options[:timer] || Timer.new
|
54
42
|
@max_run_time = options[:max_run_time] || DEFAULT_MAX_RUN_TIME
|
55
43
|
@steps = [[nil, nil, nil, nil]]
|
56
44
|
@log = []
|
57
45
|
@status = nil
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
46
|
+
end
|
47
|
+
|
48
|
+
# The shell PS1, as configured in ENV.
|
49
|
+
def ps1
|
50
|
+
ENV['PS1']
|
51
|
+
end
|
52
|
+
|
53
|
+
# The shell PS2, as configured in ENV.
|
54
|
+
def ps2
|
55
|
+
ENV['PS2']
|
62
56
|
end
|
63
57
|
|
64
58
|
# Define a step. At each step:
|
@@ -99,11 +93,11 @@ module ShellTest
|
|
99
93
|
|
100
94
|
def split(str)
|
101
95
|
scanner = StringScanner.new(str)
|
102
|
-
scanner.scan_until(/(#{
|
96
|
+
scanner.scan_until(/(#{Regexp.escape(ps1)})/)
|
103
97
|
scanner.pos -= scanner[1].to_s.length
|
104
98
|
|
105
99
|
args = []
|
106
|
-
while output = scanner.scan_until(/(#{
|
100
|
+
while output = scanner.scan_until(/(#{Regexp.escape(ps1)}|#{Regexp.escape(ps2)}|\{\{(.*?)\}\})/)
|
107
101
|
match = scanner[1]
|
108
102
|
input = scanner[2] ? "#{scanner[2]}\n" : scanner.scan_until(/\n/)
|
109
103
|
|
@@ -115,15 +109,15 @@ module ShellTest
|
|
115
109
|
|
116
110
|
case match
|
117
111
|
when ps1
|
118
|
-
prompt =
|
112
|
+
prompt = match
|
119
113
|
if max_run_time == -1
|
120
114
|
max_run_time = nil
|
121
115
|
end
|
122
116
|
when ps2
|
123
|
-
prompt =
|
117
|
+
prompt = match
|
124
118
|
else
|
125
119
|
output = output.chomp(match)
|
126
|
-
prompt =
|
120
|
+
prompt = output.split("\n").last
|
127
121
|
end
|
128
122
|
|
129
123
|
args << output
|
@@ -159,7 +153,7 @@ module ShellTest
|
|
159
153
|
args.pop
|
160
154
|
else
|
161
155
|
args.last << ps1
|
162
|
-
args.concat [
|
156
|
+
args.concat [ps1, "exit\n", nil, nil]
|
163
157
|
end
|
164
158
|
|
165
159
|
while !args.empty?
|
@@ -178,53 +172,42 @@ module ShellTest
|
|
178
172
|
# Spawns a PTY shell session and yields an Agent to the block. The
|
179
173
|
# session is logged to log and the final exit status set into status
|
180
174
|
# (any previous values are overwritten).
|
181
|
-
#
|
182
|
-
# ==== ENV variables
|
183
|
-
#
|
184
|
-
# PS1 and PS2 are set into ENV for the duration of the block and so in
|
185
|
-
# most cases the shell inherits those values. Keep in mind, however,
|
186
|
-
# that the shell config scripts can set these variables and on some
|
187
|
-
# distributions (ex SLES 10) the config script do not respect prior
|
188
|
-
# values.
|
189
|
-
#
|
190
175
|
def spawn
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
raise "stty failure\n#{summary}"
|
212
|
-
end
|
213
|
-
|
214
|
-
log.clear
|
176
|
+
@log = []
|
177
|
+
@status = super(shell) do |master, slave|
|
178
|
+
agent = Agent.new(master, slave, timer)
|
179
|
+
timer.start(max_run_time)
|
180
|
+
|
181
|
+
if stty
|
182
|
+
# It would be lovely to work this into steps somehow, or to set
|
183
|
+
# the stty externally like:
|
184
|
+
#
|
185
|
+
# system("stty #{stty} < '#{master.path}'")
|
186
|
+
#
|
187
|
+
# Unfortunately the former complicates result and the latter
|
188
|
+
# doesn't work. In tests the stty settings DO get set but they
|
189
|
+
# don't refresh in the pty.
|
190
|
+
log << agent.on(ps1, "stty #{stty}\n")
|
191
|
+
log << agent.on(ps1, "echo $?\n")
|
192
|
+
log << agent.on(ps1, "\n")
|
193
|
+
|
194
|
+
unless log.last == "0\n#{ps1}"
|
195
|
+
raise "stty failure\n#{summary}"
|
215
196
|
end
|
216
197
|
|
217
|
-
|
218
|
-
|
219
|
-
rescue Agent::ReadError
|
220
|
-
log << $!.buffer
|
221
|
-
$!.message << "\n#{summary}"
|
222
|
-
raise
|
223
|
-
end
|
198
|
+
log.clear
|
199
|
+
end
|
224
200
|
|
225
|
-
|
226
|
-
agent
|
201
|
+
begin
|
202
|
+
yield agent
|
203
|
+
rescue Agent::ReadError
|
204
|
+
log << $!.buffer
|
205
|
+
$!.message << "\n#{summary}"
|
206
|
+
raise
|
227
207
|
end
|
208
|
+
|
209
|
+
timer.stop
|
210
|
+
agent.close
|
228
211
|
end
|
229
212
|
self
|
230
213
|
end
|
@@ -14,40 +14,74 @@ module ShellTest
|
|
14
14
|
# process and raise a PTY::ChildExited error in some cases. As a result
|
15
15
|
# manual calls to Process.wait (which would set $?) cause a race
|
16
16
|
# condition. Rely on the output of spawn instead.
|
17
|
-
def spawn(cmd)
|
17
|
+
def spawn(cmd, log=[])
|
18
|
+
# The race condition described above actually applies to both kill and
|
19
|
+
# wait which raise Errno::ESRCH or Errno::ECHILD if they lose the
|
20
|
+
# race. This code is designed to capture those errors if they occur
|
21
|
+
# and then give the cleanup thread a chance to take over; eventually
|
22
|
+
# it will raise a ChildExited error. This is a sketchy use of
|
23
|
+
# exceptions for flow control but there is little option - a
|
24
|
+
# consequence of PTY using threads with side effects.
|
18
25
|
PTY.spawn(cmd) do |slave, master, pid|
|
19
26
|
begin
|
20
27
|
yield(master, slave)
|
21
|
-
|
28
|
+
|
29
|
+
begin
|
30
|
+
Process.wait(pid)
|
31
|
+
rescue Errno::ECHILD
|
32
|
+
Thread.pass
|
33
|
+
raise
|
34
|
+
end
|
22
35
|
|
23
36
|
rescue PTY::ChildExited
|
24
|
-
#
|
25
|
-
# route. 1.9.2 does not exit this way.
|
37
|
+
# This is the 'normal' exit route on 1.8.6 and 1.8.7.
|
26
38
|
return $!.status
|
27
39
|
|
28
|
-
rescue Exception
|
29
|
-
|
30
|
-
Process.kill(9, pid)
|
40
|
+
rescue Exception => error
|
41
|
+
begin
|
31
42
|
|
32
|
-
|
33
|
-
|
43
|
+
# Manually cleanup the pid on error. This code no longer cares
|
44
|
+
# what exactly happens to $? - the point is to make sure the
|
45
|
+
# child doesn't become a zombie and then re-raise the error.
|
34
46
|
begin
|
35
|
-
|
36
|
-
rescue Errno::
|
37
|
-
|
47
|
+
Process.kill(9, pid)
|
48
|
+
rescue Errno::ESRCH
|
49
|
+
Thread.pass
|
50
|
+
raise
|
38
51
|
end
|
39
|
-
end
|
40
52
|
|
41
|
-
|
42
|
-
|
43
|
-
|
53
|
+
# Clearing the slave allows quicker exits on OS X.
|
54
|
+
while IO.select([slave],nil,nil,0.1)
|
55
|
+
begin
|
56
|
+
break unless slave.read(1)
|
57
|
+
rescue Errno::EIO
|
58
|
+
# On some linux (ex ubuntu) read can return an eof or fail with
|
59
|
+
# an EIO error when a terminal disconnect occurs and an EIO
|
60
|
+
# condition occurs - the exact behavior is unspecified but the
|
61
|
+
# meaning is the same... no more data is available, so break.
|
62
|
+
break
|
63
|
+
end
|
64
|
+
end
|
44
65
|
|
45
|
-
|
66
|
+
begin
|
67
|
+
Process.wait(pid)
|
68
|
+
rescue Errno::ECHILD
|
69
|
+
Thread.pass
|
70
|
+
raise
|
71
|
+
end
|
72
|
+
|
73
|
+
rescue PTY::ChildExited
|
74
|
+
# The cleanup thread could finish at any point in the rescue
|
75
|
+
# handling so account for that here.
|
76
|
+
ensure
|
77
|
+
raise error
|
78
|
+
end
|
46
79
|
end
|
47
80
|
end
|
48
81
|
|
49
82
|
$?
|
50
83
|
end
|
84
|
+
|
51
85
|
end
|
52
86
|
end
|
53
87
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'shell_test/env_methods'
|
1
2
|
require 'shell_test/regexp_escape'
|
2
3
|
require 'shell_test/string_methods'
|
3
4
|
require 'shell_test/shell_methods/session'
|
@@ -7,6 +8,18 @@ module ShellTest
|
|
7
8
|
include StringMethods
|
8
9
|
include EnvMethods
|
9
10
|
|
11
|
+
attr_reader :original_env
|
12
|
+
|
13
|
+
def setup
|
14
|
+
super
|
15
|
+
@original_env = set_env('PS1' => '$ ', 'PS2' => '> ')
|
16
|
+
end
|
17
|
+
|
18
|
+
def teardown
|
19
|
+
set_env(@original_env)
|
20
|
+
super
|
21
|
+
end
|
22
|
+
|
10
23
|
def pty(script, options={}, &block)
|
11
24
|
_pty outdent(script), options, &block
|
12
25
|
end
|
data/lib/shell_test/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shell_test
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 4
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.4.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Simon Chiang
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-11-01 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -48,7 +48,7 @@ files:
|
|
48
48
|
- README.rdoc
|
49
49
|
- MIT-LICENSE
|
50
50
|
has_rdoc: true
|
51
|
-
homepage:
|
51
|
+
homepage: http://github.com/thinkerbot/shell_test
|
52
52
|
licenses:
|
53
53
|
- MIT
|
54
54
|
post_install_message:
|