wireframe-resque_unit 0.4.1.1

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