1
0
Fork 0
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:
Jamis Buck 2007-03-04 18:19:26 +00:00
parent b5daf9f58b
commit bdef53cbfd
32 changed files with 1160 additions and 758 deletions

View file

@ -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]

View file

@ -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

View file

@ -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"

View file

@ -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

View 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

View 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

View 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
View 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

View file

@ -1,5 +1,6 @@
#require 'capistrano/extensions'
require 'capistrano/logger'
require 'capistrano/extensions'
require 'capistrano/utils'
require 'capistrano/configuration/connections'
require 'capistrano/configuration/execution'

View file

@ -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}"

View file

@ -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)

View file

@ -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

View file

@ -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!

View file

@ -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

View file

@ -91,6 +91,7 @@ module Capistrano
@original_procs = {}
set :ssh_options, {}
set :logger, logger
end
private :initialize_with_variables

View file

@ -5,4 +5,5 @@ module Capistrano
class CommandError < Error; end
class ConnectionError < Error; end
class UploadError < Error; end
class NoSuchTaskError < Error; end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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
View 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
View 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
View 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
View 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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