qs 0.0.1 → 0.1.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.
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