capistrano 1.4.2 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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