robot-army 0.1.8

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