wesabe-robot-army 0.1.1 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/examples/whoami.rb +13 -0
- data/lib/robot-army.rb +47 -21
- data/lib/robot-army/at_exit.rb +19 -0
- data/lib/robot-army/eval_builder.rb +84 -0
- data/lib/robot-army/eval_command.rb +17 -0
- data/lib/robot-army/keychain.rb +10 -0
- data/lib/robot-army/marshal_ext.rb +26 -7
- data/lib/robot-army/remote_evaler.rb +59 -0
- data/lib/robot-army/ruby2ruby_ext.rb +6 -18
- data/lib/robot-army/task_master.rb +113 -201
- data/spec/at_exit_spec.rb +25 -0
- data/spec/connection_spec.rb +126 -0
- data/spec/dependency_loader_spec.rb +46 -0
- data/spec/gate_keeper_spec.rb +46 -0
- data/spec/integration_spec.rb +40 -0
- data/spec/io_spec.rb +36 -0
- data/spec/keychain_spec.rb +15 -0
- data/spec/loader_spec.rb +13 -0
- data/spec/marshal_ext_spec.rb +89 -0
- data/spec/messenger_spec.rb +28 -0
- data/spec/officer_spec.rb +36 -0
- data/spec/proxy_spec.rb +52 -0
- data/spec/ruby2ruby_ext_spec.rb +67 -0
- data/spec/soldier_spec.rb +71 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/task_master_spec.rb +272 -0
- metadata +49 -16
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::AtExit do
|
4
|
+
before do
|
5
|
+
@at_exit = RobotArmy::AtExit.shared_instance
|
6
|
+
end
|
7
|
+
|
8
|
+
it "runs the provided block when directed" do
|
9
|
+
foo = 'foo'
|
10
|
+
@at_exit.at_exit { foo = 'bar' }
|
11
|
+
foo.must == 'foo'
|
12
|
+
@at_exit.do_exit
|
13
|
+
foo.must == 'bar'
|
14
|
+
end
|
15
|
+
|
16
|
+
it "does not run the same block twice" do
|
17
|
+
foo = 0
|
18
|
+
@at_exit.at_exit { foo += 1 }
|
19
|
+
foo.must == 0
|
20
|
+
@at_exit.do_exit
|
21
|
+
foo.must == 1
|
22
|
+
@at_exit.do_exit
|
23
|
+
foo.must == 1
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Connection do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@host = 'example.com'
|
7
|
+
@connection = RobotArmy::Connection.new(@host)
|
8
|
+
@messenger = mock(:messenger)
|
9
|
+
@messenger.stub!(:post)
|
10
|
+
@connection.stub!(:messenger).and_return(@messenger)
|
11
|
+
@connection.stub!(:start_child)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "is not closed after opening" do
|
15
|
+
# when
|
16
|
+
@connection.open
|
17
|
+
|
18
|
+
# then
|
19
|
+
@connection.must_not be_closed
|
20
|
+
end
|
21
|
+
|
22
|
+
it "returns itself from open" do
|
23
|
+
@connection.open.must == @connection
|
24
|
+
end
|
25
|
+
|
26
|
+
it "returns the result of the block passed to open" do
|
27
|
+
# when
|
28
|
+
@connection.stub!(:close)
|
29
|
+
|
30
|
+
# then
|
31
|
+
@connection.open{ 3 }.must == 3
|
32
|
+
end
|
33
|
+
|
34
|
+
it "closes the connection if a block is passed to open" do
|
35
|
+
# then
|
36
|
+
proc{ @connection.open{ 3 } }.
|
37
|
+
must_not change(@connection, :closed?).from(true)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "closes the connection even if an exception is raised in the block passed to open" do
|
41
|
+
# then
|
42
|
+
proc do
|
43
|
+
proc{ @connection.open{ raise 'BOO!' } }.
|
44
|
+
must_not change(@connection, :closed?).from(true)
|
45
|
+
end.
|
46
|
+
must raise_error('BOO!')
|
47
|
+
end
|
48
|
+
|
49
|
+
it "does not start another child process if we're already open" do
|
50
|
+
# then
|
51
|
+
@connection.should_not_receive(:start_child)
|
52
|
+
|
53
|
+
# when
|
54
|
+
@connection.stub!(:closed?).and_return(false)
|
55
|
+
@connection.open
|
56
|
+
end
|
57
|
+
|
58
|
+
it "raises an exception when calling close if a connection is already closed" do
|
59
|
+
# when
|
60
|
+
@connection.stub!(:closed?).and_return(true)
|
61
|
+
|
62
|
+
# then
|
63
|
+
proc{ @connection.close }.must raise_error(RobotArmy::ConnectionNotOpen)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "sends an exit command to its child upon closing" do
|
67
|
+
# then
|
68
|
+
@messenger.should_receive(:post).with(:command => :exit)
|
69
|
+
|
70
|
+
# when
|
71
|
+
@connection.stub!(:closed?).and_return(false)
|
72
|
+
@connection.close
|
73
|
+
end
|
74
|
+
|
75
|
+
it "starts a closed local connection when calling localhost" do
|
76
|
+
RobotArmy::Connection.localhost.must be_closed
|
77
|
+
end
|
78
|
+
|
79
|
+
it "raises a Warning when handling a message with status=warning" do
|
80
|
+
proc{ @connection.handle_response(:status => 'warning', :data => 'foobar') }.
|
81
|
+
must raise_error(RobotArmy::Warning)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe RobotArmy::Connection, 'answer_sudo_prompt' do
|
86
|
+
before do
|
87
|
+
@connection = RobotArmy::Connection.new(:localhost, 'root')
|
88
|
+
@password_proc = proc { 'password' }
|
89
|
+
@stdin = StringIO.new
|
90
|
+
@stderr = stub(:stderr, :readpartial => nil)
|
91
|
+
end
|
92
|
+
|
93
|
+
it "calls back using the password proc if it is a proc" do
|
94
|
+
# when
|
95
|
+
@connection.stub!(:password).and_return(@password_proc)
|
96
|
+
@connection.stub!(:asking_for_password?).and_return(true, false)
|
97
|
+
@connection.answer_sudo_prompt(@stdin, @stderr)
|
98
|
+
|
99
|
+
# then
|
100
|
+
@stdin.string.must == "password\n"
|
101
|
+
end
|
102
|
+
|
103
|
+
it "raises if password is a string and is rejected" do
|
104
|
+
# when
|
105
|
+
@connection.stub!(:password).and_return('password')
|
106
|
+
@connection.stub!(:asking_for_password?).and_return(true)
|
107
|
+
|
108
|
+
# then
|
109
|
+
proc { @connection.answer_sudo_prompt(@stdin, @stderr) }.
|
110
|
+
must raise_error(RobotArmy::InvalidPassword)
|
111
|
+
end
|
112
|
+
|
113
|
+
it "calls back three times before raising if password is a proc" do
|
114
|
+
calls = 0
|
115
|
+
|
116
|
+
# when
|
117
|
+
@connection.stub!(:password).and_return(proc{ calls += 1 })
|
118
|
+
|
119
|
+
# then
|
120
|
+
@connection.should_receive(:asking_for_password?).
|
121
|
+
exactly(4).times.and_return(true)
|
122
|
+
proc { @connection.answer_sudo_prompt(@stdin, @stderr) }.
|
123
|
+
must raise_error(RobotArmy::InvalidPassword)
|
124
|
+
calls.must == 3
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::DependencyLoader do
|
4
|
+
before do
|
5
|
+
@loader = RobotArmy::DependencyLoader.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should have no dependencies by default" do
|
9
|
+
@loader.dependencies.must == []
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should store the dependency requirement by name" do
|
13
|
+
name = "RedCloth"
|
14
|
+
@loader.add_dependency name
|
15
|
+
@loader.dependencies.must == [[name]]
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should store the dependency requirement with version restriction" do
|
19
|
+
name = "RedCloth"
|
20
|
+
version_str = "> 3.1.0"
|
21
|
+
@loader.add_dependency name, version_str
|
22
|
+
@loader.dependencies.must == [[name, version_str]]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should gem load a dependency by name only" do
|
26
|
+
name = "foobarbaz"
|
27
|
+
@loader.add_dependency name
|
28
|
+
@loader.should_receive(:gem).with(name)
|
29
|
+
@loader.load!
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should gem load a dependency by name and version" do
|
33
|
+
name = "foobarbaz"
|
34
|
+
version = "> 3.1"
|
35
|
+
@loader.add_dependency name, version
|
36
|
+
@loader.should_receive(:gem).with(name, version)
|
37
|
+
@loader.load!
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
it "should raise when a dependency is not met" do
|
42
|
+
@loader.add_dependency "foobarbaz"
|
43
|
+
@loader.should_receive(:gem).and_raise Gem::LoadError
|
44
|
+
lambda { @loader.load! }.must raise_error(RobotArmy::DependencyError)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::GateKeeper do
|
4
|
+
before do
|
5
|
+
# given
|
6
|
+
@keeper = RobotArmy::GateKeeper.new
|
7
|
+
@host = 'example.com'
|
8
|
+
@connection = mock(:connection)
|
9
|
+
@connection.stub!(:closed?).and_return(false)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "establishes a new connection to a host if one does not already exist" do
|
13
|
+
# then
|
14
|
+
@keeper.should_receive(:establish_connection).with(@host)
|
15
|
+
|
16
|
+
# when
|
17
|
+
@keeper.stub!(:get_connection).and_return(nil)
|
18
|
+
@keeper.connect(@host)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "terminates all connections on close" do
|
22
|
+
# then
|
23
|
+
@connection.should_receive(:close)
|
24
|
+
|
25
|
+
# when
|
26
|
+
@keeper.stub!(:connections).and_return(@host => @connection)
|
27
|
+
@keeper.close
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates a new Connection with the given host when establish_connection is called" do
|
31
|
+
# then
|
32
|
+
RobotArmy::OfficerConnection.should_receive(:new).with(@host).and_return(@connection)
|
33
|
+
@connection.should_receive(:open).and_return(@connection)
|
34
|
+
|
35
|
+
# when
|
36
|
+
@keeper.establish_connection(@host)
|
37
|
+
|
38
|
+
# and
|
39
|
+
@keeper.connections[@host].must == @connection
|
40
|
+
end
|
41
|
+
|
42
|
+
it "has a shared instance that doesn't change" do
|
43
|
+
RobotArmy::GateKeeper.shared_instance.must be_an_instance_of(RobotArmy::GateKeeper)
|
44
|
+
RobotArmy::GateKeeper.shared_instance.must == RobotArmy::GateKeeper.shared_instance
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
# THIS SPEC IS INTENDED TO BE RUN BY ITSELF:
|
4
|
+
#
|
5
|
+
# env SUDO_AS=brad INTEGRATION_HOST=example.com spec spec/integration_spec.rb --format=specdoc --color
|
6
|
+
#
|
7
|
+
|
8
|
+
if $INTEGRATION_HOST = ENV['INTEGRATION_HOST']
|
9
|
+
$TESTING = false
|
10
|
+
$ROBOT_ARMY_DEBUG = true
|
11
|
+
|
12
|
+
class Integration < RobotArmy::TaskMaster
|
13
|
+
host $INTEGRATION_HOST
|
14
|
+
end
|
15
|
+
|
16
|
+
describe Integration do
|
17
|
+
before do
|
18
|
+
@tm = Integration.new
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can do sudo" do
|
22
|
+
@tm.sudo { Time.now }.must be_a(Time)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "does sudo as root by default" do
|
26
|
+
@tm.sudo { Process.uid }.must == 0
|
27
|
+
end
|
28
|
+
|
29
|
+
it "can do sudo as ourselves" do
|
30
|
+
my_remote_uid = @tm.remote { Process.uid }
|
31
|
+
@tm.sudo(:user => ENV['USER']) { Process.uid }.must == my_remote_uid
|
32
|
+
end
|
33
|
+
|
34
|
+
if sudo_user = ENV['SUDO_AS']
|
35
|
+
it "can sudo as another non-root user" do
|
36
|
+
@tm.sudo(:user => sudo_user) { ENV['USER'] }.must == sudo_user
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/spec/io_spec.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::IO, 'class method read_data' do
|
4
|
+
before do
|
5
|
+
@stream = stub(:stream)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "reads all data as long as it is available" do
|
9
|
+
RobotArmy::IO.stub!(:has_data?).and_return(true, true, false)
|
10
|
+
@stream.stub!(:readpartial).and_return('foo', 'bar')
|
11
|
+
|
12
|
+
RobotArmy::IO.read_data(@stream).must == "foobar"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe RobotArmy::IO do
|
17
|
+
before do
|
18
|
+
@stream = RobotArmy::IO.new(:stdout)
|
19
|
+
@upstream = RobotArmy.upstream = stub(:upstream)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "can capture output of IO method calls" do
|
23
|
+
@stream.send(:capture, :print, 'foo').must == 'foo'
|
24
|
+
end
|
25
|
+
|
26
|
+
it "proxies output upstream" do
|
27
|
+
@upstream.should_receive(:post).
|
28
|
+
with(:status => 'output', :data => {:stream => 'stdout', :string => "foo\n"})
|
29
|
+
|
30
|
+
@stream.puts 'foo'
|
31
|
+
end
|
32
|
+
|
33
|
+
after do
|
34
|
+
@stream.stop_capture
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Keychain do
|
4
|
+
before do
|
5
|
+
@keychain = RobotArmy::Keychain.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "asks for all passwords over stdin" do
|
9
|
+
@keychain.
|
10
|
+
should_receive(:read_with_prompt).
|
11
|
+
with("[sudo] password for bob@example.com: ").
|
12
|
+
and_return("god")
|
13
|
+
@keychain.get_password_for_user_on_host('bob', 'example.com').must == 'god'
|
14
|
+
end
|
15
|
+
end
|
data/spec/loader_spec.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe RobotArmy::Loader do
|
4
|
+
before do
|
5
|
+
@loader = RobotArmy::Loader.new
|
6
|
+
@messenger = @loader.messenger = mock(:messenger)
|
7
|
+
@messenger.stub!(:post)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "doesn't catch the RobotArmy::Exit exception" do
|
11
|
+
proc{ @loader.safely{ raise RobotArmy::Exit } }.must raise_error(RobotArmy::Exit)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
class CannotMarshalMe
|
4
|
+
def marshalable?
|
5
|
+
false
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class CustomContainer
|
10
|
+
def initialize(child)
|
11
|
+
@child = child
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Marshal do
|
16
|
+
it "can dump Fixnums" do
|
17
|
+
42.must be_marshalable
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can dump Strings" do
|
21
|
+
"foo".must be_marshalable
|
22
|
+
end
|
23
|
+
|
24
|
+
it "can dump Arrays" do
|
25
|
+
[2, 'foo'].must be_marshalable
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can't dump things that return false for marshalable?" do
|
29
|
+
CannotMarshalMe.new.must_not be_marshalable
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can't dump Arrays containing unmarshalable items" do
|
33
|
+
[2, $stdin].must_not be_marshalable
|
34
|
+
[2, CannotMarshalMe.new].must_not be_marshalable
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can dump hashes" do
|
38
|
+
{:foo => 'bar'}.must be_marshalable
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can't dump hashes with unmarshalable keys" do
|
42
|
+
{$stdin => 'bar'}.must_not be_marshalable
|
43
|
+
{CannotMarshalMe.new => 'bar'}.must_not be_marshalable
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can't dump hashes with unmarshalable values" do
|
47
|
+
{:foo => $stdin}.must_not be_marshalable
|
48
|
+
{:foo => CannotMarshalMe.new}.must_not be_marshalable
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can't dump hashes with an unmarshalable default value" do
|
52
|
+
Hash.new($stdin).must_not be_marshalable
|
53
|
+
Hash.new(CannotMarshalMe.new).must_not be_marshalable
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can dump hashes with a marshalable default value" do
|
57
|
+
Hash.new(0).must be_marshalable
|
58
|
+
end
|
59
|
+
|
60
|
+
it "can't dump hashes with a default proc" do
|
61
|
+
Hash.new {|a,b| a[b] = rand}.must_not be_marshalable
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can't dump objects containing references to unmarshalable objects" do
|
65
|
+
CustomContainer.new(CannotMarshalMe.new).must_not be_marshalable
|
66
|
+
end
|
67
|
+
|
68
|
+
it "can't dump IOs" do
|
69
|
+
$stdin.must_not be_marshalable
|
70
|
+
end
|
71
|
+
|
72
|
+
it "can't dump Methods" do
|
73
|
+
method(:to_s).must_not be_marshalable
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can't dump bindings" do
|
77
|
+
binding.must_not be_marshalable
|
78
|
+
end
|
79
|
+
|
80
|
+
it "can't dump Procs" do
|
81
|
+
proc{ 2 }.must_not be_marshalable
|
82
|
+
end
|
83
|
+
|
84
|
+
it "can't dump anything whose _dump method raises a TypeError" do
|
85
|
+
class NotDumpable; def _dump(*args); raise TypeError; end; end
|
86
|
+
NotDumpable.new.must_not be_marshalable
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|