sanford 0.17.0 → 0.18.0

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,84 @@
1
+ require 'assert'
2
+ require 'sanford/io_pipe'
3
+
4
+ require 'thread'
5
+
6
+ class Sanford::IOPipe
7
+
8
+ class UnitTests < Assert::Context
9
+ desc "Sanford::IOPipe"
10
+ setup do
11
+ # mimic how IO.select responds
12
+ @io_select_response = Factory.boolean ? [[NULL], [], []] : nil
13
+ @io_select_called_with = nil
14
+ Assert.stub(IO, :select) do |*args|
15
+ @io_select_called_with = args
16
+ @io_select_response
17
+ end
18
+
19
+ @io_pipe = Sanford::IOPipe.new
20
+ end
21
+ subject{ @io_pipe }
22
+
23
+ should have_readers :reader, :writer
24
+ should have_imeths :setup, :teardown
25
+ should have_imeths :read, :write, :wait
26
+
27
+ should "default its reader and writer" do
28
+ assert_same NULL, subject.reader
29
+ assert_same NULL, subject.writer
30
+ end
31
+
32
+ should "change its reader/writer to an IO pipe when setup" do
33
+ subject.setup
34
+ assert_not_same NULL, subject.reader
35
+ assert_not_same NULL, subject.writer
36
+ assert_instance_of IO, subject.reader
37
+ assert_instance_of IO, subject.writer
38
+ end
39
+
40
+ should "close its reader/writer and set them to defaults when torn down" do
41
+ subject.setup
42
+ reader = subject.reader
43
+ writer = subject.writer
44
+
45
+ subject.teardown
46
+ assert_true reader.closed?
47
+ assert_true writer.closed?
48
+ assert_same NULL, subject.reader
49
+ assert_same NULL, subject.writer
50
+ end
51
+
52
+ should "be able to read/write values" do
53
+ subject.setup
54
+
55
+ value = Factory.string(NUMBER_OF_BYTES)
56
+ subject.write(value)
57
+ assert_equal value, subject.read
58
+ end
59
+
60
+ should "only read/write a fixed number of bytes" do
61
+ subject.setup
62
+
63
+ value = Factory.string
64
+ subject.write(value)
65
+ assert_equal value[0, NUMBER_OF_BYTES], subject.read
66
+ end
67
+
68
+ should "be able to wait until there is something to read" do
69
+ subject.setup
70
+
71
+ result = subject.wait
72
+ exp = [[subject.reader], nil, nil, nil]
73
+ assert_equal exp, @io_select_called_with
74
+ assert_equal !!@io_select_response, result
75
+
76
+ timeout = Factory.integer
77
+ subject.wait(timeout)
78
+ exp = [[subject.reader], nil, nil, timeout]
79
+ assert_equal exp, @io_select_called_with
80
+ end
81
+
82
+ end
83
+
84
+ end
@@ -1,6 +1,7 @@
1
1
  require 'assert'
2
2
  require 'sanford/process'
3
3
 
4
+ require 'sanford/io_pipe'
4
5
  require 'sanford/server'
5
6
  require 'test/support/pid_file_spy'
6
7
 
@@ -9,22 +10,37 @@ class Sanford::Process
9
10
  class UnitTests < Assert::Context
10
11
  desc "Sanford::Process"
11
12
  setup do
13
+ @current_env_server_fd = ENV['SANFORD_SERVER_FD']
14
+ @current_env_client_fds = ENV['SANFORD_CLIENT_FDS']
15
+ @current_env_skip_daemonize = ENV['SANFORD_SKIP_DAEMONIZE']
16
+ ENV.delete('SANFORD_SERVER_FD')
17
+ ENV.delete('SANFORD_CLIENT_FDS')
18
+ ENV.delete('SANFORD_SKIP_DAEMONIZE')
19
+
12
20
  @process_class = Sanford::Process
13
21
  end
22
+ teardown do
23
+ ENV['SANFORD_SKIP_DAEMONIZE'] = @current_env_skip_daemonize
24
+ ENV['SANFORD_CLIENT_FDS'] = @current_env_client_fds
25
+ ENV['SANFORD_SERVER_FD'] = @current_env_server_fd
26
+ end
14
27
  subject{ @process_class }
15
28
 
29
+ should "know its wait for signals timeout" do
30
+ assert_equal 15, WAIT_FOR_SIGNALS_TIMEOUT
31
+ end
32
+
33
+ should "know its signal strings" do
34
+ assert_equal 'H', HALT
35
+ assert_equal 'S', STOP
36
+ assert_equal 'R', RESTART
37
+ end
38
+
16
39
  end
17
40
 
18
41
  class InitTests < UnitTests
19
42
  desc "when init"
20
43
  setup do
21
- @current_env_server_fd = ENV['SANFORD_SERVER_FD']
22
- @current_env_client_fds = ENV['SANFORD_CLIENT_FDS']
23
- @current_env_skip_daemonize = ENV['SANFORD_SKIP_DAEMONIZE']
24
- ENV.delete('SANFORD_SERVER_FD')
25
- ENV.delete('SANFORD_CLIENT_FDS')
26
- ENV.delete('SANFORD_SKIP_DAEMONIZE')
27
-
28
44
  @server_spy = ServerSpy.new
29
45
 
30
46
  @pid_file_spy = PIDFileSpy.new(Factory.integer)
@@ -37,14 +53,9 @@ class Sanford::Process
37
53
 
38
54
  @process = @process_class.new(@server_spy)
39
55
  end
40
- teardown do
41
- ENV['SANFORD_SKIP_DAEMONIZE'] = @current_env_skip_daemonize
42
- ENV['SANFORD_CLIENT_FDS'] = @current_env_client_fds
43
- ENV['SANFORD_SERVER_FD'] = @current_env_server_fd
44
- end
45
56
  subject{ @process }
46
57
 
47
- should have_readers :server, :name, :pid_file, :restart_cmd
58
+ should have_readers :server, :name, :pid_file, :signal_io, :restart_cmd
48
59
  should have_readers :server_ip, :server_port, :server_fd, :client_fds
49
60
  should have_imeths :run, :daemonize?
50
61
 
@@ -52,16 +63,17 @@ class Sanford::Process
52
63
  assert_equal @server_spy, subject.server
53
64
  end
54
65
 
55
- should "know its name, pid file and restart cmd" do
56
- expected = "sanford-#{@server_spy.name}-" \
57
- "#{@server_spy.configured_ip}-#{@server_spy.configured_port}"
58
- assert_equal expected, subject.name
59
- assert_equal @pid_file_spy, subject.pid_file
60
- assert_equal @restart_cmd_spy, subject.restart_cmd
66
+ should "know its name, pid file, signal io and restart cmd" do
67
+ exp = "sanford: #{@server_spy.process_label}"
68
+ assert_equal exp, subject.name
69
+
70
+ assert_equal @pid_file_spy, subject.pid_file
71
+ assert_instance_of Sanford::IOPipe, subject.signal_io
72
+ assert_equal @restart_cmd_spy, subject.restart_cmd
61
73
  end
62
74
 
63
75
  should "know its server ip, port and file descriptor" do
64
- assert_equal @server_spy.configured_ip, subject.server_ip
76
+ assert_equal @server_spy.configured_ip, subject.server_ip
65
77
  assert_equal @server_spy.configured_port, subject.server_port
66
78
  assert_nil subject.server_fd
67
79
  end
@@ -124,28 +136,14 @@ class Sanford::Process
124
136
 
125
137
  @current_process_name = $0
126
138
 
127
- @term_signal_trap_block = nil
128
- @term_signal_trap_called = false
129
- Assert.stub(::Signal, :trap).with("TERM") do |&block|
130
- @term_signal_trap_block = block
131
- @term_signal_trap_called = true
132
- end
133
-
134
- @int_signal_trap_block = nil
135
- @int_signal_trap_called = false
136
- Assert.stub(::Signal, :trap).with("INT") do |&block|
137
- @int_signal_trap_block = block
138
- @int_signal_trap_called = true
139
- end
140
-
141
- @usr2_signal_trap_block = nil
142
- @usr2_signal_trap_called = false
143
- Assert.stub(::Signal, :trap).with("USR2") do |&block|
144
- @usr2_signal_trap_block = block
145
- @usr2_signal_trap_called = true
139
+ @signal_traps = []
140
+ Assert.stub(::Signal, :trap) do |signal, &block|
141
+ @signal_traps << SignalTrap.new(signal, block)
146
142
  end
147
143
  end
148
144
  teardown do
145
+ @process.signal_io.write(HALT)
146
+ @thread.join if @thread
149
147
  $0 = @current_process_name
150
148
  end
151
149
 
@@ -154,58 +152,74 @@ class Sanford::Process
154
152
  class RunTests < RunSetupTests
155
153
  desc "and run"
156
154
  setup do
157
- @process.run
155
+ @wait_timeout = nil
156
+ Assert.stub(@process.signal_io, :wait) do |timeout|
157
+ @wait_timeout = timeout
158
+ sleep 2*JOIN_SECONDS
159
+ false
160
+ end
161
+
162
+ @thread = Thread.new{ @process.run }
163
+ @thread.join(JOIN_SECONDS)
164
+ end
165
+ teardown do
166
+ # manually unstub or the process thread will hang forever
167
+ Assert.unstub(@process.signal_io, :wait)
158
168
  end
159
169
 
160
- should "not have daemonized the process" do
170
+ should "not daemonize the process" do
161
171
  assert_false @daemonize_called
162
172
  end
163
173
 
164
- should "have started the server listening" do
174
+ should "start the server listening" do
165
175
  assert_true @server_spy.listen_called
166
- expected = [ subject.server_ip, subject.server_port ]
167
- assert_equal expected, @server_spy.listen_args
176
+ exp = [subject.server_ip, subject.server_port]
177
+ assert_equal exp, @server_spy.listen_args
168
178
  end
169
179
 
170
- should "have set the process name" do
180
+ should "set the process name" do
171
181
  assert_equal $0, subject.name
172
182
  end
173
183
 
174
- should "have written the PID file" do
184
+ should "write its PID file" do
175
185
  assert_true @pid_file_spy.write_called
176
186
  end
177
187
 
178
- should "have trapped signals" do
179
- assert_true @term_signal_trap_called
180
- assert_false @server_spy.stop_called
181
- @term_signal_trap_block.call
182
- assert_true @server_spy.stop_called
183
-
184
- assert_true @int_signal_trap_called
185
- assert_false @server_spy.halt_called
186
- @int_signal_trap_block.call
187
- assert_true @server_spy.halt_called
188
-
189
- assert_true @usr2_signal_trap_called
190
- assert_false @server_spy.pause_called
191
- @usr2_signal_trap_block.call
192
- assert_true @server_spy.pause_called
188
+ should "trap signals" do
189
+ assert_equal 3, @signal_traps.size
190
+ assert_equal ['INT', 'TERM', 'USR2'], @signal_traps.map(&:signal)
193
191
  end
194
192
 
195
- should "have started the server" do
193
+ should "start the server" do
196
194
  assert_true @server_spy.start_called
197
195
  end
198
196
 
199
- should "have joined the server thread" do
200
- assert_true @server_spy.thread.join_called
197
+ should "sleep its thread waiting for signals" do
198
+ assert_equal WAIT_FOR_SIGNALS_TIMEOUT, @wait_timeout
199
+ assert_equal 'sleep', @thread.status
201
200
  end
202
201
 
203
202
  should "not run the restart cmd" do
204
- assert_false @restart_cmd_spy.run_called
203
+ assert_nil @restart_cmd_spy.run_called_for
205
204
  end
206
205
 
207
- should "have removed the PID file" do
208
- assert_true @pid_file_spy.remove_called
206
+ end
207
+
208
+ class SignalTrapsTests < RunSetupTests
209
+ desc "signal traps"
210
+ setup do
211
+ # setup the io pipe so we can see whats written to it
212
+ @process.signal_io.setup
213
+ end
214
+ teardown do
215
+ @process.signal_io.teardown
216
+ end
217
+
218
+ should "write the signals to processes signal IO" do
219
+ @signal_traps.each do |signal_trap|
220
+ signal_trap.block.call
221
+ assert_equal signal_trap.signal, subject.signal_io.read
222
+ end
209
223
  end
210
224
 
211
225
  end
@@ -214,7 +228,8 @@ class Sanford::Process
214
228
  desc "that should daemonize is run"
215
229
  setup do
216
230
  Assert.stub(@process, :daemonize?){ true }
217
- @process.run
231
+ @thread = Thread.new{ @process.run }
232
+ @thread.join(JOIN_SECONDS)
218
233
  end
219
234
 
220
235
  should "have daemonized the process" do
@@ -228,13 +243,14 @@ class Sanford::Process
228
243
  setup do
229
244
  ENV['SANFORD_SERVER_FD'] = Factory.integer.to_s
230
245
  @process = @process_class.new(@server_spy)
231
- @process.run
246
+ @thread = Thread.new{ @process.run }
247
+ @thread.join(JOIN_SECONDS)
232
248
  end
233
249
 
234
250
  should "have used the file descriptor when listening" do
235
251
  assert_true @server_spy.listen_called
236
- expected = [ @process.server_fd ]
237
- assert_equal expected, @server_spy.listen_args
252
+ exp = [@process.server_fd]
253
+ assert_equal exp, @server_spy.listen_args
238
254
  end
239
255
 
240
256
  end
@@ -245,7 +261,8 @@ class Sanford::Process
245
261
  @client_fds = [ Factory.integer, Factory.integer ]
246
262
  ENV['SANFORD_CLIENT_FDS'] = @client_fds.join(',')
247
263
  @process = @process_class.new(@server_spy)
248
- @process.run
264
+ @thread = Thread.new{ @process.run }
265
+ @thread.join(JOIN_SECONDS)
249
266
  end
250
267
 
251
268
  should "have used the client file descriptors when starting" do
@@ -255,25 +272,120 @@ class Sanford::Process
255
272
 
256
273
  end
257
274
 
258
- class RunAndServerPausedTests < RunSetupTests
259
- desc "then run and then paused"
275
+ class RunAndHaltTests < RunSetupTests
276
+ desc "and run with a halt signal"
260
277
  setup do
261
- server_fd = Factory.integer
262
- Assert.stub(@server_spy, :file_descriptor){ server_fd }
263
- client_fds = [ Factory.integer, Factory.integer ]
264
- Assert.stub(@server_spy, :client_file_descriptors){ client_fds }
278
+ @thread = Thread.new{ @process.run }
279
+ @thread.join(JOIN_SECONDS)
280
+ @process.signal_io.write(HALT)
281
+ @thread.join(JOIN_SECONDS)
282
+ end
283
+
284
+ should "halt its server" do
285
+ assert_true @server_spy.halt_called
286
+ assert_equal [true], @server_spy.halt_args
287
+ end
288
+
289
+ should "not set the env var to skip daemonize" do
290
+ assert_equal @current_env_skip_daemonize, ENV['SANFORD_SKIP_DAEMONIZE']
291
+ end
292
+
293
+ should "not run the restart cmd" do
294
+ assert_nil @restart_cmd_spy.run_called_for
295
+ end
296
+
297
+ should "remove the PID file" do
298
+ assert_true @pid_file_spy.remove_called
299
+ end
300
+
301
+ end
302
+
303
+ class RunAndStopTests < RunSetupTests
304
+ desc "and run with a stop signal"
305
+ setup do
306
+ @thread = Thread.new{ @process.run }
307
+ @thread.join(JOIN_SECONDS)
308
+ @process.signal_io.write(STOP)
309
+ @thread.join(JOIN_SECONDS)
310
+ end
311
+
312
+ should "stop its server" do
313
+ assert_true @server_spy.stop_called
314
+ assert_equal [true], @server_spy.stop_args
315
+ end
316
+
317
+ should "not set the env var to skip daemonize" do
318
+ assert_equal @current_env_skip_daemonize, ENV['SANFORD_SKIP_DAEMONIZE']
319
+ end
320
+
321
+ should "not run the restart cmd" do
322
+ assert_nil @restart_cmd_spy.run_called_for
323
+ end
324
+
325
+ should "remove the PID file" do
326
+ assert_true @pid_file_spy.remove_called
327
+ end
328
+
329
+ end
265
330
 
266
- # mimicing pause being called by a signal, after the thread is joined
267
- @server_spy.thread.on_join{ @server_spy.pause }
268
- @process.run
331
+ class RunAndRestartTests < RunSetupTests
332
+ desc "and run with a restart signal"
333
+ setup do
334
+ @thread = Thread.new{ @process.run }
335
+ @thread.join(JOIN_SECONDS)
336
+ @process.signal_io.write(RESTART)
337
+ @thread.join(JOIN_SECONDS)
269
338
  end
270
339
 
271
- should "set env vars for restarting and run the restart cmd" do
272
- assert_equal @server_spy.file_descriptor.to_s, ENV['SANFORD_SERVER_FD']
273
- expected = @server_spy.client_file_descriptors.join(',')
274
- assert_equal expected, ENV['SANFORD_CLIENT_FDS']
275
- assert_equal 'yes', ENV['SANFORD_SKIP_DAEMONIZE']
276
- assert_true @restart_cmd_spy.run_called
340
+ should "pause its server" do
341
+ assert_true @server_spy.pause_called
342
+ assert_equal [true], @server_spy.pause_args
343
+ end
344
+
345
+ should "run the restart cmd" do
346
+ assert_equal @server_spy, @restart_cmd_spy.run_called_for
347
+ end
348
+
349
+ end
350
+
351
+ class RunWithServerCrashTests < RunSetupTests
352
+ desc "and run with the server crashing"
353
+ setup do
354
+ Assert.stub(@process.signal_io, :wait) do |timeout|
355
+ sleep JOIN_SECONDS * 0.5 # ensure this has time to run before the thread
356
+ # joins below
357
+ false
358
+ end
359
+
360
+ @thread = Thread.new{ @process.run }
361
+ @thread.join(JOIN_SECONDS)
362
+ @server_spy.start_called = false
363
+ @thread.join(JOIN_SECONDS)
364
+ end
365
+ teardown do
366
+ # manually unstub or the process thread will hang forever
367
+ Assert.unstub(@process.signal_io, :wait)
368
+ end
369
+
370
+ should "re-start its server" do
371
+ assert_true @server_spy.start_called
372
+ end
373
+
374
+ end
375
+
376
+ class RunWithInvalidSignalTests < RunSetupTests
377
+ desc "and run with unsupported signals"
378
+ setup do
379
+ # ruby throws an argument error if the OS doesn't support a signal
380
+ Assert.stub(::Signal, :trap){ raise ArgumentError }
381
+
382
+ @thread = Thread.new{ @process.run }
383
+ @thread.join(JOIN_SECONDS)
384
+ end
385
+
386
+ should "start normally" do
387
+ assert_true @server_spy.start_called
388
+ assert_equal 'sleep', @thread.status
277
389
  end
278
390
 
279
391
  end
@@ -289,6 +401,12 @@ class Sanford::Process
289
401
  Assert.stub(File, :stat).with(Dir.pwd){ @ruby_pwd_stat }
290
402
  Assert.stub(File, :stat).with(ENV['PWD']){ env_pwd_stat }
291
403
 
404
+ @server_spy = ServerSpy.new
405
+ server_fd = Factory.integer
406
+ Assert.stub(@server_spy, :file_descriptor){ server_fd }
407
+ client_fds = Factory.integer(3).times.map{ Factory.integer }
408
+ Assert.stub(@server_spy, :client_file_descriptors){ client_fds }
409
+
292
410
  @chdir_called_with = nil
293
411
  Assert.stub(Dir, :chdir){ |*args| @chdir_called_with = args }
294
412
 
@@ -317,10 +435,38 @@ class Sanford::Process
317
435
  assert_equal [Gem.ruby, $0, ARGV].flatten, subject.argv
318
436
  end
319
437
 
320
- should "change the dir and run a kernel exec when run" do
321
- subject.run
322
- assert_equal [subject.dir], @chdir_called_with
323
- assert_equal subject.argv, @exec_called_with
438
+ if RUBY_VERSION == '1.8.7'
439
+
440
+ should "set env vars, change the dir and kernel exec when run" do
441
+ subject.run(@server_spy)
442
+
443
+ assert_equal @server_spy.file_descriptor.to_s, ENV['SANFORD_SERVER_FD']
444
+ exp = @server_spy.client_file_descriptors.join(',')
445
+ assert_equal exp, ENV['SANFORD_CLIENT_FDS']
446
+ assert_equal 'yes', ENV['SANFORD_SKIP_DAEMONIZE']
447
+
448
+ assert_equal [subject.dir], @chdir_called_with
449
+ assert_equal subject.argv, @exec_called_with
450
+ end
451
+
452
+ else
453
+
454
+ should "kernel exec when run" do
455
+ subject.run(@server_spy)
456
+
457
+ env = {
458
+ 'SANFORD_SERVER_FD' => @server_spy.file_descriptor.to_s,
459
+ 'SANFORD_CLIENT_FDS' => @server_spy.client_file_descriptors.join(','),
460
+ 'SANFORD_SKIP_DAEMONIZE' => 'yes'
461
+ }
462
+ fd_redirects = (
463
+ [@server_spy.file_descriptor] +
464
+ @server_spy.client_file_descriptors
465
+ ).inject({}){ |h, fd| h.merge!(fd => fd) }
466
+ options = { :chdir => subject.dir }.merge!(fd_redirects)
467
+ assert_equal ([env] + subject.argv + [options]), @exec_called_with
468
+ end
469
+
324
470
  end
325
471
 
326
472
  end
@@ -364,31 +510,35 @@ class Sanford::Process
364
510
 
365
511
  end
366
512
 
513
+ SignalTrap = Struct.new(:signal, :block)
514
+
367
515
  class ServerSpy
368
516
  include Sanford::Server
369
517
 
370
- name Factory.string
371
- ip Factory.string
372
- port Factory.integer
518
+ name Factory.string
519
+ ip Factory.string
520
+ port Factory.integer
373
521
  pid_file Factory.file_path
374
522
 
375
- attr_reader :listen_called, :start_called
376
- attr_reader :stop_called, :halt_called, :pause_called
377
- attr_reader :listen_args, :start_args
378
- attr_reader :thread
523
+ attr_accessor :process_label, :listen_called
524
+ attr_accessor :start_called, :stop_called, :halt_called, :pause_called
525
+ attr_reader :listen_args, :start_args, :stop_args, :halt_args, :pause_args
379
526
 
380
527
  def initialize(*args)
381
528
  super
382
- @listen_called = false
383
- @start_called = false
384
- @stop_called = false
385
- @halt_called = false
386
- @pause_called = false
387
529
 
388
- @listen_args = nil
389
- @start_args = nil
530
+ @process_label = Factory.string
390
531
 
391
- @thread = ThreadSpy.new
532
+ @listen_args = nil
533
+ @listen_called = false
534
+ @start_args = nil
535
+ @start_called = false
536
+ @stop_args = nil
537
+ @stop_called = false
538
+ @halt_args = nil
539
+ @halt_called = false
540
+ @pause_args = nil
541
+ @pause_called = false
392
542
  end
393
543
 
394
544
  def listen(*args)
@@ -399,53 +549,38 @@ class Sanford::Process
399
549
  def start(*args)
400
550
  @start_args = args
401
551
  @start_called = true
402
- @thread
403
552
  end
404
553
 
405
554
  def stop(*args)
555
+ @stop_args = args
406
556
  @stop_called = true
407
557
  end
408
558
 
409
559
  def halt(*args)
560
+ @halt_args = args
410
561
  @halt_called = true
411
562
  end
412
563
 
564
+
413
565
  def pause(*args)
566
+ @pause_args = args
414
567
  @pause_called = true
415
568
  end
416
569
 
417
- def paused?
418
- @pause_called
419
- end
420
- end
421
-
422
- class ThreadSpy
423
- attr_reader :join_called, :on_join_proc
424
-
425
- def initialize
426
- @join_called = false
427
- @on_join_proc = proc{ }
428
- end
429
-
430
- def on_join(&block)
431
- @on_join_proc = block
432
- end
433
-
434
- def join
435
- @join_called = true
436
- @on_join_proc.call
570
+ def running?
571
+ !!@start_called
437
572
  end
438
573
  end
439
574
 
440
575
  class RestartCmdSpy
441
- attr_reader :run_called
576
+ attr_reader :run_called_for
442
577
 
443
578
  def initialize
444
- @run_called = false
579
+ @run_called_for = nil
445
580
  end
446
581
 
447
- def run
448
- @run_called = true
582
+ def run(server)
583
+ @run_called_for = server
449
584
  end
450
585
  end
451
586