r-train 0.9.1
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +45 -0
- data/.travis.yml +12 -0
- data/Gemfile +22 -0
- data/LICENSE +201 -0
- data/README.md +137 -0
- data/Rakefile +39 -0
- data/lib/train.rb +100 -0
- data/lib/train/errors.rb +23 -0
- data/lib/train/extras.rb +15 -0
- data/lib/train/extras/command_wrapper.rb +105 -0
- data/lib/train/extras/file_common.rb +131 -0
- data/lib/train/extras/linux_file.rb +74 -0
- data/lib/train/extras/linux_lsb.rb +60 -0
- data/lib/train/extras/os_common.rb +131 -0
- data/lib/train/extras/os_detect_darwin.rb +32 -0
- data/lib/train/extras/os_detect_linux.rb +126 -0
- data/lib/train/extras/os_detect_unix.rb +77 -0
- data/lib/train/extras/os_detect_windows.rb +73 -0
- data/lib/train/extras/stat.rb +92 -0
- data/lib/train/extras/windows_file.rb +85 -0
- data/lib/train/options.rb +80 -0
- data/lib/train/plugins.rb +40 -0
- data/lib/train/plugins/base_connection.rb +86 -0
- data/lib/train/plugins/transport.rb +49 -0
- data/lib/train/transports/docker.rb +102 -0
- data/lib/train/transports/local.rb +52 -0
- data/lib/train/transports/local_file.rb +77 -0
- data/lib/train/transports/local_os.rb +51 -0
- data/lib/train/transports/mock.rb +125 -0
- data/lib/train/transports/ssh.rb +163 -0
- data/lib/train/transports/ssh_connection.rb +216 -0
- data/lib/train/transports/winrm.rb +187 -0
- data/lib/train/transports/winrm_connection.rb +258 -0
- data/lib/train/version.rb +7 -0
- data/test/integration/.kitchen.yml +43 -0
- data/test/integration/Berksfile +3 -0
- data/test/integration/bootstrap.sh +17 -0
- data/test/integration/chefignore +1 -0
- data/test/integration/cookbooks/test/metadata.rb +1 -0
- data/test/integration/cookbooks/test/recipes/default.rb +101 -0
- data/test/integration/docker_run.rb +153 -0
- data/test/integration/docker_test.rb +24 -0
- data/test/integration/docker_test_container.rb +24 -0
- data/test/integration/helper.rb +58 -0
- data/test/integration/sudo/nopasswd.rb +16 -0
- data/test/integration/sudo/passwd.rb +21 -0
- data/test/integration/sudo/run_as.rb +12 -0
- data/test/integration/test-runner.yaml +24 -0
- data/test/integration/test_local.rb +19 -0
- data/test/integration/test_ssh.rb +24 -0
- data/test/integration/tests/path_block_device_test.rb +74 -0
- data/test/integration/tests/path_character_device_test.rb +74 -0
- data/test/integration/tests/path_file_test.rb +79 -0
- data/test/integration/tests/path_folder_test.rb +88 -0
- data/test/integration/tests/path_missing_test.rb +77 -0
- data/test/integration/tests/path_pipe_test.rb +78 -0
- data/test/integration/tests/path_symlink_test.rb +83 -0
- data/test/integration/tests/run_command_test.rb +28 -0
- data/test/unit/extras/command_wrapper_test.rb +41 -0
- data/test/unit/extras/file_common_test.rb +133 -0
- data/test/unit/extras/linux_file_test.rb +98 -0
- data/test/unit/extras/os_common_test.rb +258 -0
- data/test/unit/extras/stat_test.rb +105 -0
- data/test/unit/helper.rb +6 -0
- data/test/unit/plugins/connection_test.rb +44 -0
- data/test/unit/plugins/transport_test.rb +111 -0
- data/test/unit/plugins_test.rb +22 -0
- data/test/unit/train_test.rb +132 -0
- data/test/unit/transports/local_file_test.rb +112 -0
- data/test/unit/transports/local_test.rb +73 -0
- data/test/unit/transports/mock_test.rb +76 -0
- data/test/unit/transports/ssh_test.rb +95 -0
- data/test/unit/version_test.rb +8 -0
- data/train.gemspec +32 -0
- metadata +299 -0
data/test/unit/helper.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe 'v1 Connection Plugin' do
|
5
|
+
describe 'empty v1 connection plugin' do
|
6
|
+
let(:cls) { Train::Plugins::Transport::BaseConnection }
|
7
|
+
let(:connection) { cls.new({}) }
|
8
|
+
|
9
|
+
it 'provides a close method' do
|
10
|
+
connection.close # wont raise
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'provides a run_command method' do
|
14
|
+
proc { connection.run_command('') }.must_raise Train::ClientError
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'provides an os method' do
|
18
|
+
proc { connection.os }.must_raise Train::ClientError
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'provides a file method' do
|
22
|
+
proc { connection.file('') }.must_raise Train::ClientError
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'provides a login command method' do
|
26
|
+
proc { connection.login_command }.must_raise Train::ClientError
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can wait until ready' do
|
30
|
+
connection.wait_until_ready # wont raise
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'provides a default logger' do
|
34
|
+
connection.method(:logger).call
|
35
|
+
.must_be_instance_of(Logger)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'must use the user-provided logger' do
|
39
|
+
l = rand
|
40
|
+
cls.new({logger: l})
|
41
|
+
.method(:logger).call.must_equal(l)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe 'v1 Transport Plugin' do
|
5
|
+
describe 'empty v1 transport plugin' do
|
6
|
+
let(:plugin) { Class.new(Train.plugin(1)) }
|
7
|
+
|
8
|
+
it 'initializes an empty configuration' do
|
9
|
+
plugin.new.options.must_equal({})
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'saves the provided configuration' do
|
13
|
+
conf = { a: rand }
|
14
|
+
plugin.new(conf).options.must_equal(conf)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'saves the provided configuration' do
|
18
|
+
conf = { a: rand }
|
19
|
+
plugin.new(conf).options.must_equal(conf)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'provides a default logger' do
|
23
|
+
conf = { a: rand }
|
24
|
+
plugin.new(conf)
|
25
|
+
.method(:logger).call
|
26
|
+
.must_be_instance_of(Logger)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can configure custom loggers' do
|
30
|
+
l = rand
|
31
|
+
plugin.new({ logger: l })
|
32
|
+
.method(:logger).call
|
33
|
+
.must_equal(l)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'provides a connection method' do
|
37
|
+
proc { plugin.new.connection }.must_raise Train::ClientError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'registered with a name' do
|
42
|
+
before do
|
43
|
+
Train::Plugins.registry.clear
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'doesnt have any plugins in the registry if none were configured' do
|
47
|
+
Train::Plugins.registry.empty?.must_equal true
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is is added to the plugins registry' do
|
51
|
+
plugin_name = rand
|
52
|
+
Train::Plugins.registry.wont_include(plugin_name)
|
53
|
+
|
54
|
+
plugin = Class.new(Train.plugin(1)) do
|
55
|
+
name plugin_name
|
56
|
+
end
|
57
|
+
|
58
|
+
Train::Plugins.registry[plugin_name].must_equal(plugin)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe 'with options' do
|
63
|
+
def train_class(opts = {})
|
64
|
+
name = rand.to_s
|
65
|
+
plugin = Class.new(Train.plugin(1)) do
|
66
|
+
option name, opts
|
67
|
+
end
|
68
|
+
[name, plugin]
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'exposes the parameters via api' do
|
72
|
+
name, plugin = train_class
|
73
|
+
plugin.default_options.keys.must_equal [name]
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'exposes the parameters via api' do
|
77
|
+
default = rand.to_s
|
78
|
+
name, plugin = train_class({ default: default })
|
79
|
+
plugin.default_options[name][:default].must_equal default
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'option must be required' do
|
83
|
+
name, plugin = train_class(required: true)
|
84
|
+
plugin.default_options[name][:required].must_equal true
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'default option must not be required' do
|
88
|
+
name, plugin = train_class
|
89
|
+
plugin.default_options[name][:required].must_equal nil
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'can include options from another module' do
|
93
|
+
nameA, pluginA = train_class
|
94
|
+
b = Class.new(Train.plugin(1)) do
|
95
|
+
include_options(pluginA)
|
96
|
+
end
|
97
|
+
b.default_options[nameA].wont_be_nil
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'overwrites existing options when including' do
|
101
|
+
old = rand.to_s
|
102
|
+
nu = rand.to_s
|
103
|
+
nameA, pluginA = train_class({ default: nu })
|
104
|
+
b = Class.new(Train.plugin(1)) do
|
105
|
+
option nameA, default: old
|
106
|
+
include_options(pluginA)
|
107
|
+
end
|
108
|
+
b.default_options[nameA][:default].must_equal nu
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'helper'
|
3
|
+
|
4
|
+
describe Train::Plugins do
|
5
|
+
it 'provides a method to create new v1 transport plugins' do
|
6
|
+
Train.plugin(1).must_equal Train::Plugins::Transport
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'fails when called with an unsupported plugin version' do
|
10
|
+
proc {
|
11
|
+
Train.plugin(2)
|
12
|
+
}.must_raise Train::ClientError
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'defaults to v1 plugins' do
|
16
|
+
Train.plugin.must_equal Train::Plugins::Transport
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'provides a registry of plugins' do
|
20
|
+
Train::Plugins.registry.must_be_instance_of(Hash)
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# Author:: Dominik Richter (<dominik.richter@gmail.com>)
|
4
|
+
require 'helper'
|
5
|
+
|
6
|
+
describe Train do
|
7
|
+
before do
|
8
|
+
Train::Plugins.registry.clear
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#create' do
|
12
|
+
it 'raises an error if the plugin isnt found' do
|
13
|
+
proc { Train.create('missing') }.must_raise Train::UserError
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'load a plugin if it isnt in the registry yet via symbol' do
|
17
|
+
Kernel.stub :require, true do
|
18
|
+
ex = Class.new(Train.plugin 1) { name 'existing' }
|
19
|
+
train = Train.create(:existing)
|
20
|
+
train.class.must_equal ex
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'load a plugin if it isnt in the registry yet via string' do
|
25
|
+
Kernel.stub :require, true do
|
26
|
+
ex = Class.new(Train.plugin 1) { name 'existing' }
|
27
|
+
train = Train.create('existing')
|
28
|
+
train.class.must_equal ex
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#options' do
|
34
|
+
it 'raises exception if a given transport plugin isnt found' do
|
35
|
+
proc { Train.options('missing') }.must_raise Train::UserError
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'provides empty options of a transport plugin' do
|
39
|
+
Class.new(Train.plugin 1) { name 'none' }
|
40
|
+
Train.options('none').must_equal({})
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'provides all options of a transport plugin' do
|
44
|
+
Class.new(Train.plugin 1) {
|
45
|
+
name 'one'
|
46
|
+
option :one, required: true, default: 123
|
47
|
+
}
|
48
|
+
Train.options('one').must_equal({
|
49
|
+
one: {
|
50
|
+
required: true,
|
51
|
+
default: 123,
|
52
|
+
}
|
53
|
+
})
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#target_config' do
|
58
|
+
it 'configures resolves target' do
|
59
|
+
org = {
|
60
|
+
target: 'ssh://user:pass@host.com:123/path',
|
61
|
+
}
|
62
|
+
res = Train.target_config(org)
|
63
|
+
res[:backend].must_equal 'ssh'
|
64
|
+
res[:host].must_equal 'host.com'
|
65
|
+
res[:user].must_equal 'user'
|
66
|
+
res[:password].must_equal 'pass'
|
67
|
+
res[:port].must_equal 123
|
68
|
+
res[:target].must_equal org[:target]
|
69
|
+
res[:path].must_equal '/path'
|
70
|
+
org.keys.must_equal [:target]
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'resolves a target while keeping existing fields' do
|
74
|
+
org = {
|
75
|
+
target: 'ssh://user:pass@host.com:123/path',
|
76
|
+
backend: rand,
|
77
|
+
host: rand,
|
78
|
+
user: rand,
|
79
|
+
password: rand,
|
80
|
+
port: rand,
|
81
|
+
path: rand
|
82
|
+
}
|
83
|
+
res = Train.target_config(org)
|
84
|
+
res.must_equal org
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'resolves a winrm target' do
|
88
|
+
org = {
|
89
|
+
target: 'winrm://Administrator@192.168.10.140',
|
90
|
+
backend: 'winrm',
|
91
|
+
host: '192.168.10.140',
|
92
|
+
user: 'Administrator',
|
93
|
+
password: nil,
|
94
|
+
port: nil,
|
95
|
+
path: nil
|
96
|
+
}
|
97
|
+
res = Train.target_config(org)
|
98
|
+
res.must_equal org
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'keeps the configuration when incorrect target is supplied' do
|
102
|
+
org = {
|
103
|
+
target: 'wrong',
|
104
|
+
}
|
105
|
+
res = Train.target_config(org)
|
106
|
+
res[:backend].must_be_nil
|
107
|
+
res[:host].must_be_nil
|
108
|
+
res[:user].must_be_nil
|
109
|
+
res[:password].must_be_nil
|
110
|
+
res[:port].must_be_nil
|
111
|
+
res[:path].must_be_nil
|
112
|
+
res[:target].must_equal org[:target]
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'always takes ruby sumbols as configuration fields' do
|
116
|
+
org = {
|
117
|
+
'target' => 'ssh://user:pass@host.com:123/path',
|
118
|
+
'backend' => rand,
|
119
|
+
'host' => rand,
|
120
|
+
'user' => rand,
|
121
|
+
'password' => rand,
|
122
|
+
'port' => rand,
|
123
|
+
'path' => rand
|
124
|
+
}
|
125
|
+
nu = org.each_with_object({}) { |(x, y), acc|
|
126
|
+
acc[x.to_sym] = y; acc
|
127
|
+
}
|
128
|
+
res = Train.target_config(org)
|
129
|
+
res.must_equal nu
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'helper'
|
7
|
+
require 'train/transports/local'
|
8
|
+
|
9
|
+
describe 'local file transport' do
|
10
|
+
let(:transport) { Train::Transports::Local.new }
|
11
|
+
let(:connection) { transport.connection }
|
12
|
+
|
13
|
+
it 'gets file contents' do
|
14
|
+
res = rand.to_s
|
15
|
+
File.stub :read, res do
|
16
|
+
connection.file(rand.to_s).content.must_equal(res)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'checks for file existance' do
|
21
|
+
File.stub :exist?, true do
|
22
|
+
connection.file(rand.to_s).exist?.must_equal(true)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
{
|
27
|
+
exist?: :exist?,
|
28
|
+
file?: :file?,
|
29
|
+
socket?: :socket?,
|
30
|
+
directory?: :directory?,
|
31
|
+
symlink?: :symlink?,
|
32
|
+
pipe?: :pipe?,
|
33
|
+
character_device?: :chardev?,
|
34
|
+
block_device?: :blockdev?,
|
35
|
+
}.each do |method, file_method|
|
36
|
+
it "checks if file is a #{method}" do
|
37
|
+
File.stub file_method.to_sym, true do
|
38
|
+
connection.file(rand.to_s).method(method.to_sym).call.must_equal(true)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'checks a file\'s link path' do
|
44
|
+
out = rand.to_s
|
45
|
+
File.stub :readlink, out do
|
46
|
+
File.stub :symlink?, true do
|
47
|
+
connection.file(rand.to_s).link_path.must_equal out
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe 'file metadata' do
|
53
|
+
let(:stat) { Struct.new(:mode, :size, :mtime, :uid, :gid) }
|
54
|
+
let(:statres) { stat.new(00140755, rand, (rand*100).to_i, rand, rand) }
|
55
|
+
|
56
|
+
def meta_stub(method, param, &block)
|
57
|
+
pwres = Struct.new(:name)
|
58
|
+
Etc.stub :getpwuid, pwres.new('owner') do
|
59
|
+
Etc.stub :getgrgid, pwres.new('group') do
|
60
|
+
File.stub method, param do; yield; end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'recognizes type' do
|
66
|
+
meta_stub :lstat, statres do
|
67
|
+
connection.file(rand.to_s).type.must_equal :socket
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'recognizes mode' do
|
72
|
+
meta_stub :lstat, statres do
|
73
|
+
connection.file(rand.to_s).mode.must_equal 00755
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'recognizes mtime' do
|
78
|
+
meta_stub :lstat, statres do
|
79
|
+
connection.file(rand.to_s).mtime.must_equal statres.mtime
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'recognizes size' do
|
84
|
+
meta_stub :lstat, statres do
|
85
|
+
connection.file(rand.to_s).size.must_equal statres.size
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'recognizes owner' do
|
90
|
+
meta_stub :lstat, statres do
|
91
|
+
connection.file(rand.to_s).owner.must_equal 'owner'
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'recognizes group' do
|
96
|
+
meta_stub :lstat, statres do
|
97
|
+
connection.file(rand.to_s).group.must_equal 'group'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'recognizes selinux label' do
|
102
|
+
meta_stub :lstat, statres do
|
103
|
+
label = rand.to_s
|
104
|
+
res = Train::Extras::CommandResult.new(label, nil, 0)
|
105
|
+
connection.stub :run_command, res do
|
106
|
+
connection.file(rand.to_s).selinux_label.must_equal label
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# author: Dominik Richter
|
4
|
+
# author: Christoph Hartmann
|
5
|
+
|
6
|
+
require 'helper'
|
7
|
+
require 'train/transports/local'
|
8
|
+
|
9
|
+
describe 'local transport' do
|
10
|
+
let(:transport) { Train::Transports::Local.new }
|
11
|
+
let(:connection) { transport.connection }
|
12
|
+
|
13
|
+
it 'can be instantiated' do
|
14
|
+
transport.wont_be_nil
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'gets the connection' do
|
18
|
+
connection.must_be_kind_of Train::Transports::Local::Connection
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'doesnt wait to be read' do
|
22
|
+
connection.wait_until_ready.must_be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'can be closed' do
|
26
|
+
connection.close.must_be_nil
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'has no login command' do
|
30
|
+
connection.login_command.must_be_nil
|
31
|
+
end
|
32
|
+
|
33
|
+
describe 'when running a local command' do
|
34
|
+
let(:mock) { Minitest::Mock.new }
|
35
|
+
|
36
|
+
def mock_run_cmd(cmd, &block)
|
37
|
+
mock.expect :run_command, nil
|
38
|
+
Mixlib::ShellOut.stub :new, mock do |*args|
|
39
|
+
block.call()
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'gets stdout' do
|
44
|
+
mock_run_cmd(rand) do
|
45
|
+
x = rand
|
46
|
+
mock.expect :stdout, x
|
47
|
+
mock.expect :stderr, nil
|
48
|
+
mock.expect :exitstatus, nil
|
49
|
+
connection.run_command(rand).stdout.must_equal x
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'gets stderr' do
|
54
|
+
mock_run_cmd(rand) do
|
55
|
+
x = rand
|
56
|
+
mock.expect :stdout, nil
|
57
|
+
mock.expect :stderr, x
|
58
|
+
mock.expect :exitstatus, nil
|
59
|
+
connection.run_command(rand).stderr.must_equal x
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'gets exit_status' do
|
64
|
+
mock_run_cmd(rand) do
|
65
|
+
x = rand
|
66
|
+
mock.expect :stdout, nil
|
67
|
+
mock.expect :stderr, nil
|
68
|
+
mock.expect :exitstatus, x
|
69
|
+
connection.run_command(rand).exit_status.must_equal x
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|