qs 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +1 -0
  2. data/Gemfile +6 -1
  3. data/LICENSE.txt +1 -1
  4. data/bench/config.qs +46 -0
  5. data/bench/queue.rb +8 -0
  6. data/bench/report.rb +114 -0
  7. data/bench/report.txt +11 -0
  8. data/bin/qs +7 -0
  9. data/lib/qs/cli.rb +124 -0
  10. data/lib/qs/client.rb +121 -0
  11. data/lib/qs/config_file.rb +79 -0
  12. data/lib/qs/daemon.rb +350 -0
  13. data/lib/qs/daemon_data.rb +46 -0
  14. data/lib/qs/error_handler.rb +58 -0
  15. data/lib/qs/job.rb +70 -0
  16. data/lib/qs/job_handler.rb +90 -0
  17. data/lib/qs/logger.rb +23 -0
  18. data/lib/qs/payload_handler.rb +136 -0
  19. data/lib/qs/pid_file.rb +42 -0
  20. data/lib/qs/process.rb +136 -0
  21. data/lib/qs/process_signal.rb +20 -0
  22. data/lib/qs/qs_runner.rb +49 -0
  23. data/lib/qs/queue.rb +69 -0
  24. data/lib/qs/redis_item.rb +33 -0
  25. data/lib/qs/route.rb +52 -0
  26. data/lib/qs/runner.rb +26 -0
  27. data/lib/qs/test_helpers.rb +17 -0
  28. data/lib/qs/test_runner.rb +43 -0
  29. data/lib/qs/version.rb +1 -1
  30. data/lib/qs.rb +92 -2
  31. data/qs.gemspec +7 -2
  32. data/test/helper.rb +8 -1
  33. data/test/support/app_daemon.rb +74 -0
  34. data/test/support/config.qs +7 -0
  35. data/test/support/config_files/empty.qs +0 -0
  36. data/test/support/config_files/invalid.qs +1 -0
  37. data/test/support/config_files/valid.qs +7 -0
  38. data/test/support/config_invalid_run.qs +3 -0
  39. data/test/support/config_no_run.qs +0 -0
  40. data/test/support/factory.rb +14 -0
  41. data/test/support/pid_file_spy.rb +19 -0
  42. data/test/support/runner_spy.rb +17 -0
  43. data/test/system/daemon_tests.rb +226 -0
  44. data/test/unit/cli_tests.rb +188 -0
  45. data/test/unit/client_tests.rb +269 -0
  46. data/test/unit/config_file_tests.rb +59 -0
  47. data/test/unit/daemon_data_tests.rb +96 -0
  48. data/test/unit/daemon_tests.rb +702 -0
  49. data/test/unit/error_handler_tests.rb +163 -0
  50. data/test/unit/job_handler_tests.rb +253 -0
  51. data/test/unit/job_tests.rb +132 -0
  52. data/test/unit/logger_tests.rb +38 -0
  53. data/test/unit/payload_handler_tests.rb +276 -0
  54. data/test/unit/pid_file_tests.rb +70 -0
  55. data/test/unit/process_signal_tests.rb +61 -0
  56. data/test/unit/process_tests.rb +371 -0
  57. data/test/unit/qs_runner_tests.rb +166 -0
  58. data/test/unit/qs_tests.rb +217 -0
  59. data/test/unit/queue_tests.rb +132 -0
  60. data/test/unit/redis_item_tests.rb +49 -0
  61. data/test/unit/route_tests.rb +81 -0
  62. data/test/unit/runner_tests.rb +63 -0
  63. data/test/unit/test_helper_tests.rb +61 -0
  64. data/test/unit/test_runner_tests.rb +128 -0
  65. metadata +180 -15
data/lib/qs.rb CHANGED
@@ -1,5 +1,95 @@
1
- require "qs/version"
1
+ require 'ns-options'
2
+ require 'qs/version'
3
+ require 'qs/client'
4
+ require 'qs/daemon'
5
+ require 'qs/job_handler'
6
+ require 'qs/queue'
2
7
 
3
8
  module Qs
4
- # TODO: your code goes here...
9
+
10
+ def self.config; @config ||= Config.new; end
11
+ def self.configure(&block)
12
+ block.call(self.config)
13
+ end
14
+
15
+ def self.init
16
+ self.config.redis.url ||= RedisUrl.new(
17
+ self.config.redis.ip,
18
+ self.config.redis.port,
19
+ self.config.redis.db
20
+ )
21
+
22
+ @serializer ||= self.config.serializer
23
+ @deserializer ||= self.config.deserializer
24
+ @client ||= Client.new(self.redis_config)
25
+ @redis ||= @client.redis
26
+ true
27
+ end
28
+
29
+ def self.reset!
30
+ self.config.reset
31
+ @serializer = nil
32
+ @deserializer = nil
33
+ @client = nil
34
+ @redis = nil
35
+ true
36
+ end
37
+
38
+ def self.enqueue(queue, job_name, params = nil)
39
+ @client.enqueue(queue, job_name, params)
40
+ end
41
+
42
+ def self.push(queue_name, payload)
43
+ @client.push(queue_name, payload)
44
+ end
45
+
46
+ def self.serialize(payload)
47
+ @serializer.call(payload)
48
+ end
49
+
50
+ def self.deserialize(serialized_payload)
51
+ @deserializer.call(serialized_payload)
52
+ end
53
+
54
+ def self.client
55
+ @client
56
+ end
57
+
58
+ def self.redis
59
+ @redis
60
+ end
61
+
62
+ def self.redis_config
63
+ self.config.redis.to_hash
64
+ end
65
+
66
+ class Config
67
+ include NsOptions::Proxy
68
+
69
+ option :serializer, Proc, :default => proc{ |p| ::JSON.dump(p) }
70
+ option :deserializer, Proc, :default => proc{ |p| ::JSON.load(p) }
71
+
72
+ option :timeout, Float
73
+
74
+ namespace :redis do
75
+ option :ip, :default => 'localhost'
76
+ option :port, :default => 6379
77
+ option :db, :default => 0
78
+
79
+ option :url
80
+
81
+ option :redis_ns, String, :default => 'qs'
82
+ option :driver, String, :default => 'ruby'
83
+ option :timeout, Integer, :default => 1
84
+ option :size, Integer, :default => 4
85
+ end
86
+ end
87
+
88
+ module RedisUrl
89
+ def self.new(ip, port, db)
90
+ return if ip.to_s.empty? || port.to_s.empty? || db.to_s.empty?
91
+ "redis://#{ip}:#{port}/#{db}"
92
+ end
93
+ end
94
+
5
95
  end
data/qs.gemspec CHANGED
@@ -11,13 +11,18 @@ Gem::Specification.new do |gem|
11
11
  gem.description = %q{Define message queues. Process jobs and events. Profit.}
12
12
  gem.summary = %q{Define message queues. Process jobs and events. Profit.}
13
13
  gem.homepage = "http://github.com/redding/qs"
14
+ gem.license = 'MIT'
14
15
 
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
19
  gem.require_paths = ["lib"]
19
20
 
20
- gem.add_development_dependency("assert")
21
- # TODO: gem.add_dependency("gem-name", ["~> 0.0"])
21
+ gem.add_dependency("dat-worker-pool", ["~> 0.5"])
22
+ gem.add_dependency("hella-redis", ["~> 0.2"])
23
+ gem.add_dependency("ns-options", ["~> 1.1"])
24
+ gem.add_dependency("SystemTimer", ["~> 1.2"])
22
25
 
26
+ gem.add_development_dependency("assert", ["~> 2.14"])
27
+ gem.add_development_dependency("scmd", ["~> 2.3"])
23
28
  end
data/test/helper.rb CHANGED
@@ -4,4 +4,11 @@
4
4
  # add the root dir to the load path
5
5
  $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
6
6
 
7
- # TODO: put test helpers here...
7
+ require 'pry' # require pry for debugging (`binding.pry`)
8
+
9
+ require 'pathname'
10
+ ROOT_PATH = Pathname.new(File.expand_path('../..', __FILE__))
11
+
12
+ require 'test/support/factory'
13
+
14
+ require 'json' # so the default serializer/deserializer procs will work
@@ -0,0 +1,74 @@
1
+ require 'qs'
2
+
3
+ AppQueue = Qs::Queue.new do
4
+ name 'app_main'
5
+
6
+ job_handler_ns 'AppHandlers'
7
+
8
+ job 'basic', 'Basic'
9
+ job 'error', 'Error'
10
+ job 'timeout', 'Timeout'
11
+ job 'slow', 'Slow'
12
+ end
13
+
14
+ class AppDaemon
15
+ include Qs::Daemon
16
+
17
+ name 'app'
18
+
19
+ logger Logger.new(ROOT_PATH.join('log/app_daemon.log').to_s)
20
+ verbose_logging true
21
+
22
+ queue AppQueue
23
+
24
+ error do |exception, context|
25
+ job_name = context.job.name if context.job
26
+ case(job_name)
27
+ when 'error', 'timeout'
28
+ message = "#{exception.class}: #{exception.message}"
29
+ Qs.redis.with{ |c| c.set('last_error', message) }
30
+ when 'slow'
31
+ Qs.redis.with{ |c| c.set('last_error', exception.class.to_s) }
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ module AppHandlers
38
+
39
+ class Basic
40
+ include Qs::JobHandler
41
+
42
+ def run!
43
+ Qs.redis.with{ |c| c.set(params['key'], params['value']) }
44
+ end
45
+ end
46
+
47
+ class Error
48
+ include Qs::JobHandler
49
+
50
+ def run!
51
+ raise params['error_message']
52
+ end
53
+ end
54
+
55
+ class Timeout
56
+ include Qs::JobHandler
57
+
58
+ timeout 0.2
59
+
60
+ def run!
61
+ sleep 2
62
+ end
63
+ end
64
+
65
+ class Slow
66
+ include Qs::JobHandler
67
+
68
+ def run!
69
+ sleep 5
70
+ Qs.redis.with{ |c| c.set('slow', 'finished') }
71
+ end
72
+ end
73
+
74
+ end
@@ -0,0 +1,7 @@
1
+ require 'test/support/app_daemon'
2
+
3
+ if !defined?(TestConstant)
4
+ TestConstant = Class.new
5
+ end
6
+
7
+ run AppDaemon.new
File without changes
@@ -0,0 +1 @@
1
+ run 'test'
@@ -0,0 +1,7 @@
1
+ require 'qs'
2
+
3
+ class MyDaemon
4
+ include Qs::Daemon
5
+ end
6
+
7
+ run MyDaemon.new
@@ -0,0 +1,3 @@
1
+ MyDaemon = Class.new
2
+
3
+ run MyDaemon
File without changes
@@ -0,0 +1,14 @@
1
+ require 'assert/factory'
2
+
3
+ module Factory
4
+ extend Assert::Factory
5
+
6
+ def self.exception(klass = nil, message = nil)
7
+ klass ||= StandardError
8
+ message ||= Factory.text
9
+ exception = nil
10
+ begin; raise(klass, message); rescue StandardError => exception; end
11
+ exception
12
+ end
13
+
14
+ end
@@ -0,0 +1,19 @@
1
+ class PIDFileSpy
2
+
3
+ attr_reader :pid, :write_called, :remove_called
4
+
5
+ def initialize(pid)
6
+ @pid = pid
7
+ @write_called = false
8
+ @remove_called = false
9
+ end
10
+
11
+ def write
12
+ @write_called = true
13
+ end
14
+
15
+ def remove
16
+ @remove_called = true
17
+ end
18
+
19
+ end
@@ -0,0 +1,17 @@
1
+ class RunnerSpy
2
+
3
+ attr_reader :handler_class, :args, :handler
4
+ attr_reader :run_called
5
+
6
+ def initialize(handler_class, args = nil)
7
+ @handler_class = handler_class
8
+ @args = args
9
+ @handler = Factory.string
10
+ @run_called = false
11
+ end
12
+
13
+ def run
14
+ @run_called = true
15
+ end
16
+
17
+ end
@@ -0,0 +1,226 @@
1
+ require 'assert'
2
+ require 'qs/daemon'
3
+
4
+ require 'test/support/app_daemon'
5
+
6
+ module Qs::Daemon
7
+
8
+ class SystemTests < Assert::Context
9
+ desc "Qs::Daemon"
10
+ setup do
11
+ Qs.reset!
12
+ @qs_test_mode = ENV['QS_TEST_MODE']
13
+ ENV['QS_TEST_MODE'] = nil
14
+ Qs.init
15
+ @orig_config = AppDaemon.configuration.to_hash
16
+ end
17
+ teardown do
18
+ @daemon_runner.stop if @daemon_runner
19
+ AppDaemon.configuration.apply(@orig_config) # reset daemon config
20
+ Qs.redis.with{ |c| c.del('slow') }
21
+ Qs.redis.with{ |c| c.del('last_error') }
22
+ Qs.client.clear(AppQueue.redis_key)
23
+ Qs.reset!
24
+ ENV['QS_TEST_MODE'] = @qs_test_mode
25
+ end
26
+
27
+ end
28
+
29
+ class RunningDaemonSetupTests < SystemTests
30
+ setup do
31
+ @daemon = AppDaemon.new
32
+ @daemon_runner = DaemonRunner.new(@daemon)
33
+ @thread = @daemon_runner.start
34
+ end
35
+
36
+ end
37
+
38
+ class BasicJobTests < RunningDaemonSetupTests
39
+ desc "with a basic job added"
40
+ setup do
41
+ @key, @value = [Factory.string, Factory.string]
42
+ AppQueue.add('basic', {
43
+ 'key' => @key,
44
+ 'value' => @value
45
+ })
46
+ @thread.join 0.5
47
+ end
48
+
49
+ should "run the job" do
50
+ assert_equal @value, Qs.redis.with{ |c| c.get(@key) }
51
+ end
52
+
53
+ end
54
+
55
+ class JobThatErrorsTests < RunningDaemonSetupTests
56
+ desc "with a job that errors"
57
+ setup do
58
+ @error_message = Factory.text
59
+ AppQueue.add('error', 'error_message' => @error_message)
60
+ @thread.join 0.5
61
+ end
62
+
63
+ should "run the configured error handler procs" do
64
+ exp = "RuntimeError: #{@error_message}"
65
+ assert_equal exp, Qs.redis.with{ |c| c.get('last_error') }
66
+ end
67
+
68
+ end
69
+
70
+ class TimeoutJobTests < RunningDaemonSetupTests
71
+ desc "with a job that times out"
72
+ setup do
73
+ AppQueue.add('timeout')
74
+ @thread.join 1 # let the daemon have time to process the job
75
+ end
76
+
77
+ should "run the configured error handler procs" do
78
+ handler_class = AppHandlers::Timeout
79
+ exp = "Qs::TimeoutError: #{handler_class} timed out " \
80
+ "(#{handler_class.timeout}s)"
81
+ assert_equal exp, Qs.redis.with{ |c| c.get('last_error') }
82
+ end
83
+
84
+ end
85
+
86
+ class NoWorkersAvailableTests < SystemTests
87
+ desc "when no workers are available"
88
+ setup do
89
+ AppDaemon.workers 0 # no workers available, don't do this
90
+ @daemon = AppDaemon.new
91
+ @daemon_runner = DaemonRunner.new(@daemon)
92
+ @thread = @daemon_runner.start
93
+ end
94
+
95
+ should "shutdown when stopped" do
96
+ @daemon.stop
97
+ @thread.join 2 # give it time to shutdown, should be faster
98
+ assert_false @thread.alive?
99
+ end
100
+
101
+ should "shutdown when halted" do
102
+ @daemon.halt
103
+ @thread.join 2 # give it time to shutdown, should be faster
104
+ assert_false @thread.alive?
105
+ end
106
+
107
+ end
108
+
109
+ class ShutdownWithoutTimeoutTests < SystemTests
110
+ desc "without a shutdown timeout"
111
+ setup do
112
+ AppDaemon.shutdown_timeout nil # disable shutdown timeout
113
+ @daemon = AppDaemon.new
114
+ @daemon_runner = DaemonRunner.new(@daemon)
115
+ @thread = @daemon_runner.start
116
+
117
+ AppQueue.add('slow')
118
+ @thread.join 1 # let the daemon have time to process the job
119
+ end
120
+
121
+ should "shutdown and let the job finished" do
122
+ @daemon.stop
123
+ @thread.join 10 # give it time to shutdown, should be faster
124
+ assert_false @thread.alive?
125
+ assert_equal 'finished', Qs.redis.with{ |c| c.get('slow') }
126
+ end
127
+
128
+ should "shutdown and not let the job finished" do
129
+ @daemon.halt
130
+ @thread.join 2 # give it time to shutdown, should be faster
131
+ assert_false @thread.alive?
132
+ assert_nil Qs.redis.with{ |c| c.get('slow') }
133
+ exp = "Qs::ShutdownError"
134
+ assert_equal exp, Qs.redis.with{ |c| c.get('last_error') }
135
+ end
136
+
137
+ end
138
+
139
+ class ShutdownWithTimeoutTests < SystemTests
140
+ desc "with a shutdown timeout"
141
+ setup do
142
+ AppDaemon.shutdown_timeout 1
143
+ @daemon = AppDaemon.new
144
+ @daemon_runner = DaemonRunner.new(@daemon)
145
+ @thread = @daemon_runner.start
146
+
147
+ AppQueue.add('slow')
148
+ @thread.join 1 # let the daemon have time to process the job
149
+ end
150
+
151
+ should "shutdown and not let the job finished" do
152
+ @daemon.stop
153
+ @thread.join 2 # give it time to shutdown, should be faster
154
+ assert_false @thread.alive?
155
+ assert_nil Qs.redis.with{ |c| c.get('slow') }
156
+ exp = "Qs::ShutdownError"
157
+ assert_equal exp, Qs.redis.with{ |c| c.get('last_error') }
158
+ end
159
+
160
+ should "shutdown and not let the job finished" do
161
+ @daemon.halt
162
+ @thread.join 2 # give it time to shutdown, should be faster
163
+ assert_false @thread.alive?
164
+ assert_nil Qs.redis.with{ |c| c.get('slow') }
165
+ exp = "Qs::ShutdownError"
166
+ assert_equal exp, Qs.redis.with{ |c| c.get('last_error') }
167
+ end
168
+
169
+ end
170
+
171
+ class ShutdownWithUnprocessedRedisItemTests < SystemTests
172
+ desc "with a redis item that gets picked up but doesn't get processed"
173
+ setup do
174
+ Assert.stub(Qs::PayloadHandler, :new){ sleep 5 }
175
+
176
+ AppDaemon.shutdown_timeout 1
177
+ AppDaemon.workers 2
178
+ @daemon = AppDaemon.new
179
+ @daemon_runner = DaemonRunner.new(@daemon)
180
+ @thread = @daemon_runner.start
181
+
182
+ AppQueue.add('slow')
183
+ AppQueue.add('slow')
184
+ AppQueue.add('basic')
185
+ @thread.join 1 # let the daemon have time to process jobs
186
+ end
187
+
188
+ should "shutdown and requeue the redis item" do
189
+ @daemon.stop
190
+ @thread.join 2 # give it time to shutdown, should be faster
191
+ assert_false @thread.alive?
192
+ # TODO - better way to read whats on a queue
193
+ serialized_payloads = Qs.redis.with{ |c| c.lrange(AppQueue.redis_key, 0, 3) }
194
+ names = serialized_payloads.map{ |sp| Qs::Job.parse(Qs.deserialize(sp)).name }
195
+ assert_equal ['basic', 'slow', 'slow'], names
196
+ end
197
+
198
+ should "shutdown and requeue the redis item" do
199
+ @daemon.halt
200
+ @thread.join 2 # give it time to shutdown, should be faster
201
+ assert_false @thread.alive?
202
+ # TODO - better way to read whats on a queue
203
+ serialized_payloads = Qs.redis.with{ |c| c.lrange(AppQueue.redis_key, 0, 3) }
204
+ names = serialized_payloads.map{ |sp| Qs::Job.parse(Qs.deserialize(sp)).name }
205
+ assert_equal ['basic', 'slow', 'slow'], names
206
+ end
207
+
208
+ end
209
+
210
+ class DaemonRunner
211
+ def initialize(daemon)
212
+ @daemon = daemon
213
+ @thread = nil
214
+ end
215
+
216
+ def start
217
+ @thread = @daemon.start
218
+ end
219
+
220
+ def stop
221
+ @daemon.halt
222
+ @thread.join if @thread
223
+ end
224
+ end
225
+
226
+ end
@@ -0,0 +1,188 @@
1
+ require 'assert'
2
+ require 'qs/cli'
3
+
4
+ require 'qs/daemon'
5
+
6
+ class Qs::CLI
7
+
8
+ class UnitTests < Assert::Context
9
+ desc "Qs::CLI"
10
+ setup do
11
+ @kernel_spy = KernelSpy.new
12
+ @file_path = Factory.file_path
13
+
14
+ @daemon = TestDaemon.new
15
+
16
+ @config_file = FakeConfigFile.new(@daemon)
17
+ Assert.stub(Qs::ConfigFile, :new).with(@file_path){ @config_file }
18
+
19
+ @cli = Qs::CLI.new(@kernel_spy)
20
+ end
21
+ subject{ @cli }
22
+
23
+ should have_cmeths :run
24
+ should have_imeths :run
25
+
26
+ end
27
+
28
+ class CommandTests < UnitTests
29
+ setup do
30
+ @process_spy = ProcessSpy.new
31
+ @process_signal_spy = ProcessSignalSpy.new
32
+ end
33
+
34
+ end
35
+
36
+ class DefaultsTests < CommandTests
37
+ desc "with no command or file path"
38
+ setup do
39
+ file_path = 'config.qs'
40
+ Assert.stub(Qs::ConfigFile, :new).with(file_path){ @config_file }
41
+ Assert.stub(Qs::Process, :new).with(@daemon, :daemonize => false) do
42
+ @process_spy
43
+ end
44
+
45
+ @cli.run
46
+ end
47
+
48
+ should "have defaulted the command and file path" do
49
+ assert_true @process_spy.run_called
50
+ end
51
+
52
+ end
53
+
54
+ class RunTests < CommandTests
55
+ desc "with the run command"
56
+ setup do
57
+ Assert.stub(Qs::Process, :new).with(@daemon, :daemonize => false) do
58
+ @process_spy
59
+ end
60
+
61
+ @cli.run(@file_path, 'run')
62
+ end
63
+
64
+ should "have built and run a non-daemonized process" do
65
+ assert_true @process_spy.run_called
66
+ end
67
+
68
+ end
69
+
70
+ class StartTests < CommandTests
71
+ desc "with the start command"
72
+ setup do
73
+ Assert.stub(Qs::Process, :new).with(@daemon, :daemonize => true) do
74
+ @process_spy
75
+ end
76
+
77
+ @cli.run(@file_path, 'start')
78
+ end
79
+
80
+ should "have built and run a daemonized process" do
81
+ assert_true @process_spy.run_called
82
+ end
83
+
84
+ end
85
+
86
+ class StopTests < CommandTests
87
+ desc "with the stop command"
88
+ setup do
89
+ Assert.stub(Qs::ProcessSignal, :new).with(@daemon, 'TERM') do
90
+ @process_signal_spy
91
+ end
92
+
93
+ @cli.run(@file_path, 'stop')
94
+ end
95
+
96
+ should "have built and sent a TERM signal" do
97
+ assert_true @process_signal_spy.send_called
98
+ end
99
+
100
+ end
101
+
102
+ class RestartTests < CommandTests
103
+ desc "with the restart command"
104
+ setup do
105
+ Assert.stub(Qs::ProcessSignal, :new).with(@daemon, 'USR2') do
106
+ @process_signal_spy
107
+ end
108
+
109
+ @cli.run(@file_path, 'restart')
110
+ end
111
+
112
+ should "have built and sent a USR2 signal" do
113
+ assert_true @process_signal_spy.send_called
114
+ end
115
+
116
+ end
117
+
118
+ class InvalidCommandTests < UnitTests
119
+ desc "with an invalid command"
120
+ setup do
121
+ @command = Factory.string
122
+ @cli.run(@file_path, @command)
123
+ end
124
+
125
+ should "output the error with the help" do
126
+ expected = "#{@command.inspect} is not a valid command"
127
+ assert_includes expected, @kernel_spy.output
128
+ assert_includes "Usage: qs", @kernel_spy.output
129
+ end
130
+
131
+ end
132
+
133
+ class KernelSpy
134
+ attr_reader :exit_status
135
+
136
+ def initialize
137
+ @output = StringIO.new
138
+ @exit_status = nil
139
+ end
140
+
141
+ def output
142
+ @output.rewind
143
+ @output.read
144
+ end
145
+
146
+ def puts(message)
147
+ @output.puts(message)
148
+ end
149
+
150
+ def exit(code)
151
+ @exit_status = code
152
+ end
153
+ end
154
+
155
+ class TestDaemon
156
+ include Qs::Daemon
157
+
158
+ name Factory.string
159
+ queue Qs::Queue.new{ name Factory.string }
160
+ end
161
+
162
+ FakeConfigFile = Struct.new(:daemon)
163
+
164
+ class ProcessSpy
165
+ attr_reader :run_called
166
+
167
+ def initialize
168
+ @run_called = false
169
+ end
170
+
171
+ def run
172
+ @run_called = true
173
+ end
174
+ end
175
+
176
+ class ProcessSignalSpy
177
+ attr_reader :send_called
178
+
179
+ def initialize
180
+ @send_called = false
181
+ end
182
+
183
+ def send
184
+ @send_called = true
185
+ end
186
+ end
187
+
188
+ end