qs 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +6 -1
- data/LICENSE.txt +1 -1
- data/bench/config.qs +46 -0
- data/bench/queue.rb +8 -0
- data/bench/report.rb +114 -0
- data/bench/report.txt +11 -0
- data/bin/qs +7 -0
- data/lib/qs/cli.rb +124 -0
- data/lib/qs/client.rb +121 -0
- data/lib/qs/config_file.rb +79 -0
- data/lib/qs/daemon.rb +350 -0
- data/lib/qs/daemon_data.rb +46 -0
- data/lib/qs/error_handler.rb +58 -0
- data/lib/qs/job.rb +70 -0
- data/lib/qs/job_handler.rb +90 -0
- data/lib/qs/logger.rb +23 -0
- data/lib/qs/payload_handler.rb +136 -0
- data/lib/qs/pid_file.rb +42 -0
- data/lib/qs/process.rb +136 -0
- data/lib/qs/process_signal.rb +20 -0
- data/lib/qs/qs_runner.rb +49 -0
- data/lib/qs/queue.rb +69 -0
- data/lib/qs/redis_item.rb +33 -0
- data/lib/qs/route.rb +52 -0
- data/lib/qs/runner.rb +26 -0
- data/lib/qs/test_helpers.rb +17 -0
- data/lib/qs/test_runner.rb +43 -0
- data/lib/qs/version.rb +1 -1
- data/lib/qs.rb +92 -2
- data/qs.gemspec +7 -2
- data/test/helper.rb +8 -1
- data/test/support/app_daemon.rb +74 -0
- data/test/support/config.qs +7 -0
- data/test/support/config_files/empty.qs +0 -0
- data/test/support/config_files/invalid.qs +1 -0
- data/test/support/config_files/valid.qs +7 -0
- data/test/support/config_invalid_run.qs +3 -0
- data/test/support/config_no_run.qs +0 -0
- data/test/support/factory.rb +14 -0
- data/test/support/pid_file_spy.rb +19 -0
- data/test/support/runner_spy.rb +17 -0
- data/test/system/daemon_tests.rb +226 -0
- data/test/unit/cli_tests.rb +188 -0
- data/test/unit/client_tests.rb +269 -0
- data/test/unit/config_file_tests.rb +59 -0
- data/test/unit/daemon_data_tests.rb +96 -0
- data/test/unit/daemon_tests.rb +702 -0
- data/test/unit/error_handler_tests.rb +163 -0
- data/test/unit/job_handler_tests.rb +253 -0
- data/test/unit/job_tests.rb +132 -0
- data/test/unit/logger_tests.rb +38 -0
- data/test/unit/payload_handler_tests.rb +276 -0
- data/test/unit/pid_file_tests.rb +70 -0
- data/test/unit/process_signal_tests.rb +61 -0
- data/test/unit/process_tests.rb +371 -0
- data/test/unit/qs_runner_tests.rb +166 -0
- data/test/unit/qs_tests.rb +217 -0
- data/test/unit/queue_tests.rb +132 -0
- data/test/unit/redis_item_tests.rb +49 -0
- data/test/unit/route_tests.rb +81 -0
- data/test/unit/runner_tests.rb +63 -0
- data/test/unit/test_helper_tests.rb +61 -0
- data/test/unit/test_runner_tests.rb +128 -0
- metadata +180 -15
data/lib/qs.rb
CHANGED
@@ -1,5 +1,95 @@
|
|
1
|
-
require
|
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
|
-
|
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.
|
21
|
-
|
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
|
-
#
|
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
|
File without changes
|
@@ -0,0 +1 @@
|
|
1
|
+
run 'test'
|
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
|