mamiya 0.0.1.alpha21 → 0.0.1.alpha22

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