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,53 @@
1
+
2
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
3
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
4
+
5
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
6
+
7
+ <head>
8
+ <meta http-equiv="content-type" content="text/html;charset=UTF-8" />
9
+ <title>System down for maintenance</title>
10
+
11
+ <style type="text/css">
12
+ div.outer {
13
+ position: absolute;
14
+ left: 50%;
15
+ top: 50%;
16
+ width: 500px;
17
+ height: 300px;
18
+ margin-left: -260px;
19
+ margin-top: -150px;
20
+ }
21
+
22
+ .DialogBody {
23
+ margin: 0;
24
+ padding: 10px;
25
+ text-align: left;
26
+ border: 1px solid #ccc;
27
+ border-right: 1px solid #999;
28
+ border-bottom: 1px solid #999;
29
+ background-color: #fff;
30
+ }
31
+
32
+ body { background-color: #fff; }
33
+ </style>
34
+ </head>
35
+
36
+ <body>
37
+
38
+ <div class="outer">
39
+ <div class="DialogBody" style="text-align: center;">
40
+ <div style="text-align: center; width: 200px; margin: 0 auto;">
41
+ <p style="color: red; font-size: 16px; line-height: 20px;">
42
+ The system is down for <%= reason ? reason : "maintenance" %>
43
+ as of <%= Time.now.strftime("%H:%M %Z") %>.
44
+ </p>
45
+ <p style="color: #666;">
46
+ It'll be back <%= deadline ? deadline : "shortly" %>.
47
+ </p>
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ </body>
53
+ </html>
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minestrone
4
+ class ServerDefinition
5
+ include Comparable
6
+
7
+ attr_reader :host
8
+ attr_reader :user
9
+ attr_reader :port
10
+ attr_reader :options
11
+
12
+ # The default user name to use when a user name is not explicitly provided
13
+ def self.default_user
14
+ ENV['USER'] || ENV['USERNAME'] || "not-specified"
15
+ end
16
+
17
+ def initialize(string, options = {})
18
+ @user, @host, @port = string.match(/^(?:([^;,:=]+)@|)(.*?)(?::(\d+)|)$/)[1,3]
19
+
20
+ @options = options.dup
21
+ user_opt, port_opt = @options.delete(:user), @options.delete(:port)
22
+
23
+ @user ||= user_opt
24
+ @port ||= port_opt
25
+
26
+ @port = @port.to_i if @port
27
+ end
28
+
29
+ def <=>(server)
30
+ [host, port, user] <=> [server.host, server.port, server.user]
31
+ end
32
+
33
+ # Redefined, so that Array#uniq will work to remove duplicate server
34
+ # definitions, based solely on their host names.
35
+ def eql?(server)
36
+ (host == server.host) && (user == server.user) && (port == server.port)
37
+ end
38
+
39
+ alias :== :eql?
40
+
41
+ # Redefined, so that Array#uniq will work to remove duplicate server
42
+ # definitions, based on their connection information.
43
+ def hash
44
+ @hash ||= [host, user, port].hash
45
+ end
46
+
47
+ def to_s
48
+ @to_s ||= begin
49
+ s = host
50
+ s = "#{user}@#{s}" if user
51
+ s = "#{s}:#{port}" if port && port != 22
52
+ s
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,81 @@
1
+ require 'net/ssh'
2
+
3
+ module Minestrone
4
+
5
+ #
6
+ # A helper class for dealing with SSH connections.
7
+ #
8
+
9
+ class SSH
10
+
11
+ # Patch an accessor onto an SSH connection so that we can record the server
12
+ # definition object that defines the connection.
13
+ module Server #:nodoc:
14
+ def self.apply_to(connection, server)
15
+ connection.extend(Server)
16
+ connection.xserver = server
17
+ connection
18
+ end
19
+
20
+ attr_accessor :xserver
21
+ end
22
+
23
+ # An abstraction to make it possible to connect to the server via public key.
24
+ #
25
+ # +server+ must be an instance of ServerDefinition.
26
+ #
27
+ # If a block is given, the new session is yielded to it, otherwise the new
28
+ # session is returned.
29
+ #
30
+ # If an :ssh_options key exists in +options+, it is passed to the Net::SSH
31
+ # constructor. Values in +options+ are then merged into it, and any
32
+ # connection information in +server+ is added last, so that +server+ info
33
+ # takes precedence over +options+, which takes precendence over ssh_options.
34
+
35
+ def self.connect(server, options = {})
36
+ connection_strategy(server, options) do |host, user, connection_options|
37
+ connection = Net::SSH.start(host, user, connection_options)
38
+ Server.apply_to(connection, server)
39
+ end
40
+ end
41
+
42
+ # Abstracts the logic for establishing an SSH connection.
43
+ #
44
+ # This will yield the hostname, username, and a hash of connection options
45
+ # to the given block, which should return a new connection.
46
+
47
+ def self.connection_strategy(server, options = {}, &block)
48
+
49
+ # construct the hash of ssh options that should be passed more-or-less
50
+ # directly to Net::SSH. This will be the general ssh options, merged with
51
+ # the server-specific ssh-options.
52
+ ssh_options = (options[:ssh_options] || {}).merge(server.options[:ssh_options] || {})
53
+
54
+ # load any SSH configuration files that were specified in the SSH options. This
55
+ # will load from ~/.ssh/config and /etc/ssh_config by default (see Net::SSH
56
+ # for details). Merge the explicitly given ssh_options over the top of the info
57
+ # from the config file.
58
+ ssh_options = Net::SSH.configuration_for(server.host, ssh_options.fetch(:config, true)).merge(ssh_options)
59
+
60
+ # Once we've loaded the config, we don't need Net::SSH to do it again.
61
+ ssh_options[:config] = false
62
+
63
+ ssh_options[:verbose] = :debug if options[:verbose] && options[:verbose] > 0
64
+
65
+ user = server.user || options[:user] || ssh_options[:username] || ssh_options[:user] || ServerDefinition.default_user
66
+ port = server.port || options[:port] || ssh_options[:port]
67
+
68
+ # the .ssh/config file might have changed the host-name on us
69
+ host = ssh_options.fetch(:host_name, server.host)
70
+
71
+ ssh_options[:port] = port if port
72
+
73
+ # delete these, since we've determined which username to use by this point
74
+ ssh_options.delete(:username)
75
+ ssh_options.delete(:user)
76
+
77
+ connection_options = ssh_options.merge(:auth_methods => ['publickey'])
78
+ yield host, user, connection_options
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minestrone/server_definition'
4
+
5
+ module Minestrone
6
+
7
+ class TaskDefinition
8
+ attr_reader :name, :namespace, :options, :body, :desc, :on_error
9
+
10
+ def initialize(name, namespace, options = {}, &block)
11
+ @name, @namespace, @options = name, namespace, options
12
+ @desc = @options.delete(:desc)
13
+ @on_error = options.delete(:on_error)
14
+ @body = block or raise ArgumentError, "a task requires a block"
15
+ end
16
+
17
+ # Returns the task's fully-qualified name, including the namespace
18
+ def fully_qualified_name
19
+ @fully_qualified_name ||= begin
20
+ if namespace.default_task == self
21
+ namespace.fully_qualified_name
22
+ else
23
+ [namespace.fully_qualified_name, name].compact.join(":")
24
+ end
25
+ end
26
+ end
27
+
28
+ def name=(value)
29
+ raise ArgumentError, "expected a valid task name" if !value.respond_to?(:to_sym)
30
+ @name = value.to_sym
31
+ end
32
+
33
+ # Returns the description for this task, with newlines collapsed and
34
+ # whitespace stripped. Returns the empty string if there is no
35
+ # description for this task.
36
+
37
+ def description(rebuild = false)
38
+ @description = nil if rebuild
39
+
40
+ @description ||= begin
41
+ description = @desc || ""
42
+
43
+ indentation = description[/\A\s+/]
44
+
45
+ if indentation
46
+ reformatted_description = "".dup
47
+
48
+ description.strip.each_line do |line|
49
+ line = line.chomp.sub(/^#{indentation}/, "")
50
+ line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/]
51
+ reformatted_description << line << "\n"
52
+ end
53
+
54
+ description = reformatted_description
55
+ end
56
+
57
+ description.strip.gsub(/\r\n/, "\n")
58
+ end
59
+ end
60
+
61
+ # Returns the first sentence of the full description. If +max_length+ is
62
+ # given, the result will be truncated if it is longer than +max_length+,
63
+ # and an ellipsis appended.
64
+
65
+ def brief_description(max_length = nil)
66
+ brief = description[/^.*?\.(?=\s|$)/] || description
67
+
68
+ if max_length && brief.length > max_length
69
+ brief = brief[0,max_length-3] + "..."
70
+ end
71
+
72
+ brief
73
+ end
74
+
75
+ # Indicates whether the task wants to continue, even if a server has failed
76
+ # previously
77
+
78
+ def continue_on_error?
79
+ @on_error == :continue
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/scp'
4
+ require 'net/sftp'
5
+
6
+ require 'minestrone/errors'
7
+ require 'minestrone/processable'
8
+
9
+ module Minestrone
10
+ class Transfer
11
+ include Processable
12
+
13
+ def self.process(direction, from, to, session, options = {}, &block)
14
+ new(direction, from, to, session, options, &block).process!
15
+ end
16
+
17
+ attr_reader :session, :options, :callback
18
+ attr_reader :transport, :direction, :from, :to
19
+ attr_reader :logger, :transfer
20
+
21
+ def initialize(direction, from, to, session, options = {}, &block)
22
+ @direction = direction
23
+ @from = from
24
+ @to = to
25
+ @session = session
26
+ @options = options
27
+ @callback = block
28
+
29
+ @transport = options.fetch(:via, :sftp)
30
+ @logger = options.delete(:logger)
31
+
32
+ prepare_transfer
33
+ end
34
+
35
+ def process!
36
+ loop do
37
+ begin
38
+ break unless process_iteration { active? }
39
+ rescue Exception => error
40
+ if error.respond_to?(:session)
41
+ handle_error(error)
42
+ else
43
+ raise
44
+ end
45
+ end
46
+ end
47
+
48
+ if transfer[:failed]
49
+ server = transfer[:server]
50
+ transfer_error = transfer[:error]
51
+ error = TransferError.new("#{operation} via #{transport} failed on #{server}: #{transfer_error} (#{transfer_error.message})")
52
+ error.host = server
53
+
54
+ logger.important(error.message) if logger
55
+ raise error
56
+ end
57
+
58
+ logger.debug "#{transport} #{operation} complete" if logger
59
+ self
60
+ end
61
+
62
+ def active?
63
+ transfer.active?
64
+ end
65
+
66
+ def operation
67
+ "#{direction}load"
68
+ end
69
+
70
+ def sanitized_from
71
+ if from.responds_to?(:read)
72
+ "#<#{from.class}>"
73
+ else
74
+ from
75
+ end
76
+ end
77
+
78
+ def sanitized_to
79
+ if to.responds_to?(:read)
80
+ "#<#{to.class}>"
81
+ else
82
+ to
83
+ end
84
+ end
85
+
86
+
87
+ private
88
+
89
+ def prepare_transfer
90
+ logger.info "#{transport} #{operation} #{from} -> #{to}" if logger
91
+
92
+ session_from = normalize(from)
93
+ session_to = normalize(to)
94
+
95
+ @transfer = case transport
96
+ when :sftp
97
+ prepare_sftp_transfer(session_from, session_to)
98
+ when :scp
99
+ prepare_scp_transfer(session_from, session_to)
100
+ else
101
+ raise ArgumentError, "unsupported transport type: #{transport.inspect}"
102
+ end
103
+ end
104
+
105
+ def prepare_scp_transfer(from, to)
106
+ real_callback = callback || Proc.new do |channel, name, sent, total|
107
+ logger.trace "[#{channel[:host]}] #{name}" if logger && sent == 0
108
+ end
109
+
110
+ channel = case direction
111
+ when :up
112
+ session.scp.upload(from, to, options, &real_callback)
113
+ when :down
114
+ session.scp.download(from, to, options, &real_callback)
115
+ else
116
+ raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
117
+ end
118
+
119
+ channel[:server] = session.xserver
120
+ channel[:host] = session.xserver.host
121
+
122
+ channel
123
+ end
124
+
125
+ class SFTPTransferWrapper
126
+ attr_reader :operation
127
+
128
+ def initialize(session, &callback)
129
+ session.sftp(false).connect do |sftp|
130
+ @operation = callback.call(sftp)
131
+ end
132
+ end
133
+
134
+ def active?
135
+ @operation.nil? || @operation.active?
136
+ end
137
+
138
+ def [](key)
139
+ @operation[key]
140
+ end
141
+
142
+ def []=(key, value)
143
+ @operation[key] = value
144
+ end
145
+
146
+ def abort!
147
+ @operation.abort!
148
+ end
149
+ end
150
+
151
+ def prepare_sftp_transfer(from, to)
152
+ SFTPTransferWrapper.new(session) do |sftp|
153
+ real_callback = Proc.new do |event, op, *args|
154
+ if callback
155
+ callback.call(event, op, *args)
156
+ elsif event == :open
157
+ logger.trace "[#{op[:host]}] #{args[0].remote}"
158
+ elsif event == :finish
159
+ logger.trace "[#{op[:host]}] done"
160
+ end
161
+ end
162
+
163
+ opts = options.dup
164
+ opts[:properties] = (opts[:properties] || {}).merge(
165
+ :server => session.xserver,
166
+ :host => session.xserver.host
167
+ )
168
+
169
+ case direction
170
+ when :up
171
+ sftp.upload(from, to, opts, &real_callback)
172
+ when :down
173
+ sftp.download(from, to, opts, &real_callback)
174
+ else
175
+ raise ArgumentError, "unsupported transfer direction: #{direction.inspect}"
176
+ end
177
+ end
178
+ end
179
+
180
+ def normalize(argument)
181
+ if argument.is_a?(String)
182
+ argument.gsub(/\$CAPISTRANO:HOST\$/, session.xserver.host)
183
+ elsif argument.respond_to?(:read)
184
+ pos = argument.pos
185
+ clone = StringIO.new(argument.read)
186
+ clone.pos = argument.pos = pos
187
+ clone
188
+ else
189
+ argument
190
+ end
191
+ end
192
+
193
+ def handle_error(error)
194
+ raise error if error.message.include?('expected a file to upload')
195
+
196
+ transfer[:error] = error
197
+ transfer[:failed] = true
198
+
199
+ case transport
200
+ when :sftp then transfer.abort!
201
+ when :scp then transfer.close
202
+ end
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minestrone
4
+ class Version
5
+ def self.to_s
6
+ "0.0.1"
7
+ end
8
+ end
9
+
10
+ VERSION = Version.to_s
11
+ end
data/lib/minestrone.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'minestrone/configuration'
2
+ require 'minestrone/extensions'
3
+ require 'minestrone/ext/string'
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.push File.expand_path("lib", __dir__)
4
+ require "minestrone/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "minestrone"
8
+ spec.version = Minestrone::Version.to_s
9
+ spec.platform = Gem::Platform::RUBY
10
+ spec.authors = ["Jamis Buck", "Lee Hambley", "Kuba Suder"]
11
+ spec.homepage = "http://github.com/mackuba/minestrone"
12
+ spec.summary = "Minestrone - Welcome to easy deployment with Ruby over SSH"
13
+ spec.description = "Minestrone is a utility and framework for executing commands on a remote machine, via SSH."
14
+ spec.files = `git ls-files`.split("\n")
15
+ spec.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ spec.executables = `git ls-files -- bin/*`.split("\n").map { |file| File.basename(file) }
17
+ spec.require_paths = ["lib"]
18
+ spec.extra_rdoc_files = [
19
+ "README.md"
20
+ ]
21
+
22
+ spec.required_ruby_version = ">= 3.0.0"
23
+
24
+ spec.add_dependency 'benchmark', '~> 0.5'
25
+ spec.add_dependency 'highline', '>= 0'
26
+ spec.add_dependency 'net-ssh', '>= 7.2'
27
+ spec.add_dependency 'net-sftp', '>= 3.0'
28
+ spec.add_dependency 'net-scp', '>= 3.0'
29
+
30
+ # used silently by net-ssh but undeclared
31
+ spec.add_dependency 'logger'
32
+ end
@@ -0,0 +1,130 @@
1
+ require "utils"
2
+ require 'minestrone/cli'
3
+
4
+ class CLIExecuteTest < Test::Unit::TestCase
5
+ class MockCLI < Minestrone::CLI
6
+ attr_reader :options
7
+
8
+ def initialize
9
+ @options = {}
10
+ end
11
+ end
12
+
13
+ def setup
14
+ @cli = MockCLI.new
15
+ @logger = stub_everything
16
+ @config = stub(:logger => @logger, :debug= => nil, :dry_run= => nil)
17
+ @config.stubs(:set)
18
+ @config.stubs(:load)
19
+ @config.stubs(:trigger)
20
+ @cli.stubs(:instantiate_configuration).returns(@config)
21
+ end
22
+
23
+ def test_execute_should_set_logger_verbosity
24
+ @cli.options[:verbose] = 7
25
+ @logger.expects(:level=).with(7)
26
+ @cli.execute!
27
+ end
28
+
29
+ def test_execute_should_set_password
30
+ @cli.options[:password] = "nosoup4u"
31
+ @config.expects(:set).with(:password, "nosoup4u")
32
+ @cli.execute!
33
+ end
34
+
35
+ def test_execute_should_set_prevars_before_loading
36
+ @config.expects(:load).never
37
+ @config.expects(:set).with(:environment, "foobar")
38
+ @config.expects(:load).with("standard")
39
+ @cli.options[:pre_vars] = { :environment => "foobar" }
40
+ @cli.execute!
41
+ end
42
+
43
+ def test_execute_should_load_sysconf_if_sysconf_set_and_exists
44
+ @cli.options[:sysconf] = "/etc/minestrone.conf"
45
+ @config.expects(:load).with("/etc/minestrone.conf")
46
+ File.expects(:file?).with("/etc/minestrone.conf").returns(true)
47
+ @cli.execute!
48
+ end
49
+
50
+ def test_execute_should_not_load_sysconf_when_sysconf_set_and_not_exists
51
+ @cli.options[:sysconf] = "/etc/minestrone.conf"
52
+ File.expects(:file?).with("/etc/minestrone.conf").returns(false)
53
+ @cli.execute!
54
+ end
55
+
56
+ def test_execute_should_load_dotfile_if_dotfile_set_and_exists
57
+ @cli.options[:dotfile] = "/home/jamis/.caprc"
58
+ @config.expects(:load).with("/home/jamis/.caprc")
59
+ File.expects(:file?).with("/home/jamis/.caprc").returns(true)
60
+ @cli.execute!
61
+ end
62
+
63
+ def test_execute_should_not_load_dotfile_when_dotfile_set_and_not_exists
64
+ @cli.options[:dotfile] = "/home/jamis/.caprc"
65
+ File.expects(:file?).with("/home/jamis/.caprc").returns(false)
66
+ @cli.execute!
67
+ end
68
+
69
+ def test_execute_should_load_recipes_when_recipes_are_given
70
+ @cli.options[:recipes] = %w(config/deploy path/to/extra)
71
+ @config.expects(:load).with("config/deploy")
72
+ @config.expects(:load).with("path/to/extra")
73
+ @cli.execute!
74
+ end
75
+
76
+ def test_execute_should_set_vars_and_execute_tasks
77
+ @cli.options[:vars] = { :foo => "bar", :baz => "bang" }
78
+ @cli.options[:actions] = %w(first second)
79
+ @config.expects(:set).with(:foo, "bar")
80
+ @config.expects(:set).with(:baz, "bang")
81
+ @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish)
82
+ @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish)
83
+ @cli.execute!
84
+ end
85
+
86
+ def test_execute_should_call_load_and_exit_triggers
87
+ @cli.options[:actions] = %w(first second)
88
+ @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish)
89
+ @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish)
90
+ @config.expects(:trigger).never
91
+ @config.expects(:trigger).with(:load)
92
+ @config.expects(:trigger).with(:exit)
93
+ @cli.execute!
94
+ end
95
+
96
+ def test_execute_should_call_handle_error_when_exceptions_occur
97
+ @config.expects(:load).raises(Exception, "boom")
98
+ @cli.expects(:handle_error).with { |e,| Exception === e }
99
+ @cli.execute!
100
+ end
101
+
102
+ def test_execute_should_return_config_instance
103
+ assert_equal @config, @cli.execute!
104
+ end
105
+
106
+ def test_instantiate_configuration_should_return_new_configuration_instance
107
+ assert_instance_of Minestrone::Configuration, MockCLI.new.instantiate_configuration
108
+ end
109
+
110
+ def test_handle_error_with_auth_error_should_abort_with_message_including_user_name
111
+ @cli.expects(:abort).with { |s| s.include?("jamis") }
112
+ @cli.handle_error(Net::SSH::AuthenticationFailed.new("jamis"))
113
+ end
114
+
115
+ def test_handle_error_with_cap_error_should_abort_with_message
116
+ @cli.expects(:abort).with("Wish you were here")
117
+ @cli.handle_error(Minestrone::Error.new("Wish you were here"))
118
+ end
119
+
120
+ def test_handle_error_with_other_errors_should_reraise_error
121
+ other_error = Class.new(RuntimeError)
122
+ assert_raises(other_error) { @cli.handle_error(other_error.new("boom")) }
123
+ end
124
+
125
+ def test_class_execute_method_should_call_parse_and_execute_with_ARGV
126
+ cli = mock(:execute! => nil)
127
+ MockCLI.expects(:parse).with(ARGV).returns(cli)
128
+ MockCLI.execute
129
+ end
130
+ end