angael 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,11 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ doc
7
+ tmp
8
+ rerun.txt
9
+ Gemfile.lock
10
+ .bundle
11
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in angael.gemspec
4
+ gemspec
@@ -0,0 +1,54 @@
1
+ # Angael
2
+
3
+ Angael is a lightweight library for running repetitive background processes.
4
+
5
+ ## Documentation
6
+
7
+ Angael's model of running background processes involves two classes: a worker and a manager.
8
+
9
+ Theoretically you do not need to modify Angael's built in manager (Angael::Manager).
10
+ It already has the basic logic for starting and stopping the workers.
11
+
12
+ Since workers are very different depending on the task at hand, Angael doesn't
13
+ include a Worker class. Instead there is just a module (Angael::Worker) which
14
+ you can include into your own class.
15
+ When you include Angael::Worker your class is expected to define a method called
16
+ `work`. This method will be called repeatedly until the the worker is stopped.
17
+ Also note, Angael::Worker defines an initialize method. If you require your own
18
+ initializer, take care that you either call super or you set the appropriate
19
+ instance variables.
20
+
21
+ ## Example
22
+
23
+
24
+ ```
25
+ class MailMan
26
+ include Angael::Worker
27
+ def work
28
+ deliver_letters
29
+ end
30
+
31
+ def deliver_letters
32
+ # Your cool code
33
+ end
34
+ end
35
+
36
+ mail_man_manager = Angael::Manager.new(MailMan)
37
+
38
+ # This will loop forever until it receives a SIGINT or SIGTERM.
39
+ mail_man_manager.start!
40
+
41
+ ```
42
+
43
+ ## Setup
44
+
45
+ Gemfile
46
+
47
+ gem 'angael', :git => 'git://github.com/thoughtless/angael.git'
48
+
49
+ `bundle install`
50
+
51
+
52
+ ## Contribute
53
+
54
+ See [http://github.com/thoughtless/angael](http://github.com/thoughtless/angael)
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "angael/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "angael"
7
+ s.version = Angael::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Paul Cortens"]
10
+ s.email = ["paul@thoughtless.ca"]
11
+ s.homepage = "http://github.com/thoughtless/angael"
12
+ s.summary = %q{Lightweight library for running repetitive background processes.}
13
+ #s.description = %q{TODO: Write a gem description}
14
+
15
+ s.rubyforge_project = "angael"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency('rspec', '2.6.0')
23
+ s.add_development_dependency('rspec-process-mocks')
24
+ end
@@ -0,0 +1,5 @@
1
+ module Angael; end
2
+
3
+ require 'angael/version'
4
+ require 'angael/worker'
5
+ require 'angael/manager'
@@ -0,0 +1,78 @@
1
+ module Angael
2
+ # A Manager has a number of of worker objects. Starting the Manager simply
3
+ # calls #start! on each worker, then it goes into an infinite loop, waiting
4
+ # for SIGINT or SIGTERM. When either of those is received, the manager will
5
+ # call #stop! on each Worker.
6
+ class Manager
7
+ attr_reader :workers
8
+
9
+ # Creates a new manager.
10
+ #
11
+ # @worker_class [Class] The class to use for the workers. Must respond
12
+ # to #new, #start!, and #stop!
13
+ # @worker_count [Integer] The number of workers to manager. Default is 1.
14
+ # @worker_args [Array] An array of arguments that will be passed to
15
+ # worker_class.new. The arguments will be splatted
16
+ # (i.e. `worker_class.new(*worker_args)`). Default is an empty Array, i.e.
17
+ # no arguments.
18
+ # @opts [Hash] Additional options:
19
+ # :logger => A logger object, which should follow the Logger class in the
20
+ # standard library. Default nil, as in no logging.
21
+ # :log_level => The log level, as defined by the Logger class in the
22
+ # standard library. One of:
23
+ # Logger::FATAL
24
+ # Logger::ERROR
25
+ # Logger::WARN
26
+ # Logger::INFO # Default
27
+ # Logger::DEBUG
28
+ def initialize(worker_class, worker_count=1, worker_args=[], opts={})
29
+ @workers = []
30
+ worker_count.times { workers << worker_class.new(*worker_args) }
31
+ @logger = opts[:logger]
32
+ if @logger
33
+ @log_level = opts[:log_level] || begin
34
+ require 'logger' # Only require it if it is absolutely neccessary.
35
+ Logger::INFO
36
+ end
37
+ end
38
+ end
39
+
40
+
41
+ # Starts workers by calling Worker#start! Loops forever waiting for SIGINT
42
+ # or SIGTERM, at which time it calls Worker#stop! on each worker.
43
+ def start!
44
+ workers.each { |w| w.start! }
45
+
46
+ loop do
47
+ trap("INT") do
48
+ stop!
49
+ end
50
+ trap("TERM") do
51
+ stop!
52
+ end
53
+ sleep 1
54
+ end
55
+ end
56
+
57
+
58
+
59
+ #########
60
+ private #
61
+ #########
62
+
63
+ def stop!
64
+ log("SIGINT Received")
65
+ workers.each { |w|
66
+ log("Calling #stop! for worker #{w.inspect}")
67
+ w.stop!
68
+ log("Finished call to #stop! for worker #{w.inspect}")
69
+ }
70
+ exit 0
71
+ end
72
+
73
+ def log(msg)
74
+ @logger.add(@log_level, "#{Time.now.utc} - #{self.class} (pid #{$$}): #{msg}") if @logger
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module Angael
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,191 @@
1
+ require 'timeout'
2
+ module Angael
3
+ # Usage
4
+ # include Angael::Worker
5
+ # def work
6
+ # # Do something interesting, without raising an exception.
7
+ # end
8
+ # You can also add some optional behavior by defining the following methods:
9
+ # #after_fork - This is run once, immediately after the child process is forked
10
+ # #fork_child - This actually does the forking. You can overwrite this method
11
+ # to do wrap the child process in a block. This is useful for
12
+ # exception handling. Be sure to actually fork or you may break
13
+ # something important.
14
+ module Worker
15
+ class ChildProcessNotStoppedError < StandardError; end
16
+
17
+ attr_reader :pid
18
+
19
+ # Options:
20
+ # :batch_timeout - After this number of seconds, other workers will be able
21
+ # to work on the jobs reserved by #process_jobs.
22
+ # :batch_timeout_buffer - This is the number of seconds between when the
23
+ # worker stops processing jobs and when other workers
24
+ # can start processing the jobs that this worker had
25
+ # resered. This should be set to the maximum length
26
+ # of time a single job should take, plus the maximum
27
+ # expected discrepancy between the system clocks on
28
+ # all the worker servers.
29
+ # :logger => A logger object, which should follow the Logger class in the
30
+ # standard library. Default nil, as in no logging.
31
+ # :log_level => The log level, as defined by the Logger class in the
32
+ # standard library. One of:
33
+ # Logger::FATAL
34
+ # Logger::ERROR
35
+ # Logger::WARN
36
+ # Logger::INFO # Default
37
+ # Logger::DEBUG
38
+ def initialize(attrs={})
39
+ @timeout = attrs[:timeout] || 60 # Seconds
40
+ @batch_size = attrs[:batch_size] || 1
41
+ @batch_timeout = attrs[:batch_timeout] || @batch_size * 5 # Seconds
42
+ @batch_timeout_buffer = attrs[:batch_timeout_buffer] || 5 # Seconds
43
+ @logger = attrs[:logger]
44
+ if @logger
45
+ @log_level = attrs[:log_level] || begin
46
+ require 'logger' # Only require it if it is absolutely neccessary.
47
+ Logger::INFO
48
+ end
49
+ end
50
+ end
51
+
52
+
53
+ # Loops forever, taking jobs off the queue. SIGINT will stop it after
54
+ # allowing any jobs already taken from the queue to be processed.
55
+ def start!
56
+ trap("CHLD") do
57
+ log("trapped SIGCHLD. Child PID #{pid}.")
58
+
59
+ # @stopping is set by #stop!. If it is true, then the child process was
60
+ # expected to die. If it is false/nil, then this is unexpected.
61
+ log("Child process died unexpectedly") unless @stopping
62
+ # Reap the child process so that #started? will return false. But we can't
63
+ # block because this may be called for a Worker when a different Worker's
64
+ # child is the process that died.
65
+ wait_for_child(:dont_block => true) if pid
66
+ end
67
+
68
+ @pid = fork_child do
69
+ log("Started")
70
+
71
+ if respond_to?(:after_fork)
72
+ log("Running after fork callback")
73
+ after_fork
74
+ log("Finished running after fork callback")
75
+ end
76
+
77
+ @interrupted = false
78
+ trap("INT") do
79
+ log("SIGINT Received")
80
+ @interrupted = true
81
+ end
82
+ trap("TERM") do
83
+ log("SIGTERM Received")
84
+ @interrupted = true
85
+ end
86
+
87
+ loop do
88
+ if @interrupted
89
+ log("Child process exiting gracefully")
90
+ exit 0
91
+ end
92
+ work
93
+ end
94
+ end
95
+ end
96
+
97
+ def stop!
98
+ unless started?
99
+ log("Called stop for worker with PID #{pid} but it is not started")
100
+ return false
101
+ end
102
+
103
+ # Some internal state so that other parts of our code know that we
104
+ # intentionally stopped the child process.
105
+ @stopping = true
106
+
107
+ begin
108
+ log("Sending SIGINT to child process with pid #{pid}.")
109
+ Timeout::timeout(@timeout) do
110
+ Process.kill('INT', pid)
111
+ wait_for_child
112
+ end
113
+ rescue Timeout::Error
114
+ begin
115
+ log("Child process with pid #{pid} did not stop with #@timeout seconds of SIGINT. Sending SIGKILL to child process.")
116
+ # This only leaves 1 second for the SIGKILL to take effect. I don't
117
+ # know if that is enough time (or maybe too much time).
118
+ Timeout::timeout(1) do
119
+ Process.kill('KILL', pid)
120
+ wait_for_child
121
+ end
122
+ rescue Timeout::Error
123
+ if pid_running?
124
+ msg = "Unable to kill child process with PID: #{pid}"
125
+ log(msg)
126
+ raise ChildProcessNotStoppedError, msg
127
+ end
128
+ end
129
+ end
130
+ @stopping = false
131
+ end
132
+
133
+ def started?
134
+ !!(pid && pid_running?)
135
+ end
136
+ def stopped?
137
+ !started?
138
+ end
139
+
140
+ #########
141
+ private #
142
+ #########
143
+
144
+
145
+ # The worker will call this method over and over in a loop.
146
+ def work
147
+ raise "implement this in a class that includes this module"
148
+ end
149
+
150
+
151
+ def log(msg)
152
+ @logger.add(@log_level, "#{Time.now.utc} - #{self.class} (pid #{$$}): #{msg}") if @logger
153
+ end
154
+
155
+
156
+ # Note: if the pid is running, but this process doesn't have permissions to
157
+ # access it, then this will return false.
158
+ def pid_running?
159
+ begin
160
+ Process.kill(0, pid) == 1
161
+ rescue Errno::ESRCH, Errno::EPERM
162
+ false
163
+ end
164
+ end
165
+
166
+ # Will just return if the child process is not running.
167
+ def wait_for_child(opts={})
168
+ begin
169
+ log("Waiting for child with pid #{pid}.")
170
+ if opts[:dont_block]
171
+ # When this is called as the result of a SIGCHLD
172
+ # we need to pass in Process::WNOHANG as the 2nd argument, otherwise when
173
+ # there are multiple workers, some workers will trap SIGCHLD when other
174
+ # workers' child processes die. Without this argument, those workers will
175
+ # hang forever, which also hangs the worker manager.
176
+ Process.wait(pid, Process::WNOHANG)
177
+ else
178
+ Process.wait(pid)
179
+ end
180
+ rescue Errno::ECHILD
181
+ # The child process has already been reaped.
182
+ end
183
+ end
184
+
185
+ # This is the standard/default way of doing it. Overwrite this if you want
186
+ # to wrap it in an exception handler, for example.
187
+ def fork_child(&block)
188
+ Process.fork &block
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Angael::Manager do
4
+ describe ".new" do
5
+ subject { Angael::Manager.new(Angael::TestSupport::SampleWorker, 2) }
6
+ it "should have 2 workers when 2 is passed in to the initializer" do
7
+ should have(2).workers
8
+ end
9
+
10
+ it "should call new on the passed in job class" do
11
+ Angael::TestSupport::SampleWorker.should_receive(:new).exactly(2).times
12
+ Angael::Manager.new(Angael::TestSupport::SampleWorker, 2)
13
+ end
14
+
15
+ it "should call new on the passed in job class with the passed in arguments" do
16
+ args = [1, 2, 3]
17
+ Angael::TestSupport::SampleWorker.should_receive(:new).with(*args).exactly(2).times
18
+ Angael::Manager.new(Angael::TestSupport::SampleWorker, 2, args)
19
+ end
20
+ end
21
+
22
+ describe "#start!" do
23
+ subject { Angael::Manager.new(Angael::TestSupport::SampleWorker, 2) }
24
+ it "should start all its workers" do
25
+ subject.workers.each do |w|
26
+ w.should_receive_in_child_process(:start!)
27
+ end
28
+
29
+ pid = Process.fork do
30
+ subject.start!
31
+ end
32
+
33
+ # Need to sleep to allow time for temp files used by should_receive_in_child_process to flush
34
+ sleep 0.1
35
+
36
+ # Clean up
37
+ Process.kill('KILL', pid)
38
+ Process.wait(pid)
39
+ end
40
+
41
+
42
+ %w(INT TERM).each do |sig|
43
+ context "when it receives a SIG#{sig}" do
44
+ it "should call #stop! on each Worker" do
45
+ subject.workers.each do |w|
46
+ w.stub(:start!) # We don't care about the sub-process, so don't start it.
47
+
48
+ w.should_receive_in_child_process(:stop!)
49
+ end
50
+
51
+ pid = Process.fork do
52
+ subject.start!
53
+ end
54
+ sleep 0.1 # Give the process a chance to start.
55
+ Process.kill(sig, pid)
56
+ sleep 0.1 # Give the TempFile a chance to flush
57
+
58
+ # Clean up
59
+ Process.wait(pid)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,276 @@
1
+ require 'spec_helper'
2
+ require 'tempfile'
3
+
4
+
5
+ describe Angael::Worker do
6
+ subject { Angael::TestSupport::SampleWorker.new }
7
+
8
+ describe "#start!" do
9
+ before { subject.stub(:work => nil) }
10
+ after { subject.stop! if subject.started? }
11
+
12
+ it "should set #pid" do
13
+ subject.pid.should be_nil
14
+ subject.start!
15
+ subject.pid.should_not be_nil
16
+ end
17
+
18
+ it "should create a process that has the pid of #pid" do
19
+ subject.start!
20
+ pid_running?(subject.pid).should be_true
21
+ end
22
+
23
+ it "should be started" do
24
+ subject.should_not be_started
25
+ subject.start!
26
+ subject.should be_started
27
+ end
28
+
29
+ it "should be doing work" do
30
+ # TODO: Update #should_receive_in_child_process to allow for a
31
+ # #more_than_once option, then use that instead of setting this all
32
+ # up manually.
33
+
34
+ # I'm using this temp file to ensure we are actually doing something. I
35
+ # can't just use should_receive because the "work" is done in a different
36
+ # process.
37
+ file = Tempfile.new('work-stub')
38
+ subject.stub(:work) do
39
+ @work_counter ||= 1
40
+ msg = "I am working. My PID is #{$$}. Run number #@work_counter"
41
+ file.puts msg
42
+ end
43
+
44
+ subject.start!
45
+
46
+ # Check that work was done (i.e. there is something in the file).
47
+ sleep 0.1
48
+ file.rewind
49
+ lines = file.readlines.size
50
+ lines.should > 0
51
+
52
+ # Check that more work was done (i.e. there is more in the file than
53
+ # last time we checked).
54
+ old_lines = lines
55
+ sleep 0.3
56
+ file.rewind
57
+ lines = file.readlines.size
58
+ lines.should > old_lines
59
+ end
60
+
61
+ context "child process dies unexpectedly" do
62
+ before { subject.start! }
63
+ it "should not be started" do
64
+ Process.kill('KILL', subject.pid)
65
+ sleep 0.1 # Wait for SIGKILL to take effect.
66
+ subject.should_not be_started
67
+ end
68
+
69
+ it "should still have #pid set to the child process's pid" do
70
+ pid = subject.pid
71
+ Process.kill('KILL', subject.pid)
72
+ subject.pid.should == pid
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+
79
+
80
+ describe "#stop!" do
81
+ before { subject.stub(:work => nil) }
82
+ after { subject.stop! }
83
+
84
+ context "is stopped" do
85
+ it "should return false" do
86
+ subject.should_not be_started
87
+ subject.stop!.should be_false
88
+ end
89
+
90
+ it "should not send a SIGINT to the child process" do
91
+ should_not_receive_and_run(Process, :kill, 'INT', subject.pid)
92
+ subject.stop!
93
+ end
94
+ end
95
+
96
+ context "is started" do
97
+ it "should send a SIGINT to the child process" do
98
+ subject.start!
99
+ should_receive_and_run(Process, :kill, 'INT', subject.pid)
100
+ subject.stop!
101
+ end
102
+
103
+ context "child process does die within the worker's timeout" do
104
+ subject { @worker ||= Angael::TestSupport::SampleWorker.new(:timeout => 2) }
105
+ before do
106
+ subject.stub(:work) { nil }
107
+ subject.start!
108
+ end
109
+ it "should be stopped" do
110
+ subject.stop!
111
+ subject.should be_stopped
112
+ end
113
+
114
+ it "should not have a child process with the pid #pid" do
115
+ subject.stop!
116
+ pid_running?(subject.pid).should be_false
117
+ end
118
+
119
+ it "should not send a SIGKILL to the child process" do
120
+ should_not_receive_and_run(Process, :kill, 'KILL', subject.pid)
121
+ subject.stop!
122
+ end
123
+
124
+ it "should have the (now dead) child process' PID as #pid" do
125
+ pid = subject.pid
126
+ subject.stop!
127
+ subject.pid.should == pid
128
+ end
129
+ end
130
+
131
+ context "child process does not die within the worker's timeout" do
132
+ subject { @worker ||= Angael::TestSupport::SampleWorker.new(:timeout => 1) }
133
+ before do
134
+ subject.stub(:work) { sleep 1000 }
135
+ subject.start!
136
+ sleep 1 # Leave some time for the child process to enter the sleep.
137
+ end
138
+
139
+ it "should be stopped" do
140
+ subject.stop!
141
+ subject.should be_stopped
142
+ end
143
+
144
+ it "should not have a child process with the pid #pid" do
145
+ subject.stop!
146
+ pid_running?(subject.pid).should be_false
147
+ end
148
+
149
+ it "should send a SIGKILL to the child process" do
150
+ should_receive_and_run(Process, :kill, 'KILL', subject.pid)
151
+ subject.stop!
152
+ end
153
+
154
+ context "child process does not die after receiving SIGKILL" do
155
+ before do
156
+ Process.instance_eval { alias :_original_kill :kill }
157
+ # This ensures that SIGKILL doesn't kill the child process, but
158
+ # all other signals are processed.
159
+ Process.stub(:kill) do |*args|
160
+ if args == ['KILL', subject.pid]
161
+ nil
162
+ else
163
+ Process._original_kill(*args)
164
+ end
165
+ end
166
+ end
167
+ after do
168
+ # Clean up
169
+ Process._original_kill('KILL', subject.pid)
170
+ sleep 0.1 # Wait for SIGKILL to take effect.
171
+ pid_running?(subject.pid).should be_false
172
+ end
173
+
174
+ it "should raise an error with the child process' pid in the message" do
175
+ pid_running?(subject.pid).should be_true
176
+ lambda do
177
+ subject.stop!
178
+ end.should raise_error(Angael::Worker::ChildProcessNotStoppedError, /#{subject.pid}/)
179
+
180
+ # Confirm the PID is still running
181
+ pid_running?(subject.pid).should be_true
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+
189
+ # Note: Be very careful when trying to refactor these tests. The exact timing
190
+ # of signals is not very easy to predict. The specs in this describe
191
+ # block have been carefully tested (with many test runs) to make sure
192
+ # they are very unlikely to fail for random timing reasons. If you must
193
+ # change these specs, make sure you run the whole test suite several
194
+ # times to make sure all the specs consistently pass.
195
+ describe "child process handling of signals" do
196
+ context "when started" do
197
+ before { subject.stub(:work) }
198
+
199
+ %w(INT TERM).each do |sig|
200
+ context "when child process receives SIG#{sig}" do
201
+ it "should exit gracefully (i.e. with status 0)" do
202
+ subject.start!
203
+ # Don't let the worker reap its child process because we need to
204
+ # get at the PID and status here.
205
+ subject.stub(:wait_for_child)
206
+ sleep 0.1 # Make sure there was enough time for the child process to start.
207
+ Process.kill(sig, subject.pid)
208
+ pid, status = Process.wait2(subject.pid)
209
+ status.should == 0
210
+ end
211
+
212
+ it "should reap the child process" do
213
+ # As a general rule, I would only expect this method to be called
214
+ # once. However, signal behavior is difficult to predict. In some
215
+ # of our test runs, it is called more than once. If we do receive
216
+ # multiple SIGCHLD signals (which would be what causes this method
217
+ # to be called multiple times) there are no ill side effects. Thus
218
+ # I think it is quite safe to allow this method to be called more
219
+ # than once.
220
+ # -- Paul Cortens (2011-05-18)
221
+ subject.should_receive(:wait_for_child).at_least(:once)
222
+
223
+ subject.start!
224
+ sleep 0.1 # Make sure there was enough time for the child process to start.
225
+ Process.kill(sig, subject.pid)
226
+
227
+ # We need to wait this long to make sure the child process had time
228
+ # to respond to the signal. If this is decreased to 'sleep 0.1' then
229
+ # the tests will sometimes fail because there wasn't enough time for
230
+ # the trap block to run #wait_for_child.
231
+ sleep 0.5
232
+
233
+ # Clean up zombie child processes.
234
+ Process.wait(subject.pid)
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+
242
+
243
+ ###########
244
+ # Helpers #
245
+ ###########
246
+
247
+ # Note: if the pid is running, but this process doesn't have permissions to
248
+ # access it, then this will return false.
249
+ def pid_running?(pid)
250
+ begin
251
+ Process.kill(0, pid) == 1
252
+ rescue Errno::ESRCH, Errno::EPERM
253
+ false
254
+ end
255
+ end
256
+
257
+ # These methods let me set an expectation that amethod should be called, but
258
+ # also allow that method to actually be called.
259
+ def stub_and_run(object, method, *args)
260
+ unstubbed_method = "_unstubbed_#{method}".to_sym
261
+ method = method.to_sym
262
+ # This is a bit ugly, but because alias is not a method, but a keyword, we
263
+ # need to use eval like this. For more details see this thread:
264
+ # http://www.ruby-forum.com/topic/135598
265
+ object.instance_eval "alias #{unstubbed_method.inspect} #{method.inspect}"
266
+ object.stub(method) { |*args| object.send(unstubbed_method, *args) }
267
+ end
268
+ def should_receive_and_run(object, method, *args)
269
+ stub_and_run(object, method, *args)
270
+ object.should_receive(method).with(*args)
271
+ end
272
+ def should_not_receive_and_run(object, method, *args)
273
+ stub_and_run(object, method, *args)
274
+ object.should_not_receive(method).with(*args)
275
+ end
276
+ end
@@ -0,0 +1,15 @@
1
+ require 'bundler'
2
+ Bundler.require(:default, :development)
3
+ require 'rspec' # => required for RubyMine to be able to run specs
4
+
5
+ Dir[File.expand_path(File.dirname(__FILE__) + '/support/**/*.rb')].each { |f| require f }
6
+
7
+ RSpec.configure do |config|
8
+
9
+ config.mock_with :rspec
10
+ require 'rspec/process_mocks' # This line must be after 'config.mock_with :rspec'
11
+
12
+ # gets rid of the bacetrace that we don't care about
13
+ config.backtrace_clean_patterns = [/\.rvm\/gems\//]
14
+
15
+ end
@@ -0,0 +1,10 @@
1
+ module Angael
2
+ module TestSupport
3
+ class SampleWorker
4
+ include Angael::Worker
5
+ def work
6
+ # Do Nothing
7
+ end
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: angael
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.1
6
+ platform: ruby
7
+ authors:
8
+ - Paul Cortens
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-05-28 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: rspec
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - "="
23
+ - !ruby/object:Gem::Version
24
+ version: 2.6.0
25
+ type: :development
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-process-mocks
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :development
37
+ version_requirements: *id002
38
+ description:
39
+ email:
40
+ - paul@thoughtless.ca
41
+ executables: []
42
+
43
+ extensions: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ files:
48
+ - .gitignore
49
+ - Gemfile
50
+ - README.md
51
+ - angael.gemspec
52
+ - lib/angael.rb
53
+ - lib/angael/manager.rb
54
+ - lib/angael/version.rb
55
+ - lib/angael/worker.rb
56
+ - spec/lib/angael/manager_spec.rb
57
+ - spec/lib/angael/worker_spec.rb
58
+ - spec/spec_helper.rb
59
+ - spec/support/test_worker.rb
60
+ has_rdoc: true
61
+ homepage: http://github.com/thoughtless/angael
62
+ licenses: []
63
+
64
+ post_install_message:
65
+ rdoc_options: []
66
+
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: "0"
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project: angael
84
+ rubygems_version: 1.5.0
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Lightweight library for running repetitive background processes.
88
+ test_files: []
89
+