shell_test 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|