shell_test 0.2.0 → 0.3.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 +35 -44
- data/lib/shell_test/env_methods.rb +44 -0
- data/lib/shell_test/file_methods.rb +8 -7
- data/lib/shell_test/regexp_escape.rb +21 -13
- data/lib/shell_test/shell_methods.rb +46 -80
- data/lib/shell_test/shell_methods/agent.rb +135 -0
- data/lib/shell_test/shell_methods/session.rb +293 -0
- data/lib/shell_test/shell_methods/timer.rb +71 -0
- data/lib/shell_test/shell_methods/utils.rb +53 -0
- data/lib/shell_test/string_methods.rb +47 -14
- data/lib/shell_test/version.rb +1 -1
- metadata +11 -7
- data/lib/shell_test/command_parser.rb +0 -67
data/History.rdoc
CHANGED
@@ -1,3 +1,10 @@
|
|
1
|
+
== 0.3.0 2011/10/23
|
2
|
+
|
3
|
+
Rewrite and expansion of library to use PTY shell sessions rather than
|
4
|
+
individual system commands to test scripts. This marks a significant break in
|
5
|
+
functionality. Per-command exit status tests are no longer possible, but
|
6
|
+
testing scripts with state is now possible.
|
7
|
+
|
1
8
|
== 0.2.0 2011/08/03
|
2
9
|
|
3
10
|
* Split out a StringMethods module [issue #7]
|
data/README.rdoc
CHANGED
@@ -28,43 +28,33 @@ may be used independently, but by including ShellTest you get them all:
|
|
28
28
|
assert_script %{
|
29
29
|
$ sh '#{script}' moon
|
30
30
|
goodnight moon
|
31
|
-
}
|
31
|
+
}, :exitstatus => 0
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
35
|
==== {ShellMethods}[link:classes/ShellTest/ShellMethods.html]
|
36
36
|
|
37
37
|
Provides the shell testing methods. These methods are designed to input a
|
38
|
-
string that looks like terminal input/output.
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
Special comments following the first line of a command can turn off
|
43
|
-
output/status checking, or specify a different exit status to expect.
|
38
|
+
string that looks like terminal input/output. Indeed with PS1 and PS2 set to
|
39
|
+
their POSIX defaults, assert_script tests can be created by directly copying
|
40
|
+
from the terminal.
|
44
41
|
|
45
42
|
require 'shell_test/unit'
|
46
|
-
|
47
43
|
class ShellMethodsExample < Test::Unit::TestCase
|
48
44
|
include ShellTest::ShellMethods
|
49
|
-
|
50
|
-
def
|
51
|
-
with_env("
|
45
|
+
|
46
|
+
def test_a_script_with_env_variables
|
47
|
+
with_env("THIS" => "moon") do
|
52
48
|
assert_script %{
|
53
|
-
$
|
49
|
+
$ THAT="boat"
|
50
|
+
$ echo "goodnight $THIS"
|
54
51
|
goodnight moon
|
52
|
+
$ echo "goodnight $THAT"
|
53
|
+
goodnight boat
|
55
54
|
}
|
56
55
|
end
|
57
56
|
end
|
58
|
-
|
59
|
-
def test_multiple_commands
|
60
|
-
assert_script %{
|
61
|
-
$ echo one
|
62
|
-
one
|
63
|
-
$ echo two
|
64
|
-
two
|
65
|
-
}
|
66
|
-
end
|
67
|
-
|
57
|
+
|
68
58
|
def test_multiline_commands
|
69
59
|
assert_script %{
|
70
60
|
$ for n in one two; do
|
@@ -74,28 +64,35 @@ output/status checking, or specify a different exit status to expect.
|
|
74
64
|
two
|
75
65
|
}
|
76
66
|
end
|
77
|
-
|
78
|
-
def
|
79
|
-
assert_script %{
|
80
|
-
$ true # [0]
|
81
|
-
$ false # [1]
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def test_exit_status_and_not_ouptut
|
67
|
+
|
68
|
+
def test_script_with_overall_and_per_command_timeouts
|
86
69
|
assert_script %{
|
87
|
-
$
|
88
|
-
|
70
|
+
$ sleep 0.1 # [0.5]
|
71
|
+
$ sleep 0.1 # [0.5]
|
72
|
+
}, :max_run_time => 1
|
89
73
|
end
|
90
|
-
|
91
|
-
def
|
74
|
+
|
75
|
+
def test_scripts_where_the_output_is_variable
|
92
76
|
assert_script_match %{
|
93
77
|
$ cal
|
94
78
|
:...:
|
95
|
-
Su Mo Tu We Th Fr Sa
|
96
|
-
|
79
|
+
Su Mo Tu We Th Fr Sa:. *.:
|
80
|
+
:....:
|
97
81
|
}
|
98
82
|
end
|
83
|
+
|
84
|
+
def test_scripts_that_take_input
|
85
|
+
assert_script %{
|
86
|
+
$ sudo echo 'sorry i cant do that dave'
|
87
|
+
Password:{{notIt}}
|
88
|
+
Sorry, try again.
|
89
|
+
Password:{{mayBeThis}}
|
90
|
+
Sorry, try again.
|
91
|
+
Password:{{cr@pWhatIsIt}}
|
92
|
+
Sorry, try again.
|
93
|
+
sudo: 3 incorrect password attempts
|
94
|
+
}, :max_run_time => 10
|
95
|
+
end
|
99
96
|
end
|
100
97
|
|
101
98
|
==== {FileMethods}[link:classes/ShellTest/FileMethods.html]
|
@@ -130,17 +127,11 @@ ShellTest is available as a gem[http://rubygems.org/gems/shell_test].
|
|
130
127
|
|
131
128
|
To get started, checkout the code from GitHub[http://github.com/thinkerbot/shell_test] and run:
|
132
129
|
|
133
|
-
git clone git://github.com/thinkerbot/shell_test.git
|
134
|
-
cd shell_test
|
135
130
|
rake test
|
136
131
|
|
137
|
-
To test against multiple platforms I suggest using rvm. In that case:
|
138
|
-
|
139
|
-
rvm rake test
|
140
|
-
|
141
132
|
Please report any issues {here}[http://github.com/thinkerbot/shell_test/issues].
|
142
133
|
|
143
134
|
== Info
|
144
135
|
|
145
|
-
Developer:: {Simon Chiang}[http://
|
136
|
+
Developer:: {Simon Chiang}[http://github.com/thinkerbot]
|
146
137
|
License:: {MIT-Style}[link:files/MIT-LICENSE.html]
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module ShellTest
|
2
|
+
module EnvMethods
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# Sets the specified ENV variables and returns the *current* env.
|
6
|
+
# If replace is true, current ENV variables are replaced; otherwise
|
7
|
+
# the new env variables are simply added to the existing set.
|
8
|
+
def set_env(env={}, replace=false)
|
9
|
+
current_env = {}
|
10
|
+
ENV.each_pair do |key, value|
|
11
|
+
current_env[key] = value
|
12
|
+
end
|
13
|
+
|
14
|
+
ENV.clear if replace
|
15
|
+
|
16
|
+
env.each_pair do |key, value|
|
17
|
+
if value.nil?
|
18
|
+
ENV.delete(key)
|
19
|
+
else
|
20
|
+
ENV[key] = value
|
21
|
+
end
|
22
|
+
end if env
|
23
|
+
|
24
|
+
current_env
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the specified ENV variables for the duration of the block.
|
28
|
+
# If replace is true, current ENV variables are replaced; otherwise
|
29
|
+
# the new env variables are simply added to the existing set.
|
30
|
+
#
|
31
|
+
# Returns the block return.
|
32
|
+
def with_env(env={}, replace=false)
|
33
|
+
current_env = nil
|
34
|
+
begin
|
35
|
+
current_env = set_env(env, replace)
|
36
|
+
yield
|
37
|
+
ensure
|
38
|
+
if current_env
|
39
|
+
set_env(current_env, true)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -128,10 +128,14 @@ module ShellTest
|
|
128
128
|
include StringMethods
|
129
129
|
extend ModuleMethods
|
130
130
|
|
131
|
+
# Returns the absolute path to the current working directory.
|
132
|
+
attr_reader :user_dir
|
133
|
+
|
131
134
|
# Calls cleanup to remove any files left over from previous test runs (for
|
132
135
|
# instance by running with a flag to keep outputs).
|
133
136
|
def setup
|
134
137
|
super
|
138
|
+
@user_dir = Dir.pwd
|
135
139
|
cleanup
|
136
140
|
end
|
137
141
|
|
@@ -161,11 +165,6 @@ module ShellTest
|
|
161
165
|
ENV["KEEP_OUTPUTS"] == "true"
|
162
166
|
end
|
163
167
|
|
164
|
-
# Returns the absolute path to the current working directory.
|
165
|
-
def user_dir
|
166
|
-
@user_dir ||= File.expand_path('.')
|
167
|
-
end
|
168
|
-
|
169
168
|
# Returns the absolute path to a directory specific to the current test
|
170
169
|
# class, specifically the class.class_dir expanded relative to the
|
171
170
|
# user_dir.
|
@@ -211,7 +210,9 @@ module ShellTest
|
|
211
210
|
# Creates a directory under method_dir.
|
212
211
|
def prepare_dir(relative_path)
|
213
212
|
target_dir = path(relative_path)
|
214
|
-
|
213
|
+
unless File.directory?(target_dir)
|
214
|
+
FileUtils.mkdir_p(target_dir)
|
215
|
+
end
|
215
216
|
target_dir
|
216
217
|
end
|
217
218
|
|
@@ -223,7 +224,7 @@ module ShellTest
|
|
223
224
|
FileUtils.rm(target)
|
224
225
|
else
|
225
226
|
target_dir = File.dirname(target)
|
226
|
-
FileUtils.mkdir_p(target_dir)
|
227
|
+
FileUtils.mkdir_p(target_dir) unless File.exists?(target_dir)
|
227
228
|
end
|
228
229
|
|
229
230
|
FileUtils.touch(target)
|
@@ -1,23 +1,25 @@
|
|
1
1
|
module ShellTest
|
2
2
|
# RegexpEscape is a subclass of regexp that escapes all but the text in a
|
3
|
-
# special escape sequence. This allows the creation of complex regexps
|
4
|
-
#
|
3
|
+
# special escape sequence. This allows the creation of complex regexps to
|
4
|
+
# match, for instance, console output.
|
5
5
|
#
|
6
|
-
# The RegexpEscape.escape (or equivalently the quote) method does the
|
7
|
-
#
|
8
|
-
#
|
6
|
+
# The RegexpEscape.escape (or equivalently the quote) method does the work;
|
7
|
+
# all regexp-active characters are escaped except for characters enclosed by
|
8
|
+
# ':.' and '.:' delimiters.
|
9
9
|
#
|
10
10
|
# RegexpEscape.escape('reg[exp]+ chars. are(quoted)') # => 'reg\[exp\]\+\ chars\.\ are\(quoted\)'
|
11
11
|
# RegexpEscape.escape('these are not: :.a(b*)c.:') # => 'these\ are\ not:\ a(b*)c'
|
12
12
|
#
|
13
|
-
#
|
14
|
-
#
|
13
|
+
# All-period regexps are treated specially. A single period is translated
|
14
|
+
# to '.*?' to lazily match anything on a single line. Multiple periods are
|
15
|
+
# translated to '(?:(?m).*?)' to lazily match anything actoss multiple
|
16
|
+
# lines. Use the '.{n}' notation to specify n arbitrary characters.
|
15
17
|
#
|
16
|
-
# RegexpEscape.escape('
|
17
|
-
# RegexpEscape.escape(':..{1}.:')
|
18
|
+
# RegexpEscape.escape('a:...:b:....:c') # => 'a.*?b(?:(?m).*?)c'
|
19
|
+
# RegexpEscape.escape('a:..{1}.:b') # => 'a.{1}b'
|
18
20
|
#
|
19
|
-
# RegexpEscape instances are initialized using the escaped input string
|
20
|
-
#
|
21
|
+
# RegexpEscape instances are initialized using the escaped input string and
|
22
|
+
# return the original string upon to_s.
|
21
23
|
#
|
22
24
|
# str = %q{
|
23
25
|
# a multiline
|
@@ -50,8 +52,14 @@ module ShellTest
|
|
50
52
|
substituents = []
|
51
53
|
str.scan(ESCAPE_SEQUENCE) do
|
52
54
|
regexp_str = $&[2...-2]
|
53
|
-
|
54
|
-
|
55
|
+
substituents << case regexp_str
|
56
|
+
when '.'
|
57
|
+
".*?"
|
58
|
+
when /\A\.+\z/
|
59
|
+
"(?:(?m).*?)"
|
60
|
+
else
|
61
|
+
regexp_str
|
62
|
+
end
|
55
63
|
end
|
56
64
|
substituents << ""
|
57
65
|
|
@@ -1,84 +1,20 @@
|
|
1
1
|
require 'shell_test/regexp_escape'
|
2
|
-
require 'shell_test/command_parser'
|
3
2
|
require 'shell_test/string_methods'
|
3
|
+
require 'shell_test/shell_methods/session'
|
4
4
|
|
5
5
|
module ShellTest
|
6
6
|
module ShellMethods
|
7
7
|
include StringMethods
|
8
|
+
include EnvMethods
|
8
9
|
|
9
|
-
def
|
10
|
-
|
11
|
-
@notify_method_name = true
|
10
|
+
def pty(script, options={}, &block)
|
11
|
+
_pty outdent(script), options, &block
|
12
12
|
end
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# Returns true if the ENV variable 'VERBOSE' is true. When verbose,
|
20
|
-
# ShellTest prints the expanded commands of sh_test to $stdout.
|
21
|
-
def verbose?
|
22
|
-
verbose = ENV['VERBOSE']
|
23
|
-
verbose && verbose =~ /^true$/i ? true : false
|
24
|
-
end
|
25
|
-
|
26
|
-
# Sets the specified ENV variables and returns the *current* env.
|
27
|
-
# If replace is true, current ENV variables are replaced; otherwise
|
28
|
-
# the new env variables are simply added to the existing set.
|
29
|
-
def set_env(env={}, replace=false)
|
30
|
-
current_env = {}
|
31
|
-
ENV.each_pair do |key, value|
|
32
|
-
current_env[key] = value
|
33
|
-
end
|
34
|
-
|
35
|
-
ENV.clear if replace
|
36
|
-
|
37
|
-
env.each_pair do |key, value|
|
38
|
-
if value.nil?
|
39
|
-
ENV.delete(key)
|
40
|
-
else
|
41
|
-
ENV[key] = value
|
42
|
-
end
|
43
|
-
end if env
|
44
|
-
|
45
|
-
current_env
|
46
|
-
end
|
47
|
-
|
48
|
-
# Sets the specified ENV variables for the duration of the block.
|
49
|
-
# If replace is true, current ENV variables are replaced; otherwise
|
50
|
-
# the new env variables are simply added to the existing set.
|
51
|
-
#
|
52
|
-
# Returns the block return.
|
53
|
-
def with_env(env={}, replace=false)
|
54
|
-
current_env = nil
|
55
|
-
begin
|
56
|
-
current_env = set_env(env, replace)
|
57
|
-
yield
|
58
|
-
ensure
|
59
|
-
if current_env
|
60
|
-
set_env(current_env, true)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def sh(cmd)
|
66
|
-
if @notify_method_name && verbose?
|
67
|
-
@notify_method_name = false
|
68
|
-
puts
|
69
|
-
puts method_name
|
70
|
-
end
|
71
|
-
|
72
|
-
start = Time.now
|
73
|
-
result = `#{cmd}`
|
74
|
-
finish = Time.now
|
75
|
-
|
76
|
-
if verbose?
|
77
|
-
elapsed = "%.3f" % [finish-start]
|
78
|
-
puts " (#{elapsed}s) #{cmd}"
|
79
|
-
end
|
80
|
-
|
81
|
-
result
|
14
|
+
def _pty(script, options={}, &block)
|
15
|
+
session = Session.new(options)
|
16
|
+
session.parse(script, options, &block)
|
17
|
+
session.run
|
82
18
|
end
|
83
19
|
|
84
20
|
def assert_script(script, options={})
|
@@ -86,11 +22,26 @@ module ShellTest
|
|
86
22
|
end
|
87
23
|
|
88
24
|
def _assert_script(script, options={})
|
89
|
-
|
90
|
-
|
25
|
+
pty = _pty(script, options) do |session, expected, actual|
|
26
|
+
expected = expand_ctrl_chars(expected)
|
27
|
+
actual = expand_ctrl_chars(actual)
|
28
|
+
|
29
|
+
_assert_str_equal expected, actual do
|
30
|
+
session.summary %Q{
|
31
|
+
%s (elapsed: %.2fs max: %.2fs)
|
32
|
+
=========================================================
|
33
|
+
%s
|
34
|
+
-------------------- expected output --------------------
|
35
|
+
#{whitespace_escape(expected)}
|
36
|
+
------------------------ but was ------------------------
|
37
|
+
#{whitespace_escape(actual)}
|
38
|
+
=========================================================
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
91
42
|
|
92
|
-
|
93
|
-
assert_equal(status,
|
43
|
+
if status = options[:exitstatus]
|
44
|
+
assert_equal(status, pty.status.exitstatus)
|
94
45
|
end
|
95
46
|
end
|
96
47
|
|
@@ -99,11 +50,26 @@ module ShellTest
|
|
99
50
|
end
|
100
51
|
|
101
52
|
def _assert_script_match(script, options={})
|
102
|
-
|
103
|
-
|
53
|
+
pty = _pty(script, options) do |session, expected, actual|
|
54
|
+
expected = expand_ctrl_chars(expected)
|
55
|
+
actual = expand_ctrl_chars(actual)
|
56
|
+
|
57
|
+
_assert_str_match expected, actual do
|
58
|
+
session.summary %Q{
|
59
|
+
%s (%.2f:%.2fs)
|
60
|
+
=========================================================
|
61
|
+
%s
|
62
|
+
----------------- expected output like ------------------
|
63
|
+
#{whitespace_escape(expected)}
|
64
|
+
------------------------ but was ------------------------
|
65
|
+
#{whitespace_escape(actual)}
|
66
|
+
=========================================================
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
104
70
|
|
105
|
-
|
106
|
-
assert_equal(status,
|
71
|
+
if status = options[:exitstatus]
|
72
|
+
assert_equal(status, pty.status.exitstatus)
|
107
73
|
end
|
108
74
|
end
|
109
75
|
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require 'shell_test/shell_methods/timer'
|
2
|
+
|
3
|
+
module ShellTest
|
4
|
+
module ShellMethods
|
5
|
+
# Agent wraps a PTY master-slave pair and provides methods for doing reads
|
6
|
+
# and writes. The expect method is inspired by the IO.expect method in the
|
7
|
+
# stdlib, but tailored to allow for better timeout control.
|
8
|
+
class Agent
|
9
|
+
|
10
|
+
# The pty master
|
11
|
+
attr_reader :master
|
12
|
+
|
13
|
+
# The pty slave
|
14
|
+
attr_reader :slave
|
15
|
+
|
16
|
+
# The timer managing read timeouts. The timer ensures that the timeouts
|
17
|
+
# used by expect are never negative, or nil to indicate no timeout.
|
18
|
+
#
|
19
|
+
# If implementing a custom timer, note that timeouts are set on the
|
20
|
+
# timer using `timer.timeout=` and retrieved via `timer.timeout`.
|
21
|
+
attr_reader :timer
|
22
|
+
|
23
|
+
# A hash of [name, regexp] pairs mapping logical names to prompts.
|
24
|
+
attr_reader :prompts
|
25
|
+
|
26
|
+
def initialize(master, slave, timer, prompts={})
|
27
|
+
@master = master
|
28
|
+
@slave = slave
|
29
|
+
@timer = timer
|
30
|
+
@prompts = prompts
|
31
|
+
end
|
32
|
+
|
33
|
+
def on(prompt, input, timeout=nil)
|
34
|
+
output = expect(prompt, timeout)
|
35
|
+
write(input)
|
36
|
+
output
|
37
|
+
end
|
38
|
+
|
39
|
+
# Reads from the slave until the prompt (a regexp) is matched and
|
40
|
+
# returns the resulting string. If a nil prompt is given then expect
|
41
|
+
# reads until the slave eof.
|
42
|
+
#
|
43
|
+
# A timeout may be given. If the slave doesn't produce the expected
|
44
|
+
# 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 prompt
|
46
|
+
# matches.
|
47
|
+
def expect(prompt, timeout=nil)
|
48
|
+
regexp = prompts[prompt] || prompt
|
49
|
+
timer.timeout = timeout
|
50
|
+
|
51
|
+
buffer = ''
|
52
|
+
while true
|
53
|
+
timeout = timer.timeout
|
54
|
+
|
55
|
+
# Use read+select instead of read_nonblock to avoid polling in a
|
56
|
+
# tight loop. Don't bother with readpartial and partial lengths. It
|
57
|
+
# would be an optimization, especially because the regexp matches
|
58
|
+
# each loop, but adds complexity because expect could read past the
|
59
|
+
# end of the regexp and it is unlikely to be necessary in test
|
60
|
+
# scenarios (ie this is not meant to be a general solution).
|
61
|
+
unless IO.select([slave],nil,nil,timeout)
|
62
|
+
msg = "timeout waiting for %s after %.2fs" % [regexp ? regexp.inspect : 'EOF', timeout]
|
63
|
+
raise ReadError.new(msg, buffer)
|
64
|
+
end
|
65
|
+
|
66
|
+
begin
|
67
|
+
c = slave.read(1)
|
68
|
+
rescue Errno::EIO
|
69
|
+
# On some linux (ex ubuntu) read can return an eof or fail with
|
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.
|
73
|
+
c = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
if c.nil?
|
77
|
+
if regexp.nil?
|
78
|
+
break
|
79
|
+
else
|
80
|
+
raise ReadError.new("end of file reached", buffer)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
buffer << c
|
85
|
+
|
86
|
+
if regexp && buffer =~ regexp
|
87
|
+
break
|
88
|
+
end
|
89
|
+
end
|
90
|
+
buffer
|
91
|
+
end
|
92
|
+
|
93
|
+
# Read to the end of the slave. Raises a ReadError if the slave eof is
|
94
|
+
# not reached within the timeout.
|
95
|
+
def read(timeout=nil)
|
96
|
+
expect nil, timeout
|
97
|
+
end
|
98
|
+
|
99
|
+
# Writes to the master. A timeout may be given. If the master doesn't
|
100
|
+
# become available for writing within the timeout then write raises an
|
101
|
+
# WriteError.
|
102
|
+
def write(input, timeout=nil)
|
103
|
+
unless IO.select(nil,[master],nil,timeout)
|
104
|
+
raise WriteError.new("timeout waiting for master")
|
105
|
+
end
|
106
|
+
master.print input
|
107
|
+
end
|
108
|
+
|
109
|
+
# Closes the master and slave.
|
110
|
+
def close
|
111
|
+
unless master.closed?
|
112
|
+
master.close
|
113
|
+
end
|
114
|
+
unless slave.closed?
|
115
|
+
slave.close
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Raised when Agent writes timeout.
|
120
|
+
class WriteError < RuntimeError
|
121
|
+
end
|
122
|
+
|
123
|
+
# Raised when Agent reads fail or timeout.
|
124
|
+
class ReadError < RuntimeError
|
125
|
+
# The buffer containing whatever had been read at the time of error.
|
126
|
+
attr_reader :buffer
|
127
|
+
|
128
|
+
def initialize(message, buffer)
|
129
|
+
@buffer = buffer
|
130
|
+
super message
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
require 'shell_test/env_methods'
|
2
|
+
require 'shell_test/shell_methods/agent'
|
3
|
+
require 'shell_test/shell_methods/utils'
|
4
|
+
require 'strscan'
|
5
|
+
|
6
|
+
module ShellTest
|
7
|
+
module ShellMethods
|
8
|
+
|
9
|
+
# Session is an engine for running shell sessions.
|
10
|
+
class Session
|
11
|
+
include EnvMethods
|
12
|
+
include Utils
|
13
|
+
|
14
|
+
DEFAULT_SHELL = '/bin/sh'
|
15
|
+
DEFAULT_PS1 = '$ '
|
16
|
+
DEFAULT_PS2 = '> '
|
17
|
+
DEFAULT_STTY = '-echo -onlcr'
|
18
|
+
DEFAULT_MAX_RUN_TIME = 1
|
19
|
+
|
20
|
+
# The session shell
|
21
|
+
attr_reader :shell
|
22
|
+
|
23
|
+
# The shell PS1
|
24
|
+
attr_reader :ps1
|
25
|
+
|
26
|
+
# The shell PS2
|
27
|
+
attr_reader :ps2
|
28
|
+
|
29
|
+
# Aguments string passed stty on run
|
30
|
+
attr_reader :stty
|
31
|
+
|
32
|
+
# The session timer, used by agents to determine timeouts
|
33
|
+
attr_reader :timer
|
34
|
+
|
35
|
+
# The maximum run time for the session
|
36
|
+
attr_reader :max_run_time
|
37
|
+
|
38
|
+
# An array of entries like [prompt, input, max_run_time, callback] that
|
39
|
+
# indicate each step of a session. See the on method for adding steps.
|
40
|
+
attr_reader :steps
|
41
|
+
|
42
|
+
# A log of the output at each step (set during run)
|
43
|
+
attr_reader :log
|
44
|
+
|
45
|
+
# A Process::Status for the session (set by run)
|
46
|
+
attr_reader :status
|
47
|
+
|
48
|
+
def initialize(options={})
|
49
|
+
@shell = options[:shell] || DEFAULT_SHELL
|
50
|
+
@ps1 = options[:ps1] || DEFAULT_PS1
|
51
|
+
@ps2 = options[:ps2] || DEFAULT_PS2
|
52
|
+
@stty = options[:stty] || DEFAULT_STTY
|
53
|
+
@timer = options[:timer] || Timer.new
|
54
|
+
@max_run_time = options[:max_run_time] || DEFAULT_MAX_RUN_TIME
|
55
|
+
@steps = [[nil, nil, nil, nil]]
|
56
|
+
@log = []
|
57
|
+
@status = nil
|
58
|
+
@prompts = {
|
59
|
+
:ps1 => /#{Regexp.escape(ps1)}/,
|
60
|
+
:ps2 => /#{Regexp.escape(ps2)}/
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
# Define a step. At each step:
|
65
|
+
#
|
66
|
+
# 1. The session waits until the prompt is matched
|
67
|
+
# 2. The input is written to the shell (if given)
|
68
|
+
# 3. The output passed to the callback (if given)
|
69
|
+
#
|
70
|
+
# If the next prompt (or an EOF if there is no next prompt) is not
|
71
|
+
# reached within max_run_time then a ReadError occurs. Special
|
72
|
+
# considerations:
|
73
|
+
#
|
74
|
+
# * The prompt should be a regular expression, :ps1, or :ps2.
|
75
|
+
# * A nil max_run_time indicates no maximum run time - which more
|
76
|
+
# accurately means the input can go until the overall max_run_time for
|
77
|
+
# the session runs out.
|
78
|
+
# * The output passed to the callback will include the string matched by
|
79
|
+
# the next prompt, if present.
|
80
|
+
#
|
81
|
+
# Returns self.
|
82
|
+
def on(prompt, input=nil, max_run_time=nil, &callback) # :yields: output
|
83
|
+
if prompt.nil?
|
84
|
+
raise ArgumentError, "no prompt specified"
|
85
|
+
end
|
86
|
+
|
87
|
+
# Stagger assignment of step arguments so that the callback will be
|
88
|
+
# recieve the output of the input. Not only is this more intuitive, it
|
89
|
+
# ensures the last step will read to EOF (which expedites waiting on
|
90
|
+
# the session to terminate).
|
91
|
+
last = steps.last
|
92
|
+
last[0] = prompt
|
93
|
+
last[1] = input
|
94
|
+
last[2] = max_run_time
|
95
|
+
|
96
|
+
steps << [nil, nil, nil, callback]
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def split(str)
|
101
|
+
scanner = StringScanner.new(str)
|
102
|
+
scanner.scan_until(/(#{@prompts[:ps1]})/)
|
103
|
+
scanner.pos -= scanner[1].to_s.length
|
104
|
+
|
105
|
+
args = []
|
106
|
+
while output = scanner.scan_until(/(#{@prompts[:ps1]}|#{@prompts[:ps2]}|\{\{(.*?)\}\})/)
|
107
|
+
match = scanner[1]
|
108
|
+
input = scanner[2] ? "#{scanner[2]}\n" : scanner.scan_until(/\n/)
|
109
|
+
|
110
|
+
max_run_time = -1
|
111
|
+
input.sub!(/\#\s*\[(\d+(?:\.\d+)?)\].*$/) do
|
112
|
+
max_run_time = $1.to_f
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
case match
|
117
|
+
when ps1
|
118
|
+
prompt = :ps1
|
119
|
+
if max_run_time == -1
|
120
|
+
max_run_time = nil
|
121
|
+
end
|
122
|
+
when ps2
|
123
|
+
prompt = :ps2
|
124
|
+
else
|
125
|
+
output = output.chomp(match)
|
126
|
+
prompt = /^#{output.split("\n").last}\z/
|
127
|
+
end
|
128
|
+
|
129
|
+
args << output
|
130
|
+
args << prompt
|
131
|
+
args << input
|
132
|
+
args << max_run_time
|
133
|
+
end
|
134
|
+
|
135
|
+
args << scanner.rest
|
136
|
+
args
|
137
|
+
end
|
138
|
+
|
139
|
+
# Parses a terminal snippet into steps that a Session can run, and adds
|
140
|
+
# those steps to self. The snippet should utilize ps1 and ps2 as set on
|
141
|
+
# self. An exit command is added unless the :noexit option is set to
|
142
|
+
# true.
|
143
|
+
#
|
144
|
+
# session = Session.new
|
145
|
+
# session.parse %{
|
146
|
+
# $ echo abc
|
147
|
+
# abc
|
148
|
+
# }
|
149
|
+
# session.run.result # => "$ echo abc\nabc\n$ exit\nexit\n"
|
150
|
+
#
|
151
|
+
# Steps are registered with a callback block, if given, to recieve the
|
152
|
+
# expected and actual outputs during run. Normally the callback is used
|
153
|
+
# to validate that the run is going as planned.
|
154
|
+
def parse(script, options={}, &block)
|
155
|
+
args = split(script)
|
156
|
+
args.shift # ignore script before first prompt
|
157
|
+
|
158
|
+
if options[:noexit]
|
159
|
+
args.pop
|
160
|
+
else
|
161
|
+
args.last << ps1
|
162
|
+
args.concat [:ps1, "exit\n", nil, nil]
|
163
|
+
end
|
164
|
+
|
165
|
+
while !args.empty?
|
166
|
+
prompt = args.shift
|
167
|
+
input = args.shift
|
168
|
+
max_run_time = args.shift
|
169
|
+
output = args.shift
|
170
|
+
callback = make_callback(output, &block)
|
171
|
+
|
172
|
+
on(prompt, input, max_run_time, &callback)
|
173
|
+
end
|
174
|
+
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
# Spawns a PTY shell session and yields an Agent to the block. The
|
179
|
+
# session is logged to log and the final exit status set into status
|
180
|
+
# (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
|
+
def spawn
|
191
|
+
with_env('PS1' => ps1, 'PS2' => ps2) do
|
192
|
+
@log = []
|
193
|
+
@status = super(shell) do |master, slave|
|
194
|
+
agent = Agent.new(master, slave, timer, @prompts)
|
195
|
+
timer.start(max_run_time)
|
196
|
+
|
197
|
+
if stty
|
198
|
+
# It would be lovely to work this into steps somehow, or to set
|
199
|
+
# the stty externally like:
|
200
|
+
#
|
201
|
+
# system("stty #{stty} < '#{master.path}'")
|
202
|
+
#
|
203
|
+
# Unfortunately the former complicates result and the latter
|
204
|
+
# doesn't work. In tests the stty settings DO get set but they
|
205
|
+
# don't refresh in the pty.
|
206
|
+
log << agent.on(:ps1, "stty #{stty}\n")
|
207
|
+
log << agent.on(:ps1, "echo $?\n")
|
208
|
+
log << agent.on(:ps1, "\n")
|
209
|
+
|
210
|
+
unless log.last == "0\n#{ps1}"
|
211
|
+
raise "stty failure\n#{summary}"
|
212
|
+
end
|
213
|
+
|
214
|
+
log.clear
|
215
|
+
end
|
216
|
+
|
217
|
+
begin
|
218
|
+
yield agent
|
219
|
+
rescue Agent::ReadError
|
220
|
+
log << $!.buffer
|
221
|
+
$!.message << "\n#{summary}"
|
222
|
+
raise
|
223
|
+
end
|
224
|
+
|
225
|
+
timer.stop
|
226
|
+
agent.close
|
227
|
+
end
|
228
|
+
end
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
# Runs each of steps within a shell session and collects the
|
233
|
+
# inputs/outputs into log. After run the exit status of the session is
|
234
|
+
# set into status.
|
235
|
+
def run
|
236
|
+
spawn do |agent|
|
237
|
+
timeout = nil
|
238
|
+
steps.each do |prompt, input, max_run_time, callback|
|
239
|
+
buffer = agent.expect(prompt, timeout)
|
240
|
+
log << buffer
|
241
|
+
|
242
|
+
if callback
|
243
|
+
callback.call buffer
|
244
|
+
end
|
245
|
+
|
246
|
+
if input
|
247
|
+
log << input
|
248
|
+
agent.write(input)
|
249
|
+
end
|
250
|
+
|
251
|
+
timeout = max_run_time
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Returns what would appear to the user at the current point in the
|
257
|
+
# session (with granularity of an input/output step).
|
258
|
+
#
|
259
|
+
# Currently result ONLY works as intended when stty is set to turn off
|
260
|
+
# input echo and output carriage returns, either with '-echo -onlcr'
|
261
|
+
# (the default) or 'raw'. Otherwise the inputs can appear twice in the
|
262
|
+
# result and there will be inconsistent end-of-lines.
|
263
|
+
def result
|
264
|
+
log.join
|
265
|
+
end
|
266
|
+
|
267
|
+
# Formats the status of self into a string. A format string can be
|
268
|
+
# provided - it is evaluated using '%' using arguments: [shell,
|
269
|
+
# elapsed_time, result]
|
270
|
+
def summary(format=nil)
|
271
|
+
(format || %Q{
|
272
|
+
%s (elapsed: %.2fs max: %.2fs)
|
273
|
+
=========================================================
|
274
|
+
%s
|
275
|
+
=========================================================
|
276
|
+
}) % [shell, timer.elapsed_time, max_run_time, result]
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
# helper to make a callback for validating output
|
282
|
+
def make_callback(output) # :nodoc:
|
283
|
+
if output && block_given?
|
284
|
+
lambda do |actual|
|
285
|
+
yield(self, output, actual)
|
286
|
+
end
|
287
|
+
else
|
288
|
+
nil
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ShellTest
|
2
|
+
module ShellMethods
|
3
|
+
class Timer
|
4
|
+
attr_reader :clock
|
5
|
+
attr_reader :start_time
|
6
|
+
attr_reader :stop_time
|
7
|
+
attr_reader :step_time
|
8
|
+
|
9
|
+
def initialize(clock=Time)
|
10
|
+
@clock = clock
|
11
|
+
reset
|
12
|
+
end
|
13
|
+
|
14
|
+
def current_time
|
15
|
+
clock.now.to_f
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset
|
19
|
+
@start_time = nil
|
20
|
+
@stop_time = nil
|
21
|
+
@step_time = 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def start(max_run_time=60)
|
25
|
+
reset
|
26
|
+
@start_time = current_time
|
27
|
+
@stop_time = start_time + max_run_time
|
28
|
+
@step_time = stop_time
|
29
|
+
end
|
30
|
+
|
31
|
+
def running?
|
32
|
+
start_time.nil? || stop_time.nil? ? false : true
|
33
|
+
end
|
34
|
+
|
35
|
+
def elapsed_time
|
36
|
+
current_time - start_time
|
37
|
+
end
|
38
|
+
|
39
|
+
def stop
|
40
|
+
if running?
|
41
|
+
elapsed = elapsed_time
|
42
|
+
reset
|
43
|
+
elapsed
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def timeout=(timeout)
|
50
|
+
unless running?
|
51
|
+
raise "cannot set timeout unless running"
|
52
|
+
end
|
53
|
+
|
54
|
+
case
|
55
|
+
when timeout.nil?
|
56
|
+
@step_time = stop_time
|
57
|
+
when timeout < 0
|
58
|
+
step_time
|
59
|
+
else
|
60
|
+
mtime = current_time + timeout
|
61
|
+
@step_time = mtime > stop_time ? stop_time : mtime
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def timeout
|
66
|
+
timeout = step_time - current_time
|
67
|
+
timeout < 0 ? 0 : timeout
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'pty'
|
2
|
+
|
3
|
+
module ShellTest
|
4
|
+
module ShellMethods
|
5
|
+
module Utils
|
6
|
+
module_function
|
7
|
+
|
8
|
+
# Spawns a PTY session and returns the Process::Status for the session
|
9
|
+
# upon completion. The PTY process is killed upon an unhandled error
|
10
|
+
# (but the error is re-raised for further handling).
|
11
|
+
#
|
12
|
+
# Note that $? is set by spawn but is not reliable until 1.9.2 (ish).
|
13
|
+
# Prior to that PTY used a cleanup thread that would wait on a spawned
|
14
|
+
# process and raise a PTY::ChildExited error in some cases. As a result
|
15
|
+
# manual calls to Process.wait (which would set $?) cause a race
|
16
|
+
# condition. Rely on the output of spawn instead.
|
17
|
+
def spawn(cmd)
|
18
|
+
PTY.spawn(cmd) do |slave, master, pid|
|
19
|
+
begin
|
20
|
+
yield(master, slave)
|
21
|
+
Process.wait(pid)
|
22
|
+
|
23
|
+
rescue PTY::ChildExited
|
24
|
+
# handle a ChildExited error on 1.8.6 and 1.8.7 as a 'normal' exit
|
25
|
+
# route. 1.9.2 does not exit this way.
|
26
|
+
return $!.status
|
27
|
+
|
28
|
+
rescue Exception
|
29
|
+
# cleanup the pty on error
|
30
|
+
Process.kill(9, pid)
|
31
|
+
|
32
|
+
# clearing the slave allows the wait to complete faster
|
33
|
+
while IO.select([slave],nil,nil,0.1)
|
34
|
+
begin
|
35
|
+
break unless slave.read(1)
|
36
|
+
rescue Errno::EIO
|
37
|
+
break
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# any wait can cause a ChildExited error so account for that here
|
42
|
+
# - the $? is indeterminate in this case prior to 1.9.2
|
43
|
+
Process.wait(pid) rescue PTY::ChildExited
|
44
|
+
|
45
|
+
raise
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
$?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -3,19 +3,21 @@ module ShellTest
|
|
3
3
|
# Asserts whether or not the a and b strings are equal, with a more
|
4
4
|
# readable output than assert_equal for large strings (especially large
|
5
5
|
# strings with significant whitespace).
|
6
|
-
def assert_str_equal(a, b, msg=nil)
|
7
|
-
_assert_str_equal outdent(a), b, msg
|
6
|
+
def assert_str_equal(a, b, msg=nil, &block)
|
7
|
+
_assert_str_equal outdent(a), b, msg, &block
|
8
8
|
end
|
9
9
|
|
10
|
+
# Same as assert_str_equal but does not outdent.
|
10
11
|
def _assert_str_equal(a, b, msg=nil)
|
11
12
|
if a == b
|
12
13
|
assert true
|
13
14
|
else
|
14
|
-
flunk %Q{
|
15
|
+
flunk block_given? ? yield(a, b) : %Q{
|
16
|
+
=========================================================
|
15
17
|
#{msg}
|
16
|
-
|
18
|
+
-------------------- expected output --------------------
|
17
19
|
#{whitespace_escape(a)}
|
18
|
-
|
20
|
+
------------------------ but was ------------------------
|
19
21
|
#{whitespace_escape(b)}
|
20
22
|
=========================================================
|
21
23
|
}
|
@@ -26,12 +28,13 @@ module ShellTest
|
|
26
28
|
# provides a more readable output in the case of a failure as compared
|
27
29
|
# with assert_match.
|
28
30
|
#
|
29
|
-
# If a is a string it is turned into a RegexpEscape.
|
30
|
-
def assert_str_match(a, b, msg=nil)
|
31
|
+
# If a is a string then it is turned into a RegexpEscape.
|
32
|
+
def assert_str_match(a, b, msg=nil, &block)
|
31
33
|
a = outdent(a) if a.kind_of?(String)
|
32
|
-
_assert_str_match a, b, msg
|
34
|
+
_assert_str_match a, b, msg, &block
|
33
35
|
end
|
34
36
|
|
37
|
+
# Same as assert_str_match but does not outdent.
|
35
38
|
def _assert_str_match(a, b, msg=nil)
|
36
39
|
if a.kind_of?(String)
|
37
40
|
a = RegexpEscape.new(a)
|
@@ -40,25 +43,33 @@ module ShellTest
|
|
40
43
|
if b =~ a
|
41
44
|
assert true
|
42
45
|
else
|
43
|
-
flunk %Q{
|
46
|
+
flunk block_given? ? yield(a,b) : %Q{
|
47
|
+
=========================================================
|
44
48
|
#{msg}
|
45
|
-
|
49
|
+
----------------- expected output like ------------------
|
46
50
|
#{whitespace_escape(a)}
|
47
|
-
|
51
|
+
------------------------ but was ------------------------
|
48
52
|
#{whitespace_escape(b)}
|
49
53
|
=========================================================
|
50
54
|
}
|
51
55
|
end
|
52
56
|
end
|
53
57
|
|
54
|
-
#
|
58
|
+
# Indents each line of str as specified.
|
59
|
+
def indent(str, indent=' ')
|
60
|
+
str.split("\n").collect do |frag|
|
61
|
+
"#{indent}#{frag}"
|
62
|
+
end.join("\n")
|
63
|
+
end
|
64
|
+
|
65
|
+
# Strips indentation off of the input string.
|
55
66
|
def outdent(str)
|
56
67
|
str =~ /\A(?:\s*?\n)( *)(.*)\z/m ? $2.gsub!(/^ {0,#{$1.length}}/, '') : str
|
57
68
|
end
|
58
69
|
|
59
|
-
#
|
70
|
+
# Escapes non-printable characters into readable text.
|
60
71
|
def whitespace_escape(str)
|
61
|
-
str.to_s.gsub(/\s/) do |match|
|
72
|
+
str = str.to_s.gsub(/\s/) do |match|
|
62
73
|
case match
|
63
74
|
when "\n" then "\\n\n"
|
64
75
|
when "\t" then "\\t"
|
@@ -67,6 +78,28 @@ module ShellTest
|
|
67
78
|
else match
|
68
79
|
end
|
69
80
|
end
|
81
|
+
str.gsub!("\b", "\\b")
|
82
|
+
str.gsub!("\a", "\\a")
|
83
|
+
str.split("\0").join('\\0')
|
84
|
+
end
|
85
|
+
|
86
|
+
# Expands non-printable characters (ie control characters) in str to their
|
87
|
+
# common print equivalents. Specifically:
|
88
|
+
#
|
89
|
+
# ctrl char before after
|
90
|
+
# null "ab\0c" "abc"
|
91
|
+
# bell "ab\ac" "abc"
|
92
|
+
# backspace "ab\bc" "ac"
|
93
|
+
# horizonal tab "ab\tc" "ab\tc"
|
94
|
+
# line feed "ab\nc" "ab\nc"
|
95
|
+
# form feed "ab\fc" "ab\n c"
|
96
|
+
# carraige return "ab\rc" "c"
|
97
|
+
#
|
98
|
+
def expand_ctrl_chars(str)
|
99
|
+
str = str.gsub(/^.*?\r/, '')
|
100
|
+
str.gsub!(/(\A#{"\b"}|.#{"\b"}|#{"\a"}|#{"\0"})/m, '')
|
101
|
+
str.gsub!(/(^.*?)\f/) { "#{$1}\n#{' ' * $1.length}" }
|
102
|
+
str
|
70
103
|
end
|
71
104
|
end
|
72
105
|
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: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 3
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.3.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-10-23 00:00:00 -06:00
|
19
19
|
default_executable:
|
20
20
|
dependencies: []
|
21
21
|
|
@@ -32,10 +32,14 @@ extra_rdoc_files:
|
|
32
32
|
- MIT-LICENSE
|
33
33
|
files:
|
34
34
|
- lib/shell_test.rb
|
35
|
-
- lib/shell_test/
|
35
|
+
- lib/shell_test/env_methods.rb
|
36
36
|
- lib/shell_test/file_methods.rb
|
37
37
|
- lib/shell_test/regexp_escape.rb
|
38
38
|
- lib/shell_test/shell_methods.rb
|
39
|
+
- lib/shell_test/shell_methods/agent.rb
|
40
|
+
- lib/shell_test/shell_methods/session.rb
|
41
|
+
- lib/shell_test/shell_methods/timer.rb
|
42
|
+
- lib/shell_test/shell_methods/utils.rb
|
39
43
|
- lib/shell_test/string_methods.rb
|
40
44
|
- lib/shell_test/unit.rb
|
41
45
|
- lib/shell_test/unit/shim.rb
|
@@ -45,8 +49,8 @@ files:
|
|
45
49
|
- MIT-LICENSE
|
46
50
|
has_rdoc: true
|
47
51
|
homepage: ""
|
48
|
-
licenses:
|
49
|
-
|
52
|
+
licenses:
|
53
|
+
- MIT
|
50
54
|
post_install_message:
|
51
55
|
rdoc_options:
|
52
56
|
- --main
|
@@ -1,67 +0,0 @@
|
|
1
|
-
module ShellTest
|
2
|
-
class CommandParser
|
3
|
-
attr_reader :ps1
|
4
|
-
attr_reader :ps2
|
5
|
-
|
6
|
-
def initialize(options={})
|
7
|
-
options = {
|
8
|
-
:ps1 => '$ ',
|
9
|
-
:ps2 => '> '
|
10
|
-
}.merge(options)
|
11
|
-
|
12
|
-
@ps1 = options[:ps1]
|
13
|
-
@ps2 = options[:ps2]
|
14
|
-
end
|
15
|
-
|
16
|
-
def parse_cmd(cmd)
|
17
|
-
cmd =~ /.*?#\s*(?:\[(\d+)\])?\s*(\.{3})?/
|
18
|
-
exit_status = $1 ? $1.to_i : 0
|
19
|
-
output = $2 ? nil : ""
|
20
|
-
|
21
|
-
[cmd, output, exit_status]
|
22
|
-
end
|
23
|
-
|
24
|
-
def parse(script)
|
25
|
-
commands = []
|
26
|
-
|
27
|
-
command, output, exit_status = nil, "", 0
|
28
|
-
script.each_line do |line|
|
29
|
-
case
|
30
|
-
when line.index(ps1) == 0
|
31
|
-
if command
|
32
|
-
commands << [command, output, exit_status]
|
33
|
-
end
|
34
|
-
|
35
|
-
command, output, exit_status = parse_cmd lchomp(ps1, line)
|
36
|
-
|
37
|
-
when command.nil?
|
38
|
-
unless line.strip.empty?
|
39
|
-
command, output, exit_status = parse_cmd(line)
|
40
|
-
end
|
41
|
-
|
42
|
-
when line.index(ps2) == 0
|
43
|
-
command << lchomp(ps2, line)
|
44
|
-
|
45
|
-
when output.nil?
|
46
|
-
output = line
|
47
|
-
|
48
|
-
else
|
49
|
-
output << line
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
if command
|
54
|
-
commands << [command, output, exit_status]
|
55
|
-
end
|
56
|
-
|
57
|
-
commands
|
58
|
-
end
|
59
|
-
|
60
|
-
private
|
61
|
-
|
62
|
-
def lchomp(prefix, line) # :nodoc:
|
63
|
-
length = prefix.length
|
64
|
-
line[length, line.length - length]
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|