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
@@ -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
- require 'test_helper'
1
+ require_relative 'test_helper'
2
+ require_relative 'code_workflow_example'
2
3
 
3
4
  module Dynflow
4
- class ActionTest < Action
5
5
 
6
- output_format do
7
- param :id, String
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
- def run
11
- output['id'] = input['name']
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 'running an action' do
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
- it 'executed the run method storing results to output attribute'do
19
- action = ActionTest.new('name' => 'zoo')
20
- action.run
21
- action.output.must_equal('id' => "zoo")
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
@@ -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