wireframe-resque_unit 0.4.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,130 @@
1
+ ResqueUnit
2
+ ==========
3
+
4
+ ResqueUnit provides some extra assertions and a mock Resque for
5
+ testing Rails code that depends on Resque. You can install it as
6
+ either a gem or a plugin:
7
+
8
+ gem install resque_unit
9
+
10
+ and in your test.rb:
11
+
12
+ config.gem 'resque_unit'
13
+
14
+ If you'd rather install it as a plugin, you should be able to run
15
+
16
+ script/plugin install git://github.com/justinweiss/resque_unit.git
17
+
18
+ inside your Rails projects.
19
+
20
+ Examples
21
+ ========
22
+
23
+ ResqueUnit provides some extra assertions for your unit tests. For
24
+ example, if you have code that queues a resque job:
25
+
26
+ class MyJob
27
+ @queue = :low
28
+
29
+ def self.perform(x)
30
+ # do stuff
31
+ end
32
+ end
33
+
34
+ def queue_job
35
+ Resque.enqueue(MyJob, 1)
36
+ end
37
+
38
+ You can write a unit test for code that queues this job:
39
+
40
+ def test_job_queued
41
+ queue_job
42
+ assert_queued(MyJob) # assert that MyJob was queued in the :low queue
43
+ end
44
+
45
+ You can also verify that a job was queued with arguments:
46
+
47
+ def test_job_queued_with_arguments
48
+ queue_job
49
+ assert_queued(MyJob, [1])
50
+ end
51
+
52
+ And you can run all the jobs in the queue, so you can verify that they
53
+ run correctly:
54
+
55
+ def test_job_runs
56
+ queue_job
57
+ Resque.run!
58
+ assert stuff_was_done, "Job didn't run"
59
+ end
60
+
61
+ You can also access the queues directly:
62
+
63
+ def test_jobs_in_queue
64
+ queue_job
65
+ assert_equal 1, Resque.queue(:low).length
66
+ end
67
+
68
+ Finally, you can enable hooks:
69
+
70
+ Resque.enable_hooks!
71
+
72
+ class MyJobWithHooks
73
+ @queue = :hooked
74
+
75
+ def self.perform(x)
76
+ # do stuff
77
+ end
78
+
79
+ def self.after_enqueue_mark(*args)
80
+ # called when the job is enqueued
81
+ end
82
+
83
+ def self.before_perform_mark(*args)
84
+ # called just before the +perform+ method
85
+ end
86
+
87
+ def self.after_perform_mark(*args)
88
+ # called just after the +perform+ method
89
+ end
90
+
91
+ def self.failure_perform_mark(*args)
92
+ # called if the +perform+ method raised
93
+ end
94
+
95
+ end
96
+
97
+ def queue_job
98
+ Resque.enqueue(MyJobWithHooks, 1)
99
+ end
100
+
101
+ Caveats
102
+ =======
103
+
104
+ * You should make sure that you call `Resque.reset!` in your test's
105
+ setup method to clear all of the test queues.
106
+ * Hooks support is optional. Just because you probably don't want to call
107
+ them during unit tests if they play with a DB. Call `Resque.enable_hooks!`
108
+ in your tests's setup method to enable hooks. To disable hooks, call
109
+ `Resque.disable_hooks!`.
110
+
111
+ Resque-Scheduler Support
112
+ ========================
113
+
114
+ By calling `require 'resque_unit_scheduler'`, ResqueUnit will provide
115
+ mocks for [resque-scheduler's](http://github.com/bvandenbos/resque-scheduler)
116
+ `enqueue_at` and `enqueue_in` methods, along with a few extra
117
+ assertions. These are used like this:
118
+
119
+ Resque.enqueue_in(600, MediumPriorityJob) # enqueues MediumPriorityJob in 600 seconds
120
+ assert_queued_in(600, MediumPriorityJob) # will pass
121
+ assert_not_queued_in(300, MediumPriorityJob) # will also pass
122
+
123
+ Resque.enqueue_at(Time.now + 10, MediumPriorityJob) # enqueues MediumPriorityJob at 10 seconds from now
124
+ assert_queued_at(Time.now + 10, MediumPriorityJob) # will pass
125
+ assert_not_queued_at(Time.now + 1, MediumPriorityJob) # will also pass
126
+
127
+ For now, `assert_queued` and `assert_not_queued` will pass for any
128
+ scheduled job. `Resque.run!` will run all scheduled jobs as well.
129
+
130
+ Copyright (c) 2010 Justin Weiss, released under the MIT license
@@ -0,0 +1,13 @@
1
+ module ResqueUnit
2
+ end
3
+
4
+ begin
5
+ require 'yajl'
6
+ rescue LoadError
7
+ require 'json'
8
+ end
9
+
10
+ require 'resque_unit/helpers'
11
+ require 'resque_unit/resque'
12
+ require 'resque_unit/errors'
13
+ require 'resque_unit/plugin'
@@ -0,0 +1,81 @@
1
+ # These are a group of assertions you can use in your unit tests to
2
+ # verify that your code is using Resque correctly.
3
+ module ResqueUnit::Assertions
4
+
5
+ # Asserts that +klass+ has been queued into its appropriate queue at
6
+ # least once. If +args+ is nil, it only asserts that the klass has
7
+ # been queued. Otherwise, it asserts that the klass has been queued
8
+ # with the correct arguments. Pass an empty array for +args+ if you
9
+ # want to assert that klass has been queued without arguments. Pass a block
10
+ # if you want to assert something was queued within its execution.
11
+ def assert_queued(klass, args = nil, message = nil, &block)
12
+ queue_name = Resque.queue_for(klass)
13
+ assert_job_created(queue_name, klass, args, message, &block)
14
+ end
15
+ alias assert_queues assert_queued
16
+
17
+ # The opposite of +assert_queued+.
18
+ def assert_not_queued(klass = nil, args = nil, message = nil, &block)
19
+ queue_name = Resque.queue_for(klass)
20
+
21
+ queue = if block_given?
22
+ snapshot = Resque.size(queue_name)
23
+ yield
24
+ Resque.all(queue_name)[snapshot..-1]
25
+ else
26
+ Resque.all(queue_name)
27
+ end
28
+
29
+ assert_with_custom_message(!in_queue?(queue, klass, args),
30
+ message || "#{klass}#{args ? " with #{args.inspect}" : ""} should not have been queued in #{queue_name}.")
31
+ end
32
+
33
+ # Asserts no jobs were queued within the block passed.
34
+ def assert_nothing_queued(message = nil, &block)
35
+ snapshot = Resque.size
36
+ yield
37
+ present = Resque.size
38
+ assert_equal snapshot, present, message || "No jobs should have been queued"
39
+ end
40
+
41
+ # Asserts that a job was created and queued into the specified queue
42
+ def assert_job_created(queue_name, klass, args = nil, message = nil, &block)
43
+ queue = if block_given?
44
+ snapshot = Resque.size(queue_name)
45
+ yield
46
+ Resque.all(queue_name)[snapshot..-1]
47
+ else
48
+ Resque.all(queue_name)
49
+ end
50
+
51
+ assert_with_custom_message(in_queue?(queue, klass, args),
52
+ message || "#{klass}#{args ? " with #{args.inspect}" : ""} should have been queued in #{queue_name}: #{queue.inspect}.")
53
+ end
54
+
55
+ private
56
+
57
+ # In Test::Unit, +assert_block+ displays only the message on a test
58
+ # failure and +assert+ always appends a message to the end of the
59
+ # passed-in assertion message. In MiniTest, it's the other way
60
+ # around. This abstracts those differences and never appends a
61
+ # message to the one the user passed in.
62
+ def assert_with_custom_message(value, message = nil)
63
+ if defined?(MiniTest::Assertions)
64
+ assert value, message
65
+ else
66
+ assert_block message do
67
+ value
68
+ end
69
+ end
70
+ end
71
+
72
+ def in_queue?(queue, klass, args = nil)
73
+ !matching_jobs(queue, klass, args).empty?
74
+ end
75
+
76
+ def matching_jobs(queue, klass, args = nil)
77
+ normalized_args = Resque.decode(Resque.encode(args)) if args
78
+ queue.select {|e| e["class"] == klass.to_s && (!args || e["args"] == normalized_args )}
79
+ end
80
+
81
+ end
@@ -0,0 +1,17 @@
1
+ # Re-define errors in from Resque, in case the 'resque' gem was not loaded.
2
+ module Resque
3
+ # Raised whenever we need a queue but none is provided.
4
+ unless defined?(NoQueueError)
5
+ class NoQueueError < RuntimeError; end
6
+ end
7
+
8
+ # Raised when trying to create a job without a class
9
+ unless defined?(NoClassError)
10
+ class NoClassError < RuntimeError; end
11
+ end
12
+
13
+ # Raised when a worker was killed while processing a job.
14
+ unless defined?(DirtyExit)
15
+ class DirtyExit < RuntimeError; end
16
+ end
17
+ end
@@ -0,0 +1,57 @@
1
+ module Resque
2
+ module Helpers
3
+ # Given a Ruby object, returns a string suitable for storage in a
4
+ # queue.
5
+ def encode(object)
6
+ if defined? Yajl
7
+ Yajl::Encoder.encode(object)
8
+ else
9
+ object.to_json
10
+ end
11
+ end
12
+
13
+ # Given a string, returns a Ruby object.
14
+ def decode(object)
15
+ return unless object
16
+
17
+ if defined? Yajl
18
+ begin
19
+ Yajl::Parser.parse(object, :check_utf8 => false)
20
+ rescue Yajl::ParseError
21
+ end
22
+ else
23
+ begin
24
+ JSON.parse(object)
25
+ rescue JSON::ParserError
26
+ end
27
+ end
28
+ end
29
+
30
+ # Given a word with dashes, returns a camel cased version of it.
31
+ #
32
+ # classify('job-name') # => 'JobName'
33
+ def classify(dashed_word)
34
+ dashed_word.split('-').each { |part| part[0] = part[0].chr.upcase }.join
35
+ end
36
+
37
+ # Given a camel cased word, returns the constant it represents
38
+ #
39
+ # constantize('JobName') # => JobName
40
+ def constantize(camel_cased_word)
41
+ camel_cased_word = camel_cased_word.to_s
42
+
43
+ if camel_cased_word.include?('-')
44
+ camel_cased_word = classify(camel_cased_word)
45
+ end
46
+
47
+ names = camel_cased_word.split('::')
48
+ names.shift if names.empty? || names.first.empty?
49
+
50
+ constant = Object
51
+ names.each do |name|
52
+ constant = constant.const_get(name) || constant.const_missing(name)
53
+ end
54
+ constant
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ # A copy of the original Resque:Plugin class from the "resque" gem
2
+ # No need to redefine this class if the resque gem was already loaded
3
+ unless defined?(Resque::Plugin)
4
+
5
+ module Resque
6
+ module Plugin
7
+ extend self
8
+
9
+ LintError = Class.new(RuntimeError)
10
+
11
+ # Ensure that your plugin conforms to good hook naming conventions.
12
+ #
13
+ # Resque::Plugin.lint(MyResquePlugin)
14
+ def lint(plugin)
15
+ hooks = before_hooks(plugin) + around_hooks(plugin) + after_hooks(plugin)
16
+
17
+ hooks.each do |hook|
18
+ if hook =~ /perform$/
19
+ raise LintError, "#{plugin}.#{hook} is not namespaced"
20
+ end
21
+ end
22
+
23
+ failure_hooks(plugin).each do |hook|
24
+ if hook =~ /failure$/
25
+ raise LintError, "#{plugin}.#{hook} is not namespaced"
26
+ end
27
+ end
28
+ end
29
+
30
+ # Given an object, returns a list `before_perform` hook names.
31
+ def before_hooks(job)
32
+ job.methods.grep(/^before_perform/).sort
33
+ end
34
+
35
+ # Given an object, returns a list `around_perform` hook names.
36
+ def around_hooks(job)
37
+ job.methods.grep(/^around_perform/).sort
38
+ end
39
+
40
+ # Given an object, returns a list `after_perform` hook names.
41
+ def after_hooks(job)
42
+ job.methods.grep(/^after_perform/).sort
43
+ end
44
+
45
+ # Given an object, returns a list `on_failure` hook names.
46
+ def failure_hooks(job)
47
+ job.methods.grep(/^on_failure/).sort
48
+ end
49
+
50
+ # Given an object, returns a list `after_enqueue` hook names.
51
+ def after_enqueue_hooks(job)
52
+ job.methods.grep(/^after_enqueue/).sort
53
+ end
54
+
55
+ # Given an object, returns a list `before_enqueue` hook names.
56
+ def before_enqueue_hooks(job)
57
+ job.methods.grep(/^before_enqueue/).sort
58
+ end
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ unless defined?(Resque::Job::DontPerform)
65
+ module Resque
66
+ class Job
67
+ DontPerform = Class.new(StandardError)
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,224 @@
1
+ # The fake Resque class. This needs to be loaded after the real Resque
2
+ # for the assertions in +ResqueUnit::Assertions+ to work.
3
+ module Resque
4
+ include Helpers
5
+ extend self
6
+
7
+ # Resets all the queues to the empty state. This should be called in
8
+ # your test's +setup+ method until I can figure out a way for it to
9
+ # automatically be called.
10
+ #
11
+ # If <tt>queue_name</tt> is given, then resets only that queue.
12
+ def reset!(queue_name = nil)
13
+ if @queue && queue_name
14
+ @queue[queue_name] = []
15
+ else
16
+ @queue = Hash.new { |h, k| h[k] = [] }
17
+ end
18
+ end
19
+
20
+ # Returns a hash of all the queue names and jobs that have been queued. The
21
+ # format is <tt>{queue_name => [job, ..]}</tt>.
22
+ def self.queues
23
+ @queue || reset!
24
+ end
25
+
26
+ # Returns an array of all the jobs that have been queued. Each
27
+ # element is of the form +{"class" => klass, "args" => args}+ where
28
+ # +klass+ is the job's class and +args+ is an array of the arguments
29
+ # passed to the job.
30
+ def queue(queue_name)
31
+ queues[queue_name]
32
+ end
33
+
34
+ # Return an array of all jobs' payloads for queue
35
+ # Elements are decoded
36
+ def all(queue_name)
37
+ result = list_range(queue_name, 0, size(queue_name))
38
+ result.is_a?(Array) ? result : [ result ]
39
+ end
40
+
41
+ # Returns an array of jobs' payloads for queue.
42
+ #
43
+ # start and count should be integer and can be used for pagination.
44
+ # start is the item to begin, count is how many items to return.
45
+ #
46
+ # To get the 3rd page of a 30 item, paginatied list one would use:
47
+ # Resque.peek('my_list', 59, 30)
48
+ def peek(queue_name, start = 0, count = 1)
49
+ list_range(queue_name, start, count)
50
+ end
51
+
52
+ # Gets a range of jobs' payloads from queue.
53
+ # Returns single element if count equal 1
54
+ # Elements are decoded
55
+ def list_range(key, start = 0, count = 1)
56
+ data = if count == 1
57
+ decode(queues[key][start])
58
+ else
59
+ (queues[key][start...start + count] || []).map { |entry| decode(entry) }
60
+ end
61
+ end
62
+
63
+ # Yes, all Resque hooks!
64
+ def enable_hooks!
65
+ @hooks_enabled = true
66
+ end
67
+
68
+ def disable_hooks!
69
+ @hooks_enabled = nil
70
+ end
71
+
72
+ # Executes all jobs in all queues in an undefined order.
73
+ def run!
74
+ payloads = []
75
+ @queue.each do |queue_name, queue|
76
+ payloads.concat queue.slice!(0, queue.size)
77
+ end
78
+ exec_payloads payloads.shuffle
79
+ end
80
+
81
+ def run_for!(queue_name, limit=false)
82
+ queue = @queue[queue_name]
83
+ exec_payloads queue.slice!(0, ( limit ? limit : queue.size) ).shuffle
84
+ end
85
+
86
+ def exec_payloads(raw_payloads)
87
+ raw_payloads.each do |raw_payload|
88
+ job_payload = decode(raw_payload)
89
+ @hooks_enabled ? perform_with_hooks(job_payload) : perform_without_hooks(job_payload)
90
+ end
91
+ end
92
+ private :exec_payloads
93
+
94
+ # 1. Execute all jobs in all queues in an undefined order,
95
+ # 2. Check if new jobs were announced, and execute them.
96
+ # 3. Repeat 3
97
+ def full_run!
98
+ run! until empty_queues?
99
+ end
100
+
101
+ # Returns the size of the given queue
102
+ def size(queue_name = nil)
103
+ if queue_name
104
+ queues[queue_name].length
105
+ else
106
+ queues.values.flatten.length
107
+ end
108
+ end
109
+
110
+ # :nodoc:
111
+ def enqueue(klass, *args)
112
+ enqueue_to( queue_for(klass), klass, *args)
113
+ end
114
+
115
+ # :nodoc:
116
+ def enqueue_to( queue_name, klass, *args )
117
+ # Behaves like Resque, raise if no queue was specifed
118
+ raise NoQueueError.new("Jobs must be placed onto a queue.") unless queue_name
119
+ enqueue_unit(queue_name, {"class" => klass.name, "args" => args })
120
+ end
121
+
122
+ # :nodoc:
123
+ def queue_for(klass)
124
+ klass.instance_variable_get(:@queue) || (klass.respond_to?(:queue) && klass.queue)
125
+ end
126
+ alias :queue_from_class :queue_for
127
+
128
+ # :nodoc:
129
+ def empty_queues?
130
+ queues.all? do |k, v|
131
+ v.empty?
132
+ end
133
+ end
134
+
135
+ def enqueue_unit(queue_name, hash)
136
+ klass = constantize(hash["class"])
137
+ if @hooks_enabled
138
+ before_hooks = Plugin.before_enqueue_hooks(klass).map do |hook|
139
+ klass.send(hook, *hash["args"])
140
+ end
141
+ return nil if before_hooks.any? { |result| result == false }
142
+ end
143
+ queue(queue_name) << encode(hash)
144
+ if @hooks_enabled
145
+ Plugin.after_enqueue_hooks(klass).each do |hook|
146
+ klass.send(hook, *hash["args"])
147
+ end
148
+ end
149
+ queue(queue_name).size
150
+ end
151
+
152
+ # Call perform on the job class
153
+ def perform_without_hooks(job_payload)
154
+ constantize(job_payload["class"]).perform(*job_payload["args"])
155
+ end
156
+
157
+ # Call perform on the job class, and adds support for Resque hooks.
158
+ def perform_with_hooks(job_payload)
159
+ job_class = constantize(job_payload["class"])
160
+ before_hooks = Resque::Plugin.before_hooks(job_class)
161
+ around_hooks = Resque::Plugin.around_hooks(job_class)
162
+ after_hooks = Resque::Plugin.after_hooks(job_class)
163
+ failure_hooks = Resque::Plugin.failure_hooks(job_class)
164
+
165
+ begin
166
+ # Execute before_perform hook. Abort the job gracefully if
167
+ # Resque::DontPerform is raised.
168
+ begin
169
+ before_hooks.each do |hook|
170
+ job_class.send(hook, *job_payload["args"])
171
+ end
172
+ rescue Resque::Job::DontPerform
173
+ return false
174
+ end
175
+
176
+ # Execute the job. Do it in an around_perform hook if available.
177
+ if around_hooks.empty?
178
+ perform_without_hooks(job_payload)
179
+ job_was_performed = true
180
+ else
181
+ # We want to nest all around_perform plugins, with the last one
182
+ # finally calling perform
183
+ stack = around_hooks.reverse.inject(nil) do |last_hook, hook|
184
+ if last_hook
185
+ lambda do
186
+ job_class.send(hook, *job_payload["args"]) { last_hook.call }
187
+ end
188
+ else
189
+ lambda do
190
+ job_class.send(hook, *job_payload["args"]) do
191
+ result = perform_without_hooks(job_payload)
192
+ job_was_performed = true
193
+ result
194
+ end
195
+ end
196
+ end
197
+ end
198
+ stack.call
199
+ end
200
+
201
+ # Execute after_perform hook
202
+ after_hooks.each do |hook|
203
+ job_class.send(hook, *job_payload["args"])
204
+ end
205
+
206
+ # Return true if the job was performed
207
+ return job_was_performed
208
+
209
+ # If an exception occurs during the job execution, look for an
210
+ # on_failure hook then re-raise.
211
+ rescue Object => e
212
+ failure_hooks.each { |hook| job_class.send(hook, e, *job_payload["args"]) }
213
+ raise e
214
+ end
215
+ end
216
+
217
+ class Job
218
+ extend Helpers
219
+ def self.create(queue, klass_name, *args)
220
+ Resque.enqueue_unit(queue, {"class" => constantize(klass_name), "args" => args})
221
+ end
222
+ end
223
+
224
+ end