test-kitchen 1.2.1 → 1.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.
- checksums.yaml +4 -4
- data/.cane +1 -1
- data/.rubocop.yml +3 -0
- data/.travis.yml +20 -9
- data/CHANGELOG.md +219 -108
- data/Gemfile +10 -6
- data/Guardfile +38 -9
- data/README.md +11 -1
- data/Rakefile +21 -37
- data/bin/kitchen +4 -4
- data/features/kitchen_action_commands.feature +161 -0
- data/features/kitchen_console_command.feature +34 -0
- data/features/kitchen_diagnose_command.feature +64 -0
- data/features/kitchen_init_command.feature +29 -17
- data/features/kitchen_list_command.feature +2 -2
- data/features/kitchen_login_command.feature +56 -0
- data/features/{sink_command.feature → kitchen_sink_command.feature} +0 -0
- data/features/kitchen_test_command.feature +88 -0
- data/features/step_definitions/gem_steps.rb +8 -6
- data/features/step_definitions/git_steps.rb +4 -2
- data/features/step_definitions/output_steps.rb +5 -0
- data/features/support/env.rb +12 -9
- data/lib/kitchen.rb +60 -38
- data/lib/kitchen/base64_stream.rb +55 -0
- data/lib/kitchen/busser.rb +124 -58
- data/lib/kitchen/cli.rb +121 -38
- data/lib/kitchen/collection.rb +3 -3
- data/lib/kitchen/color.rb +4 -4
- data/lib/kitchen/command.rb +78 -11
- data/lib/kitchen/command/action.rb +3 -2
- data/lib/kitchen/command/console.rb +12 -5
- data/lib/kitchen/command/diagnose.rb +17 -3
- data/lib/kitchen/command/driver_discover.rb +26 -7
- data/lib/kitchen/command/exec.rb +41 -0
- data/lib/kitchen/command/list.rb +44 -14
- data/lib/kitchen/command/login.rb +2 -1
- data/lib/kitchen/command/sink.rb +2 -1
- data/lib/kitchen/command/test.rb +5 -4
- data/lib/kitchen/config.rb +146 -14
- data/lib/kitchen/configurable.rb +314 -0
- data/lib/kitchen/data_munger.rb +522 -18
- data/lib/kitchen/diagnostic.rb +43 -4
- data/lib/kitchen/driver.rb +4 -4
- data/lib/kitchen/driver/base.rb +80 -115
- data/lib/kitchen/driver/dummy.rb +34 -6
- data/lib/kitchen/driver/proxy.rb +14 -3
- data/lib/kitchen/driver/ssh_base.rb +61 -7
- data/lib/kitchen/errors.rb +109 -9
- data/lib/kitchen/generator/driver_create.rb +39 -5
- data/lib/kitchen/generator/init.rb +130 -45
- data/lib/kitchen/instance.rb +162 -28
- data/lib/kitchen/lazy_hash.rb +79 -7
- data/lib/kitchen/loader/yaml.rb +159 -27
- data/lib/kitchen/logger.rb +267 -21
- data/lib/kitchen/logging.rb +30 -3
- data/lib/kitchen/login_command.rb +11 -2
- data/lib/kitchen/metadata_chopper.rb +2 -2
- data/lib/kitchen/provisioner.rb +4 -4
- data/lib/kitchen/provisioner/base.rb +107 -103
- data/lib/kitchen/provisioner/chef/berkshelf.rb +36 -8
- data/lib/kitchen/provisioner/chef/librarian.rb +40 -11
- data/lib/kitchen/provisioner/chef_base.rb +206 -167
- data/lib/kitchen/provisioner/chef_solo.rb +25 -7
- data/lib/kitchen/provisioner/chef_zero.rb +105 -29
- data/lib/kitchen/provisioner/dummy.rb +1 -1
- data/lib/kitchen/provisioner/shell.rb +21 -6
- data/lib/kitchen/rake_tasks.rb +8 -3
- data/lib/kitchen/shell_out.rb +15 -18
- data/lib/kitchen/ssh.rb +122 -27
- data/lib/kitchen/state_file.rb +24 -7
- data/lib/kitchen/thor_tasks.rb +9 -4
- data/lib/kitchen/util.rb +43 -118
- data/lib/kitchen/version.rb +1 -1
- data/lib/vendor/hash_recursive_merge.rb +10 -2
- data/spec/kitchen/base64_stream_spec.rb +77 -0
- data/spec/kitchen/busser_spec.rb +490 -0
- data/spec/kitchen/collection_spec.rb +10 -10
- data/spec/kitchen/color_spec.rb +2 -2
- data/spec/kitchen/config_spec.rb +234 -62
- data/spec/kitchen/configurable_spec.rb +490 -0
- data/spec/kitchen/data_munger_spec.rb +1070 -862
- data/spec/kitchen/diagnostic_spec.rb +79 -0
- data/spec/kitchen/driver/base_spec.rb +80 -85
- data/spec/kitchen/driver/dummy_spec.rb +43 -14
- data/spec/kitchen/driver/proxy_spec.rb +134 -0
- data/spec/kitchen/driver/ssh_base_spec.rb +644 -0
- data/spec/kitchen/driver_spec.rb +15 -15
- data/spec/kitchen/errors_spec.rb +309 -0
- data/spec/kitchen/instance_spec.rb +143 -46
- data/spec/kitchen/lazy_hash_spec.rb +36 -9
- data/spec/kitchen/loader/yaml_spec.rb +237 -226
- data/spec/kitchen/logger_spec.rb +419 -0
- data/spec/kitchen/logging_spec.rb +59 -0
- data/spec/kitchen/login_command_spec.rb +49 -0
- data/spec/kitchen/metadata_chopper_spec.rb +82 -0
- data/spec/kitchen/platform_spec.rb +4 -4
- data/spec/kitchen/provisioner/base_spec.rb +65 -125
- data/spec/kitchen/provisioner/chef_base_spec.rb +798 -0
- data/spec/kitchen/provisioner/chef_solo_spec.rb +316 -0
- data/spec/kitchen/provisioner/chef_zero_spec.rb +624 -0
- data/spec/kitchen/provisioner/shell_spec.rb +269 -0
- data/spec/kitchen/provisioner_spec.rb +6 -6
- data/spec/kitchen/shell_out_spec.rb +143 -0
- data/spec/kitchen/ssh_spec.rb +683 -0
- data/spec/kitchen/state_file_spec.rb +28 -21
- data/spec/kitchen/suite_spec.rb +7 -7
- data/spec/kitchen/util_spec.rb +68 -10
- data/spec/kitchen_spec.rb +107 -0
- data/spec/spec_helper.rb +18 -13
- data/support/chef-client-zero.rb +10 -9
- data/support/chef_helpers.sh +16 -0
- data/support/download_helpers.sh +109 -0
- data/test-kitchen.gemspec +42 -33
- metadata +107 -33
@@ -0,0 +1,269 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require_relative "../../spec_helper"
|
20
|
+
|
21
|
+
require "kitchen"
|
22
|
+
require "kitchen/provisioner/shell"
|
23
|
+
|
24
|
+
describe Kitchen::Provisioner::Shell do
|
25
|
+
|
26
|
+
let(:logged_output) { StringIO.new }
|
27
|
+
let(:logger) { Logger.new(logged_output) }
|
28
|
+
|
29
|
+
let(:config) do
|
30
|
+
{ :test_base_path => "/basist", :kitchen_root => "/rooty" }
|
31
|
+
end
|
32
|
+
|
33
|
+
let(:suite) do
|
34
|
+
stub(:name => "fries")
|
35
|
+
end
|
36
|
+
|
37
|
+
let(:instance) do
|
38
|
+
stub(:name => "coolbeans", :logger => logger, :suite => suite)
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:provisioner) do
|
42
|
+
Class.new(Kitchen::Provisioner::Shell) {
|
43
|
+
def calculate_path(path, _opts = {})
|
44
|
+
"<calculated>/#{path}"
|
45
|
+
end
|
46
|
+
}.new(config).finalize_config!(instance)
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "configuration" do
|
50
|
+
|
51
|
+
it ":script uses calculate_path and is expanded" do
|
52
|
+
provisioner[:script].must_equal "/rooty/<calculated>/bootstrap.sh"
|
53
|
+
end
|
54
|
+
|
55
|
+
it ":data_path uses calculate_path and is expanded" do
|
56
|
+
provisioner[:data_path].must_equal "/rooty/<calculated>/data"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#init_command" do
|
61
|
+
|
62
|
+
let(:cmd) { provisioner.init_command }
|
63
|
+
|
64
|
+
it "uses bourne shell" do
|
65
|
+
cmd.must_match(/\Ash -c '$/)
|
66
|
+
cmd.must_match(/'\Z/)
|
67
|
+
end
|
68
|
+
|
69
|
+
it "uses sudo for rm when configured" do
|
70
|
+
config[:sudo] = true
|
71
|
+
|
72
|
+
cmd.must_match regexify("sudo -E rm -rf ", :partial_line)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "does not use sudo for rm when configured" do
|
76
|
+
config[:sudo] = false
|
77
|
+
|
78
|
+
provisioner.init_command.
|
79
|
+
must_match regexify("rm -rf ", :partial_line)
|
80
|
+
provisioner.init_command.
|
81
|
+
wont_match regexify("sudo -E rm -rf ", :partial_line)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "removes the data directory" do
|
85
|
+
config[:root_path] = "/route"
|
86
|
+
|
87
|
+
cmd.must_match %r{rm -rf\b.*\s+/route/data\s+}
|
88
|
+
end
|
89
|
+
|
90
|
+
it "creates :root_path directory" do
|
91
|
+
config[:root_path] = "/root/path"
|
92
|
+
|
93
|
+
cmd.must_match regexify("mkdir -p /root/path", :partial_line)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#run_command" do
|
98
|
+
|
99
|
+
let(:cmd) { provisioner.run_command }
|
100
|
+
|
101
|
+
it "uses bourne shell" do
|
102
|
+
cmd.must_match(/\Ash -c '$/)
|
103
|
+
cmd.must_match(/'\Z/)
|
104
|
+
end
|
105
|
+
|
106
|
+
it "uses sudo for script when configured" do
|
107
|
+
config[:root_path] = "/r"
|
108
|
+
config[:sudo] = true
|
109
|
+
|
110
|
+
cmd.must_match regexify("sudo -E /r/bootstrap.sh", :partial_line)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "does not use sudo for script when configured" do
|
114
|
+
config[:root_path] = "/r"
|
115
|
+
config[:sudo] = false
|
116
|
+
|
117
|
+
cmd.must_match regexify("/r/bootstrap.sh", :partial_line)
|
118
|
+
cmd.wont_match regexify("sudo -E /r/bootstrap.sh", :partial_line)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
describe "#create_sandbox" do
|
123
|
+
|
124
|
+
before do
|
125
|
+
@root = Dir.mktmpdir
|
126
|
+
config[:kitchen_root] = @root
|
127
|
+
end
|
128
|
+
|
129
|
+
after do
|
130
|
+
FileUtils.remove_entry(@root)
|
131
|
+
begin
|
132
|
+
provisioner.cleanup_sandbox
|
133
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
let(:provisioner) do
|
138
|
+
Kitchen::Provisioner::Shell.new(config).finalize_config!(instance)
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "data files" do
|
142
|
+
|
143
|
+
before do
|
144
|
+
create_files_under("#{config[:kitchen_root]}/my_data")
|
145
|
+
config[:data_path] = "#{config[:kitchen_root]}/my_data"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "skips directory creation if :data_path is not set" do
|
149
|
+
config[:data_path] = nil
|
150
|
+
provisioner.create_sandbox
|
151
|
+
|
152
|
+
sandbox_path("data").directory?.must_equal false
|
153
|
+
end
|
154
|
+
|
155
|
+
it "copies tree from :data_path into sandbox" do
|
156
|
+
provisioner.create_sandbox
|
157
|
+
|
158
|
+
sandbox_path("data/alpha.txt").file?.must_equal true
|
159
|
+
IO.read(sandbox_path("data/alpha.txt")).must_equal "stuff"
|
160
|
+
sandbox_path("data/sub").directory?.must_equal true
|
161
|
+
sandbox_path("data/sub/bravo.txt").file?.must_equal true
|
162
|
+
IO.read(sandbox_path("data/sub/bravo.txt")).must_equal "junk"
|
163
|
+
end
|
164
|
+
|
165
|
+
it "logs a message on info" do
|
166
|
+
provisioner.create_sandbox
|
167
|
+
|
168
|
+
logged_output.string.must_match info_line("Preparing data")
|
169
|
+
end
|
170
|
+
|
171
|
+
it "logs a message on debug" do
|
172
|
+
provisioner.create_sandbox
|
173
|
+
|
174
|
+
logged_output.string.must_match debug_line(
|
175
|
+
"Using data from #{config[:kitchen_root]}/my_data")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
describe "script file" do
|
180
|
+
|
181
|
+
describe "with a valid :script file" do
|
182
|
+
|
183
|
+
before do
|
184
|
+
File.open("#{config[:kitchen_root]}/my_script", "wb") do |file|
|
185
|
+
file.write("gonuts")
|
186
|
+
end
|
187
|
+
config[:script] = "#{config[:kitchen_root]}/my_script"
|
188
|
+
end
|
189
|
+
|
190
|
+
it "creates a file in the sandbox directory" do
|
191
|
+
provisioner.create_sandbox
|
192
|
+
|
193
|
+
sandbox_path("my_script").file?.must_equal true
|
194
|
+
sandbox_path("my_script").executable?.must_equal true
|
195
|
+
IO.read(sandbox_path("my_script")).must_equal "gonuts"
|
196
|
+
end
|
197
|
+
|
198
|
+
it "logs a message on info" do
|
199
|
+
provisioner.create_sandbox
|
200
|
+
|
201
|
+
logged_output.string.must_match info_line("Preparing script")
|
202
|
+
end
|
203
|
+
|
204
|
+
it "logs a message on debug" do
|
205
|
+
provisioner.create_sandbox
|
206
|
+
|
207
|
+
logged_output.string.must_match debug_line(
|
208
|
+
"Using script from #{config[:kitchen_root]}/my_script")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
describe "with no :script file" do
|
213
|
+
|
214
|
+
before { config[:script] = nil }
|
215
|
+
|
216
|
+
it "logs a message on info" do
|
217
|
+
provisioner.create_sandbox
|
218
|
+
|
219
|
+
logged_output.string.must_match info_line("Preparing script")
|
220
|
+
end
|
221
|
+
|
222
|
+
it "logs a warning on info" do
|
223
|
+
provisioner.create_sandbox
|
224
|
+
|
225
|
+
logged_output.string.must_match info_line(
|
226
|
+
"bootstrap.sh not found so Kitchen will run a stubbed script. " \
|
227
|
+
"Is this intended?")
|
228
|
+
end
|
229
|
+
|
230
|
+
it "creates a file in the sandbox directory" do
|
231
|
+
provisioner.create_sandbox
|
232
|
+
|
233
|
+
sandbox_path("bootstrap.sh").file?.must_equal true
|
234
|
+
sandbox_path("bootstrap.sh").executable?.must_equal true
|
235
|
+
IO.read(sandbox_path("bootstrap.sh")).
|
236
|
+
must_match(/NO BOOTSTRAP SCRIPT PRESENT/)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def sandbox_path(path)
|
242
|
+
Pathname.new(provisioner.sandbox_path).join(path)
|
243
|
+
end
|
244
|
+
|
245
|
+
def create_files_under(path)
|
246
|
+
FileUtils.mkdir_p(File.join(path, "sub"))
|
247
|
+
File.open(File.join(path, "alpha.txt"), "wb") do |file|
|
248
|
+
file.write("stuff")
|
249
|
+
end
|
250
|
+
File.open(File.join(path, "sub", "bravo.txt"), "wb") do |file|
|
251
|
+
file.write("junk")
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def info_line(msg)
|
256
|
+
%r{^I, .* : #{Regexp.escape(msg)}$}
|
257
|
+
end
|
258
|
+
|
259
|
+
def debug_line(msg)
|
260
|
+
%r{^D, .* : #{Regexp.escape(msg)}$}
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
def regexify(str, line = :whole_line)
|
265
|
+
r = Regexp.escape(str)
|
266
|
+
r = "^\s*#{r}$" if line == :whole_line
|
267
|
+
Regexp.new(r)
|
268
|
+
end
|
269
|
+
end
|
@@ -16,12 +16,12 @@
|
|
16
16
|
# See the License for the specific language governing permissions and
|
17
17
|
# limitations under the License.
|
18
18
|
|
19
|
-
require_relative
|
19
|
+
require_relative "../spec_helper"
|
20
20
|
|
21
|
-
require
|
22
|
-
require
|
23
|
-
require
|
24
|
-
require
|
21
|
+
require "kitchen/errors"
|
22
|
+
require "kitchen/logging"
|
23
|
+
require "kitchen/provisioner"
|
24
|
+
require "kitchen/provisioner/base"
|
25
25
|
|
26
26
|
module Kitchen
|
27
27
|
|
@@ -63,7 +63,7 @@ describe Kitchen::Provisioner do
|
|
63
63
|
# pretend require worked
|
64
64
|
Kitchen::Provisioner.stubs(:require).returns(true)
|
65
65
|
|
66
|
-
proc { Kitchen::Provisioner.for_plugin(
|
66
|
+
proc { Kitchen::Provisioner.for_plugin("nope", {}) }.
|
67
67
|
must_raise Kitchen::ClientError
|
68
68
|
end
|
69
69
|
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require_relative "../spec_helper"
|
20
|
+
|
21
|
+
require "kitchen/errors"
|
22
|
+
require "kitchen/shell_out"
|
23
|
+
require "kitchen/util"
|
24
|
+
|
25
|
+
module Kitchen
|
26
|
+
|
27
|
+
class Shelly
|
28
|
+
|
29
|
+
include Kitchen::ShellOut
|
30
|
+
|
31
|
+
attr_reader :logs
|
32
|
+
|
33
|
+
def debug(msg)
|
34
|
+
@logs ||= []
|
35
|
+
@logs << msg
|
36
|
+
end
|
37
|
+
|
38
|
+
def logger
|
39
|
+
"alogger"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe Kitchen::ShellOut do
|
45
|
+
|
46
|
+
let(:command) do
|
47
|
+
stub(
|
48
|
+
:run_command => true,
|
49
|
+
:error! => true,
|
50
|
+
:stdout => "",
|
51
|
+
:execution_time => 123
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
let(:subject) { Kitchen::Shelly.new }
|
56
|
+
|
57
|
+
describe "#run_command" do
|
58
|
+
|
59
|
+
let(:opts) do
|
60
|
+
{ :live_stream => "alogger", :timeout => 60000 }
|
61
|
+
end
|
62
|
+
|
63
|
+
before do
|
64
|
+
Mixlib::ShellOut.stubs(:new).returns(command)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "builds a Mixlib::ShellOut object with default options" do
|
68
|
+
Mixlib::ShellOut.unstub(:new)
|
69
|
+
Mixlib::ShellOut.expects(:new).with("yoyo", opts).returns(command)
|
70
|
+
|
71
|
+
subject.run_command("yoyo")
|
72
|
+
end
|
73
|
+
|
74
|
+
[:timeout, :cwd, :environment].each do |attr|
|
75
|
+
it "builds a Mixlib::ShellOut object with a custom #{attr}" do
|
76
|
+
opts[attr] = "custom"
|
77
|
+
|
78
|
+
Mixlib::ShellOut.unstub(:new)
|
79
|
+
Mixlib::ShellOut.expects(:new).with("yoyo", opts).returns(command)
|
80
|
+
|
81
|
+
subject.run_command("yoyo", attr => "custom")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "returns the command's standard out" do
|
86
|
+
command.stubs(:stdout).returns("sweetness")
|
87
|
+
|
88
|
+
subject.run_command("icecream").must_equal "sweetness"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "raises a ShellCommandFailed if the command does not cleanly exit" do
|
92
|
+
command.stubs(:error!).
|
93
|
+
raises(Mixlib::ShellOut::ShellCommandFailed, "boom bad")
|
94
|
+
|
95
|
+
err = proc { subject.run_command("boom") }.
|
96
|
+
must_raise Kitchen::ShellOut::ShellCommandFailed
|
97
|
+
err.message.must_equal "boom bad"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "raises a Kitchen::Errror tagged exception for unknown exceptions" do
|
101
|
+
command.stubs(:error!).raises(IOError, "boom bad")
|
102
|
+
|
103
|
+
err = proc { subject.run_command("boom") }.must_raise IOError
|
104
|
+
err.must_be_kind_of Kitchen::Error
|
105
|
+
err.message.must_equal "boom bad"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "prepends with sudo if :use_sudo is truthy" do
|
109
|
+
Mixlib::ShellOut.unstub(:new)
|
110
|
+
Mixlib::ShellOut.expects(:new).with("sudo -E yo", opts).returns(command)
|
111
|
+
|
112
|
+
subject.run_command("yo", :use_sudo => true)
|
113
|
+
end
|
114
|
+
|
115
|
+
it "logs a debug BEGIN message" do
|
116
|
+
subject.run_command("echo whoopa\ndoopa\ndo")
|
117
|
+
|
118
|
+
subject.logs.first.
|
119
|
+
must_equal "[local command] BEGIN (echo whoopa\ndoopa\ndo)"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "logs a debug BEGIN message with custom log subject" do
|
123
|
+
subject.run_command("tenac", :log_subject => "thed")
|
124
|
+
|
125
|
+
subject.logs.first.must_equal "[thed command] BEGIN (tenac)"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "truncates the debug BEGIN command if it spans multiple lines" do
|
129
|
+
end
|
130
|
+
|
131
|
+
it "logs a debug END message" do
|
132
|
+
subject.run_command("echo whoopa doopa")
|
133
|
+
|
134
|
+
subject.logs.last.must_equal "[local command] END (2m3.00s)"
|
135
|
+
end
|
136
|
+
|
137
|
+
it "logs a debug END message with custom log subject" do
|
138
|
+
subject.run_command("tenac", :log_subject => "thed")
|
139
|
+
|
140
|
+
subject.logs.last.must_equal "[thed command] END (2m3.00s)"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,683 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Author:: Fletcher Nichol (<fnichol@nichol.ca>)
|
4
|
+
#
|
5
|
+
# Copyright (C) 2014, Fletcher Nichol
|
6
|
+
#
|
7
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
8
|
+
# you may not use this file except in compliance with the License.
|
9
|
+
# You may obtain a copy of the License at
|
10
|
+
#
|
11
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
12
|
+
#
|
13
|
+
# Unless required by applicable law or agreed to in writing, software
|
14
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
15
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
16
|
+
# See the License for the specific language governing permissions and
|
17
|
+
# limitations under the License.
|
18
|
+
|
19
|
+
require_relative "../spec_helper"
|
20
|
+
|
21
|
+
require "net/ssh/test"
|
22
|
+
|
23
|
+
require "kitchen/ssh"
|
24
|
+
|
25
|
+
# Hack to sort results in `Dir.entries` only within the yielded block, to limit
|
26
|
+
# the "behavior pollution" to other code. This was needed for Net::SCP, as
|
27
|
+
# recursive directory upload doesn't sort the file and directory upload
|
28
|
+
# candidates which leads to different results based on the underlying
|
29
|
+
# filesystem (i.e. lexically sorted, inode insertion, mtime/atime, total
|
30
|
+
# randomness, etc.)
|
31
|
+
#
|
32
|
+
# See: https://github.com/net-ssh/net-scp/blob/a24948/lib/net/scp/upload.rb#L52
|
33
|
+
|
34
|
+
def with_sorted_dir_entries
|
35
|
+
Dir.class_exec do
|
36
|
+
class << self
|
37
|
+
alias_method :__entries__, :entries unless method_defined?(:__entries__)
|
38
|
+
|
39
|
+
def entries(*args)
|
40
|
+
send(:__entries__, *args).sort
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
yield
|
46
|
+
|
47
|
+
Dir.class_exec do
|
48
|
+
class << self
|
49
|
+
alias_method :entries, :__entries__
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Terrible hack to deal with Net::SSH:Test::Extensions which monkey patches
|
55
|
+
# `IO.select` with a version for testing Net::SSH code. Unfortunetly this
|
56
|
+
# impacts other code, so we'll "un-patch" this after each spec and "re-patch"
|
57
|
+
# it before the next one.
|
58
|
+
|
59
|
+
def depatch_io
|
60
|
+
IO.class_exec do
|
61
|
+
class << self
|
62
|
+
alias_method :select, :select_for_real
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def repatch_io
|
68
|
+
IO.class_exec do
|
69
|
+
class << self
|
70
|
+
alias_method :select, :select_for_test
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Major hack-and-a-half to add basic `Channel#request_pty` support to
|
76
|
+
# Net::SSH's testing framework. The `Net::SSH::Test::LocalPacket` does not
|
77
|
+
# recognize the `"pty-req"` request type, so bombs out whenever this channel
|
78
|
+
# request is sent.
|
79
|
+
#
|
80
|
+
# This "make-work" fix adds a method (`#sends_request_pty`) which works just
|
81
|
+
# like `#sends_exec` expcept that it enqueues a patched subclass of
|
82
|
+
# `LocalPacket` which can deal with the `"pty-req"` type.
|
83
|
+
#
|
84
|
+
# An upstream patch to Net::SSH will be required to retire this yak shave ;)
|
85
|
+
|
86
|
+
module Net
|
87
|
+
|
88
|
+
module SSH
|
89
|
+
|
90
|
+
module Test
|
91
|
+
|
92
|
+
class Channel
|
93
|
+
|
94
|
+
def sends_request_pty
|
95
|
+
pty_data = ["xterm", 80, 24, 640, 480, "\0"]
|
96
|
+
|
97
|
+
script.events << Class.new(Net::SSH::Test::LocalPacket) do
|
98
|
+
def types
|
99
|
+
if @type == 98 && @data[1] == "pty-req"
|
100
|
+
@types ||= [
|
101
|
+
:long, :string, :bool, :string,
|
102
|
+
:long, :long, :long, :long, :string
|
103
|
+
]
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end.new(:channel_request, remote_id, "pty-req", false, *pty_data)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe Kitchen::SSH do
|
116
|
+
|
117
|
+
include Net::SSH::Test
|
118
|
+
|
119
|
+
let(:logged_output) { StringIO.new }
|
120
|
+
let(:logger) { Logger.new(logged_output) }
|
121
|
+
let(:opts) { Hash.new }
|
122
|
+
let(:ssh) { Kitchen::SSH.new("foo", "me", opts) }
|
123
|
+
let(:conn) { connection }
|
124
|
+
|
125
|
+
before do
|
126
|
+
repatch_io
|
127
|
+
logger.level = Logger::DEBUG
|
128
|
+
opts[:logger] = logger
|
129
|
+
Net::SSH.stubs(:start).returns(conn)
|
130
|
+
end
|
131
|
+
|
132
|
+
after do
|
133
|
+
depatch_io
|
134
|
+
end
|
135
|
+
|
136
|
+
describe "establishing a connection" do
|
137
|
+
|
138
|
+
[
|
139
|
+
Errno::EACCES, Errno::EADDRINUSE, Errno::ECONNREFUSED,
|
140
|
+
Errno::ECONNRESET, Errno::ENETUNREACH, Errno::EHOSTUNREACH,
|
141
|
+
Net::SSH::Disconnect, Net::SSH::AuthenticationFailed
|
142
|
+
].each do |klass|
|
143
|
+
describe "raising #{klass}" do
|
144
|
+
|
145
|
+
before do
|
146
|
+
Net::SSH.stubs(:start).raises(klass)
|
147
|
+
opts[:ssh_retries] = 3
|
148
|
+
ssh.stubs(:sleep)
|
149
|
+
end
|
150
|
+
|
151
|
+
it "reraises the #{klass} exception" do
|
152
|
+
proc { ssh.exec("nope") }.must_raise klass
|
153
|
+
end
|
154
|
+
|
155
|
+
it "attempts to connect ':ssh_retries' times" do
|
156
|
+
begin
|
157
|
+
ssh.exec("nope")
|
158
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
159
|
+
end
|
160
|
+
|
161
|
+
logged_output.string.lines.select { |l|
|
162
|
+
l =~ debug_line("[SSH] opening connection to me@foo:22<{:ssh_retries=>3}>")
|
163
|
+
}.size.must_equal opts[:ssh_retries]
|
164
|
+
end
|
165
|
+
|
166
|
+
it "sleeps for 1 second between retries" do
|
167
|
+
ssh.unstub(:sleep)
|
168
|
+
ssh.expects(:sleep).with(1).twice
|
169
|
+
|
170
|
+
begin
|
171
|
+
ssh.exec("nope")
|
172
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
it "logs the first 2 retry failures on info" do
|
177
|
+
begin
|
178
|
+
ssh.exec("nope")
|
179
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
180
|
+
end
|
181
|
+
|
182
|
+
logged_output.string.lines.select { |l|
|
183
|
+
l =~ info_line_with("[SSH] connection failed, retrying ")
|
184
|
+
}.size.must_equal 2
|
185
|
+
end
|
186
|
+
|
187
|
+
it "logs the last retry failures on warn" do
|
188
|
+
begin
|
189
|
+
ssh.exec("nope")
|
190
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
191
|
+
end
|
192
|
+
|
193
|
+
logged_output.string.lines.select { |l|
|
194
|
+
l =~ warn_line_with("[SSH] connection failed, terminating ")
|
195
|
+
}.size.must_equal 1
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "#exec" do
|
202
|
+
|
203
|
+
describe "for a successful command" do
|
204
|
+
|
205
|
+
before do
|
206
|
+
story do |script|
|
207
|
+
channel = script.opens_channel
|
208
|
+
channel.sends_request_pty
|
209
|
+
channel.sends_exec("doit")
|
210
|
+
channel.gets_data("ok\n")
|
211
|
+
channel.gets_extended_data("some stderr stuffs\n")
|
212
|
+
channel.gets_exit_status(0)
|
213
|
+
channel.gets_close
|
214
|
+
channel.sends_close
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
it "logger displays command on debug" do
|
219
|
+
assert_scripted { ssh.exec("doit") }
|
220
|
+
|
221
|
+
logged_output.string.must_match debug_line(
|
222
|
+
"[SSH] me@foo:22<{}> (doit)"
|
223
|
+
)
|
224
|
+
end
|
225
|
+
|
226
|
+
it "logger displays establishing connection on debug" do
|
227
|
+
assert_scripted { ssh.exec("doit") }
|
228
|
+
|
229
|
+
logged_output.string.must_match debug_line(
|
230
|
+
"[SSH] opening connection to me@foo:22<{}>"
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
it "logger captures stdout" do
|
235
|
+
assert_scripted { ssh.exec("doit") }
|
236
|
+
|
237
|
+
logged_output.string.must_match(/^ok$/)
|
238
|
+
end
|
239
|
+
|
240
|
+
it "logger captures stderr" do
|
241
|
+
assert_scripted { ssh.exec("doit") }
|
242
|
+
|
243
|
+
logged_output.string.must_match(/^some stderr stuffs$/)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
describe "for a failed command" do
|
248
|
+
|
249
|
+
before do
|
250
|
+
story do |script|
|
251
|
+
channel = script.opens_channel
|
252
|
+
channel.sends_request_pty
|
253
|
+
channel.sends_exec("doit")
|
254
|
+
channel.gets_data("nope\n")
|
255
|
+
channel.gets_extended_data("youdead\n")
|
256
|
+
channel.gets_exit_status(42)
|
257
|
+
channel.gets_close
|
258
|
+
channel.sends_close
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
it "logger displays command on debug" do
|
263
|
+
begin
|
264
|
+
assert_scripted { ssh.exec("doit") }
|
265
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
266
|
+
end
|
267
|
+
|
268
|
+
logged_output.string.must_match debug_line(
|
269
|
+
"[SSH] me@foo:22<{}> (doit)"
|
270
|
+
)
|
271
|
+
end
|
272
|
+
|
273
|
+
it "logger displays establishing connection on debug" do
|
274
|
+
begin
|
275
|
+
assert_scripted { ssh.exec("doit") }
|
276
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
277
|
+
end
|
278
|
+
|
279
|
+
logged_output.string.must_match debug_line(
|
280
|
+
"[SSH] opening connection to me@foo:22<{}>"
|
281
|
+
)
|
282
|
+
end
|
283
|
+
|
284
|
+
it "logger captures stdout" do
|
285
|
+
begin
|
286
|
+
assert_scripted { ssh.exec("doit") }
|
287
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
288
|
+
end
|
289
|
+
|
290
|
+
logged_output.string.must_match(/^nope$/)
|
291
|
+
end
|
292
|
+
|
293
|
+
it "logger captures stderr" do
|
294
|
+
begin
|
295
|
+
assert_scripted { ssh.exec("doit") }
|
296
|
+
rescue # rubocop:disable Lint/HandleExceptions
|
297
|
+
end
|
298
|
+
|
299
|
+
logged_output.string.must_match(/^youdead$/)
|
300
|
+
end
|
301
|
+
|
302
|
+
it "raises an SSHFailed exception" do
|
303
|
+
err = proc { ssh.exec("doit") }.must_raise Kitchen::SSHFailed
|
304
|
+
err.message.must_equal "SSH exited (42) for command: [doit]"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
describe "#upload!" do
|
310
|
+
|
311
|
+
let(:content) { "a" * 1234 }
|
312
|
+
|
313
|
+
let(:src) do
|
314
|
+
file = Tempfile.new("file")
|
315
|
+
file.write("a" * 1234)
|
316
|
+
file.close
|
317
|
+
FileUtils.chmod(0755, file.path)
|
318
|
+
file
|
319
|
+
end
|
320
|
+
|
321
|
+
before do
|
322
|
+
expect_scp_session("-t /tmp/remote") do |channel|
|
323
|
+
channel.gets_data("\0")
|
324
|
+
channel.sends_data("C0755 1234 #{File.basename(src.path)}\n")
|
325
|
+
channel.gets_data("\0")
|
326
|
+
channel.sends_data("a" * 1234)
|
327
|
+
channel.sends_data("\0")
|
328
|
+
channel.gets_data("\0")
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
after do
|
333
|
+
src.unlink
|
334
|
+
end
|
335
|
+
|
336
|
+
it "uploads a file to remote over scp" do
|
337
|
+
assert_scripted do
|
338
|
+
ssh.upload!(src.path, "/tmp/remote")
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
it "logs upload progress to debug" do
|
343
|
+
assert_scripted do
|
344
|
+
ssh.upload!(src.path, "/tmp/remote")
|
345
|
+
end
|
346
|
+
|
347
|
+
logged_output.string.must_match debug_line(
|
348
|
+
"[SSH] opening connection to me@foo:22<{}>"
|
349
|
+
)
|
350
|
+
logged_output.string.must_match debug_line(
|
351
|
+
"Uploaded #{src.path} (1234 bytes)"
|
352
|
+
)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
describe "#upload_path!" do
|
357
|
+
|
358
|
+
before do
|
359
|
+
@dir = Dir.mktmpdir("local")
|
360
|
+
FileUtils.chmod(0700, @dir)
|
361
|
+
File.open("#{@dir}/alpha", "wb") { |f| f.write("alpha-contents\n") }
|
362
|
+
FileUtils.chmod(0644, "#{@dir}/alpha")
|
363
|
+
FileUtils.mkdir_p("#{@dir}/subdir")
|
364
|
+
FileUtils.chmod(0755, "#{@dir}/subdir")
|
365
|
+
File.open("#{@dir}/subdir/beta", "wb") { |f| f.write("beta-contents\n") }
|
366
|
+
FileUtils.chmod(0555, "#{@dir}/subdir/beta")
|
367
|
+
File.open("#{@dir}/zulu", "wb") { |f| f.write("zulu-contents\n") }
|
368
|
+
FileUtils.chmod(0444, "#{@dir}/zulu")
|
369
|
+
|
370
|
+
expect_scp_session("-t -r /tmp/remote") do |channel|
|
371
|
+
channel.gets_data("\0")
|
372
|
+
channel.sends_data("D0700 0 #{File.basename(@dir)}\n")
|
373
|
+
channel.gets_data("\0")
|
374
|
+
channel.sends_data("C0644 15 alpha\n")
|
375
|
+
channel.gets_data("\0")
|
376
|
+
channel.sends_data("alpha-contents\n")
|
377
|
+
channel.sends_data("\0")
|
378
|
+
channel.gets_data("\0")
|
379
|
+
channel.sends_data("D0755 0 subdir\n")
|
380
|
+
channel.gets_data("\0")
|
381
|
+
channel.sends_data("C0555 14 beta\n")
|
382
|
+
channel.gets_data("\0")
|
383
|
+
channel.sends_data("beta-contents\n")
|
384
|
+
channel.sends_data("\0")
|
385
|
+
channel.gets_data("\0")
|
386
|
+
channel.sends_data("E\n")
|
387
|
+
channel.gets_data("\0")
|
388
|
+
channel.sends_data("C0444 14 zulu\n")
|
389
|
+
channel.gets_data("\0")
|
390
|
+
channel.sends_data("zulu-contents\n")
|
391
|
+
channel.sends_data("\0")
|
392
|
+
channel.gets_data("\0")
|
393
|
+
channel.sends_data("E\n")
|
394
|
+
channel.gets_data("\0")
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
after do
|
399
|
+
FileUtils.remove_entry_secure(@dir)
|
400
|
+
end
|
401
|
+
|
402
|
+
it "uploads a file to remote over scp" do
|
403
|
+
with_sorted_dir_entries do
|
404
|
+
assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
it "logs upload progress to debug" do
|
409
|
+
remote_base = "/tmp/#{File.basename(@dir)}"
|
410
|
+
|
411
|
+
with_sorted_dir_entries do
|
412
|
+
assert_scripted { ssh.upload_path!(@dir, "/tmp/remote") }
|
413
|
+
end
|
414
|
+
|
415
|
+
logged_output.string.must_match debug_line(
|
416
|
+
"[SSH] opening connection to me@foo:22<{}>"
|
417
|
+
)
|
418
|
+
logged_output.string.must_match debug_line(
|
419
|
+
"Uploaded #{remote_base}/alpha (15 bytes)"
|
420
|
+
)
|
421
|
+
logged_output.string.must_match debug_line(
|
422
|
+
"Uploaded #{remote_base}/subdir/beta (14 bytes)"
|
423
|
+
)
|
424
|
+
logged_output.string.must_match debug_line(
|
425
|
+
"Uploaded #{remote_base}/zulu (14 bytes)"
|
426
|
+
)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
describe "#shutdown" do
|
431
|
+
|
432
|
+
before do
|
433
|
+
story do |script|
|
434
|
+
channel = script.opens_channel
|
435
|
+
channel.sends_request_pty
|
436
|
+
channel.sends_exec("doit")
|
437
|
+
channel.gets_data("ok\n")
|
438
|
+
channel.gets_exit_status(0)
|
439
|
+
channel.gets_close
|
440
|
+
channel.sends_close
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
it "logger displays closing connection on debug" do
|
445
|
+
conn.expects(:shutdown!)
|
446
|
+
|
447
|
+
assert_scripted do
|
448
|
+
ssh.exec("doit")
|
449
|
+
ssh.shutdown
|
450
|
+
end
|
451
|
+
|
452
|
+
logged_output.string.must_match debug_line(
|
453
|
+
"[SSH] closing connection to me@foo:22<{}>"
|
454
|
+
)
|
455
|
+
end
|
456
|
+
|
457
|
+
it "only closes the connection once for multiple calls" do
|
458
|
+
conn.expects(:shutdown!).once
|
459
|
+
|
460
|
+
assert_scripted do
|
461
|
+
ssh.exec("doit")
|
462
|
+
ssh.shutdown
|
463
|
+
ssh.shutdown
|
464
|
+
ssh.shutdown
|
465
|
+
end
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
describe "block form" do
|
470
|
+
|
471
|
+
before do
|
472
|
+
story do |script|
|
473
|
+
channel = script.opens_channel
|
474
|
+
channel.sends_request_pty
|
475
|
+
channel.sends_exec("doit")
|
476
|
+
channel.gets_data("ok\n")
|
477
|
+
channel.gets_exit_status(0)
|
478
|
+
channel.gets_close
|
479
|
+
channel.sends_close
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
it "shuts down the connection when block closes" do
|
484
|
+
conn.expects(:shutdown!)
|
485
|
+
|
486
|
+
Kitchen::SSH.new("foo", "me", opts) do |ssh|
|
487
|
+
ssh.exec("doit")
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
describe "#login_command" do
|
493
|
+
|
494
|
+
let(:login_command) { ssh.login_command }
|
495
|
+
let(:cmd) { login_command.cmd_array.join(" ") }
|
496
|
+
|
497
|
+
it "returns a LoginCommand" do
|
498
|
+
login_command.must_be_instance_of Kitchen::LoginCommand
|
499
|
+
end
|
500
|
+
|
501
|
+
it "is an SSH command" do
|
502
|
+
cmd.must_match %r{^ssh }
|
503
|
+
cmd.must_match %r{ me@foo$}
|
504
|
+
end
|
505
|
+
|
506
|
+
it "sets the UserKnownHostsFile option" do
|
507
|
+
cmd.must_match regexify(" -o UserKnownHostsFile=/dev/null ")
|
508
|
+
end
|
509
|
+
|
510
|
+
it "sets the StrictHostKeyChecking option" do
|
511
|
+
cmd.must_match regexify(" -o StrictHostKeyChecking=no ")
|
512
|
+
end
|
513
|
+
|
514
|
+
it "won't set IdentitiesOnly option by default" do
|
515
|
+
cmd.wont_match regexify(" -o IdentitiesOnly=")
|
516
|
+
end
|
517
|
+
|
518
|
+
it "sets the IdentiesOnly option if :keys option is given" do
|
519
|
+
opts[:keys] = ["yep"]
|
520
|
+
|
521
|
+
cmd.must_match regexify(" -o IdentitiesOnly=yes ")
|
522
|
+
end
|
523
|
+
|
524
|
+
it "sets the LogLevel option to VERBOSE if logger is set to debug" do
|
525
|
+
logger.level = ::Logger::DEBUG
|
526
|
+
opts[:logger] = logger
|
527
|
+
|
528
|
+
cmd.must_match regexify(" -o LogLevel=VERBOSE ")
|
529
|
+
end
|
530
|
+
|
531
|
+
it "sets the LogLevel option to ERROR if logger is not set to debug" do
|
532
|
+
logger.level = ::Logger::INFO
|
533
|
+
opts[:logger] = logger
|
534
|
+
|
535
|
+
cmd.must_match regexify(" -o LogLevel=ERROR ")
|
536
|
+
end
|
537
|
+
|
538
|
+
it "won't set the ForwardAgent option by default" do
|
539
|
+
cmd.wont_match regexify(" -o ForwardAgent=")
|
540
|
+
end
|
541
|
+
|
542
|
+
it "sets the ForwardAgent option to yes if truthy" do
|
543
|
+
opts[:forward_agent] = "yep"
|
544
|
+
|
545
|
+
cmd.must_match regexify(" -o ForwardAgent=yes")
|
546
|
+
end
|
547
|
+
|
548
|
+
it "sets the ForwardAgent option to no if falsey" do
|
549
|
+
opts[:forward_agent] = false
|
550
|
+
|
551
|
+
cmd.must_match regexify(" -o ForwardAgent=no")
|
552
|
+
end
|
553
|
+
|
554
|
+
it "won't add any SSH keys by default" do
|
555
|
+
cmd.wont_match regexify(" -i ")
|
556
|
+
end
|
557
|
+
|
558
|
+
it "sets SSH keys options if given" do
|
559
|
+
opts[:keys] = %w[one two]
|
560
|
+
|
561
|
+
cmd.must_match regexify(" -i one ")
|
562
|
+
cmd.must_match regexify(" -i two ")
|
563
|
+
end
|
564
|
+
|
565
|
+
it "sets the port option to 22 by default" do
|
566
|
+
cmd.must_match regexify(" -p 22 ")
|
567
|
+
end
|
568
|
+
|
569
|
+
it "sets the port option" do
|
570
|
+
opts[:port] = 1234
|
571
|
+
|
572
|
+
cmd.must_match regexify(" -p 1234 ")
|
573
|
+
end
|
574
|
+
end
|
575
|
+
|
576
|
+
describe "#test_ssh" do
|
577
|
+
|
578
|
+
let(:tcp_socket) { stub(:select_for_read? => true, :close => true) }
|
579
|
+
|
580
|
+
before { ssh.stubs(:sleep) }
|
581
|
+
|
582
|
+
it "returns a truthy value" do
|
583
|
+
TCPSocket.stubs(:new).returns(tcp_socket)
|
584
|
+
|
585
|
+
result = ssh.send(:test_ssh)
|
586
|
+
result.wont_equal nil
|
587
|
+
result.wont_equal false
|
588
|
+
end
|
589
|
+
|
590
|
+
it "closes socket when finished" do
|
591
|
+
TCPSocket.stubs(:new).returns(tcp_socket)
|
592
|
+
tcp_socket.expects(:close)
|
593
|
+
|
594
|
+
ssh.send(:test_ssh)
|
595
|
+
end
|
596
|
+
|
597
|
+
[
|
598
|
+
SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH,
|
599
|
+
Errno::ENETUNREACH, IOError
|
600
|
+
].each do |klass|
|
601
|
+
describe "when #{klass} is raised" do
|
602
|
+
|
603
|
+
before { TCPSocket.stubs(:new).raises(klass) }
|
604
|
+
|
605
|
+
it "returns false" do
|
606
|
+
ssh.send(:test_ssh).must_equal false
|
607
|
+
end
|
608
|
+
|
609
|
+
it "sleeps for 2 seconds" do
|
610
|
+
ssh.expects(:sleep).with(2)
|
611
|
+
|
612
|
+
ssh.send(:test_ssh)
|
613
|
+
end
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
[
|
618
|
+
Errno::EPERM, Errno::ETIMEDOUT
|
619
|
+
].each do |klass|
|
620
|
+
describe "when #{klass} is raised" do
|
621
|
+
|
622
|
+
it "returns false when #{klass} is raised" do
|
623
|
+
TCPSocket.stubs(:new).raises(klass)
|
624
|
+
|
625
|
+
ssh.send(:test_ssh).must_equal false
|
626
|
+
end
|
627
|
+
end
|
628
|
+
end
|
629
|
+
end
|
630
|
+
|
631
|
+
describe "#wait" do
|
632
|
+
|
633
|
+
let(:not_ready) do
|
634
|
+
stub(:select_for_read? => false, :idle! => true, :close => true)
|
635
|
+
end
|
636
|
+
|
637
|
+
let(:ready) do
|
638
|
+
stub(:select_for_read? => true, :close => true)
|
639
|
+
end
|
640
|
+
|
641
|
+
it "logs to info for each retry" do
|
642
|
+
TCPSocket.stubs(:new).returns(not_ready, not_ready, ready)
|
643
|
+
ssh.wait
|
644
|
+
|
645
|
+
logged_output.string.lines.select { |l|
|
646
|
+
l =~ info_line_with("Waiting for foo:22...")
|
647
|
+
}.size.must_equal 2
|
648
|
+
end
|
649
|
+
end
|
650
|
+
|
651
|
+
def expect_scp_session(args)
|
652
|
+
story do |script|
|
653
|
+
channel = script.opens_channel
|
654
|
+
channel.sends_exec("scp #{args}")
|
655
|
+
yield channel if block_given?
|
656
|
+
channel.sends_eof
|
657
|
+
channel.gets_exit_status(0)
|
658
|
+
channel.gets_eof
|
659
|
+
channel.gets_close
|
660
|
+
channel.sends_close
|
661
|
+
end
|
662
|
+
end
|
663
|
+
|
664
|
+
def regexify(string)
|
665
|
+
Regexp.new(Regexp.escape(string))
|
666
|
+
end
|
667
|
+
|
668
|
+
def debug_line(msg)
|
669
|
+
%r{^D, .* : #{Regexp.escape(msg)}$}
|
670
|
+
end
|
671
|
+
|
672
|
+
def debug_line_with(msg)
|
673
|
+
%r{^D, .* : #{Regexp.escape(msg)}}
|
674
|
+
end
|
675
|
+
|
676
|
+
def info_line_with(msg)
|
677
|
+
%r{^I, .* : #{Regexp.escape(msg)}}
|
678
|
+
end
|
679
|
+
|
680
|
+
def warn_line_with(msg)
|
681
|
+
%r{^W, .* : #{Regexp.escape(msg)}}
|
682
|
+
end
|
683
|
+
end
|