dynflow 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. data/.gitignore +6 -0
  2. data/.travis.yml +9 -0
  3. data/Gemfile +0 -10
  4. data/MIT-LICENSE +1 -1
  5. data/README.md +99 -37
  6. data/Rakefile +2 -6
  7. data/doc/images/logo.png +0 -0
  8. data/dynflow.gemspec +10 -1
  9. data/examples/generate_work_for_daemon.rb +24 -0
  10. data/examples/orchestrate.rb +121 -0
  11. data/examples/run_daemon.rb +17 -0
  12. data/examples/web_console.rb +29 -0
  13. data/lib/dynflow.rb +27 -6
  14. data/lib/dynflow/action.rb +185 -77
  15. data/lib/dynflow/action/cancellable_polling.rb +18 -0
  16. data/lib/dynflow/action/finalize_phase.rb +18 -0
  17. data/lib/dynflow/action/flow_phase.rb +44 -0
  18. data/lib/dynflow/action/format.rb +46 -0
  19. data/lib/dynflow/action/missing.rb +26 -0
  20. data/lib/dynflow/action/plan_phase.rb +85 -0
  21. data/lib/dynflow/action/polling.rb +49 -0
  22. data/lib/dynflow/action/presenter.rb +51 -0
  23. data/lib/dynflow/action/progress.rb +62 -0
  24. data/lib/dynflow/action/run_phase.rb +43 -0
  25. data/lib/dynflow/action/suspended.rb +21 -0
  26. data/lib/dynflow/clock.rb +133 -0
  27. data/lib/dynflow/daemon.rb +29 -0
  28. data/lib/dynflow/execution_plan.rb +285 -33
  29. data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
  30. data/lib/dynflow/execution_plan/output_reference.rb +52 -0
  31. data/lib/dynflow/execution_plan/steps.rb +12 -0
  32. data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
  33. data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
  34. data/lib/dynflow/execution_plan/steps/error.rb +33 -0
  35. data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
  36. data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
  37. data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
  38. data/lib/dynflow/executors.rb +9 -0
  39. data/lib/dynflow/executors/abstract.rb +32 -0
  40. data/lib/dynflow/executors/parallel.rb +88 -0
  41. data/lib/dynflow/executors/parallel/core.rb +119 -0
  42. data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
  43. data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
  44. data/lib/dynflow/executors/parallel/pool.rb +102 -0
  45. data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
  46. data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
  47. data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
  48. data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
  49. data/lib/dynflow/executors/parallel/worker.rb +30 -0
  50. data/lib/dynflow/executors/remote_via_socket.rb +38 -0
  51. data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
  52. data/lib/dynflow/flows.rb +13 -0
  53. data/lib/dynflow/flows/abstract.rb +36 -0
  54. data/lib/dynflow/flows/abstract_composed.rb +104 -0
  55. data/lib/dynflow/flows/atom.rb +36 -0
  56. data/lib/dynflow/flows/concurrence.rb +28 -0
  57. data/lib/dynflow/flows/sequence.rb +13 -0
  58. data/lib/dynflow/future.rb +173 -0
  59. data/lib/dynflow/listeners.rb +7 -0
  60. data/lib/dynflow/listeners/abstract.rb +13 -0
  61. data/lib/dynflow/listeners/serialization.rb +41 -0
  62. data/lib/dynflow/listeners/socket.rb +88 -0
  63. data/lib/dynflow/logger_adapters.rb +8 -0
  64. data/lib/dynflow/logger_adapters/abstract.rb +30 -0
  65. data/lib/dynflow/logger_adapters/delegator.rb +13 -0
  66. data/lib/dynflow/logger_adapters/formatters.rb +8 -0
  67. data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
  68. data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
  69. data/lib/dynflow/logger_adapters/simple.rb +59 -0
  70. data/lib/dynflow/micro_actor.rb +102 -0
  71. data/lib/dynflow/persistence.rb +53 -0
  72. data/lib/dynflow/persistence_adapters.rb +6 -0
  73. data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
  74. data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
  75. data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
  76. data/lib/dynflow/serializable.rb +66 -0
  77. data/lib/dynflow/simple_world.rb +18 -0
  78. data/lib/dynflow/stateful.rb +40 -0
  79. data/lib/dynflow/testing.rb +32 -0
  80. data/lib/dynflow/testing/assertions.rb +64 -0
  81. data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
  82. data/lib/dynflow/testing/dummy_executor.rb +29 -0
  83. data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
  84. data/lib/dynflow/testing/dummy_step.rb +19 -0
  85. data/lib/dynflow/testing/dummy_world.rb +33 -0
  86. data/lib/dynflow/testing/factories.rb +83 -0
  87. data/lib/dynflow/testing/managed_clock.rb +23 -0
  88. data/lib/dynflow/testing/mimic.rb +38 -0
  89. data/lib/dynflow/transaction_adapters.rb +9 -0
  90. data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
  91. data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
  92. data/lib/dynflow/transaction_adapters/none.rb +12 -0
  93. data/lib/dynflow/version.rb +1 -1
  94. data/lib/dynflow/web_console.rb +277 -0
  95. data/lib/dynflow/world.rb +168 -0
  96. data/test/action_test.rb +89 -11
  97. data/test/clock_test.rb +59 -0
  98. data/test/code_workflow_example.rb +382 -0
  99. data/test/execution_plan_test.rb +195 -64
  100. data/test/executor_test.rb +692 -0
  101. data/test/persistance_adapters_test.rb +173 -0
  102. data/test/test_helper.rb +316 -1
  103. data/test/testing_test.rb +148 -0
  104. data/test/web_console_test.rb +38 -0
  105. data/web/assets/javascripts/application.js +25 -0
  106. data/web/assets/stylesheets/application.css +101 -0
  107. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
  108. data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
  109. data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
  110. data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
  111. data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
  112. data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
  113. data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
  114. data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
  115. data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
  116. data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
  117. data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
  118. data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
  119. data/web/assets/vendor/jquery/jquery.js +9807 -0
  120. data/web/views/flow.erb +19 -0
  121. data/web/views/flow_step.erb +31 -0
  122. data/web/views/index.erb +39 -0
  123. data/web/views/layout.erb +20 -0
  124. data/web/views/plan_step.erb +11 -0
  125. data/web/views/show.erb +54 -0
  126. metadata +250 -11
  127. data/examples/events.rb +0 -71
  128. data/examples/workflow.rb +0 -140
  129. data/lib/dynflow/bus.rb +0 -168
  130. data/lib/dynflow/dispatcher.rb +0 -36
  131. data/lib/dynflow/logger.rb +0 -34
  132. data/lib/dynflow/step.rb +0 -234
  133. data/test/bus_test.rb +0 -150
@@ -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