mamiya 0.0.1.alpha21 → 0.0.1.alpha22

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/example/.gitignore +5 -0
  3. data/example/Procfile +1 -1
  4. data/example/README.md +83 -0
  5. data/example/config.agent.rb +40 -0
  6. data/example/config.rb +20 -6
  7. data/example/deploy.rb +27 -11
  8. data/example/source/README.md +1 -0
  9. data/lib/mamiya/agent/actions.rb +8 -3
  10. data/lib/mamiya/agent/task_queue.rb +9 -0
  11. data/lib/mamiya/agent/tasks/abstract.rb +13 -0
  12. data/lib/mamiya/agent/tasks/clean.rb +36 -4
  13. data/lib/mamiya/agent/tasks/fetch.rb +1 -0
  14. data/lib/mamiya/agent/tasks/notifyable.rb +0 -1
  15. data/lib/mamiya/agent/tasks/prepare.rb +103 -0
  16. data/lib/mamiya/agent.rb +46 -12
  17. data/lib/mamiya/cli/client.rb +35 -7
  18. data/lib/mamiya/cli.rb +44 -5
  19. data/lib/mamiya/configuration.rb +12 -0
  20. data/lib/mamiya/dsl.rb +6 -2
  21. data/lib/mamiya/helpers/git.rb +24 -0
  22. data/lib/mamiya/master/agent_monitor.rb +22 -2
  23. data/lib/mamiya/master/agent_monitor_handlers.rb +17 -0
  24. data/lib/mamiya/master/web.rb +42 -3
  25. data/lib/mamiya/master.rb +4 -0
  26. data/lib/mamiya/script.rb +28 -8
  27. data/lib/mamiya/steps/abstract.rb +1 -0
  28. data/lib/mamiya/steps/build.rb +107 -19
  29. data/lib/mamiya/steps/extract.rb +1 -0
  30. data/lib/mamiya/steps/prepare.rb +60 -0
  31. data/lib/mamiya/steps/switch.rb +76 -0
  32. data/lib/mamiya/storages/filesystem.rb +92 -0
  33. data/lib/mamiya/storages/mock.rb +1 -0
  34. data/lib/mamiya/util/label_matcher.rb +7 -3
  35. data/lib/mamiya/version.rb +1 -1
  36. data/mamiya.gemspec +1 -1
  37. data/spec/agent/actions_spec.rb +25 -0
  38. data/spec/agent/task_queue_spec.rb +42 -6
  39. data/spec/agent/tasks/abstract_spec.rb +35 -0
  40. data/spec/agent/tasks/clean_spec.rb +94 -45
  41. data/spec/agent/tasks/fetch_spec.rb +1 -0
  42. data/spec/agent/tasks/prepare_spec.rb +127 -0
  43. data/spec/agent_spec.rb +75 -27
  44. data/spec/dsl_spec.rb +6 -8
  45. data/spec/master/agent_monitor_spec.rb +142 -4
  46. data/spec/master/web_spec.rb +43 -1
  47. data/spec/steps/build_spec.rb +101 -0
  48. data/spec/steps/prepare_spec.rb +125 -0
  49. data/spec/steps/switch_spec.rb +146 -0
  50. data/spec/storages/filesystem_spec.rb +305 -0
  51. data/spec/util/label_matcher_spec.rb +32 -0
  52. metadata +20 -6
  53. data/config.example.yml +0 -11
  54. data/example.rb +0 -74
@@ -1,24 +1,73 @@
1
1
  require 'mamiya/steps/abstract'
2
2
  require 'mamiya/package'
3
3
 
4
+ require 'fileutils'
5
+
4
6
  module Mamiya
5
7
  module Steps
6
8
  class Build < Abstract
9
+ class ScriptFileNotSpecified < Exception; end
10
+ class ApplicationNotSpecified < Exception; end
11
+
7
12
  def run!
8
13
  @exception = nil
9
14
 
15
+ unless script_file
16
+ raise ScriptFileNotSpecified, "Set script files to :script_file"
17
+ end
18
+
19
+ unless script.application
20
+ raise ApplicationNotSpecified, ":application should be specified in your script file"
21
+ end
22
+
10
23
  logger.info "Initiating package build"
11
24
 
25
+ run_before_build
26
+ run_prepare_build
27
+ run_build
28
+
29
+ # XXX: Is this really suitable here? Package class should do?
30
+ copy_deploy_scripts
31
+
32
+ set_metadata
33
+
34
+ build_package
35
+
36
+ logger.info "Packed."
37
+
38
+ rescue Exception => e
39
+ @exception = e
40
+ raise
41
+ ensure
42
+ logger.warn "Exception occured, cleaning up..." if @exception
43
+
44
+ if script_dest.exist?
45
+ FileUtils.remove_entry_secure script_dest
46
+ end
47
+
48
+ logger.info "Running script.after_build"
49
+ script.after_build[@exception]
50
+
51
+ logger.info "DONE!" unless @exception
52
+ end
53
+
54
+ private
55
+
56
+ def run_before_build
12
57
  logger.info "Running script.before_build"
13
58
  script.before_build[]
59
+ end
14
60
 
61
+ def run_prepare_build
15
62
  unless script.skip_prepare_build
16
63
  logger.info "Running script.prepare_build"
17
64
  script.prepare_build[File.exists?(script.build_from)]
18
65
  else
19
66
  logger.debug "prepare_build skipped due to script.skip_prepare_build"
20
67
  end
68
+ end
21
69
 
70
+ def run_build
22
71
  old_pwd = Dir.pwd
23
72
  begin
24
73
  # Using without block because chdir in block shows warning
@@ -29,24 +78,42 @@ module Mamiya
29
78
  ensure
30
79
  Dir.chdir old_pwd
31
80
  end
81
+ end
32
82
 
83
+ def copy_deploy_scripts
84
+ # XXX: TODO: move to another class?
85
+ logger.info "Copying script files..."
33
86
 
34
- logger.debug "Determining package name..."
35
- package_name = Dir.chdir(script.build_from) {
36
- script.package_name[
37
- [Time.now.strftime("%Y-%m-%d_%H.%M.%S"), script.application]
38
- ].join('-')
39
- }
40
- logger.info "Package name determined: #{package_name}"
87
+ if script_dest.exist?
88
+ logger.warn "Removing existing .mamiya.script"
89
+ FileUtils.remove_entry_secure script_dest
90
+ end
91
+ script_dest.mkdir
41
92
 
42
- package_path = File.join(script.build_to, package_name)
43
- package = Mamiya::Package.new(package_path)
44
- package.meta[:application] = script.application
93
+ logger.info "- #{script_file} -> #{script_dest}"
94
+ FileUtils.cp script_file, script_dest
95
+
96
+ if script.script_additionals
97
+ script_dir = Pathname.new(File.dirname(script_file))
98
+ script.script_additionals.each do |additional|
99
+ src = script_dir.join(additional)
100
+ dst = script_dest.join(additional)
101
+ logger.info "- #{src} -> #{dst}"
102
+ FileUtils.mkdir_p dst.dirname
103
+ FileUtils.cp_r src, dst
104
+ end
105
+ end
106
+ end
45
107
 
108
+ def set_metadata
109
+ package.meta[:application] = script.application
110
+ package.meta[:script] = File.basename(script_file)
46
111
  Dir.chdir(script.build_from) do
47
112
  package.meta.replace script.package_meta[package.meta]
48
113
  end
114
+ end
49
115
 
116
+ def build_package
50
117
  logger.info "Packaging to: #{package.path}"
51
118
  logger.debug "meta=#{package.meta.inspect}"
52
119
  package.build!(script.build_from,
@@ -54,18 +121,39 @@ module Mamiya
54
121
  dereference_symlinks: script.dereference_symlinks || false,
55
122
  package_under: script.package_under || nil,
56
123
  logger: logger)
57
- logger.info "Packed."
124
+ end
58
125
 
59
- rescue Exception => e
60
- @exception = e
61
- raise
62
- ensure
63
- logger.warn "Exception occured, cleaning up..." if @exception
126
+ def package_name
127
+ @package_name ||= begin
128
+ logger.debug "Determining package name..."
129
+ name = Dir.chdir(script.build_from) {
130
+ script.package_name[
131
+ [Time.now.strftime("%Y-%m-%d_%H.%M.%S"), script.application]
132
+ ].join('-')
133
+ }
134
+ logger.info "Package name determined: #{name}"
135
+ name
136
+ end
137
+ end
64
138
 
65
- logger.info "Running script.after_build"
66
- script.after_build[@exception]
139
+ def package_path
140
+ @package_path ||= File.join(script.build_to, package_name)
141
+ end
67
142
 
68
- logger.info "DONE!" unless @exception
143
+ def package
144
+ @package ||= Mamiya::Package.new(package_path)
145
+ end
146
+
147
+ def script_file
148
+ @script_file ||= script.script_file || script._file
149
+ end
150
+
151
+ def script_dest
152
+ @script_dest ||= if script.package_under
153
+ Pathname.new File.join(script.build_from, script.package_under, '.mamiya.script')
154
+ else
155
+ Pathname.new File.join(script.build_from, '.mamiya.script')
156
+ end
69
157
  end
70
158
  end
71
159
  end
@@ -4,6 +4,7 @@ require 'mamiya/steps/abstract'
4
4
  module Mamiya
5
5
  module Steps
6
6
  class Extract < Abstract
7
+ # XXX: extract step is really needed? doing this in prepare step for consistency with agent/tasks/prepare, is better, I guess.
7
8
  def run!
8
9
  package = case options[:package]
9
10
  when Mamiya::Package
@@ -0,0 +1,60 @@
1
+ require 'mamiya/steps/abstract'
2
+
3
+ require 'pathname'
4
+ require 'json'
5
+
6
+ module Mamiya
7
+ module Steps
8
+ class Prepare < Abstract
9
+ def run!
10
+ @exception = nil
11
+ old_pwd = Dir.pwd
12
+ Dir.chdir(target)
13
+
14
+ logger.info "Preparing #{target}..."
15
+
16
+ script.before_prepare(labels)[]
17
+ script.prepare(labels)[]
18
+
19
+ File.write target.join('.mamiya.prepared'), "#{Time.now.to_i}\n"
20
+ rescue Exception => e
21
+ @exception = e
22
+ raise e
23
+ ensure
24
+ Dir.chdir old_pwd if old_pwd
25
+ logger.warn "Exception occured, cleaning up..." if @exception
26
+
27
+ script.after_prepare(labels)[@exception]
28
+
29
+ logger.info "DONE!" unless @exception
30
+ end
31
+
32
+ # This class see target_dir's script
33
+ alias given_script script
34
+
35
+ def script
36
+ @target_script ||= Mamiya::Script.new.load!(
37
+ target.join('.mamiya.script', target_meta['script'])).tap do |script|
38
+ # XXX: release_path is set by options[:target] but deploy_to is set by script?
39
+ script.set(:release_path, target)
40
+ script.set(:logger, logger)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def target
47
+ @target ||= Pathname.new(options[:target]).realpath
48
+ end
49
+
50
+ def target_meta
51
+ @target_meta ||= JSON.parse target.join('.mamiya.meta.json').read
52
+ end
53
+
54
+ def labels
55
+ # XXX: TODO: is it sure that passing labels via options of step?
56
+ options[:labels]
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,76 @@
1
+ require 'mamiya/steps/abstract'
2
+
3
+ module Mamiya
4
+ module Steps
5
+ class Switch < Abstract
6
+ def run!
7
+ @exception = nil
8
+ logger.info "Switching to #{target}"
9
+
10
+ script.before_switch(labels)[]
11
+
12
+ # TODO: link with relative if available?
13
+ File.unlink script.current_path if script.current_path.symlink?
14
+ script.current_path.make_symlink(target.realpath)
15
+
16
+ if do_release?
17
+ begin
18
+ old_pwd = Dir.pwd
19
+ Dir.chdir(target)
20
+
21
+ logger.info "Releasing..."
22
+
23
+ script.release(labels)[@exception]
24
+ ensure
25
+ Dir.chdir old_pwd if old_pwd
26
+ end
27
+ else
28
+ logger.warn "Skipping release (:no_release is set)"
29
+ end
30
+
31
+ rescue Exception => e
32
+ @exception = e
33
+ raise e
34
+ ensure
35
+ logger.warn "Exception occured, cleaning up..." if @exception
36
+
37
+ script.after_switch(labels)[@exception]
38
+
39
+ logger.info "DONE!" unless @exception
40
+ end
41
+
42
+ # XXX: dupe with prepare step. modulize?
43
+
44
+ # This class see target_dir's script
45
+ alias given_script script
46
+
47
+ def script
48
+ @target_script ||= Mamiya::Script.new.load!(
49
+ target.join('.mamiya.script', target_meta['script'])).tap do |script|
50
+ # XXX: release_path is set by options[:target] but deploy_to is set by script?
51
+ script.set(:release_path, target)
52
+ script.set(:logger, logger)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def do_release?
59
+ !options[:no_release]
60
+ end
61
+
62
+ def target
63
+ @target ||= Pathname.new(options[:target]).realpath
64
+ end
65
+
66
+ def target_meta
67
+ @target_meta ||= JSON.parse target.join('.mamiya.meta.json').read
68
+ end
69
+
70
+ def labels
71
+ # XXX: TODO: is it sure that passing labels via options of step?
72
+ options[:labels]
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,92 @@
1
+ require 'mamiya/package'
2
+ require 'mamiya/storages/abstract'
3
+ require 'fileutils'
4
+
5
+ module Mamiya
6
+ module Storages
7
+ class Filesystem < Mamiya::Storages::Abstract
8
+ def self.find(config={})
9
+ Hash[Dir[File.join(config[:path], '*')].map do |app_path|
10
+ app = File.basename(app_path)
11
+ [app, self.new(config.merge(application: app))]
12
+ end]
13
+ end
14
+
15
+ def packages
16
+ storage_path.children(false).group_by { |child|
17
+ child.to_s.sub(Package::PATH_SUFFIXES,'')
18
+ }.select { |key, files|
19
+ files.find { |file| file.to_s.end_with?('.tar.gz') } &&
20
+ files.find { |file| file.to_s.end_with?('.json') }
21
+ }.keys.sort
22
+ end
23
+
24
+ def push(package)
25
+ raise TypeError, "package should be a kind of Mamiya::Package" unless package.kind_of?(Mamiya::Package)
26
+ raise NotBuilt, "package not built" unless package.exists?
27
+
28
+ if package_exist?(package.name)
29
+ raise AlreadyExists
30
+ end
31
+
32
+ storage_path.mkpath
33
+
34
+ FileUtils.cp package.path, storage_path.join("#{package.name}.tar.gz")
35
+ FileUtils.cp package.meta_path, storage_path.join("#{package.name}.json")
36
+ end
37
+
38
+ def fetch(package_name, destination)
39
+ package_name = normalize_package_name(package_name)
40
+ raise NotFound unless package_exist?(package_name)
41
+
42
+ package_path = File.join(destination, "#{package_name}.tar.gz")
43
+ meta_path = File.join(destination, "#{package_name}.json")
44
+
45
+ if File.exists?(package_path) || File.exists?(meta_path)
46
+ raise AlreadyFetched
47
+ end
48
+
49
+ FileUtils.cp storage_path.join("#{package_name}.tar.gz"), package_path
50
+ FileUtils.cp storage_path.join("#{package_name}.json"), meta_path
51
+
52
+ return Mamiya::Package.new(package_path)
53
+ end
54
+
55
+ def meta(package_name)
56
+ package_name = normalize_package_name(package_name)
57
+ return unless package_exist?(package_name)
58
+
59
+ JSON.parse storage_path.join("#{package_name}.json").read
60
+ end
61
+
62
+ def remove(package_name)
63
+ package_name = normalize_package_name(package_name)
64
+
65
+ package_path = storage_path.join("#{package_name}.tar.gz")
66
+ meta_path = storage_path.join("#{package_name}.json")
67
+
68
+ if [package_path, meta_path].all? { |_| !_.exist? }
69
+ raise Mamiya::Storages::Abstract::NotFound
70
+ end
71
+
72
+ package_path.delete if package_path.exist?
73
+ meta_path.delete if meta_path.exist?
74
+ end
75
+
76
+ private
77
+
78
+ def storage_path
79
+ @storage_path ||= Pathname.new(@config[:path]).join(@config[:application])
80
+ end
81
+
82
+ def package_exist?(name)
83
+ storage_path.join("#{name}.tar.gz").exist? &&
84
+ storage_path.join("#{name}.json").exist?
85
+ end
86
+
87
+ def normalize_package_name(name)
88
+ name.sub(/\.(?:tar\.gz|json)\z/, '')
89
+ end
90
+ end
91
+ end
92
+ end
@@ -14,6 +14,7 @@ module Mamiya
14
14
  end
15
15
 
16
16
  def self.find(config={})
17
+ # TODO: FIXME: Should be an Hash
17
18
  storage.keys
18
19
  end
19
20
 
@@ -1,8 +1,12 @@
1
1
  module Mamiya
2
2
  module Util
3
3
  module LabelMatcher
4
+ def self.parse_string_expr(str)
5
+ str.split(/\|/).map{ |_| _.split(/,/) }
6
+ end
7
+
4
8
  def match?(*expressions)
5
- labels = self.labels()
9
+ labels = self.labels().map(&:to_s)
6
10
 
7
11
  if expressions.all? { |_| _.kind_of?(Symbol) || _.kind_of?(String) }
8
12
  return self.match?(expressions)
@@ -11,12 +15,12 @@ module Mamiya
11
15
  expressions.any? do |expression|
12
16
  case expression
13
17
  when Symbol, String
14
- labels.include?(expression)
18
+ labels.include?(expression.to_s)
15
19
  when Array
16
20
  if expression.any? { |_| _.kind_of?(Array) }
17
21
  self.match?(*expression)
18
22
  else
19
- expression.all? { |_| labels.include?(_) }
23
+ expression.all? { |_| labels.include?(_.to_s) }
20
24
  end
21
25
  end
22
26
  end
@@ -1,3 +1,3 @@
1
1
  module Mamiya
2
- VERSION = "0.0.1.alpha21"
2
+ VERSION = "0.0.1.alpha22"
3
3
  end
data/mamiya.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.add_runtime_dependency "aws-sdk-core", "2.0.0.rc15"
23
23
  spec.add_runtime_dependency "term-ansicolor", ">= 1.3.0"
24
24
  unless ENV["MAMIYA_VILLEIN_PATH"]
25
- spec.add_runtime_dependency "villein", ">= 0.3.2"
25
+ spec.add_runtime_dependency "villein", ">= 0.5.0"
26
26
  end
27
27
 
28
28
  spec.add_runtime_dependency "sinatra", ">= 1.4.5"
@@ -28,5 +28,30 @@ describe Mamiya::Agent::Actions do
28
28
 
29
29
  agent.distribute('myapp', 'mypkg')
30
30
  end
31
+
32
+ context "with labels" do
33
+ it "adds _labels on task" do
34
+ expect(agent).to receive(:trigger).with('task', task: 'fetch', app: 'myapp', pkg: 'mypkg', _labels: ['foo'], coalesce: false)
35
+
36
+ agent.distribute('myapp', 'mypkg', labels: ['foo'])
37
+ end
38
+ end
39
+
40
+ end
41
+
42
+ describe "#prepare" do
43
+ it "sends prepare request" do
44
+ expect(agent).to receive(:trigger).with('task', task: 'prepare', app: 'myapp', pkg: 'mypkg', coalesce: false)
45
+
46
+ agent.prepare('myapp', 'mypkg')
47
+ end
48
+
49
+ context "with labels" do
50
+ it "adds _labels on task" do
51
+ expect(agent).to receive(:trigger).with('task', task: 'prepare', app: 'myapp', pkg: 'mypkg', _labels: ['foo'], coalesce: false)
52
+
53
+ agent.prepare('myapp', 'mypkg', labels: ['foo'])
54
+ end
55
+ end
31
56
  end
32
57
  end
@@ -103,6 +103,42 @@ describe Mamiya::Agent::TaskQueue do
103
103
  expect(task_class_a.runs[1]['foo']).to eq '2'
104
104
  end
105
105
 
106
+ describe "(task with _labels)" do
107
+ it "runs on matched agent" do
108
+ queue.start!
109
+ expect(agent).to receive(:match?).with(['foo', 'bar']).and_return(true)
110
+
111
+ queue.enqueue(:a, 'foo' => '1', '_labels' => ['foo', 'bar'])
112
+
113
+ 100.times { break if task_class_a.runs.size == 1; sleep 0.01 }
114
+ expect(task_class_a.runs.size).to eq 1
115
+ expect(task_class_a.runs[0]['foo']).to eq '1'
116
+ end
117
+
118
+ it "doesn't run on not matched agent" do
119
+ queue.start!
120
+ expect(agent).to receive(:match?).with(['foo', 'bar']).and_return(false)
121
+
122
+ queue.enqueue(:a, 'foo' => '1', '_labels' => ['foo', 'bar'])
123
+ queue.enqueue(:a, 'foo' => '2')
124
+
125
+ 100.times { break if task_class_a.runs.size == 1; sleep 0.01 }
126
+ expect(task_class_a.runs.size).to eq 1
127
+ expect(task_class_a.runs[0]['foo']).to eq '2'
128
+ end
129
+
130
+ it "removes _labels from task on run" do
131
+ queue.start!
132
+ expect(agent).to receive(:match?).with(['foo', 'bar']).and_return(true)
133
+
134
+ queue.enqueue(:a, 'foo' => '1', '_labels' => ['foo', 'bar'])
135
+
136
+ 100.times { break if task_class_a.runs.size == 1; sleep 0.01 }
137
+ expect(task_class_a.runs.size).to eq 1
138
+ expect(task_class_a.runs[0].key?('_labels')).to be_false
139
+ end
140
+ end
141
+
106
142
  describe "#working?" do
107
143
  it "returns true if there're any working tasks" do
108
144
  queue.start!
@@ -134,12 +170,12 @@ describe Mamiya::Agent::TaskQueue do
134
170
 
135
171
  100.times { break unless task_class_a.locks.empty?; sleep 0.01 }
136
172
  expect(task_class_a.locks).not_to be_empty
137
- expect(queue.status[:a][:working]).to eq('wait' => true, 'id' => 1)
173
+ expect(queue.status[:a][:working]).to eq('wait' => true, 'id' => 1, 'task' => :a)
138
174
 
139
175
  queue.enqueue(:a, 'id' => 2)
140
176
  100.times { break unless queue.status[:a][:queue].empty?; sleep 0.01 }
141
177
  expect(queue.status[:a][:queue].size).to eq 1
142
- expect(queue.status[:a][:queue].first).to eq('id' => 2)
178
+ expect(queue.status[:a][:queue].first).to eq('id' => 2, 'task' => :a)
143
179
 
144
180
  task_class_a.locks.values.last << true
145
181
 
@@ -200,16 +236,16 @@ describe Mamiya::Agent::TaskQueue do
200
236
  expect(task_class_a.locks).not_to be_empty
201
237
  expect(task_class_b.locks).not_to be_empty
202
238
 
203
- expect(queue.status[:a][:working]).to eq('wait' => true, 'id' => 1)
204
- expect(queue.status[:b][:working]).to eq('wait' => true, 'id' => 2)
239
+ expect(queue.status[:a][:working]).to eq('wait' => true, 'id' => 1, 'task' => :a)
240
+ expect(queue.status[:b][:working]).to eq('wait' => true, 'id' => 2, 'task' => :b)
205
241
 
206
242
  queue.enqueue(:a, 'id' => 3)
207
243
  queue.enqueue(:b, 'id' => 4)
208
244
  100.times { break if !queue.status[:a][:queue].empty? && !queue.status[:b][:queue].empty?; sleep 0.01 }
209
245
  expect(queue.status[:a][:queue].size).to eq 1
210
- expect(queue.status[:a][:queue].first).to eq('id' => 3)
246
+ expect(queue.status[:a][:queue].first).to eq('id' => 3, 'task' => :a)
211
247
  expect(queue.status[:b][:queue].size).to eq 1
212
- expect(queue.status[:b][:queue].first).to eq('id' => 4)
248
+ expect(queue.status[:b][:queue].first).to eq('id' => 4, 'task' => :b)
213
249
 
214
250
  task_class_a.locks.values.last << true
215
251
  task_class_b.locks.values.last << true
@@ -41,6 +41,41 @@ describe Mamiya::Agent::Tasks::Abstract do
41
41
 
42
42
  expect(task.error).to eq err
43
43
  end
44
+
45
+ context "with _chain" do
46
+ let(:job) { {'foo' => 'bar', '_chain' => ['another']} }
47
+
48
+ it "enqueues next _chain, keeping same task specification" do
49
+ expect(queue).to receive(:enqueue).with(:another,
50
+ 'foo' => 'bar',
51
+ )
52
+
53
+ task.execute
54
+ end
55
+ end
56
+
57
+ context "with multiple _chain" do
58
+ let(:job) { {'foo' => 'bar', '_chain' => ['b', 'c']} }
59
+
60
+ it "enqueues next _chain, keeping same task specification" do
61
+ expect(queue).to receive(:enqueue).with(:b,
62
+ 'foo' => 'bar',
63
+ '_chain' => ['c'],
64
+ )
65
+
66
+ task.execute
67
+ end
68
+ end
69
+
70
+ context "with empty _chain" do
71
+ let(:job) { {'foo' => 'bar', '_chain' => []} }
72
+
73
+ it "doesn't enqueue nothing" do
74
+ expect(queue).not_to receive(:enqueue)
75
+ task.execute
76
+ end
77
+ end
78
+
44
79
  end
45
80
 
46
81
  describe ".identifier" do