dynflow 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,168 @@
|
|
1
|
+
module Dynflow
|
2
|
+
class World
|
3
|
+
include Algebrick::TypeCheck
|
4
|
+
|
5
|
+
attr_reader :executor, :persistence, :transaction_adapter, :action_classes, :subscription_index,
|
6
|
+
:logger_adapter, :options
|
7
|
+
|
8
|
+
def initialize(options_hash = {})
|
9
|
+
@options = default_options.merge options_hash
|
10
|
+
@logger_adapter = Type! option_val(:logger_adapter), LoggerAdapters::Abstract
|
11
|
+
@transaction_adapter = Type! option_val(:transaction_adapter), TransactionAdapters::Abstract
|
12
|
+
persistence_adapter = Type! option_val(:persistence_adapter), PersistenceAdapters::Abstract
|
13
|
+
@persistence = Persistence.new(self, persistence_adapter)
|
14
|
+
@executor = Type! option_val(:executor), Executors::Abstract
|
15
|
+
@action_classes = option_val(:action_classes)
|
16
|
+
calculate_subscription_index
|
17
|
+
|
18
|
+
executor.initialized.wait
|
19
|
+
@termination_barrier = Mutex.new
|
20
|
+
|
21
|
+
transaction_adapter.check self
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_options
|
25
|
+
@default_options ||=
|
26
|
+
{ action_classes: Action.all_children,
|
27
|
+
logger_adapter: LoggerAdapters::Simple.new,
|
28
|
+
executor: -> world { Executors::Parallel.new(world, options[:pool_size]) } }
|
29
|
+
end
|
30
|
+
|
31
|
+
def clock
|
32
|
+
@clock ||= Clock.new(logger)
|
33
|
+
end
|
34
|
+
|
35
|
+
def logger
|
36
|
+
logger_adapter.dynflow_logger
|
37
|
+
end
|
38
|
+
|
39
|
+
def action_logger
|
40
|
+
logger_adapter.action_logger
|
41
|
+
end
|
42
|
+
|
43
|
+
def subscribed_actions(action_class)
|
44
|
+
@subscription_index.has_key?(action_class) ? @subscription_index[action_class] : []
|
45
|
+
end
|
46
|
+
|
47
|
+
# reload actions classes, intended only for devel
|
48
|
+
def reload!
|
49
|
+
@action_classes.map! { |klass| klass.to_s.constantize }
|
50
|
+
calculate_subscription_index
|
51
|
+
end
|
52
|
+
|
53
|
+
class TriggerResult
|
54
|
+
include Algebrick::TypeCheck
|
55
|
+
|
56
|
+
attr_reader :execution_plan_id, :planned, :finished
|
57
|
+
alias_method :id, :execution_plan_id
|
58
|
+
alias_method :planned?, :planned
|
59
|
+
|
60
|
+
def initialize(execution_plan_id, planned, finished)
|
61
|
+
@execution_plan_id = Type! execution_plan_id, String
|
62
|
+
@planned = Type! planned, TrueClass, FalseClass
|
63
|
+
@finished = Type! finished, Future
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_a
|
67
|
+
[execution_plan_id, planned, finished]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [TriggerResult]
|
72
|
+
# blocks until action_class is planned
|
73
|
+
def trigger(action_class, *args)
|
74
|
+
execution_plan = plan(action_class, *args)
|
75
|
+
planned = execution_plan.state == :planned
|
76
|
+
finished = if planned
|
77
|
+
execute(execution_plan.id)
|
78
|
+
else
|
79
|
+
Future.new.resolve(execution_plan)
|
80
|
+
end
|
81
|
+
return TriggerResult.new(execution_plan.id, planned, finished)
|
82
|
+
end
|
83
|
+
|
84
|
+
def event(execution_plan_id, step_id, event, future = Future.new)
|
85
|
+
executor.event execution_plan_id, step_id, event, future
|
86
|
+
end
|
87
|
+
|
88
|
+
def plan(action_class, *args)
|
89
|
+
ExecutionPlan.new(self).tap do |execution_plan|
|
90
|
+
execution_plan.prepare(action_class)
|
91
|
+
execution_plan.plan(*args)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Future] containing execution_plan when finished
|
96
|
+
# raises when ExecutionPlan is not accepted for execution
|
97
|
+
def execute(execution_plan_id, finished = Future.new)
|
98
|
+
executor.execute execution_plan_id, finished
|
99
|
+
end
|
100
|
+
|
101
|
+
def terminate(future = Future.new)
|
102
|
+
@termination_barrier.synchronize do
|
103
|
+
if @executor_terminated.nil?
|
104
|
+
@executor_terminated = Future.new
|
105
|
+
@clock_terminated = Future.new
|
106
|
+
executor.terminate(@executor_terminated).
|
107
|
+
do_then { clock.ask(MicroActor::Terminate, @clock_terminated) }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
Future.join([@executor_terminated, @clock_terminated], future)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Detects execution plans that are marked as running but no executor
|
114
|
+
# handles them (probably result of non-standard executor termination)
|
115
|
+
#
|
116
|
+
# The current implementation expects no execution_plan being actually run
|
117
|
+
# by the executor.
|
118
|
+
#
|
119
|
+
# TODO: persist the running executors in the system, so that we can detect
|
120
|
+
# the orphaned execution plans. The register should be managable by the
|
121
|
+
# console, so that the administrator can unregister dead executors when needed.
|
122
|
+
# After the executor is unregistered, the consistency check should be performed
|
123
|
+
# to fix the orphaned plans as well.
|
124
|
+
def consistency_check
|
125
|
+
abnormal_execution_plans = self.persistence.find_execution_plans filters: { 'state' => %w(running planning) }
|
126
|
+
if abnormal_execution_plans.empty?
|
127
|
+
logger.info 'Clean start.'
|
128
|
+
else
|
129
|
+
format_str = '%36s %10s %10s'
|
130
|
+
message = ['Abnormal execution plans, process was probably killed.',
|
131
|
+
'Following ExecutionPlans will be set to paused, admin has to fix them manually.',
|
132
|
+
(format format_str, 'ExecutionPlan', 'state', 'result'),
|
133
|
+
*(abnormal_execution_plans.map { |ep| format format_str, ep.id, ep.state, ep.result })]
|
134
|
+
|
135
|
+
logger.error message.join("\n")
|
136
|
+
|
137
|
+
abnormal_execution_plans.each do |ep|
|
138
|
+
ep.update_state case ep.state
|
139
|
+
when :planning
|
140
|
+
:stopped
|
141
|
+
when :running
|
142
|
+
:paused
|
143
|
+
else
|
144
|
+
raise
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
private
|
151
|
+
|
152
|
+
def calculate_subscription_index
|
153
|
+
@subscription_index = action_classes.each_with_object(Hash.new { |h, k| h[k] = [] }) do |klass, index|
|
154
|
+
next unless klass.subscribe
|
155
|
+
Array(klass.subscribe).each { |subscribed_class| index[subscribed_class.to_s.constantize] << klass }
|
156
|
+
end.tap { |o| o.freeze }
|
157
|
+
end
|
158
|
+
|
159
|
+
def option_val(key)
|
160
|
+
val = options.fetch(key)
|
161
|
+
if val.is_a? Proc
|
162
|
+
options[key] = val.call(self)
|
163
|
+
else
|
164
|
+
val
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
data/test/action_test.rb
CHANGED
@@ -1,25 +1,103 @@
|
|
1
|
-
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require_relative 'code_workflow_example'
|
2
3
|
|
3
4
|
module Dynflow
|
4
|
-
class ActionTest < Action
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
|
7
|
+
describe Action::Missing do
|
8
|
+
include WorldInstance
|
9
|
+
|
10
|
+
let :action_data do
|
11
|
+
{ class: 'RenamedAction',
|
12
|
+
id: 123,
|
13
|
+
input: {},
|
14
|
+
execution_plan_id: 123 }
|
8
15
|
end
|
9
16
|
|
10
|
-
|
11
|
-
|
17
|
+
subject do
|
18
|
+
state_holder = ExecutionPlan::Steps::Abstract.allocate
|
19
|
+
state_holder.set_state :success, true
|
20
|
+
Action.from_hash(action_data, :run_phase, state_holder, world)
|
12
21
|
end
|
13
22
|
|
23
|
+
specify { subject.action_class.name.must_equal 'RenamedAction' }
|
24
|
+
specify { assert subject.is_a? Action }
|
14
25
|
end
|
15
26
|
|
16
|
-
describe
|
27
|
+
describe "extending action phase" do
|
28
|
+
|
29
|
+
module TestExtending
|
30
|
+
|
31
|
+
module Extension
|
32
|
+
def new_method
|
33
|
+
end
|
34
|
+
end
|
17
35
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
36
|
+
class ExtendedAction < Dynflow::Action
|
37
|
+
def self.phase_modules
|
38
|
+
super.merge(run_phase: [Extension]) { |key, old, new| old + new }.freeze
|
39
|
+
end
|
40
|
+
end
|
22
41
|
end
|
23
42
|
|
43
|
+
it "is possible to extend the action just for some phase" do
|
44
|
+
refute TestExtending::ExtendedAction.plan_phase.instance_methods.include?(:new_method)
|
45
|
+
refute Dynflow::Action.run_phase.instance_methods.include?(:new_method)
|
46
|
+
assert TestExtending::ExtendedAction.run_phase.instance_methods.include?(:new_method)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
describe 'children' do
|
52
|
+
include WorldInstance
|
53
|
+
|
54
|
+
smart_action_class = Class.new(Dynflow::Action)
|
55
|
+
smarter_action_class = Class.new(smart_action_class)
|
56
|
+
|
57
|
+
specify { refute smart_action_class.phase? }
|
58
|
+
specify { refute smarter_action_class.phase? }
|
59
|
+
specify { assert smarter_action_class.plan_phase.phase? }
|
60
|
+
|
61
|
+
specify { smart_action_class.all_children.must_include smarter_action_class }
|
62
|
+
specify { smart_action_class.all_children.size.must_equal 1 }
|
63
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.plan_phase }
|
64
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.run_phase }
|
65
|
+
specify { smart_action_class.all_children.wont_include smarter_action_class.finalize_phase }
|
66
|
+
|
67
|
+
describe 'World#subscribed_actions' do
|
68
|
+
event_action_class = CodeWorkflowExample::Triage
|
69
|
+
subscribed_action_class = CodeWorkflowExample::NotifyAssignee
|
70
|
+
|
71
|
+
specify { subscribed_action_class.subscribe.must_equal event_action_class }
|
72
|
+
specify { world.subscribed_actions(event_action_class).must_include subscribed_action_class }
|
73
|
+
specify { world.subscribed_actions(event_action_class).size.must_equal 1 }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe Action::Presenter do
|
78
|
+
include WorldInstance
|
79
|
+
|
80
|
+
let :execution_plan do
|
81
|
+
id, planned, finished = *world.trigger(CodeWorkflowExample::IncomingIssues, issues_data)
|
82
|
+
raise unless planned
|
83
|
+
finished.value
|
84
|
+
end
|
85
|
+
|
86
|
+
let :issues_data do
|
87
|
+
[{ 'author' => 'Peter Smith', 'text' => 'Failing test' },
|
88
|
+
{ 'author' => 'John Doe', 'text' => 'Internal server error' }]
|
89
|
+
end
|
90
|
+
|
91
|
+
let :presenter do
|
92
|
+
execution_plan.actions.find do |action|
|
93
|
+
action.is_a? CodeWorkflowExample::IncomingIssues
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
specify { presenter.action_class.must_equal CodeWorkflowExample::IncomingIssues }
|
98
|
+
|
99
|
+
it 'allows aggregating data from other actions' do
|
100
|
+
presenter.summary.must_equal(assignees: ["John Doe"])
|
101
|
+
end
|
24
102
|
end
|
25
103
|
end
|
data/test/clock_test.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
clock_class = Dynflow::Clock
|
5
|
+
|
6
|
+
describe clock_class do
|
7
|
+
|
8
|
+
let(:clock) { clock_class.new Logger.new($stderr) }
|
9
|
+
|
10
|
+
it 'refuses who without #<< method' do
|
11
|
+
clock.initialized.wait
|
12
|
+
-> { clock.ping Object.new, 0.1, :pong }.must_raise TypeError
|
13
|
+
clock.ping [], 0.1, :pong
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
it 'pongs' do
|
18
|
+
q = Queue.new
|
19
|
+
clock.initialized.wait
|
20
|
+
start = Time.now
|
21
|
+
|
22
|
+
clock.ping q, 0.1, o = Object.new
|
23
|
+
assert_equal o, q.pop
|
24
|
+
finish = Time.now
|
25
|
+
assert_in_delta 0.1, finish - start, 0.02
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'pongs on expected times' do
|
29
|
+
q = Queue.new
|
30
|
+
clock.initialized.wait
|
31
|
+
start = Time.now
|
32
|
+
|
33
|
+
clock.ping q, 0.3, :a
|
34
|
+
clock.ping q, 0.1, :b
|
35
|
+
clock.ping q, 0.2, :c
|
36
|
+
|
37
|
+
assert_equal :b, q.pop
|
38
|
+
assert_in_delta 0.1, Time.now - start, 0.02
|
39
|
+
assert_equal :c, q.pop
|
40
|
+
assert_in_delta 0.2, Time.now - start, 0.02
|
41
|
+
assert_equal :a, q.pop
|
42
|
+
assert_in_delta 0.3, Time.now - start, 0.02
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'works under stress' do
|
46
|
+
clock.initialized.wait
|
47
|
+
threads = Array.new(4) do
|
48
|
+
Thread.new do
|
49
|
+
q = Queue.new
|
50
|
+
times = 20
|
51
|
+
times.times { |i| clock.ping q, rand, i }
|
52
|
+
assert_equal (0...times).to_a, Array.new(times) { q.pop }.sort
|
53
|
+
end
|
54
|
+
end
|
55
|
+
threads.each &:join
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Dynflow
|
4
|
+
module CodeWorkflowExample
|
5
|
+
|
6
|
+
class IncomingIssues < Action
|
7
|
+
|
8
|
+
def plan(issues)
|
9
|
+
issues.each do |issue|
|
10
|
+
plan_action(IncomingIssue, issue)
|
11
|
+
end
|
12
|
+
plan_self('issues' => issues)
|
13
|
+
end
|
14
|
+
|
15
|
+
input_format do
|
16
|
+
param :issues, Array do
|
17
|
+
param :author, String
|
18
|
+
param :text, String
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def finalize
|
23
|
+
TestExecutionLog.finalize << self
|
24
|
+
end
|
25
|
+
|
26
|
+
def summary
|
27
|
+
triages = all_actions.find_all do |action|
|
28
|
+
action.is_a? Dynflow::CodeWorkflowExample::Triage
|
29
|
+
end
|
30
|
+
assignees = triages.map do |triage|
|
31
|
+
triage.output[:classification] &&
|
32
|
+
triage.output[:classification][:assignee]
|
33
|
+
end.compact.uniq
|
34
|
+
{ assignees: assignees }
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
class Slow < Action
|
40
|
+
def plan(seconds)
|
41
|
+
plan_self interval: seconds
|
42
|
+
end
|
43
|
+
|
44
|
+
def run
|
45
|
+
sleep input[:interval]
|
46
|
+
action_logger.debug 'done with sleeping'
|
47
|
+
$slow_actions_done ||= 0
|
48
|
+
$slow_actions_done +=1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class IncomingIssue < Action
|
53
|
+
|
54
|
+
def plan(issue)
|
55
|
+
plan_self(issue)
|
56
|
+
plan_action(Triage, issue)
|
57
|
+
end
|
58
|
+
|
59
|
+
input_format do
|
60
|
+
param :author, String
|
61
|
+
param :text, String
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
class Triage < Action
|
67
|
+
|
68
|
+
def plan(issue)
|
69
|
+
triage = plan_self(issue)
|
70
|
+
plan_action(UpdateIssue,
|
71
|
+
author: triage.input[:author],
|
72
|
+
text: triage.input[:text],
|
73
|
+
assignee: triage.output[:classification][:assignee],
|
74
|
+
severity: triage.output[:classification][:severity])
|
75
|
+
end
|
76
|
+
|
77
|
+
input_format do
|
78
|
+
param :author, String
|
79
|
+
param :text, String
|
80
|
+
end
|
81
|
+
|
82
|
+
output_format do
|
83
|
+
param :classification, Hash do
|
84
|
+
param :assignee, String
|
85
|
+
param :severity, %w[low medium high]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def run
|
90
|
+
TestExecutionLog.run << self
|
91
|
+
TestPause.pause if input[:text].include? 'get a break'
|
92
|
+
error! 'Trolling detected' if input[:text] == "trolling"
|
93
|
+
self.output[:classification] = { assignee: 'John Doe', severity: 'medium' }
|
94
|
+
end
|
95
|
+
|
96
|
+
def finalize
|
97
|
+
error! 'Trolling detected' if input[:text] == "trolling in finalize"
|
98
|
+
TestExecutionLog.finalize << self
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
class UpdateIssue < Action
|
104
|
+
|
105
|
+
input_format do
|
106
|
+
param :author, String
|
107
|
+
param :text, String
|
108
|
+
param :assignee, String
|
109
|
+
param :severity, %w[low medium high]
|
110
|
+
end
|
111
|
+
|
112
|
+
def run
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
class NotifyAssignee < Action
|
117
|
+
|
118
|
+
def self.subscribe
|
119
|
+
Triage
|
120
|
+
end
|
121
|
+
|
122
|
+
input_format do
|
123
|
+
param :triage, Triage.output_format
|
124
|
+
end
|
125
|
+
|
126
|
+
def plan(*args)
|
127
|
+
plan_self(:triage => trigger.output)
|
128
|
+
end
|
129
|
+
|
130
|
+
def run
|
131
|
+
end
|
132
|
+
|
133
|
+
def finalize
|
134
|
+
TestExecutionLog.finalize << self
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
class Commit < Action
|
139
|
+
input_format do
|
140
|
+
param :sha, String
|
141
|
+
end
|
142
|
+
|
143
|
+
def plan(commit, reviews = { 'Morfeus' => true, 'Neo' => true })
|
144
|
+
sequence do
|
145
|
+
ci, review_actions = concurrence do
|
146
|
+
[plan_action(Ci, :commit => commit),
|
147
|
+
reviews.map do |name, result|
|
148
|
+
plan_action(Review, commit, name, result)
|
149
|
+
end]
|
150
|
+
end
|
151
|
+
|
152
|
+
plan_action(Merge,
|
153
|
+
commit: commit,
|
154
|
+
ci_result: ci.output[:passed],
|
155
|
+
review_results: review_actions.map { |ra| ra.output[:passed] })
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class FastCommit < Action
|
161
|
+
|
162
|
+
def plan(commit)
|
163
|
+
sequence do
|
164
|
+
ci, review = concurrence do
|
165
|
+
[plan_action(Ci, commit: commit),
|
166
|
+
plan_action(Review, commit, 'Morfeus', true)]
|
167
|
+
end
|
168
|
+
|
169
|
+
plan_action(Merge,
|
170
|
+
commit: commit,
|
171
|
+
ci_result: ci.output[:passed],
|
172
|
+
review_results: [review.output[:passed]])
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
input_format do
|
177
|
+
param :sha, String
|
178
|
+
end
|
179
|
+
|
180
|
+
end
|
181
|
+
|
182
|
+
class Ci < Action
|
183
|
+
|
184
|
+
input_format do
|
185
|
+
param :commit, Commit.input_format
|
186
|
+
end
|
187
|
+
|
188
|
+
output_format do
|
189
|
+
param :passed, :boolean
|
190
|
+
end
|
191
|
+
|
192
|
+
def run
|
193
|
+
output.update passed: true
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
class Review < Action
|
198
|
+
|
199
|
+
input_format do
|
200
|
+
param :reviewer, String
|
201
|
+
param :commit, Commit.input_format
|
202
|
+
end
|
203
|
+
|
204
|
+
output_format do
|
205
|
+
param :passed, :boolean
|
206
|
+
end
|
207
|
+
|
208
|
+
def plan(commit, reviewer, result = true)
|
209
|
+
plan_self commit: commit, reviewer: reviewer, result: result
|
210
|
+
end
|
211
|
+
|
212
|
+
def run
|
213
|
+
output.update passed: input[:result]
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class Merge < Action
|
218
|
+
|
219
|
+
input_format do
|
220
|
+
param :commit, Commit.input_format
|
221
|
+
param :ci_result, Ci.output_format
|
222
|
+
param :review_results, array_of(Review.output_format)
|
223
|
+
end
|
224
|
+
|
225
|
+
def run
|
226
|
+
output.update passed: (input.fetch(:ci_result) && input.fetch(:review_results).all?)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
class Dummy < Action
|
231
|
+
end
|
232
|
+
|
233
|
+
class DummyWithFinalize < Action
|
234
|
+
def finalize
|
235
|
+
TestExecutionLog.finalize << self
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
class DummyTrigger < Action
|
240
|
+
end
|
241
|
+
|
242
|
+
class DummyAnotherTrigger < Action
|
243
|
+
end
|
244
|
+
|
245
|
+
class DummySubscribe < Action
|
246
|
+
|
247
|
+
def self.subscribe
|
248
|
+
DummyTrigger
|
249
|
+
end
|
250
|
+
|
251
|
+
def run
|
252
|
+
end
|
253
|
+
|
254
|
+
end
|
255
|
+
|
256
|
+
class DummyMultiSubscribe < Action
|
257
|
+
|
258
|
+
def self.subscribe
|
259
|
+
[DummyTrigger, DummyAnotherTrigger]
|
260
|
+
end
|
261
|
+
|
262
|
+
def run
|
263
|
+
end
|
264
|
+
|
265
|
+
end
|
266
|
+
|
267
|
+
class CancelableSuspended < Action
|
268
|
+
include Dynflow::Action::CancellablePolling
|
269
|
+
|
270
|
+
Cancel = Dynflow::Action::CancellablePolling::Cancel
|
271
|
+
|
272
|
+
def invoke_external_task
|
273
|
+
{ progress: 0 }
|
274
|
+
end
|
275
|
+
|
276
|
+
def poll_external_task
|
277
|
+
progress = external_task[:progress] + 10
|
278
|
+
if progress > 25 && input[:text] =~ /cancel/
|
279
|
+
world.event execution_plan_id, run_step_id, Cancel
|
280
|
+
end
|
281
|
+
{ progress: progress }
|
282
|
+
end
|
283
|
+
|
284
|
+
def cancel_external_task
|
285
|
+
if input[:text] !~ /cancel fail/
|
286
|
+
{ cancelled: true }
|
287
|
+
else
|
288
|
+
error! 'action cancelled'
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def external_task=(external_task_data)
|
293
|
+
self.output.update external_task_data
|
294
|
+
end
|
295
|
+
|
296
|
+
def external_task
|
297
|
+
output
|
298
|
+
end
|
299
|
+
|
300
|
+
def done?
|
301
|
+
external_task[:progress] >= 100
|
302
|
+
end
|
303
|
+
|
304
|
+
def poll_interval
|
305
|
+
0.1
|
306
|
+
end
|
307
|
+
|
308
|
+
def run_progress
|
309
|
+
output[:progress].to_f / 100
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class DummySuspended < Action
|
314
|
+
include Action::Polling
|
315
|
+
|
316
|
+
def invoke_external_task
|
317
|
+
error! 'Trolling detected' if input[:text] == 'troll setup'
|
318
|
+
{ progress: 0, done: false }
|
319
|
+
end
|
320
|
+
|
321
|
+
def external_task=(external_task_data)
|
322
|
+
self.output.update external_task_data
|
323
|
+
end
|
324
|
+
|
325
|
+
def external_task
|
326
|
+
output
|
327
|
+
end
|
328
|
+
|
329
|
+
def poll_external_task
|
330
|
+
if input[:text] == 'troll progress' && !output[:trolled]
|
331
|
+
output[:trolled] = true
|
332
|
+
error! 'Trolling detected'
|
333
|
+
end
|
334
|
+
|
335
|
+
if input[:text] =~ /pause in progress (\d+)/
|
336
|
+
TestPause.pause if output[:progress] == $1.to_i
|
337
|
+
end
|
338
|
+
|
339
|
+
progress = output[:progress] + 10
|
340
|
+
{ progress: progress, done: progress >= 100 }
|
341
|
+
end
|
342
|
+
|
343
|
+
def done?
|
344
|
+
external_task[:progress] >= 100
|
345
|
+
end
|
346
|
+
|
347
|
+
def poll_interval
|
348
|
+
0.001
|
349
|
+
end
|
350
|
+
|
351
|
+
def run_progress
|
352
|
+
output[:progress].to_f / 100
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
class DummyHeavyProgress < Action
|
357
|
+
|
358
|
+
def plan(input)
|
359
|
+
sequence do
|
360
|
+
plan_self(input)
|
361
|
+
plan_action(DummySuspended, input)
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
def run
|
366
|
+
end
|
367
|
+
|
368
|
+
def finalize
|
369
|
+
$dummy_heavy_progress = 'dummy_heavy_progress'
|
370
|
+
end
|
371
|
+
|
372
|
+
def run_progress_weight
|
373
|
+
4
|
374
|
+
end
|
375
|
+
|
376
|
+
def finalize_progress_weight
|
377
|
+
5
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
end
|
382
|
+
end
|