minestrone 0.0.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.
Files changed (96) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +32 -0
  3. data/.gitignore +5 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +35 -0
  7. data/Rakefile +10 -0
  8. data/bin/capify +89 -0
  9. data/bin/min +5 -0
  10. data/docs/lib-codebase-map.md +162 -0
  11. data/docs/lib-dependency-graph.svg +129 -0
  12. data/lib/minestrone/callback.rb +45 -0
  13. data/lib/minestrone/cli/help.rb +131 -0
  14. data/lib/minestrone/cli/help.txt +72 -0
  15. data/lib/minestrone/cli/options.rb +232 -0
  16. data/lib/minestrone/cli.rb +159 -0
  17. data/lib/minestrone/command.rb +177 -0
  18. data/lib/minestrone/configuration/actions/file_transfer.rb +53 -0
  19. data/lib/minestrone/configuration/actions/inspect.rb +46 -0
  20. data/lib/minestrone/configuration/actions/invocation.rb +202 -0
  21. data/lib/minestrone/configuration/alias_task.rb +29 -0
  22. data/lib/minestrone/configuration/callbacks.rb +129 -0
  23. data/lib/minestrone/configuration/connections.rb +66 -0
  24. data/lib/minestrone/configuration/execution.rb +139 -0
  25. data/lib/minestrone/configuration/loading.rb +207 -0
  26. data/lib/minestrone/configuration/log_formatters.rb +75 -0
  27. data/lib/minestrone/configuration/namespaces.rb +225 -0
  28. data/lib/minestrone/configuration/servers.rb +70 -0
  29. data/lib/minestrone/configuration/variables.rb +115 -0
  30. data/lib/minestrone/configuration.rb +69 -0
  31. data/lib/minestrone/errors.rb +17 -0
  32. data/lib/minestrone/ext/string.rb +7 -0
  33. data/lib/minestrone/extensions.rb +56 -0
  34. data/lib/minestrone/logger.rb +171 -0
  35. data/lib/minestrone/processable.rb +50 -0
  36. data/lib/minestrone/recipes/deploy/assets.rb +194 -0
  37. data/lib/minestrone/recipes/deploy/bundler.rb +81 -0
  38. data/lib/minestrone/recipes/deploy/dependencies.rb +44 -0
  39. data/lib/minestrone/recipes/deploy/local_dependency.rb +45 -0
  40. data/lib/minestrone/recipes/deploy/remote_dependency.rb +119 -0
  41. data/lib/minestrone/recipes/deploy/scm/base.rb +204 -0
  42. data/lib/minestrone/recipes/deploy/scm/git.rb +284 -0
  43. data/lib/minestrone/recipes/deploy/scm/none.rb +54 -0
  44. data/lib/minestrone/recipes/deploy/scm.rb +22 -0
  45. data/lib/minestrone/recipes/deploy/strategy/base.rb +87 -0
  46. data/lib/minestrone/recipes/deploy/strategy/copy.rb +353 -0
  47. data/lib/minestrone/recipes/deploy/strategy/remote_cache.rb +80 -0
  48. data/lib/minestrone/recipes/deploy/strategy.rb +22 -0
  49. data/lib/minestrone/recipes/deploy.rb +639 -0
  50. data/lib/minestrone/recipes/standard.rb +23 -0
  51. data/lib/minestrone/recipes/templates/maintenance.rhtml +53 -0
  52. data/lib/minestrone/server_definition.rb +56 -0
  53. data/lib/minestrone/ssh.rb +81 -0
  54. data/lib/minestrone/task_definition.rb +82 -0
  55. data/lib/minestrone/transfer.rb +205 -0
  56. data/lib/minestrone/version.rb +11 -0
  57. data/lib/minestrone.rb +3 -0
  58. data/minestrone.gemspec +32 -0
  59. data/test/cli/execute_test.rb +130 -0
  60. data/test/cli/help_test.rb +178 -0
  61. data/test/cli/options_test.rb +315 -0
  62. data/test/cli/ui_test.rb +26 -0
  63. data/test/cli_test.rb +17 -0
  64. data/test/command_test.rb +305 -0
  65. data/test/configuration/actions/file_transfer_test.rb +61 -0
  66. data/test/configuration/actions/inspect_test.rb +76 -0
  67. data/test/configuration/actions/invocation_test.rb +258 -0
  68. data/test/configuration/alias_task_test.rb +110 -0
  69. data/test/configuration/callbacks_test.rb +201 -0
  70. data/test/configuration/connections_test.rb +192 -0
  71. data/test/configuration/execution_test.rb +176 -0
  72. data/test/configuration/loading_test.rb +149 -0
  73. data/test/configuration/namespace_dsl_test.rb +325 -0
  74. data/test/configuration/servers_test.rb +100 -0
  75. data/test/configuration/variables_test.rb +191 -0
  76. data/test/configuration_test.rb +77 -0
  77. data/test/deploy/local_dependency_test.rb +61 -0
  78. data/test/deploy/remote_dependency_test.rb +146 -0
  79. data/test/deploy/scm/base_test.rb +55 -0
  80. data/test/deploy/scm/git_test.rb +260 -0
  81. data/test/deploy/scm/none_test.rb +26 -0
  82. data/test/deploy/strategy/copy_test.rb +360 -0
  83. data/test/extensions_test.rb +69 -0
  84. data/test/fixtures/cli_integration.rb +5 -0
  85. data/test/fixtures/config.rb +4 -0
  86. data/test/fixtures/custom.rb +3 -0
  87. data/test/logger_formatting_test.rb +149 -0
  88. data/test/logger_test.rb +134 -0
  89. data/test/recipes_test.rb +26 -0
  90. data/test/server_definition_test.rb +121 -0
  91. data/test/ssh_test.rb +99 -0
  92. data/test/task_definition_test.rb +117 -0
  93. data/test/transfer_test.rb +172 -0
  94. data/test/utils.rb +28 -0
  95. data/test/version_test.rb +11 -0
  96. metadata +258 -0
@@ -0,0 +1,305 @@
1
+ require "utils"
2
+ require 'minestrone/command'
3
+ require 'minestrone/configuration'
4
+
5
+ class CommandTest < Test::Unit::TestCase
6
+ class FakeChannel < Hash
7
+ def exec(*); end
8
+ def send_data(*); end
9
+ def eof!; end
10
+ def close; end
11
+ def request_pty(*); end
12
+ def on_data(*); end
13
+ def on_extended_data(*); end
14
+ def on_request(*); end
15
+ def on_close(*); end
16
+ end
17
+
18
+ def test_command_should_keep_session
19
+ session = mock_session
20
+ assert_equal session, Minestrone::Command.new("ls", session).session
21
+ end
22
+
23
+ def test_command_with_newlines_should_be_properly_escaped
24
+ cmd = Minestrone::Command.new("ls\necho", mock_session)
25
+ assert_equal "ls\\\necho", cmd.command
26
+ end
27
+
28
+ def test_command_with_crlf_newlines_should_be_properly_escaped
29
+ cmd = Minestrone::Command.new("ls\r\necho", mock_session)
30
+ assert_equal "ls\\\necho", cmd.command
31
+ end
32
+
33
+ def test_command_with_pty_should_request_pty_and_register_success_callback
34
+ session = setup_for_extracting_channel_action(:request_pty, true) do |ch|
35
+ ch.expects(:exec).with(%(sh -c 'ls'))
36
+ end
37
+ open_test_channel("ls", session, :pty => true)
38
+ end
39
+
40
+ def test_command_with_env_key_should_have_environment_constructed_and_prepended
41
+ session = setup_for_extracting_channel_action do |ch|
42
+ ch.expects(:request_pty).never
43
+ ch.expects(:exec).with(%(env FOO=bar sh -c 'ls'))
44
+ end
45
+ open_test_channel("ls", session, :env => { "FOO" => "bar" })
46
+ end
47
+
48
+ def test_env_with_symbolic_key_should_be_accepted_as_a_string
49
+ session = setup_for_extracting_channel_action do |ch|
50
+ ch.expects(:exec).with(%(env FOO=bar sh -c 'ls'))
51
+ end
52
+ open_test_channel("ls", session, :env => { :FOO => "bar" })
53
+ end
54
+
55
+ def test_env_as_string_should_be_substituted_in_directly
56
+ session = setup_for_extracting_channel_action do |ch|
57
+ ch.expects(:exec).with(%(env HOWDY=there sh -c 'ls'))
58
+ end
59
+ open_test_channel("ls", session, :env => "HOWDY=there")
60
+ end
61
+
62
+ def test_env_with_symbolic_value_should_be_accepted_as_string
63
+ session = setup_for_extracting_channel_action do |ch|
64
+ ch.expects(:exec).with(%(env FOO=bar sh -c 'ls'))
65
+ end
66
+ open_test_channel("ls", session, :env => { "FOO" => :bar })
67
+ end
68
+
69
+ def test_env_value_should_be_escaped
70
+ session = setup_for_extracting_channel_action do |ch|
71
+ ch.expects(:exec).with(%(env FOO=(\\ \\\"bar\\\"\\ ) sh -c 'ls'))
72
+ end
73
+ open_test_channel("ls", session, :env => { "FOO" => '( "bar" )' })
74
+ end
75
+
76
+ def test_env_with_multiple_keys_should_chain_the_entries_together
77
+ session = setup_for_extracting_channel_action do |ch|
78
+ ch.expects(:exec).with do |command|
79
+ command =~ /^env / &&
80
+ command =~ /\ba=b\b/ &&
81
+ command =~ /\bc=d\b/ &&
82
+ command =~ /\be=f\b/ &&
83
+ command =~ / sh -c 'ls'$/
84
+ end
85
+ end
86
+ open_test_channel("ls", session, :env => { :a => :b, :c => :d, :e => :f })
87
+ end
88
+
89
+ def test_open_channel_should_set_host_key_on_channel
90
+ channel = nil
91
+ session = setup_for_extracting_channel_action { |ch| channel = ch }
92
+ open_test_channel("ls", session)
93
+ assert_equal "minestrone", channel[:host]
94
+ end
95
+
96
+ def test_open_channel_should_set_options_key_on_channel
97
+ channel = nil
98
+ session = setup_for_extracting_channel_action { |ch| channel = ch }
99
+ open_test_channel("ls", session, :data => "here we go")
100
+ assert_equal({ :data => 'here we go' }, channel[:options])
101
+ end
102
+
103
+ def test_successful_channel_should_send_command
104
+ session = setup_for_extracting_channel_action do |ch|
105
+ ch.expects(:exec).with(%(sh -c 'ls'))
106
+ end
107
+ open_test_channel("ls", session)
108
+ end
109
+
110
+ def test_successful_channel_with_shell_option_should_send_command_via_specified_shell
111
+ session = setup_for_extracting_channel_action do |ch|
112
+ ch.expects(:exec).with(%(/bin/bash -c 'ls'))
113
+ end
114
+ open_test_channel("ls", session, :shell => "/bin/bash")
115
+ end
116
+
117
+ def test_successful_channel_with_shell_false_should_send_command_without_shell
118
+ session = setup_for_extracting_channel_action do |ch|
119
+ ch.expects(:exec).with(%(echo `hostname`))
120
+ end
121
+ open_test_channel("echo `hostname`", session, :shell => false)
122
+ end
123
+
124
+ def test_successful_channel_should_send_data_if_data_key_is_present
125
+ session = setup_for_extracting_channel_action do |ch|
126
+ ch.expects(:exec).with(%(sh -c 'ls'))
127
+ ch.expects(:send_data).with("here we go")
128
+ end
129
+ open_test_channel("ls", session, :data => "here we go")
130
+ end
131
+
132
+ def test_unsuccessful_pty_request_should_close_channel
133
+ session = setup_for_extracting_channel_action(:request_pty, false) do |ch|
134
+ ch.expects(:close)
135
+ end
136
+ open_test_channel("ls", session, :pty => true)
137
+ end
138
+
139
+ def test_on_data_should_invoke_callback_as_stdout
140
+ session = setup_for_extracting_channel_action(:on_data, "hello")
141
+ called = false
142
+ open_test_channel("ls", session) do |ch, stream, data|
143
+ called = true
144
+ assert_equal :out, stream
145
+ assert_equal "hello", data
146
+ end
147
+ assert called
148
+ end
149
+
150
+ def test_on_extended_data_should_invoke_callback_as_stderr
151
+ session = setup_for_extracting_channel_action(:on_extended_data, 2, "hello")
152
+ called = false
153
+ open_test_channel("ls", session) do |ch, stream, data|
154
+ called = true
155
+ assert_equal :err, stream
156
+ assert_equal "hello", data
157
+ end
158
+ assert called
159
+ end
160
+
161
+ def test_on_request_should_record_exit_status
162
+ data = mock(:read_long => 5)
163
+ channel = nil
164
+ session = setup_for_extracting_channel_action([:on_request, "exit-status"], data) { |ch| channel = ch }
165
+ open_test_channel("ls", session)
166
+ assert_equal 5, channel[:status]
167
+ end
168
+
169
+ def test_on_request_should_log_exit_signal_if_logger_present
170
+ data = mock(:read_string => "TERM")
171
+ logger = stub_everything
172
+
173
+ session = setup_for_extracting_channel_action([:on_request, "exit-signal"], data)
174
+ logger.expects(:important).with("command received signal TERM", server("minestrone"))
175
+
176
+ open_test_channel("puppet", session, :logger => logger)
177
+ end
178
+
179
+ def test_on_close_should_set_channel_closed
180
+ channel = nil
181
+ session = setup_for_extracting_channel_action(:on_close) { |ch| channel = ch }
182
+ open_test_channel("ls", session)
183
+ assert channel[:closed]
184
+ end
185
+
186
+ def test_stop_should_close_open_channel
187
+ cmd = Minestrone::Command.new("ls", mock_session)
188
+ cmd.send(:open_channel, mock_session(new_channel(false)))
189
+ cmd.stop!
190
+ end
191
+
192
+ def test_process_should_return_cleanly_if_channel_has_zero_exit_status
193
+ cmd = Minestrone::Command.new("ls", mock_session(new_channel(true, 0)))
194
+ assert_nothing_raised { cmd.process! }
195
+ end
196
+
197
+ def test_process_should_raise_error_if_channel_has_non_zero_exit_status
198
+ cmd = Minestrone::Command.new("ls", mock_session(new_channel(true, 1)))
199
+ assert_raises(Minestrone::CommandError) { cmd.process! }
200
+ end
201
+
202
+ def test_command_error_should_include_accessor_with_host
203
+ cmd = Minestrone::Command.new("ls", mock_session(new_channel(true, 1)))
204
+
205
+ begin
206
+ cmd.process!
207
+ flunk "expected an exception to be raised"
208
+ rescue Minestrone::CommandError => e
209
+ assert e.respond_to?(:host)
210
+ assert_equal "minestrone", e.host.to_s
211
+ end
212
+ end
213
+
214
+ def test_process_should_loop_until_channel_is_closed
215
+ ch = mock("channel")
216
+ ch.stubs(:to_ary)
217
+ ch.stubs(:[]).with(:closed).returns(false, false, false, true)
218
+ ch.expects(:[]).with(:status).returns(0)
219
+ cmd = Minestrone::Command.new("ls", mock_session(ch))
220
+ assert_nothing_raised do
221
+ cmd.process!
222
+ end
223
+ end
224
+
225
+ def test_process_should_instantiate_command_and_process!
226
+ cmd = mock("command", :process! => nil)
227
+ session = mock_session
228
+ Minestrone::Command.expects(:new).with("ls -l", session, {:foo => "bar"}).returns(cmd)
229
+ Minestrone::Command.process("ls -l", session, :foo => "bar")
230
+ end
231
+
232
+ def test_process_with_host_placeholder_should_substitute_host_placeholder_with_each_host
233
+ session = setup_for_extracting_channel_action do |ch|
234
+ ch.expects(:exec).with(%(sh -c 'echo minestrone'))
235
+ end
236
+ open_test_channel("echo $CAPISTRANO:HOST$", session)
237
+ end
238
+
239
+ def test_process_with_unknown_placeholder_should_not_replace_placeholder
240
+ session = setup_for_extracting_channel_action do |ch|
241
+ ch.expects(:exec).with(%(sh -c 'echo $CAPISTRANO:OTHER$'))
242
+ end
243
+ open_test_channel("echo $CAPISTRANO:OTHER$", session)
244
+ end
245
+
246
+ def test_input_stream_closed_when_eof_option_is_true
247
+ channel = nil
248
+ session = setup_for_extracting_channel_action { |ch| channel = ch }
249
+ channel.expects(:eof!)
250
+ open_test_channel("cat", session, :data => "here we go", :eof => true)
251
+ assert_equal({ :data => 'here we go', :eof => true }, channel[:options])
252
+ end
253
+
254
+ private
255
+
256
+ def mock_session(channel = nil)
257
+ stub('session',
258
+ :open_channel => channel,
259
+ :preprocess => true,
260
+ :postprocess => true,
261
+ :listeners => {},
262
+ :xserver => server("minestrone"))
263
+ end
264
+
265
+ class MockChannel < Hash
266
+ def close
267
+ end
268
+ end
269
+
270
+ def new_channel(closed, status = nil)
271
+ ch = MockChannel.new
272
+ ch.update({ :closed => closed, :host => "minestrone", :server => server("minestrone") })
273
+ ch[:status] = status if status
274
+ ch.expects(:close) unless closed
275
+ ch
276
+ end
277
+
278
+ def setup_for_extracting_channel_action(action = nil, *args)
279
+ s = server("minestrone")
280
+ session = mock("session", :xserver => s)
281
+
282
+ channel = FakeChannel.new
283
+ session.expects(:open_channel).yields(channel)
284
+
285
+ channel.stubs(:on_data)
286
+ channel.stubs(:on_extended_data)
287
+ channel.stubs(:on_request)
288
+ channel.stubs(:on_close)
289
+
290
+ if action
291
+ action = Array(action)
292
+ channel.expects(action.first).with(*action[1..-1]).yields(channel, *args)
293
+ end
294
+
295
+ yield channel if block_given?
296
+
297
+ session
298
+ end
299
+
300
+ def open_test_channel(command, session, options = {}, &block)
301
+ cmd = Minestrone::Command.new(command, session, options, &block)
302
+ cmd.send(:open_channel, session)
303
+ cmd
304
+ end
305
+ end
@@ -0,0 +1,61 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/actions/file_transfer'
3
+
4
+ class ConfigurationActionsFileTransferTest < Test::Unit::TestCase
5
+ class MockConfig
6
+ include Minestrone::Configuration::Actions::FileTransfer
7
+ attr_accessor :session, :dry_run
8
+ end
9
+
10
+ def setup
11
+ @config = MockConfig.new
12
+ @config.stubs(:logger).returns(stub_everything)
13
+ end
14
+
15
+ def test_put_should_delegate_to_upload
16
+ @config.expects(:upload).with { |from, to, opts|
17
+ from.string == "some data" && to == "test.txt" && opts == { :mode => 0777 } }
18
+ @config.expects(:run).never
19
+ @config.put("some data", "test.txt", :mode => 0777)
20
+ end
21
+
22
+ def test_get_should_delegate_to_download
23
+ @config.expects(:download).with("testr.txt", "testl.txt", { :foo => "bar" })
24
+ @config.get("testr.txt", "testl.txt", :foo => "bar")
25
+ end
26
+
27
+ def test_upload_should_delegate_to_transfer
28
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", { :foo => "bar" })
29
+ @config.upload("testl.txt", "testr.txt", :foo => "bar")
30
+ end
31
+
32
+ def test_upload_without_mode_should_not_try_to_chmod
33
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", { :foo => "bar" })
34
+ @config.expects(:run).never
35
+ @config.upload("testl.txt", "testr.txt", :foo => "bar")
36
+ end
37
+
38
+ def test_upload_with_mode_should_try_to_chmod
39
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", { :foo => "bar" })
40
+ @config.expects(:run).with("chmod 775 testr.txt", {:foo => "bar"})
41
+ @config.upload("testl.txt", "testr.txt", :mode => 0775, :foo => "bar")
42
+ end
43
+
44
+ def test_upload_with_symbolic_mode_should_try_to_chmod
45
+ @config.expects(:transfer).with(:up, "testl.txt", "testr.txt", { :foo => "bar" })
46
+ @config.expects(:run).with("chmod g+w testr.txt", {:foo => "bar"})
47
+ @config.upload("testl.txt", "testr.txt", :mode => "g+w", :foo => "bar")
48
+ end
49
+
50
+ def test_download_should_delegate_to_transfer
51
+ @config.expects(:transfer).with(:down, "testr.txt", "testl.txt", { :foo => "bar" })
52
+ @config.download("testr.txt", "testl.txt", :foo => "bar")
53
+ end
54
+
55
+ def test_transfer_should_invoke_transfer_on_the_configured_server
56
+ @config.session = 1
57
+ @config.expects(:execute_on_server).yields
58
+ Minestrone::Transfer.expects(:process).with(:up, "testl.txt", "testr.txt", 1, {:foo => "bar", :logger => @config.logger})
59
+ @config.transfer(:up, "testl.txt", "testr.txt", :foo => "bar")
60
+ end
61
+ end
@@ -0,0 +1,76 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/actions/inspect'
3
+
4
+ class ConfigurationActionsInspectTest < Test::Unit::TestCase
5
+ class MockConfig
6
+ include Minestrone::Configuration::Actions::Inspect
7
+ end
8
+
9
+ def setup
10
+ @config = MockConfig.new
11
+ @config.stubs(:logger).returns(stub_everything)
12
+ @config.stubs(:sudo).returns('sudo')
13
+ end
14
+
15
+ def test_stream_should_pass_options_through_to_run
16
+ @config.expects(:invoke_command).with("tail -f foo.log", { :once => true, :eof => true })
17
+ @config.stream("tail -f foo.log", :once => true)
18
+ end
19
+
20
+ def test_stream_with_sudo_should_avoid_closing_stdin
21
+ @config.expects(:invoke_command).with("sudo tail -f foo.log", { :once => true, :eof => false })
22
+ @config.stream("sudo tail -f foo.log", :once => true)
23
+ end
24
+
25
+ def test_stream_should_emit_stdout_via_puts
26
+ @config.expects(:invoke_command).yields(mock("channel"), :out, "something streamed")
27
+ @config.expects(:puts).with("something streamed")
28
+ @config.expects(:warn).never
29
+ @config.stream("tail -f foo.log")
30
+ end
31
+
32
+ def test_stream_should_emit_stderr_via_warn
33
+ ch = mock("channel")
34
+ ch.expects(:[]).with(:server).returns(server("minestrone"))
35
+ @config.expects(:invoke_command).yields(ch, :err, "something streamed")
36
+ @config.expects(:puts).never
37
+ @config.expects(:warn).with("[err :: minestrone] something streamed")
38
+ @config.stream("tail -f foo.log")
39
+ end
40
+
41
+ def test_capture_should_pass_options_merged_with_once_to_run
42
+ @config.expects(:invoke_command).with("hostname", { :foo => "bar", :once => true, :eof => true })
43
+ @config.capture("hostname", :foo => "bar")
44
+ end
45
+
46
+ def test_capture_with_sudo_should_avoid_closing_stdin
47
+ @config.expects(:invoke_command).with("sudo hostname", { :foo => "bar", :once => true, :eof => false })
48
+ @config.capture("sudo hostname", :foo => "bar")
49
+ end
50
+
51
+ def test_capture_with_stderr_should_emit_stderr_via_warn
52
+ ch = mock("channel")
53
+ ch.expects(:[]).with(:server).returns(server("minestrone"))
54
+ @config.expects(:invoke_command).yields(ch, :err, "boom")
55
+ @config.expects(:warn).with("[err :: minestrone] boom")
56
+ @config.capture("hostname")
57
+ end
58
+
59
+ def test_capture_with_stdout_should_aggregate_and_return_stdout
60
+ config_expects_invoke_command_to_loop_with(mock("channel"), "foo", "bar", "baz")
61
+ assert_equal "foobarbaz", @config.capture("hostname")
62
+ end
63
+
64
+ private
65
+
66
+ def config_expects_invoke_command_to_loop_with(channel, *output)
67
+ class <<@config
68
+ attr_accessor :script, :channel
69
+ def invoke_command(*args)
70
+ script.each { |item| yield channel, :out, item }
71
+ end
72
+ end
73
+ @config.channel = channel
74
+ @config.script = output
75
+ end
76
+ end
@@ -0,0 +1,258 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/actions/invocation'
3
+ require 'minestrone/configuration/actions/file_transfer'
4
+
5
+ class ConfigurationActionsInvocationTest < Test::Unit::TestCase
6
+ class MockConfig
7
+ attr_reader :options
8
+ attr_accessor :debug
9
+ attr_accessor :dry_run
10
+ attr_accessor :server
11
+ attr_accessor :session
12
+
13
+ def initialize
14
+ @options = {}
15
+ @server = nil
16
+ initialize_invocation
17
+ end
18
+
19
+ def [](*args)
20
+ @options[*args]
21
+ end
22
+
23
+ def set(name, value)
24
+ @options[name] = value
25
+ end
26
+
27
+ def fetch(*args)
28
+ @options.fetch(*args)
29
+ end
30
+
31
+ def active_server
32
+ @server
33
+ end
34
+
35
+ def execute_on_server
36
+ yield
37
+ end
38
+
39
+ include Minestrone::Configuration::Actions::Invocation
40
+ include Minestrone::Configuration::Actions::FileTransfer
41
+ end
42
+
43
+ def setup
44
+ @config = make_config
45
+ @original_io_proc = MockConfig.default_io_proc
46
+ end
47
+
48
+ def teardown
49
+ MockConfig.default_io_proc = @original_io_proc
50
+ end
51
+
52
+ def test_run_options_should_be_passed_to_command_process
53
+ ::Minestrone::Command.expects(:process).with("ls", @config.session, has_entries(:foo => "bar", :eof => true, :logger => @config.logger, :configuration => @config))
54
+ @config.run "ls", :foo => "bar"
55
+ end
56
+
57
+ def test_run_will_return_if_dry_run
58
+ @config.expects(:dry_run).returns(true)
59
+ @config.expects(:execute_on_server).never
60
+ @config.run "ls", :foo => "bar"
61
+ end
62
+
63
+ def test_put_wont_transfer_if_dry_run
64
+ config = make_config
65
+ config.dry_run = true
66
+ config.server = "foo"
67
+ config.expects(:execute_on_server).never
68
+ ::Minestrone::Transfer.expects(:process).never
69
+ config.put "foo", "bar", :mode => 0644
70
+ end
71
+
72
+ def test_add_default_command_options_should_return_bare_options_if_there_is_no_env_or_shell_specified
73
+ assert_equal({:foo => "bar"}, @config.add_default_command_options(:foo => "bar"))
74
+ end
75
+
76
+ def test_add_default_command_options_should_merge_default_environment_as_env
77
+ @config[:default_environment][:bang] = "baz"
78
+ assert_equal({:foo => "bar", :env => { :bang => "baz" }}, @config.add_default_command_options(:foo => "bar"))
79
+ end
80
+
81
+ def test_add_default_command_options_should_merge_env_with_default_environment
82
+ @config[:default_environment][:bang] = "baz"
83
+ @config[:default_environment][:bacon] = "crunchy"
84
+ assert_equal({:foo => "bar", :env => { :bang => "baz", :bacon => "chunky", :flip => "flop" }}, @config.add_default_command_options(:foo => "bar", :env => {:bacon => "chunky", :flip => "flop"}))
85
+ end
86
+
87
+ def test_add_default_command_options_should_use_default_shell_if_present
88
+ @config.set :default_shell, "/bin/bash"
89
+ assert_equal({:foo => "bar", :shell => "/bin/bash"}, @config.add_default_command_options(:foo => "bar"))
90
+ end
91
+
92
+ def test_add_default_command_options_should_use_default_shell_of_false_if_present
93
+ @config.set :default_shell, false
94
+ assert_equal({:foo => "bar", :shell => false}, @config.add_default_command_options(:foo => "bar"))
95
+ end
96
+
97
+ def test_add_default_command_options_should_use_shell_in_preference_of_default_shell
98
+ @config.set :default_shell, "/bin/bash"
99
+ assert_equal({:foo => "bar", :shell => "/bin/sh"}, @config.add_default_command_options(:foo => "bar", :shell => "/bin/sh"))
100
+ end
101
+
102
+ def test_default_io_proc_should_log_stdout_arguments_as_info
103
+ ch = { :host => "minestrone",
104
+ :server => server("minestrone"),
105
+ :options => { :logger => mock("logger") } }
106
+ ch[:options][:logger].expects(:info).with("data stuff", "out :: minestrone")
107
+ MockConfig.default_io_proc[ch, :out, "data stuff"]
108
+ end
109
+
110
+ def test_default_io_proc_should_log_stderr_arguments_as_important
111
+ ch = { :host => "minestrone",
112
+ :server => server("minestrone"),
113
+ :options => { :logger => mock("logger") } }
114
+ ch[:options][:logger].expects(:important).with("data stuff", "err :: minestrone")
115
+ MockConfig.default_io_proc[ch, :err, "data stuff"]
116
+ end
117
+
118
+ def test_sudo_should_default_to_sudo
119
+ @config.expects(:run).with("sudo -p 'sudo password: ' ls", {})
120
+ @config.sudo "ls"
121
+ end
122
+
123
+ def test_sudo_should_keep_input_stream_open
124
+ @config.expects(:execute_on_server)
125
+ @config.sudo "ls", :foo => "bar"
126
+ end
127
+
128
+ def test_sudo_should_use_sudo_variable_definition
129
+ @config.expects(:run).with("/opt/local/bin/sudo -p 'sudo password: ' ls", {})
130
+ @config.options[:sudo] = "/opt/local/bin/sudo"
131
+ @config.sudo "ls"
132
+ end
133
+
134
+ def test_sudo_should_interpret_as_option_as_user
135
+ @config.expects(:run).with("sudo -p 'sudo password: ' -u app ls", {})
136
+ @config.sudo "ls", :as => "app"
137
+ end
138
+
139
+ def test_sudo_should_pass_options_through_to_run
140
+ @config.expects(:run).with("sudo -p 'sudo password: ' ls", { :foo => "bar" })
141
+ @config.sudo "ls", :foo => "bar"
142
+ end
143
+
144
+ def test_sudo_should_avoid_minus_p_when_sudo_prompt_is_empty
145
+ @config.set :sudo_prompt, ""
146
+ @config.expects(:run).with("sudo ls", {})
147
+ @config.sudo "ls"
148
+ end
149
+
150
+ def test_sudo_should_interpret_sudo_prompt_variable_as_custom_prompt
151
+ @config.set :sudo_prompt, "give it to me: "
152
+ @config.expects(:run).with("sudo -p 'give it to me: ' ls", {})
153
+ @config.sudo "ls"
154
+ end
155
+
156
+ def test_sudo_behavior_callback_should_send_password_when_prompted_with_default_sudo_prompt
157
+ ch = mock("channel")
158
+ ch.expects(:send_data).with("g00b3r\n")
159
+ @config.options[:password] = "g00b3r"
160
+ @config.sudo_behavior_callback(nil)[ch, nil, "sudo password: "]
161
+ end
162
+
163
+ def test_sudo_behavior_callback_should_send_password_when_prompted_with_custom_sudo_prompt
164
+ ch = mock("channel")
165
+ ch.expects(:send_data).with("g00b3r\n")
166
+ @config.set :sudo_prompt, "give it to me: "
167
+ @config.options[:password] = "g00b3r"
168
+ @config.sudo_behavior_callback(nil)[ch, nil, "give it to me: "]
169
+ end
170
+
171
+ def test_sudo_behavior_callback_with_incorrect_password_on_first_prompt
172
+ ch = mock("channel")
173
+ ch.stubs(:[]).with(:host).returns("minestrone")
174
+ ch.stubs(:[]).with(:server).returns(server("minestrone"))
175
+ @config.expects(:reset!).with(:password)
176
+ @config.sudo_behavior_callback(nil)[ch, nil, "Sorry, try again."]
177
+ end
178
+
179
+ def test_sudo_behavior_callback_with_incorrect_password_on_subsequent_prompts
180
+ callback = @config.sudo_behavior_callback(nil)
181
+
182
+ ch = mock("channel")
183
+ ch.stubs(:[]).with(:host).returns("minestrone")
184
+ ch.stubs(:[]).with(:server).returns(server("minestrone"))
185
+ ch2 = mock("channel")
186
+ ch2.stubs(:[]).with(:host).returns("cap2")
187
+ ch2.stubs(:[]).with(:server).returns(server("cap2"))
188
+
189
+ @config.expects(:reset!).with(:password).times(2)
190
+
191
+ callback[ch, nil, "Sorry, try again."]
192
+ callback[ch2, nil, "Sorry, try again."] # shouldn't call reset!
193
+ callback[ch, nil, "Sorry, try again."]
194
+ end
195
+
196
+ def test_sudo_behavior_callback_should_reset_password_and_prompt_again_if_output_includes_both_cues
197
+ ch = mock("channel")
198
+ ch.stubs(:[]).with(:host).returns("minestrone")
199
+ ch.stubs(:[]).with(:server).returns(server("minestrone"))
200
+ ch.expects(:send_data, "password!\n").times(2)
201
+
202
+ @config.set(:password, "password!")
203
+ @config.expects(:reset!).with(:password)
204
+
205
+ callback = @config.sudo_behavior_callback(nil)
206
+ callback[ch, :out, "sudo password: "]
207
+ callback[ch, :out, "Sorry, try again.\nsudo password: "]
208
+ end
209
+
210
+ def test_sudo_behavior_callback_should_defer_to_fallback_for_other_output
211
+ callback = @config.sudo_behavior_callback(inspectable_proc)
212
+
213
+ a = mock("channel", :called => true)
214
+ b = mock("stream", :called => true)
215
+ c = mock("data", :called => true)
216
+
217
+ callback[a, b, c]
218
+ end
219
+
220
+ def test_invoke_command_should_default_to_run
221
+ @config.expects(:run).with("ls", { :once => true })
222
+ @config.invoke_command("ls", :once => true)
223
+ end
224
+
225
+ def test_invoke_command_should_delegate_to_method_identified_by_via
226
+ @config.expects(:foobar).with("ls", { :once => true })
227
+ @config.invoke_command("ls", :once => true, :via => :foobar)
228
+ end
229
+
230
+ def test_run_only_logs_once
231
+ @config.server = :app
232
+
233
+ logger = mock('logger')
234
+ logger.stubs(:debug).with("executing \"ls\"")
235
+ @config.stubs(:logger).returns(logger)
236
+
237
+ @config.expects(:execute_on_server)
238
+
239
+ @config.run("ls")
240
+ end
241
+
242
+ private
243
+
244
+ def make_config
245
+ config = MockConfig.new
246
+ config.stubs(:logger).returns(stub_everything)
247
+ config
248
+ end
249
+
250
+ def inspectable_proc
251
+ Proc.new do |ch, stream, data|
252
+ ch.called
253
+ stream.called
254
+ data.called
255
+ end
256
+ end
257
+
258
+ end