dynflow 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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