mirror of
https://github.com/capistrano/capistrano
synced 2023-03-27 23:21:18 -04:00
tests for the gateway and command classes
git-svn-id: http://svn.rubyonrails.org/rails/tools/capistrano@6271 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
parent
46ef78d2d8
commit
6a6733f8d3
3 changed files with 376 additions and 42 deletions
|
@ -5,19 +5,18 @@ module Capistrano
|
|||
class Command
|
||||
class Error < RuntimeError; end
|
||||
|
||||
attr_reader :servers, :command, :options, :actor
|
||||
attr_reader :command, :sessions, :options
|
||||
|
||||
def initialize(servers, command, callback, options, actor) #:nodoc:
|
||||
@servers = servers
|
||||
def initialize(command, sessions, options={}, &block) #:nodoc:
|
||||
@command = extract_environment(options) + command.strip.gsub(/\r?\n/, "\\\n")
|
||||
@callback = callback
|
||||
@sessions = sessions
|
||||
@options = options
|
||||
@actor = actor
|
||||
@callback = block
|
||||
@channels = open_channels
|
||||
end
|
||||
|
||||
def logger #:nodoc:
|
||||
actor.logger
|
||||
options[:logger]
|
||||
end
|
||||
|
||||
# Processes the command in parallel on all specified hosts. If the command
|
||||
|
@ -41,10 +40,10 @@ module Capistrano
|
|||
sleep 0.01 # a brief respite, to keep the CPU from going crazy
|
||||
end
|
||||
|
||||
logger.trace "command finished"
|
||||
logger.trace "command finished" if logger
|
||||
|
||||
if failed = @channels.detect { |ch| ch[:status] != 0 }
|
||||
raise Error, "command #{@command.inspect} failed on #{failed[:host]}"
|
||||
raise Error, "command #{command.inspect} failed on #{failed[:host]}"
|
||||
end
|
||||
|
||||
self
|
||||
|
@ -61,20 +60,23 @@ module Capistrano
|
|||
private
|
||||
|
||||
def open_channels
|
||||
@servers.map do |server|
|
||||
@actor.sessions[server].open_channel do |channel|
|
||||
channel[:host] = server
|
||||
channel[:actor] = @actor # so callbacks can access the actor instance
|
||||
sessions.map do |session|
|
||||
session.open_channel do |channel|
|
||||
channel[:host] = session.host
|
||||
channel[:options] = options
|
||||
channel.request_pty :want_reply => true
|
||||
|
||||
channel.on_success do |ch|
|
||||
logger.trace "executing command", ch[:host]
|
||||
logger.trace "executing command", ch[:host] if logger
|
||||
ch.exec command
|
||||
ch.send_data options[:data] if options[:data]
|
||||
end
|
||||
|
||||
channel.on_failure do |ch|
|
||||
logger.important "could not open channel", ch[:host]
|
||||
# just log it, don't actually raise an exception, since the
|
||||
# process method will see that the status is not zero and will
|
||||
# raise an exception then.
|
||||
logger.important "could not open channel", ch[:host] if logger
|
||||
ch.close
|
||||
end
|
||||
|
||||
|
@ -105,8 +107,11 @@ module Capistrano
|
|||
# extract_environment(options) returns:
|
||||
# "TEST=(\ \"quoted\"\ ) PATH=/opt/ruby/bin:$PATH"
|
||||
def extract_environment(options)
|
||||
Array(options[:env]).inject("") do |string, (name, value)|
|
||||
string << %|#{name}=#{value.gsub(/"/, "\\\"").gsub(/ /, "\\ ")} |
|
||||
env = options[:env]
|
||||
return "#{env} " if String === env
|
||||
Array(env).inject("") do |string, (name, value)|
|
||||
value = value.to_s.gsub(/[ "]/) { |m| "\\#{m}" }
|
||||
string << "#{name}=#{value} "
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,43 +1,233 @@
|
|||
$:.unshift File.dirname(__FILE__) + "/../lib"
|
||||
|
||||
require 'stringio'
|
||||
require 'test/unit'
|
||||
require "#{File.dirname(__FILE__)}/utils"
|
||||
require 'capistrano/command'
|
||||
|
||||
class CommandTest < Test::Unit::TestCase
|
||||
class MockSession
|
||||
def open_channel
|
||||
{ :closed => true, :status => 0 }
|
||||
def test_command_should_open_channels_on_all_sessions
|
||||
s1 = mock(:open_channel => nil)
|
||||
s2 = mock(:open_channel => nil)
|
||||
s3 = mock(:open_channel => nil)
|
||||
assert_equal "ls", Capistrano::Command.new("ls", [s1, s2, s3]).command
|
||||
end
|
||||
|
||||
def test_command_with_newlines_should_be_properly_escaped
|
||||
cmd = Capistrano::Command.new("ls\necho", [mock(:open_channel => nil)])
|
||||
assert_equal "ls\\\necho", cmd.command
|
||||
end
|
||||
|
||||
def test_command_with_windows_newlines_should_be_properly_escaped
|
||||
cmd = Capistrano::Command.new("ls\r\necho", [mock(:open_channel => nil)])
|
||||
assert_equal "ls\\\necho", cmd.command
|
||||
end
|
||||
|
||||
def test_command_with_env_key_should_have_environment_constructed_and_prepended
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => { "FOO" => "bar" })
|
||||
assert_equal "FOO=bar ls", cmd.command
|
||||
end
|
||||
|
||||
def test_env_with_symbolic_key_should_be_accepted_as_a_string
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => { :FOO => "bar" })
|
||||
assert_equal "FOO=bar ls", cmd.command
|
||||
end
|
||||
|
||||
def test_env_as_string_should_be_substituted_in_directly
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => "HOWDY" )
|
||||
assert_equal "HOWDY ls", cmd.command
|
||||
end
|
||||
|
||||
def test_env_with_symbolic_value_should_be_accepted_as_string
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => { "FOO" => :bar })
|
||||
assert_equal "FOO=bar ls", cmd.command
|
||||
end
|
||||
|
||||
def test_env_value_should_be_escaped
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => { "FOO" => '( "bar" )' })
|
||||
assert_equal "FOO=(\\ \\\"bar\\\"\\ ) ls", cmd.command
|
||||
end
|
||||
|
||||
def test_env_with_multiple_keys_should_chain_the_entries_together
|
||||
cmd = Capistrano::Command.new("ls", [mock(:open_channel => nil)], :env => { :a => :b, :c => :d, :e => :f })
|
||||
env = cmd.command[/^(.*) ls$/, 1]
|
||||
assert_match(/\ba=b\b/, env)
|
||||
assert_match(/\bc=d\b/, env)
|
||||
assert_match(/\be=f\b/, env)
|
||||
end
|
||||
|
||||
def test_open_channel_should_set_host_key_on_channel
|
||||
session = mock(:host => "capistrano")
|
||||
channel = stub_everything
|
||||
|
||||
session.expects(:open_channel).yields(channel)
|
||||
channel.expects(:[]=).with(:host, "capistrano")
|
||||
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
def test_open_channel_should_set_options_key_on_channel
|
||||
session = mock(:host => "capistrano")
|
||||
channel = stub_everything
|
||||
|
||||
session.expects(:open_channel).yields(channel)
|
||||
channel.expects(:[]=).with(:options, {:data => "here we go"})
|
||||
|
||||
Capistrano::Command.new("ls", [session], :data => "here we go")
|
||||
end
|
||||
|
||||
def test_open_channel_should_request_pty
|
||||
session = mock(:host => "capistrano")
|
||||
channel = stub_everything
|
||||
|
||||
session.expects(:open_channel).yields(channel)
|
||||
channel.expects(:request_pty).with(:want_reply => true)
|
||||
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
def test_successful_channel_should_send_command
|
||||
session = setup_for_extracting_channel_action(:on_success) do |ch|
|
||||
ch.expects(:exec).with("ls")
|
||||
end
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
class MockActor
|
||||
attr_reader :sessions
|
||||
|
||||
def initialize
|
||||
@sessions = Hash.new { |h,k| h[k] = MockSession.new }
|
||||
def test_successful_channel_should_send_data_if_data_key_is_present
|
||||
session = setup_for_extracting_channel_action(:on_success) do |ch|
|
||||
ch.expects(:exec).with("ls")
|
||||
ch.expects(:send_data).with("here we go")
|
||||
end
|
||||
Capistrano::Command.new("ls", [session], :data => "here we go")
|
||||
end
|
||||
|
||||
def setup
|
||||
@actor = MockActor.new
|
||||
def test_unsuccessful_channel_should_close_channel
|
||||
session = setup_for_extracting_channel_action(:on_failure) do |ch|
|
||||
ch.expects(:close)
|
||||
end
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
def test_command_executes_on_all_servers
|
||||
command = Capistrano::Command.new(%w(server1 server2 server3),
|
||||
"hello", nil, {}, @actor)
|
||||
assert_equal %w(server1 server2 server3), @actor.sessions.keys.sort
|
||||
def test_on_data_should_invoke_callback_as_stdout
|
||||
session = setup_for_extracting_channel_action(:on_data, "hello")
|
||||
called = false
|
||||
Capistrano::Command.new("ls", [session]) do |ch, stream, data|
|
||||
called = true
|
||||
assert_equal :out, stream
|
||||
assert_equal "hello", data
|
||||
end
|
||||
assert called
|
||||
end
|
||||
|
||||
def test_command_with_newlines
|
||||
command = Capistrano::Command.new(%w(server1), "hello\nworld", nil, {},
|
||||
@actor)
|
||||
assert_equal "hello\\\nworld", command.command
|
||||
def test_on_extended_data_should_invoke_callback_as_stderr
|
||||
session = setup_for_extracting_channel_action(:on_extended_data, 2, "hello")
|
||||
called = false
|
||||
Capistrano::Command.new("ls", [session]) do |ch, stream, data|
|
||||
called = true
|
||||
assert_equal :err, stream
|
||||
assert_equal "hello", data
|
||||
end
|
||||
assert called
|
||||
end
|
||||
|
||||
def test_command_with_windows_newlines
|
||||
command = Capistrano::Command.new(%w(server1), "hello\r\nworld", nil, {},
|
||||
@actor)
|
||||
assert_equal "hello\\\nworld", command.command
|
||||
def test_on_request_should_record_exit_status
|
||||
data = mock(:read_long => 5)
|
||||
session = setup_for_extracting_channel_action(:on_request, "exit-status", nil, data) do |ch|
|
||||
ch.expects(:[]=).with(:status, 5)
|
||||
end
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
def test_on_close_should_set_channel_closed
|
||||
session = setup_for_extracting_channel_action(:on_close) do |ch|
|
||||
ch.expects(:[]=).with(:closed, true)
|
||||
end
|
||||
Capistrano::Command.new("ls", [session])
|
||||
end
|
||||
|
||||
def test_stop_should_close_all_open_channels
|
||||
sessions = [mock("session", :open_channel => new_channel(false)),
|
||||
mock("session", :open_channel => new_channel(true)),
|
||||
mock("session", :open_channel => new_channel(false))]
|
||||
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
cmd.stop!
|
||||
end
|
||||
|
||||
def test_process_should_return_cleanly_if_all_channels_have_zero_exit_status
|
||||
sessions = [mock("session", :open_channel => new_channel(true, 0)),
|
||||
mock("session", :open_channel => new_channel(true, 0)),
|
||||
mock("session", :open_channel => new_channel(true, 0))]
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
assert_nothing_raised { cmd.process! }
|
||||
end
|
||||
|
||||
def test_process_should_raise_error_if_any_channel_has_non_zero_exit_status
|
||||
sessions = [mock("session", :open_channel => new_channel(true, 0)),
|
||||
mock("session", :open_channel => new_channel(true, 0)),
|
||||
mock("session", :open_channel => new_channel(true, 1))]
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
assert_raises(Capistrano::Command::Error) { cmd.process! }
|
||||
end
|
||||
|
||||
def test_process_should_loop_until_all_channels_are_closed
|
||||
new_channel = Proc.new do |times|
|
||||
ch = mock("channel")
|
||||
returns = [false] * (times-1)
|
||||
ch.stubs(:[]).with(:closed).returns(lambda { returns.empty? ? true : returns.pop })
|
||||
con = mock("connection")
|
||||
con.expects(:process).with(true).times(times-1)
|
||||
ch.expects(:connection).times(times-1).returns(con)
|
||||
ch.expects(:[]).with(:status).returns(0)
|
||||
ch
|
||||
end
|
||||
|
||||
sessions = [mock("session", :open_channel => new_channel[5]),
|
||||
mock("session", :open_channel => new_channel[10]),
|
||||
mock("session", :open_channel => new_channel[7])]
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
assert_nothing_raised { cmd.process! }
|
||||
end
|
||||
|
||||
def test_process_should_ping_all_connections_each_second
|
||||
now = Time.now
|
||||
|
||||
new_channel = Proc.new do
|
||||
ch = mock("channel")
|
||||
ch.stubs(:[]).with(:closed).returns(lambda { Time.now - now < 1.1 ? false : true })
|
||||
ch.stubs(:[]).with(:status).returns(0)
|
||||
con = mock("connection")
|
||||
con.stubs(:process)
|
||||
con.expects(:ping!)
|
||||
ch.stubs(:connection).returns(con)
|
||||
ch
|
||||
end
|
||||
|
||||
sessions = [mock("session", :open_channel => new_channel[]),
|
||||
mock("session", :open_channel => new_channel[]),
|
||||
mock("session", :open_channel => new_channel[])]
|
||||
cmd = Capistrano::Command.new("ls", sessions)
|
||||
assert_nothing_raised { cmd.process! }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_channel(closed, status=nil)
|
||||
ch = mock("channel")
|
||||
ch.expects(:[]).with(:closed).returns(closed)
|
||||
ch.expects(:[]).with(:status).returns(status) if status
|
||||
ch.expects(:close) unless closed
|
||||
ch.stubs(:[]).with(:host).returns("capistrano")
|
||||
ch
|
||||
end
|
||||
|
||||
def setup_for_extracting_channel_action(action, *args)
|
||||
session = mock(:host => "capistrano")
|
||||
|
||||
channel = stub_everything
|
||||
session.expects(:open_channel).yields(channel)
|
||||
|
||||
ch = mock
|
||||
channel.expects(action).yields(ch, *args)
|
||||
|
||||
yield ch if block_given?
|
||||
|
||||
session
|
||||
end
|
||||
end
|
||||
|
|
139
test/gateway_test.rb
Normal file
139
test/gateway_test.rb
Normal file
|
@ -0,0 +1,139 @@
|
|||
require "#{File.dirname(__FILE__)}/utils"
|
||||
require 'capistrano/gateway'
|
||||
|
||||
class GatewayTest < Test::Unit::TestCase
|
||||
def teardown
|
||||
Thread.list { |t| t.kill unless Thread.current == t }
|
||||
end
|
||||
|
||||
def test_initialize_should_open_and_set_session_value
|
||||
run_test_initialize_should_open_and_set_session_value
|
||||
end
|
||||
|
||||
def test_initialize_when_connect_lags_should_open_and_set_session_value
|
||||
run_test_initialize_should_open_and_set_session_value do |expects|
|
||||
expects.with { |*args| sleep 0.2; true }
|
||||
end
|
||||
end
|
||||
|
||||
def test_shutdown_without_any_open_connections_should_terminate_session
|
||||
gateway = new_gateway
|
||||
gateway.shutdown!
|
||||
assert !gateway.thread.alive?
|
||||
assert !gateway.session.looping?
|
||||
end
|
||||
|
||||
def test_connect_to_should_start_local_ports_at_65535
|
||||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1", :port => 65535).returns :app1
|
||||
newsess = gateway.connect_to(server("app1"))
|
||||
assert_equal :app1, newsess
|
||||
assert_equal [65535, "app1", 22], gateway.session.forward.active_locals[65535]
|
||||
end
|
||||
|
||||
def test_connect_to_should_decrement_port_and_retry_if_ports_are_in_use
|
||||
gateway = new_gateway(:reserved => lambda { |n| n > 65000 })
|
||||
expect_connect_to(:host => "127.0.0.1", :port => 65000).returns :app1
|
||||
newsess = gateway.connect_to(server("app1"))
|
||||
assert_equal :app1, newsess
|
||||
assert_equal [65000, "app1", 22], gateway.session.forward.active_locals[65000]
|
||||
end
|
||||
|
||||
def test_connect_to_should_honor_user_specification_in_server_definition
|
||||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1", :user => "jamis", :port => 65535).returns :app1
|
||||
newsess = gateway.connect_to(server("jamis@app1"))
|
||||
assert_equal :app1, newsess
|
||||
assert_equal [65535, "app1", 22], gateway.session.forward.active_locals[65535]
|
||||
end
|
||||
|
||||
def test_connect_to_should_honor_port_specification_in_server_definition
|
||||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1", :port => 65535).returns :app1
|
||||
newsess = gateway.connect_to(server("app1:1234"))
|
||||
assert_equal :app1, newsess
|
||||
assert_equal [65535, "app1", 1234], gateway.session.forward.active_locals[65535]
|
||||
end
|
||||
|
||||
def test_shutdown_should_cancel_active_forwarded_ports
|
||||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1", :port => 65535).returns :app1
|
||||
gateway.connect_to(server("app1"))
|
||||
assert !gateway.session.forward.active_locals.empty?
|
||||
gateway.shutdown!
|
||||
assert gateway.session.forward.active_locals.empty?
|
||||
end
|
||||
|
||||
def test_error_while_connecting_should_cause_connection_to_fail
|
||||
gateway = new_gateway
|
||||
expect_connect_to(:host => "127.0.0.1").raises(RuntimeError)
|
||||
gateway.expects(:warn).times(2)
|
||||
assert_raises(Capistrano::Gateway::Error) { gateway.connect_to(server("app1")) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def expect_connect_to(options={})
|
||||
Capistrano::SSH.expects(:connect).with do |server,config|
|
||||
options.all? do |key, value|
|
||||
case key
|
||||
when :host then server.host == value
|
||||
when :user then server.user == value
|
||||
when :port then server.port == value
|
||||
else false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def new_gateway(options={})
|
||||
expect_connect_to(:host => "capistrano").yields(MockSession.new(options))
|
||||
Capistrano::Gateway.new(server("capistrano"))
|
||||
end
|
||||
|
||||
def run_test_initialize_should_open_and_set_session_value
|
||||
session = mock("Net::SSH session")
|
||||
session.expects(:loop)
|
||||
expectation = Capistrano::SSH.expects(:connect).yields(session)
|
||||
yield expectation if block_given?
|
||||
gateway = Capistrano::Gateway.new(server("capistrano"))
|
||||
gateway.thread.join
|
||||
assert_equal session, gateway.session
|
||||
end
|
||||
|
||||
class MockForward
|
||||
attr_reader :active_locals
|
||||
|
||||
def initialize(options)
|
||||
@options = options
|
||||
@active_locals = {}
|
||||
end
|
||||
|
||||
def cancel_local(port)
|
||||
@active_locals.delete(port)
|
||||
end
|
||||
|
||||
def local(lport, host, rport)
|
||||
raise Errno::EADDRINUSE if @options[:reserved] && @options[:reserved][lport]
|
||||
@active_locals[lport] = [lport, host, rport]
|
||||
end
|
||||
end
|
||||
|
||||
class MockSession
|
||||
attr_reader :forward
|
||||
|
||||
def initialize(options={})
|
||||
@forward = MockForward.new(options)
|
||||
end
|
||||
|
||||
def looping?
|
||||
@looping
|
||||
end
|
||||
|
||||
def loop
|
||||
@looping = true
|
||||
sleep 0.1 while yield
|
||||
@looping = false
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Add table
Reference in a new issue