postbox 0.2.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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Phunkwork
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ # Postbox
2
+
3
+ Prototype. Not to be described yet..
4
+
5
+
6
+ ## Note on Patches/Pull Requests
7
+
8
+ * Fork the project.
9
+ * Make your feature addition or bug fix.
10
+ * Add specs for it.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ * Send me a pull request. Bonus points for topic branches.
13
+
14
+
15
+ ## Copyright
16
+
17
+ Copyright (c) 2010 Phunkwork. See LICENSE for details.
@@ -0,0 +1,70 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "postbox"
8
+ gem.summary = %Q{An asynchronous http push library}
9
+ gem.description = %Q{Asynchronous http push library. Designed to handle lots of messages without interrupting the application flow.}
10
+ gem.email = "andreas@phunkwork.com"
11
+ gem.homepage = "http://github.com/phunkwork/postbox"
12
+ gem.authors = ["Andreas Wolff"]
13
+ gem.add_dependency "httparty" ,">= 0.5.2"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'spec/rake/spectask'
22
+ Spec::Rake::SpecTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.spec_files = FileList['spec/**/*_spec.rb']
25
+ end
26
+
27
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
28
+ spec.libs << 'lib' << 'spec'
29
+ spec.pattern = 'spec/**/*_spec.rb'
30
+ spec.rcov = true
31
+ end
32
+
33
+ task :spec => :check_dependencies
34
+
35
+ begin
36
+ require 'reek/adapters/rake_task'
37
+ Reek::RakeTask.new do |t|
38
+ t.fail_on_error = true
39
+ t.verbose = false
40
+ t.source_files = 'lib/**/*.rb'
41
+ end
42
+ rescue LoadError
43
+ task :reek do
44
+ abort "Reek is not available. In order to run reek, you must: sudo gem install reek"
45
+ end
46
+ end
47
+
48
+ begin
49
+ require 'roodi'
50
+ require 'roodi_task'
51
+ RoodiTask.new do |t|
52
+ t.verbose = false
53
+ end
54
+ rescue LoadError
55
+ task :roodi do
56
+ abort "Roodi is not available. In order to run roodi, you must: sudo gem install roodi"
57
+ end
58
+ end
59
+
60
+ task :default => :spec
61
+
62
+ require 'rake/rdoctask'
63
+ Rake::RDocTask.new do |rdoc|
64
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
65
+
66
+ rdoc.rdoc_dir = 'rdoc'
67
+ rdoc.title = "postbox #{version}"
68
+ rdoc.rdoc_files.include('README*')
69
+ rdoc.rdoc_files.include('lib/**/*.rb')
70
+ end
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ # TODO
2
+
3
+ * Steak acceptance tests
4
+ * Readme and Documentation
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'httparty'
3
+
4
+ module Postbox
5
+ autoload :HTTP, 'postbox/http'
6
+ autoload :Stack, 'postbox/stack'
7
+ autoload :Worker, 'postbox/worker'
8
+
9
+ extend self
10
+
11
+ IS_IRB = $0 == 'irb'
12
+
13
+ attr_writer :attempt_count, :attempt_delay
14
+
15
+ def attempt_count
16
+ @attempt_count ||= 10
17
+ end
18
+
19
+ def attempt_delay
20
+ @attempt_delay ||= 6
21
+ end
22
+
23
+ def post(url, params = {}, &callback)
24
+ item = Stack.push(url, params, &callback)
25
+ Postbox::Worker.worker_thread.join if IS_IRB
26
+ return item
27
+ end
28
+
29
+ end
@@ -0,0 +1,26 @@
1
+ module Postbox
2
+ module HTTP
3
+ extend self
4
+ include HTTParty
5
+
6
+ def post_item(item)
7
+ begin
8
+ item.response = post(item.url, :body => item.params)
9
+ item.success = (200..299) === item.response.code
10
+ return true
11
+ rescue Timeout::Error, Errno::ECONNREFUSED, SocketError => error
12
+ item.last_error = error
13
+ item.last_attempt_at = Time.now
14
+ item.post_attempts += 1
15
+ return false
16
+ rescue => error
17
+ item.success = false
18
+ item.last_error = error
19
+ return false
20
+ ensure
21
+ item.callback.call(item) unless item.callback.nil?
22
+ end
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ require 'ostruct'
2
+
3
+ module Postbox
4
+ module Stack
5
+ extend self
6
+ extend Enumerable
7
+
8
+ def << (item)
9
+ stack << item
10
+ end
11
+
12
+ def stack
13
+ @stack ||= []
14
+ end
15
+
16
+ def each
17
+ stack.each { |e| yield(e) }
18
+ end
19
+
20
+ def empty?
21
+ stack.empty?
22
+ end
23
+
24
+ def shift
25
+ stack.shift
26
+ end
27
+
28
+ def size
29
+ stack.size
30
+ end
31
+
32
+ def push(url, params = {}, &callback)
33
+ stack << item = OpenStruct.new(:url => url, :params => params, :callback => callback, :post_attempts => 0)
34
+ Postbox::Worker.wakeup
35
+ return item
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,50 @@
1
+ module Postbox
2
+ module Worker
3
+ extend self
4
+
5
+ attr_accessor :worker_thread
6
+
7
+ def wakeup
8
+ if !@worker_thread.nil? && @worker_thread.alive?
9
+ @worker_thread.wakeup
10
+ else
11
+ @worker_thread = init
12
+ end
13
+ end
14
+
15
+ def init
16
+ Thread.new do
17
+ loop do
18
+ if Postbox::Stack.empty?
19
+ Thread.main.wakeup if Postbox::IS_IRB
20
+ sleep
21
+ else
22
+ unless process_item(Postbox::Stack.shift)
23
+ sleep 0.5
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ def process_item(item)
31
+ unless next_attempt_allowed?(item) && (attempts_exceeded?(item) || HTTP::post_item(item))
32
+ Postbox::Stack << item
33
+ return false
34
+ end
35
+
36
+ true
37
+ end
38
+
39
+
40
+ private
41
+
42
+ def attempts_exceeded?(item)
43
+ item.post_attempts >= Postbox.attempt_count
44
+ end
45
+
46
+ def next_attempt_allowed?(item)
47
+ item.last_attempt_at.blank? || item.last_attempt_at + Postbox.attempt_delay <= Time.now
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,113 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Postbox::HTTP do
4
+ describe '.post_item' do
5
+ before(:each) do
6
+ @url = 'test.host/path'
7
+ @params = { :a => "a" }
8
+ @item = OpenStruct.new(:url => @url, :params => @params, :post_attempts => 0)
9
+ end
10
+
11
+ it 'should post an item including params' do
12
+ Postbox::HTTP.should_receive(:post).with(@url, :body => @params)
13
+ Postbox::HTTP.post_item(@item)
14
+ end
15
+
16
+ it 'should assign the response to the posted item' do
17
+ Postbox::HTTP.stub!(:post).and_return("<response>")
18
+ Postbox::HTTP.post_item(@item)
19
+ @item.response.should eql("<response>")
20
+ end
21
+
22
+ it 'should give the posted item a true success attribute if the http status code is between 200 and 299' do
23
+ response = mock('Response', :code => 201)
24
+ Postbox::HTTP.stub!(:post).and_return(response)
25
+ Postbox::HTTP.post_item(@item)
26
+ @item.success.should be_true
27
+ end
28
+
29
+ context 'handling errors' do
30
+ it 'should return false' do
31
+ Postbox::HTTP.stub!(:post).and_raise("Error")
32
+ Postbox::HTTP.post_item(@item).should be_false
33
+ end
34
+
35
+ context 'which should be temporary' do
36
+ it 'should return false' do
37
+ error = Timeout::Error.new
38
+ Postbox::HTTP.stub!(:post).and_raise(error)
39
+ Postbox::HTTP.post_item(@item).should be_false
40
+ end
41
+
42
+ it 'should store the last error' do
43
+ error = Timeout::Error.new
44
+ Postbox::HTTP.stub!(:post).and_raise(error)
45
+ Postbox::HTTP.post_item(@item)
46
+ @item.last_error.should eql(error)
47
+ end
48
+
49
+ it 'should store the time when the last attempt happend' do
50
+ Postbox::HTTP.stub!(:post).and_raise(Timeout::Error)
51
+ Postbox::HTTP.post_item(@item)
52
+ @item.last_attempt_at.should_not be_nil
53
+ end
54
+
55
+ it 'should increase the attempts count if its a timeout error' do
56
+ Postbox::HTTP.stub!(:post).and_raise(Timeout::Error)
57
+ Postbox::HTTP.post_item(@item)
58
+ @item.post_attempts.should be(1)
59
+ end
60
+
61
+ it 'should increse the attempts count if its a connection error' do
62
+ Postbox::HTTP.stub!(:post).and_raise(Errno::ECONNREFUSED)
63
+ Postbox::HTTP.post_item(@item)
64
+ @item.post_attempts.should be(1)
65
+ end
66
+
67
+ it 'should increse the attempts count if its a connection error' do
68
+ Postbox::HTTP.stub!(:post).and_raise(SocketError)
69
+ Postbox::HTTP.post_item(@item)
70
+ @item.post_attempts.should be(1)
71
+ end
72
+ end
73
+
74
+ it 'should mark the posted item as failed (success == false)' do
75
+ Postbox::HTTP.stub!(:post).and_raise("Error")
76
+ Postbox::HTTP.post_item(@item)
77
+ @item.success.should be_false
78
+ end
79
+
80
+ it 'should assign the error to the item' do
81
+ error = StandardError.new
82
+ Postbox::HTTP.stub!(:post).and_raise(error)
83
+ Postbox::HTTP.post_item(@item)
84
+ @item.last_error.should eql(error)
85
+ end
86
+ end
87
+
88
+ context 'handling callbacks' do
89
+ before(:each) do
90
+ @item.callback = lambda {}
91
+ response = mock('Response', :code => 201)
92
+ Postbox::HTTP.stub!(:post).and_return(response)
93
+ end
94
+
95
+ it 'should call the block if it exists' do
96
+ @item.callback.should_receive(:call)
97
+ Postbox::HTTP.post_item(@item)
98
+ end
99
+
100
+ it 'should pass the item to the block' do
101
+ @item.callback.should_receive(:call).with(@item)
102
+ Postbox::HTTP.post_item(@item)
103
+ end
104
+
105
+ it 'should be called even if an error occurs' do
106
+ Postbox::HTTP.stub!(:post).and_raise("Error")
107
+ @item.callback.should_receive(:call)
108
+ Postbox::HTTP.post_item(@item)
109
+ end
110
+ end
111
+ end
112
+
113
+ end
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Postbox::Stack do
4
+
5
+ describe '.<<' do
6
+ it 'should put an item on to the stack' do
7
+ item = mock('item')
8
+ Postbox::Stack << item
9
+ Postbox::Stack.should include(item)
10
+ end
11
+ end
12
+
13
+ describe '.stack' do
14
+ it 'should return an array' do
15
+ Postbox::Stack.stack.should be_kind_of(Array)
16
+ end
17
+
18
+ it 'should init the array only once' do
19
+ stack = Array.new
20
+ Postbox::Stack.send(:instance_variable_set, :@stack, stack)
21
+ Postbox::Stack.stack.should be(stack)
22
+ end
23
+ end
24
+
25
+ describe '.each' do
26
+ it 'should proxy calls to the stack object' do
27
+ stack = []
28
+ Postbox::Stack.stub!(:stack).and_return(stack)
29
+ stack.should_receive(:each)
30
+ Postbox::Stack.each
31
+ end
32
+ end
33
+
34
+ describe '.push' do
35
+ before(:each) do
36
+ Postbox::Worker.stub!(:wakeup)
37
+ end
38
+
39
+ it 'should put items on the stack' do
40
+ callback = lambda { "test" }
41
+ item = Postbox::Stack.push( 'localhost/123', { :fu => 'ba' }, &callback )
42
+ item.url.should eql('localhost/123')
43
+ item.params.should eql({ :fu => 'ba' })
44
+ item.callback.should eql(callback)
45
+ end
46
+
47
+ it 'should wake up the worker after adding an item' do
48
+ Postbox::Worker.should_receive(:wakeup)
49
+ Postbox::Stack.push( 'localhost/123' )
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,89 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper'
2
+
3
+ describe Postbox::Worker do
4
+
5
+ describe '.wakeup' do
6
+ before(:each) do
7
+ @worker_thread = mock('WorkerThread')
8
+ @worker_thread.stub!(:wakeup)
9
+ end
10
+
11
+ it 'should initialize the worker' do
12
+ Postbox::Worker.should_receive(:init).and_return(@worker_thread)
13
+ Postbox::Worker.wakeup
14
+ end
15
+
16
+ context 'with living thread' do
17
+ before(:each) do
18
+ @worker_thread.stub!(:alive?).and_return(true)
19
+ end
20
+
21
+ it 'should not initialize the worker if its already alive' do
22
+ Postbox::Worker.worker_thread = @worker_thread
23
+ Postbox::Worker.should_not_receive(:init)
24
+ Postbox::Worker.wakeup
25
+ end
26
+
27
+ it 'should wakeup the worker thread' do
28
+ Postbox::Worker.worker_thread = @worker_thread
29
+ @worker_thread.should_receive(:wakeup)
30
+ Postbox::Worker.wakeup
31
+ end
32
+ end
33
+
34
+ context 'with dead thread' do
35
+ it 'should init the worker' do
36
+ @worker_thread.stub!(:alive?).and_return(false)
37
+ Postbox::Worker.worker_thread = @worker_thread
38
+ Postbox::Worker.should_receive(:init)
39
+ Postbox::Worker.wakeup
40
+ end
41
+ end
42
+ end
43
+
44
+ describe '.process_item' do
45
+ before(:each) do
46
+ Postbox.stub!(:attempt_count).and_return(5)
47
+ @item = mock('Item', :post_attempts => 0, :last_attempt_at => nil)
48
+ end
49
+
50
+ it 'should send the item' do
51
+ Postbox::HTTP.should_receive(:post_item).with(@item)
52
+ Postbox::Worker.process_item(@item)
53
+ end
54
+
55
+ context 'handling failed submissions' do
56
+ before(:each) do
57
+ Postbox::HTTP.stub!(:post_item).and_return(false)
58
+ end
59
+
60
+ it 'should put the item back on the stack' do
61
+ Postbox::Stack.should_receive(:<<).with(@item)
62
+ Postbox::Worker.process_item(@item)
63
+ end
64
+
65
+ it 'should not put the item back on stack if then max attempt count has been reached' do
66
+ @item.stub!(:post_attempts).and_return(10)
67
+ Postbox::Stack.should_not_receive(:<<).with(@item)
68
+ Postbox::Worker.process_item(@item)
69
+ end
70
+ end
71
+
72
+ context 'handling delays between submissions' do
73
+ before(:each) do
74
+ Postbox.stub!(:attempt_delay).and_return(600)
75
+ @item.stub!(:last_attempt_at).and_return(Time.now)
76
+ end
77
+
78
+ it 'should not send the item if the timespan between the last-send-timestamp and now is to short' do
79
+ Postbox::HTTP.should_not_receive(:post_item)
80
+ Postbox::Worker.process_item(@item)
81
+ end
82
+
83
+ it 'should put items back on the stack if sending is not allowed at the moment' do
84
+ Postbox::Stack.should_receive(:<<).with(@item)
85
+ Postbox::Worker.process_item(@item)
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe Postbox do
4
+
5
+ describe 'configuration attributes' do
6
+ it 'should have default values' do
7
+ Postbox::attempt_count.should eql(10)
8
+ Postbox::attempt_delay.should eql(6)
9
+ end
10
+
11
+ it 'should allow overriding the default values' do
12
+ Postbox::attempt_count = 5
13
+ Postbox::attempt_delay = 3
14
+ Postbox::attempt_count.should eql(5)
15
+ Postbox::attempt_delay.should eql(3)
16
+ end
17
+
18
+ end
19
+
20
+ describe '.post' do
21
+ it 'should push a post job to the stack' do
22
+ Postbox::Stack.should_receive(:push).with('localhost/api', anything())
23
+ Postbox.post "localhost/api"
24
+ end
25
+
26
+ it 'should accept optional params' do
27
+ Postbox::Stack.should_receive(:push).with(anything(), { :fu => "ba", "ba" => :fu })
28
+ Postbox.post "localhost/api", :fu => "ba", "ba" => :fu
29
+ end
30
+
31
+ it 'should take an optional block' do
32
+ callback = lambda { "test" }
33
+ Postbox::Stack.should_receive(:push).with(anything(), anything(), &callback)
34
+ Postbox.post "localhost/api", {}, &callback
35
+ end
36
+ end
37
+
38
+ end
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,9 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'postbox'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+
7
+ Spec::Runner.configure do |config|
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postbox
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - Andreas Wolff
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-30 00:00:00 +02:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: httparty
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 15
30
+ segments:
31
+ - 0
32
+ - 5
33
+ - 2
34
+ version: 0.5.2
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ hash: 13
46
+ segments:
47
+ - 1
48
+ - 2
49
+ - 9
50
+ version: 1.2.9
51
+ type: :development
52
+ version_requirements: *id002
53
+ description: Asynchronous http push library. Designed to handle lots of messages without interrupting the application flow.
54
+ email: andreas@phunkwork.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files:
60
+ - LICENSE
61
+ - README.md
62
+ files:
63
+ - .document
64
+ - .gitignore
65
+ - LICENSE
66
+ - README.md
67
+ - Rakefile
68
+ - TODO.md
69
+ - VERSION
70
+ - lib/postbox.rb
71
+ - lib/postbox/http.rb
72
+ - lib/postbox/stack.rb
73
+ - lib/postbox/worker.rb
74
+ - spec/postbox/http_spec.rb
75
+ - spec/postbox/stack_spec.rb
76
+ - spec/postbox/worker_spec.rb
77
+ - spec/postbox_spec.rb
78
+ - spec/spec.opts
79
+ - spec/spec_helper.rb
80
+ has_rdoc: true
81
+ homepage: http://github.com/phunkwork/postbox
82
+ licenses: []
83
+
84
+ post_install_message:
85
+ rdoc_options:
86
+ - --charset=UTF-8
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ hash: 3
95
+ segments:
96
+ - 0
97
+ version: "0"
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ hash: 3
104
+ segments:
105
+ - 0
106
+ version: "0"
107
+ requirements: []
108
+
109
+ rubyforge_project:
110
+ rubygems_version: 1.3.7
111
+ signing_key:
112
+ specification_version: 3
113
+ summary: An asynchronous http push library
114
+ test_files:
115
+ - spec/postbox/http_spec.rb
116
+ - spec/postbox/stack_spec.rb
117
+ - spec/postbox/worker_spec.rb
118
+ - spec/postbox_spec.rb
119
+ - spec/spec_helper.rb