dtsato-pipeline 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.
- data/.gitignore +7 -0
- data/CHANGELOG +12 -0
- data/LICENSE +20 -0
- data/README.rdoc +67 -0
- data/Rakefile +104 -0
- data/VERSION +1 -0
- data/examples/auto_recoverable_pipeline.rb +35 -0
- data/examples/cancelling_pipeline.rb +32 -0
- data/examples/two_step_pipeline.rb +27 -0
- data/examples/user_recoverable_pipeline.rb +33 -0
- data/generators/pipeline/pipeline_generator.rb +12 -0
- data/generators/pipeline/templates/pipeline_instances_migration.rb +16 -0
- data/generators/pipeline/templates/pipeline_stages_migration.rb +19 -0
- data/init.rb +1 -0
- data/lib/pipeline/api_methods.rb +23 -0
- data/lib/pipeline/base.rb +67 -0
- data/lib/pipeline/core_ext/symbol_attribute.rb +29 -0
- data/lib/pipeline/core_ext/transactional_attribute.rb +26 -0
- data/lib/pipeline/errors.rb +24 -0
- data/lib/pipeline/stage/base.rb +60 -0
- data/lib/pipeline.rb +16 -0
- data/pipeline.gemspec +86 -0
- data/spec/database_integration_helper.rb +42 -0
- data/spec/pipeline/api_methods_spec.rb +98 -0
- data/spec/pipeline/base_spec.rb +383 -0
- data/spec/pipeline/core_ext/symbol_attribute_spec.rb +38 -0
- data/spec/pipeline/core_ext/transactional_attribute_spec.rb +22 -0
- data/spec/pipeline/errors_spec.rb +46 -0
- data/spec/pipeline/stage/base_spec.rb +203 -0
- data/spec/rcov.opts +1 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +10 -0
- metadata +119 -0
data/pipeline.gemspec
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{pipeline}
|
5
|
+
s.version = "0.0.1"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Danilo Sato"]
|
9
|
+
s.date = %q{2009-07-30}
|
10
|
+
s.description = %q{Pipeline is a Rails plugin/gem to run asynchronous processes in a configurable pipeline.}
|
11
|
+
s.email = %q{danilo@dtsato.com}
|
12
|
+
s.extra_rdoc_files = [
|
13
|
+
"README.rdoc"
|
14
|
+
]
|
15
|
+
s.files = [
|
16
|
+
".gitignore",
|
17
|
+
"CHANGELOG",
|
18
|
+
"LICENSE",
|
19
|
+
"README.rdoc",
|
20
|
+
"Rakefile",
|
21
|
+
"VERSION",
|
22
|
+
"examples/auto_recoverable_pipeline.rb",
|
23
|
+
"examples/cancelling_pipeline.rb",
|
24
|
+
"examples/two_step_pipeline.rb",
|
25
|
+
"examples/user_recoverable_pipeline.rb",
|
26
|
+
"generators/pipeline/pipeline_generator.rb",
|
27
|
+
"generators/pipeline/templates/pipeline_instances_migration.rb",
|
28
|
+
"generators/pipeline/templates/pipeline_stages_migration.rb",
|
29
|
+
"init.rb",
|
30
|
+
"lib/pipeline.rb",
|
31
|
+
"lib/pipeline/api_methods.rb",
|
32
|
+
"lib/pipeline/base.rb",
|
33
|
+
"lib/pipeline/core_ext/symbol_attribute.rb",
|
34
|
+
"lib/pipeline/core_ext/transactional_attribute.rb",
|
35
|
+
"lib/pipeline/errors.rb",
|
36
|
+
"lib/pipeline/stage/base.rb",
|
37
|
+
"pipeline.gemspec",
|
38
|
+
"spec/database_integration_helper.rb",
|
39
|
+
"spec/pipeline/api_methods_spec.rb",
|
40
|
+
"spec/pipeline/base_spec.rb",
|
41
|
+
"spec/pipeline/core_ext/symbol_attribute_spec.rb",
|
42
|
+
"spec/pipeline/core_ext/transactional_attribute_spec.rb",
|
43
|
+
"spec/pipeline/errors_spec.rb",
|
44
|
+
"spec/pipeline/stage/base_spec.rb",
|
45
|
+
"spec/rcov.opts",
|
46
|
+
"spec/spec.opts",
|
47
|
+
"spec/spec_helper.rb"
|
48
|
+
]
|
49
|
+
s.homepage = %q{http://github.com/dtsato/pipeline}
|
50
|
+
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
|
51
|
+
s.require_paths = ["lib"]
|
52
|
+
s.rubyforge_project = %q{pipeline}
|
53
|
+
s.rubygems_version = %q{1.3.4}
|
54
|
+
s.summary = %q{A Rails plugin/gem to run asynchronous processes in a configurable pipeline}
|
55
|
+
s.test_files = [
|
56
|
+
"spec/database_integration_helper.rb",
|
57
|
+
"spec/pipeline",
|
58
|
+
"spec/pipeline/api_methods_spec.rb",
|
59
|
+
"spec/pipeline/base_spec.rb",
|
60
|
+
"spec/pipeline/core_ext",
|
61
|
+
"spec/pipeline/core_ext/symbol_attribute_spec.rb",
|
62
|
+
"spec/pipeline/core_ext/transactional_attribute_spec.rb",
|
63
|
+
"spec/pipeline/errors_spec.rb",
|
64
|
+
"spec/pipeline/stage",
|
65
|
+
"spec/pipeline/stage/base_spec.rb",
|
66
|
+
"spec/rcov.opts",
|
67
|
+
"spec/spec.opts",
|
68
|
+
"spec/spec_helper.rb"
|
69
|
+
]
|
70
|
+
|
71
|
+
if s.respond_to? :specification_version then
|
72
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
73
|
+
s.specification_version = 3
|
74
|
+
|
75
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
76
|
+
s.add_runtime_dependency(%q<activerecord>, [">= 2.0"])
|
77
|
+
s.add_runtime_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
|
78
|
+
else
|
79
|
+
s.add_dependency(%q<activerecord>, [">= 2.0"])
|
80
|
+
s.add_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
|
81
|
+
end
|
82
|
+
else
|
83
|
+
s.add_dependency(%q<activerecord>, [">= 2.0"])
|
84
|
+
s.add_dependency(%q<collectiveidea-delayed_job>, [">= 1.8.0"])
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
gem 'sqlite3-ruby'
|
3
|
+
|
4
|
+
ActiveRecord::Base.establish_connection(:adapter => 'sqlite3', :database => 'pipeline.sqlite')
|
5
|
+
ActiveRecord::Migration.verbose = false
|
6
|
+
|
7
|
+
ActiveRecord::Schema.define do
|
8
|
+
|
9
|
+
create_table :delayed_jobs, :force => true do |table|
|
10
|
+
table.integer :priority, :default => 0
|
11
|
+
table.integer :attempts, :default => 0
|
12
|
+
table.text :handler
|
13
|
+
table.string :last_error
|
14
|
+
table.datetime :run_at
|
15
|
+
table.datetime :locked_at
|
16
|
+
table.string :locked_by
|
17
|
+
table.datetime :failed_at
|
18
|
+
table.timestamps
|
19
|
+
end
|
20
|
+
|
21
|
+
create_table :pipeline_instances, :force => true do |t|
|
22
|
+
t.string :type
|
23
|
+
t.string :status
|
24
|
+
t.integer :attempts, :default => 0
|
25
|
+
t.timestamps
|
26
|
+
end
|
27
|
+
|
28
|
+
create_table :pipeline_stages, :force => true do |t|
|
29
|
+
t.references :pipeline_instance
|
30
|
+
t.string :type
|
31
|
+
t.string :name
|
32
|
+
t.string :status
|
33
|
+
t.text :message
|
34
|
+
t.integer :attempts, :default => 0
|
35
|
+
t.timestamps
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
at_exit do
|
41
|
+
File.delete("pipeline.sqlite") if File.exists?("pipeline.sqlite")
|
42
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
module Pipeline
|
4
|
+
describe ApiMethods do
|
5
|
+
class FakePipeline < Pipeline::Base
|
6
|
+
end
|
7
|
+
|
8
|
+
describe "#start" do
|
9
|
+
before(:each) do
|
10
|
+
@pipeline = FakePipeline.new
|
11
|
+
@pipeline.stub!(:save!)
|
12
|
+
Delayed::Job.stub!(:enqueue)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should only accept instance of Pipeline::Base" do
|
16
|
+
lambda {Pipeline.start(@pipeline)}.should_not raise_error
|
17
|
+
lambda {Pipeline.start(Object.new)}.should raise_error(InvalidPipelineError, "Invalid pipeline")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should save pipeline instance" do
|
21
|
+
@pipeline.should_receive(:save!)
|
22
|
+
|
23
|
+
Pipeline.start(@pipeline)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should start a job for a pipeline instance" do
|
27
|
+
Delayed::Job.should_receive(:enqueue).with(@pipeline)
|
28
|
+
|
29
|
+
Pipeline.start(@pipeline)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should provide a token for the pipeline instance" do
|
33
|
+
@pipeline.stub!(:id).and_return('123')
|
34
|
+
|
35
|
+
token = Pipeline.start(@pipeline)
|
36
|
+
token.should == '123'
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "#resume" do
|
41
|
+
before(:each) do
|
42
|
+
@pipeline = Pipeline::Base.new
|
43
|
+
Pipeline::Base.stub!(:find).with('1').and_return(@pipeline)
|
44
|
+
Delayed::Job.stub!(:enqueue)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should accept a token for a pipeline instance" do
|
48
|
+
Pipeline::Base.should_receive(:find).with('1')
|
49
|
+
|
50
|
+
Pipeline.resume('1')
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should raise error is trying to resume invalid pipeline" do
|
54
|
+
Pipeline::Base.should_receive(:find).and_return(nil)
|
55
|
+
|
56
|
+
lambda {Pipeline.resume('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should start a new job for that pipeline instance" do
|
60
|
+
Delayed::Job.should_receive(:enqueue).with(@pipeline)
|
61
|
+
|
62
|
+
Pipeline.resume('1')
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should raise error is trying to resume in invalid state" do
|
66
|
+
@pipeline.send(:_setup)
|
67
|
+
|
68
|
+
lambda {Pipeline.resume('1')}.should raise_error(InvalidStatusError, "Status is already in progress")
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#cancel" do
|
74
|
+
before(:each) do
|
75
|
+
@pipeline = Pipeline::Base.new
|
76
|
+
@pipeline.stub!(:cancel)
|
77
|
+
Pipeline::Base.stub!(:find).with('1').and_return(@pipeline)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should accept a token for a pipeline instance" do
|
81
|
+
Pipeline::Base.should_receive(:find).with('1')
|
82
|
+
Pipeline.cancel('1')
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should raise error is trying to cancel invalid pipeline" do
|
86
|
+
Pipeline::Base.should_receive(:find).and_return(nil)
|
87
|
+
|
88
|
+
lambda {Pipeline.cancel('1')}.should raise_error(InvalidPipelineError, "Invalid pipeline")
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should cancel pipeline instance" do
|
92
|
+
@pipeline.should_receive(:cancel)
|
93
|
+
Pipeline.cancel('1')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,383 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
class FirstStage < Pipeline::Stage::Base
|
4
|
+
def run
|
5
|
+
@executed = true
|
6
|
+
end
|
7
|
+
|
8
|
+
def executed?
|
9
|
+
!!@executed
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class SecondStage < FirstStage; end # Ugly.. just so I don't have to write stub again
|
14
|
+
|
15
|
+
class SamplePipeline < Pipeline::Base
|
16
|
+
define_stages FirstStage >> SecondStage
|
17
|
+
end
|
18
|
+
|
19
|
+
module Pipeline
|
20
|
+
describe Base do
|
21
|
+
|
22
|
+
describe "- configuring" do
|
23
|
+
it "should allow accessing stages" do
|
24
|
+
SamplePipeline.defined_stages.should == [FirstStage, SecondStage]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "- setup" do
|
29
|
+
before(:each) do
|
30
|
+
@pipeline = SamplePipeline.new
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should start with status not_started" do
|
34
|
+
@pipeline.status.should == :not_started
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should instantiate stages with status not_started" do
|
38
|
+
@pipeline.stages.each { |stage| stage.status.should == :not_started }
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should validate status" do
|
42
|
+
lambda {Base.new(:status => :something_else)}.should raise_error
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "- persistence" do
|
47
|
+
before(:each) do
|
48
|
+
@pipeline = Base.new
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should persist pipeline instance" do
|
52
|
+
@pipeline.id.should be_nil
|
53
|
+
lambda {@pipeline.save!}.should_not raise_error
|
54
|
+
@pipeline.id.should_not be_nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should allow retrieval by id" do
|
58
|
+
@pipeline.save!
|
59
|
+
|
60
|
+
retrieved_pipeline = Base.find(@pipeline.id.to_s)
|
61
|
+
retrieved_pipeline.should === @pipeline
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should persist type as single table inheritance" do
|
65
|
+
pipeline = SamplePipeline.new
|
66
|
+
pipeline.save!
|
67
|
+
|
68
|
+
retrieved_pipeline = Base.find(pipeline.id)
|
69
|
+
retrieved_pipeline.should be_an_instance_of(SamplePipeline)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should persist pipeline stages" do
|
73
|
+
pipeline = SamplePipeline.new
|
74
|
+
pipeline.stages.each {|stage| stage.id.should be_nil}
|
75
|
+
lambda {pipeline.save!}.should_not raise_error
|
76
|
+
pipeline.stages.each {|stage| stage.id.should_not be_nil}
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should allow retrieval of stages with pipeline instance" do
|
80
|
+
pipeline = SamplePipeline.new
|
81
|
+
pipeline.save!
|
82
|
+
|
83
|
+
retrieved_pipeline = SamplePipeline.find(pipeline.id)
|
84
|
+
retrieved_pipeline.stages.should === pipeline.stages
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should associate stages with pipeline instance" do
|
88
|
+
pipeline = SamplePipeline.new
|
89
|
+
pipeline.save!
|
90
|
+
|
91
|
+
pipeline.stages.each {|stage| stage.pipeline.should === pipeline}
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should destroy stages when pipeline instance is destroyed" do
|
95
|
+
pipeline = SamplePipeline.new
|
96
|
+
pipeline.save!
|
97
|
+
|
98
|
+
Pipeline::Stage::Base.count(:conditions => ['pipeline_instance_id = ?', pipeline.id]).should > 0
|
99
|
+
|
100
|
+
pipeline.destroy
|
101
|
+
Pipeline::Stage::Base.count(:conditions => ['pipeline_instance_id = ?', pipeline.id]).should == 0
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "- execution (success)" do
|
106
|
+
before(:each) do
|
107
|
+
@pipeline = SamplePipeline.new
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should increment attempts" do
|
111
|
+
failed_stage = SecondStage.new
|
112
|
+
failed_stage.stub!(:run).and_raise(RecoverableError.new("message", true))
|
113
|
+
SecondStage.stub!(:new).and_return(failed_stage)
|
114
|
+
|
115
|
+
pipeline = SamplePipeline.new
|
116
|
+
|
117
|
+
pipeline.perform
|
118
|
+
pipeline.attempts.should == 1
|
119
|
+
|
120
|
+
pipeline.perform
|
121
|
+
pipeline.attempts.should == 2
|
122
|
+
end
|
123
|
+
|
124
|
+
it "should perform each stage" do
|
125
|
+
@pipeline.stages.each { |stage| stage.should_not be_executed }
|
126
|
+
@pipeline.perform
|
127
|
+
@pipeline.stages.each { |stage| stage.should be_executed }
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should update pipeline status after all stages finished" do
|
131
|
+
@pipeline.perform
|
132
|
+
@pipeline.status.should == :completed
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should save status" do
|
136
|
+
@pipeline.save!
|
137
|
+
@pipeline.perform
|
138
|
+
@pipeline.reload.status.should == :completed
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "- execution (in progress)" do
|
143
|
+
it "should set status to in_progress" do
|
144
|
+
pipeline = SamplePipeline.new
|
145
|
+
pipeline.send(:_setup)
|
146
|
+
|
147
|
+
pipeline.status.should == :in_progress
|
148
|
+
pipeline.reload.status.should == :in_progress
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "- execution (irrecoverable error)" do
|
153
|
+
before(:each) do
|
154
|
+
failed_stage = SecondStage.new
|
155
|
+
failed_stage.stub!(:run).and_raise(IrrecoverableError.new)
|
156
|
+
SecondStage.stub!(:new).and_return(failed_stage)
|
157
|
+
@pipeline = SamplePipeline.new
|
158
|
+
end
|
159
|
+
|
160
|
+
it "should not re-raise error" do
|
161
|
+
lambda {@pipeline.perform}.should_not raise_error(IrrecoverableError)
|
162
|
+
end
|
163
|
+
|
164
|
+
it "should update status" do
|
165
|
+
@pipeline.perform
|
166
|
+
@pipeline.status.should == :failed
|
167
|
+
end
|
168
|
+
|
169
|
+
it "should save status" do
|
170
|
+
@pipeline.save!
|
171
|
+
@pipeline.perform
|
172
|
+
@pipeline.reload.status.should == :failed
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
describe "- execution (recoverable error that doesn't require user input)" do
|
177
|
+
before(:each) do
|
178
|
+
failed_stage = SecondStage.new
|
179
|
+
failed_stage.stub!(:run).and_raise(RecoverableError.new)
|
180
|
+
SecondStage.stub!(:new).and_return(failed_stage)
|
181
|
+
@pipeline = SamplePipeline.new
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should re-raise error (so delayed_job retry works)" do
|
185
|
+
lambda {@pipeline.perform}.should raise_error(RecoverableError)
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should keep status :in_progress" do
|
189
|
+
lambda {@pipeline.perform}.should raise_error(RecoverableError)
|
190
|
+
@pipeline.status.should == :in_progress
|
191
|
+
end
|
192
|
+
|
193
|
+
it "should save status" do
|
194
|
+
@pipeline.save!
|
195
|
+
lambda {@pipeline.perform}.should raise_error(RecoverableError)
|
196
|
+
@pipeline.reload.status.should == :in_progress
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe "- execution (recoverable error that requires user input)" do
|
201
|
+
before(:each) do
|
202
|
+
failed_stage = SecondStage.new
|
203
|
+
failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
|
204
|
+
SecondStage.stub!(:new).and_return(failed_stage)
|
205
|
+
@pipeline = SamplePipeline.new
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should not re-raise error" do
|
209
|
+
lambda {@pipeline.perform}.should_not raise_error(RecoverableError)
|
210
|
+
end
|
211
|
+
|
212
|
+
it "should update status" do
|
213
|
+
@pipeline.perform
|
214
|
+
@pipeline.status.should == :paused
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should save status" do
|
218
|
+
@pipeline.save!
|
219
|
+
@pipeline.perform
|
220
|
+
@pipeline.reload.status.should == :paused
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
describe "- execution (other errors will pause the pipeline)" do
|
225
|
+
before(:each) do
|
226
|
+
failed_stage = SecondStage.new
|
227
|
+
failed_stage.stub!(:run).and_raise(StandardError.new)
|
228
|
+
SecondStage.stub!(:new).and_return(failed_stage)
|
229
|
+
@pipeline = SamplePipeline.new
|
230
|
+
end
|
231
|
+
|
232
|
+
it "should not re-raise error" do
|
233
|
+
lambda {@pipeline.perform}.should_not raise_error(StandardError)
|
234
|
+
end
|
235
|
+
|
236
|
+
it "should update status" do
|
237
|
+
@pipeline.perform
|
238
|
+
@pipeline.status.should == :paused
|
239
|
+
end
|
240
|
+
|
241
|
+
it "should save status" do
|
242
|
+
@pipeline.save!
|
243
|
+
@pipeline.perform
|
244
|
+
@pipeline.reload.status.should == :paused
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
describe "- execution (retrying)" do
|
249
|
+
before(:each) do
|
250
|
+
@passing_stage = FirstStage.new
|
251
|
+
FirstStage.stub!(:new).and_return(@passing_stage)
|
252
|
+
|
253
|
+
@failed_stage = SecondStage.new
|
254
|
+
@failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
|
255
|
+
SecondStage.stub!(:new).and_return(@failed_stage)
|
256
|
+
@pipeline = SamplePipeline.new
|
257
|
+
end
|
258
|
+
|
259
|
+
it "should not re-raise error" do
|
260
|
+
lambda {@pipeline.perform}.should_not raise_error(RecoverableError)
|
261
|
+
end
|
262
|
+
|
263
|
+
it "should update status" do
|
264
|
+
@pipeline.perform
|
265
|
+
@pipeline.status.should == :paused
|
266
|
+
end
|
267
|
+
|
268
|
+
it "should save status" do
|
269
|
+
@pipeline.save!
|
270
|
+
@pipeline.perform
|
271
|
+
@pipeline.reload.status.should == :paused
|
272
|
+
end
|
273
|
+
|
274
|
+
it "should skip completed stages" do
|
275
|
+
@pipeline.perform
|
276
|
+
@passing_stage.attempts.should == 1
|
277
|
+
@failed_stage.attempts.should == 1
|
278
|
+
|
279
|
+
@pipeline.perform
|
280
|
+
@passing_stage.attempts.should == 1
|
281
|
+
@failed_stage.attempts.should == 2
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
describe "- execution (state transitions)" do
|
286
|
+
before(:each) do
|
287
|
+
@pipeline = Base.new
|
288
|
+
end
|
289
|
+
|
290
|
+
it "should execute if status is :not_started" do
|
291
|
+
@pipeline.should be_ok_to_resume
|
292
|
+
lambda {@pipeline.perform}.should_not raise_error(InvalidStatusError)
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should execute if status is :paused (for retrying)" do
|
296
|
+
@pipeline.send(:status=, :paused)
|
297
|
+
|
298
|
+
@pipeline.should be_ok_to_resume
|
299
|
+
lambda {@pipeline.perform}.should_not raise_error(InvalidStatusError)
|
300
|
+
end
|
301
|
+
|
302
|
+
it "should not execute if status is :in_progress" do
|
303
|
+
@pipeline.send(:status=, :in_progress)
|
304
|
+
|
305
|
+
@pipeline.should_not be_ok_to_resume
|
306
|
+
lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already in progress")
|
307
|
+
end
|
308
|
+
|
309
|
+
it "should not execute if status is :completed" do
|
310
|
+
@pipeline.send(:status=, :completed)
|
311
|
+
|
312
|
+
@pipeline.should_not be_ok_to_resume
|
313
|
+
lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already completed")
|
314
|
+
end
|
315
|
+
|
316
|
+
it "should not execute if status is :failed" do
|
317
|
+
@pipeline.send(:status=, :failed)
|
318
|
+
|
319
|
+
@pipeline.should_not be_ok_to_resume
|
320
|
+
lambda {@pipeline.perform}.should raise_error(InvalidStatusError, "Status is already failed")
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
describe "- cancelling" do
|
325
|
+
before(:each) do
|
326
|
+
@passing_stage = FirstStage.new
|
327
|
+
FirstStage.stub!(:new).and_return(@passing_stage)
|
328
|
+
|
329
|
+
@failed_stage = SecondStage.new
|
330
|
+
@failed_stage.stub!(:run).and_raise(RecoverableError.new('message', true))
|
331
|
+
SecondStage.stub!(:new).and_return(@failed_stage)
|
332
|
+
@pipeline = SamplePipeline.new
|
333
|
+
@pipeline.perform
|
334
|
+
end
|
335
|
+
|
336
|
+
it "should update status" do
|
337
|
+
@pipeline.cancel
|
338
|
+
@pipeline.status.should == :failed
|
339
|
+
end
|
340
|
+
|
341
|
+
it "should save status" do
|
342
|
+
@pipeline.save!
|
343
|
+
@pipeline.cancel
|
344
|
+
@pipeline.reload.status.should == :failed
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
describe "- cancelling (state transitions)" do
|
349
|
+
before(:each) do
|
350
|
+
@pipeline = Base.new
|
351
|
+
end
|
352
|
+
|
353
|
+
it "should cancel if status is :not_started" do
|
354
|
+
lambda {@pipeline.cancel}.should_not raise_error(InvalidStatusError)
|
355
|
+
end
|
356
|
+
|
357
|
+
it "should cancel if status is :paused (for retrying)" do
|
358
|
+
@pipeline.send(:status=, :paused)
|
359
|
+
|
360
|
+
lambda {@pipeline.cancel}.should_not raise_error(InvalidStatusError)
|
361
|
+
end
|
362
|
+
|
363
|
+
it "should not cancel if status is :in_progress" do
|
364
|
+
@pipeline.send(:status=, :in_progress)
|
365
|
+
|
366
|
+
lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already in progress")
|
367
|
+
end
|
368
|
+
|
369
|
+
it "should not cancel if status is :completed" do
|
370
|
+
@pipeline.send(:status=, :completed)
|
371
|
+
|
372
|
+
lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already completed")
|
373
|
+
end
|
374
|
+
|
375
|
+
it "should not cancel if status is :failed" do
|
376
|
+
@pipeline.send(:status=, :failed)
|
377
|
+
|
378
|
+
lambda {@pipeline.cancel}.should raise_error(InvalidStatusError, "Status is already failed")
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
end
|
383
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
|
+
|
3
|
+
# Reusing stage table to simplify database integration tests
|
4
|
+
class FakeForSymbolAttribute < ActiveRecord::Base
|
5
|
+
set_table_name :pipeline_stages
|
6
|
+
|
7
|
+
symbol_attr :status
|
8
|
+
end
|
9
|
+
|
10
|
+
module Pipeline
|
11
|
+
describe SymbolAttribute do
|
12
|
+
before(:each) do
|
13
|
+
FakeForSymbolAttribute.delete_all
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should extend active record to allow symbol attributes to be saved as string" do
|
17
|
+
obj = FakeForSymbolAttribute.new(:status => 'started')
|
18
|
+
obj.save!
|
19
|
+
obj.status.should == :started
|
20
|
+
obj.reload.status.should == :started
|
21
|
+
|
22
|
+
obj.status = 'finished'
|
23
|
+
obj.save!
|
24
|
+
obj.status.should == :finished
|
25
|
+
obj.reload.status.should == :finished
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should extend Symbol to allow symbol attributes in conditions" do
|
29
|
+
objs = FakeForSymbolAttribute.find(:all, :conditions => ['status = ?', :started])
|
30
|
+
objs.should be_empty
|
31
|
+
|
32
|
+
FakeForSymbolAttribute.create(:status => :started)
|
33
|
+
|
34
|
+
objs = FakeForSymbolAttribute.find(:all, :conditions => ['status = ?', :started])
|
35
|
+
objs.size.should == 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../../spec_helper'
|
2
|
+
|
3
|
+
# Reusing stage table to simplify database integration tests
|
4
|
+
class FakeForTransactionalAttribute < ActiveRecord::Base
|
5
|
+
set_table_name :pipeline_stages
|
6
|
+
|
7
|
+
transactional_attr :status
|
8
|
+
end
|
9
|
+
|
10
|
+
module Pipeline
|
11
|
+
describe TransactionalAttribute do
|
12
|
+
it "should extend active record to allow transactional attributes to be saved in a nested transaction" do
|
13
|
+
obj = FakeForTransactionalAttribute.create(:status => "started")
|
14
|
+
obj.status.should == "started"
|
15
|
+
obj.reload.status.should == "started"
|
16
|
+
|
17
|
+
obj.status = "finished"
|
18
|
+
obj.status.should == "finished"
|
19
|
+
obj.reload.status.should == "finished"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|