shell_test 0.1.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/MIT-LICENSE +19 -0
- data/README.rdoc +146 -0
- data/lib/shell_test/command_parser.rb +67 -0
- data/lib/shell_test/file_methods.rb +208 -0
- data/lib/shell_test/regexp_escape.rb +84 -0
- data/lib/shell_test/shell_methods.rb +176 -0
- data/lib/shell_test/unit/shim.rb +72 -0
- data/lib/shell_test/unit.rb +40 -0
- data/lib/shell_test/version.rb +3 -0
- data/lib/shell_test.rb +8 -0
- metadata +85 -0
data/History.rdoc
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2011, Simon Chiang.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
= ShellTest
|
2
|
+
|
3
|
+
Test modules for shell scripts.
|
4
|
+
|
5
|
+
== Description
|
6
|
+
|
7
|
+
Provides test modules to simplify testing of shell scripts.
|
8
|
+
|
9
|
+
ShellTest is not a testing framework. ShellTest integrates with Test::Unit and
|
10
|
+
MiniTest out of the box, but it should be possible to include the test modules
|
11
|
+
into other test frameworks.
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
|
15
|
+
ShellTest builds on modules that provide specific functionality. The modules
|
16
|
+
may be used independently, but by including ShellTest you get them all:
|
17
|
+
|
18
|
+
require 'shell_test/unit'
|
19
|
+
|
20
|
+
class ShellTestExample < Test::Unit::TestCase
|
21
|
+
include ShellTest
|
22
|
+
|
23
|
+
def test_a_script
|
24
|
+
script = prepare('script.sh') do |io|
|
25
|
+
io.puts "echo goodnight $1"
|
26
|
+
end
|
27
|
+
|
28
|
+
assert_script %{
|
29
|
+
$ sh '#{script}' moon
|
30
|
+
goodnight moon
|
31
|
+
}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
==== {ShellMethods}[link:classes/ShellTest/ShellMethods.html]
|
36
|
+
|
37
|
+
Provides the shell testing methods. These methods are designed to input a
|
38
|
+
string that looks like terminal input/output. Commands are parsed out of the
|
39
|
+
string, run, and then anything printed to stdout is compared to the expected
|
40
|
+
output. In addition the exit status is checked for success (0).
|
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.
|
44
|
+
|
45
|
+
require 'shell_test/unit'
|
46
|
+
|
47
|
+
class ShellMethodsExample < Test::Unit::TestCase
|
48
|
+
include ShellTest::ShellMethods
|
49
|
+
|
50
|
+
def test_a_script_using_variables
|
51
|
+
with_env("THING" => "moon") do
|
52
|
+
assert_script %{
|
53
|
+
$ echo "goodnight $THING"
|
54
|
+
goodnight moon
|
55
|
+
}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_multiple_commands
|
60
|
+
assert_script %{
|
61
|
+
$ echo one
|
62
|
+
one
|
63
|
+
$ echo two
|
64
|
+
two
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_multiline_commands
|
69
|
+
assert_script %{
|
70
|
+
$ for n in one two; do
|
71
|
+
> echo $n
|
72
|
+
> done
|
73
|
+
one
|
74
|
+
two
|
75
|
+
}
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_exit_statuses
|
79
|
+
assert_script %{
|
80
|
+
$ true # [0]
|
81
|
+
$ false # [1]
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_exit_status_and_not_ouptut
|
86
|
+
assert_script %{
|
87
|
+
$ date # [0] ...
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_output_with_inline_regexps
|
92
|
+
assert_script_match %{
|
93
|
+
$ cal
|
94
|
+
:...:
|
95
|
+
Su Mo Tu We Th Fr Sa
|
96
|
+
:...:
|
97
|
+
}
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
==== {FileMethods}[link:classes/ShellTest/FileMethods.html]
|
102
|
+
|
103
|
+
Sets up a temporary, test-specific directory for working with files. This
|
104
|
+
approach is better in most cases than using Tempfile because you can flag the
|
105
|
+
directory to be saved on a failure (using ENV['KEEP_OUTPUTS']='true').
|
106
|
+
|
107
|
+
By default the directory is guessed based off of the test file and test
|
108
|
+
method. If this example were located in the 'test/file_methods_example.rb'
|
109
|
+
file, then the directory for the test case would be
|
110
|
+
'test/file_methods_example/test_preparation_of_a_test_specific_file'.
|
111
|
+
|
112
|
+
require 'shell_test/unit'
|
113
|
+
|
114
|
+
class FileMethodsExample < Test::Unit::TestCase
|
115
|
+
include ShellTest::FileMethods
|
116
|
+
|
117
|
+
def test_preparation_of_a_test_specific_file
|
118
|
+
path = prepare('dir/file.txt') {|io| io << 'content' }
|
119
|
+
assert_equal "content", File.read(path)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
== Installation
|
124
|
+
|
125
|
+
ShellTest is available as a gem[http://rubygems.org/gems/shell_test].
|
126
|
+
|
127
|
+
$ gem install shell_test
|
128
|
+
|
129
|
+
== Development
|
130
|
+
|
131
|
+
To get started, checkout the code from GitHub[http://github.com/thinkerbot/shell_test] and run:
|
132
|
+
|
133
|
+
git clone https://thinkerbot@github.com/thinkerbot/shell_test.git
|
134
|
+
cd shell_test
|
135
|
+
rake test
|
136
|
+
|
137
|
+
To test against multiple platforms I suggest using rvm. In that case:
|
138
|
+
|
139
|
+
rvm rake test
|
140
|
+
|
141
|
+
Please report any issues {here}[http://github.com/thinkerbot/shell_test/issues].
|
142
|
+
|
143
|
+
== Info
|
144
|
+
|
145
|
+
Developer:: {Simon Chiang}[http://thinkerbot.posterous.com]
|
146
|
+
License:: {MIT-Style}[link:files/MIT-LICENSE.html]
|
@@ -0,0 +1,67 @@
|
|
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
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module ShellTest
|
4
|
+
module FileMethods
|
5
|
+
module ClassMethods
|
6
|
+
attr_accessor :class_dir
|
7
|
+
|
8
|
+
attr_reader :cleanup_method_registry
|
9
|
+
|
10
|
+
def cleanup_methods
|
11
|
+
@cleanup_methods ||= begin
|
12
|
+
cleanup_methods = {}
|
13
|
+
|
14
|
+
ancestors.reverse.each do |ancestor|
|
15
|
+
next unless ancestor.kind_of?(ClassMethods)
|
16
|
+
ancestor.cleanup_method_registry.each_pair do |key, value|
|
17
|
+
if value.nil?
|
18
|
+
cleanup_methods.delete(key)
|
19
|
+
else
|
20
|
+
cleanup_methods[key] = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
cleanup_methods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def reset_cleanup_methods
|
30
|
+
@cleanup_methods = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def self.initialize(base)
|
36
|
+
# Infers the test directory from the calling file.
|
37
|
+
# 'some_class_test.rb' => 'some_class_test'
|
38
|
+
call_line = caller.find {|value| value !~ /`(includ|inherit|extend)ed'$/ }
|
39
|
+
|
40
|
+
if call_line
|
41
|
+
calling_file = call_line.gsub(/:\d+(:in .*)?$/, "")
|
42
|
+
base.class_dir = calling_file.chomp(File.extname(calling_file))
|
43
|
+
else
|
44
|
+
unless Dir.respond_to?(:tmpdir)
|
45
|
+
require 'tmpdir'
|
46
|
+
end
|
47
|
+
base.class_dir = Dir.tmpdir
|
48
|
+
end
|
49
|
+
|
50
|
+
base.reset_cleanup_methods
|
51
|
+
unless base.instance_variable_defined?(:@cleanup_method_registry)
|
52
|
+
base.instance_variable_set(:@cleanup_method_registry, {})
|
53
|
+
end
|
54
|
+
|
55
|
+
unless base.instance_variable_defined?(:@cleanup_paths)
|
56
|
+
base.instance_variable_set(:@cleanup_paths, ['.'])
|
57
|
+
end
|
58
|
+
|
59
|
+
unless base.instance_variable_defined?(:@cleanup)
|
60
|
+
base.instance_variable_set(:@cleanup, true)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def inherited(base) # :nodoc:
|
65
|
+
ClassMethods.initialize(base)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def define_method_cleanup(method_name, dirs)
|
70
|
+
reset_cleanup_methods
|
71
|
+
cleanup_method_registry[method_name.to_sym] = dirs
|
72
|
+
end
|
73
|
+
|
74
|
+
def remove_method_cleanup(method_name)
|
75
|
+
reset_cleanup_methods
|
76
|
+
cleanup_method_registry.delete(method_name.to_sym)
|
77
|
+
end
|
78
|
+
|
79
|
+
def undef_method_cleanup(method_name)
|
80
|
+
reset_cleanup_methods
|
81
|
+
cleanup_method_registry[method_name.to_sym] = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
def cleanup_paths(*dirs)
|
85
|
+
@cleanup_paths = dirs
|
86
|
+
end
|
87
|
+
|
88
|
+
def cleanup(*method_names)
|
89
|
+
if method_names.empty?
|
90
|
+
@cleanup = true
|
91
|
+
else
|
92
|
+
method_names.each do |method_name|
|
93
|
+
define_method_cleanup method_name, @cleanup_paths
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def no_cleanup(*method_names)
|
99
|
+
if method_names.empty?
|
100
|
+
@cleanup = false
|
101
|
+
else
|
102
|
+
method_names.each do |method_name|
|
103
|
+
undef_method_cleanup method_name
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def method_added(sym)
|
109
|
+
if @cleanup && !cleanup_method_registry.has_key?(sym.to_sym) && sym.to_s[0, 5] == "test_"
|
110
|
+
cleanup sym
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module ModuleMethods
|
116
|
+
module_function
|
117
|
+
|
118
|
+
def included(base)
|
119
|
+
base.extend ClassMethods
|
120
|
+
base.extend ModuleMethods unless base.kind_of?(Class)
|
121
|
+
|
122
|
+
ClassMethods.initialize(base)
|
123
|
+
super
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
extend ModuleMethods
|
128
|
+
|
129
|
+
def setup
|
130
|
+
super
|
131
|
+
cleanup
|
132
|
+
end
|
133
|
+
|
134
|
+
def teardown
|
135
|
+
Dir.chdir(user_dir)
|
136
|
+
|
137
|
+
unless ENV["KEEP_OUTPUTS"] == "true"
|
138
|
+
cleanup
|
139
|
+
|
140
|
+
dir = method_dir
|
141
|
+
while dir != class_dir
|
142
|
+
dir = File.dirname(dir)
|
143
|
+
Dir.rmdir(dir)
|
144
|
+
end rescue(SystemCallError)
|
145
|
+
end
|
146
|
+
|
147
|
+
super
|
148
|
+
end
|
149
|
+
|
150
|
+
def user_dir
|
151
|
+
@user_dir ||= File.expand_path('.')
|
152
|
+
end
|
153
|
+
|
154
|
+
def class_dir
|
155
|
+
@class_dir ||= File.expand_path(self.class.class_dir, user_dir)
|
156
|
+
end
|
157
|
+
|
158
|
+
def method_dir
|
159
|
+
@method_dir ||= File.expand_path(method_name.to_s, class_dir)
|
160
|
+
end
|
161
|
+
|
162
|
+
def method_name
|
163
|
+
__name__
|
164
|
+
end
|
165
|
+
|
166
|
+
def cleanup_methods
|
167
|
+
self.class.cleanup_methods
|
168
|
+
end
|
169
|
+
|
170
|
+
def cleanup
|
171
|
+
if cleanup_paths = cleanup_methods[method_name.to_sym]
|
172
|
+
cleanup_paths.each {|relative_path| remove(relative_path) }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def path(relative_path)
|
177
|
+
path = File.expand_path(relative_path, method_dir)
|
178
|
+
|
179
|
+
unless path.index(method_dir) == 0
|
180
|
+
raise "does not make a path relative to method_dir: #{relative_path.inspect}"
|
181
|
+
end
|
182
|
+
|
183
|
+
path
|
184
|
+
end
|
185
|
+
|
186
|
+
def prepare(relative_path, content=nil, &block)
|
187
|
+
target = path(relative_path)
|
188
|
+
|
189
|
+
if File.exists?(target)
|
190
|
+
FileUtils.rm(target)
|
191
|
+
else
|
192
|
+
target_dir = File.dirname(target)
|
193
|
+
FileUtils.mkdir_p(target_dir) unless File.exists?(target_dir)
|
194
|
+
end
|
195
|
+
|
196
|
+
FileUtils.touch(target)
|
197
|
+
File.open(target, 'w') {|io| io << content } if content
|
198
|
+
File.open(target, 'a', &block) if block
|
199
|
+
|
200
|
+
target
|
201
|
+
end
|
202
|
+
|
203
|
+
def remove(relative_path)
|
204
|
+
full_path = path(relative_path)
|
205
|
+
FileUtils.rm_r(full_path) if File.exists?(full_path)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ShellTest
|
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
|
+
# to match, for instance, console output.
|
5
|
+
#
|
6
|
+
# The RegexpEscape.escape (or equivalently the quote) method does the
|
7
|
+
# work; all regexp-active characters are escaped except for characters
|
8
|
+
# enclosed by ':.' and '.:' delimiters.
|
9
|
+
#
|
10
|
+
# RegexpEscape.escape('reg[exp]+ chars. are(quoted)') # => 'reg\[exp\]\+\ chars\.\ are\(quoted\)'
|
11
|
+
# RegexpEscape.escape('these are not: :.a(b*)c.:') # => 'these\ are\ not:\ a(b*)c'
|
12
|
+
#
|
13
|
+
# In addition, all-period regexps are automatically upgraded to '.*?';
|
14
|
+
# use the '.{n}' notation to specify n arbitrary characters.
|
15
|
+
#
|
16
|
+
# RegexpEscape.escape('_:..:_:...:_:....:') # => '_.*?_.*?_.*?'
|
17
|
+
# RegexpEscape.escape(':..{1}.:') # => '.{1}'
|
18
|
+
#
|
19
|
+
# RegexpEscape instances are initialized using the escaped input string
|
20
|
+
# and return the original string upon to_s.
|
21
|
+
#
|
22
|
+
# str = %q{
|
23
|
+
# a multiline
|
24
|
+
# :...:
|
25
|
+
# example}
|
26
|
+
# r = RegexpEscape.new(str)
|
27
|
+
#
|
28
|
+
# r =~ %q{
|
29
|
+
# a multiline
|
30
|
+
# matching
|
31
|
+
# example} # => true
|
32
|
+
#
|
33
|
+
# r !~ %q{
|
34
|
+
# a failing multiline
|
35
|
+
# example} # => true
|
36
|
+
#
|
37
|
+
# r.to_s # => str
|
38
|
+
#
|
39
|
+
class RegexpEscape < Regexp
|
40
|
+
|
41
|
+
# matches the escape sequence
|
42
|
+
ESCAPE_SEQUENCE = /:\..*?\.:/
|
43
|
+
|
44
|
+
class << self
|
45
|
+
|
46
|
+
# Escapes regexp-active characters in str, except for character
|
47
|
+
# delimited by ':.' and '.:'. See the class description for
|
48
|
+
# details.
|
49
|
+
def escape(str)
|
50
|
+
substituents = []
|
51
|
+
str.scan(ESCAPE_SEQUENCE) do
|
52
|
+
regexp_str = $&[2...-2]
|
53
|
+
regexp_str = ".*?" if regexp_str =~ /^\.*$/
|
54
|
+
substituents << regexp_str
|
55
|
+
end
|
56
|
+
substituents << ""
|
57
|
+
|
58
|
+
splits = str.split(ESCAPE_SEQUENCE).collect do |split|
|
59
|
+
super(split)
|
60
|
+
end
|
61
|
+
splits << "" if splits.empty?
|
62
|
+
|
63
|
+
splits.zip(substituents).to_a.flatten.join
|
64
|
+
end
|
65
|
+
|
66
|
+
# Same as escape.
|
67
|
+
def quote(str)
|
68
|
+
escape(str)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Generates a new RegexpEscape by escaping the str, using the same
|
73
|
+
# options as Regexp.
|
74
|
+
def initialize(str, *options)
|
75
|
+
super(RegexpEscape.escape(str), *options)
|
76
|
+
@original_str = str
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the original string for self
|
80
|
+
def to_s
|
81
|
+
@original_str
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'shell_test/regexp_escape'
|
2
|
+
require 'shell_test/command_parser'
|
3
|
+
|
4
|
+
module ShellTest
|
5
|
+
module ShellMethods
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
@notify_method_name = true
|
9
|
+
end
|
10
|
+
|
11
|
+
# Parse a script into an array of [cmd, output, status] triplets.
|
12
|
+
def parse_script(script, options={})
|
13
|
+
CommandParser.new(options).parse(script)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns true if the ENV variable 'VERBOSE' is true. When verbose,
|
17
|
+
# ShellTest prints the expanded commands of sh_test to $stdout.
|
18
|
+
def verbose?
|
19
|
+
verbose = ENV['VERBOSE']
|
20
|
+
verbose && verbose =~ /^true$/i ? true : false
|
21
|
+
end
|
22
|
+
|
23
|
+
# Sets the specified ENV variables and returns the *current* env.
|
24
|
+
# If replace is true, current ENV variables are replaced; otherwise
|
25
|
+
# the new env variables are simply added to the existing set.
|
26
|
+
def set_env(env={}, replace=false)
|
27
|
+
current_env = {}
|
28
|
+
ENV.each_pair do |key, value|
|
29
|
+
current_env[key] = value
|
30
|
+
end
|
31
|
+
|
32
|
+
ENV.clear if replace
|
33
|
+
|
34
|
+
env.each_pair do |key, value|
|
35
|
+
if value.nil?
|
36
|
+
ENV.delete(key)
|
37
|
+
else
|
38
|
+
ENV[key] = value
|
39
|
+
end
|
40
|
+
end if env
|
41
|
+
|
42
|
+
current_env
|
43
|
+
end
|
44
|
+
|
45
|
+
# Sets the specified ENV variables for the duration of the block.
|
46
|
+
# If replace is true, current ENV variables are replaced; otherwise
|
47
|
+
# the new env variables are simply added to the existing set.
|
48
|
+
#
|
49
|
+
# Returns the block return.
|
50
|
+
def with_env(env={}, replace=false)
|
51
|
+
current_env = nil
|
52
|
+
begin
|
53
|
+
current_env = set_env(env, replace)
|
54
|
+
yield
|
55
|
+
ensure
|
56
|
+
if current_env
|
57
|
+
set_env(current_env, true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def sh(cmd)
|
63
|
+
if @notify_method_name && verbose?
|
64
|
+
@notify_method_name = false
|
65
|
+
puts
|
66
|
+
puts method_name
|
67
|
+
end
|
68
|
+
|
69
|
+
start = Time.now
|
70
|
+
result = `#{cmd}`
|
71
|
+
finish = Time.now
|
72
|
+
|
73
|
+
if verbose?
|
74
|
+
elapsed = "%.3f" % [finish-start]
|
75
|
+
puts " (#{elapsed}s) #{cmd}"
|
76
|
+
end
|
77
|
+
|
78
|
+
result
|
79
|
+
end
|
80
|
+
|
81
|
+
def assert_script(script, options={})
|
82
|
+
_assert_script outdent(script), options
|
83
|
+
end
|
84
|
+
|
85
|
+
def _assert_script(script, options={})
|
86
|
+
parse_script(script, options).each do |cmd, output, status|
|
87
|
+
result = sh(cmd)
|
88
|
+
|
89
|
+
_assert_output_equal(output, result, cmd) if output
|
90
|
+
assert_equal(status, $?.exitstatus, cmd) if status
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def assert_script_match(script, options={})
|
95
|
+
_assert_script_match outdent(script), options
|
96
|
+
end
|
97
|
+
|
98
|
+
def _assert_script_match(script, options={})
|
99
|
+
parse_script(script, options).each do |cmd, output, status|
|
100
|
+
result = sh(cmd)
|
101
|
+
|
102
|
+
_assert_alike(output, result, cmd) if output
|
103
|
+
assert_equal(status, $?.exitstatus, cmd) if status
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# Asserts whether or not the a and b strings are equal, with a more
|
108
|
+
# readable output than assert_equal for large strings (especially large
|
109
|
+
# strings with significant whitespace).
|
110
|
+
def assert_output_equal(a, b, msg=nil)
|
111
|
+
_assert_output_equal outdent(a), b, msg
|
112
|
+
end
|
113
|
+
|
114
|
+
def _assert_output_equal(a, b, msg=nil)
|
115
|
+
if a == b
|
116
|
+
assert true
|
117
|
+
else
|
118
|
+
flunk %Q{
|
119
|
+
#{msg}
|
120
|
+
==================== expected output ====================
|
121
|
+
#{whitespace_escape(a)}
|
122
|
+
======================== but was ========================
|
123
|
+
#{whitespace_escape(b)}
|
124
|
+
=========================================================
|
125
|
+
}
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# Asserts whether or not b is like a (which should be a Regexp), and
|
130
|
+
# provides a more readable output in the case of a failure as compared
|
131
|
+
# with assert_match.
|
132
|
+
#
|
133
|
+
# If a is a string it is turned into a RegexpEscape.
|
134
|
+
def assert_alike(a, b, msg=nil)
|
135
|
+
a = outdent(a) if a.kind_of?(String)
|
136
|
+
_assert_alike a, b, msg
|
137
|
+
end
|
138
|
+
|
139
|
+
def _assert_alike(a, b, msg=nil)
|
140
|
+
if a.kind_of?(String)
|
141
|
+
a = RegexpEscape.new(a)
|
142
|
+
end
|
143
|
+
|
144
|
+
if b =~ a
|
145
|
+
assert true
|
146
|
+
else
|
147
|
+
flunk %Q{
|
148
|
+
#{msg}
|
149
|
+
================= expected output like ==================
|
150
|
+
#{whitespace_escape(a)}
|
151
|
+
======================== but was ========================
|
152
|
+
#{whitespace_escape(b)}
|
153
|
+
=========================================================
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# helper for stripping indentation off a string
|
159
|
+
def outdent(str)
|
160
|
+
str =~ /\A(?:\s*?\n)( *)(.*)\z/m ? $2.gsub!(/^ {0,#{$1.length}}/, '') : str
|
161
|
+
end
|
162
|
+
|
163
|
+
# helper for formatting escaping whitespace into readable text
|
164
|
+
def whitespace_escape(str)
|
165
|
+
str.to_s.gsub(/\s/) do |match|
|
166
|
+
case match
|
167
|
+
when "\n" then "\\n\n"
|
168
|
+
when "\t" then "\\t"
|
169
|
+
when "\r" then "\\r"
|
170
|
+
when "\f" then "\\f"
|
171
|
+
else match
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# :stopdoc:
|
2
|
+
module ShellTest
|
3
|
+
module Unit
|
4
|
+
|
5
|
+
# An exception class to flag skips.
|
6
|
+
class SkipException < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
# Modifies how errors related to a SkipException are displayed.
|
10
|
+
module SkipDisplay
|
11
|
+
# Display S rather than E in the progress.
|
12
|
+
def single_character_display
|
13
|
+
"S"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Removes the exception class from the message.
|
17
|
+
def message
|
18
|
+
@exception.message
|
19
|
+
end
|
20
|
+
|
21
|
+
# Updates the output to look like a MiniTest skip error.
|
22
|
+
def long_display
|
23
|
+
backtrace = filter_backtrace(@exception.backtrace)
|
24
|
+
"Skipped:\n#@test_name [#{backtrace[0].sub(/:in `.*$/, "")}]:\n#{message}\n"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
require 'test/unit/testresult'
|
31
|
+
class Test::Unit::TestResult
|
32
|
+
# Returns an array of skips recorded for self.
|
33
|
+
def skips
|
34
|
+
@skips ||= []
|
35
|
+
end
|
36
|
+
|
37
|
+
# Partition errors from a SkipException from other errors and records as
|
38
|
+
# them as skips (the error is extended to display as a skip).
|
39
|
+
def add_error(error)
|
40
|
+
if error.exception.kind_of?(ShellTest::Unit::SkipException)
|
41
|
+
error.extend ShellTest::Unit::SkipDisplay
|
42
|
+
skips << error
|
43
|
+
else
|
44
|
+
@errors << error
|
45
|
+
end
|
46
|
+
|
47
|
+
notify_listeners(FAULT, error)
|
48
|
+
notify_listeners(CHANGED, self)
|
49
|
+
end
|
50
|
+
|
51
|
+
alias shell_test_original_to_s to_s
|
52
|
+
|
53
|
+
# Adds the skip count to the summary.
|
54
|
+
def to_s
|
55
|
+
"#{shell_test_original_to_s}, #{skips.length} skips"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
require 'test/unit/testcase'
|
60
|
+
class Test::Unit::TestCase
|
61
|
+
# Alias method_name to __name__ such that FileMethods can redefine
|
62
|
+
# method_name to call __name__ (circular I know, but necessary for
|
63
|
+
# compatibility with MiniTest)
|
64
|
+
alias __name__ method_name
|
65
|
+
|
66
|
+
# Call to skip a test.
|
67
|
+
def skip(msg = nil, bt = caller)
|
68
|
+
msg ||= "Skipped, no message given"
|
69
|
+
raise ShellTest::Unit::SkipException, msg, bt
|
70
|
+
end
|
71
|
+
end
|
72
|
+
# :startdoc:
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require 'shell_test'
|
3
|
+
|
4
|
+
module ShellTest
|
5
|
+
# ShellTest is designed to work with MiniTest, which is the standard testing
|
6
|
+
# framework included in ruby 1.9. Minor changes in the API break backward
|
7
|
+
# compatibility with Test::Unit and/or add functionality expected by
|
8
|
+
# ShellTest.
|
9
|
+
#
|
10
|
+
# Test::Unit can be patched by requiring the shim file before defining
|
11
|
+
# specific TestCase subclasses.
|
12
|
+
#
|
13
|
+
# require 'test/unit'
|
14
|
+
# unless Object.const_defined?(:MiniTest)
|
15
|
+
# require 'shell_test/unit/shim'
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# To let ShellTest do this for you:
|
19
|
+
#
|
20
|
+
# require 'shell_test/unit'
|
21
|
+
#
|
22
|
+
# Note that the shim script has only been tested vs the Test::Unit that
|
23
|
+
# comes with ruby 1.8.x. A Test::Unit 2.0 gem exists; use with caution.
|
24
|
+
#
|
25
|
+
# ==== Patches
|
26
|
+
#
|
27
|
+
# The shim script adds two things to Test::Unit:
|
28
|
+
#
|
29
|
+
# 1) A __name__ method which returns the test method name (alias for
|
30
|
+
# method_name)
|
31
|
+
#
|
32
|
+
# 2) A skip method which can be used to skip a test (use it like flunk)
|
33
|
+
#
|
34
|
+
module Unit
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
unless Object.const_defined?(:MiniTest)
|
39
|
+
require 'shell_test/unit/shim'
|
40
|
+
end
|
data/lib/shell_test.rb
ADDED
metadata
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: shell_test
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Simon Chiang
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-07-07 00:00:00 -06:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Provides test modules to simplify testing of shell scripts. ShellTest is not a testing framework. ShellTest integrates with Test::Unit and MiniTest out of the box, but it should be possible to include the test modules into other test frameworks.
|
23
|
+
email:
|
24
|
+
- simon.a.chiang@gmail.com
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files:
|
30
|
+
- History.rdoc
|
31
|
+
- README.rdoc
|
32
|
+
- MIT-LICENSE
|
33
|
+
files:
|
34
|
+
- lib/shell_test.rb
|
35
|
+
- lib/shell_test/command_parser.rb
|
36
|
+
- lib/shell_test/file_methods.rb
|
37
|
+
- lib/shell_test/regexp_escape.rb
|
38
|
+
- lib/shell_test/shell_methods.rb
|
39
|
+
- lib/shell_test/unit.rb
|
40
|
+
- lib/shell_test/unit/shim.rb
|
41
|
+
- lib/shell_test/version.rb
|
42
|
+
- History.rdoc
|
43
|
+
- README.rdoc
|
44
|
+
- MIT-LICENSE
|
45
|
+
has_rdoc: true
|
46
|
+
homepage: ""
|
47
|
+
licenses: []
|
48
|
+
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options:
|
51
|
+
- --main
|
52
|
+
- README.rdoc
|
53
|
+
- -S
|
54
|
+
- -N
|
55
|
+
- --title
|
56
|
+
- ShellTest
|
57
|
+
require_paths:
|
58
|
+
- lib
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
60
|
+
none: false
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
hash: 3
|
65
|
+
segments:
|
66
|
+
- 0
|
67
|
+
version: "0"
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
hash: 3
|
74
|
+
segments:
|
75
|
+
- 0
|
76
|
+
version: "0"
|
77
|
+
requirements: []
|
78
|
+
|
79
|
+
rubyforge_project: shell_test
|
80
|
+
rubygems_version: 1.6.2
|
81
|
+
signing_key:
|
82
|
+
specification_version: 3
|
83
|
+
summary: Test modules for shell scripts
|
84
|
+
test_files: []
|
85
|
+
|