gush 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.
@@ -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