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,110 @@
1
+ require 'utils'
2
+ require 'minestrone/configuration/alias_task'
3
+ require 'minestrone/configuration/execution'
4
+ require 'minestrone/configuration/namespaces'
5
+ require 'minestrone/task_definition'
6
+
7
+ class AliasTaskTest < Test::Unit::TestCase
8
+ class MockConfig
9
+ attr_reader :options
10
+ attr_accessor :logger
11
+
12
+ def initialize(options = {})
13
+ @options = {}
14
+ @logger = options.delete(:logger)
15
+ initialize_execution
16
+ initialize_namespaces
17
+ end
18
+
19
+ include Minestrone::Configuration::AliasTask
20
+ include Minestrone::Configuration::Execution
21
+ include Minestrone::Configuration::Namespaces
22
+ end
23
+
24
+ def setup
25
+ @config = MockConfig.new( :logger => stub(:debug => nil, :info => nil, :important => nil) )
26
+ end
27
+
28
+ def test_makes_a_copy_of_the_task
29
+ @config.task(:foo) { 42 }
30
+ @config.alias_task 'new_foo', 'foo'
31
+
32
+ assert @config.tasks.key?(:new_foo)
33
+ end
34
+
35
+ def test_original_task_remain_with_same_name
36
+ @config.task(:foo) { 42 }
37
+ @config.alias_task 'new_foo', 'foo'
38
+
39
+ assert_equal :foo, @config.tasks[:foo].name
40
+ assert_equal :new_foo, @config.tasks[:new_foo].name
41
+ end
42
+
43
+ def test_aliased_task_do_the_same
44
+ @config.task(:foo) { 42 }
45
+ @config.alias_task 'new_foo', 'foo'
46
+
47
+ assert_equal 42, @config.find_and_execute_task('new_foo')
48
+ end
49
+
50
+ def test_aliased_task_should_preserve_description
51
+ @config.task(:foo, :desc => "the Ultimate Question of Life, the Universe, and Everything" ) { 42 }
52
+ @config.alias_task 'new_foo', 'foo'
53
+
54
+ task = @config.find_task('foo')
55
+ new_task = @config.find_task('new_foo')
56
+
57
+ assert_equal task.description, new_task.description
58
+ end
59
+
60
+ def test_aliased_task_should_preserve_on_error
61
+ @config.task(:foo, :on_error => :continue) { 42 }
62
+ @config.alias_task 'new_foo', 'foo'
63
+
64
+ task = @config.find_task('foo')
65
+ new_task = @config.find_task('new_foo')
66
+
67
+ assert_equal task.on_error, new_task.on_error
68
+ end
69
+
70
+ def test_raise_exception_when_task_doesnt_exist
71
+ assert_raises(Minestrone::NoSuchTaskError) { @config.alias_task 'non_existant_task', 'fail_miserably' }
72
+ end
73
+
74
+ def test_convert_task_names_using_to_str
75
+ @config.task(:foo) { 42 }
76
+
77
+ @config.alias_task 'one', 'foo'
78
+ @config.alias_task :two, 'foo'
79
+ @config.alias_task 'three', :foo
80
+ @config.alias_task :four, :foo
81
+
82
+ assert @config.tasks.key?(:one)
83
+ assert @config.tasks.key?(:two)
84
+ assert @config.tasks.key?(:three)
85
+ assert @config.tasks.key?(:four)
86
+ end
87
+
88
+ def test_raise_an_exception_when_task_names_can_not_be_converted
89
+ @config.task(:foo) { 42 }
90
+
91
+ assert_raises(ArgumentError) { @config.alias_task mock('x'), :foo }
92
+ end
93
+
94
+ def test_should_include_namespace
95
+ @config.namespace(:outer) do
96
+ task(:foo) { 42 }
97
+ alias_task 'new_foo', 'foo'
98
+
99
+ namespace(:inner) do
100
+ task(:foo) { 43 }
101
+ alias_task 'new_foo', 'foo'
102
+ end
103
+ end
104
+
105
+ assert_equal 42, @config.find_and_execute_task('outer:new_foo')
106
+ assert_equal 42, @config.find_and_execute_task('outer:foo')
107
+ assert_equal 43, @config.find_and_execute_task('outer:inner:new_foo')
108
+ assert_equal 43, @config.find_and_execute_task('outer:inner:foo')
109
+ end
110
+ end
@@ -0,0 +1,201 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/callbacks'
3
+
4
+ class ConfigurationCallbacksTest < Test::Unit::TestCase
5
+ class MockConfig
6
+ attr_reader :original_initialize_called
7
+ attr_reader :called
8
+
9
+ def initialize
10
+ @original_initialize_called = true
11
+ @called = []
12
+ initialize_callbacks
13
+ end
14
+
15
+ def execute_task(task)
16
+ invoke_task_directly(task)
17
+ end
18
+
19
+ protected
20
+
21
+ def invoke_task_directly(task)
22
+ @called << task
23
+ end
24
+
25
+ include Minestrone::Configuration::Callbacks
26
+ end
27
+
28
+ def setup
29
+ @config = MockConfig.new
30
+ @config.stubs(:logger).returns(stub_everything("logger"))
31
+ end
32
+
33
+ def test_initialize_should_initialize_callbacks_collection
34
+ assert @config.original_initialize_called
35
+ assert @config.callbacks.empty?
36
+ end
37
+
38
+ def test_before_should_delegate_to_on
39
+ @config.expects(:on).with(:before, :foo, "bing:blang", {:only => :bar, :zip => :zing})
40
+ @config.before :bar, :foo, "bing:blang", :zip => :zing
41
+ end
42
+
43
+ def test_before_should_keep_task_names_as_given
44
+ @config.before "deploy:create_symlink", "bing:blang", "deploy:create_symlink"
45
+ assert_equal "bing:blang", @config.callbacks[:before][0].source
46
+ assert_equal "deploy:create_symlink", @config.callbacks[:before][1].source
47
+ assert_equal ["deploy:create_symlink"], @config.callbacks[:before][1].only
48
+ end
49
+
50
+ def test_before_should_keep_task_name_arrays_as_given
51
+ @config.before ["deploy:create_symlink", "bingo:blast"], "bing:blang"
52
+ assert_equal ["deploy:create_symlink", "bingo:blast"], @config.callbacks[:before].last.only
53
+ end
54
+
55
+ def test_after_should_delegate_to_on
56
+ @config.expects(:on).with(:after, :foo, "bing:blang", {:only => :bar, :zip => :zing})
57
+ @config.after :bar, :foo, "bing:blang", :zip => :zing
58
+ end
59
+
60
+ def test_after_should_keep_task_names_as_given
61
+ @config.after "deploy:create_symlink", "bing:blang", "deploy:create_symlink"
62
+ assert_equal "bing:blang", @config.callbacks[:after][0].source
63
+ assert_equal "deploy:create_symlink", @config.callbacks[:after][1].source
64
+ assert_equal ["deploy:create_symlink"], @config.callbacks[:after][1].only
65
+ end
66
+
67
+ def test_after_should_keep_task_name_arrays_as_given
68
+ @config.after ["deploy:create_symlink", "bingo:blast"], "bing:blang"
69
+ assert_equal ["deploy:create_symlink", "bingo:blast"], @config.callbacks[:after].last.only
70
+ end
71
+
72
+ def test_on_with_single_reference_should_add_task_callback
73
+ @config.on :before, :a_test
74
+ assert_equal 1, @config.callbacks[:before].length
75
+ assert_equal :a_test, @config.callbacks[:before][0].source
76
+ @config.expects(:find_and_execute_task).with(:a_test)
77
+ @config.callbacks[:before][0].call
78
+ end
79
+
80
+ def test_on_with_multi_reference_should_add_all_as_task_callback
81
+ @config.on :before, :first, :second, :third
82
+ assert_equal 3, @config.callbacks[:before].length
83
+ assert_equal %w(first second third), @config.callbacks[:before].map { |c| c.source.to_s }
84
+ end
85
+
86
+ def test_on_with_block_should_add_block_as_proc_callback
87
+ called = false
88
+ @config.on(:before) { called = true }
89
+ assert_equal 1, @config.callbacks[:before].length
90
+ assert_instance_of Proc, @config.callbacks[:before][0].source
91
+ @config.callbacks[:before][0].call
92
+ assert called
93
+ end
94
+
95
+ def test_on_with_single_only_should_set_only_as_string_array_on_all_references
96
+ @config.on :before, :first, "second:third", :only => :primary
97
+ assert_equal 2, @config.callbacks[:before].length
98
+ assert @config.callbacks[:before].all? { |c| c.only == %w(primary) }
99
+ end
100
+
101
+ def test_on_with_multi_only_should_set_only_as_string_array_on_all_references
102
+ @config.on :before, :first, "second:third", :only => [:primary, "other:one"]
103
+ assert_equal 2, @config.callbacks[:before].length
104
+ assert @config.callbacks[:before].all? { |c| c.only == %w(primary other:one) }
105
+ end
106
+
107
+ def test_on_with_single_except_should_set_except_as_string_array_on_all_references
108
+ @config.on :before, :first, "second:third", :except => :primary
109
+ assert_equal 2, @config.callbacks[:before].length
110
+ assert @config.callbacks[:before].all? { |c| c.except == %w(primary) }
111
+ end
112
+
113
+ def test_on_with_multi_except_should_set_except_as_string_array_on_all_references
114
+ @config.on :before, :first, "second:third", :except => [:primary, "other:one"]
115
+ assert_equal 2, @config.callbacks[:before].length
116
+ assert @config.callbacks[:before].all? { |c| c.except == %w(primary other:one) }
117
+ end
118
+
119
+ def test_on_with_only_and_block_should_set_only_as_string_array
120
+ @config.on(:before, :only => :primary) { blah }
121
+ assert_equal 1, @config.callbacks[:before].length
122
+ assert_equal %w(primary), @config.callbacks[:before].first.only
123
+ end
124
+
125
+ def test_on_with_except_and_block_should_set_except_as_string_array
126
+ @config.on(:before, :except => :primary) { blah }
127
+ assert_equal 1, @config.callbacks[:before].length
128
+ assert_equal %w(primary), @config.callbacks[:before].first.except
129
+ end
130
+
131
+ def test_on_without_tasks_or_block_should_raise_error
132
+ assert_raises(ArgumentError) { @config.on(:before) }
133
+ end
134
+
135
+ def test_on_with_both_tasks_and_block_should_raise_error
136
+ assert_raises(ArgumentError) { @config.on(:before, :first) { blah } }
137
+ end
138
+
139
+ def test_trigger_without_constraints_should_invoke_all_callbacks
140
+ task = stub(:fully_qualified_name => "any:old:thing")
141
+ @config.on(:before, :first, "second:third")
142
+ @config.on(:after, :another, "and:another")
143
+ @config.expects(:find_and_execute_task).with(:first)
144
+ @config.expects(:find_and_execute_task).with("second:third")
145
+ @config.expects(:find_and_execute_task).with(:another).never
146
+ @config.expects(:find_and_execute_task).with("and:another").never
147
+ @config.trigger(:before, task)
148
+ end
149
+
150
+ def test_trigger_with_only_constraint_should_invoke_only_matching_callbacks
151
+ task = stub(:fully_qualified_name => "any:old:thing")
152
+ @config.on(:before, :first)
153
+ @config.on(:before, "second:third", :only => "any:old:thing")
154
+ @config.on(:before, "this:too", :only => "any:other:thing")
155
+ @config.on(:after, :another, "and:another")
156
+ @config.expects(:find_and_execute_task).with(:first)
157
+ @config.expects(:find_and_execute_task).with("second:third")
158
+ @config.expects(:find_and_execute_task).with("this:too").never
159
+ @config.expects(:find_and_execute_task).with(:another).never
160
+ @config.expects(:find_and_execute_task).with("and:another").never
161
+ @config.trigger(:before, task)
162
+ end
163
+
164
+ def test_trigger_with_except_constraint_should_invoke_anything_but_matching_callbacks
165
+ task = stub(:fully_qualified_name => "any:old:thing")
166
+ @config.on(:before, :first)
167
+ @config.on(:before, "second:third", :except => "any:old:thing")
168
+ @config.on(:before, "this:too", :except => "any:other:thing")
169
+ @config.on(:after, :another, "and:another")
170
+ @config.expects(:find_and_execute_task).with(:first)
171
+ @config.expects(:find_and_execute_task).with("second:third").never
172
+ @config.expects(:find_and_execute_task).with("this:too")
173
+ @config.expects(:find_and_execute_task).with(:another).never
174
+ @config.expects(:find_and_execute_task).with("and:another").never
175
+ @config.trigger(:before, task)
176
+ end
177
+
178
+ def test_trigger_without_task_should_invoke_all_callbacks_for_that_event
179
+ @config.on(:before, :first)
180
+ @config.on(:before, "second:third", :except => "any:old:thing")
181
+ @config.on(:before, "this:too", :except => "any:other:thing")
182
+ @config.on(:after, :another, "and:another")
183
+ @config.expects(:find_and_execute_task).with(:first)
184
+ @config.expects(:find_and_execute_task).with("second:third")
185
+ @config.expects(:find_and_execute_task).with("this:too")
186
+ @config.expects(:find_and_execute_task).with(:another).never
187
+ @config.expects(:find_and_execute_task).with("and:another").never
188
+ @config.trigger(:before)
189
+ end
190
+
191
+ def test_execute_task_without_named_hooks_should_just_call_task
192
+ ns = stub("namespace", :default_task => nil, :name => "old", :fully_qualified_name => "any:old")
193
+ task = stub(:fully_qualified_name => "any:old:thing", :name => "thing", :namespace => ns)
194
+
195
+ ns.stubs(:search_task).returns(nil)
196
+
197
+ @config.execute_task(task)
198
+ assert_equal [task], @config.called
199
+ end
200
+
201
+ end
@@ -0,0 +1,192 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/connections'
3
+
4
+ class ConfigurationConnectionsTest < Test::Unit::TestCase
5
+ class MockConfig
6
+ attr_reader :original_initialize_called
7
+ attr_reader :values
8
+ attr_reader :dry_run
9
+ attr_accessor :current_task
10
+ attr_accessor :server
11
+
12
+ def initialize
13
+ @original_initialize_called = true
14
+ @values = {}
15
+ initialize_connections
16
+ end
17
+
18
+ def fetch(*args)
19
+ @values.fetch(*args)
20
+ end
21
+
22
+ def [](key)
23
+ @values[key]
24
+ end
25
+
26
+ def exists?(key)
27
+ @values.key?(key)
28
+ end
29
+
30
+ def resolved_server
31
+ @server
32
+ end
33
+
34
+ include Minestrone::Configuration::Connections
35
+ end
36
+
37
+ def setup
38
+ @config = MockConfig.new
39
+ @config.stubs(:logger).returns(stub_everything)
40
+ Net::SSH.stubs(:configuration_for).returns({})
41
+ end
42
+
43
+ def test_initialize_should_initialize_session_and_call_original_initialize
44
+ assert @config.original_initialize_called
45
+ assert_nil @config.session
46
+ assert_false @config.instance_variable_get('@failed')
47
+ end
48
+
49
+ def test_establish_connection_to_server_should_use_the_resolved_server
50
+ Minestrone::SSH.expects(:connect).with { |s,c| s.host == "minestrone" && c == @config }.returns(:success)
51
+ assert_nil @config.session
52
+ @config.server = server("minestrone")
53
+ @config.establish_connection_to_server
54
+ assert_equal :success, @config.session
55
+ end
56
+
57
+ def test_establish_connection_to_server_should_not_attempt_to_reestablish_existing_connection
58
+ Minestrone::SSH.expects(:connect).never
59
+ @config.expects(:resolved_server).never
60
+ @config.session = :ok
61
+ @config.server = server("cap1")
62
+ @config.establish_connection_to_server
63
+ assert_equal :ok, @config.session
64
+ end
65
+
66
+ def test_establish_connection_to_server_should_raise_connection_error_on_failure
67
+ Minestrone::SSH.expects(:connect).raises(Exception)
68
+ @config.server = server("cap1")
69
+ assert_raises(Minestrone::ConnectionError) {
70
+ @config.establish_connection_to_server
71
+ }
72
+ end
73
+
74
+ def test_connection_error_should_include_failed_host
75
+ Minestrone::SSH.expects(:connect).raises(Exception)
76
+ @config.server = server("cap1")
77
+ begin
78
+ @config.establish_connection_to_server
79
+ flunk "expected an exception to be raised"
80
+ rescue Minestrone::ConnectionError => e
81
+ assert e.respond_to?(:host)
82
+ assert_equal "cap1", e.host.to_s
83
+ end
84
+ end
85
+
86
+ def test_execute_on_server_should_require_a_block
87
+ assert_raises(ArgumentError) { @config.execute_on_server }
88
+ end
89
+
90
+ def test_execute_on_server_without_current_task_should_use_configured_server
91
+ host = server("first")
92
+ @config.server = host
93
+ @config.expects(:establish_connection_to_server).returns(:done)
94
+ block_called = false
95
+ @config.execute_on_server do
96
+ block_called = true
97
+ end
98
+ assert block_called
99
+ end
100
+
101
+ def test_execute_on_server_should_determine_server_from_configured_server
102
+ assert_nil @config.session
103
+ @config.server = server("cap1")
104
+ @config.current_task = mock_task
105
+ Minestrone::SSH.expects(:connect).returns(:success)
106
+ @config.execute_on_server {}
107
+ assert_equal :success, @config.session
108
+ end
109
+
110
+ def test_execute_on_server_should_yield_to_block
111
+ assert_nil @config.session
112
+ host = server("cap1")
113
+ @config.server = host
114
+ @config.current_task = mock_task
115
+ Minestrone::SSH.expects(:connect).returns(:success)
116
+ block_called = false
117
+ @config.execute_on_server do
118
+ block_called = true
119
+ assert @config.session
120
+ end
121
+ assert block_called
122
+ end
123
+
124
+ def test_execute_servers_should_raise_connection_error_on_failure_by_default
125
+ @config.current_task = mock_task
126
+ @config.server = server("cap1")
127
+ Minestrone::SSH.expects(:connect).raises(Exception)
128
+ assert_raises(Minestrone::ConnectionError) do
129
+ @config.execute_on_server do
130
+ flunk "expected an exception to be raised"
131
+ end
132
+ end
133
+ end
134
+
135
+ def test_execute_servers_should_not_raise_connection_error_on_failure_with_on_errors_continue
136
+ host = server("cap1")
137
+ @config.current_task = mock_task(:on_error => :continue)
138
+ @config.server = host
139
+ Minestrone::SSH.expects(:connect).raises(Exception)
140
+ assert_nothing_raised {
141
+ @config.execute_on_server do
142
+ flunk "should not yield after connection failure"
143
+ end
144
+ }
145
+ assert @config.instance_variable_get('@failed')
146
+ end
147
+
148
+ def test_execute_on_server_should_skip_failed_host_with_on_errors_continue
149
+ @config.current_task = mock_task(:on_error => :continue)
150
+ @config.server = server("cap1")
151
+ @config.instance_variable_set('@failed', true)
152
+
153
+ Minestrone::SSH.expects(:connect).never
154
+
155
+ @config.execute_on_server do
156
+ flunk "should not yield for failed host"
157
+ end
158
+ end
159
+
160
+ def test_execute_on_server_should_record_command_errors_with_on_errors_continue
161
+ host = server("cap1")
162
+ @config.current_task = mock_task(:on_error => :continue)
163
+ @config.server = host
164
+ Minestrone::SSH.expects(:connect).returns(:success)
165
+ @config.execute_on_server do
166
+ error = Minestrone::CommandError.new
167
+ error.host = host
168
+ raise error
169
+ end
170
+ assert @config.instance_variable_get('@failed')
171
+ end
172
+
173
+ def test_connect_should_establish_connection_to_server_in_scope
174
+ assert_nil @config.session
175
+ @config.current_task = mock_task
176
+ @config.server = server("cap1")
177
+ Minestrone::SSH.expects(:connect).returns(:success)
178
+ @config.connect!
179
+ assert_equal :success, @config.session
180
+ end
181
+
182
+ private
183
+
184
+ def mock_task(options = {})
185
+ continue_on_error = options[:on_error] == :continue
186
+ stub("task",
187
+ :fully_qualified_name => "name",
188
+ :options => options,
189
+ :continue_on_error? => continue_on_error
190
+ )
191
+ end
192
+ end
@@ -0,0 +1,176 @@
1
+ require "utils"
2
+ require 'minestrone/configuration/execution'
3
+ require 'minestrone/task_definition'
4
+
5
+ class ConfigurationExecutionTest < Test::Unit::TestCase
6
+ class MockConfig
7
+ attr_reader :tasks, :namespaces, :fully_qualified_name, :parent
8
+ attr_reader :state, :original_initialize_called
9
+ attr_accessor :logger, :default_task
10
+
11
+ def initialize(options = {})
12
+ @original_initialize_called = true
13
+ @tasks = {}
14
+ @namespaces = {}
15
+ @state = {}
16
+ @fully_qualified_name = options[:fqn]
17
+ @parent = options[:parent]
18
+ @logger = options.delete(:logger)
19
+ initialize_execution
20
+ end
21
+
22
+ include Minestrone::Configuration::Execution
23
+ end
24
+
25
+ def setup
26
+ @config = MockConfig.new(:logger => stub(:debug => nil, :info => nil, :important => nil))
27
+ @config.stubs(:search_task).returns(nil)
28
+ end
29
+
30
+ def test_initialize_should_initialize_collections
31
+ assert_nil @config.rollback_requests
32
+ assert @config.original_initialize_called
33
+ assert @config.task_call_frames.empty?
34
+ end
35
+
36
+ def test_execute_task_should_populate_call_stack
37
+ task = new_task @config, :testing
38
+ assert_nothing_raised { @config.execute_task(task) }
39
+ assert_equal %w(testing), @config.state[:testing][:stack]
40
+ assert_nil @config.state[:testing][:history]
41
+ assert @config.task_call_frames.empty?
42
+ end
43
+
44
+ def test_nested_execute_task_should_add_to_call_stack
45
+ testing = new_task @config, :testing
46
+ outer = new_task(@config, :outer) { execute_task(testing) }
47
+
48
+ assert_nothing_raised { @config.execute_task(outer) }
49
+ assert_equal %w(outer testing), @config.state[:testing][:stack]
50
+ assert_nil @config.state[:testing][:history]
51
+ assert @config.task_call_frames.empty?
52
+ end
53
+
54
+ def test_execute_task_should_execute_in_scope_of_tasks_parent
55
+ ns = stub("namespace", :tasks => {}, :default_task => nil, :fully_qualified_name => "ns")
56
+ ns.expects(:instance_eval)
57
+ testing = new_task ns, :testing
58
+ @config.execute_task(testing)
59
+ end
60
+
61
+ def test_transaction_outside_of_task_should_raise_exception
62
+ assert_raises(ScriptError) { @config.transaction {} }
63
+ end
64
+
65
+ def test_transaction_without_block_should_raise_argument_error
66
+ testing = new_task(@config, :testing) { transaction }
67
+ assert_raises(ArgumentError) { @config.execute_task(testing) }
68
+ end
69
+
70
+ def test_transaction_should_initialize_transaction_history
71
+ @config.state[:inspector] = stack_inspector
72
+ testing = new_task(@config, :testing) { transaction { instance_eval(&state[:inspector]) } }
73
+ @config.execute_task(testing)
74
+ assert_equal [], @config.state[:testing][:history]
75
+ end
76
+
77
+ def test_transaction_from_within_transaction_should_not_start_new_transaction
78
+ third = new_task(@config, :third, &stack_inspector)
79
+ second = new_task(@config, :second) { transaction { execute_task(third) } }
80
+ first = new_task(@config, :first) { transaction { execute_task(second) } }
81
+ # kind of fragile...not sure how else to check that transaction was only
82
+ # really run twice...but if the transaction was REALLY run, logger.info
83
+ # will be called once when it starts, and once when it finishes.
84
+ @config.logger = mock()
85
+ @config.logger.stubs(:debug)
86
+ @config.logger.expects(:info).times(2)
87
+ @config.execute_task(first)
88
+ end
89
+
90
+ def test_on_rollback_should_have_no_effect_outside_of_transaction
91
+ aaa = new_task(@config, :aaa) { on_rollback { state[:rollback] = true }; raise "boom" }
92
+ assert_raises(RuntimeError) { @config.execute_task(aaa) }
93
+ assert_nil @config.state[:rollback]
94
+ end
95
+
96
+ def test_exception_raised_in_transaction_should_call_all_registered_rollback_handlers_in_reverse_order
97
+ aaa = new_task(@config, :aaa) { on_rollback { (state[:rollback] ||= []) << :aaa } }
98
+ bbb = new_task(@config, :bbb) { on_rollback { (state[:rollback] ||= []) << :bbb } }
99
+ ccc = new_task(@config, :ccc) {}
100
+ ddd = new_task(@config, :ddd) { on_rollback { (state[:rollback] ||= []) << :ddd }; execute_task(bbb); execute_task(ccc) }
101
+ eee = new_task(@config, :eee) { transaction { execute_task(ddd); execute_task(aaa); raise "boom" } }
102
+ assert_raises(RuntimeError) do
103
+ @config.execute_task(eee)
104
+ end
105
+ assert_equal [:aaa, :bbb, :ddd], @config.state[:rollback]
106
+ assert_nil @config.rollback_requests
107
+ assert @config.task_call_frames.empty?
108
+ end
109
+
110
+ def test_exception_during_rollback_should_simply_be_logged_and_ignored
111
+ aaa = new_task(@config, :aaa) { on_rollback { state[:aaa] = true; raise LoadError, "ouch" }; execute_task(bbb) }
112
+ bbb = new_task(@config, :bbb) { raise MadError, "boom" }
113
+ ccc = new_task(@config, :ccc) { transaction { execute_task(aaa) } }
114
+ assert_raises(NameError) do
115
+ @config.execute_task(ccc)
116
+ end
117
+ assert @config.state[:aaa]
118
+ end
119
+
120
+ def test_on_rollback_called_twice_should_result_in_last_rollback_block_being_effective
121
+ aaa = new_task(@config, :aaa) do
122
+ transaction do
123
+ on_rollback { (state[:rollback] ||= []) << :first }
124
+ on_rollback { (state[:rollback] ||= []) << :second }
125
+ raise "boom"
126
+ end
127
+ end
128
+
129
+ assert_raises(RuntimeError) do
130
+ @config.execute_task(aaa)
131
+ end
132
+
133
+ assert_equal [:second], @config.state[:rollback]
134
+ end
135
+
136
+ def test_find_and_execute_task_should_raise_error_when_task_cannot_be_found
137
+ @config.expects(:find_task).with("path:to:task").returns(nil)
138
+ assert_raises(Minestrone::NoSuchTaskError) { @config.find_and_execute_task("path:to:task") }
139
+ end
140
+
141
+ def test_find_and_execute_task_should_execute_task_when_task_is_found
142
+ @config.expects(:find_task).with("path:to:task").returns(:found)
143
+ @config.expects(:execute_task).with(:found)
144
+ assert_nothing_raised { @config.find_and_execute_task("path:to:task") }
145
+ end
146
+
147
+ def test_find_and_execute_task_with_before_option_should_trigger_callback
148
+ @config.expects(:find_task).with("path:to:task").returns(:found)
149
+ @config.expects(:trigger).with(:incoming, :found)
150
+ @config.expects(:execute_task).with(:found)
151
+ @config.find_and_execute_task("path:to:task", :before => :incoming)
152
+ end
153
+
154
+ def test_find_and_execute_task_with_after_option_should_trigger_callback
155
+ @config.expects(:find_task).with("path:to:task").returns(:found)
156
+ @config.expects(:trigger).with(:outgoing, :found)
157
+ @config.expects(:execute_task).with(:found)
158
+ @config.find_and_execute_task("path:to:task", :after => :outgoing)
159
+ end
160
+
161
+ private
162
+
163
+ def stack_inspector
164
+ Proc.new do
165
+ (state[:trail] ||= []) << current_task.fully_qualified_name
166
+ data = state[current_task.name] = {}
167
+ data[:stack] = task_call_frames.map { |frame| frame.task.fully_qualified_name }
168
+ data[:history] = rollback_requests && rollback_requests.map { |frame| frame.task.fully_qualified_name }
169
+ end
170
+ end
171
+
172
+ def new_task(namespace, name, options = {}, &block)
173
+ block ||= stack_inspector
174
+ namespace.tasks[name] = Minestrone::TaskDefinition.new(name, namespace, &block)
175
+ end
176
+ end