gush 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gush::NullLogger do
4
+ let(:logger) { Gush::NullLogger.new }
5
+
6
+ it 'responds to logger methods and ignores them' do
7
+ [:info, :debug, :error, :fatal].each do |method|
8
+ logger.send(method, "message")
9
+ end
10
+ end
11
+
12
+ it 'works when block with message is passed' do
13
+ logger.info("progname") { "message" }
14
+ end
15
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gush::Worker do
4
+ let(:workflow_id) { '1234' }
5
+ let(:workflow) { TestWorkflow.new(workflow_id) }
6
+ let(:job) { workflow.find_job("Prepare") }
7
+ let(:config) { client.configuration.to_json }
8
+
9
+ before :each do
10
+ allow(client).to receive(:find_workflow).with(workflow_id).and_return(workflow)
11
+ allow(Gush::Client).to receive(:new).and_return(client)
12
+ end
13
+
14
+ describe "#perform" do
15
+ context "when job fails" do
16
+ before :each do
17
+ expect(job).to receive(:work).and_raise(StandardError)
18
+ job.enqueue!
19
+ job.start!
20
+ end
21
+
22
+ it "should mark it as failed" do
23
+ allow(client).to receive(:persist_job)
24
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
25
+
26
+ expect(client).to have_received(:persist_job).with(workflow_id, job).at_least(1).times do |_, job|
27
+ expect(job).to be_failed
28
+ end
29
+
30
+ end
31
+
32
+ it "reports that job failed" do
33
+ allow(client).to receive(:worker_report)
34
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
35
+ expect(client).to have_received(:worker_report).with(hash_including(status: :failed))
36
+ end
37
+
38
+ it "logs the exception" do
39
+ logger = TestLogger.new(1234, 'Prepare')
40
+ expect(logger).to receive(:<<).with(instance_of(String)).at_least(1).times
41
+ expect(workflow).to receive(:build_logger_for_job).and_return(logger)
42
+
43
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
44
+ end
45
+ end
46
+
47
+ context "when job completes successfully" do
48
+ it "should mark it as succedeed" do
49
+ allow(client).to receive(:persist_job)
50
+
51
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
52
+
53
+ expect(client).to have_received(:persist_job).at_least(1).times.with(workflow_id, job) do |_, job|
54
+ expect(job).to be_succeeded
55
+ end
56
+ end
57
+
58
+ it "reports that job succedeed" do
59
+ allow(client).to receive(:worker_report)
60
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
61
+
62
+ expect(client).to have_received(:worker_report).with(hash_including(status: :finished))
63
+ end
64
+ end
65
+
66
+ [:before_work, :work, :after_work].each do |method|
67
+ it "calls job.#{method} hook" do
68
+ expect(job).to receive(method)
69
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
70
+ end
71
+ end
72
+
73
+ it "sets up a logger for the job" do
74
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
75
+ job.start!
76
+ expect(job.logger).to be_a TestLogger
77
+ end
78
+
79
+ it "sets a job id" do
80
+ job_id = 1234
81
+ worker = Gush::Worker.new
82
+
83
+ allow(worker).to receive(:jid).and_return(job_id)
84
+
85
+ worker.perform(workflow_id, "Prepare", config)
86
+ job.enqueue!
87
+ expect(job.jid).to eq job_id
88
+ end
89
+
90
+ it "reports when the job is started" do
91
+ allow(client).to receive(:worker_report)
92
+ Gush::Worker.new.perform(workflow_id, "Prepare", config)
93
+ expect(client).to have_received(:worker_report).with(hash_including(status: :started))
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,246 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gush::Workflow do
4
+ subject { TestWorkflow.new("test-workflow") }
5
+
6
+ describe "#initialize" do
7
+ context "when configure option is true" do
8
+ it "runs #configure method " do
9
+ expect_any_instance_of(TestWorkflow).to receive(:configure)
10
+ TestWorkflow.new("name", configure: true)
11
+ end
12
+ end
13
+ end
14
+
15
+ describe "#stop!" do
16
+ it "marks workflow as stopped" do
17
+ expect{ subject.stop! }.to change{subject.stopped?}.from(false).to(true)
18
+ end
19
+ end
20
+
21
+ describe "#start!" do
22
+ it "removes stopped flag" do
23
+ subject.stopped = true
24
+ expect{ subject.start! }.to change{subject.stopped?}.from(true).to(false)
25
+ end
26
+ end
27
+
28
+ describe "#to_json" do
29
+ it "returns correct hash" do
30
+
31
+ klass = Class.new(Gush::Workflow) do
32
+ def configure
33
+ run FetchFirstJob
34
+ run PersistFirstJob, after: FetchFirstJob
35
+ end
36
+ end
37
+
38
+ result = JSON.parse(klass.new("workflow").to_json)
39
+ expected = {
40
+ "id"=>"workflow",
41
+ "name" => klass.to_s,
42
+ "klass" => klass.to_s,
43
+ "status" => "Pending",
44
+ "total" => 2,
45
+ "finished" => 0,
46
+ "started_at" => nil,
47
+ "finished_at" => nil,
48
+ "stopped" => false,
49
+ "logger_builder" => "Gush::LoggerBuilder",
50
+ "nodes" => [
51
+ {
52
+ "name"=>"FetchFirstJob", "klass"=>"FetchFirstJob", "finished"=>false, "enqueued"=>false, "failed"=>false,
53
+ "incoming"=>[], "outgoing"=>["PersistFirstJob"], "finished_at"=>nil, "started_at"=>nil, "failed_at"=>nil,
54
+ "running" => false
55
+ },
56
+ {
57
+ "name"=>"PersistFirstJob", "klass"=>"PersistFirstJob", "finished"=>false, "enqueued"=>false, "failed"=>false,
58
+ "incoming"=>["FetchFirstJob"], "outgoing"=>[], "finished_at"=>nil, "started_at"=>nil, "failed_at"=>nil,
59
+ "running" => false
60
+ }
61
+ ]
62
+ }
63
+ expect(result).to eq(expected)
64
+ end
65
+ end
66
+
67
+ describe "#find_job" do
68
+ it "finds job by its name" do
69
+ expect(TestWorkflow.new("test").find_job("PersistFirstJob")).to be_instance_of(PersistFirstJob)
70
+ end
71
+ end
72
+
73
+ describe "#run" do
74
+ context "when graph is empty" do
75
+ it "adds new job with the given class as a node" do
76
+ flow = Gush::Workflow.new("workflow")
77
+ flow.run(Gush::Job)
78
+ expect(flow.nodes.first).to be_instance_of(Gush::Job)
79
+ end
80
+ end
81
+
82
+ context "when last node is a job" do
83
+ it "attaches job as a child of the last inserted job" do
84
+ tree = Gush::Workflow.new("workflow")
85
+ klass1 = Class.new(Gush::Job)
86
+ klass2 = Class.new(Gush::Job)
87
+ tree.run(klass1)
88
+ tree.run(klass2, after: klass1)
89
+ tree.create_dependencies
90
+ expect(tree.nodes.first).to be_an_instance_of(klass1)
91
+ expect(tree.nodes.first.outgoing.first).to eq(klass2.to_s)
92
+ end
93
+ end
94
+ end
95
+
96
+ describe "#logger_builder" do
97
+ it 'sets logger builder for workflow' do
98
+ tree = Gush::Workflow.new("workflow")
99
+ tree.logger_builder(TestLoggerBuilder)
100
+ expect(tree.instance_variable_get(:@logger_builder)).to eq(TestLoggerBuilder)
101
+ end
102
+ end
103
+
104
+ describe "#build_logger_for_job" do
105
+ it 'builds a logger' do
106
+ job = double('job')
107
+ allow(job).to receive(:name) { 'a-job' }
108
+
109
+ tree = Gush::Workflow.new("workflow")
110
+ tree.logger_builder(TestLoggerBuilder)
111
+
112
+ logger = tree.build_logger_for_job(job, :jid)
113
+ expect(logger).to be_a(TestLogger)
114
+ expect(logger.jid).to eq(:jid)
115
+ expect(logger.name).to eq('a-job')
116
+ end
117
+ end
118
+
119
+ describe "#failed?" do
120
+ context "when one of the jobs failed" do
121
+ it "returns true" do
122
+ subject.find_job('Prepare').failed = true
123
+ expect(subject.failed?).to be_truthy
124
+ end
125
+ end
126
+
127
+ context "when no jobs failed" do
128
+ it "returns true" do
129
+ expect(subject.failed?).to be_falsy
130
+ end
131
+ end
132
+ end
133
+
134
+ describe "#running?" do
135
+ context "when no enqueued or running jobs" do
136
+ it "returns false" do
137
+ expect(subject.running?).to be_falsy
138
+ end
139
+ end
140
+
141
+ context "when some jobs are enqueued" do
142
+ it "returns true" do
143
+ subject.find_job('Prepare').enqueued = true
144
+ expect(subject.running?).to be_truthy
145
+ end
146
+ end
147
+
148
+ context "when some jobs are running" do
149
+ it "returns true" do
150
+ subject.find_job('Prepare').running = true
151
+ expect(subject.running?).to be_truthy
152
+ end
153
+ end
154
+ end
155
+
156
+ describe "#finished?" do
157
+ it "returns false if any jobs are unfinished" do
158
+ expect(subject.finished?).to be_falsy
159
+ end
160
+
161
+ it "returns true if all jobs are finished" do
162
+ subject.nodes.each {|n| n.finished = true }
163
+ expect(subject.finished?).to be_truthy
164
+ end
165
+ end
166
+
167
+ describe "#next_jobs" do
168
+ context "when one of the dependent jobs failed" do
169
+ it "returns only jobs with satisfied dependencies" do
170
+ subject.find_job('Prepare').finished = true
171
+ subject.find_job('FetchFirstJob').failed = true
172
+ expect(subject.next_jobs.map(&:name)).to match_array(["FetchSecondJob"])
173
+ end
174
+ end
175
+
176
+ it "returns next non-queued and unfinished jobs" do
177
+ expect(subject.next_jobs.map(&:name)).to match_array(["Prepare"])
178
+ end
179
+
180
+ it "returns all parallel non-queued and unfinished jobs" do
181
+ subject.find_job('Prepare').finished = true
182
+ expect(subject.next_jobs.map(&:name)).to match_array(["FetchFirstJob", "FetchSecondJob"])
183
+ end
184
+
185
+ it "returns empty array when there are enqueued but unfinished jobs" do
186
+ subject.find_job('Prepare').enqueued = true
187
+ expect(subject.next_jobs).to match_array([])
188
+ end
189
+
190
+ it "returns only unfinished and non-queued jobs from a parallel level" do
191
+ subject.find_job('Prepare').finished = true
192
+ subject.find_job('FetchFirstJob').finished = true
193
+ expect(subject.next_jobs.map(&:name)).to match_array(["PersistFirstJob", "FetchSecondJob"])
194
+ end
195
+
196
+ it "returns next level of unfished jobs after finished parallel level" do
197
+ subject.find_job('Prepare').finished = true
198
+ subject.find_job('PersistFirstJob').finished = true
199
+ subject.find_job('FetchFirstJob').finished = true
200
+ subject.find_job('FetchSecondJob').finished = true
201
+ expect(subject.next_jobs.map(&:name)).to match_array(["NormalizeJob"])
202
+ end
203
+
204
+ context "when mixing parallel tasks with synchronous" do
205
+ it "properly orders nested synchronous flows inside concurrent" do
206
+ flow = Gush::Workflow.new("workflow")
207
+
208
+ flow.run Prepare
209
+ flow.run NormalizeJob
210
+
211
+ flow.run FetchFirstJob, after: Prepare
212
+ flow.run PersistFirstJob, after: FetchFirstJob, before: NormalizeJob
213
+ flow.run FetchSecondJob, after: Prepare
214
+ flow.run PersistSecondJob, after: FetchSecondJob, before: NormalizeJob
215
+
216
+ flow.create_dependencies
217
+ expect(flow.next_jobs.map(&:name)).to match_array(["Prepare"])
218
+ flow.find_job("Prepare").finished = true
219
+ expect(flow.next_jobs.map(&:name)).to match_array(["FetchFirstJob", "FetchSecondJob"])
220
+ flow.find_job("FetchFirstJob").finished = true
221
+ expect(flow.next_jobs.map(&:name)).to match_array(["FetchSecondJob", "PersistFirstJob"])
222
+ flow.find_job("FetchSecondJob").finished = true
223
+ expect(flow.next_jobs.map(&:name)).to match_array(["PersistFirstJob", "PersistSecondJob"])
224
+ flow.find_job("PersistFirstJob").finished = true
225
+ expect(flow.next_jobs.map(&:name)).to match_array(["PersistSecondJob"])
226
+ flow.find_job("PersistSecondJob").finished = true
227
+ expect(flow.next_jobs.map(&:name)).to match_array(["NormalizeJob"])
228
+ end
229
+ end
230
+
231
+ it "fails when dependency resolution recurses too deep" do
232
+ flow = Gush::Workflow.new("workflow")
233
+ klass1 = Class.new(Gush::Job)
234
+ klass2 = Class.new(Gush::Job)
235
+ klass3 = Class.new(Gush::Job)
236
+ flow.run(klass1, after: klass3)
237
+ flow.run(klass2, after: klass1)
238
+ flow.run(klass3, after: klass2)
239
+ flow.create_dependencies
240
+
241
+ expect {
242
+ flow.next_jobs
243
+ }.to raise_error(DependencyLevelTooDeep)
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gush do
4
+ describe ".gushfile" do
5
+ let(:path) { Pathname("/tmp/Gushfile.rb") }
6
+
7
+ context "Gushfile.rb is missing from pwd" do
8
+ it "raises an exception" do
9
+ path.delete if path.exist?
10
+ Gush.configuration.gushfile = path
11
+
12
+ expect { Gush.gushfile }.to raise_error(Thor::Error)
13
+ end
14
+ end
15
+
16
+ context "Gushfile.rb exists" do
17
+ it "returns Pathname to it" do
18
+ FileUtils.touch(path)
19
+ Gush.configuration.gushfile = path
20
+ expect(Gush.gushfile).to eq(path.realpath)
21
+ path.delete
22
+ end
23
+ end
24
+ end
25
+
26
+ describe ".root" do
27
+ it "returns root directory of Gush" do
28
+ expected = Pathname.new(__FILE__).parent.parent.parent
29
+ expect(Gush.root).to eq(expected)
30
+ end
31
+ end
32
+
33
+ describe ".configure" do
34
+ it "runs block with config instance passed" do
35
+ expect { |b| Gush.configure(&b) }.to yield_with_args(Gush.configuration)
36
+ end
37
+ end
38
+
39
+ end
data/spec/redis.conf ADDED
@@ -0,0 +1,2 @@
1
+ port 33333
2
+ bind 127.0.0.1
@@ -0,0 +1,79 @@
1
+ require 'gush'
2
+ require 'pry'
3
+ require 'sidekiq/testing'
4
+ require 'bbq/spawn'
5
+
6
+ Sidekiq::Logging.logger = nil
7
+
8
+ class Prepare < Gush::Job; end
9
+ class FetchFirstJob < Gush::Job; end
10
+ class FetchSecondJob < Gush::Job; end
11
+ class PersistFirstJob < Gush::Job; end
12
+ class PersistSecondJob < Gush::Job; end
13
+ class NormalizeJob < Gush::Job; end
14
+
15
+ GUSHFILE = Pathname.new(__FILE__).parent.join("Gushfile.rb")
16
+
17
+ TestLogger = Struct.new(:jid, :name) do
18
+ def <<(msg)
19
+ end
20
+ end
21
+
22
+ class TestLoggerBuilder < Gush::LoggerBuilder
23
+ def build
24
+ TestLogger.new(jid, job.name)
25
+ end
26
+ end
27
+
28
+ class TestWorkflow < Gush::Workflow
29
+ def configure
30
+ logger_builder TestLoggerBuilder
31
+
32
+ run Prepare
33
+
34
+ run NormalizeJob
35
+
36
+ run FetchFirstJob, after: Prepare
37
+ run PersistFirstJob, after: FetchFirstJob, before: NormalizeJob
38
+
39
+ run FetchSecondJob, after: Prepare, before: NormalizeJob
40
+ end
41
+ end
42
+
43
+ module GushHelpers
44
+ REDIS_URL = "redis://localhost:33333/"
45
+
46
+ def redis
47
+ @redis ||= Redis.new(url: REDIS_URL)
48
+ end
49
+
50
+ def client
51
+ @client ||= Gush::Client.new(Gush::Configuration.new(gushfile: GUSHFILE, redis_url: REDIS_URL))
52
+ end
53
+ end
54
+
55
+ RSpec.configure do |config|
56
+ config.include GushHelpers
57
+
58
+ config.mock_with :rspec do |mocks|
59
+ mocks.verify_partial_doubles = true
60
+ end
61
+
62
+ orchestrator = Bbq::Spawn::Orchestrator.new
63
+
64
+ config.before(:suite) do
65
+ config_path = Pathname.pwd + "spec/redis.conf"
66
+ executor = Bbq::Spawn::Executor.new("redis-server", config_path.to_path)
67
+ orchestrator.coordinate(executor, host: '127.0.0.1', port: 33333)
68
+ orchestrator.start
69
+ end
70
+
71
+ config.after(:suite) do
72
+ orchestrator.stop
73
+ end
74
+
75
+ config.after(:each) do
76
+ Sidekiq::Worker.clear_all
77
+ redis.flushdb
78
+ end
79
+ end