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.
- 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
|