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
@@ -0,0 +1,276 @@
1
+ require 'assert'
2
+ require 'qs/payload_handler'
3
+
4
+ require 'qs/daemon_data'
5
+ require 'qs/job'
6
+ require 'qs/redis_item'
7
+
8
+ class Qs::PayloadHandler
9
+
10
+ class UnitTests < Assert::Context
11
+ desc "Qs::PayloadHandler"
12
+ setup do
13
+ Qs.init
14
+ @handler_class = Qs::PayloadHandler
15
+ end
16
+ subject{ @handler_class }
17
+
18
+ end
19
+
20
+ class InitTests < UnitTests
21
+ desc "when init"
22
+ setup do
23
+ @route_spy = RouteSpy.new
24
+ @daemon_data = Qs::DaemonData.new({
25
+ :logger => Qs::NullLogger.new,
26
+ :routes => [@route_spy]
27
+ })
28
+ @job = Qs::Job.new(@route_spy.name, Factory.string => Factory.string)
29
+ serialized_payload = Qs.serialize(@job.to_payload)
30
+ @redis_item = Qs::RedisItem.new(Factory.string, serialized_payload)
31
+
32
+ Assert.stub(Qs::Logger, :new){ |*args| QsLoggerSpy.new(*args) }
33
+
34
+ @payload_handler = @handler_class.new(@daemon_data, @redis_item)
35
+ end
36
+ subject{ @payload_handler }
37
+
38
+ should have_readers :daemon_data, :redis_item
39
+ should have_readers :logger
40
+ should have_imeths :run
41
+
42
+ should "know its daemon data, redis item and logger" do
43
+ assert_equal @daemon_data, subject.daemon_data
44
+ assert_equal @redis_item, subject.redis_item
45
+ assert_equal @daemon_data.logger, subject.logger.passed_logger
46
+ assert_equal @daemon_data.verbose_logging, subject.logger.verbose_logging
47
+ end
48
+
49
+ end
50
+
51
+ class RunTests < InitTests
52
+ desc "and run"
53
+ setup do
54
+ @payload_handler.run
55
+ end
56
+
57
+ should "run a route for the redis item" do
58
+ assert_true @route_spy.run_called
59
+ assert_equal @job, @route_spy.job_passed_to_run
60
+ assert_equal @daemon_data, @route_spy.daemon_data_passed_to_run
61
+ end
62
+
63
+ should "build up its redis item as it processes it" do
64
+ assert_equal @job, @redis_item.job
65
+ assert_equal @route_spy.handler_class, @redis_item.handler_class
66
+ assert_nil @redis_item.exception
67
+ assert_instance_of Float, @redis_item.time_taken
68
+ end
69
+
70
+ should "log its processing of the redis item" do
71
+ logger_spy = subject.logger
72
+ expected = "[Qs] ===== Running job ====="
73
+ assert_includes expected, logger_spy.verbose.info_logged
74
+ expected = "[Qs] Job: #{@redis_item.job.name.inspect}"
75
+ assert_includes expected, logger_spy.verbose.info_logged
76
+ expected = "[Qs] Params: #{@redis_item.job.params.inspect}"
77
+ assert_includes expected, logger_spy.verbose.info_logged
78
+ expected = "[Qs] Handler: #{@redis_item.handler_class}"
79
+ assert_includes expected, logger_spy.verbose.info_logged
80
+ expected = "[Qs] ===== Completed in #{@redis_item.time_taken}ms ====="
81
+ assert_includes expected, logger_spy.verbose.info_logged
82
+ assert_empty logger_spy.verbose.error_logged
83
+
84
+ expected = SummaryLine.new({
85
+ 'time' => @redis_item.time_taken,
86
+ 'handler' => @redis_item.handler_class,
87
+ 'job' => @redis_item.job.name,
88
+ 'params' => @redis_item.job.params
89
+ })
90
+ assert_equal 1, logger_spy.summary.info_logged.size
91
+ assert_equal "[Qs] #{expected}", logger_spy.summary.info_logged.first
92
+ assert_empty logger_spy.summary.error_logged
93
+ end
94
+
95
+ end
96
+
97
+ class RunWithExceptionSetupTests < InitTests
98
+ setup do
99
+ @route_exception = Factory.exception
100
+ Assert.stub(@route_spy, :run){ raise @route_exception }
101
+ Assert.stub(Qs::ErrorHandler, :new) do |*args|
102
+ @error_handler_spy = ErrorHandlerSpy.new(*args)
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ class RunWithExceptionTests < RunWithExceptionSetupTests
109
+ desc "and run with an exception"
110
+ setup do
111
+ @payload_handler.run
112
+ end
113
+
114
+ should "run an error handler" do
115
+ assert_equal @route_exception, @error_handler_spy.passed_exception
116
+ exp = {
117
+ :daemon_data => @daemon_data,
118
+ :queue_redis_key => @redis_item.queue_redis_key,
119
+ :serialized_payload => @redis_item.serialized_payload,
120
+ :job => @redis_item.job,
121
+ :handler_class => @redis_item.handler_class
122
+ }
123
+ assert_equal exp, @error_handler_spy.context_hash
124
+ assert_true @error_handler_spy.run_called
125
+ end
126
+
127
+ should "store the exception on the redis item" do
128
+ assert_equal @error_handler_spy.exception, @redis_item.exception
129
+ end
130
+
131
+ should "log its processing of the redis item" do
132
+ logger_spy = subject.logger
133
+ exception = @redis_item.exception
134
+ backtrace = exception.backtrace.join("\n")
135
+ exp = "[Qs] #{exception.class}: #{exception.message}\n#{backtrace}"
136
+ assert_equal exp, logger_spy.verbose.error_logged.first
137
+ end
138
+
139
+ end
140
+
141
+ class RunWithShutdownErrorTests < RunWithExceptionSetupTests
142
+ desc "and run with a dat worker pool shutdown error"
143
+ setup do
144
+ @shutdown_error = DatWorkerPool::ShutdownError.new(Factory.text)
145
+ Assert.stub(@route_spy, :run){ raise @shutdown_error }
146
+ end
147
+
148
+ should "run an error handler if the redis item was started" do
149
+ Assert.stub(@redis_item, :started){ true }
150
+ assert_raises{ @payload_handler.run }
151
+
152
+ passed_exception = @error_handler_spy.passed_exception
153
+ assert_instance_of Qs::ShutdownError, passed_exception
154
+ assert_equal @shutdown_error.message, passed_exception.message
155
+ assert_equal @shutdown_error.backtrace, passed_exception.backtrace
156
+ assert_true @error_handler_spy.run_called
157
+ end
158
+
159
+ should "not run an error handler if the redis item was started" do
160
+ Assert.stub(@redis_item, :started){ false }
161
+ assert_raises{ @payload_handler.run }
162
+
163
+ assert_nil @error_handler_spy
164
+ end
165
+
166
+ should "raise the shutdown error" do
167
+ assert_raises(@shutdown_error.class){ @payload_handler.run }
168
+ end
169
+
170
+ end
171
+
172
+ class RunWithExceptionWhileDebuggingTests < RunWithExceptionSetupTests
173
+ desc "and run with an exception"
174
+ setup do
175
+ ENV['QS_DEBUG'] = '1'
176
+ end
177
+ teardown do
178
+ ENV.delete('QS_DEBUG')
179
+ end
180
+
181
+ should "raise the exception" do
182
+ assert_raises(@route_exception.class){ @payload_handler.run }
183
+ end
184
+
185
+ end
186
+
187
+ class SummaryLineTests < UnitTests
188
+ desc "SummaryLine"
189
+ setup do
190
+ @attrs = {
191
+ 'time' => Factory.string,
192
+ 'handler' => Factory.string,
193
+ 'job' => Factory.string,
194
+ 'params' => Factory.string,
195
+ 'error' => Factory.string
196
+ }
197
+ @summary_line = SummaryLine.new(@attrs)
198
+ end
199
+ subject{ @summary_line }
200
+
201
+ should "build a string of all the attributes ordered with their values" do
202
+ expected = "time=#{@attrs['time'].inspect} " \
203
+ "handler=#{@attrs['handler'].inspect} " \
204
+ "job=#{@attrs['job'].inspect} " \
205
+ "params=#{@attrs['params'].inspect} " \
206
+ "error=#{@attrs['error'].inspect}"
207
+ assert_equal expected, subject
208
+ end
209
+
210
+ end
211
+
212
+ class RouteSpy
213
+ attr_reader :name
214
+ attr_reader :job_passed_to_run, :daemon_data_passed_to_run
215
+ attr_reader :run_called
216
+
217
+ def initialize
218
+ @name = Factory.string
219
+ @job_passed_to_run = nil
220
+ @daemon_data_passed_to_run = nil
221
+ @run_called = false
222
+ end
223
+
224
+ def handler_class
225
+ TestHandler
226
+ end
227
+
228
+ def run(job, daemon_data)
229
+ @job_passed_to_run = job
230
+ @daemon_data_passed_to_run = daemon_data
231
+ @run_called = true
232
+ end
233
+
234
+ TestHandler = Class.new
235
+ end
236
+
237
+ class ErrorHandlerSpy
238
+ attr_reader :passed_exception, :context_hash, :exception, :run_called
239
+
240
+ def initialize(exception, context_hash)
241
+ @passed_exception = exception
242
+ @context_hash = context_hash
243
+ @exception = Factory.exception
244
+ @run_called = false
245
+ end
246
+
247
+ def run
248
+ @run_called = true
249
+ end
250
+ end
251
+
252
+ class QsLoggerSpy
253
+ attr_reader :passed_logger, :verbose_logging
254
+ attr_reader :summary, :verbose
255
+
256
+ def initialize(passed_logger, verbose_logging)
257
+ @passed_logger = passed_logger
258
+ @verbose_logging = verbose_logging
259
+ @summary = LoggerSpy.new
260
+ @verbose = LoggerSpy.new
261
+ end
262
+
263
+ class LoggerSpy
264
+ attr_reader :info_logged, :error_logged
265
+
266
+ def initialize
267
+ @info_logged = []
268
+ @error_logged = []
269
+ end
270
+
271
+ def info(message); @info_logged << message; end
272
+ def error(message); @error_logged << message; end
273
+ end
274
+ end
275
+
276
+ end
@@ -0,0 +1,70 @@
1
+ require 'assert'
2
+ require 'qs/pid_file'
3
+
4
+ class Qs::PIDFile
5
+
6
+ class UnitTests < Assert::Context
7
+ desc "Qs::PIDFile"
8
+ setup do
9
+ @path = ROOT_PATH.join('tmp/pid_file_tests.pid').to_s
10
+ @pid_file = Qs::PIDFile.new(@path)
11
+ end
12
+ teardown do
13
+ FileUtils.rm_rf(@path)
14
+ end
15
+ subject{ @pid_file }
16
+
17
+ should have_readers :path
18
+ should have_imeths :pid, :write, :remove, :to_s
19
+
20
+ should "know its path" do
21
+ assert_equal @path, subject.path
22
+ end
23
+
24
+ should "default its path" do
25
+ pid_file = Qs::PIDFile.new(nil)
26
+ assert_equal '/dev/null', pid_file.path
27
+ end
28
+
29
+ should "know its string format" do
30
+ assert_equal @path, subject.to_s
31
+ end
32
+
33
+ should "read a PID from its file" do
34
+ pid = Factory.integer
35
+ File.open(@path, 'w'){ |f| f.puts pid }
36
+ assert_equal pid, subject.pid
37
+ end
38
+
39
+ should "raise an invalid error when it can't read from its file" do
40
+ FileUtils.rm_rf(@path)
41
+ assert_raises(InvalidError){ subject.pid }
42
+ end
43
+
44
+ should "raise an invalid error when the file doesn't have a PID in it" do
45
+ File.open(@path, 'w'){ |f| f.puts '' }
46
+ assert_raises(InvalidError){ subject.pid }
47
+ end
48
+
49
+ should "write the process PID to its file" do
50
+ assert_false File.exists?(@path)
51
+ subject.write
52
+ assert_true File.exists?(@path)
53
+ assert_equal "#{::Process.pid}\n", File.read(@path)
54
+ end
55
+
56
+ should "raise an invalid error when it can't write its file" do
57
+ Assert.stub(File, :open){ raise "can't open file" }
58
+ assert_raises(InvalidError){ subject.write }
59
+ end
60
+
61
+ should "remove its file" do
62
+ FileUtils.touch(@path)
63
+ assert_true File.exists?(@path)
64
+ subject.remove
65
+ assert_false File.exists?(@path)
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,61 @@
1
+ require 'assert'
2
+ require 'qs/process_signal'
3
+
4
+ require 'qs/daemon'
5
+ require 'test/support/pid_file_spy'
6
+
7
+ class Qs::ProcessSignal
8
+
9
+ class UnitTests < Assert::Context
10
+ desc "Qs::ProcessSignal"
11
+ setup do
12
+ @daemon = TestDaemon.new
13
+ @signal = Factory.string
14
+
15
+ @pid_file_spy = PIDFileSpy.new(Factory.integer)
16
+ Assert.stub(Qs::PIDFile, :new).with(@daemon.pid_file) do
17
+ @pid_file_spy
18
+ end
19
+
20
+ @process_signal = Qs::ProcessSignal.new(@daemon, @signal)
21
+ end
22
+ subject{ @process_signal }
23
+
24
+ should have_readers :signal, :pid
25
+ should have_imeths :send
26
+
27
+ should "know its signal and pid" do
28
+ assert_equal @signal, subject.signal
29
+ assert_equal @pid_file_spy.pid, subject.pid
30
+ end
31
+
32
+ end
33
+
34
+ class SendTests < UnitTests
35
+ desc "when sent"
36
+ setup do
37
+ @kill_called = false
38
+ Assert.stub(::Process, :kill).with(@signal, @pid_file_spy.pid) do
39
+ @kill_called = true
40
+ end
41
+
42
+ @process_signal.send
43
+ end
44
+
45
+ should "have used process kill to send the signal to the PID" do
46
+ assert_true @kill_called
47
+ end
48
+
49
+ end
50
+
51
+ class TestDaemon
52
+ include Qs::Daemon
53
+
54
+ name Factory.string
55
+ pid_file Factory.file_path
56
+
57
+ queue Qs::Queue.new{ name Factory.string }
58
+
59
+ end
60
+
61
+ end