angael 0.0.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,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
+