test-kitchen 1.7.0 → 1.7.1.dev

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