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.
- data/.gitignore +6 -0
- data/.travis.yml +9 -0
- data/Gemfile +0 -10
- data/MIT-LICENSE +1 -1
- data/README.md +99 -37
- data/Rakefile +2 -6
- data/doc/images/logo.png +0 -0
- data/dynflow.gemspec +10 -1
- data/examples/generate_work_for_daemon.rb +24 -0
- data/examples/orchestrate.rb +121 -0
- data/examples/run_daemon.rb +17 -0
- data/examples/web_console.rb +29 -0
- data/lib/dynflow.rb +27 -6
- data/lib/dynflow/action.rb +185 -77
- data/lib/dynflow/action/cancellable_polling.rb +18 -0
- data/lib/dynflow/action/finalize_phase.rb +18 -0
- data/lib/dynflow/action/flow_phase.rb +44 -0
- data/lib/dynflow/action/format.rb +46 -0
- data/lib/dynflow/action/missing.rb +26 -0
- data/lib/dynflow/action/plan_phase.rb +85 -0
- data/lib/dynflow/action/polling.rb +49 -0
- data/lib/dynflow/action/presenter.rb +51 -0
- data/lib/dynflow/action/progress.rb +62 -0
- data/lib/dynflow/action/run_phase.rb +43 -0
- data/lib/dynflow/action/suspended.rb +21 -0
- data/lib/dynflow/clock.rb +133 -0
- data/lib/dynflow/daemon.rb +29 -0
- data/lib/dynflow/execution_plan.rb +285 -33
- data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
- data/lib/dynflow/execution_plan/output_reference.rb +52 -0
- data/lib/dynflow/execution_plan/steps.rb +12 -0
- data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
- data/lib/dynflow/execution_plan/steps/error.rb +33 -0
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
- data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
- data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
- data/lib/dynflow/executors.rb +9 -0
- data/lib/dynflow/executors/abstract.rb +32 -0
- data/lib/dynflow/executors/parallel.rb +88 -0
- data/lib/dynflow/executors/parallel/core.rb +119 -0
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
- data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
- data/lib/dynflow/executors/parallel/pool.rb +102 -0
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
- data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
- data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
- data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
- data/lib/dynflow/executors/parallel/worker.rb +30 -0
- data/lib/dynflow/executors/remote_via_socket.rb +38 -0
- data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
- data/lib/dynflow/flows.rb +13 -0
- data/lib/dynflow/flows/abstract.rb +36 -0
- data/lib/dynflow/flows/abstract_composed.rb +104 -0
- data/lib/dynflow/flows/atom.rb +36 -0
- data/lib/dynflow/flows/concurrence.rb +28 -0
- data/lib/dynflow/flows/sequence.rb +13 -0
- data/lib/dynflow/future.rb +173 -0
- data/lib/dynflow/listeners.rb +7 -0
- data/lib/dynflow/listeners/abstract.rb +13 -0
- data/lib/dynflow/listeners/serialization.rb +41 -0
- data/lib/dynflow/listeners/socket.rb +88 -0
- data/lib/dynflow/logger_adapters.rb +8 -0
- data/lib/dynflow/logger_adapters/abstract.rb +30 -0
- data/lib/dynflow/logger_adapters/delegator.rb +13 -0
- data/lib/dynflow/logger_adapters/formatters.rb +8 -0
- data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
- data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
- data/lib/dynflow/logger_adapters/simple.rb +59 -0
- data/lib/dynflow/micro_actor.rb +102 -0
- data/lib/dynflow/persistence.rb +53 -0
- data/lib/dynflow/persistence_adapters.rb +6 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
- data/lib/dynflow/serializable.rb +66 -0
- data/lib/dynflow/simple_world.rb +18 -0
- data/lib/dynflow/stateful.rb +40 -0
- data/lib/dynflow/testing.rb +32 -0
- data/lib/dynflow/testing/assertions.rb +64 -0
- data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
- data/lib/dynflow/testing/dummy_executor.rb +29 -0
- data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
- data/lib/dynflow/testing/dummy_step.rb +19 -0
- data/lib/dynflow/testing/dummy_world.rb +33 -0
- data/lib/dynflow/testing/factories.rb +83 -0
- data/lib/dynflow/testing/managed_clock.rb +23 -0
- data/lib/dynflow/testing/mimic.rb +38 -0
- data/lib/dynflow/transaction_adapters.rb +9 -0
- data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
- data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
- data/lib/dynflow/transaction_adapters/none.rb +12 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +277 -0
- data/lib/dynflow/world.rb +168 -0
- data/test/action_test.rb +89 -11
- data/test/clock_test.rb +59 -0
- data/test/code_workflow_example.rb +382 -0
- data/test/execution_plan_test.rb +195 -64
- data/test/executor_test.rb +692 -0
- data/test/persistance_adapters_test.rb +173 -0
- data/test/test_helper.rb +316 -1
- data/test/testing_test.rb +148 -0
- data/test/web_console_test.rb +38 -0
- data/web/assets/javascripts/application.js +25 -0
- data/web/assets/stylesheets/application.css +101 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
- data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
- data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
- data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
- data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
- data/web/assets/vendor/jquery/jquery.js +9807 -0
- data/web/views/flow.erb +19 -0
- data/web/views/flow_step.erb +31 -0
- data/web/views/index.erb +39 -0
- data/web/views/layout.erb +20 -0
- data/web/views/plan_step.erb +11 -0
- data/web/views/show.erb +54 -0
- metadata +250 -11
- data/examples/events.rb +0 -71
- data/examples/workflow.rb +0 -140
- data/lib/dynflow/bus.rb +0 -168
- data/lib/dynflow/dispatcher.rb +0 -36
- data/lib/dynflow/logger.rb +0 -34
- data/lib/dynflow/step.rb +0 -234
- data/test/bus_test.rb +0 -150
data/test/execution_plan_test.rb
CHANGED
|
@@ -1,121 +1,252 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
plan_action(CloneRepo, {'name' => repo_name})
|
|
11
|
-
end
|
|
8
|
+
include PlanAssertions
|
|
9
|
+
include WorldInstance
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
+
let :execution_plan do
|
|
19
|
+
world.plan(CodeWorkflowExample::FastCommit, 'sha' => 'abc123')
|
|
18
20
|
end
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
let :deserialized_execution_plan do
|
|
23
|
+
world.persistence.load_execution_plan(execution_plan.id)
|
|
22
24
|
end
|
|
23
25
|
|
|
24
|
-
|
|
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
|
-
|
|
47
|
+
describe '#result' do
|
|
29
48
|
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
let :execution_plan do
|
|
50
|
+
world.plan(CodeWorkflowExample::FastCommit, 'sha' => 'abc123')
|
|
32
51
|
end
|
|
33
52
|
|
|
34
|
-
|
|
53
|
+
describe 'for error in planning phase' do
|
|
35
54
|
|
|
36
|
-
|
|
55
|
+
before { execution_plan.steps[2].set_state :error, true }
|
|
37
56
|
|
|
38
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
78
|
+
describe 'for pending step in running phase' do
|
|
49
79
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
describe 'persisted action' do
|
|
67
130
|
|
|
68
|
-
|
|
69
|
-
|
|
131
|
+
let :execution_plan do
|
|
132
|
+
world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
|
|
70
133
|
end
|
|
71
134
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
135
|
+
let :action do
|
|
136
|
+
step = execution_plan.steps[4]
|
|
137
|
+
world.persistence.load_action(step)
|
|
75
138
|
end
|
|
76
139
|
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
147
|
+
describe 'planning algorithm' do
|
|
82
148
|
|
|
83
|
-
|
|
149
|
+
describe 'single dependencies' do
|
|
150
|
+
let :execution_plan do
|
|
151
|
+
world.plan(CodeWorkflowExample::IncomingIssues, issues_data)
|
|
152
|
+
end
|
|
84
153
|
|
|
85
|
-
|
|
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
|
-
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
203
|
+
describe 'subscribed action' do
|
|
204
|
+
let :execution_plan do
|
|
205
|
+
world.plan(CodeWorkflowExample::DummyTrigger, {})
|
|
206
|
+
end
|
|
101
207
|
|
|
102
|
-
|
|
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
|
-
|
|
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
|