pipes 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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