dynflow 0.1.0 → 0.2.0

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 (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -1,121 +1,252 @@
1
- require 'test_helper'
1
+ require_relative 'test_helper'
2
+ require_relative 'code_workflow_example'
2
3
 
3
4
  module Dynflow
4
5
  module ExecutionPlanTest
5
6
  describe ExecutionPlan do
6
- class Promotion < Action
7
7
 
8
- def plan(repo_names, package_names)
9
- repo_names.each do |repo_name|
10
- plan_action(CloneRepo, {'name' => repo_name})
11
- end
8
+ include PlanAssertions
9
+ include WorldInstance
12
10
 
13
- package_names.each do |package_name|
14
- plan_action(ClonePackage, {'name' => package_name})
15
- end
11
+ let :issues_data do
12
+ [{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
13
+ { 'author' => 'John Doe', 'text' => 'Internal server error' }]
14
+ end
15
+
16
+ describe 'serialization' do
16
17
 
17
- plan_self('actions' => repo_names.size + package_names.size)
18
+ let :execution_plan do
19
+ world.plan(CodeWorkflowExample::FastCommit, 'sha' => 'abc123')
18
20
  end
19
21
 
20
- input_format do
21
- param :actions, Integer
22
+ let :deserialized_execution_plan do
23
+ world.persistence.load_execution_plan(execution_plan.id)
22
24
  end
23
25
 
24
- def run; end
26
+ describe 'serialized execution plan' do
27
+
28
+ before { execution_plan.save }
29
+
30
+ it 'restores the plan properly' do
31
+ deserialized_execution_plan.id.must_equal execution_plan.id
32
+
33
+ assert_steps_equal execution_plan.root_plan_step, deserialized_execution_plan.root_plan_step
34
+ assert_equal execution_plan.steps.keys, deserialized_execution_plan.steps.keys
35
+
36
+ deserialized_execution_plan.steps.each do |id, step|
37
+ assert_steps_equal(step, execution_plan.steps[id])
38
+ end
39
+
40
+ assert_run_flow_equal execution_plan, deserialized_execution_plan
41
+ end
42
+
43
+ end
25
44
 
26
45
  end
27
46
 
28
- class PromotionObserver < Action
47
+ describe '#result' do
29
48
 
30
- def self.subscribe
31
- Promotion
49
+ let :execution_plan do
50
+ world.plan(CodeWorkflowExample::FastCommit, 'sha' => 'abc123')
32
51
  end
33
52
 
34
- def run; end
53
+ describe 'for error in planning phase' do
35
54
 
36
- end
55
+ before { execution_plan.steps[2].set_state :error, true }
37
56
 
38
- class CloneRepo < Action
57
+ it 'should be :error' do
58
+ execution_plan.result.must_equal :error
59
+ execution_plan.error?.must_equal true
60
+ end
39
61
 
40
- input_format do
41
- param :name, String
42
62
  end
43
63
 
44
- output_format do
45
- param :id, String
64
+
65
+ describe 'for error in running phase' do
66
+
67
+ before do
68
+ step_id = execution_plan.run_flow.all_step_ids[2]
69
+ execution_plan.steps[step_id].set_state :error, true
70
+ end
71
+
72
+ it 'should be :error' do
73
+ execution_plan.result.must_equal :error
74
+ end
75
+
46
76
  end
47
77
 
48
- def run; end
78
+ describe 'for pending step in running phase' do
49
79
 
50
- end
80
+ before do
81
+ step_id = execution_plan.run_flow.all_step_ids[2]
82
+ execution_plan.steps[step_id].set_state :pending, true
83
+ end
51
84
 
52
- class ClonePackage < Action
85
+ it 'should be :pending' do
86
+ execution_plan.result.must_equal :pending
87
+ end
53
88
 
54
- input_format do
55
- param :name, String
56
89
  end
57
90
 
58
- output_format do
59
- param :id, String
91
+ describe 'for all steps successful or skipped' do
92
+
93
+ before do
94
+ execution_plan.run_flow.all_step_ids.each_with_index do |step_id, index|
95
+ step = execution_plan.steps[step_id]
96
+ step.set_state (index == 2) ? :skipped : :success, true
97
+ end
98
+ end
99
+
100
+ it 'should be :success' do
101
+ execution_plan.result.must_equal :success
102
+ end
103
+
60
104
  end
61
105
 
62
- def run; end
106
+ end
107
+
108
+ describe 'plan steps' do
109
+ let :execution_plan do
110
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
111
+ end
112
+
113
+ it 'stores the information about the sub actions' do
114
+ assert_plan_steps <<-PLAN_STEPS, execution_plan
115
+ IncomingIssues
116
+ IncomingIssue
117
+ Triage
118
+ UpdateIssue
119
+ NotifyAssignee
120
+ IncomingIssue
121
+ Triage
122
+ UpdateIssue
123
+ NotifyAssignee
124
+ PLAN_STEPS
125
+ end
63
126
 
64
127
  end
65
128
 
66
- class UpdateIndex < Action
129
+ describe 'persisted action' do
67
130
 
68
- def self.subscribe
69
- ClonePackage
131
+ let :execution_plan do
132
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
70
133
  end
71
134
 
72
- def plan(input)
73
- plan_action(YetAnotherAction, {'hello' => 'world'})
74
- super
135
+ let :action do
136
+ step = execution_plan.steps[4]
137
+ world.persistence.load_action(step)
75
138
  end
76
139
 
77
- output_format do
78
- param :indexed_name, String
140
+ it 'stores the ids for plan, run and finalize steps' do
141
+ action.plan_step_id.must_equal 3
142
+ action.run_step_id.must_equal 4
143
+ action.finalize_step_id.must_equal 5
79
144
  end
145
+ end
80
146
 
81
- def run; end
147
+ describe 'planning algorithm' do
82
148
 
83
- end
149
+ describe 'single dependencies' do
150
+ let :execution_plan do
151
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
152
+ end
84
153
 
85
- class YetAnotherAction < Action
154
+ it 'constructs the plan of actions to be executed in run phase' do
155
+ assert_run_flow <<-RUN_FLOW, execution_plan
156
+ Dynflow::Flows::Concurrence
157
+ Dynflow::Flows::Sequence
158
+ 4: Triage(pending) {"author"=>"Peter Smith", "text"=>"Failing test"}
159
+ 7: UpdateIssue(pending) {"author"=>"Peter Smith", "text"=>"Failing test", "assignee"=>Step(4).output[:classification][:assignee], "severity"=>Step(4).output[:classification][:severity]}
160
+ 9: NotifyAssignee(pending) {"triage"=>Step(4).output}
161
+ Dynflow::Flows::Sequence
162
+ 13: Triage(pending) {"author"=>"John Doe", "text"=>"Internal server error"}
163
+ 16: UpdateIssue(pending) {"author"=>"John Doe", "text"=>"Internal server error", "assignee"=>Step(13).output[:classification][:assignee], "severity"=>Step(13).output[:classification][:severity]}
164
+ 18: NotifyAssignee(pending) {"triage"=>Step(13).output}
165
+ RUN_FLOW
166
+ end
86
167
 
87
- input_format do
88
- param :name, String
89
- param :hello, String
90
168
  end
91
169
 
92
- output_format do
93
- param :hello, String
170
+ describe 'multi dependencies' do
171
+ let :execution_plan do
172
+ world.plan(CodeWorkflowExample::Commit, 'sha' => 'abc123')
173
+ end
174
+
175
+ it 'constructs the plan of actions to be executed in run phase' do
176
+ assert_run_flow <<-RUN_FLOW, execution_plan
177
+ Dynflow::Flows::Sequence
178
+ Dynflow::Flows::Concurrence
179
+ 3: Ci(pending) {"commit"=>{"sha"=>"abc123"}}
180
+ 5: Review(pending) {"commit"=>{"sha"=>"abc123"}, "reviewer"=>"Morfeus", "result"=>true}
181
+ 7: Review(pending) {"commit"=>{"sha"=>"abc123"}, "reviewer"=>"Neo", "result"=>true}
182
+ 9: Merge(pending) {"commit"=>{"sha"=>"abc123"}, "ci_result"=>Step(3).output[:passed], "review_results"=>[Step(5).output[:passed], Step(7).output[:passed]]}
183
+ RUN_FLOW
184
+ end
94
185
  end
95
186
 
96
- def plan(arg)
97
- plan_self(input.merge(arg))
187
+ describe 'sequence and concurrence keyword used' do
188
+ let :execution_plan do
189
+ world.plan(CodeWorkflowExample::FastCommit, 'sha' => 'abc123')
190
+ end
191
+
192
+ it 'constructs the plan of actions to be executed in run phase' do
193
+ assert_run_flow <<-RUN_FLOW, execution_plan
194
+ Dynflow::Flows::Sequence
195
+ Dynflow::Flows::Concurrence
196
+ 3: Ci(pending) {"commit"=>{"sha"=>"abc123"}}
197
+ 5: Review(pending) {"commit"=>{"sha"=>"abc123"}, "reviewer"=>"Morfeus", "result"=>true}
198
+ 7: Merge(pending) {"commit"=>{"sha"=>"abc123"}, "ci_result"=>Step(3).output[:passed], "review_results"=>[Step(5).output[:passed]]}
199
+ RUN_FLOW
200
+ end
98
201
  end
99
202
 
100
- def run; end
203
+ describe 'subscribed action' do
204
+ let :execution_plan do
205
+ world.plan(CodeWorkflowExample::DummyTrigger, {})
206
+ end
101
207
 
102
- end
208
+ it 'constructs the plan of actions to be executed in run phase' do
209
+ assert_run_flow <<-RUN_FLOW, execution_plan
210
+ Dynflow::Flows::Concurrence
211
+ 3: DummySubscribe(pending) {}
212
+ 5: DummyMultiSubscribe(pending) {}
213
+ RUN_FLOW
214
+ end
215
+ end
216
+
217
+
218
+ describe 'finalize flow' do
219
+
220
+ let :execution_plan do
221
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
222
+ end
223
+
224
+ it 'plans the finalize steps in a sequence' do
225
+ assert_finalize_flow <<-RUN_FLOW, execution_plan
226
+ Dynflow::Flows::Sequence
227
+ 5: Triage(pending) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"}
228
+ 10: NotifyAssignee(pending) {\"triage\"=>Step(4).output}
229
+ 14: Triage(pending) {\"author\"=>\"John Doe\", \"text\"=>\"Internal server error\"}
230
+ 19: NotifyAssignee(pending) {\"triage\"=>Step(13).output}
231
+ 20: IncomingIssues(pending) {\"issues\"=>[{\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"}, {\"author\"=>\"John Doe\", \"text\"=>\"Internal server error\"}]}
232
+ RUN_FLOW
233
+ end
103
234
 
104
- it "builds the execution plan" do
105
- execution_plan = Promotion.plan(['zoo', 'foo'], ['elephant'])
106
- expected_plan_actions =
107
- [
108
- CloneRepo.new('name' => 'zoo'),
109
- CloneRepo.new('name' => 'foo'),
110
- ClonePackage.new('name' => 'elephant'),
111
- YetAnotherAction.new('name' => 'elephant', 'hello' => 'world'),
112
- UpdateIndex.new('name' => 'elephant'),
113
- Promotion.new('actions' => 3) ,
114
- PromotionObserver.new('actions' => 3)
115
- ]
116
- execution_plan.run_steps.map(&:action).must_equal expected_plan_actions
235
+ end
117
236
  end
118
237
 
238
+ describe 'accessing actions results' do
239
+ let :execution_plan do
240
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
241
+ end
242
+
243
+ it 'provides the access to the actions data via Action::Presenter' do
244
+ execution_plan.actions.size.must_equal 9
245
+ execution_plan.actions.each do |action|
246
+ action.must_be_kind_of Action::Presenter
247
+ end
248
+ end
249
+ end
119
250
  end
120
251
  end
121
252
  end
@@ -0,0 +1,692 @@
1
+ require_relative 'test_helper'
2
+ require_relative 'code_workflow_example'
3
+
4
+ module Dynflow
5
+ module ExecutorTest
6
+ describe "executor" do
7
+
8
+ include PlanAssertions
9
+
10
+ [:world, :remote_world].each do |world_method|
11
+
12
+ describe world_method.to_s do
13
+
14
+ let(:world) { WorldInstance.send world_method }
15
+
16
+ let :issues_data do
17
+ [{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
18
+ { 'author' => 'John Doe', 'text' => 'Internal server error' }]
19
+ end
20
+
21
+ let :failing_issues_data do
22
+ [{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
23
+ { 'author' => 'John Doe', 'text' => 'trolling' }]
24
+ end
25
+
26
+ let :finalize_failing_issues_data do
27
+ [{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
28
+ { 'author' => 'John Doe', 'text' => 'trolling in finalize' }]
29
+ end
30
+
31
+ let :execution_plan do
32
+ world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
33
+ end
34
+
35
+ let :failed_execution_plan do
36
+ plan = world.plan(CodeWorkflowExample::IncomingIssues, failing_issues_data)
37
+ plan = world.execute(plan.id).value
38
+ plan.state.must_equal :paused
39
+ plan
40
+ end
41
+
42
+ let :finalize_failed_execution_plan do
43
+ plan = world.plan(CodeWorkflowExample::IncomingIssues, finalize_failing_issues_data)
44
+ plan = world.execute(plan.id).value
45
+ plan.state.must_equal :paused
46
+ plan
47
+ end
48
+
49
+ let :persisted_plan do
50
+ world.persistence.load_execution_plan(execution_plan.id)
51
+ end
52
+
53
+ let :executor_class do
54
+ Executors::Parallel
55
+ end
56
+
57
+ describe "execution plan state" do
58
+
59
+ describe "after successful planning" do
60
+
61
+ it "is pending" do
62
+ execution_plan.state.must_equal :planned
63
+ end
64
+
65
+ end
66
+
67
+ describe "after error in planning" do
68
+
69
+ class FailingAction < Dynflow::Action
70
+ def plan
71
+ raise "I failed"
72
+ end
73
+ end
74
+
75
+ let :execution_plan do
76
+ world.plan(FailingAction)
77
+ end
78
+
79
+ it "is stopped" do
80
+ execution_plan.state.must_equal :stopped
81
+ end
82
+
83
+ end
84
+
85
+ describe "when being executed" do
86
+
87
+ let :execution_plan do
88
+ world.plan(CodeWorkflowExample::IncomingIssue, { 'text' => 'get a break' })
89
+ end
90
+
91
+ before do
92
+ TestPause.setup
93
+ world.execute(execution_plan.id)
94
+ end
95
+
96
+ after do
97
+ TestPause.teardown
98
+ end
99
+
100
+ it "is running" do
101
+ TestPause.when_paused do
102
+ plan = world.persistence.load_execution_plan(execution_plan.id)
103
+ plan.state.must_equal :running
104
+ triage = plan.steps.values.find do |s|
105
+ s.is_a?(Dynflow::ExecutionPlan::Steps::RunStep) &&
106
+ s.action_class == Dynflow::CodeWorkflowExample::Triage
107
+ end
108
+ triage.state.must_equal :running
109
+ world.persistence.
110
+ load_step(triage.execution_plan_id, triage.id, world).
111
+ state.must_equal :running
112
+ end
113
+ end
114
+
115
+ it "fails when trying to execute again" do
116
+ TestPause.when_paused do
117
+ assert_raises(Dynflow::Error) { world.execute(execution_plan.id) }
118
+ end
119
+ end
120
+ end
121
+
122
+ describe "when finished successfully" do
123
+
124
+ it "is stopped" do
125
+ world.execute(execution_plan.id).value.tap do |plan|
126
+ plan.state.must_equal :stopped
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "when finished with error" do
132
+ it "is paused" do
133
+ world.execute(failed_execution_plan.id).value.tap do |plan|
134
+ plan.state.must_equal :paused
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ describe "execution of run flow" do
141
+
142
+ before do
143
+ TestExecutionLog.setup
144
+ end
145
+
146
+ let :result do
147
+ world.execute(execution_plan.id).value.tap do |result|
148
+ raise result if result.is_a? Exception
149
+ end
150
+ end
151
+
152
+ after do
153
+ TestExecutionLog.teardown
154
+ end
155
+
156
+ let :persisted_plan do
157
+ result
158
+ world.persistence.load_execution_plan(execution_plan.id)
159
+ end
160
+
161
+ describe 'cancellable action' do
162
+
163
+ describe 'successful' do
164
+ let :execution_plan do
165
+ world.plan(CodeWorkflowExample::CancelableSuspended, {})
166
+ end
167
+
168
+ it "doesn't cause problems" do
169
+ result.result.must_equal :success
170
+ result.state.must_equal :stopped
171
+ end
172
+ end
173
+
174
+ describe 'canceled' do
175
+ let :execution_plan do
176
+ world.plan(CodeWorkflowExample::CancelableSuspended, { text: 'cancel' })
177
+ end
178
+
179
+ it 'cancels' do
180
+ result.result.must_equal :success
181
+ result.state.must_equal :stopped
182
+ action = world.persistence.load_action result.steps[2]
183
+ action.output[:progress].must_equal 30
184
+ action.output[:cancelled].must_equal true
185
+ end
186
+ end
187
+
188
+ describe 'canceled failed' do
189
+ let :execution_plan do
190
+ world.plan(CodeWorkflowExample::CancelableSuspended, { text: 'cancel fail' })
191
+ end
192
+
193
+ it 'fails' do
194
+ result.result.must_equal :error
195
+ result.state.must_equal :paused
196
+ step = result.steps[2]
197
+ step.error.message.must_equal 'action cancelled'
198
+ action = world.persistence.load_action step
199
+ action.output[:progress].must_equal 30
200
+ end
201
+ end
202
+ end
203
+
204
+
205
+ describe "suspended action" do
206
+ let :execution_plan do
207
+ world.plan(CodeWorkflowExample::DummySuspended, { :external_task_id => '123' })
208
+ end
209
+
210
+ it "doesn't cause problems" do
211
+ result.result.must_equal :success
212
+ result.state.must_equal :stopped
213
+ end
214
+
215
+ it 'does set times' do
216
+ result.started_at.wont_be_nil
217
+ result.ended_at.wont_be_nil
218
+ result.execution_time.must_be :<, result.real_time
219
+ result.execution_time.must_equal(
220
+ result.steps.inject(0) { |sum, (_, step)| sum + step.execution_time })
221
+
222
+ plan_step = result.steps[1]
223
+ plan_step.started_at.wont_be_nil
224
+ plan_step.ended_at.wont_be_nil
225
+ plan_step.execution_time.must_equal plan_step.real_time
226
+
227
+ run_step = result.steps[2]
228
+ run_step.started_at.wont_be_nil
229
+ run_step.ended_at.wont_be_nil
230
+ run_step.execution_time.must_be :<, run_step.real_time
231
+ end
232
+
233
+ describe 'handling errors in setup' do
234
+ let :execution_plan do
235
+ world.plan(CodeWorkflowExample::DummySuspended,
236
+ external_task_id: '123',
237
+ text: 'troll setup')
238
+ end
239
+
240
+ it 'fails' do
241
+ assert_equal :error, result.result
242
+ assert_equal :paused, result.state
243
+ assert_equal :error,
244
+ result.steps.values.
245
+ find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.
246
+ state
247
+ end
248
+ end
249
+
250
+ describe 'progress' do
251
+ before do
252
+ TestPause.setup
253
+ @running_plan = world.execute(execution_plan.id)
254
+ end
255
+
256
+ after do
257
+ @running_plan.wait
258
+ TestPause.teardown
259
+ end
260
+
261
+ describe 'plan with one action' do
262
+ let :execution_plan do
263
+ world.plan(CodeWorkflowExample::DummySuspended,
264
+ { external_task_id: '123',
265
+ text: 'pause in progress 20%' })
266
+ end
267
+
268
+ it 'determines the progress of the execution plan in percents' do
269
+ TestPause.when_paused do
270
+ plan = world.persistence.load_execution_plan(execution_plan.id)
271
+ plan.progress.round(2).must_equal 0.2
272
+ end
273
+ end
274
+ end
275
+
276
+ describe 'plan with more action' do
277
+ let :execution_plan do
278
+ world.plan(CodeWorkflowExample::DummyHeavyProgress,
279
+ { external_task_id: '123',
280
+ text: 'pause in progress 20%' })
281
+ end
282
+
283
+ it 'takes the steps weight in account' do
284
+ TestPause.when_paused do
285
+ plan = world.persistence.load_execution_plan(execution_plan.id)
286
+ plan.progress.round(2).must_equal 0.42
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ describe 'works when resumed after error' do
293
+ let :execution_plan do
294
+ world.plan(CodeWorkflowExample::DummySuspended,
295
+ { external_task_id: '123',
296
+ text: 'troll progress' })
297
+ end
298
+
299
+ specify do
300
+ assert_equal :paused, result.state
301
+ assert_equal :error, result.result
302
+ assert_equal :error, result.steps.values.
303
+ find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.state
304
+
305
+ ep = world.execute(result.id).value
306
+ assert_equal :stopped, ep.state
307
+ assert_equal :success, ep.result
308
+ assert_equal :success, ep.steps.values.
309
+ find { |s| s.is_a? Dynflow::ExecutionPlan::Steps::RunStep }.state
310
+ end
311
+ end
312
+
313
+ end
314
+
315
+ describe "action with empty flows" do
316
+
317
+ let :execution_plan do
318
+ world.plan(CodeWorkflowExample::Dummy, { :text => "dummy" }).tap do |plan|
319
+ assert_equal plan.run_flow.size, 0
320
+ assert_equal plan.finalize_flow.size, 0
321
+ end.tap do |w|
322
+ w
323
+ end
324
+ end
325
+
326
+ it "doesn't cause problems" do
327
+ result.result.must_equal :success
328
+ result.state.must_equal :stopped
329
+ end
330
+
331
+ it 'will not run again' do
332
+ world.execute(execution_plan.id)
333
+ assert_raises(Dynflow::Error) { world.execute(execution_plan.id).value! }
334
+ end
335
+
336
+ end
337
+
338
+ describe 'action with empty run flow but some finalize flow' do
339
+
340
+ let :execution_plan do
341
+ world.plan(CodeWorkflowExample::DummyWithFinalize, { :text => "dummy" }).tap do |plan|
342
+ assert_equal plan.run_flow.size, 0
343
+ assert_equal plan.finalize_flow.size, 1
344
+ end
345
+ end
346
+
347
+ it "doesn't cause problems" do
348
+ result.result.must_equal :success
349
+ result.state.must_equal :stopped
350
+ end
351
+
352
+ end
353
+
354
+ it "runs all the steps in the run flow" do
355
+ assert_run_flow <<-EXECUTED_RUN_FLOW, persisted_plan
356
+ Dynflow::Flows::Concurrence
357
+ Dynflow::Flows::Sequence
358
+ 4: Triage(success) {"author"=>"Peter Smith", "text"=>"Failing test"} --> {"classification"=>{"assignee"=>"John Doe", "severity"=>"medium"}}
359
+ 7: UpdateIssue(success) {"author"=>"Peter Smith", "text"=>"Failing test", "assignee"=>"John Doe", "severity"=>"medium"} --> {}
360
+ 9: NotifyAssignee(success) {"triage"=>{"classification"=>{"assignee"=>"John Doe", "severity"=>"medium"}}} --> {}
361
+ Dynflow::Flows::Sequence
362
+ 13: Triage(success) {"author"=>"John Doe", "text"=>"Internal server error"} --> {"classification"=>{"assignee"=>"John Doe", "severity"=>"medium"}}
363
+ 16: UpdateIssue(success) {"author"=>"John Doe", "text"=>"Internal server error", "assignee"=>"John Doe", "severity"=>"medium"} --> {}
364
+ 18: NotifyAssignee(success) {"triage"=>{"classification"=>{"assignee"=>"John Doe", "severity"=>"medium"}}} --> {}
365
+ EXECUTED_RUN_FLOW
366
+ end
367
+
368
+ end
369
+
370
+ describe "execution of finalize flow" do
371
+
372
+ before do
373
+ TestExecutionLog.setup
374
+ result = world.execute(execution_plan.id).value
375
+ raise result if result.is_a? Exception
376
+ end
377
+
378
+ after do
379
+ TestExecutionLog.teardown
380
+ end
381
+
382
+ describe "when run flow successful" do
383
+
384
+ it "runs all the steps in the finalize flow" do
385
+ assert_finalized(Dynflow::CodeWorkflowExample::IncomingIssues,
386
+ { "issues" => [{ "author" => "Peter Smith", "text" => "Failing test" }, { "author" => "John Doe", "text" => "Internal server error" }] })
387
+ assert_finalized(Dynflow::CodeWorkflowExample::Triage,
388
+ { "author" => "Peter Smith", "text" => "Failing test" })
389
+ end
390
+ end
391
+
392
+ describe "when run flow failed" do
393
+
394
+ let :execution_plan do
395
+ failed_execution_plan
396
+ end
397
+
398
+ it "doesn't run the steps in the finalize flow" do
399
+ TestExecutionLog.finalize.size.must_equal 0
400
+ end
401
+ end
402
+
403
+ end
404
+
405
+ describe "re-execution of run flow after fix in run phase" do
406
+
407
+ after do
408
+ TestExecutionLog.teardown
409
+ end
410
+
411
+ let :resumed_execution_plan do
412
+ failed_step = failed_execution_plan.steps.values.find do |step|
413
+ step.state == :error
414
+ end
415
+ world.persistence.load_action(failed_step).tap do |action|
416
+ action.input[:text] = "ok"
417
+ world.persistence.save_action(failed_step.execution_plan_id, action)
418
+ end
419
+ TestExecutionLog.setup
420
+ world.execute(failed_execution_plan.id).value
421
+ end
422
+
423
+ it "runs all the steps in the run flow" do
424
+ resumed_execution_plan.state.must_equal :stopped
425
+ resumed_execution_plan.result.must_equal :success
426
+
427
+ run_triages = TestExecutionLog.run.find_all do |action_class, input|
428
+ action_class == CodeWorkflowExample::Triage
429
+ end
430
+ run_triages.size.must_equal 1
431
+
432
+ assert_run_flow <<-EXECUTED_RUN_FLOW, resumed_execution_plan
433
+ Dynflow::Flows::Concurrence
434
+ Dynflow::Flows::Sequence
435
+ 4: Triage(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
436
+ 7: UpdateIssue(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\", \"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"} --> {}
437
+ 9: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
438
+ Dynflow::Flows::Sequence
439
+ 13: Triage(success) {\"author\"=>\"John Doe\", \"text\"=>\"ok\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
440
+ 16: UpdateIssue(success) {\"author\"=>\"John Doe\", \"text\"=>\"trolling\", \"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"} --> {}
441
+ 18: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
442
+ EXECUTED_RUN_FLOW
443
+ end
444
+
445
+ end
446
+ describe "re-execution of run flow after fix in finalize phase" do
447
+
448
+ after do
449
+ TestExecutionLog.teardown
450
+ end
451
+
452
+ let :resumed_execution_plan do
453
+ failed_step = finalize_failed_execution_plan.steps.values.find do |step|
454
+ step.state == :error
455
+ end
456
+ world.persistence.load_action(failed_step).tap do |action|
457
+ action.input[:text] = "ok"
458
+ world.persistence.save_action(failed_step.execution_plan_id, action)
459
+ end
460
+ TestExecutionLog.setup
461
+ world.execute(finalize_failed_execution_plan.id).value
462
+ end
463
+
464
+ it "runs all the steps in the finalize flow" do
465
+ resumed_execution_plan.state.must_equal :stopped
466
+ resumed_execution_plan.result.must_equal :success
467
+
468
+ run_triages = TestExecutionLog.finalize.find_all do |action_class, input|
469
+ action_class == CodeWorkflowExample::Triage
470
+ end
471
+ run_triages.size.must_equal 2
472
+
473
+ assert_finalize_flow <<-EXECUTED_RUN_FLOW, resumed_execution_plan
474
+ Dynflow::Flows::Sequence
475
+ 5: Triage(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
476
+ 10: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
477
+ 14: Triage(success) {\"author\"=>\"John Doe\", \"text\"=>\"ok\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
478
+ 19: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
479
+ 20: IncomingIssues(success) {\"issues\"=>[{\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"}, {\"author\"=>\"John Doe\", \"text\"=>\"trolling in finalize\"}]} --> {}
480
+ EXECUTED_RUN_FLOW
481
+ end
482
+
483
+ end
484
+
485
+ describe "re-execution of run flow after skipping" do
486
+
487
+ after do
488
+ TestExecutionLog.teardown
489
+ end
490
+
491
+ let :resumed_execution_plan do
492
+ failed_step = failed_execution_plan.steps.values.find do |step|
493
+ step.state == :error
494
+ end
495
+ failed_execution_plan.skip(failed_step)
496
+ TestExecutionLog.setup
497
+ world.execute(failed_execution_plan.id).value
498
+ end
499
+
500
+ it "runs all pending steps except skipped" do
501
+ resumed_execution_plan.state.must_equal :stopped
502
+ resumed_execution_plan.result.must_equal :success
503
+
504
+ run_triages = TestExecutionLog.run.find_all do |action_class, input|
505
+ action_class == CodeWorkflowExample::Triage
506
+ end
507
+ run_triages.size.must_equal 0
508
+
509
+ assert_run_flow <<-EXECUTED_RUN_FLOW, resumed_execution_plan
510
+ Dynflow::Flows::Concurrence
511
+ Dynflow::Flows::Sequence
512
+ 4: Triage(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
513
+ 7: UpdateIssue(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\", \"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"} --> {}
514
+ 9: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
515
+ Dynflow::Flows::Sequence
516
+ 13: Triage(skipped) {\"author\"=>\"John Doe\", \"text\"=>\"trolling\"} --> {}
517
+ 16: UpdateIssue(skipped) {\"author\"=>\"John Doe\", \"text\"=>\"trolling\", \"assignee\"=>Step(13).output[:classification][:assignee], \"severity\"=>Step(13).output[:classification][:severity]} --> {}
518
+ 18: NotifyAssignee(skipped) {\"triage\"=>Step(13).output} --> {}
519
+ EXECUTED_RUN_FLOW
520
+
521
+ assert_finalize_flow <<-FINALIZE_FLOW, resumed_execution_plan
522
+ Dynflow::Flows::Sequence
523
+ 5: Triage(success) {\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"} --> {\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}
524
+ 10: NotifyAssignee(success) {\"triage\"=>{\"classification\"=>{\"assignee\"=>\"John Doe\", \"severity\"=>\"medium\"}}} --> {}
525
+ 14: Triage(skipped) {\"author\"=>\"John Doe\", \"text\"=>\"trolling\"} --> {}
526
+ 19: NotifyAssignee(skipped) {\"triage\"=>Step(13).output} --> {}
527
+ 20: IncomingIssues(success) {\"issues\"=>[{\"author\"=>\"Peter Smith\", \"text\"=>\"Failing test\"}, {\"author\"=>\"John Doe\", \"text\"=>\"trolling\"}]} --> {}
528
+ FINALIZE_FLOW
529
+
530
+ end
531
+ end
532
+
533
+ describe 'FlowManager' do
534
+ let(:manager) { Executors::Parallel::FlowManager.new execution_plan, execution_plan.run_flow }
535
+
536
+ def assert_next_steps(expected_next_step_ids, finished_step_id = nil, success = true)
537
+ if finished_step_id
538
+ step = manager.execution_plan.steps[finished_step_id]
539
+ next_steps = manager.cursor_index[step.id].what_is_next(step, success)
540
+ else
541
+ next_steps = manager.start
542
+ end
543
+ next_step_ids = next_steps.map(&:id)
544
+ assert_equal Set.new(expected_next_step_ids), Set.new(next_step_ids)
545
+ end
546
+
547
+ describe 'what_is_next' do
548
+ it 'returns next steps after required steps were finished' do
549
+ assert_next_steps([4, 13])
550
+ assert_next_steps([7], 4)
551
+ assert_next_steps([9], 7)
552
+ assert_next_steps([], 9)
553
+ assert_next_steps([16], 13)
554
+ assert_next_steps([18], 16)
555
+ assert_next_steps([], 18)
556
+ assert manager.done?
557
+ end
558
+ end
559
+
560
+ describe 'what_is_next with errors' do
561
+
562
+ it "doesn't return next steps if requirements failed" do
563
+ assert_next_steps([4, 13])
564
+ assert_next_steps([], 4, false)
565
+ end
566
+
567
+
568
+ it "is not done while other steps can be finished" do
569
+ assert_next_steps([4, 13])
570
+ assert_next_steps([], 4, false)
571
+ assert !manager.done?
572
+ assert_next_steps([], 13, false)
573
+ assert manager.done?
574
+ end
575
+ end
576
+
577
+ end
578
+
579
+ describe 'Pool::RoundRobin' do
580
+ let(:rr) { Dynflow::Executors::Parallel::Pool::RoundRobin.new }
581
+ it do
582
+ rr.next.must_be_nil
583
+ rr.next.must_be_nil
584
+ rr.must_be_empty
585
+ rr.add 1
586
+ rr.next.must_equal 1
587
+ rr.next.must_equal 1
588
+ rr.add 2
589
+ rr.next.must_equal 2
590
+ rr.next.must_equal 1
591
+ rr.next.must_equal 2
592
+ rr.delete 1
593
+ rr.next.must_equal 2
594
+ rr.next.must_equal 2
595
+ rr.delete 2
596
+ rr.next.must_be_nil
597
+ rr.must_be_empty
598
+ end
599
+ end
600
+
601
+ describe 'Pool::JobStorage' do
602
+ FakeStep ||= Struct.new(:execution_plan_id)
603
+
604
+ let(:storage) { Dynflow::Executors::Parallel::Pool::JobStorage.new }
605
+ it do
606
+ storage.must_be_empty
607
+ storage.pop.must_be_nil
608
+ storage.pop.must_be_nil
609
+
610
+ storage.add s = FakeStep.new(1)
611
+ storage.pop.must_equal s
612
+ storage.must_be_empty
613
+ storage.pop.must_be_nil
614
+
615
+ storage.add s11 = FakeStep.new(1)
616
+ storage.add s12 = FakeStep.new(1)
617
+ storage.add s13 = FakeStep.new(1)
618
+ storage.add s21 = FakeStep.new(2)
619
+ storage.add s22 = FakeStep.new(2)
620
+ storage.add s31 = FakeStep.new(3)
621
+
622
+ storage.pop.must_equal s21
623
+ storage.pop.must_equal s31
624
+ storage.pop.must_equal s11
625
+ storage.pop.must_equal s22
626
+ storage.pop.must_equal s12
627
+ storage.pop.must_equal s13
628
+
629
+ storage.must_be_empty
630
+ storage.pop.must_be_nil
631
+ end
632
+ end
633
+
634
+ end
635
+ end
636
+
637
+ describe 'termination' do
638
+ let(:normal_world) { WorldInstance.create_world }
639
+ let(:remote_world) { WorldInstance.create_remote_world(normal_world).last }
640
+
641
+ [:normal_world, :remote_world].each do |which|
642
+ describe which do
643
+ let(:world) { self.send which }
644
+
645
+ if which == :normal_world
646
+ it 'executes until its done when terminating' do
647
+ $slow_actions_done = 0
648
+ world.trigger(CodeWorkflowExample::Slow, 0.02)
649
+ world.terminate.wait
650
+ $slow_actions_done.must_equal 1
651
+ end
652
+
653
+ it 'executes until its done when terminating even suspended' do
654
+ result = world.trigger(CodeWorkflowExample::DummySuspended,
655
+ external_task_id: '123',
656
+ text: 'none')
657
+ world.terminate.wait
658
+ assert result.finished.ready?
659
+ end
660
+ end
661
+
662
+ it 'does not accept new work' do
663
+ assert world.terminate.wait
664
+ -> { world.trigger(CodeWorkflowExample::Slow, 0.02) }.must_raise Dynflow::Error
665
+ end
666
+
667
+ it 'it terminates when no work' do
668
+ skip 'blocks occasionally' if which == :remote_world # FIXME
669
+ world.trigger(CodeWorkflowExample::Slow, 0.02).finished.wait
670
+ assert world.terminate.wait
671
+ end
672
+
673
+ it 'it terminates when no work right after initialization' do
674
+ assert world.terminate.wait
675
+ end
676
+
677
+ it 'second terminate works' do
678
+ assert world.terminate.wait
679
+ assert world.terminate.wait
680
+ end
681
+
682
+ it 'second terminate works concurrently' do
683
+ assert [world.terminate, world.terminate].map(&:value).all?
684
+ end
685
+
686
+ end
687
+ end
688
+
689
+ end
690
+ end
691
+ end
692
+ end