mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
Refactored CLI stuff. Improved help system.
git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6313 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
b5daf9f58b
commit
bdef53cbfd
32 changed files with 1160 additions and 758 deletions
|
@ -1,3 +1,11 @@
|
|||
*SVN*
|
||||
|
||||
* Added -e switch to explain specific task. Added -X to extend -x. Made -h much briefer. Added -T to list known tasks. [Jamis Buck]
|
||||
|
||||
* Added namespaces for tasks [Jamis Buck]
|
||||
|
||||
* Merged the Configuration and Actor classes, performed various other massive refactorings of the code [Jamis Buck]
|
||||
|
||||
*1.4.1* (February 24, 2007)
|
||||
|
||||
* Use the no-auth-cache option with subversion so that username/password tokens do not get cached by capistrano usage [jonathan]
|
||||
|
|
9
bin/cap
9
bin/cap
|
@ -1,11 +1,4 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
begin
|
||||
require 'rubygems'
|
||||
rescue LoadError
|
||||
# no rubygems to load, so we fail silently
|
||||
end
|
||||
|
||||
require 'capistrano/cli'
|
||||
|
||||
Capistrano::CLI.execute!
|
||||
Capistrano::CLI.execute
|
||||
|
|
|
@ -7,8 +7,7 @@ Gem::Specification.new do |s|
|
|||
s.platform = Gem::Platform::RUBY
|
||||
s.summary = <<-DESC.strip.gsub(/\n\s+/, " ")
|
||||
Capistrano is a framework and utility for executing commands in parallel
|
||||
on multiple remote machines, via SSH. The primary goal is to simplify and
|
||||
automate the deployment of web applications.
|
||||
on multiple remote machines, via SSH.
|
||||
DESC
|
||||
|
||||
s.files = Dir.glob("{bin,lib,examples,test}/**/*") + %w(README MIT-LICENSE CHANGELOG THANKS)
|
||||
|
@ -18,10 +17,9 @@ Gem::Specification.new do |s|
|
|||
s.bindir = "bin"
|
||||
s.executables << "cap"
|
||||
|
||||
s.add_dependency 'rake', ">= 0.7.0"
|
||||
|
||||
s.add_dependency 'net-ssh', ">= #{Capistrano::Version::SSH_REQUIRED.join(".")}"
|
||||
s.add_dependency 'net-sftp', ">= #{Capistrano::Version::SFTP_REQUIRED.join(".")}"
|
||||
s.add_dependency 'highline'
|
||||
|
||||
s.author = "Jamis Buck"
|
||||
s.email = "jamis@37signals.com"
|
||||
|
|
|
@ -1,83 +1,24 @@
|
|||
require 'optparse'
|
||||
require 'capistrano'
|
||||
require 'capistrano/cli/execute'
|
||||
require 'capistrano/cli/help'
|
||||
require 'capistrano/cli/options'
|
||||
require 'capistrano/cli/ui'
|
||||
|
||||
module Capistrano
|
||||
# The CLI class encapsulates the behavior of capistrano when it is invoked
|
||||
# as a command-line utility. This allows other programs to embed ST and
|
||||
# preserve it's command-line semantics.
|
||||
# as a command-line utility. This allows other programs to embed Capistrano
|
||||
# and preserve it's command-line semantics.
|
||||
class CLI
|
||||
# Invoke capistrano using the ARGV array as the option parameters. This
|
||||
# is what the command-line capistrano utility does.
|
||||
def self.execute!
|
||||
new.execute!
|
||||
end
|
||||
|
||||
# The following determines whether or not echo-suppression is available.
|
||||
# This requires the termios library to be installed (which, unfortunately,
|
||||
# is not available for Windows).
|
||||
begin
|
||||
require 'termios'
|
||||
|
||||
# Enable or disable stdin echoing to the terminal.
|
||||
def self.echo(enable)
|
||||
term = Termios::getattr(STDIN)
|
||||
|
||||
if enable
|
||||
term.c_lflag |= (Termios::ECHO | Termios::ICANON)
|
||||
else
|
||||
term.c_lflag &= ~Termios::ECHO
|
||||
end
|
||||
|
||||
Termios::setattr(STDIN, Termios::TCSANOW, term)
|
||||
end
|
||||
rescue LoadError
|
||||
def self.echo(enable)
|
||||
end
|
||||
end
|
||||
|
||||
# execute the associated block with echo-suppression enabled. Note that
|
||||
# if termios is not available, echo suppression will not be available
|
||||
# either.
|
||||
def self.with_echo
|
||||
unless @warned_about_echo
|
||||
puts "WARNING: Password will echo -- install the 'termios' gem to hide your password." if !defined?(Termios) && RUBY_PLATFORM !~ /mswin/
|
||||
@warned_about_echo = true
|
||||
end
|
||||
echo(false)
|
||||
yield
|
||||
ensure
|
||||
echo(true)
|
||||
end
|
||||
|
||||
# Prompt for a password using echo suppression.
|
||||
def self.password_prompt(prompt="Password: ")
|
||||
sync = STDOUT.sync
|
||||
begin
|
||||
with_echo do
|
||||
STDOUT.sync = true
|
||||
print(prompt)
|
||||
STDIN.gets.chomp
|
||||
end
|
||||
ensure
|
||||
STDOUT.sync = sync
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
# The array of (unparsed) command-line options
|
||||
attr_reader :args
|
||||
|
||||
# The hash of (parsed) command-line options
|
||||
attr_reader :options
|
||||
|
||||
# Create a new CLI instance using the given array of command-line parameters
|
||||
# to initialize it. By default, +ARGV+ is used, but you can specify a
|
||||
# different set of parameters (such as when embedded ST in a program):
|
||||
# different set of parameters (such as when embedded cap in a program):
|
||||
#
|
||||
# require 'capistrano/cli'
|
||||
# Capistrano::CLI.new(%w(-vvvv -r config/deploy -a update_code)).execute!
|
||||
# Capistrano::CLI.parse(%w(-vvvv -r config/deploy update_code)).execute!
|
||||
#
|
||||
# Note that you can also embed ST directly by creating a new Configuration
|
||||
# Note that you can also embed cao directly by creating a new Configuration
|
||||
# instance and setting it up, but you'll often wind up duplicating logic
|
||||
# defined in the CLI class. The above snippet, redone using the Configuration
|
||||
# class directly, would look like:
|
||||
|
@ -87,261 +28,18 @@ module Capistrano
|
|||
# config = Capistrano::Configuration.new
|
||||
# config.logger_level = Capistrano::Logger::TRACE
|
||||
# config.set(:password) { Capistrano::CLI.password_prompt }
|
||||
# config.load "standard", "config/deploy"
|
||||
# config.actor.update_code
|
||||
# config.load "config/deploy"
|
||||
# config.update_code
|
||||
#
|
||||
# There may be times that you want/need the additional control offered by
|
||||
# manipulating the Configuration directly, but generally interfacing with
|
||||
# the CLI class is recommended.
|
||||
def initialize(args = ARGV)
|
||||
@args = args
|
||||
@options = { :recipes => [], :actions => [], :vars => {},
|
||||
:pre_vars => {}, :sysconf => default_sysconf, :dotfile => default_dotfile }
|
||||
|
||||
OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{$0} [options] [args]"
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Recipe Options -----------------------"
|
||||
opts.separator ""
|
||||
|
||||
opts.on("-a", "--action ACTION",
|
||||
"An action to execute. Multiple actions may",
|
||||
"be specified, and are loaded in the given order."
|
||||
) { |value| @options[:actions] << value }
|
||||
|
||||
opts.on("-f", "--file FILE",
|
||||
"A recipe file to load. Multiple recipes may",
|
||||
"be specified, and are loaded in the given order."
|
||||
) { |value| @options[:recipes] << value }
|
||||
|
||||
opts.on("-p", "--password [PASSWORD]",
|
||||
"The password to use when connecting. If the switch",
|
||||
"is given without a password, the password will be",
|
||||
"prompted for immediately. (Default: prompt for password",
|
||||
"the first time it is needed.)"
|
||||
) { |value| @options[:password] = value }
|
||||
|
||||
opts.on("-r", "--recipe RECIPE",
|
||||
"A recipe file to load. Multiple recipes may",
|
||||
"be specified, and are loaded in the given order.",
|
||||
"(This option is deprecated--please use -f instead)"
|
||||
) do |value|
|
||||
warn "Deprecated -r/--recipe flag used. Please use -f instead"
|
||||
@options[:recipes] << value
|
||||
end
|
||||
|
||||
opts.on("-s", "--set NAME=VALUE",
|
||||
"Specify a variable and it's value to set. This",
|
||||
"will be set after loading all recipe files."
|
||||
) do |pair|
|
||||
name, value = pair.split(/=/, 2)
|
||||
@options[:vars][name.to_sym] = value
|
||||
end
|
||||
|
||||
opts.on("-S", "--set-before NAME=VALUE",
|
||||
"Specify a variable and it's value to set. This",
|
||||
"will be set BEFORE loading all recipe files."
|
||||
) do |pair|
|
||||
name, value = pair.split(/=/, 2)
|
||||
@options[:pre_vars][name.to_sym] = value
|
||||
end
|
||||
|
||||
opts.on("-x", "--skip-config",
|
||||
"Disables the loading of the default personal config",
|
||||
"file. Specifying -C after this option will reenable",
|
||||
"it. (Default: config file is loaded)"
|
||||
) { @options[:dotfile] = nil }
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Framework Integration Options --------"
|
||||
opts.separator ""
|
||||
|
||||
opts.on("-A", "--apply-to DIRECTORY",
|
||||
"Create a minimal set of scripts and recipes to use",
|
||||
"capistrano with the application at the given",
|
||||
"directory. (Currently only works with Rails apps.)"
|
||||
) { |value| @options[:apply_to] = value }
|
||||
|
||||
opts.separator ""
|
||||
opts.separator "Miscellaneous Options ----------------"
|
||||
opts.separator ""
|
||||
|
||||
opts.on("-h", "--help", "Display this help message") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
|
||||
opts.on("-P", "--[no-]pretend",
|
||||
"Run the task(s), but don't actually connect to or",
|
||||
"execute anything on the servers. (For various reasons",
|
||||
"this will not necessarily be an accurate depiction",
|
||||
"of the work that will actually be performed.",
|
||||
"Default: don't pretend.)"
|
||||
) { |value| @options[:pretend] = value }
|
||||
|
||||
opts.on("-q", "--quiet",
|
||||
"Make the output as quiet as possible (the default)"
|
||||
) { @options[:verbose] = 0 }
|
||||
|
||||
opts.on("-v", "--verbose",
|
||||
"Specify the verbosity of the output.",
|
||||
"May be given multiple times. (Default: silent)"
|
||||
) { @options[:verbose] ||= 0; @options[:verbose] += 1 }
|
||||
|
||||
opts.on("-V", "--version",
|
||||
"Display the version info for this utility"
|
||||
) do
|
||||
require 'capistrano/version'
|
||||
puts "Capistrano v#{Capistrano::Version::STRING}"
|
||||
exit
|
||||
end
|
||||
|
||||
opts.separator ""
|
||||
opts.separator <<-DETAIL.split(/\n/)
|
||||
You can use the --apply-to switch to generate a minimal set of capistrano
|
||||
scripts and recipes for an application. Just specify the path to the application
|
||||
as the argument to --apply-to, like this:
|
||||
|
||||
cap --apply-to ~/projects/myapp
|
||||
|
||||
You'll wind up with a sample deployment recipe in config/deploy.rb and some new
|
||||
rake tasks in lib/tasks.
|
||||
|
||||
(Currently, --apply-to only works with Rails applications.)
|
||||
DETAIL
|
||||
#' # vim syntax highlighting fix
|
||||
|
||||
if args.empty?
|
||||
puts opts
|
||||
exit
|
||||
else
|
||||
opts.parse!(args)
|
||||
end
|
||||
end
|
||||
|
||||
check_options!
|
||||
|
||||
password_proc = Proc.new { self.class.password_prompt }
|
||||
|
||||
if !@options.has_key?(:password)
|
||||
@options[:password] = password_proc
|
||||
elsif !@options[:password]
|
||||
@options[:password] = password_proc.call
|
||||
end
|
||||
def initialize(args)
|
||||
@args = args.dup
|
||||
end
|
||||
|
||||
# Beginning running Capistrano based on the configured options.
|
||||
def execute!
|
||||
if @options[:apply_to]
|
||||
execute_apply_to!
|
||||
else
|
||||
execute_recipes!
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Load the recipes specified by the options, and execute the actions
|
||||
# specified.
|
||||
def execute_recipes!
|
||||
config = Capistrano::Configuration.new
|
||||
config.logger.level = options[:verbose]
|
||||
config.set :password, options[:password]
|
||||
config.set :pretend, options[:pretend]
|
||||
|
||||
options[:pre_vars].each { |name, value| config.set(name, value) }
|
||||
|
||||
# load the standard recipe definition
|
||||
config.load "standard"
|
||||
|
||||
# load systemwide config/recipe definition
|
||||
config.load(@options[:sysconf]) if @options[:sysconf] && File.exist?(@options[:sysconf])
|
||||
|
||||
# load user config/recipe definition
|
||||
config.load(@options[:dotfile]) if @options[:dotfile] && File.exist?(@options[:dotfile])
|
||||
|
||||
options[:recipes].each { |recipe| config.load(recipe) }
|
||||
options[:vars].each { |name, value| config.set(name, value) }
|
||||
|
||||
actor = config.actor
|
||||
options[:actions].each { |action| actor.send action }
|
||||
rescue Exception => error
|
||||
handle_error(error)
|
||||
end
|
||||
|
||||
# Load the Rails generator and apply it to the specified directory.
|
||||
def execute_apply_to!
|
||||
require 'capistrano/generators/rails/loader'
|
||||
Generators::RailsLoader.load! @options
|
||||
end
|
||||
|
||||
APPLY_TO_OPTIONS = [:apply_to]
|
||||
RECIPE_OPTIONS = [:password]
|
||||
DEFAULT_RECIPES = %w(Capfile capfile config/deploy.rb)
|
||||
|
||||
# A sanity check to ensure that a valid operation is specified.
|
||||
def check_options!
|
||||
# if no verbosity has been specified, be verbose
|
||||
@options[:verbose] = 3 if !@options.has_key?(:verbose)
|
||||
|
||||
apply_to_given = !(@options.keys & APPLY_TO_OPTIONS).empty?
|
||||
recipe_given = !(@options.keys & RECIPE_OPTIONS).empty? ||
|
||||
!@options[:recipes].empty? ||
|
||||
!@options[:actions].empty?
|
||||
|
||||
if apply_to_given && recipe_given
|
||||
abort "You cannot specify both recipe options and framework integration options."
|
||||
elsif !apply_to_given
|
||||
look_for_default_recipe_file! if @options[:recipes].empty?
|
||||
look_for_raw_actions!
|
||||
abort "You must specify at least one action" if @options[:actions].empty?
|
||||
else
|
||||
@options[:application] = args.shift
|
||||
@options[:recipe_file] = args.shift
|
||||
end
|
||||
end
|
||||
|
||||
def default_sysconf
|
||||
File.join(sysconf_directory, "capistrano.conf")
|
||||
end
|
||||
|
||||
def default_dotfile
|
||||
File.join(home_directory, ".caprc")
|
||||
end
|
||||
|
||||
def sysconf_directory
|
||||
# I'm guessing at where Windows users would keep their conf file.
|
||||
ENV["SystemRoot"] || '/etc'
|
||||
end
|
||||
|
||||
def home_directory
|
||||
ENV["HOME"] ||
|
||||
(ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") ||
|
||||
"/"
|
||||
end
|
||||
|
||||
def look_for_default_recipe_file!
|
||||
DEFAULT_RECIPES.each do |file|
|
||||
if File.exist?(file)
|
||||
@options[:recipes] << file
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def look_for_raw_actions!
|
||||
@options[:actions].concat(@args)
|
||||
end
|
||||
|
||||
def handle_error(error)
|
||||
case error
|
||||
when Net::SSH::AuthenticationFailed
|
||||
abort "authentication failed for `#{error.message}'"
|
||||
when Capistrano::Command::Error
|
||||
abort(error.message)
|
||||
else raise error
|
||||
end
|
||||
end
|
||||
# Mix-in the actual behavior
|
||||
include Execute, Options, UI
|
||||
include Help # needs to be included last, because it overrides some methods
|
||||
end
|
||||
end
|
||||
|
|
73
lib/capistrano/cli/execute.rb
Normal file
73
lib/capistrano/cli/execute.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
require 'capistrano/configuration'
|
||||
|
||||
module Capistrano
|
||||
class CLI
|
||||
module Execute
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Invoke capistrano using the ARGV array as the option parameters. This
|
||||
# is what the command-line capistrano utility does.
|
||||
def execute
|
||||
parse(ARGV).execute!
|
||||
end
|
||||
end
|
||||
|
||||
# Using the options build when the command-line was parsed, instantiate
|
||||
# a new Capistrano configuration, initialize it, and execute the
|
||||
# requested actions.
|
||||
def execute!
|
||||
config = instantiate_configuration
|
||||
config.logger.level = options[:verbose]
|
||||
|
||||
set_pre_vars(config)
|
||||
load_recipes(config)
|
||||
|
||||
execute_requested_actions(config)
|
||||
rescue Exception => error
|
||||
handle_error(error)
|
||||
end
|
||||
|
||||
def execute_requested_actions(config)
|
||||
Array(options[:vars]).each { |name, value| config.set(name, value) }
|
||||
Array(options[:actions]).each { |action| config.find_and_execute_task(action) }
|
||||
end
|
||||
|
||||
def set_pre_vars(config) #:nodoc:
|
||||
config.set :password, options[:password]
|
||||
Array(options[:pre_vars]).each { |name, value| config.set(name, value) }
|
||||
end
|
||||
|
||||
def load_recipes(config) #:nodoc:
|
||||
# load the standard recipe definition
|
||||
config.load "standard"
|
||||
|
||||
# load systemwide config/recipe definition
|
||||
config.load(options[:sysconf]) if options[:sysconf] && File.file?(options[:sysconf])
|
||||
|
||||
# load user config/recipe definition
|
||||
config.load(options[:dotfile]) if options[:dotfile] && File.file?(options[:dotfile])
|
||||
|
||||
Array(options[:recipes]).each { |recipe| config.load(recipe) }
|
||||
end
|
||||
|
||||
# Primarily useful for testing, but subclasses of CLI could conceivably
|
||||
# override this method to return a Configuration subclass or replacement.
|
||||
def instantiate_configuration #:nodoc:
|
||||
Capistrano::Configuration.new
|
||||
end
|
||||
|
||||
def handle_error(error) #:nodoc:
|
||||
case error
|
||||
when Net::SSH::AuthenticationFailed
|
||||
abort "authentication failed for `#{error.message}'"
|
||||
when Capistrano::Error
|
||||
abort(error.message)
|
||||
else raise error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
76
lib/capistrano/cli/help.rb
Normal file
76
lib/capistrano/cli/help.rb
Normal file
|
@ -0,0 +1,76 @@
|
|||
module Capistrano
|
||||
class CLI
|
||||
module Help
|
||||
LINE_PADDING = 7
|
||||
MIN_MAX_LEN = 30
|
||||
HEADER_LEN = 60
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :execute_requested_actions_without_help, :execute_requested_actions
|
||||
base.send :alias_method, :execute_requested_actions, :execute_requested_actions_with_help
|
||||
end
|
||||
|
||||
def execute_requested_actions_with_help(config)
|
||||
if options[:tasks]
|
||||
task_list(config)
|
||||
elsif options[:explain]
|
||||
explain_task(config, options[:explain])
|
||||
else
|
||||
execute_requested_actions_without_help(config)
|
||||
end
|
||||
end
|
||||
|
||||
def task_list(config) #:nodoc:
|
||||
tasks = config.task_list(:all)
|
||||
|
||||
if tasks.empty?
|
||||
warn "There are no tasks available. Please specify a recipe file to load."
|
||||
else
|
||||
tasks = tasks.sort_by { |task| task.fully_qualified_name }
|
||||
|
||||
longest = tasks.map { |task| task.fully_qualified_name.length }.max
|
||||
max_length = output_columns - longest - LINE_PADDING
|
||||
max_length = MIN_MAX_LEN if max_length < MIN_MAX_LEN
|
||||
|
||||
tasks.each do |task|
|
||||
puts "cap %-#{longest}s # %s" % [task.fully_qualified_name, task.brief_description(max_length)]
|
||||
end
|
||||
|
||||
puts
|
||||
puts "Extended help may be available for any of these tasks."
|
||||
puts "Type `#{$0} -e taskname' to view it."
|
||||
end
|
||||
end
|
||||
|
||||
def explain_task(config, name) #:nodoc:
|
||||
task = config.find_task(name)
|
||||
if task.nil?
|
||||
warn "The task `#{name}' does not exist."
|
||||
else
|
||||
puts "-" * HEADER_LEN
|
||||
puts "cap #{name}"
|
||||
puts "-" * HEADER_LEN
|
||||
|
||||
if task.description.empty?
|
||||
puts "There is no description for this task."
|
||||
else
|
||||
task.description.each_line do |line|
|
||||
lines = line.gsub(/(.{1,#{output_columns}})(?:\s+|\Z)/, "\\1\n").split(/\n/)
|
||||
if lines.empty?
|
||||
puts
|
||||
else
|
||||
puts lines
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
def output_columns #:nodoc:
|
||||
@output_columns ||= self.class.ui.output_cols > 80 ? 80 : self.class.ui.output_cols
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
161
lib/capistrano/cli/options.rb
Normal file
161
lib/capistrano/cli/options.rb
Normal file
|
@ -0,0 +1,161 @@
|
|||
require 'optparse'
|
||||
|
||||
module Capistrano
|
||||
class CLI
|
||||
module Options
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Return a new CLI instance with the given arguments pre-parsed and
|
||||
# ready for execution.
|
||||
def parse(args)
|
||||
cli = new(args)
|
||||
cli.parse_options!
|
||||
cli
|
||||
end
|
||||
end
|
||||
|
||||
# The hash of (parsed) command-line options
|
||||
attr_reader :options
|
||||
|
||||
# Return an OptionParser instance that defines the acceptable command
|
||||
# line switches for Capistrano, and what their corresponding behaviors
|
||||
# are.
|
||||
def option_parser #:nodoc:
|
||||
@option_parser ||= OptionParser.new do |opts|
|
||||
opts.banner = "Usage: #{$0} [options] action ..."
|
||||
|
||||
opts.on("-e", "--explain TASK",
|
||||
"Displays help (if available) for the task."
|
||||
) { |value| options[:explain] = value }
|
||||
|
||||
opts.on("-f", "--file FILE",
|
||||
"A recipe file to load. May be given more than once."
|
||||
) { |value| options[:recipes] << value }
|
||||
|
||||
opts.on("-h", "--help", "Display this help message.") do
|
||||
puts opts
|
||||
exit
|
||||
end
|
||||
|
||||
opts.on("-p", "--password",
|
||||
"Immediately prompt for the password."
|
||||
) { options[:password] = nil }
|
||||
|
||||
opts.on("-q", "--quiet",
|
||||
"Make the output as quiet as possible (default)"
|
||||
) { options[:verbose] = 0 }
|
||||
|
||||
opts.on("-S", "--set-before NAME=VALUE",
|
||||
"Set a variable before the recipes are loaded."
|
||||
) do |pair|
|
||||
name, value = pair.split(/=/, 2)
|
||||
options[:pre_vars][name.to_sym] = value
|
||||
end
|
||||
|
||||
opts.on("-s", "--set NAME=VALUE",
|
||||
"Set a variable after the recipes are loaded."
|
||||
) do |pair|
|
||||
name, value = pair.split(/=/, 2)
|
||||
options[:vars][name.to_sym] = value
|
||||
end
|
||||
|
||||
opts.on("-T", "--tasks",
|
||||
"List all tasks in the loaded recipe files."
|
||||
) { |value| options[:tasks] = true }
|
||||
|
||||
opts.on("-V", "--version",
|
||||
"Display the Capistrano version, and exit."
|
||||
) do
|
||||
require 'capistrano/version'
|
||||
puts "Capistrano v#{Capistrano::Version::STRING}"
|
||||
exit
|
||||
end
|
||||
|
||||
opts.on("-v", "--verbose",
|
||||
"Be more verbose. May be given more than once."
|
||||
) { options[:verbose] ||= 0; options[:verbose] += 1 }
|
||||
|
||||
opts.on("-X", "--skip-system-config",
|
||||
"Don't load the system config file (capistrano.conf)"
|
||||
) { options.delete(:sysconf) }
|
||||
|
||||
opts.on("-x", "--skip-user-config",
|
||||
"Don't load the user config file (.caprc)"
|
||||
) { options.delete(:dotfile) }
|
||||
end
|
||||
end
|
||||
|
||||
# If the arguments to the command are empty, this will print the
|
||||
# allowed options and exit. Otherwise, it will parse the command
|
||||
# line and set up any default options.
|
||||
def parse_options! #:nodoc:
|
||||
@options = { :recipes => [], :actions => [],
|
||||
:vars => {}, :pre_vars => {},
|
||||
:sysconf => default_sysconf, :dotfile => default_dotfile }
|
||||
|
||||
if args.empty?
|
||||
warn "Please specify at least one action to execute."
|
||||
warn option_parser
|
||||
exit
|
||||
end
|
||||
|
||||
option_parser.parse!(args)
|
||||
|
||||
# if no verbosity has been specified, be verbose
|
||||
options[:verbose] = 3 if !options.has_key?(:verbose)
|
||||
|
||||
look_for_default_recipe_file! if options[:recipes].empty?
|
||||
extract_environment_variables!
|
||||
|
||||
options[:actions].concat(args)
|
||||
|
||||
password = options.has_key?(:password)
|
||||
options[:password] = Proc.new { self.class.password_prompt }
|
||||
options[:password] = options[:password].call if password
|
||||
end
|
||||
|
||||
# Extracts name=value pairs from the remaining command-line arguments
|
||||
# and assigns them as environment variables.
|
||||
def extract_environment_variables! #:nodoc:
|
||||
args.delete_if do |arg|
|
||||
next unless arg.match(/^(\w+)=(.*)$/)
|
||||
ENV[$1] = $2
|
||||
end
|
||||
end
|
||||
|
||||
# Looks for a default recipe file in the current directory.
|
||||
def look_for_default_recipe_file! #:nodoc:
|
||||
%w(Capfile capfile).each do |file|
|
||||
if File.file?(file)
|
||||
options[:recipes] << file
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def default_sysconf #:nodoc:
|
||||
File.join(sysconf_directory, "capistrano.conf")
|
||||
end
|
||||
|
||||
def default_dotfile #:nodoc:
|
||||
File.join(home_directory, ".caprc")
|
||||
end
|
||||
|
||||
def sysconf_directory #:nodoc:
|
||||
# TODO if anyone cares, feel free to submit a patch that uses a more
|
||||
# appropriate location for this file in Windows.
|
||||
ENV["SystemRoot"] || '/etc'
|
||||
end
|
||||
|
||||
def home_directory #:nodoc:
|
||||
ENV["HOME"] ||
|
||||
(ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") ||
|
||||
"/"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
24
lib/capistrano/cli/ui.rb
Normal file
24
lib/capistrano/cli/ui.rb
Normal file
|
@ -0,0 +1,24 @@
|
|||
require 'highline'
|
||||
|
||||
module Capistrano
|
||||
class CLI
|
||||
module UI
|
||||
def self.included(base) #:nodoc:
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Return the object that provides UI-specific methods, such as prompts
|
||||
# and more.
|
||||
def ui
|
||||
@ui ||= HighLine.new
|
||||
end
|
||||
|
||||
# Prompt for a password using echo suppression.
|
||||
def password_prompt(prompt="Password: ")
|
||||
ui.ask(prompt) { |q| q.echo = false }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,6 @@
|
|||
#require 'capistrano/extensions'
|
||||
require 'capistrano/logger'
|
||||
require 'capistrano/extensions'
|
||||
require 'capistrano/utils'
|
||||
|
||||
require 'capistrano/configuration/connections'
|
||||
require 'capistrano/configuration/execution'
|
||||
|
|
|
@ -11,7 +11,7 @@ module Capistrano
|
|||
# one. Do note that this is quite expensive from a bandwidth
|
||||
# perspective, so use it with care.
|
||||
#
|
||||
# The command is invoked via #invoke.
|
||||
# The command is invoked via #invoke_command.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
|
@ -20,7 +20,7 @@ module Capistrano
|
|||
# stream "tail -f #{shared_path}/log/fastcgi.crash.log"
|
||||
# end
|
||||
def stream(command, options={})
|
||||
invoke(command, options) do |ch, stream, out|
|
||||
invoke_command(command, options) do |ch, stream, out|
|
||||
puts out if stream == :out
|
||||
warn "[err :: #{ch[:host]}] #{out}" if stream == :err
|
||||
end
|
||||
|
@ -28,10 +28,10 @@ module Capistrano
|
|||
|
||||
# Executes the given command on the first server targetted by the
|
||||
# current task, collects it's stdout into a string, and returns the
|
||||
# string. The command is invoked via #invoke.
|
||||
# string. The command is invoked via #invoke_command.
|
||||
def capture(command, options={})
|
||||
output = ""
|
||||
invoke(command, options.merge(:once => true)) do |ch, stream, data|
|
||||
invoke_command(command, options.merge(:once => true)) do |ch, stream, data|
|
||||
case stream
|
||||
when :out then output << data
|
||||
when :err then raise CaptureError, "error processing #{command.inspect}: #{data.inspect}"
|
||||
|
|
|
@ -21,7 +21,7 @@ module Capistrano
|
|||
# to determine what method to use to invoke the command. It defaults
|
||||
# to :run, but may be :sudo, or any other method that conforms to the
|
||||
# same interface as run and sudo.
|
||||
def invoke(cmd, options={}, &block)
|
||||
def invoke_command(cmd, options={}, &block)
|
||||
options = options.dup
|
||||
via = options.delete(:via) || :run
|
||||
send(via, cmd, options, &block)
|
||||
|
|
|
@ -45,16 +45,11 @@ module Capistrano
|
|||
# establish connections to servers defined via ServerDefinition objects.
|
||||
def connection_factory
|
||||
@connection_factory ||= begin
|
||||
options = { :user => fetch(:user, nil),
|
||||
:password => fetch(:password, nil),
|
||||
:port => fetch(:port, nil),
|
||||
:ssh_options => fetch(:ssh_options, nil) }
|
||||
|
||||
if exists?(:gateway)
|
||||
logger.debug "establishing connection to gateway `#{fetch(:gateway)}'"
|
||||
Gateway.new(ServerDefinition.new(fetch(:gateway)), options.merge(:logger => logger))
|
||||
Gateway.new(ServerDefinition.new(fetch(:gateway)), self)
|
||||
else
|
||||
DefaultConnectionFactory.new(options)
|
||||
DefaultConnectionFactory.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
require 'capistrano/errors'
|
||||
|
||||
module Capistrano
|
||||
class Configuration
|
||||
module Execution
|
||||
|
@ -70,15 +72,9 @@ module Capistrano
|
|||
|
||||
# Executes the task with the given name, including the before and after
|
||||
# hooks.
|
||||
def execute_task(name, namespace, fail_silently=false)
|
||||
name = name.to_sym
|
||||
unless task = namespace.tasks[name]
|
||||
return if fail_silently
|
||||
fqn = " in `#{namespace.fully_qualified_name}'" if namespace.parent
|
||||
raise NoMethodError, "no such task `#{name}'#{fqn}"
|
||||
end
|
||||
|
||||
execute_task("before_#{name}", namespace, true)
|
||||
def execute_task(task)
|
||||
before = task.namespace.tasks[:"before_#{task.name}"]
|
||||
execute_task(before) if before
|
||||
logger.debug "executing `#{task.fully_qualified_name}'"
|
||||
|
||||
begin
|
||||
|
@ -88,10 +84,19 @@ module Capistrano
|
|||
pop_task_call_frame
|
||||
end
|
||||
|
||||
execute_task("after_#{name}", namespace, true)
|
||||
after = task.namespace.tasks[:"after_#{task.name}"]
|
||||
execute_task(after) if after
|
||||
result
|
||||
end
|
||||
|
||||
# Attempts to locate the task at the given fully-qualified path, and
|
||||
# execute it. If no such task exists, a Capistrano::NoSuchTaskError will
|
||||
# be raised.
|
||||
def find_and_execute_task(path)
|
||||
task = find_task(path) or raise NoSuchTaskError, "the task `#{path}' does not exist"
|
||||
execute_task(task)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def rollback!
|
||||
|
|
|
@ -3,6 +3,8 @@ require 'capistrano/task_definition'
|
|||
module Capistrano
|
||||
class Configuration
|
||||
module Namespaces
|
||||
DEFAULT_TASK = :default
|
||||
|
||||
def self.included(base) #:nodoc:
|
||||
base.send :alias_method, :initialize_without_namespaces, :initialize
|
||||
base.send :alias_method, :initialize, :initialize_with_namespaces
|
||||
|
@ -91,36 +93,76 @@ module Capistrano
|
|||
end
|
||||
end
|
||||
|
||||
# Find the task with the given name, where name is the fully-qualified
|
||||
# name of the task. This will search into the namespaces and return
|
||||
# the referenced task, or nil if no such task can be found. If the name
|
||||
# refers to a namespace, the task in that namespace named "default"
|
||||
# will be returned instead, if one exists.
|
||||
def find_task(name)
|
||||
parts = name.to_s.split(/:/)
|
||||
tail = parts.pop.to_sym
|
||||
|
||||
ns = self
|
||||
until parts.empty?
|
||||
ns = ns.namespaces[parts.shift.to_sym]
|
||||
return nil if ns.nil?
|
||||
end
|
||||
|
||||
if ns.namespaces.key?(tail)
|
||||
ns = ns.namespaces[tail]
|
||||
tail = DEFAULT_TASK
|
||||
end
|
||||
|
||||
ns.tasks[tail]
|
||||
end
|
||||
|
||||
# Returns the default task for this namespace. This will be +nil+ if
|
||||
# the namespace is at the top-level, and will otherwise return the
|
||||
# task named "default". If no such task exists, +nil+ will be returned.
|
||||
def default_task
|
||||
return nil if parent.nil?
|
||||
return tasks[DEFAULT_TASK]
|
||||
end
|
||||
|
||||
# Returns the tasks in this namespace as an array of TaskDefinition
|
||||
# objects. If a non-false parameter is given, all tasks in all
|
||||
# namespaces under this namespace will be returned as well.
|
||||
def task_list(all=false)
|
||||
list = tasks.values
|
||||
namespaces.each { |name,space| list.concat(space.task_list(:all)) } if all
|
||||
list
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_methods
|
||||
public_methods.concat(protected_methods).concat(private_methods)
|
||||
end
|
||||
|
||||
class Namespace
|
||||
def initialize(name, parent)
|
||||
@parent = parent
|
||||
@name = name
|
||||
end
|
||||
|
||||
def role(*args)
|
||||
raise NotImplementedError, "roles cannot be defined in a namespace"
|
||||
end
|
||||
|
||||
def respond_to?(sym)
|
||||
super || parent.respond_to?(sym)
|
||||
end
|
||||
|
||||
def method_missing(sym, *args, &block)
|
||||
if parent.respond_to?(sym)
|
||||
parent.send(sym, *args, &block)
|
||||
else
|
||||
super
|
||||
class Namespace
|
||||
def initialize(name, parent)
|
||||
@parent = parent
|
||||
@name = name
|
||||
end
|
||||
end
|
||||
|
||||
include Capistrano::Configuration::Namespaces
|
||||
end
|
||||
def role(*args)
|
||||
raise NotImplementedError, "roles cannot be defined in a namespace"
|
||||
end
|
||||
|
||||
def respond_to?(sym)
|
||||
super || parent.respond_to?(sym)
|
||||
end
|
||||
|
||||
def method_missing(sym, *args, &block)
|
||||
if parent.respond_to?(sym)
|
||||
parent.send(sym, *args, &block)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
include Capistrano::Configuration::Namespaces
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -91,6 +91,7 @@ module Capistrano
|
|||
@original_procs = {}
|
||||
|
||||
set :ssh_options, {}
|
||||
set :logger, logger
|
||||
end
|
||||
private :initialize_with_variables
|
||||
|
||||
|
|
|
@ -5,4 +5,5 @@ module Capistrano
|
|||
class CommandError < Error; end
|
||||
class ConnectionError < Error; end
|
||||
class UploadError < Error; end
|
||||
class NoSuchTaskError < Error; end
|
||||
end
|
|
@ -1,38 +1,36 @@
|
|||
require 'capistrano/actor'
|
||||
|
||||
module Capistrano
|
||||
class ExtensionProxy
|
||||
def initialize(actor, mod)
|
||||
@actor = actor
|
||||
extend(mod)
|
||||
end
|
||||
|
||||
def method_missing(sym, *args, &block)
|
||||
@actor.send(sym, *args, &block)
|
||||
end
|
||||
end
|
||||
|
||||
EXTENSIONS = {}
|
||||
|
||||
def self.plugin(name, mod)
|
||||
return false if EXTENSIONS.has_key?(name)
|
||||
|
||||
Capistrano::Actor.class_eval <<-STR, __FILE__, __LINE__+1
|
||||
def #{name}
|
||||
@__#{name}_proxy ||= Capistrano::ExtensionProxy.new(self, Capistrano::EXTENSIONS[#{name.inspect}])
|
||||
end
|
||||
STR
|
||||
|
||||
EXTENSIONS[name] = mod
|
||||
return true
|
||||
end
|
||||
|
||||
def self.remove_plugin(name)
|
||||
if EXTENSIONS.delete(name)
|
||||
Capistrano::Actor.send(:remove_method, name)
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
end
|
||||
# module Capistrano
|
||||
# class ExtensionProxy
|
||||
# def initialize(actor, mod)
|
||||
# @actor = actor
|
||||
# extend(mod)
|
||||
# end
|
||||
#
|
||||
# def method_missing(sym, *args, &block)
|
||||
# @actor.send(sym, *args, &block)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# EXTENSIONS = {}
|
||||
#
|
||||
# def self.plugin(name, mod)
|
||||
# return false if EXTENSIONS.has_key?(name)
|
||||
#
|
||||
# Capistrano::Actor.class_eval <<-STR, __FILE__, __LINE__+1
|
||||
# def #{name}
|
||||
# @__#{name}_proxy ||= Capistrano::ExtensionProxy.new(self, Capistrano::EXTENSIONS[#{name.inspect}])
|
||||
# end
|
||||
# STR
|
||||
#
|
||||
# EXTENSIONS[name] = mod
|
||||
# return true
|
||||
# end
|
||||
#
|
||||
# def self.remove_plugin(name)
|
||||
# if EXTENSIONS.delete(name)
|
||||
# Capistrano::Actor.send(:remove_method, name)
|
||||
# return true
|
||||
# end
|
||||
#
|
||||
# return false
|
||||
# end
|
||||
# end
|
||||
|
|
|
@ -73,7 +73,7 @@ module Capistrano
|
|||
# Net::SSH connection via that port.
|
||||
def connect_to(server)
|
||||
connection = nil
|
||||
logger.trace "establishing connection to `#{server.host}' via gateway" if logger
|
||||
logger.debug "establishing connection to `#{server.host}' via gateway" if logger
|
||||
local_port = next_port
|
||||
|
||||
thread = Thread.new do
|
||||
|
|
|
@ -1,287 +1,35 @@
|
|||
# Standard tasks that are useful for most recipes. It makes a few assumptions:
|
||||
#
|
||||
# * The :app role has been defined as the set of machines consisting of the
|
||||
# application servers.
|
||||
# * The :web role has been defined as the set of machines consisting of the
|
||||
# web servers.
|
||||
# * The :db role has been defined as the set of machines consisting of the
|
||||
# databases, with exactly one set up as the :primary DB server.
|
||||
# * The Rails spawner and reaper scripts are being used to manage the FCGI
|
||||
# processes.
|
||||
|
||||
set :rake, "rake"
|
||||
|
||||
set :rails_env, :production
|
||||
|
||||
set :migrate_target, :current
|
||||
set :migrate_env, ""
|
||||
|
||||
set :use_sudo, true
|
||||
set(:run_method) { use_sudo ? :sudo : :run }
|
||||
|
||||
set :spinner_user, :app
|
||||
|
||||
desc "Enumerate and describe every available task."
|
||||
task :show_tasks do
|
||||
puts "Available tasks"
|
||||
puts "---------------"
|
||||
each_task do |info|
|
||||
wrap_length = 80 - info[:longest] - 1
|
||||
lines = info[:desc].gsub(/(.{1,#{wrap_length}})(?:\s|\Z)+/, "\\1\n").split(/\n/)
|
||||
puts "%-#{info[:longest]}s %s" % [info[:task], lines.shift]
|
||||
puts "%#{info[:longest]}s %s" % ["", lines.shift] until lines.empty?
|
||||
puts
|
||||
end
|
||||
end
|
||||
|
||||
desc "Set up the expected application directory structure on all boxes"
|
||||
task :setup, :except => { :no_release => true } do
|
||||
run <<-CMD
|
||||
umask 02 &&
|
||||
mkdir -p #{deploy_to} #{releases_path} #{shared_path} #{shared_path}/system &&
|
||||
mkdir -p #{shared_path}/log &&
|
||||
mkdir -p #{shared_path}/pids
|
||||
CMD
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Disable the web server by writing a "maintenance.html" file to the web
|
||||
servers. The servers must be configured to detect the presence of this file,
|
||||
and if it is present, always display it instead of performing the request.
|
||||
Invoke a single command on the remote servers. This is useful for performing \
|
||||
one-off commands that may not require a full task to be written for them. \
|
||||
Simply specify the command to execute via the COMMAND environment variable. \
|
||||
To execute the command only on certain roles, specify the ROLES environment \
|
||||
variable as a comma-delimited list of role names. Alternatively, you can \
|
||||
specify the HOSTS environment variable as a comma-delimited list of hostnames \
|
||||
to execute the task on those hosts, explicitly. Lastly, if you want to \
|
||||
execute the command via sudo, specify a non-empty value for the SUDO \
|
||||
environment variable.
|
||||
|
||||
Sample usage:
|
||||
|
||||
$ cap COMMAND=uptime HOSTS=foo.capistano.test invoke
|
||||
$ cap ROLES=app,web SUDO=1 COMMAND="tail -f /var/log/messages" invoke
|
||||
DESC
|
||||
task :disable_web, :roles => :web do
|
||||
on_rollback { delete "#{shared_path}/system/maintenance.html" }
|
||||
|
||||
maintenance = render("maintenance", :deadline => ENV['UNTIL'],
|
||||
:reason => ENV['REASON'])
|
||||
put maintenance, "#{shared_path}/system/maintenance.html", :mode => 0644
|
||||
end
|
||||
|
||||
desc %(Re-enable the web server by deleting any "maintenance.html" file.)
|
||||
task :enable_web, :roles => :web do
|
||||
delete "#{shared_path}/system/maintenance.html"
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Sets group permissions on checkout. Useful for team environments, bad on
|
||||
shared hosts. Override this task if you're on a shared host.
|
||||
DESC
|
||||
task :set_permissions, :except => { :no_release => true } do
|
||||
run "chmod -R g+w #{release_path}"
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Update all servers with the latest release of the source code. All this does
|
||||
is do a checkout (as defined by the selected scm module).
|
||||
DESC
|
||||
task :update_code, :except => { :no_release => true } do
|
||||
on_rollback { delete release_path, :recursive => true }
|
||||
|
||||
source.checkout(self)
|
||||
|
||||
set_permissions
|
||||
|
||||
run <<-CMD
|
||||
rm -rf #{release_path}/log #{release_path}/public/system &&
|
||||
ln -nfs #{shared_path}/log #{release_path}/log &&
|
||||
ln -nfs #{shared_path}/system #{release_path}/public/system
|
||||
CMD
|
||||
|
||||
run <<-CMD
|
||||
test -d #{shared_path}/pids &&
|
||||
rm -rf #{release_path}/tmp/pids &&
|
||||
ln -nfs #{shared_path}/pids #{release_path}/tmp/pids; true
|
||||
CMD
|
||||
|
||||
# update the asset timestamps so they are in sync across all servers. This
|
||||
# lets the asset timestamping feature of rails work correctly
|
||||
stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
|
||||
asset_paths = %w(images stylesheets javascripts).map { |p| "#{release_path}/public/#{p}" }
|
||||
run "TZ=UTC find #{asset_paths.join(" ")} -exec touch -t #{stamp} {} \\;; true"
|
||||
|
||||
# uncache the list of releases, so that the next time it is called it will
|
||||
# include the newly released path.
|
||||
@releases = nil
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Rollback the latest checked-out version to the previous one by fixing the
|
||||
symlinks and deleting the current release from all servers.
|
||||
DESC
|
||||
task :rollback_code, :except => { :no_release => true } do
|
||||
if releases.length < 2
|
||||
raise "could not rollback the code because there is no prior release"
|
||||
else
|
||||
run <<-CMD
|
||||
ln -nfs #{previous_release} #{current_path} &&
|
||||
rm -rf #{current_release}
|
||||
CMD
|
||||
end
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Update the 'current' symlink to point to the latest version of
|
||||
the application's code.
|
||||
DESC
|
||||
task :symlink, :except => { :no_release => true } do
|
||||
on_rollback { run "ln -nfs #{previous_release} #{current_path}" }
|
||||
run "ln -nfs #{current_release} #{current_path}"
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Restart the FCGI processes on the app server. This uses the :use_sudo
|
||||
variable to determine whether to use sudo or not. By default, :use_sudo is
|
||||
set to true, but you can set it to false if you are in a shared environment.
|
||||
DESC
|
||||
task :restart, :roles => :app do
|
||||
send(run_method, "#{current_path}/script/process/reaper")
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Updates the code and fixes the symlink under a transaction
|
||||
DESC
|
||||
task :update do
|
||||
transaction do
|
||||
update_code
|
||||
symlink
|
||||
end
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Run the migrate rake task. By default, it runs this in the version of the app
|
||||
indicated by the 'current' symlink. (This means you should not invoke this task
|
||||
until the symlink has been updated to the most recent version.) However, you
|
||||
can specify a different release via the migrate_target variable, which must be
|
||||
one of "current" (for the default behavior), or "latest" (for the latest release
|
||||
to be deployed with the update_code task). You can also specify additional
|
||||
environment variables to pass to rake via the migrate_env variable. Finally, you
|
||||
can specify the full path to the rake executable by setting the rake variable.
|
||||
DESC
|
||||
task :migrate, :roles => :db, :only => { :primary => true } do
|
||||
directory = case migrate_target.to_sym
|
||||
when :current then current_path
|
||||
when :latest then current_release
|
||||
else
|
||||
raise ArgumentError,
|
||||
"you must specify one of current or latest for migrate_target"
|
||||
end
|
||||
|
||||
run "cd #{directory} && " +
|
||||
"#{rake} RAILS_ENV=#{rails_env} #{migrate_env} db:migrate"
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
A macro-task that updates the code, fixes the symlink, and restarts the
|
||||
application servers.
|
||||
DESC
|
||||
task :deploy do
|
||||
update
|
||||
restart
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Similar to deploy, but it runs the migrate task on the new release before
|
||||
updating the symlink. (Note that the update in this case it is not atomic,
|
||||
and transactions are not used, because migrations are not guaranteed to be
|
||||
reversible.)
|
||||
DESC
|
||||
task :deploy_with_migrations do
|
||||
update_code
|
||||
|
||||
begin
|
||||
old_migrate_target = migrate_target
|
||||
set :migrate_target, :latest
|
||||
migrate
|
||||
ensure
|
||||
set :migrate_target, old_migrate_target
|
||||
end
|
||||
|
||||
symlink
|
||||
|
||||
restart
|
||||
end
|
||||
|
||||
desc "A macro-task that rolls back the code and restarts the application servers."
|
||||
task :rollback do
|
||||
rollback_code
|
||||
restart
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Displays the diff between HEAD and what was last deployed. (Not available
|
||||
with all SCM's.)
|
||||
DESC
|
||||
task :diff_from_last_deploy do
|
||||
diff = source.diff(self)
|
||||
puts
|
||||
puts diff
|
||||
puts
|
||||
end
|
||||
|
||||
desc "Update the currently released version of the software directly via an SCM update operation"
|
||||
task :update_current do
|
||||
source.update(self)
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Removes unused releases from the releases directory. By default, the last 5
|
||||
releases are retained, but this can be configured with the 'keep_releases'
|
||||
variable. This will use sudo to do the delete by default, but you can specify
|
||||
that run should be used by setting the :use_sudo variable to false.
|
||||
DESC
|
||||
task :cleanup, :except => { :no_release => true } do
|
||||
count = (self[:keep_releases] || 5).to_i
|
||||
if count >= releases.length
|
||||
logger.important "no old releases to clean up"
|
||||
else
|
||||
logger.info "keeping #{count} of #{releases.length} deployed releases"
|
||||
directories = (releases - releases.last(count)).map { |release|
|
||||
File.join(releases_path, release) }.join(" ")
|
||||
|
||||
send(run_method, "rm -rf #{directories}")
|
||||
end
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Start the spinner daemon for the application (requires script/spin). This will
|
||||
use sudo to start the spinner by default, unless :use_sudo is false. If using
|
||||
sudo, you can specify the user that the spinner ought to run as by setting the
|
||||
:spinner_user variable (defaults to :app).
|
||||
DESC
|
||||
task :spinner, :roles => :app do
|
||||
user = (use_sudo && spinner_user) ? "-u #{spinner_user} " : ""
|
||||
send(run_method, "#{user}#{current_path}/script/spin")
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Used only for deploying when the spinner isn't running. It invokes 'update',
|
||||
and when it finishes it then invokes the spinner task (to start the spinner).
|
||||
DESC
|
||||
task :cold_deploy do
|
||||
update
|
||||
spinner
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
A simple task for performing one-off commands that may not require a full task
|
||||
to be written for them. Simply specify the command to execute via the COMMAND
|
||||
environment variable. To execute the command only on certain roles, specify
|
||||
the ROLES environment variable as a comma-delimited list of role names. Lastly,
|
||||
if you want to execute the command via sudo, specify a non-empty value for the
|
||||
SUDO environment variable.
|
||||
DESC
|
||||
task :invoke, :roles => Capistrano.str2roles(ENV["ROLES"] || "") do
|
||||
task :invoke do
|
||||
method = ENV["SUDO"] ? :sudo : :run
|
||||
send(method, ENV["COMMAND"])
|
||||
invoke_command(ENV["COMMAND"], :via => method)
|
||||
end
|
||||
|
||||
desc <<-DESC
|
||||
Begin an interactive Capistrano session. This gives you an interactive
|
||||
terminal from which to execute tasks and commands on all of your servers.
|
||||
(This is still an experimental feature, and is subject to change without
|
||||
Begin an interactive Capistrano session. This gives you an interactive \
|
||||
terminal from which to execute tasks and commands on all of your servers. \
|
||||
(This is still an experimental feature, and is subject to change without \
|
||||
notice!)
|
||||
|
||||
Sample usage:
|
||||
|
||||
$ cap shell
|
||||
DESC
|
||||
task(:shell) do
|
||||
task :shell do
|
||||
require 'capistrano/shell'
|
||||
Capistrano::Shell.run!(self)
|
||||
end
|
||||
Capistrano::Shell.run(self)
|
||||
end
|
|
@ -11,7 +11,7 @@ module Capistrano
|
|||
attr_reader :actor
|
||||
|
||||
# Instantiate a new shell and begin executing it immediately.
|
||||
def self.run!(actor)
|
||||
def self.run(actor)
|
||||
new(actor).run!
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,13 @@ module Capistrano
|
|||
|
||||
# Returns the task's fully-qualified name, including the namespace
|
||||
def fully_qualified_name
|
||||
@fully_qualified_name ||= [namespace.fully_qualified_name, name].compact.join(":")
|
||||
@fully_qualified_name ||= begin
|
||||
if namespace.default_task == self
|
||||
namespace.fully_qualified_name
|
||||
else
|
||||
[namespace.fully_qualified_name, name].compact.join(":")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the list of server definitions (_not_ connections to servers)
|
||||
|
@ -27,7 +33,33 @@ module Capistrano
|
|||
apply_except(apply_only(find_servers_by_role)).uniq
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# Returns the description for this task, with newlines collapsed and
|
||||
# whitespace stripped. Returns the empty string if there is no
|
||||
# description for this task.
|
||||
def description(rebuild=false)
|
||||
@description = nil if rebuild
|
||||
@description ||= begin
|
||||
description = options[:desc] || ""
|
||||
description.strip.
|
||||
gsub(/\r\n/, "\n").
|
||||
gsub(/\\\n/, " ")
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the first sentence of the full description. If +max_length+ is
|
||||
# given, the result will be truncated if it is longer than +max_length+,
|
||||
# and an ellipsis appended.
|
||||
def brief_description(max_length=nil)
|
||||
brief = description[/^.*?\./] || description
|
||||
|
||||
if max_length && brief.length > max_length
|
||||
brief = brief[0,max_length-3] + "..."
|
||||
end
|
||||
|
||||
brief
|
||||
end
|
||||
|
||||
private
|
||||
def find_servers_by_role
|
||||
roles = namespace.roles
|
||||
|
|
116
test/cli/execute_test.rb
Normal file
116
test/cli/execute_test.rb
Normal file
|
@ -0,0 +1,116 @@
|
|||
require "#{File.dirname(__FILE__)}/../utils"
|
||||
require 'capistrano/cli/execute'
|
||||
|
||||
class CLIExecuteTest < Test::Unit::TestCase
|
||||
class MockCLI
|
||||
attr_reader :options
|
||||
|
||||
def initialize
|
||||
@options = {}
|
||||
end
|
||||
|
||||
include Capistrano::CLI::Execute
|
||||
end
|
||||
|
||||
def setup
|
||||
@cli = MockCLI.new
|
||||
@logger = stub_everything
|
||||
@config = stub(:logger => @logger)
|
||||
@config.stubs(:set)
|
||||
@config.stubs(:load)
|
||||
@cli.stubs(:instantiate_configuration).returns(@config)
|
||||
end
|
||||
|
||||
def test_execute_should_set_logger_verbosity
|
||||
@cli.options[:verbose] = 7
|
||||
@logger.expects(:level=).with(7)
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_set_password
|
||||
@cli.options[:password] = "nosoup4u"
|
||||
@config.expects(:set).with(:password, "nosoup4u")
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_set_prevars_before_loading
|
||||
@config.expects(:load).never
|
||||
@config.expects(:set).with(:stage, "foobar").returns(Proc.new { @config.expects(:load).with("standard") })
|
||||
@cli.options[:pre_vars] = { :stage => "foobar" }
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_load_sysconf_if_sysconf_set_and_exists
|
||||
@cli.options[:sysconf] = "/etc/capistrano.conf"
|
||||
@config.expects(:load).with("/etc/capistrano.conf")
|
||||
File.expects(:file?).with("/etc/capistrano.conf").returns(true)
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_not_load_sysconf_when_sysconf_set_and_not_exists
|
||||
@cli.options[:sysconf] = "/etc/capistrano.conf"
|
||||
File.expects(:file?).with("/etc/capistrano.conf").returns(false)
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_load_dotfile_if_dotfile_set_and_exists
|
||||
@cli.options[:dotfile] = "/home/jamis/.caprc"
|
||||
@config.expects(:load).with("/home/jamis/.caprc")
|
||||
File.expects(:file?).with("/home/jamis/.caprc").returns(true)
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_not_load_dotfile_when_dotfile_set_and_not_exists
|
||||
@cli.options[:dotfile] = "/home/jamis/.caprc"
|
||||
File.expects(:file?).with("/home/jamis/.caprc").returns(false)
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_load_recipes_when_recipes_are_given
|
||||
@cli.options[:recipes] = %w(config/deploy path/to/extra)
|
||||
@config.expects(:load).with("config/deploy")
|
||||
@config.expects(:load).with("path/to/extra")
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_set_vars_and_execute_tasks
|
||||
@cli.options[:vars] = { :foo => "bar", :baz => "bang" }
|
||||
@cli.options[:actions] = %w(first second)
|
||||
@config.expects(:set).with(:foo, "bar")
|
||||
@config.expects(:set).with(:baz, "bang")
|
||||
@config.expects(:find_and_execute_task).with("first")
|
||||
@config.expects(:find_and_execute_task).with("second")
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_execute_should_call_handle_error_when_exceptions_occur
|
||||
@config.expects(:load).raises(Exception, "boom")
|
||||
@cli.expects(:handle_error).with { |e,| Exception === e }
|
||||
@cli.execute!
|
||||
end
|
||||
|
||||
def test_instantiate_configuration_should_return_new_configuration_instance
|
||||
assert_instance_of Capistrano::Configuration, MockCLI.new.instantiate_configuration
|
||||
end
|
||||
|
||||
def test_handle_error_with_auth_error_should_abort_with_message_including_user_name
|
||||
@cli.expects(:abort).with { |s| s.include?("jamis") }
|
||||
@cli.handle_error(Net::SSH::AuthenticationFailed.new("jamis"))
|
||||
end
|
||||
|
||||
def test_handle_error_with_cap_error_should_abort_with_message
|
||||
@cli.expects(:abort).with("Wish you were here")
|
||||
@cli.handle_error(Capistrano::Error.new("Wish you were here"))
|
||||
end
|
||||
|
||||
def test_handle_error_with_other_errors_should_reraise_error
|
||||
other_error = Class.new(RuntimeError)
|
||||
assert_raises(other_error) { @cli.handle_error(other_error.new("boom")) }
|
||||
end
|
||||
|
||||
def test_class_execute_method_should_call_parse_and_execute_with_ARGV
|
||||
cli = mock(:execute! => nil)
|
||||
MockCLI.expects(:parse).with(ARGV).returns(cli)
|
||||
MockCLI.execute
|
||||
end
|
||||
end
|
102
test/cli/help_test.rb
Normal file
102
test/cli/help_test.rb
Normal file
|
@ -0,0 +1,102 @@
|
|||
require "#{File.dirname(__FILE__)}/../utils"
|
||||
require 'capistrano/cli/help'
|
||||
|
||||
class CLIHelpTest < Test::Unit::TestCase
|
||||
class MockCLI
|
||||
attr_reader :options, :called_original
|
||||
|
||||
def initialize
|
||||
@options = {}
|
||||
@called_original = false
|
||||
end
|
||||
|
||||
def execute_requested_actions(config)
|
||||
@called_original = config
|
||||
end
|
||||
|
||||
include Capistrano::CLI::Help
|
||||
end
|
||||
|
||||
def setup
|
||||
@cli = MockCLI.new
|
||||
@ui = stub("ui", :output_cols => 80)
|
||||
MockCLI.stubs(:ui).returns(@ui)
|
||||
end
|
||||
|
||||
def test_execute_requested_actions_without_tasks_or_explain_should_call_original
|
||||
@cli.execute_requested_actions(:config)
|
||||
@cli.expects(:task_list).never
|
||||
@cli.expects(:explain_task).never
|
||||
assert_equal :config, @cli.called_original
|
||||
end
|
||||
|
||||
def test_execute_requested_actions_with_tasks_should_call_task_list
|
||||
@cli.options[:tasks] = true
|
||||
@cli.expects(:task_list).with(:config)
|
||||
@cli.expects(:explain_task).never
|
||||
@cli.execute_requested_actions(:config)
|
||||
assert !@cli.called_original
|
||||
end
|
||||
|
||||
def test_execute_requested_actions_with_explain_should_call_explain_task
|
||||
@cli.options[:explain] = "deploy_with_niftiness"
|
||||
@cli.expects(:task_list).never
|
||||
@cli.expects(:explain_task).with(:config, "deploy_with_niftiness")
|
||||
@cli.execute_requested_actions(:config)
|
||||
assert !@cli.called_original
|
||||
end
|
||||
|
||||
def test_task_list_with_no_tasks_should_emit_warning
|
||||
config = mock("config", :task_list => [])
|
||||
@cli.expects(:warn)
|
||||
@cli.task_list(config)
|
||||
end
|
||||
|
||||
def test_task_list_should_query_all_tasks_in_all_namespaces
|
||||
expected_max_len = 80 - 3 - MockCLI::LINE_PADDING
|
||||
task_list = [task("c"), task("g", "c:g"), task("b", "c:b"), task("a")]
|
||||
task_list.each { |t| t.expects(:brief_description).with(expected_max_len).returns(t.fully_qualified_name) }
|
||||
|
||||
config = mock("config")
|
||||
config.expects(:task_list).with(:all).returns(task_list)
|
||||
@cli.stubs(:puts)
|
||||
@cli.task_list(config)
|
||||
end
|
||||
|
||||
def test_task_list_should_never_use_less_than_MIN_MAX_LEN_chars_for_descriptions
|
||||
@ui.stubs(:output_cols).returns(20)
|
||||
t = task("c")
|
||||
t.expects(:brief_description).with(30).returns("hello")
|
||||
config = mock("config", :task_list => [t])
|
||||
@cli.stubs(:puts)
|
||||
@cli.task_list(config)
|
||||
end
|
||||
|
||||
def test_explain_task_should_warn_if_task_does_not_exist
|
||||
config = mock("config", :find_task => nil)
|
||||
@cli.expects(:warn).with { |s,| s =~ /`deploy_with_niftiness'/ }
|
||||
@cli.explain_task(config, "deploy_with_niftiness")
|
||||
end
|
||||
|
||||
def test_explain_task_with_task_that_has_no_description_should_emit_stub
|
||||
t = mock("task", :description => "")
|
||||
config = mock("config")
|
||||
config.expects(:find_task).with("deploy_with_niftiness").returns(t)
|
||||
@cli.stubs(:puts)
|
||||
@cli.expects(:puts).with("There is no description for this task.")
|
||||
@cli.explain_task(config, "deploy_with_niftiness")
|
||||
end
|
||||
|
||||
def test_explain_task_with_task_should_format_description
|
||||
t = stub("task", :description => "line1\nline2\n\nline3")
|
||||
config = mock("config", :find_task => t)
|
||||
@cli.stubs(:puts)
|
||||
@cli.explain_task(config, "deploy_with_niftiness")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def task(name, fqn=name)
|
||||
stub("task", :name => name, :fully_qualified_name => fqn)
|
||||
end
|
||||
end
|
186
test/cli/options_test.rb
Normal file
186
test/cli/options_test.rb
Normal file
|
@ -0,0 +1,186 @@
|
|||
require "#{File.dirname(__FILE__)}/../utils"
|
||||
require 'capistrano/cli/options'
|
||||
|
||||
class CLIOptionsTest < Test::Unit::TestCase
|
||||
class ExitException < Exception; end
|
||||
|
||||
class MockCLI
|
||||
def initialize
|
||||
@args = []
|
||||
end
|
||||
|
||||
attr_reader :args
|
||||
|
||||
include Capistrano::CLI::Options
|
||||
end
|
||||
|
||||
def setup
|
||||
@cli = MockCLI.new
|
||||
end
|
||||
|
||||
def test_parse_options_should_require_non_empty_args_list
|
||||
@cli.stubs(:warn)
|
||||
@cli.expects(:exit).raises(ExitException)
|
||||
assert_raises(ExitException) { @cli.parse_options! }
|
||||
end
|
||||
|
||||
def test_parse_options_with_e_should_set_explain_option
|
||||
@cli.args << "-e" << "sample"
|
||||
@cli.parse_options!
|
||||
assert_equal "sample", @cli.options[:explain]
|
||||
end
|
||||
|
||||
def test_parse_options_with_f_should_add_recipe_file
|
||||
@cli.args << "-f" << "deploy"
|
||||
@cli.parse_options!
|
||||
assert_equal %w(deploy), @cli.options[:recipes]
|
||||
end
|
||||
|
||||
def test_parse_options_with_multiple_f_should_add_each_as_recipe_file
|
||||
@cli.args << "-f" << "deploy" << "-f" << "monitor"
|
||||
@cli.parse_options!
|
||||
assert_equal %w(deploy monitor), @cli.options[:recipes]
|
||||
end
|
||||
|
||||
def test_parse_options_with_h_should_show_options_and_exit
|
||||
@cli.expects(:puts).with(@cli.option_parser)
|
||||
@cli.expects(:exit).raises(ExitException)
|
||||
@cli.args << "-h"
|
||||
assert_raises(ExitException) { @cli.parse_options! }
|
||||
end
|
||||
|
||||
def test_parse_options_with_p_should_prompt_for_password
|
||||
MockCLI.expects(:password_prompt).returns(:the_password)
|
||||
@cli.args << "-p"
|
||||
@cli.parse_options!
|
||||
assert_equal :the_password, @cli.options[:password]
|
||||
end
|
||||
|
||||
def test_parse_options_without_p_should_set_proc_for_password
|
||||
@cli.args << "-e" << "sample"
|
||||
@cli.parse_options!
|
||||
assert_instance_of Proc, @cli.options[:password]
|
||||
end
|
||||
|
||||
def test_parse_options_with_q_should_set_verbose_to_0
|
||||
@cli.args << "-q"
|
||||
@cli.parse_options!
|
||||
assert_equal 0, @cli.options[:verbose]
|
||||
end
|
||||
|
||||
def test_parse_options_with_S_should_set_pre_vars
|
||||
@cli.args << "-S" << "foo=bar"
|
||||
@cli.parse_options!
|
||||
assert_equal "bar", @cli.options[:pre_vars][:foo]
|
||||
end
|
||||
|
||||
def test_parse_options_with_s_should_set_vars
|
||||
@cli.args << "-s" << "foo=bar"
|
||||
@cli.parse_options!
|
||||
assert_equal "bar", @cli.options[:vars][:foo]
|
||||
end
|
||||
|
||||
def test_parse_options_with_T_should_set_tasks_option
|
||||
@cli.args << "-T"
|
||||
@cli.parse_options!
|
||||
assert @cli.options[:tasks]
|
||||
end
|
||||
|
||||
def test_parse_options_with_V_should_show_version_and_exit
|
||||
@cli.args << "-V"
|
||||
@cli.expects(:puts).with { |s| s.include?(Capistrano::Version::STRING) }
|
||||
@cli.expects(:exit).raises(ExitException)
|
||||
assert_raises(ExitException) { @cli.parse_options! }
|
||||
end
|
||||
|
||||
def test_parse_options_with_v_should_set_verbose_to_1
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
assert_equal 1, @cli.options[:verbose]
|
||||
end
|
||||
|
||||
def test_parse_options_with_multiple_v_should_set_verbose_accordingly
|
||||
@cli.args << "-vvvvvvv"
|
||||
@cli.parse_options!
|
||||
assert_equal 7, @cli.options[:verbose]
|
||||
end
|
||||
|
||||
def test_parse_options_without_X_should_set_sysconf
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
assert @cli.options.key?(:sysconf)
|
||||
end
|
||||
|
||||
def test_parse_options_with_X_should_unset_sysconf
|
||||
@cli.args << "-X"
|
||||
@cli.parse_options!
|
||||
assert !@cli.options.key?(:sysconf)
|
||||
end
|
||||
|
||||
def test_parse_options_without_x_should_set_dotfile
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
assert @cli.options.key?(:dotfile)
|
||||
end
|
||||
|
||||
def test_parse_options_with_x_should_unset_dotfile
|
||||
@cli.args << "-x"
|
||||
@cli.parse_options!
|
||||
assert !@cli.options.key?(:dotfile)
|
||||
end
|
||||
|
||||
def test_parse_options_without_q_or_v_should_set_verbose_to_3
|
||||
@cli.args << "-T"
|
||||
@cli.parse_options!
|
||||
assert_equal 3, @cli.options[:verbose]
|
||||
end
|
||||
|
||||
def test_should_search_for_default_recipes_if_f_not_given
|
||||
@cli.expects(:look_for_default_recipe_file!)
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
end
|
||||
|
||||
def test_should_not_search_for_default_recipes_if_f_given
|
||||
@cli.expects(:look_for_default_recipe_file!).never
|
||||
@cli.args << "-f" << "hello"
|
||||
@cli.parse_options!
|
||||
end
|
||||
|
||||
def test_should_extract_env_vars_from_command_line
|
||||
assert_nil ENV["HELLO"]
|
||||
assert_nil ENV["ANOTHER"]
|
||||
|
||||
@cli.args << "HELLO=world" << "hello" << "ANOTHER=value"
|
||||
@cli.parse_options!
|
||||
|
||||
assert_equal "world", ENV["HELLO"]
|
||||
assert_equal "value", ENV["ANOTHER"]
|
||||
ensure
|
||||
ENV["HELLO"] = ENV["ANOTHER"] = nil
|
||||
end
|
||||
|
||||
def test_remaining_args_should_be_added_to_actions_list
|
||||
@cli.args << "-v" << "HELLO=world" << "-f" << "foo" << "something" << "else"
|
||||
@cli.parse_options!
|
||||
assert_equal %w(something else), @cli.args
|
||||
ensure
|
||||
ENV["HELLO"] = nil
|
||||
end
|
||||
|
||||
def test_search_for_default_recipe_file_should_look_for_Capfile
|
||||
File.stubs(:file?).returns(false)
|
||||
File.expects(:file?).with("Capfile").returns(true)
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
assert_equal %w(Capfile), @cli.options[:recipes]
|
||||
end
|
||||
|
||||
def test_search_for_default_recipe_file_should_look_for_capfile
|
||||
File.stubs(:file?).returns(false)
|
||||
File.expects(:file?).with("capfile").returns(true)
|
||||
@cli.args << "-v"
|
||||
@cli.parse_options!
|
||||
assert_equal %w(capfile), @cli.options[:recipes]
|
||||
end
|
||||
end
|
28
test/cli/ui_test.rb
Normal file
28
test/cli/ui_test.rb
Normal file
|
@ -0,0 +1,28 @@
|
|||
require "#{File.dirname(__FILE__)}/../utils"
|
||||
require 'capistrano/cli/ui'
|
||||
|
||||
class CLIUITest < Test::Unit::TestCase
|
||||
class MockCLI
|
||||
include Capistrano::CLI::UI
|
||||
end
|
||||
|
||||
def test_ui_should_return_highline_instance
|
||||
assert_instance_of HighLine, MockCLI.ui
|
||||
end
|
||||
|
||||
def test_password_prompt_should_have_default_prompt_and_set_echo_false
|
||||
q = mock("question")
|
||||
q.expects(:echo=).with(false)
|
||||
ui = mock("ui")
|
||||
ui.expects(:ask).with("Password: ").yields(q).returns("sayuncle")
|
||||
MockCLI.expects(:ui).returns(ui)
|
||||
assert_equal "sayuncle", MockCLI.password_prompt
|
||||
end
|
||||
|
||||
def test_password_prompt_with_custom_prompt_should_use_custom_prompt
|
||||
ui = mock("ui")
|
||||
ui.expects(:ask).with("Give the passphrase: ").returns("sayuncle")
|
||||
MockCLI.expects(:ui).returns(ui)
|
||||
assert_equal "sayuncle", MockCLI.password_prompt("Give the passphrase: ")
|
||||
end
|
||||
end
|
|
@ -12,12 +12,12 @@ class ConfigurationActionsRunTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_stream_should_pass_options_through_to_run
|
||||
@config.expects(:invoke).with("tail -f foo.log", :once => true)
|
||||
@config.expects(:invoke_command).with("tail -f foo.log", :once => true)
|
||||
@config.stream("tail -f foo.log", :once => true)
|
||||
end
|
||||
|
||||
def test_stream_should_emit_stdout_via_puts
|
||||
@config.expects(:invoke).yields(mock("channel"), :out, "something streamed")
|
||||
@config.expects(:invoke_command).yields(mock("channel"), :out, "something streamed")
|
||||
@config.expects(:puts).with("something streamed")
|
||||
@config.expects(:warn).never
|
||||
@config.stream("tail -f foo.log")
|
||||
|
@ -26,33 +26,33 @@ class ConfigurationActionsRunTest < Test::Unit::TestCase
|
|||
def test_stream_should_emit_stderr_via_warn
|
||||
ch = mock("channel")
|
||||
ch.expects(:[]).with(:host).returns("capistrano")
|
||||
@config.expects(:invoke).yields(ch, :err, "something streamed")
|
||||
@config.expects(:invoke_command).yields(ch, :err, "something streamed")
|
||||
@config.expects(:puts).never
|
||||
@config.expects(:warn).with("[err :: capistrano] something streamed")
|
||||
@config.stream("tail -f foo.log")
|
||||
end
|
||||
|
||||
def test_capture_should_pass_options_merged_with_once_to_run
|
||||
@config.expects(:invoke).with("hostname", :foo => "bar", :once => true)
|
||||
@config.expects(:invoke_command).with("hostname", :foo => "bar", :once => true)
|
||||
@config.capture("hostname", :foo => "bar")
|
||||
end
|
||||
|
||||
def test_capture_with_stderr_result_should_raise_capture_error
|
||||
@config.expects(:invoke).yields(mock("channel"), :err, "boom")
|
||||
@config.expects(:invoke_command).yields(mock("channel"), :err, "boom")
|
||||
assert_raises(Capistrano::CaptureError) { @config.capture("hostname") }
|
||||
end
|
||||
|
||||
def test_capture_with_stdout_should_aggregate_and_return_stdout
|
||||
config_expects_invoke_to_loop_with(mock("channel"), "foo", "bar", "baz")
|
||||
config_expects_invoke_command_to_loop_with(mock("channel"), "foo", "bar", "baz")
|
||||
assert_equal "foobarbaz", @config.capture("hostname")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def config_expects_invoke_to_loop_with(channel, *output)
|
||||
def config_expects_invoke_command_to_loop_with(channel, *output)
|
||||
class <<@config
|
||||
attr_accessor :script, :channel
|
||||
def invoke(*args)
|
||||
def invoke_command(*args)
|
||||
script.each { |item| yield channel, :out, item }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
require "#{File.dirname(__FILE__)}/../../utils"
|
||||
require 'capistrano/configuration/actions/invocation'
|
||||
|
||||
class ConfigurationActionsRunTest < Test::Unit::TestCase
|
||||
class ConfigurationActionsInvocationTest < Test::Unit::TestCase
|
||||
class MockConfig
|
||||
attr_reader :options
|
||||
|
||||
|
@ -125,14 +125,14 @@ class ConfigurationActionsRunTest < Test::Unit::TestCase
|
|||
callback[a, b, c]
|
||||
end
|
||||
|
||||
def test_invoke_should_default_to_run
|
||||
def test_invoke_command_should_default_to_run
|
||||
@config.expects(:run).with("ls", :once => true)
|
||||
@config.invoke("ls", :once => true)
|
||||
@config.invoke_command("ls", :once => true)
|
||||
end
|
||||
|
||||
def test_invoke_should_delegate_to_method_identified_by_via
|
||||
def test_invoke_command_should_delegate_to_method_identified_by_via
|
||||
@config.expects(:foobar).with("ls", :once => true)
|
||||
@config.invoke("ls", :once => true, :via => :foobar)
|
||||
@config.invoke_command("ls", :once => true, :via => :foobar)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -16,6 +16,10 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase
|
|||
@values.fetch(*args)
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@values[key]
|
||||
end
|
||||
|
||||
def exists?(key)
|
||||
@values.key?(key)
|
||||
end
|
||||
|
@ -49,9 +53,8 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_default_connection_factory_honors_config_options
|
||||
@config.values.update(@ssh_options)
|
||||
server = server("capistrano")
|
||||
Capistrano::SSH.expects(:connect).with(server, @ssh_options).returns(:session)
|
||||
Capistrano::SSH.expects(:connect).with(server, @config).returns(:session)
|
||||
assert_equal :session, @config.connection_factory.connect_to(server)
|
||||
end
|
||||
|
||||
|
@ -65,7 +68,7 @@ class ConfigurationConnectionsTest < Test::Unit::TestCase
|
|||
def test_connection_factory_as_gateway_should_honor_config_options
|
||||
@config.values[:gateway] = "capistrano"
|
||||
@config.values.update(@ssh_options)
|
||||
Capistrano::SSH.expects(:connect).with { |s,opts| s.host == "capistrano" && opts == @ssh_options.merge(:logger => @config.logger) }.yields(stub_everything)
|
||||
Capistrano::SSH.expects(:connect).with { |s,opts| s.host == "capistrano" && opts == @config }.yields(stub_everything)
|
||||
assert_instance_of Capistrano::Gateway, @config.connection_factory
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ class ConfigurationExecutionTest < Test::Unit::TestCase
|
|||
class MockConfig
|
||||
attr_reader :tasks, :namespaces, :fully_qualified_name, :parent
|
||||
attr_reader :state, :original_initialize_called
|
||||
attr_accessor :logger
|
||||
attr_accessor :logger, :default_task
|
||||
|
||||
def initialize(options={})
|
||||
@original_initialize_called = true
|
||||
|
@ -31,47 +31,35 @@ class ConfigurationExecutionTest < Test::Unit::TestCase
|
|||
assert @config.task_call_frames.empty?
|
||||
end
|
||||
|
||||
def test_execute_task_with_unknown_task_should_raise_error
|
||||
assert_raises(NoMethodError) do
|
||||
@config.execute_task(:bogus, @config)
|
||||
end
|
||||
end
|
||||
|
||||
def test_execute_task_with_unknown_task_and_fail_silently_should_fail_silently
|
||||
assert_nothing_raised do
|
||||
@config.execute_task(:bogus, @config, true)
|
||||
end
|
||||
end
|
||||
|
||||
def test_execute_task_should_populate_call_stack
|
||||
new_task @config, :testing
|
||||
assert_nothing_raised { @config.execute_task :testing, @config }
|
||||
task = new_task @config, :testing
|
||||
assert_nothing_raised { @config.execute_task(task) }
|
||||
assert_equal %w(testing), @config.state[:testing][:stack]
|
||||
assert_nil @config.state[:testing][:history]
|
||||
assert @config.task_call_frames.empty?
|
||||
end
|
||||
|
||||
def test_nested_execute_task_should_add_to_call_stack
|
||||
new_task @config, :testing
|
||||
new_task(@config, :outer) { execute_task :testing, self }
|
||||
testing = new_task @config, :testing
|
||||
outer = new_task(@config, :outer) { execute_task(testing) }
|
||||
|
||||
assert_nothing_raised { @config.execute_task :outer, @config }
|
||||
assert_nothing_raised { @config.execute_task(outer) }
|
||||
assert_equal %w(outer testing), @config.state[:testing][:stack]
|
||||
assert_nil @config.state[:testing][:history]
|
||||
assert @config.task_call_frames.empty?
|
||||
end
|
||||
|
||||
def test_execute_task_should_execute_before_hook_if_defined
|
||||
new_task @config, :testing
|
||||
testing = new_task @config, :testing
|
||||
new_task @config, :before_testing
|
||||
@config.execute_task :testing, @config
|
||||
@config.execute_task(testing)
|
||||
assert_equal %w(before_testing testing), @config.state[:trail]
|
||||
end
|
||||
|
||||
def test_execute_task_should_execute_after_hook_if_defined
|
||||
new_task @config, :testing
|
||||
testing = new_task @config, :testing
|
||||
new_task @config, :after_testing
|
||||
@config.execute_task :testing, @config
|
||||
@config.execute_task(testing)
|
||||
assert_equal %w(testing after_testing), @config.state[:trail]
|
||||
end
|
||||
|
||||
|
@ -80,38 +68,38 @@ class ConfigurationExecutionTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_transaction_without_block_should_raise_argument_error
|
||||
new_task(@config, :testing) { transaction }
|
||||
assert_raises(ArgumentError) { @config.execute_task :testing, @config }
|
||||
testing = new_task(@config, :testing) { transaction }
|
||||
assert_raises(ArgumentError) { @config.execute_task(testing) }
|
||||
end
|
||||
|
||||
def test_transaction_should_initialize_transaction_history
|
||||
@config.state[:inspector] = stack_inspector
|
||||
new_task(@config, :testing) { transaction { instance_eval(&state[:inspector]) } }
|
||||
@config.execute_task :testing, @config
|
||||
testing = new_task(@config, :testing) { transaction { instance_eval(&state[:inspector]) } }
|
||||
@config.execute_task(testing)
|
||||
assert_equal [], @config.state[:testing][:history]
|
||||
end
|
||||
|
||||
def test_transaction_from_within_transaction_should_not_start_new_transaction
|
||||
new_task(@config, :third, &stack_inspector)
|
||||
new_task(@config, :second) { transaction { execute_task(:third, self) } }
|
||||
new_task(@config, :first) { transaction { execute_task(:second, self) } }
|
||||
third = new_task(@config, :third, &stack_inspector)
|
||||
second = new_task(@config, :second) { transaction { execute_task(third) } }
|
||||
first = new_task(@config, :first) { transaction { execute_task(second) } }
|
||||
# kind of fragile...not sure how else to check that transaction was only
|
||||
# really run twice...but if the transaction was REALLY run, logger.info
|
||||
# will be called once when it starts, and once when it finishes.
|
||||
@config.logger = mock()
|
||||
@config.logger.stubs(:debug)
|
||||
@config.logger.expects(:info).times(2)
|
||||
@config.execute_task :first, @config
|
||||
@config.execute_task(first)
|
||||
end
|
||||
|
||||
def test_exception_raised_in_transaction_should_call_all_registered_rollback_handlers_in_reverse_order
|
||||
new_task(@config, :aaa) { on_rollback { (state[:rollback] ||= []) << :aaa } }
|
||||
new_task(@config, :bbb) { on_rollback { (state[:rollback] ||= []) << :bbb } }
|
||||
new_task(@config, :ccc) {}
|
||||
new_task(@config, :ddd) { on_rollback { (state[:rollback] ||= []) << :ddd }; execute_task(:bbb, self); execute_task(:ccc, self) }
|
||||
new_task(@config, :eee) { transaction { execute_task(:ddd, self); execute_task(:aaa, self); raise "boom" } }
|
||||
aaa = new_task(@config, :aaa) { on_rollback { (state[:rollback] ||= []) << :aaa } }
|
||||
bbb = new_task(@config, :bbb) { on_rollback { (state[:rollback] ||= []) << :bbb } }
|
||||
ccc = new_task(@config, :ccc) {}
|
||||
ddd = new_task(@config, :ddd) { on_rollback { (state[:rollback] ||= []) << :ddd }; execute_task(bbb); execute_task(ccc) }
|
||||
eee = new_task(@config, :eee) { transaction { execute_task(ddd); execute_task(aaa); raise "boom" } }
|
||||
assert_raises(RuntimeError) do
|
||||
@config.execute_task :eee, @config
|
||||
@config.execute_task(eee)
|
||||
end
|
||||
assert_equal [:aaa, :bbb, :ddd], @config.state[:rollback]
|
||||
assert_nil @config.rollback_requests
|
||||
|
@ -119,15 +107,25 @@ class ConfigurationExecutionTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_exception_during_rollback_should_simply_be_logged_and_ignored
|
||||
new_task(@config, :aaa) { on_rollback { state[:aaa] = true; raise LoadError, "ouch" }; execute_task(:bbb, self) }
|
||||
new_task(@config, :bbb) { raise MadError, "boom" }
|
||||
new_task(@config, :ccc) { transaction { execute_task(:aaa, self) } }
|
||||
aaa = new_task(@config, :aaa) { on_rollback { state[:aaa] = true; raise LoadError, "ouch" }; execute_task(bbb) }
|
||||
bbb = new_task(@config, :bbb) { raise MadError, "boom" }
|
||||
ccc = new_task(@config, :ccc) { transaction { execute_task(aaa) } }
|
||||
assert_raises(NameError) do
|
||||
@config.execute_task :ccc, @config
|
||||
@config.execute_task(ccc)
|
||||
end
|
||||
assert @config.state[:aaa]
|
||||
end
|
||||
|
||||
def test_find_and_execute_task_should_raise_error_when_task_cannot_be_found
|
||||
@config.expects(:find_task).with("path:to:task").returns(nil)
|
||||
assert_raises(Capistrano::NoSuchTaskError) { @config.find_and_execute_task("path:to:task") }
|
||||
end
|
||||
|
||||
def test_find_and_execute_task_should_execute_task_when_task_is_found
|
||||
@config.expects(:find_task).with("path:to:task").returns(:found)
|
||||
@config.expects(:execute_task).with(:found)
|
||||
assert_nothing_raised { @config.find_and_execute_task("path:to:task") }
|
||||
end
|
||||
private
|
||||
|
||||
def stack_inspector
|
||||
|
|
|
@ -159,4 +159,78 @@ class ConfigurationNamespacesDSLTest < Test::Unit::TestCase
|
|||
@config.namespace(:outer) { namespace(:inner) {} }
|
||||
assert_equal @config.namespaces[:outer], @config.namespaces[:outer].namespaces[:inner].parent
|
||||
end
|
||||
|
||||
def test_find_task_should_dereference_nested_tasks
|
||||
@config.namespace(:outer) do
|
||||
namespace(:inner) { task(:nested) { puts "nested" } }
|
||||
end
|
||||
|
||||
task = @config.find_task("outer:inner:nested")
|
||||
assert_not_nil task
|
||||
assert_equal "outer:inner:nested", task.fully_qualified_name
|
||||
end
|
||||
|
||||
def test_find_task_should_return_nil_if_no_task_matches
|
||||
assert_nil @config.find_task("outer:inner:nested")
|
||||
end
|
||||
|
||||
def test_find_task_should_return_default_if_deferences_to_namespace_and_namespace_has_default
|
||||
@config.namespace(:outer) do
|
||||
namespace(:inner) { task(:default) { puts "nested" } }
|
||||
end
|
||||
|
||||
task = @config.find_task("outer:inner")
|
||||
assert_not_nil task
|
||||
assert_equal :default, task.name
|
||||
assert_equal "outer:inner", task.namespace.fully_qualified_name
|
||||
end
|
||||
|
||||
def test_find_task_should_return_nil_if_deferences_to_namespace_and_namespace_has_no_default
|
||||
@config.namespace(:outer) do
|
||||
namespace(:inner) { task(:nested) { puts "nested" } }
|
||||
end
|
||||
|
||||
assert_nil @config.find_task("outer:inner")
|
||||
end
|
||||
|
||||
def test_default_task_should_return_nil_for_top_level
|
||||
@config.task(:default) {}
|
||||
assert_nil @config.default_task
|
||||
end
|
||||
|
||||
def test_default_task_should_return_nil_for_namespace_without_default
|
||||
@config.namespace(:outer) { task(:nested) { puts "nested" } }
|
||||
assert_nil @config.namespaces[:outer].default_task
|
||||
end
|
||||
|
||||
def test_default_task_should_return_task_for_namespace_with_default
|
||||
@config.namespace(:outer) { task(:default) { puts "nested" } }
|
||||
task = @config.namespaces[:outer].default_task
|
||||
assert_not_nil task
|
||||
assert_equal :default, task.name
|
||||
end
|
||||
|
||||
def test_task_list_should_return_only_tasks_immediately_within_namespace
|
||||
@config.task(:first) { puts "here" }
|
||||
@config.namespace(:outer) do
|
||||
task(:second) { puts "here" }
|
||||
namespace(:inner) do
|
||||
task(:third) { puts "here" }
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal %w(first), @config.task_list.map { |t| t.fully_qualified_name }
|
||||
end
|
||||
|
||||
def test_task_list_with_all_should_return_all_tasks_under_this_namespace_recursively
|
||||
@config.task(:first) { puts "here" }
|
||||
@config.namespace(:outer) do
|
||||
task(:second) { puts "here" }
|
||||
namespace(:inner) do
|
||||
task(:third) { puts "here" }
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal %w(first outer:inner:third outer:second), @config.task_list(:all).map { |t| t.fully_qualified_name }.sort
|
||||
end
|
||||
end
|
|
@ -13,12 +13,13 @@ class ConfigurationVariablesTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def setup
|
||||
MockConfig.any_instance.stubs(:logger).returns(stub_everything)
|
||||
@config = MockConfig.new
|
||||
end
|
||||
|
||||
def test_initialize_should_initialize_variables_hash
|
||||
assert @config.original_initialize_called
|
||||
assert_equal({:ssh_options => {}}, @config.variables)
|
||||
assert_equal({:ssh_options => {}, :logger => @config.logger}, @config.variables)
|
||||
end
|
||||
|
||||
def test_set_should_add_variable_to_hash
|
||||
|
|
|
@ -13,7 +13,7 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_task_without_roles_should_apply_to_all_defined_hosts
|
||||
task = new_task(:testing, @namespace)
|
||||
task = new_task(:testing)
|
||||
assert_equal %w(app1 app2 app3 web1 web2 file).sort, task.servers.map { |s| s.host }.sort
|
||||
end
|
||||
|
||||
|
@ -39,7 +39,7 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
|
||||
def test_task_with_roles_as_environment_variable_should_apply_only_to_that_role
|
||||
ENV['ROLES'] = "app,file"
|
||||
task = new_task(:testing, @namespace)
|
||||
task = new_task(:testing)
|
||||
assert_equal %w(app1 app2 app3 file).sort, task.servers.map { |s| s.host }.sort
|
||||
ensure
|
||||
ENV['ROLES'] = nil
|
||||
|
@ -47,7 +47,7 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
|
||||
def test_task_with_hosts_as_environment_variable_should_apply_only_to_those_hosts
|
||||
ENV['HOSTS'] = "foo,bar"
|
||||
task = new_task(:testing, @namespace)
|
||||
task = new_task(:testing)
|
||||
assert_equal %w(foo bar).sort, task.servers.map { |s| s.host }.sort
|
||||
ensure
|
||||
ENV['HOSTS'] = nil
|
||||
|
@ -64,7 +64,7 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
end
|
||||
|
||||
def test_fqn_at_top_level_should_be_task_name
|
||||
task = new_task(:testing, @namespace)
|
||||
task = new_task(:testing)
|
||||
assert_equal "testing", task.fully_qualified_name
|
||||
end
|
||||
|
||||
|
@ -74,16 +74,56 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
assert_equal "outer:inner:testing", task.fully_qualified_name
|
||||
end
|
||||
|
||||
def test_fqn_at_top_level_when_default_should_be_default
|
||||
task = new_task(:default)
|
||||
assert_equal "default", task.fully_qualified_name
|
||||
end
|
||||
|
||||
def test_fqn_in_namespace_when_default_should_be_namespace_fqn
|
||||
ns = namespace("outer:inner")
|
||||
task = new_task(:default, ns)
|
||||
ns.stubs(:default_task => task)
|
||||
assert_equal "outer:inner", task.fully_qualified_name
|
||||
end
|
||||
|
||||
def test_task_should_require_block
|
||||
assert_raises(ArgumentError) do
|
||||
Capistrano::TaskDefinition.new(:testing, @namespace)
|
||||
end
|
||||
end
|
||||
|
||||
def test_description_should_return_empty_string_if_not_given
|
||||
assert_equal "", new_task(:testing).description
|
||||
end
|
||||
|
||||
def test_description_should_return_desc_attribute
|
||||
assert_equal "something", new_task(:testing, @namespace, :desc => "something").description
|
||||
end
|
||||
|
||||
def test_description_should_strip_leading_and_trailing_whitespace
|
||||
assert_equal "something", new_task(:testing, @namespace, :desc => " something ").description
|
||||
end
|
||||
|
||||
def test_description_should_normalize_newlines
|
||||
assert_equal "a\nb\nc", new_task(:testing, @namespace, :desc => "a\nb\r\nc").description
|
||||
end
|
||||
|
||||
def test_task_brief_description_should_return_first_sentence_in_description
|
||||
desc = "This is the task. It does all kinds of things."
|
||||
task = new_task(:testing, @namespace, :desc => desc)
|
||||
assert_equal "This is the task.", task.brief_description
|
||||
end
|
||||
|
||||
def test_task_brief_description_should_truncate_if_length_given
|
||||
desc = "This is the task that does all kinds of things. And then some."
|
||||
task = new_task(:testing, @namespace, :desc => desc)
|
||||
assert_equal "This is the task ...", task.brief_description(20)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def namespace(fqn=nil)
|
||||
space = stub(:roles => {}, :fully_qualified_name => fqn)
|
||||
space = stub(:roles => {}, :fully_qualified_name => fqn, :default_task => nil)
|
||||
yield(space) if block_given?
|
||||
space
|
||||
end
|
||||
|
@ -94,7 +134,7 @@ class TaskDefinitionTest < Test::Unit::TestCase
|
|||
space.roles[name].concat(args.map { |h| Capistrano::ServerDefinition.new(h, opts) })
|
||||
end
|
||||
|
||||
def new_task(name, namespace, options={}, &block)
|
||||
def new_task(name, namespace=@namespace, options={}, &block)
|
||||
block ||= Proc.new {}
|
||||
task = Capistrano::TaskDefinition.new(name, namespace, options, &block)
|
||||
assert_equal block, task.body
|
||||
|
|
Loading…
Reference in a new issue