dynflow 0.7.6 → 0.7.7

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 (151) hide show
  1. data/README.md +11 -3
  2. data/doc/pages/.gitignore +7 -0
  3. data/doc/pages/Gemfile +8 -0
  4. data/doc/pages/Rakefile +26 -0
  5. data/doc/pages/_config.yml +38 -0
  6. data/doc/pages/plugins/alert_block.rb +27 -0
  7. data/doc/pages/plugins/div_tag.rb +24 -0
  8. data/doc/pages/plugins/graphviz.rb +121 -0
  9. data/doc/pages/plugins/plantuml.rb +85 -0
  10. data/doc/pages/plugins/play.rb +13 -0
  11. data/doc/pages/plugins/tags.rb +138 -0
  12. data/doc/pages/plugins/toc.rb +20 -0
  13. data/doc/pages/source/.nojekyll +0 -0
  14. data/doc/pages/source/404.md +6 -0
  15. data/doc/pages/source/_includes/disqus.html +25 -0
  16. data/doc/pages/source/_includes/google_analytics.html +12 -0
  17. data/doc/pages/source/_includes/google_plus_one.html +2 -0
  18. data/doc/pages/source/_includes/menu.html +19 -0
  19. data/doc/pages/source/_includes/menu_brand.html +2 -0
  20. data/doc/pages/source/_includes/menu_right.html +1 -0
  21. data/doc/pages/source/_includes/post_item.html +10 -0
  22. data/doc/pages/source/_includes/scroll_to.html +24 -0
  23. data/doc/pages/source/_includes/twitter_sharing.html +9 -0
  24. data/doc/pages/source/_layouts/default.html +70 -0
  25. data/doc/pages/source/_layouts/page.html +47 -0
  26. data/doc/pages/source/_layouts/post.html +19 -0
  27. data/doc/pages/source/_layouts/presentation.html +39 -0
  28. data/doc/pages/source/_layouts/tag_page.html +12 -0
  29. data/doc/pages/source/_sass/_bootstrap-compass.scss +9 -0
  30. data/doc/pages/source/_sass/_bootstrap-mincer.scss +19 -0
  31. data/doc/pages/source/_sass/_bootstrap-sprockets.scss +9 -0
  32. data/doc/pages/source/_sass/_bootstrap-variables.sass +865 -0
  33. data/doc/pages/source/_sass/_bootstrap.scss +50 -0
  34. data/doc/pages/source/_sass/_specific.scss +16 -0
  35. data/doc/pages/source/_sass/_style.scss +172 -0
  36. data/doc/pages/source/_sass/bootstrap/_alerts.scss +73 -0
  37. data/doc/pages/source/_sass/bootstrap/_badges.scss +67 -0
  38. data/doc/pages/source/_sass/bootstrap/_breadcrumbs.scss +26 -0
  39. data/doc/pages/source/_sass/bootstrap/_button-groups.scss +243 -0
  40. data/doc/pages/source/_sass/bootstrap/_buttons.scss +160 -0
  41. data/doc/pages/source/_sass/bootstrap/_carousel.scss +269 -0
  42. data/doc/pages/source/_sass/bootstrap/_close.scss +36 -0
  43. data/doc/pages/source/_sass/bootstrap/_code.scss +69 -0
  44. data/doc/pages/source/_sass/bootstrap/_component-animations.scss +38 -0
  45. data/doc/pages/source/_sass/bootstrap/_dropdowns.scss +214 -0
  46. data/doc/pages/source/_sass/bootstrap/_forms.scss +570 -0
  47. data/doc/pages/source/_sass/bootstrap/_glyphicons.scss +301 -0
  48. data/doc/pages/source/_sass/bootstrap/_grid.scss +84 -0
  49. data/doc/pages/source/_sass/bootstrap/_input-groups.scss +166 -0
  50. data/doc/pages/source/_sass/bootstrap/_jumbotron.scss +50 -0
  51. data/doc/pages/source/_sass/bootstrap/_labels.scss +66 -0
  52. data/doc/pages/source/_sass/bootstrap/_list-group.scss +124 -0
  53. data/doc/pages/source/_sass/bootstrap/_media.scss +61 -0
  54. data/doc/pages/source/_sass/bootstrap/_mixins.scss +39 -0
  55. data/doc/pages/source/_sass/bootstrap/_modals.scss +148 -0
  56. data/doc/pages/source/_sass/bootstrap/_navbar.scss +663 -0
  57. data/doc/pages/source/_sass/bootstrap/_navs.scss +244 -0
  58. data/doc/pages/source/_sass/bootstrap/_normalize.scss +427 -0
  59. data/doc/pages/source/_sass/bootstrap/_pager.scss +54 -0
  60. data/doc/pages/source/_sass/bootstrap/_pagination.scss +88 -0
  61. data/doc/pages/source/_sass/bootstrap/_panels.scss +265 -0
  62. data/doc/pages/source/_sass/bootstrap/_popovers.scss +135 -0
  63. data/doc/pages/source/_sass/bootstrap/_print.scss +107 -0
  64. data/doc/pages/source/_sass/bootstrap/_progress-bars.scss +87 -0
  65. data/doc/pages/source/_sass/bootstrap/_responsive-embed.scss +35 -0
  66. data/doc/pages/source/_sass/bootstrap/_responsive-utilities.scss +177 -0
  67. data/doc/pages/source/_sass/bootstrap/_scaffolding.scss +150 -0
  68. data/doc/pages/source/_sass/bootstrap/_tables.scss +234 -0
  69. data/doc/pages/source/_sass/bootstrap/_theme.scss +273 -0
  70. data/doc/pages/source/_sass/bootstrap/_thumbnails.scss +38 -0
  71. data/doc/pages/source/_sass/bootstrap/_tooltip.scss +103 -0
  72. data/doc/pages/source/_sass/bootstrap/_type.scss +298 -0
  73. data/doc/pages/source/_sass/bootstrap/_utilities.scss +56 -0
  74. data/doc/pages/source/_sass/bootstrap/_variables.scss +862 -0
  75. data/doc/pages/source/_sass/bootstrap/_wells.scss +29 -0
  76. data/doc/pages/source/_sass/bootstrap/mixins/_alerts.scss +14 -0
  77. data/doc/pages/source/_sass/bootstrap/mixins/_background-variant.scss +11 -0
  78. data/doc/pages/source/_sass/bootstrap/mixins/_border-radius.scss +18 -0
  79. data/doc/pages/source/_sass/bootstrap/mixins/_buttons.scss +52 -0
  80. data/doc/pages/source/_sass/bootstrap/mixins/_center-block.scss +7 -0
  81. data/doc/pages/source/_sass/bootstrap/mixins/_clearfix.scss +22 -0
  82. data/doc/pages/source/_sass/bootstrap/mixins/_forms.scss +88 -0
  83. data/doc/pages/source/_sass/bootstrap/mixins/_gradients.scss +58 -0
  84. data/doc/pages/source/_sass/bootstrap/mixins/_grid-framework.scss +81 -0
  85. data/doc/pages/source/_sass/bootstrap/mixins/_grid.scss +122 -0
  86. data/doc/pages/source/_sass/bootstrap/mixins/_hide-text.scss +21 -0
  87. data/doc/pages/source/_sass/bootstrap/mixins/_image.scss +33 -0
  88. data/doc/pages/source/_sass/bootstrap/mixins/_labels.scss +12 -0
  89. data/doc/pages/source/_sass/bootstrap/mixins/_list-group.scss +31 -0
  90. data/doc/pages/source/_sass/bootstrap/mixins/_nav-divider.scss +10 -0
  91. data/doc/pages/source/_sass/bootstrap/mixins/_nav-vertical-align.scss +9 -0
  92. data/doc/pages/source/_sass/bootstrap/mixins/_opacity.scss +8 -0
  93. data/doc/pages/source/_sass/bootstrap/mixins/_pagination.scss +23 -0
  94. data/doc/pages/source/_sass/bootstrap/mixins/_panels.scss +24 -0
  95. data/doc/pages/source/_sass/bootstrap/mixins/_progress-bar.scss +10 -0
  96. data/doc/pages/source/_sass/bootstrap/mixins/_reset-filter.scss +8 -0
  97. data/doc/pages/source/_sass/bootstrap/mixins/_resize.scss +6 -0
  98. data/doc/pages/source/_sass/bootstrap/mixins/_responsive-visibility.scss +21 -0
  99. data/doc/pages/source/_sass/bootstrap/mixins/_size.scss +10 -0
  100. data/doc/pages/source/_sass/bootstrap/mixins/_tab-focus.scss +9 -0
  101. data/doc/pages/source/_sass/bootstrap/mixins/_table-row.scss +28 -0
  102. data/doc/pages/source/_sass/bootstrap/mixins/_text-emphasis.scss +11 -0
  103. data/doc/pages/source/_sass/bootstrap/mixins/_text-overflow.scss +8 -0
  104. data/doc/pages/source/_sass/bootstrap/mixins/_vendor-prefixes.scss +222 -0
  105. data/doc/pages/source/atom.xml +32 -0
  106. data/doc/pages/source/bootstrap/config.json +429 -0
  107. data/doc/pages/source/bootstrap/css/bootstrap-theme.css +479 -0
  108. data/doc/pages/source/bootstrap/css/bootstrap-theme.min.css +10 -0
  109. data/doc/pages/source/bootstrap/css/bootstrap.css +6564 -0
  110. data/doc/pages/source/bootstrap/css/bootstrap.min.css +10 -0
  111. data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.eot +0 -0
  112. data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.svg +288 -0
  113. data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.ttf +0 -0
  114. data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.woff +0 -0
  115. data/doc/pages/source/bootstrap/fonts/glyphicons-halflings-regular.woff2 +0 -0
  116. data/doc/pages/source/bootstrap/js/bootstrap.js +2309 -0
  117. data/doc/pages/source/bootstrap/js/bootstrap.min.js +12 -0
  118. data/doc/pages/source/css/app.scss +10 -0
  119. data/doc/pages/source/css/syntax.css +60 -0
  120. data/doc/pages/source/documentation/index.md +977 -0
  121. data/doc/pages/source/faq/index.md +16 -0
  122. data/doc/pages/source/images/dynflow-logos.svg +423 -0
  123. data/doc/pages/source/images/logo-long.png +0 -0
  124. data/doc/pages/source/images/logo-long.svg +116 -0
  125. data/doc/pages/source/images/logo-square.png +0 -0
  126. data/doc/pages/source/images/logo-square.svg +75 -0
  127. data/doc/pages/source/images/noise.png +0 -0
  128. data/doc/pages/source/images/screenshot.png +0 -0
  129. data/doc/pages/source/index.md +64 -0
  130. data/doc/pages/source/media/index.md +20 -0
  131. data/doc/pages/source/projects/index.md +32 -0
  132. data/dynflow.gemspec +2 -3
  133. data/examples/sub_plans.rb +37 -0
  134. data/lib/dynflow/action.rb +28 -8
  135. data/lib/dynflow/action/polling.rb +6 -5
  136. data/lib/dynflow/action/with_sub_plans.rb +162 -0
  137. data/lib/dynflow/execution_plan.rb +25 -7
  138. data/lib/dynflow/execution_plan/steps/abstract.rb +5 -2
  139. data/lib/dynflow/execution_plan/steps/plan_step.rb +6 -2
  140. data/lib/dynflow/execution_plan/steps/run_step.rb +4 -0
  141. data/lib/dynflow/persistence.rb +5 -0
  142. data/lib/dynflow/persistence_adapters/sequel.rb +12 -2
  143. data/lib/dynflow/persistence_adapters/sequel_migrations/003_parent_action.rb +9 -0
  144. data/lib/dynflow/version.rb +1 -1
  145. data/lib/dynflow/web_console.rb +21 -7
  146. data/lib/dynflow/world.rb +26 -2
  147. data/test/action_test.rb +107 -0
  148. data/test/persistance_adapters_test.rb +2 -2
  149. data/test/test_helper.rb +1 -1
  150. data/web/views/flow_step.erb +3 -0
  151. metadata +137 -4
@@ -97,6 +97,10 @@ module Dynflow
97
97
  result == :error
98
98
  end
99
99
 
100
+ def error_in_plan?
101
+ steps_in_state(:error).any? { |step| step.is_a? Steps::PlanStep }
102
+ end
103
+
100
104
  def errors
101
105
  steps.values.map(&:error).compact
102
106
  end
@@ -105,6 +109,10 @@ module Dynflow
105
109
  Type! entry_action.rescue_strategy, Action::Rescue::Strategy
106
110
  end
107
111
 
112
+ def sub_plans
113
+ persistence.find_execution_plans(filters: { 'caller_execution_plan_id' => self.id })
114
+ end
115
+
108
116
  def rescue_plan_id
109
117
  case rescue_strategy
110
118
  when Action::Rescue::Pause
@@ -141,9 +149,12 @@ module Dynflow
141
149
  @last_step_id += 1
142
150
  end
143
151
 
144
- def prepare(action_class)
152
+ def prepare(action_class, options = {})
153
+ options = options.dup
154
+ caller_action = Type! options.delete(:caller_action), Dynflow::Action, NilClass
155
+ raise "Unexpected options #{options.keys.inspect}" unless options.empty?
145
156
  save
146
- @root_plan_step = add_plan_step(action_class)
157
+ @root_plan_step = add_plan_step(action_class, caller_action)
147
158
  @root_plan_step.save
148
159
  end
149
160
 
@@ -226,9 +237,13 @@ module Dynflow
226
237
  current_run_flow.add_and_resolve(@dependency_graph, new_flow) if current_run_flow
227
238
  end
228
239
 
229
- def add_plan_step(action_class, planned_by = nil)
230
- add_step(Steps::PlanStep, action_class, generate_action_id, planned_by && planned_by.plan_step_id).tap do |step|
231
- step.initialize_action
240
+ def add_plan_step(action_class, caller_action = nil)
241
+ add_step(Steps::PlanStep, action_class, generate_action_id).tap do |step|
242
+ # TODO: to be removed and preferred by the caller_action
243
+ if caller_action && caller_action.execution_plan_id == self.id
244
+ @steps[caller_action.plan_step_id].children << step.id
245
+ end
246
+ step.initialize_action(caller_action)
232
247
  end
233
248
  end
234
249
 
@@ -312,13 +327,17 @@ module Dynflow
312
327
  end
313
328
  end
314
329
 
330
+ def caller_execution_plan_id
331
+ entry_action.caller_execution_plan_id
332
+ end
333
+
315
334
  private
316
335
 
317
336
  def persistence
318
337
  world.persistence
319
338
  end
320
339
 
321
- def add_step(step_class, action_class, action_id, planned_by_step_id = nil)
340
+ def add_step(step_class, action_class, action_id)
322
341
  step_class.new(self.id,
323
342
  self.generate_step_id,
324
343
  :pending,
@@ -327,7 +346,6 @@ module Dynflow
327
346
  nil,
328
347
  world).tap do |new_step|
329
348
  @steps[new_step.id] = new_step
330
- @steps[planned_by_step_id].children << new_step.id if planned_by_step_id
331
349
  end
332
350
  end
333
351
 
@@ -113,8 +113,7 @@ module Dynflow
113
113
  # @return [Action] in presentation mode, intended for retrieving: progress information,
114
114
  # details, human outputs, etc.
115
115
  def action(execution_plan)
116
- attributes = world.persistence.adapter.load_action(execution_plan_id, action_id)
117
- Action.from_hash(attributes.update(phase: Action::Present, execution_plan: execution_plan), world)
116
+ world.persistence.load_action_for_presentation(execution_plan, action_id)
118
117
  end
119
118
 
120
119
  def skippable?
@@ -125,6 +124,10 @@ module Dynflow
125
124
  false
126
125
  end
127
126
 
127
+ def with_sub_plans?
128
+ false
129
+ end
130
+
128
131
  protected
129
132
 
130
133
  def self.new_from_hash(hash, execution_plan_id, world)
@@ -76,14 +76,18 @@ module Dynflow
76
76
  hash[:children]
77
77
  end
78
78
 
79
- def initialize_action
79
+ def initialize_action(caller_action)
80
80
  attributes = { execution_plan_id: execution_plan_id,
81
81
  id: action_id,
82
82
  step: self,
83
83
  plan_step_id: self.id,
84
84
  run_step_id: nil,
85
85
  finalize_step_id: nil,
86
- phase: phase}
86
+ phase: phase }
87
+ if caller_action
88
+ attributes.update(caller_execution_plan_id: caller_action.execution_plan_id,
89
+ caller_action_id: caller_action.id)
90
+ end
87
91
  @action = action_class.new(attributes, world)
88
92
  persistence.save_action(execution_plan_id, @action)
89
93
  @action
@@ -23,6 +23,10 @@ module Dynflow
23
23
  self.action_class < Action::Cancellable
24
24
  end
25
25
 
26
+ def with_sub_plans?
27
+ self.action_class < Action::WithSubPlans
28
+ end
29
+
26
30
  def mark_to_skip
27
31
  case self.state
28
32
  when :error
@@ -19,6 +19,11 @@ module Dynflow
19
19
  return Action.from_hash(attributes, step.world)
20
20
  end
21
21
 
22
+ def load_action_for_presentation(execution_plan, action_id)
23
+ attributes = adapter.load_action(execution_plan.id, action_id)
24
+ Action.from_hash(attributes.update(phase: Action::Present, execution_plan: execution_plan), @world)
25
+ end
26
+
22
27
  def save_action(execution_plan_id, action)
23
28
  adapter.save_action(execution_plan_id, action.id, action.to_hash)
24
29
  end
@@ -27,7 +27,7 @@ module Dynflow
27
27
  end
28
28
 
29
29
  META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
30
- action: [],
30
+ action: %w(caller_execution_plan_id caller_action_id),
31
31
  step: %w(state started_at ended_at real_time execution_time action_id progress_done progress_weight) }
32
32
 
33
33
  def initialize(config)
@@ -151,8 +151,18 @@ module Dynflow
151
151
  def filter(data_set, options)
152
152
  filters = Type! options[:filters], NilClass, Hash
153
153
  return data_set if filters.nil?
154
+ unknown = filters.keys - META_DATA.fetch(:execution_plan) - %w[caller_execution_plan_id caller_action_id]
154
155
 
155
- unless (unknown = filters.keys - META_DATA.fetch(:execution_plan)).empty?
156
+ if filters.key?('caller_action_id') && !filters.key?('caller_execution_plan_id')
157
+ raise ArgumentError, "caller_action_id given but caller_execution_plan_id missing"
158
+ end
159
+
160
+ if filters.key?('caller_execution_plan_id')
161
+ data_set = data_set.join_table(:inner, TABLES[:action], :execution_plan_uuid => :uuid).
162
+ select_all(TABLES[:execution_plan]).distinct
163
+ end
164
+
165
+ unless (unknown).empty?
156
166
  raise ArgumentError, "unkown columns: #{unknown.inspect}"
157
167
  end
158
168
 
@@ -0,0 +1,9 @@
1
+ Sequel.migration do
2
+ change do
3
+ alter_table(:dynflow_actions) do
4
+ add_column :caller_execution_plan_id, String, fixed: true, size: 36
5
+ add_column :caller_action_id, Fixnum
6
+ add_index [:caller_execution_plan_id, :caller_action_id]
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.7.6'
2
+ VERSION = '0.7.7'
3
3
  end
@@ -143,7 +143,7 @@ module Dynflow
143
143
  end
144
144
 
145
145
  def updated_url(new_params)
146
- url("?" + Rack::Utils.build_nested_query(params.merge(new_params.stringify_keys)))
146
+ url(request.path_info + "?" + Rack::Utils.build_nested_query(params.merge(new_params.stringify_keys)))
147
147
  end
148
148
 
149
149
  def paginated_url(delta)
@@ -211,7 +211,7 @@ module Dynflow
211
211
  end
212
212
  end
213
213
 
214
- def filtering_options
214
+ def filtering_options(show_all = false)
215
215
  return @filtering_options if @filtering_options
216
216
 
217
217
  if params[:filters]
@@ -223,7 +223,8 @@ module Dynflow
223
223
 
224
224
  filters = params[:filters]
225
225
  elsif supported_filter?('state')
226
- filters = { 'state' => ExecutionPlan.states.map(&:to_s) - ['stopped'] }
226
+ excluded_states = show_all ? [] : ['stopped']
227
+ filters = { 'state' => ExecutionPlan.states.map(&:to_s) - excluded_states }
227
228
  else
228
229
  filters = {}
229
230
  end
@@ -231,6 +232,13 @@ module Dynflow
231
232
  return @filtering_options
232
233
  end
233
234
 
235
+ def find_execution_plans_options(show_all = false)
236
+ options = HashWithIndifferentAccess.new
237
+ options.merge!(filtering_options(show_all))
238
+ options.merge!(pagination_options)
239
+ options.merge!(ordering_options)
240
+ end
241
+
234
242
  def filter_checkbox(field, values)
235
243
  out = "<p>#{field}: %s</p>"
236
244
  checkboxes = values.map do |value|
@@ -245,15 +253,21 @@ module Dynflow
245
253
  end
246
254
 
247
255
  get('/') do
248
- options = HashWithIndifferentAccess.new
249
- options.merge!(filtering_options)
250
- options.merge!(pagination_options)
251
- options.merge!(ordering_options)
256
+ options = find_execution_plans_options
252
257
 
253
258
  @plans = world.persistence.find_execution_plans(options)
254
259
  erb :index
255
260
  end
256
261
 
262
+ get('/:execution_plan_id/actions/:action_id/sub_plans') do |execution_plan_id, action_id|
263
+ options = find_execution_plans_options(true)
264
+ options[:filters].update('caller_execution_plan_id' => execution_plan_id,
265
+ 'caller_action_id' => action_id)
266
+ @plans = world.persistence.find_execution_plans(options)
267
+ erb :index
268
+ end
269
+
270
+
257
271
  get('/:id') do |id|
258
272
  @plan = world.persistence.load_execution_plan(id)
259
273
  @notice = params[:notice]
data/lib/dynflow/world.rb CHANGED
@@ -91,8 +91,14 @@ module Dynflow
91
91
 
92
92
  # @return [TriggerResult]
93
93
  # blocks until action_class is planned
94
- def trigger(action_class, *args)
95
- execution_plan = plan(action_class, *args)
94
+ # if no arguments given, the plan is expected to be returned by a block
95
+ def trigger(action_class = nil, *args, &block)
96
+ if action_class.nil?
97
+ raise 'Neither action_class nor a block given' if block.nil?
98
+ execution_plan = block.call(self)
99
+ else
100
+ execution_plan = plan(action_class, *args)
101
+ end
96
102
  planned = execution_plan.state == :planned
97
103
 
98
104
  if planned
@@ -107,7 +113,14 @@ module Dynflow
107
113
  end
108
114
 
109
115
  def event(execution_plan_id, step_id, event, future = Future.new)
116
+ # we do this to avoid unresolved future when getting into
117
+ # the executor mailbox right at the termination.
118
+ # TODO: concurrent-ruby dead letter routing should make this
119
+ # more elegant
120
+ raise Dynflow::Error, "terminating world is not accepting events" if terminating?
110
121
  executor.event execution_plan_id, step_id, event, future
122
+ rescue => e
123
+ future.fail e
111
124
  end
112
125
 
113
126
  def plan(action_class, *args)
@@ -117,6 +130,13 @@ module Dynflow
117
130
  end
118
131
  end
119
132
 
133
+ def plan_with_caller(caller_action, action_class, *args)
134
+ ExecutionPlan.new(self).tap do |execution_plan|
135
+ execution_plan.prepare(action_class, caller_action: caller_action)
136
+ execution_plan.plan(*args)
137
+ end
138
+ end
139
+
120
140
  # @return [Future] containing execution_plan when finished
121
141
  # raises when ExecutionPlan is not accepted for execution
122
142
  def execute(execution_plan_id, finished = Future.new)
@@ -138,6 +158,10 @@ module Dynflow
138
158
  Future.join([@executor_terminated, @clock_terminated], future)
139
159
  end
140
160
 
161
+ def terminating?
162
+ !!@executor_terminated
163
+ end
164
+
141
165
  # Detects execution plans that are marked as running but no executor
142
166
  # handles them (probably result of non-standard executor termination)
143
167
  #
data/test/action_test.rb CHANGED
@@ -252,5 +252,112 @@ module Dynflow
252
252
  end
253
253
 
254
254
  end
255
+
256
+ describe Action::WithSubPlans do
257
+
258
+ class FailureSimulator
259
+ class << self
260
+ attr_accessor :fail_in_child_plan, :fail_in_child_run
261
+
262
+ def reset!
263
+ self.fail_in_child_plan = self.fail_in_child_run = false
264
+ end
265
+ end
266
+ end
267
+
268
+ class ParentAction < Dynflow::Action
269
+
270
+ include Dynflow::Action::WithSubPlans
271
+
272
+ def create_sub_plans
273
+ input[:count].times.map{ trigger(ChildAction) }
274
+ end
275
+
276
+ def resume
277
+ output[:custom_resume] = true
278
+ super
279
+ end
280
+ end
281
+
282
+ class ChildAction < Dynflow::Action
283
+ def plan
284
+ if FailureSimulator.fail_in_child_plan
285
+ raise "Fail in child plan"
286
+ end
287
+ super
288
+ end
289
+
290
+ def run
291
+ if FailureSimulator.fail_in_child_run
292
+ raise "Fail in child run"
293
+ end
294
+ end
295
+ end
296
+
297
+ let(:execution_plan) { world.trigger(ParentAction, count: 2).finished.value }
298
+
299
+ before do
300
+ FailureSimulator.reset!
301
+ end
302
+
303
+ specify "the sub-plan stores the information about its parent" do
304
+ sub_plans = execution_plan.sub_plans
305
+ sub_plans.size.must_equal 2
306
+ sub_plans.each { |sub_plan| sub_plan.caller_execution_plan_id.must_equal execution_plan.id }
307
+ end
308
+
309
+ specify "it saves the information about number for sub plans in the output" do
310
+ execution_plan.entry_action.output.must_equal('total_count' => 2,
311
+ 'failed_count' => 0,
312
+ 'success_count' => 2)
313
+ end
314
+
315
+ specify "when a sub plan fails, the caller action fails as well" do
316
+ FailureSimulator.fail_in_child_run = true
317
+ execution_plan.entry_action.output.must_equal('total_count' => 2,
318
+ 'failed_count' => 2,
319
+ 'success_count' => 0)
320
+ execution_plan.state.must_equal :paused
321
+ execution_plan.result.must_equal :error
322
+ end
323
+
324
+ describe 'resuming' do
325
+ specify "resuming the action depends on the resume method definition" do
326
+ FailureSimulator.fail_in_child_plan = true
327
+ execution_plan.state.must_equal :paused
328
+ FailureSimulator.fail_in_child_plan = false
329
+ resumed_plan = world.execute(execution_plan.id).value
330
+ resumed_plan.entry_action.output[:custom_resume].must_equal true
331
+ end
332
+
333
+ specify "by default, when no sub plans were planned successfully, it call create_sub_plans again" do
334
+ FailureSimulator.fail_in_child_plan = true
335
+ execution_plan.state.must_equal :paused
336
+ FailureSimulator.fail_in_child_plan = false
337
+ resumed_plan = world.execute(execution_plan.id).value
338
+ resumed_plan.state.must_equal :stopped
339
+ resumed_plan.result.must_equal :success
340
+ end
341
+
342
+ specify "by default, when any sub-plan was planned, it succeeds only when the sub-plans were already finished" do
343
+ FailureSimulator.fail_in_child_run = true
344
+ execution_plan.state.must_equal :paused
345
+ sub_plans = execution_plan.sub_plans
346
+
347
+ FailureSimulator.fail_in_child_run = false
348
+ resumed_plan = world.execute(execution_plan.id).value
349
+ resumed_plan.state.must_equal :paused
350
+
351
+ world.execute(sub_plans.first.id).wait
352
+ resumed_plan = world.execute(execution_plan.id).value
353
+ resumed_plan.state.must_equal :paused
354
+
355
+ sub_plans.drop(1).each { |sub_plan| world.execute(sub_plan.id).wait }
356
+ resumed_plan = world.execute(execution_plan.id).value
357
+ resumed_plan.state.must_equal :stopped
358
+ resumed_plan.result.must_equal :success
359
+ end
360
+ end
361
+ end
255
362
  end
256
363
  end
@@ -87,13 +87,13 @@ module PersistenceAdapterTest
87
87
  real_time: 0.0, execution_time: 0.0 }
88
88
  storage.save_execution_plan('plan1', plan)
89
89
 
90
- action = { id: 1 }
90
+ action = { id: 1, caller_execution_plan_id: nil, caller_action_id: nil }
91
91
  -> { storage.load_action('plan1', 1) }.must_raise KeyError
92
92
 
93
93
  storage.save_action('plan1', 1, action)
94
94
  storage.load_action('plan1', 1)[:id].must_equal 1
95
95
  storage.load_action('plan1', 1)['id'].must_equal 1
96
- storage.load_action('plan1', 1).keys.size.must_equal 1
96
+ storage.load_action('plan1', 1).keys.must_equal %w[id caller_execution_plan_id caller_action_id]
97
97
 
98
98
  storage.save_action('plan1', 1, nil)
99
99
  -> { storage.load_action('plan1', 1) }.must_raise KeyError