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.
@@ -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.must == @dump
19
+ end
20
+
21
+ it "gets messages from @in" do
22
+ # when
23
+ @in.string = @dump
24
+
25
+ # then
26
+ @messenger.get.must == @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.must_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').must == '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.must == '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 }.must 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
+ must == "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.must == "1"
10
+ end
11
+
12
+ it "can render itself as ruby that evaluates to a Proc" do
13
+ @proc.to_ruby.must == "proc { 1 }"
14
+ end
15
+
16
+ it "can get a list of arguments" do
17
+ proc{ |a, b| a + b }.arguments.must == %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.must =~ /\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.must == "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.must == []
58
+ end
59
+
60
+ it "returns a single argument for a method with a single argument" do
61
+ @one_arg.arguments.must == %w[a]
62
+ end
63
+
64
+ it "returns a comma-separated list of arguments when there are many args" do
65
+ @many_args.arguments.must == %w[a b]
66
+ end
67
+ end
@@ -0,0 +1,71 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ describe RobotArmy::Soldier do
4
+ before do
5
+ # given
6
+ @messenger = mock(:messenger)
7
+ @soldier = RobotArmy::Soldier.new(@messenger)
8
+ end
9
+
10
+ it "can accept eval commands" do
11
+ # then
12
+ @soldier.run(:eval, :code => '3+4', :file => __FILE__, :line => __LINE__).
13
+ must == 7
14
+
15
+ # and
16
+ @soldier.run(:eval, :code => 'Time.now', :file => __FILE__, :line => __LINE__).
17
+ must be_an_instance_of(Time)
18
+ end
19
+
20
+ it "evaluates each command in the same process" do
21
+ # when
22
+ pid = proc{ @soldier.run(:eval, :code => 'Process.pid', :file => __FILE__, :line => __LINE__) }
23
+
24
+ # then
25
+ pid.call.must == pid.call
26
+ end
27
+
28
+ it "raises on unrecognized commands" do
29
+ proc{ @soldier.run(:foo, nil) }.must raise_error(ArgumentError)
30
+ end
31
+
32
+ it "listens for commands from the messenger to run" do
33
+ # then
34
+ @soldier.should_receive(:run).with(:eval, :code => 'Hash.new')
35
+
36
+ # when
37
+ @messenger.stub!(:post)
38
+ @messenger.stub!(:get).and_return(:command => :eval, :data => {:code => 'Hash.new'})
39
+ @soldier.listen
40
+ end
41
+
42
+ it "posts through the messenger the result of commands run by listening" do
43
+ # then
44
+ @messenger.should_receive(:post).with(:status => 'ok', :data => 1)
45
+
46
+ # when
47
+ @messenger.stub!(:get).and_return(:command => :eval, :data => {:code => '1'})
48
+ @soldier.stub!(:run).and_return(1)
49
+ @soldier.listen
50
+ end
51
+
52
+ it "posts back and raises RobotArmy::Exit when running the exit command" do
53
+ @messenger.should_receive(:post).with(:status => 'ok')
54
+ proc{ @soldier.run(:exit, nil) }.must raise_error(RobotArmy::Exit)
55
+ end
56
+
57
+ it "returns the pid and type when asked for info" do
58
+ @soldier.run(:info, nil).must == {:pid => Process.pid, :type => 'RobotArmy::Soldier'}
59
+ end
60
+
61
+ it "posts back a warning if the :eval return value is not marshalable" do
62
+ # then
63
+ @messenger.should_receive(:post).
64
+ with(:status => 'warning', :data => "ignoring invalid remote return value #{$stdin.inspect}")
65
+
66
+ # when
67
+ @messenger.stub!(:get).and_return(
68
+ :command => :eval, :data => {:code => '$stdin', :file => __FILE__, :line => __LINE__})
69
+ @soldier.listen
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ $TESTING=true
2
+ load File.join(File.dirname(__FILE__), '..', 'lib', 'robot-army.rb')
3
+
4
+ module Spec::Expectations::ObjectExpectations
5
+ alias_method :must, :should
6
+ alias_method :must_not, :should_not
7
+ undef_method :should
8
+ undef_method :should_not
9
+ end
10
+
11
+ Spec::Runner.configure do |config|
12
+ def capture(stream)
13
+ begin
14
+ stream = stream.to_s
15
+ eval "$#{stream} = StringIO.new"
16
+ yield
17
+ result = eval("$#{stream}").string
18
+ ensure
19
+ eval("$#{stream} = #{stream.upcase}")
20
+ end
21
+
22
+ result
23
+ end
24
+
25
+ alias silence capture
26
+ end
@@ -0,0 +1,272 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+
3
+ class Example < RobotArmy::TaskMaster
4
+ hosts %[www1.example.com www2.example.com]
5
+ end
6
+
7
+ class Localhost < RobotArmy::TaskMaster
8
+ host :localhost
9
+ end
10
+
11
+ describe RobotArmy::TaskMaster, 'host management' do
12
+ before do
13
+ @example = Example.new
14
+ end
15
+
16
+ it "allows setting a single host" do
17
+ Example.host 'example.com'
18
+ Example.host.must == 'example.com'
19
+ end
20
+
21
+ it "allows accessing multi-hosts when using the single-host interface" do
22
+ Example.host 'example.com'
23
+ Example.hosts.must == %w[example.com]
24
+ end
25
+
26
+ it "allows setting multiple hosts on the class" do
27
+ Example.hosts %w[example.com test.com]
28
+ Example.hosts.must == %w[example.com test.com]
29
+ end
30
+
31
+ it "denies accessing a single host when using the multi-host interface" do
32
+ Example.hosts %w[example.com test.com]
33
+ proc { Example.host }.must raise_error(
34
+ RobotArmy::HostArityError, "There are 2 hosts, so calling host doesn't make sense")
35
+ end
36
+
37
+ it "instances default to the hosts set on the class" do
38
+ Example.host 'example.com'
39
+ @example.host.must == 'example.com'
40
+
41
+ Example.hosts %w[example.com test.com]
42
+ @example.hosts.must == %w[example.com test.com]
43
+ end
44
+
45
+ it "allows setting a single host on an instance" do
46
+ @example.host = 'example.com'
47
+ @example.host.must == 'example.com'
48
+ end
49
+
50
+ it "allows accessing multi-hosts when using the single-host interface on instances" do
51
+ @example.host = 'example.com'
52
+ @example.hosts.must == %w[example.com]
53
+ end
54
+
55
+ it "allows setting multiple hosts on an instance" do
56
+ @example.hosts = %w[example.com test.com]
57
+ @example.hosts.must == %w[example.com test.com]
58
+ end
59
+
60
+ it "denies accessing a single host when using the multi-host interface" do
61
+ @example.hosts = %w[example.com test.com test2.com]
62
+ proc { @example.host }.must raise_error(
63
+ RobotArmy::HostArityError, "There are 3 hosts, so calling host doesn't make sense")
64
+ end
65
+ end
66
+
67
+ describe RobotArmy::TaskMaster, 'remote' do
68
+ before do
69
+ @localhost = Localhost.new
70
+ @example = Example.new
71
+ end
72
+
73
+ it "returns a single item when using the single-host interface" do
74
+ @localhost.stub!(:remote_eval).and_return(7)
75
+ @localhost.remote { 3+4 }.must == 7
76
+ end
77
+
78
+ it "returns an array of items when using the multi-host interface" do
79
+ @example.stub!(:remote_eval).and_return(7)
80
+ @example.remote { 3+4 }.must == [7, 7]
81
+ end
82
+ end
83
+
84
+ describe RobotArmy::TaskMaster do
85
+ before do
86
+ @localhost = Localhost.new
87
+ @example = Example.new
88
+ end
89
+
90
+ it "runs a remote block on each host" do
91
+ @example.should_receive(:remote_eval).exactly(2).times
92
+ @example.remote { 3+4 }
93
+ end
94
+
95
+
96
+ it "can execute a Ruby block and return the result" do
97
+ @localhost.remote { 3+4 }.must == 7
98
+ end
99
+
100
+ it "executes its block in a different process" do
101
+ @localhost.remote { Process.pid }.must_not == Process.pid
102
+ end
103
+
104
+ it "preserves local variables" do
105
+ a = 42
106
+ @localhost.remote { a }.must == 42
107
+ end
108
+
109
+ it "warns about invalid remote return values" do
110
+ capture(:stderr) { @localhost.remote { $stdin } }.
111
+ must =~ /WARNING: ignoring invalid remote return value/
112
+ end
113
+
114
+ it "returns nil if the remote return value is invalid" do
115
+ silence(:stderr) { @localhost.remote { $stdin }.must be_nil }
116
+ end
117
+
118
+ it "re-raises exceptions thrown remotely" do
119
+ proc { @localhost.remote { raise ArgumentError, "You fool!" } }.
120
+ must raise_error(ArgumentError)
121
+ end
122
+
123
+ it "prints the child Ruby's stderr to stderr" do
124
+ pending('we may not want to do this, even')
125
+ capture(:stderr) { @localhost.remote { $stderr.print "foo" } }.must == "foo"
126
+ end
127
+
128
+ it "runs multiple remote blocks for the same host in different processes" do
129
+ @localhost.remote { $a = 1 }
130
+ @localhost.remote { $a }.must be_nil
131
+ end
132
+
133
+ it "only loads one Officer process on the remote machine" do
134
+ info = @localhost.connection(@localhost.host).info
135
+ info[:pid].must_not == Process.pid
136
+ info[:type].must == 'RobotArmy::Officer'
137
+ @localhost.connection(@localhost.host).info.must == info
138
+ end
139
+
140
+ it "runs as a normal (non-super) user by default" do
141
+ @localhost.remote{ Process.uid }.must_not == 0
142
+ end
143
+
144
+ it "loads dependencies" do
145
+ @localhost.dependency "thor"
146
+ @localhost.remote { Thor ; 45 }.must == 45 # loading should not bail here
147
+ end
148
+
149
+ it "delegates scp to the scp binary" do
150
+ @localhost.should_receive(:`).with('scp -q file.tgz example.com:/tmp 2>&1')
151
+ @localhost.host = 'example.com'
152
+ @localhost.scp 'file.tgz', '/tmp'
153
+ end
154
+
155
+ it "delegates to scp without a host when host is localhost" do
156
+ @localhost.should_receive(:`).with('scp -q file.tgz /tmp 2>&1')
157
+ @localhost.scp 'file.tgz', '/tmp'
158
+ end
159
+ end
160
+
161
+ describe RobotArmy::TaskMaster, 'scp' do
162
+ before do
163
+ @localhost = Localhost.new
164
+ end
165
+
166
+ it "raises if scp fails due to a permissions error" do
167
+ @localhost.stub!(:`).and_return("scp: /tmp/foo: Permission denied\n")
168
+ $?.stub!(:exitstatus).and_return(1)
169
+ lambda { @localhost.scp('foo', '/tmp') }.must raise_error(Errno::EACCES)
170
+ end
171
+
172
+ it "raises if scp cannot locate the source file" do
173
+ lambda { @localhost.scp('i-dont-exist', '/tmp') }.must raise_error(Errno::ENOENT)
174
+ end
175
+ end
176
+
177
+ describe RobotArmy::TaskMaster, 'remote (with args)' do
178
+ before do
179
+ @localhost = Localhost.new
180
+ end
181
+
182
+ it "can pass arguments explicitly" do
183
+ @localhost.remote(:args => [42]) { |a| a }.must == 42
184
+ end
185
+
186
+ it "shadows local variables of the same name" do
187
+ a = 23
188
+ @localhost.remote(:args => [42]) { |a| a }.must == 42
189
+ end
190
+ end
191
+
192
+ describe RobotArmy::TaskMaster, 'cptemp' do
193
+ before do
194
+ @localhost = Localhost.new
195
+ @path = 'cptemp-spec-file'
196
+ File.open(@path, 'w') {|f| f << 'testing'}
197
+ end
198
+
199
+ it "safely copies to a new temporary directory" do
200
+ destination = @localhost.cptemp @path
201
+ File.read(destination).must == 'testing'
202
+ end
203
+
204
+ it "yields the path to each host if a block is passed" do
205
+ path, pid = @localhost.cptemp(@path) { |path| [path, Process.pid] }
206
+ File.basename(path).must == @path
207
+ pid.must_not be_nil
208
+ pid.must_not == Process.pid
209
+ end
210
+
211
+ it "deletes the file on exit" do
212
+ destination = @localhost.cptemp @path
213
+ RobotArmy::AtExit.shared_instance.do_exit
214
+ fail "Expected cptemp'ed file to be deleted when exit callbacks were run" if File.exist?(destination)
215
+ end
216
+
217
+ after do
218
+ FileUtils.rm_f(@path)
219
+ end
220
+ end
221
+
222
+ describe RobotArmy::TaskMaster, 'with proxies' do
223
+ before do
224
+ @localhost = Localhost.new
225
+ end
226
+
227
+ it "can allow remote method calls on the local object" do
228
+ def @localhost.foo; 'bar'; end
229
+ @localhost.remote { foo }.must == 'bar'
230
+ end
231
+
232
+ it "allows calling methods with arguments" do
233
+ def @localhost.echo(o) o; end
234
+ @localhost.remote { echo 42 }.must == 42
235
+ end
236
+
237
+ it "allows passing a block to method calls on proxy objects" do
238
+ pending('this is insane. should I do this?')
239
+ end
240
+
241
+ it "allows interaction with IOs" do
242
+ capture(:stdout) {
243
+ stdout = $stdout
244
+ @localhost.remote { stdout.puts "hey there" }
245
+ }.must == "hey there\n"
246
+ end
247
+
248
+ it "returns a proxy if the return value of an upstream call can't be marshaled" do
249
+ def @localhost.stdout; $stdout; end
250
+ capture(:stdout) { @localhost.remote { stdout.puts "foo" } }.must == "foo\n"
251
+ end
252
+ end
253
+
254
+ describe RobotArmy::TaskMaster, 'sudo' do
255
+ before do
256
+ @localhost = Localhost.new
257
+ end
258
+
259
+ it "runs remote with the root user by default" do
260
+ @localhost.should_receive(:remote).
261
+ with(@localhost.hosts, :user => 'root')
262
+
263
+ @localhost.sudo { File.read('/etc/passwd') }
264
+ end
265
+
266
+ it "allows specifying a particular user" do
267
+ @localhost.should_receive(:remote).
268
+ with(@localhost.hosts, :user => 'www-data')
269
+
270
+ @localhost.sudo(:user => 'www-data') { %x{/etc/init.d/apache2 restart} }
271
+ end
272
+ end