shell_test 0.3.0 → 0.4.0
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/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:
|