shell_test 0.2.0 → 0.3.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 +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
|