robot-army 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. data/LICENSE +13 -0
  2. data/README.markdown +34 -0
  3. data/Rakefile +9 -0
  4. data/examples/whoami.rb +13 -0
  5. data/lib/robot-army.rb +109 -0
  6. data/lib/robot-army/at_exit.rb +19 -0
  7. data/lib/robot-army/connection.rb +174 -0
  8. data/lib/robot-army/dependency_loader.rb +38 -0
  9. data/lib/robot-army/eval_builder.rb +84 -0
  10. data/lib/robot-army/eval_command.rb +17 -0
  11. data/lib/robot-army/gate_keeper.rb +28 -0
  12. data/lib/robot-army/io.rb +106 -0
  13. data/lib/robot-army/keychain.rb +10 -0
  14. data/lib/robot-army/loader.rb +85 -0
  15. data/lib/robot-army/marshal_ext.rb +52 -0
  16. data/lib/robot-army/messenger.rb +31 -0
  17. data/lib/robot-army/officer.rb +35 -0
  18. data/lib/robot-army/officer_connection.rb +5 -0
  19. data/lib/robot-army/officer_loader.rb +13 -0
  20. data/lib/robot-army/proxy.rb +35 -0
  21. data/lib/robot-army/remote_evaler.rb +59 -0
  22. data/lib/robot-army/ruby2ruby_ext.rb +19 -0
  23. data/lib/robot-army/soldier.rb +37 -0
  24. data/lib/robot-army/task_master.rb +317 -0
  25. data/spec/at_exit_spec.rb +25 -0
  26. data/spec/connection_spec.rb +126 -0
  27. data/spec/dependency_loader_spec.rb +46 -0
  28. data/spec/gate_keeper_spec.rb +46 -0
  29. data/spec/integration_spec.rb +40 -0
  30. data/spec/io_spec.rb +36 -0
  31. data/spec/keychain_spec.rb +15 -0
  32. data/spec/loader_spec.rb +13 -0
  33. data/spec/marshal_ext_spec.rb +89 -0
  34. data/spec/messenger_spec.rb +28 -0
  35. data/spec/officer_spec.rb +36 -0
  36. data/spec/proxy_spec.rb +52 -0
  37. data/spec/ruby2ruby_ext_spec.rb +67 -0
  38. data/spec/soldier_spec.rb +71 -0
  39. data/spec/spec_helper.rb +19 -0
  40. data/spec/task_master_spec.rb +306 -0
  41. metadata +142 -0
@@ -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.should == []
10
+ end
11
+
12
+ it "should store the dependency requirement by name" do
13
+ name = "RedCloth"
14
+ @loader.add_dependency name
15
+ @loader.dependencies.should == [[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.should == [[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! }.should 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].should == @connection
40
+ end
41
+
42
+ it "has a shared instance that doesn't change" do
43
+ RobotArmy::GateKeeper.shared_instance.should be_an_instance_of(RobotArmy::GateKeeper)
44
+ RobotArmy::GateKeeper.shared_instance.should == 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 }.should be_a(Time)
23
+ end
24
+
25
+ it "does sudo as root by default" do
26
+ @tm.sudo { Process.uid }.should == 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 }.should == 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'] }.should == 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).should == "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').should == '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').should == 'god'
14
+ end
15
+ end
@@ -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 } }.should 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.should be_marshalable
18
+ end
19
+
20
+ it "can dump Strings" do
21
+ "foo".should be_marshalable
22
+ end
23
+
24
+ it "can dump Arrays" do
25
+ [2, 'foo'].should be_marshalable
26
+ end
27
+
28
+ it "can't dump things that return false for marshalable?" do
29
+ CannotMarshalMe.new.should_not be_marshalable
30
+ end
31
+
32
+ it "can't dump Arrays containing unmarshalable items" do
33
+ [2, $stdin].should_not be_marshalable
34
+ [2, CannotMarshalMe.new].should_not be_marshalable
35
+ end
36
+
37
+ it "can dump hashes" do
38
+ {:foo => 'bar'}.should be_marshalable
39
+ end
40
+
41
+ it "can't dump hashes with unmarshalable keys" do
42
+ {$stdin => 'bar'}.should_not be_marshalable
43
+ {CannotMarshalMe.new => 'bar'}.should_not be_marshalable
44
+ end
45
+
46
+ it "can't dump hashes with unmarshalable values" do
47
+ {:foo => $stdin}.should_not be_marshalable
48
+ {:foo => CannotMarshalMe.new}.should_not be_marshalable
49
+ end
50
+
51
+ it "can't dump hashes with an unmarshalable default value" do
52
+ Hash.new($stdin).should_not be_marshalable
53
+ Hash.new(CannotMarshalMe.new).should_not be_marshalable
54
+ end
55
+
56
+ it "can dump hashes with a marshalable default value" do
57
+ Hash.new(0).should be_marshalable
58
+ end
59
+
60
+ it "can't dump hashes with a default proc" do
61
+ Hash.new {|a,b| a[b] = rand}.should_not be_marshalable
62
+ end
63
+
64
+ it "can't dump objects containing references to unmarshalable objects" do
65
+ CustomContainer.new(CannotMarshalMe.new).should_not be_marshalable
66
+ end
67
+
68
+ it "can't dump IOs" do
69
+ $stdin.should_not be_marshalable
70
+ end
71
+
72
+ it "can't dump Methods" do
73
+ method(:to_s).should_not be_marshalable
74
+ end
75
+
76
+ it "can't dump bindings" do
77
+ binding.should_not be_marshalable
78
+ end
79
+
80
+ it "can't dump Procs" do
81
+ proc{ 2 }.should_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.should_not be_marshalable
87
+ end
88
+ end
89
+
@@ -0,0 +1,28 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RobotArmy::Messenger do
4
+ before do
5
+ # given
6
+ @in, @out = StringIO.new, StringIO.new
7
+
8
+ @messenger = RobotArmy::Messenger.new(@in, @out)
9
+ @response = {:status => 'ok', :data => 1}
10
+ @dump = "#{Base64.encode64(Marshal.dump(@response))}|"
11
+ end
12
+
13
+ it "posts messages to @out" do
14
+ # when
15
+ @messenger.post(@response)
16
+
17
+ # then
18
+ @out.string.should == @dump
19
+ end
20
+
21
+ it "gets messages from @in" do
22
+ # when
23
+ @in.string = @dump
24
+
25
+ # then
26
+ @messenger.get.should == @response
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RobotArmy::Officer do
4
+ before do
5
+ # given
6
+ @messenger = mock(:messenger)
7
+ @officer = RobotArmy::Officer.new(@messenger)
8
+ end
9
+
10
+ it "evaluates each command in a different process" do
11
+ # when
12
+ pid = proc{ @officer.run(:eval, :code => 'Process.pid', :file => __FILE__, :line => __LINE__) }
13
+
14
+ # then
15
+ pid.call.should_not == pid.call
16
+ end
17
+
18
+ it "asks for a password by posting back status=password" do
19
+ # then
20
+ @messenger.should_receive(:post).
21
+ with(:status => 'password', :data => {:as => 'root', :user => ENV['USER']})
22
+
23
+ # when
24
+ @messenger.stub!(:get).and_return(:status => 'ok', :data => 'password')
25
+ @officer.ask_for_password('root')
26
+ end
27
+
28
+ it "returns the password given upstream" do
29
+ # when
30
+ @messenger.stub!(:post)
31
+ @messenger.stub!(:get).and_return(:status => 'ok', :data => 'password')
32
+
33
+ # then
34
+ @officer.ask_for_password('root').should == 'password'
35
+ end
36
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RobotArmy::Proxy do
4
+ before do
5
+ # given
6
+ @messenger = stub(:messenger, :post => nil, :get => nil)
7
+ @hash = self.hash
8
+ @proxy = RobotArmy::Proxy.new(@messenger, @hash)
9
+ end
10
+
11
+ it "posts back a proxy status when a method is called on it" do
12
+ # then
13
+ @messenger.should_receive(:post).
14
+ with(:status => 'proxy', :data => {:hash => @hash, :call => [:to_s]})
15
+
16
+ # when
17
+ @messenger.stub!(:get).and_return(:status => 'ok', :data => 'foo')
18
+ RobotArmy::Connection.stub!(:handle_response)
19
+ @proxy.to_s
20
+ end
21
+
22
+ it "returns the value returned by a successful incoming message" do
23
+ # when
24
+ @messenger.stub!(:get).and_return(:status => 'ok', :data => 'bar')
25
+
26
+ # then
27
+ @proxy.to_s.should == 'bar'
28
+ end
29
+
30
+ it "lets exceptions bubble up from handling the message" do
31
+ # when
32
+ RobotArmy::Connection.stub!(:handle_response).and_raise
33
+
34
+ # then
35
+ proc { @proxy.to_s }.should raise_error
36
+ end
37
+
38
+ it "returns a new proxy if the response has status 'proxy'" do
39
+ # then
40
+ RobotArmy::Proxy.should_receive(:new).
41
+ with(@messenger, @hash)
42
+
43
+ # when
44
+ @messenger.stub!(:get).and_return(:status => 'proxy', :data => @hash)
45
+ @proxy.me
46
+ end
47
+
48
+ it "can generate Ruby code to create a Proxy for an object" do
49
+ RobotArmy::Proxy.generator_for(self).
50
+ should == "RobotArmy::Proxy.new(RobotArmy.upstream, #{self.hash.inspect})"
51
+ end
52
+ end
@@ -0,0 +1,67 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe Proc, "to_ruby" do
4
+ before do
5
+ @proc = proc{ 1 }
6
+ end
7
+
8
+ it "can render itself as ruby not enclosed in a proc" do
9
+ @proc.to_ruby_without_proc_wrapper.should == "1"
10
+ end
11
+
12
+ it "can render itself as ruby that evaluates to a Proc" do
13
+ @proc.to_ruby.should == "proc { 1 }"
14
+ end
15
+
16
+ it "can get a list of arguments" do
17
+ proc{ |a, b| a + b }.arguments.should == %w[a b]
18
+ end
19
+ end
20
+
21
+ class MethodToRubyFixture
22
+ def one
23
+ 1
24
+ end
25
+
26
+ def echo(a)
27
+ a
28
+ end
29
+
30
+ def add(a, b)
31
+ a + b
32
+ end
33
+ end
34
+
35
+ describe Method, "to_ruby" do
36
+ before do
37
+ @method = MethodToRubyFixture.new.method(:one)
38
+ end
39
+
40
+ it "can render itself as ruby that executes itself" do
41
+ @method.to_ruby_without_method_declaration.should =~ /\A\s*1\s*\Z/
42
+ end
43
+
44
+ it "can render itself as ruby that evaluates to a Method" do
45
+ @method.to_ruby.should == "def one\n 1\nend"
46
+ end
47
+ end
48
+
49
+ describe Method, "arguments" do
50
+ before do
51
+ @no_args = MethodToRubyFixture.new.method(:one)
52
+ @one_arg = MethodToRubyFixture.new.method(:echo)
53
+ @many_args = MethodToRubyFixture.new.method(:add)
54
+ end
55
+
56
+ it "returns an empty list for a method without arguments" do
57
+ @no_args.arguments.should == []
58
+ end
59
+
60
+ it "returns a single argument for a method with a single argument" do
61
+ @one_arg.arguments.should == %w[a]
62
+ end
63
+
64
+ it "returns a comma-separated list of arguments when there are many args" do
65
+ @many_args.arguments.should == %w[a b]
66
+ end
67
+ end