capistrano 1.4.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. data/CHANGELOG +140 -4
  2. data/MIT-LICENSE +1 -1
  3. data/README +22 -14
  4. data/bin/cap +1 -8
  5. data/bin/capify +77 -0
  6. data/examples/sample.rb +10 -109
  7. data/lib/capistrano.rb +1 -0
  8. data/lib/capistrano/callback.rb +41 -0
  9. data/lib/capistrano/cli.rb +17 -317
  10. data/lib/capistrano/cli/execute.rb +82 -0
  11. data/lib/capistrano/cli/help.rb +102 -0
  12. data/lib/capistrano/cli/help.txt +53 -0
  13. data/lib/capistrano/cli/options.rb +183 -0
  14. data/lib/capistrano/cli/ui.rb +28 -0
  15. data/lib/capistrano/command.rb +62 -29
  16. data/lib/capistrano/configuration.rb +25 -226
  17. data/lib/capistrano/configuration/actions/file_transfer.rb +35 -0
  18. data/lib/capistrano/configuration/actions/inspect.rb +46 -0
  19. data/lib/capistrano/configuration/actions/invocation.rb +127 -0
  20. data/lib/capistrano/configuration/callbacks.rb +148 -0
  21. data/lib/capistrano/configuration/connections.rb +159 -0
  22. data/lib/capistrano/configuration/execution.rb +126 -0
  23. data/lib/capistrano/configuration/loading.rb +112 -0
  24. data/lib/capistrano/configuration/namespaces.rb +190 -0
  25. data/lib/capistrano/configuration/roles.rb +51 -0
  26. data/lib/capistrano/configuration/servers.rb +75 -0
  27. data/lib/capistrano/configuration/variables.rb +127 -0
  28. data/lib/capistrano/errors.rb +15 -0
  29. data/lib/capistrano/extensions.rb +27 -8
  30. data/lib/capistrano/gateway.rb +54 -29
  31. data/lib/capistrano/logger.rb +11 -11
  32. data/lib/capistrano/recipes/compat.rb +32 -0
  33. data/lib/capistrano/recipes/deploy.rb +483 -0
  34. data/lib/capistrano/recipes/deploy/dependencies.rb +44 -0
  35. data/lib/capistrano/recipes/deploy/local_dependency.rb +46 -0
  36. data/lib/capistrano/recipes/deploy/remote_dependency.rb +65 -0
  37. data/lib/capistrano/recipes/deploy/scm.rb +19 -0
  38. data/lib/capistrano/recipes/deploy/scm/base.rb +180 -0
  39. data/lib/capistrano/recipes/deploy/scm/bzr.rb +86 -0
  40. data/lib/capistrano/recipes/deploy/scm/cvs.rb +151 -0
  41. data/lib/capistrano/recipes/deploy/scm/darcs.rb +85 -0
  42. data/lib/capistrano/recipes/deploy/scm/mercurial.rb +129 -0
  43. data/lib/capistrano/recipes/deploy/scm/perforce.rb +126 -0
  44. data/lib/capistrano/recipes/deploy/scm/subversion.rb +103 -0
  45. data/lib/capistrano/recipes/deploy/strategy.rb +19 -0
  46. data/lib/capistrano/recipes/deploy/strategy/base.rb +64 -0
  47. data/lib/capistrano/recipes/deploy/strategy/checkout.rb +20 -0
  48. data/lib/capistrano/recipes/deploy/strategy/copy.rb +143 -0
  49. data/lib/capistrano/recipes/deploy/strategy/export.rb +20 -0
  50. data/lib/capistrano/recipes/deploy/strategy/remote.rb +52 -0
  51. data/lib/capistrano/recipes/deploy/strategy/remote_cache.rb +47 -0
  52. data/lib/capistrano/recipes/deploy/templates/maintenance.rhtml +53 -0
  53. data/lib/capistrano/recipes/standard.rb +26 -276
  54. data/lib/capistrano/recipes/templates/maintenance.rhtml +1 -1
  55. data/lib/capistrano/recipes/upgrade.rb +33 -0
  56. data/lib/capistrano/server_definition.rb +51 -0
  57. data/lib/capistrano/shell.rb +125 -81
  58. data/lib/capistrano/ssh.rb +80 -36
  59. data/lib/capistrano/task_definition.rb +69 -0
  60. data/lib/capistrano/upload.rb +146 -0
  61. data/lib/capistrano/version.rb +13 -17
  62. data/test/cli/execute_test.rb +132 -0
  63. data/test/cli/help_test.rb +139 -0
  64. data/test/cli/options_test.rb +226 -0
  65. data/test/cli/ui_test.rb +28 -0
  66. data/test/cli_test.rb +17 -0
  67. data/test/command_test.rb +284 -25
  68. data/test/configuration/actions/file_transfer_test.rb +40 -0
  69. data/test/configuration/actions/inspect_test.rb +62 -0
  70. data/test/configuration/actions/invocation_test.rb +195 -0
  71. data/test/configuration/callbacks_test.rb +206 -0
  72. data/test/configuration/connections_test.rb +288 -0
  73. data/test/configuration/execution_test.rb +159 -0
  74. data/test/configuration/loading_test.rb +119 -0
  75. data/test/configuration/namespace_dsl_test.rb +283 -0
  76. data/test/configuration/roles_test.rb +47 -0
  77. data/test/configuration/servers_test.rb +90 -0
  78. data/test/configuration/variables_test.rb +180 -0
  79. data/test/configuration_test.rb +60 -212
  80. data/test/deploy/scm/base_test.rb +55 -0
  81. data/test/deploy/strategy/copy_test.rb +146 -0
  82. data/test/extensions_test.rb +69 -0
  83. data/test/fixtures/cli_integration.rb +5 -0
  84. data/test/fixtures/custom.rb +2 -2
  85. data/test/gateway_test.rb +167 -0
  86. data/test/logger_test.rb +123 -0
  87. data/test/server_definition_test.rb +108 -0
  88. data/test/shell_test.rb +64 -0
  89. data/test/ssh_test.rb +67 -154
  90. data/test/task_definition_test.rb +101 -0
  91. data/test/upload_test.rb +131 -0
  92. data/test/utils.rb +31 -39
  93. data/test/version_test.rb +24 -0
  94. metadata +145 -98
  95. data/THANKS +0 -4
  96. data/lib/capistrano/actor.rb +0 -567
  97. data/lib/capistrano/generators/rails/deployment/deployment_generator.rb +0 -25
  98. data/lib/capistrano/generators/rails/deployment/templates/capistrano.rake +0 -49
  99. data/lib/capistrano/generators/rails/deployment/templates/deploy.rb +0 -122
  100. data/lib/capistrano/generators/rails/loader.rb +0 -20
  101. data/lib/capistrano/scm/base.rb +0 -61
  102. data/lib/capistrano/scm/baz.rb +0 -118
  103. data/lib/capistrano/scm/bzr.rb +0 -70
  104. data/lib/capistrano/scm/cvs.rb +0 -129
  105. data/lib/capistrano/scm/darcs.rb +0 -27
  106. data/lib/capistrano/scm/mercurial.rb +0 -83
  107. data/lib/capistrano/scm/perforce.rb +0 -139
  108. data/lib/capistrano/scm/subversion.rb +0 -128
  109. data/lib/capistrano/transfer.rb +0 -97
  110. data/lib/capistrano/utils.rb +0 -26
  111. data/test/actor_test.rb +0 -402
  112. data/test/scm/cvs_test.rb +0 -196
  113. data/test/scm/subversion_test.rb +0 -145
@@ -0,0 +1,69 @@
1
+ require 'capistrano/server_definition'
2
+
3
+ module Capistrano
4
+ # Represents the definition of a single task.
5
+ class TaskDefinition
6
+ attr_reader :name, :namespace, :options, :body, :desc, :on_error
7
+
8
+ def initialize(name, namespace, options={}, &block)
9
+ @name, @namespace, @options = name, namespace, options
10
+ @desc = @options.delete(:desc)
11
+ @on_error = options.delete(:on_error)
12
+ @body = block or raise ArgumentError, "a task requires a block"
13
+ @servers = nil
14
+ end
15
+
16
+ # Returns the task's fully-qualified name, including the namespace
17
+ def fully_qualified_name
18
+ @fully_qualified_name ||= begin
19
+ if namespace.default_task == self
20
+ namespace.fully_qualified_name
21
+ else
22
+ [namespace.fully_qualified_name, name].compact.join(":")
23
+ end
24
+ end
25
+ end
26
+
27
+ # Returns the description for this task, with newlines collapsed and
28
+ # whitespace stripped. Returns the empty string if there is no
29
+ # description for this task.
30
+ def description(rebuild=false)
31
+ @description = nil if rebuild
32
+ @description ||= begin
33
+ description = @desc || ""
34
+
35
+ indentation = description[/\A\s+/]
36
+ if indentation
37
+ reformatted_description = ""
38
+ description.strip.each_line do |line|
39
+ line = line.chomp.sub(/^#{indentation}/, "")
40
+ line = line.gsub(/#{indentation}\s*/, " ") if line[/^\S/]
41
+ reformatted_description << line << "\n"
42
+ end
43
+ description = reformatted_description
44
+ end
45
+
46
+ description.strip.gsub(/\r\n/, "\n")
47
+ end
48
+ end
49
+
50
+ # Returns the first sentence of the full description. If +max_length+ is
51
+ # given, the result will be truncated if it is longer than +max_length+,
52
+ # and an ellipsis appended.
53
+ def brief_description(max_length=nil)
54
+ brief = description[/^.*?\.(?=\s|$)/] || description
55
+
56
+ if max_length && brief.length > max_length
57
+ brief = brief[0,max_length-3] + "..."
58
+ end
59
+
60
+ brief
61
+ end
62
+
63
+ # Indicates whether the task wants to continue, even if a server has failed
64
+ # previously
65
+ def continue_on_error?
66
+ @on_error == :continue
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,146 @@
1
+ require 'net/sftp'
2
+ require 'net/sftp/operations/errors'
3
+ require 'capistrano/errors'
4
+
5
+ module Capistrano
6
+ unless ENV['SKIP_VERSION_CHECK']
7
+ require 'capistrano/version'
8
+ require 'net/sftp/version'
9
+ sftp_version = [Net::SFTP::Version::MAJOR, Net::SFTP::Version::MINOR, Net::SFTP::Version::TINY]
10
+ required_version = [1,1,0]
11
+ if !Capistrano::Version.check(required_version, sftp_version)
12
+ raise "You have Net::SFTP #{sftp_version.join(".")}, but you need at least #{required_version.join(".")}. Net::SFTP will not be used."
13
+ end
14
+ end
15
+
16
+ # This class encapsulates a single file upload to be performed in parallel
17
+ # across multiple machines, using the SFTP protocol. Although it is intended
18
+ # to be used primarily from within Capistrano, it may also be used standalone
19
+ # if you need to simply upload a file to multiple servers.
20
+ #
21
+ # Basic Usage:
22
+ #
23
+ # begin
24
+ # uploader = Capistrano::Upload.new(sessions, "remote-file.txt",
25
+ # :data => "the contents of the file to upload")
26
+ # uploader.process!
27
+ # rescue Capistrano::UploadError => e
28
+ # warn "Could not upload the file: #{e.message}"
29
+ # end
30
+ class Upload
31
+ def self.process(sessions, filename, options)
32
+ new(sessions, filename, options).process!
33
+ end
34
+
35
+ attr_reader :sessions, :filename, :options
36
+ attr_reader :failed, :completed
37
+
38
+ # Creates and prepares a new Upload instance. The +sessions+ parameter
39
+ # must be an array of open Net::SSH sessions. The +filename+ is the name
40
+ # (including path) of the destination file on the remote server. The
41
+ # +options+ hash accepts the following keys (as symbols):
42
+ #
43
+ # * data: required. Should refer to a String containing the contents of
44
+ # the file to upload.
45
+ # * mode: optional. The "mode" of the destination file. Defaults to 0660.
46
+ # * logger: optional. Should point to a Capistrano::Logger instance, if
47
+ # given.
48
+ def initialize(sessions, filename, options)
49
+ raise ArgumentError, "you must specify the data to upload via the :data option" unless options[:data]
50
+
51
+ @sessions = sessions
52
+ @filename = filename
53
+ @options = options
54
+
55
+ @completed = @failed = 0
56
+ @sftps = setup_sftp
57
+ end
58
+
59
+ # Uploads to all specified servers in parallel. If any one of the servers
60
+ # fails, an exception will be raised (UploadError).
61
+ def process!
62
+ logger.debug "uploading #{filename}" if logger
63
+ while running?
64
+ @sftps.each do |sftp|
65
+ next if sftp.channel[:done]
66
+ begin
67
+ sftp.channel.connection.process(true)
68
+ rescue Net::SFTP::Operations::StatusException => error
69
+ logger.important "uploading failed: #{error.description}", sftp.channel[:server] if logger
70
+ failed!(sftp)
71
+ end
72
+ end
73
+ sleep 0.01 # a brief respite, to keep the CPU from going crazy
74
+ end
75
+ logger.trace "upload finished" if logger
76
+
77
+ if (failed = @sftps.select { |sftp| sftp.channel[:failed] }).any?
78
+ hosts = failed.map { |sftp| sftp.channel[:server] }
79
+ error = UploadError.new("upload of #{filename} failed on #{hosts.join(',')}")
80
+ error.hosts = hosts
81
+ raise error
82
+ end
83
+
84
+ self
85
+ end
86
+
87
+ private
88
+
89
+ def logger
90
+ options[:logger]
91
+ end
92
+
93
+ def setup_sftp
94
+ sessions.map do |session|
95
+ server = session.xserver
96
+ sftp = session.sftp
97
+ sftp.connect unless sftp.state == :open
98
+
99
+ sftp.channel[:server] = server
100
+ sftp.channel[:done] = false
101
+ sftp.channel[:failed] = false
102
+
103
+ real_filename = filename.gsub(/\$CAPISTRANO:HOST\$/, server.host)
104
+ sftp.open(real_filename, IO::WRONLY | IO::CREAT | IO::TRUNC, options[:mode] || 0660) do |status, handle|
105
+ break unless check_status(sftp, "open #{real_filename}", server, status)
106
+
107
+ logger.info "uploading data to #{server}:#{real_filename}" if logger
108
+ sftp.write(handle, options[:data] || "") do |status|
109
+ break unless check_status(sftp, "write to #{server}:#{real_filename}", server, status)
110
+ sftp.close_handle(handle) do
111
+ logger.debug "done uploading data to #{server}:#{real_filename}" if logger
112
+ completed!(sftp)
113
+ end
114
+ end
115
+ end
116
+
117
+ sftp
118
+ end
119
+ end
120
+
121
+ def check_status(sftp, action, server, status)
122
+ return true if status.code == Net::SFTP::Session::FX_OK
123
+
124
+ logger.error "could not #{action} on #{server} (#{status.message})" if logger
125
+ failed!(sftp)
126
+
127
+ return false
128
+ end
129
+
130
+ def running?
131
+ completed < @sftps.length
132
+ end
133
+
134
+ def failed!(sftp)
135
+ completed!(sftp)
136
+ @failed += 1
137
+ sftp.channel[:failed] = true
138
+ end
139
+
140
+ def completed!(sftp)
141
+ @completed += 1
142
+ sftp.channel[:done] = true
143
+ end
144
+ end
145
+
146
+ end
@@ -1,26 +1,22 @@
1
1
  module Capistrano
2
2
  module Version #:nodoc:
3
- # A method for comparing versions of required modules. It expects
4
- # arrays as parameters, and returns true if the first is no less than the
5
- # second, and strictly less than the third.
6
- def self.check(actual, minimum, maximum) #:nodoc:
7
- actual = actual[0] * 1_000_000 + actual[1] * 1_000 + actual[2]
8
- minimum = minimum[0] * 1_000_000 + minimum[1] * 1_000 + minimum[2]
9
- maximum = maximum[0] * 1_000_000 + maximum[1] * 1_000 + maximum[2]
10
-
11
- return actual >= minimum && actual < maximum
3
+ # A method for comparing versions of required modules. It expects two
4
+ # arrays of integers as parameters, the first being the minimum version
5
+ # required, and the second being the actual version available. It returns
6
+ # true if the actual version is at least equal to the required version.
7
+ def self.check(required, actual) #:nodoc:
8
+ required = required.map { |v| "%06d" % v }.join(".")
9
+ actual = actual.map { |v| "%06d" % v }.join(".")
10
+ return actual >= required
12
11
  end
13
12
 
14
- MAJOR = 1
15
- MINOR = 4
16
- TINY = 2
13
+ MAJOR = 2
14
+ MINOR = 0
15
+ TINY = 0
17
16
 
18
17
  STRING = [MAJOR, MINOR, TINY].join(".")
19
18
 
20
- MINIMUM_SSH_REQUIRED = [1,0,10]
21
- MAXIMUM_SSH_REQUIRED = [1,99,0]
22
-
23
- MINIMUM_SFTP_REQUIRED = [1,1,0]
24
- MAXIMUM_SFTP_REQUIRED = [1,99,0]
19
+ SSH_REQUIRED = [1,0,10]
20
+ SFTP_REQUIRED = [1,1,0]
25
21
  end
26
22
  end
@@ -0,0 +1,132 @@
1
+ require "#{File.dirname(__FILE__)}/../utils"
2
+ require 'capistrano/cli/execute'
3
+
4
+ class CLIExecuteTest < Test::Unit::TestCase
5
+ class MockCLI
6
+ attr_reader :options
7
+
8
+ def initialize
9
+ @options = {}
10
+ end
11
+
12
+ include Capistrano::CLI::Execute
13
+ end
14
+
15
+ def setup
16
+ @cli = MockCLI.new
17
+ @logger = stub_everything
18
+ @config = stub(:logger => @logger)
19
+ @config.stubs(:set)
20
+ @config.stubs(:load)
21
+ @config.stubs(:trigger)
22
+ @cli.stubs(:instantiate_configuration).returns(@config)
23
+ end
24
+
25
+ def test_execute_should_set_logger_verbosity
26
+ @cli.options[:verbose] = 7
27
+ @logger.expects(:level=).with(7)
28
+ @cli.execute!
29
+ end
30
+
31
+ def test_execute_should_set_password
32
+ @cli.options[:password] = "nosoup4u"
33
+ @config.expects(:set).with(:password, "nosoup4u")
34
+ @cli.execute!
35
+ end
36
+
37
+ def test_execute_should_set_prevars_before_loading
38
+ @config.expects(:load).never
39
+ @config.expects(:set).with(:stage, "foobar")
40
+ @config.expects(:load).with("standard")
41
+ @cli.options[:pre_vars] = { :stage => "foobar" }
42
+ @cli.execute!
43
+ end
44
+
45
+ def test_execute_should_load_sysconf_if_sysconf_set_and_exists
46
+ @cli.options[:sysconf] = "/etc/capistrano.conf"
47
+ @config.expects(:load).with("/etc/capistrano.conf")
48
+ File.expects(:file?).with("/etc/capistrano.conf").returns(true)
49
+ @cli.execute!
50
+ end
51
+
52
+ def test_execute_should_not_load_sysconf_when_sysconf_set_and_not_exists
53
+ @cli.options[:sysconf] = "/etc/capistrano.conf"
54
+ File.expects(:file?).with("/etc/capistrano.conf").returns(false)
55
+ @cli.execute!
56
+ end
57
+
58
+ def test_execute_should_load_dotfile_if_dotfile_set_and_exists
59
+ @cli.options[:dotfile] = "/home/jamis/.caprc"
60
+ @config.expects(:load).with("/home/jamis/.caprc")
61
+ File.expects(:file?).with("/home/jamis/.caprc").returns(true)
62
+ @cli.execute!
63
+ end
64
+
65
+ def test_execute_should_not_load_dotfile_when_dotfile_set_and_not_exists
66
+ @cli.options[:dotfile] = "/home/jamis/.caprc"
67
+ File.expects(:file?).with("/home/jamis/.caprc").returns(false)
68
+ @cli.execute!
69
+ end
70
+
71
+ def test_execute_should_load_recipes_when_recipes_are_given
72
+ @cli.options[:recipes] = %w(config/deploy path/to/extra)
73
+ @config.expects(:load).with("config/deploy")
74
+ @config.expects(:load).with("path/to/extra")
75
+ @cli.execute!
76
+ end
77
+
78
+ def test_execute_should_set_vars_and_execute_tasks
79
+ @cli.options[:vars] = { :foo => "bar", :baz => "bang" }
80
+ @cli.options[:actions] = %w(first second)
81
+ @config.expects(:set).with(:foo, "bar")
82
+ @config.expects(:set).with(:baz, "bang")
83
+ @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish)
84
+ @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish)
85
+ @cli.execute!
86
+ end
87
+
88
+ def test_execute_should_call_load_and_exit_triggers
89
+ @cli.options[:actions] = %w(first second)
90
+ @config.expects(:find_and_execute_task).with("first", :before => :start, :after => :finish)
91
+ @config.expects(:find_and_execute_task).with("second", :before => :start, :after => :finish)
92
+ @config.expects(:trigger).never
93
+ @config.expects(:trigger).with(:load)
94
+ @config.expects(:trigger).with(:exit)
95
+ @cli.execute!
96
+ end
97
+
98
+ def test_execute_should_call_handle_error_when_exceptions_occur
99
+ @config.expects(:load).raises(Exception, "boom")
100
+ @cli.expects(:handle_error).with { |e,| Exception === e }
101
+ @cli.execute!
102
+ end
103
+
104
+ def test_execute_should_return_config_instance
105
+ assert_equal @config, @cli.execute!
106
+ end
107
+
108
+ def test_instantiate_configuration_should_return_new_configuration_instance
109
+ assert_instance_of Capistrano::Configuration, MockCLI.new.instantiate_configuration
110
+ end
111
+
112
+ def test_handle_error_with_auth_error_should_abort_with_message_including_user_name
113
+ @cli.expects(:abort).with { |s| s.include?("jamis") }
114
+ @cli.handle_error(Net::SSH::AuthenticationFailed.new("jamis"))
115
+ end
116
+
117
+ def test_handle_error_with_cap_error_should_abort_with_message
118
+ @cli.expects(:abort).with("Wish you were here")
119
+ @cli.handle_error(Capistrano::Error.new("Wish you were here"))
120
+ end
121
+
122
+ def test_handle_error_with_other_errors_should_reraise_error
123
+ other_error = Class.new(RuntimeError)
124
+ assert_raises(other_error) { @cli.handle_error(other_error.new("boom")) }
125
+ end
126
+
127
+ def test_class_execute_method_should_call_parse_and_execute_with_ARGV
128
+ cli = mock(:execute! => nil)
129
+ MockCLI.expects(:parse).with(ARGV).returns(cli)
130
+ MockCLI.execute
131
+ end
132
+ end
@@ -0,0 +1,139 @@
1
+ require "#{File.dirname(__FILE__)}/../utils"
2
+ require 'capistrano/cli/help'
3
+
4
+ class CLIHelpTest < Test::Unit::TestCase
5
+ class MockCLI
6
+ attr_reader :options, :called_original
7
+
8
+ def initialize
9
+ @options = {}
10
+ @called_original = false
11
+ end
12
+
13
+ def execute_requested_actions(config)
14
+ @called_original = config
15
+ end
16
+
17
+ include Capistrano::CLI::Help
18
+ end
19
+
20
+ def setup
21
+ @cli = MockCLI.new
22
+ @cli.options[:verbose] = 0
23
+ @ui = stub("ui", :output_cols => 80, :output_rows => 20, :page_at= => nil)
24
+ MockCLI.stubs(:ui).returns(@ui)
25
+ end
26
+
27
+ def test_execute_requested_actions_without_tasks_or_explain_should_call_original
28
+ @cli.execute_requested_actions(:config)
29
+ @cli.expects(:task_list).never
30
+ @cli.expects(:explain_task).never
31
+ assert_equal :config, @cli.called_original
32
+ end
33
+
34
+ def test_execute_requested_actions_with_tasks_should_call_task_list
35
+ @cli.options[:tasks] = true
36
+ @cli.expects(:task_list).with(:config)
37
+ @cli.expects(:explain_task).never
38
+ @cli.execute_requested_actions(:config)
39
+ assert !@cli.called_original
40
+ end
41
+
42
+ def test_execute_requested_actions_with_explain_should_call_explain_task
43
+ @cli.options[:explain] = "deploy_with_niftiness"
44
+ @cli.expects(:task_list).never
45
+ @cli.expects(:explain_task).with(:config, "deploy_with_niftiness")
46
+ @cli.execute_requested_actions(:config)
47
+ assert !@cli.called_original
48
+ end
49
+
50
+ def test_task_list_with_no_tasks_should_emit_warning
51
+ config = mock("config", :task_list => [])
52
+ @cli.expects(:warn)
53
+ @cli.task_list(config)
54
+ end
55
+
56
+ def test_task_list_should_query_all_tasks_in_all_namespaces
57
+ expected_max_len = 80 - 3 - MockCLI::LINE_PADDING
58
+ task_list = [task("c"), task("g", "c:g"), task("b", "c:b"), task("a")]
59
+ task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name) }
60
+
61
+ config = mock("config")
62
+ config.expects(:task_list).with(:all).returns(task_list)
63
+ @cli.stubs(:puts)
64
+ @cli.task_list(config)
65
+ end
66
+
67
+ def test_task_list_should_never_use_less_than_MIN_MAX_LEN_chars_for_descriptions
68
+ @ui.stubs(:output_cols).returns(20)
69
+ t = task("c")
70
+ t.expects(:brief_description).with(30).returns("hello")
71
+ config = mock("config", :task_list => [t])
72
+ @cli.stubs(:puts)
73
+ @cli.task_list(config)
74
+ end
75
+
76
+ def test_task_list_should_not_include_tasks_with_blank_description_or_internal_by_default
77
+ t1 = task("c")
78
+ t1.expects(:brief_description).returns("hello")
79
+ t2 = task("d", "d", "[internal] howdy")
80
+ t2.expects(:brief_description).never
81
+ t3 = task("e", "e", "")
82
+ t3.expects(:brief_description).never
83
+
84
+ config = mock("config", :task_list => [t1, t2, t3])
85
+ @cli.stubs(:puts)
86
+ @cli.expects(:puts).never.with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ }
87
+ @cli.task_list(config)
88
+ end
89
+
90
+ def test_task_list_should_include_tasks_with_blank_descriptions_and_internal_when_verbose
91
+ t1 = task("c")
92
+ t1.expects(:brief_description).returns("hello")
93
+ t2 = task("d", "d", "[internal] howdy")
94
+ t2.expects(:brief_description).returns("[internal] howdy")
95
+ t3 = task("e", "e", "")
96
+ t3.expects(:brief_description).returns("")
97
+
98
+ config = mock("config", :task_list => [t1, t2, t3])
99
+ @cli.options[:verbose] = 1
100
+ @cli.stubs(:puts)
101
+ @cli.expects(:puts).with { |s,| (s || "").include?("[internal]") || s =~ /#\s*$/ }.at_least_once
102
+ @cli.task_list(config)
103
+ end
104
+
105
+ def test_explain_task_should_warn_if_task_does_not_exist
106
+ config = mock("config", :find_task => nil)
107
+ @cli.expects(:warn).with { |s,| s =~ /`deploy_with_niftiness'/ }
108
+ @cli.explain_task(config, "deploy_with_niftiness")
109
+ end
110
+
111
+ def test_explain_task_with_task_that_has_no_description_should_emit_stub
112
+ t = mock("task", :description => "")
113
+ config = mock("config")
114
+ config.expects(:find_task).with("deploy_with_niftiness").returns(t)
115
+ @cli.stubs(:puts)
116
+ @cli.expects(:puts).with("There is no description for this task.")
117
+ @cli.explain_task(config, "deploy_with_niftiness")
118
+ end
119
+
120
+ def test_explain_task_with_task_should_format_description
121
+ t = stub("task", :description => "line1\nline2\n\nline3")
122
+ config = mock("config", :find_task => t)
123
+ @cli.stubs(:puts)
124
+ @cli.explain_task(config, "deploy_with_niftiness")
125
+ end
126
+
127
+ def test_long_help_should_load_and_format_help_txt_file
128
+ File.expects(:dirname).returns "a/b/c"
129
+ File.expects(:read).with("a/b/c/help.txt").returns("text")
130
+ @ui.expects(:say).with("text\n")
131
+ @cli.long_help
132
+ end
133
+
134
+ private
135
+
136
+ def task(name, fqn=name, desc="a description")
137
+ stub("task", :name => name, :fully_qualified_name => fqn, :description => desc)
138
+ end
139
+ end