pipes 0.1.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,22 @@
1
+ require 'spec_helper'
2
+ require 'pipes/resque_hooks'
3
+
4
+ describe Pipes::ResqueHooks do
5
+ class ResqueWorker
6
+ extend Pipes::ResqueHooks
7
+ end
8
+
9
+ describe '.after_perform_pipes' do
10
+ it 'lets the Redis store know it is finished working' do
11
+ Pipes::Store.should_receive(:done)
12
+ ResqueWorker.after_perform_pipes
13
+ end
14
+ end
15
+
16
+ describe '.on_failure_pipes' do
17
+ it 'clears the current job, forfeiting any remaining stages' do
18
+ Pipes::Store.should_receive(:done)
19
+ ResqueWorker.on_failure_pipes(Exception.new)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,110 @@
1
+ require 'spec_helper'
2
+ require 'mock_jobs'
3
+
4
+ describe Pipes::Runner do
5
+
6
+ describe '.run' do
7
+
8
+ let(:jobs) { [Writers::ContentWriter, Writers::AnotherContentWriter, Publishers::Publisher] }
9
+ let(:arg) { 'en-US' }
10
+
11
+ before do
12
+ Pipes.configure do |config|
13
+ config.stages do
14
+ content_writers [{Writers::ContentWriter => :publishers}]
15
+ publishers [Publishers::Publisher]
16
+ end
17
+ end
18
+ end
19
+
20
+ context 'with dependency resolution turned off' do
21
+
22
+ let(:options) { {resolve: false} }
23
+
24
+ it 'adds a pipe for all the jobs and runs it, filtering out jobs that are not configured' do
25
+ pipe = [
26
+ {name: :content_writers, jobs: [{class: Writers::ContentWriter, args: [arg]}]},
27
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: [arg]}]}
28
+ ]
29
+
30
+ Pipes::Store.should_receive(:add_pipe).with(pipe, options)
31
+ Pipes::Runner.run(jobs, arg, options)
32
+ end
33
+
34
+ it 'can be turned off in the config' do
35
+ original = Pipes.resolve
36
+ Pipes.resolve = false
37
+
38
+ Pipes::Store.should_receive(:add_pipe).with([{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]}], {})
39
+ Pipes::Runner.run(Writers::ContentWriter)
40
+
41
+ Pipes.resolve = original
42
+ end
43
+
44
+ it 'accepts an array of strings of jobs' do
45
+ Pipes::Store.should_receive(:add_pipe).with([{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]}], options)
46
+ Pipes::Runner.run(['Writers::ContentWriter'], options)
47
+ end
48
+
49
+ it 'accepts an array of symbols referring to stages' do
50
+ pipe = [
51
+ {name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]},
52
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: []}]}
53
+ ]
54
+
55
+ Pipes::Store.should_receive(:add_pipe).with(pipe, options)
56
+ Pipes::Runner.run([:content_writers, :publishers], options)
57
+ end
58
+
59
+ it 'accepts a singular class' do
60
+ Pipes::Store.should_receive(:add_pipe).with([{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]}], options)
61
+ Pipes::Runner.run(Writers::ContentWriter, options)
62
+ end
63
+
64
+ it 'accepts a singular string referring to a class' do
65
+ Pipes::Store.should_receive(:add_pipe).with([{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]}], options)
66
+ Pipes::Runner.run('Writers::ContentWriter', options)
67
+ end
68
+
69
+ it 'accepts a symbol referring to a stage' do
70
+ Pipes::Store.should_receive(:add_pipe).with([{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]}], options)
71
+ Pipes::Runner.run(:content_writers, options)
72
+ end
73
+
74
+ end
75
+
76
+ context 'with dependency resolution turned on' do
77
+
78
+ let(:options) { {resolve: true} }
79
+ let(:pipe) {
80
+ [
81
+ {name: :content_writers, jobs: [{class: Writers::ContentWriter, args: []}]},
82
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: []}]}
83
+ ]
84
+ }
85
+
86
+ it 'includes all dependencies of the provided job' do
87
+ Pipes::Store.should_receive(:add_pipe).with(pipe, options)
88
+ Pipes::Runner.run(Writers::ContentWriter, options)
89
+ end
90
+
91
+ it 'is the default' do
92
+ Pipes::Store.should_receive(:add_pipe).with(pipe, {})
93
+ Pipes::Runner.run(Writers::ContentWriter)
94
+ end
95
+
96
+ it 'can overwrite the value defined in the config' do
97
+ original = Pipes.resolve
98
+ Pipes.resolve = false
99
+
100
+ Pipes::Store.should_receive(:add_pipe).with(pipe, options)
101
+ Pipes::Runner.run(Writers::ContentWriter, options)
102
+
103
+ Pipes.resolve = original
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+
110
+ end
@@ -0,0 +1,169 @@
1
+ require 'spec_helper'
2
+ require 'mock_jobs'
3
+
4
+ describe Pipes::StageParser do
5
+ subject { Pipes::StageParser }
6
+
7
+ describe '#stage_names' do
8
+ it 'returns the name of the stages, in the same order' do
9
+ stages = {
10
+ writers: [ Writers::ContentWriter ],
11
+ publishers: [ Publishers::Publisher ]
12
+ }
13
+
14
+ subject.new(stages).stage_names.should == [:writers, :publishers]
15
+ end
16
+ end
17
+
18
+ describe '#jobs_in_stage' do
19
+ it 'returns all jobs for a given stage, without dependents' do
20
+ stages = {
21
+ writers: [ { Writers::ContentWriter => Publishers::Publisher } ],
22
+ publishers: [ Publishers::Publisher ]
23
+ }
24
+
25
+ subject.new(stages).jobs_in_stage(:writers).should == [Writers::ContentWriter]
26
+ end
27
+ end
28
+
29
+ describe '#dependents_for' do
30
+ let(:stages) {
31
+ {
32
+ writers: [ { Writers::ContentWriter => :publishers } ],
33
+ publishers: [ Publishers::Publisher => Emailers::Email ],
34
+ emailers: [ Emailers::Email ]
35
+ }
36
+ }
37
+
38
+ it 'returns an empty array for jobs that do not exist' do
39
+ subject.new(stages).dependents_for(Writers::AnotherContentWriter).should == []
40
+ end
41
+
42
+ it 'returns an array containing the recursive dependents' do
43
+ expected = [Publishers::Publisher, Emailers::Email]
44
+
45
+ subject.new(stages).dependents_for(Writers::ContentWriter).should == expected
46
+ end
47
+ end
48
+
49
+ describe '#stages_with_resolved_dependencies' do
50
+ context 'when the stage contains only job classes' do
51
+ let(:stages) {
52
+ {
53
+ writers: [ Writers::ContentWriter ],
54
+ publishers: [ Publishers::Publisher ]
55
+ }
56
+ }
57
+
58
+ it 'returns a set of nested arrays, keeping the defined order, each representing a stage and its jobs with empty dependents' do
59
+ expected = {
60
+ writers: [ { Writers::ContentWriter => [] } ],
61
+ publishers: [ { Publishers::Publisher => [] } ]
62
+ }
63
+
64
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
65
+ end
66
+ end
67
+
68
+ context 'when the stage containing a hash defining a dependent class' do
69
+ let(:stages) {
70
+ { writers: [ { Writers::ContentWriter => Publishers::Publisher } ] }
71
+ }
72
+
73
+ it 'adds the dependent class to the list' do
74
+ expected = {
75
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher] } ]
76
+ }
77
+
78
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
79
+ end
80
+ end
81
+
82
+ context 'with the stage containing a hash defining a dependent stage' do
83
+ let(:stages) {
84
+ {
85
+ writers: [ { Writers::ContentWriter => :publishers } ],
86
+ publishers: [ Publishers::Publisher ]
87
+ }
88
+ }
89
+
90
+ it 'adds all classes within the dependent stage to the list' do
91
+ expected = {
92
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher] } ],
93
+ publishers: [ { Publishers::Publisher => [] } ]
94
+ }
95
+
96
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
97
+ end
98
+ end
99
+
100
+ context 'with the stage containing a hash defining an array of dependent classes' do
101
+ let(:stages) {
102
+ {
103
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher] } ],
104
+ publishers: [ Publishers::Publisher ]
105
+ }
106
+ }
107
+
108
+ it 'adds all classes within the array to the list' do
109
+ expected = {
110
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher] } ],
111
+ publishers: [ { Publishers::Publisher => [] } ]
112
+ }
113
+
114
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
115
+ end
116
+ end
117
+
118
+ context 'with the stage containing a hash defining an array of dependent stages' do
119
+ let(:stages) {
120
+ {
121
+ writers: [ { Writers::ContentWriter => [:publishers] } ],
122
+ publishers: [ Publishers::Publisher ]
123
+ }
124
+ }
125
+
126
+ it 'adds all classes within the dependent stages to the list' do
127
+ expected = {
128
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher] } ],
129
+ publishers: [ { Publishers::Publisher => [] } ]
130
+ }
131
+
132
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
133
+ end
134
+ end
135
+
136
+ context 'with a comlex configuration, intermixing dependent types' do
137
+ let (:stages) {
138
+ {
139
+ writers: [ { Writers::ContentWriter => [:publishers, Uploaders::Rsync] },
140
+ { Writers::AnotherContentWriter => [Emailers::Email] }
141
+ ],
142
+ publishers: [ { Publishers::Publisher => :emailers } ],
143
+ messengers: [ { Messengers::SMS => :uploaders } ],
144
+ uploaders: [ { Uploaders::Rsync => Notifiers::Twitter } ],
145
+ emailers: [ Emailers::Email, Emailers::AnotherEmail ],
146
+ notifiers: [ Notifiers::Twitter ]
147
+ }
148
+ }
149
+
150
+ it 'resolves all dependencies' do
151
+ expected = {
152
+ writers: [ { Writers::ContentWriter => [Publishers::Publisher, Emailers::Email, Emailers::AnotherEmail,
153
+ Uploaders::Rsync, Notifiers::Twitter] },
154
+ { Writers::AnotherContentWriter => [Emailers::Email] }
155
+ ],
156
+ publishers: [ { Publishers::Publisher => [Emailers::Email, Emailers::AnotherEmail] } ],
157
+ messengers: [ { Messengers::SMS => [Uploaders::Rsync, Notifiers::Twitter] } ],
158
+ uploaders: [ { Uploaders::Rsync => [Notifiers::Twitter] } ],
159
+ emailers: [ { Emailers::Email => [] },
160
+ { Emailers::AnotherEmail => [] }
161
+ ],
162
+ notifiers: [ { Notifiers::Twitter => [] } ]
163
+ }
164
+
165
+ subject.new(stages).stages_with_resolved_dependencies.should == expected
166
+ end
167
+ end
168
+ end
169
+ end
@@ -0,0 +1,181 @@
1
+ require 'spec_helper'
2
+ require 'mock_jobs'
3
+
4
+ describe Pipes::Store do
5
+
6
+ def mock_stages
7
+ Pipes::Store.stub!(:stages) { stages }
8
+ stages
9
+ end
10
+
11
+ def mock_pending_jobs(stage)
12
+ list = send("pending_jobs_#{stage}".to_sym)
13
+ Pipes::Store.stub!(:pending_jobs).with(stage) { list }
14
+ list
15
+ end
16
+
17
+ let(:stages) { [] }
18
+ let(:pending_jobs_content_writers) { [] }
19
+ let(:pending_jobs_publishers) { [] }
20
+
21
+ let(:job_options) { 'en-US' }
22
+ let(:pipe) { [{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: [job_options]}]},
23
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: [job_options]}]}] }
24
+
25
+ before do
26
+ Pipes::Store.clear_all
27
+ end
28
+
29
+ describe '.add_pipe' do
30
+ before do
31
+ @writers = mock_pending_jobs(:content_writers)
32
+ @publishers = mock_pending_jobs(:publishers)
33
+ Pipes::Store.stub!(:next_stage)
34
+ end
35
+
36
+ it 'adds the job to Redis and fires off the next available stage' do
37
+ Pipes::Store.should_receive(:next_stage)
38
+ Pipes::Store.add_pipe(pipe)
39
+
40
+ @writers.should == [{class: Writers::ContentWriter, args: [job_options]}]
41
+ @publishers.should == [{class: Publishers::Publisher, args: [job_options]}]
42
+ end
43
+
44
+ it 'does not add duplicate jobs to stages' do
45
+ another_pipe = [{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: [job_options]},
46
+ {class: Writers::AnotherContentWriter, args: [job_options]}]},
47
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: [job_options]}]}]
48
+
49
+ Pipes::Store.add_pipe(pipe)
50
+ Pipes::Store.add_pipe(another_pipe)
51
+
52
+ @writers.should == [{class: Writers::ContentWriter, args: [job_options]}, {class: Writers::AnotherContentWriter, args: [job_options]}]
53
+ @publishers.should == [{class: Publishers::Publisher, args: [job_options]}]
54
+ end
55
+
56
+ context 'with allow_duplicates set' do
57
+ it 'does not add jobs that have the same class already in the queue' do
58
+ mock_stages
59
+
60
+ Pipes::Store.add_pipe(pipe)
61
+
62
+ @writers.should_receive(:<<)
63
+ @publishers.should_not_receive(:<<)
64
+
65
+ another_pipe = [{name: :content_writers, jobs: [{class: Writers::ContentWriter, args: ['some arg']}]},
66
+ {name: :publishers, jobs: [{class: Publishers::Publisher, args: ['some arg']}]}]
67
+
68
+ Pipes::Store.add_pipe(another_pipe, {allow_duplicates: [:content_writers]})
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ describe '.next_stage' do
75
+ context 'with jobs left in the queue' do
76
+ it 'pulls the next stage with pending jobs off the queue and runs it' do
77
+ Pipes::Store.stub!(:stages) { [:content_writers, :publishers] }
78
+
79
+ writers = mock_pending_jobs(:content_writers) << {class: Writers::ContentWriter, args: []}
80
+ publishers = mock_pending_jobs(:publishers) << {class: Publishers::Publisher, args: []}
81
+
82
+ Pipes::Store.should_receive(:run_stage).with([{class: Writers::ContentWriter, args: []}])
83
+ Pipes::Store.should_not_receive(:run_stage).with([{class: Publishers::Publisher, args: []}])
84
+
85
+ Pipes::Store.next_stage
86
+ end
87
+
88
+ it 'runs stages in the order determined by the stages list' do
89
+ Pipes::Store.stub!(:stages) { [:publishers, :content_writers] }
90
+
91
+ writers = mock_pending_jobs(:content_writers) << {class: Writers::ContentWriter, args: []}
92
+ publishers = mock_pending_jobs(:publishers) << {class: Publishers::Publisher, args: []}
93
+
94
+ Pipes::Store.should_not_receive(:run_stage).with([{class: Writers::ContentWriter, args: []}])
95
+ Pipes::Store.should_receive(:run_stage).with([{class: Publishers::Publisher, args: []}])
96
+
97
+ Pipes::Store.next_stage
98
+ end
99
+ end
100
+
101
+ context 'without jobs left in the queue' do
102
+ it 'fires off the next pipe' do
103
+ mock_stages
104
+ Pipes::Store.should_not_receive(:run_stage)
105
+ Pipes::Store.next_stage
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '.run_stage' do
111
+ it 'sets the remaining counter and enqueues the jobs' do
112
+ stage = [{class: Writers::ContentWriter, args: [job_options]},
113
+ {class: Writers::AnotherContentWriter, args: [job_options]}]
114
+
115
+ remaining = mock('Remaining Jobs')
116
+ Pipes::Store.stub!(:remaining_jobs) { remaining }
117
+
118
+ remaining.should_receive(:clear)
119
+ remaining.should_receive(:incr).with(2)
120
+
121
+ Resque.should_receive(:enqueue).with(Writers::ContentWriter, 'en-US')
122
+ Resque.should_receive(:enqueue).with(Writers::AnotherContentWriter, 'en-US')
123
+
124
+ Pipes::Store.run_stage(stage)
125
+ end
126
+ end
127
+
128
+ describe '.done' do
129
+ context 'as the last job in a stage' do
130
+ it 'fires off the next stage' do
131
+ remaining = mock('Remaining Jobs', {decrement: 0}) { 1 }
132
+ Pipes::Store.stub!(:remaining_jobs) { remaining }
133
+
134
+ Pipes::Store.should_receive(:next_stage)
135
+ Pipes::Store.done
136
+ end
137
+ end
138
+
139
+ context 'with jobs still running' do
140
+ it 'decrements the remaining jobs but does not run the next stage' do
141
+ remaining = mock('Remaining Jobs') { 2 }
142
+ Pipes::Store.stub!(:remaining_jobs) { remaining }
143
+
144
+ remaining.should_receive(:decrement) { 1 }
145
+ Pipes::Store.should_not_receive(:next_stage)
146
+ Pipes::Store.done
147
+ end
148
+ end
149
+ end
150
+
151
+ describe '.clear' do
152
+ it 'clears out the jobs for the given stage' do
153
+ writers = mock_pending_jobs(:content_writers)
154
+ writers << {class: Writers::ContentWriter, args: [job_options]}
155
+
156
+ Pipes::Store.clear(:content_writers)
157
+ writers.should == []
158
+ end
159
+ end
160
+
161
+ describe '.clear_all' do
162
+ it 'deletes Redis keys for all stages, even those not currently being used' do
163
+ Redis::List.new('pipes:stages:some_stage_used_for_testing') << 'stategy'
164
+ Redis::List.new('pipes:stages:any_stage_test') << 'stategy'
165
+
166
+ Pipes::Store.clear_all
167
+
168
+ Redis::List.new('pipes:stages:some_stage_used_for_testing').should == []
169
+ Redis::List.new('pipes:stages:any_stage_test').should == []
170
+ end
171
+
172
+ it 'resets the remaining jobs counter' do
173
+ remaining = mock('Remaining Jobs')
174
+ Pipes::Store.stub!(:remaining_jobs) { remaining }
175
+
176
+ remaining.should_receive(:clear)
177
+
178
+ Pipes::Store.clear_all
179
+ end
180
+ end
181
+ end