1
0
Fork 0
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:
Jamis Buck 2007-03-01 06:23:26 +00:00
parent 46ef78d2d8
commit 6a6733f8d3
3 changed files with 376 additions and 42 deletions

View file

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

View file

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