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,173 @@
1
+ require_relative 'test_helper'
2
+ require 'fileutils'
3
+
4
+ module PersistenceAdapterTest
5
+ def storage
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def prepare_plans
10
+ proto_plans = [{ id: 'plan1', state: 'paused' },
11
+ { id: 'plan2', state: 'stopped' },
12
+ { id: 'plan3', state: 'paused' }]
13
+ proto_plans.map do |h|
14
+ h.merge result: nil, started_at: (Time.now-20).to_s, ended_at: (Time.now-10).to_s,
15
+ real_time: 0.0, execution_time: 0.0
16
+ end.tap do |plans|
17
+ plans.each { |plan| storage.save_execution_plan(plan[:id], plan) }
18
+ end
19
+ end
20
+
21
+ def test_load_execution_plans
22
+ plans = prepare_plans
23
+ loaded_plans = storage.find_execution_plans
24
+ loaded_plans.size.must_equal 3
25
+ loaded_plans.must_include plans[0].with_indifferent_access
26
+ loaded_plans.must_include plans[1].with_indifferent_access
27
+ end
28
+
29
+ def test_pagination
30
+ prepare_plans
31
+ if storage.pagination?
32
+ loaded_plans = storage.find_execution_plans(page: 0, per_page: 1)
33
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1']
34
+
35
+ loaded_plans = storage.find_execution_plans(page: 1, per_page: 1)
36
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2']
37
+ end
38
+ end
39
+
40
+ def test_ordering
41
+ prepare_plans
42
+ if storage.ordering_by.include?(:state)
43
+ loaded_plans = storage.find_execution_plans(order_by: 'state')
44
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan3', 'plan2']
45
+
46
+ loaded_plans = storage.find_execution_plans(order_by: 'state', desc: true)
47
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2', 'plan3', 'plan1']
48
+ end
49
+ end
50
+
51
+ def test_filtering
52
+ prepare_plans
53
+ if storage.ordering_by.include?(:state)
54
+ loaded_plans = storage.find_execution_plans(filters: { state: ['paused'] })
55
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan3']
56
+
57
+ loaded_plans = storage.find_execution_plans(filters: { state: ['stopped'] })
58
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2']
59
+
60
+ loaded_plans = storage.find_execution_plans(filters: { state: [] })
61
+ loaded_plans.map { |h| h[:id] }.must_equal []
62
+
63
+ loaded_plans = storage.find_execution_plans(filters: { state: ['stopped', 'paused'] })
64
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan2', 'plan3']
65
+
66
+ loaded_plans = storage.find_execution_plans(filters: { 'state' => ['stopped', 'paused'] })
67
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan2', 'plan3']
68
+ end
69
+ end
70
+
71
+ def test_save_execution_plan
72
+ plan = { id: 'plan1', state: :pending, result: nil, started_at: nil, ended_at: nil,
73
+ real_time: 0.0, execution_time: 0.0 }
74
+ -> { storage.load_execution_plan('plan1') }.must_raise KeyError
75
+
76
+ storage.save_execution_plan('plan1', plan)
77
+ storage.load_execution_plan('plan1')[:id].must_equal 'plan1'
78
+ storage.load_execution_plan('plan1')['id'].must_equal 'plan1'
79
+ storage.load_execution_plan('plan1').keys.size.must_equal 7
80
+
81
+ storage.save_execution_plan('plan1', nil)
82
+ -> { storage.load_execution_plan('plan1') }.must_raise KeyError
83
+ end
84
+
85
+ def test_save_action
86
+ plan = { id: 'plan1', state: :pending, result: nil, started_at: nil, ended_at: nil,
87
+ real_time: 0.0, execution_time: 0.0 }
88
+ storage.save_execution_plan('plan1', plan)
89
+
90
+ action = { id: 1 }
91
+ -> { storage.load_action('plan1', 1) }.must_raise KeyError
92
+
93
+ storage.save_action('plan1', 1, action)
94
+ storage.load_action('plan1', 1)[:id].must_equal 1
95
+ storage.load_action('plan1', 1)['id'].must_equal 1
96
+ storage.load_action('plan1', 1).keys.size.must_equal 1
97
+
98
+ storage.save_action('plan1', 1, nil)
99
+ -> { storage.load_action('plan1', 1) }.must_raise KeyError
100
+
101
+ storage.save_execution_plan('plan1', nil)
102
+ end
103
+
104
+ end
105
+
106
+ class SequelTest < MiniTest::Test
107
+ include PersistenceAdapterTest
108
+
109
+ def storage
110
+ @storage ||= Dynflow::PersistenceAdapters::Sequel.new 'sqlite:/'
111
+ end
112
+
113
+ def test_stores_meta_data
114
+ plans = prepare_plans
115
+
116
+ plans.each do |original|
117
+ stored = storage.to_hash.fetch(:execution_plans).find { |ep| ep[:uuid] == original[:id] }
118
+ stored.each { |k, v| stored[k] = v.to_s if v.is_a? Time }
119
+ storage.class::META_DATA.fetch(:execution_plan).each do |name|
120
+ stored.fetch(name.to_sym).must_equal original.fetch(name.to_sym)
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ #class MemoryTest < MiniTest::Unit::TestCase
127
+ # include PersistenceAdapterTest
128
+ #
129
+ # def storage
130
+ # @storage ||= Dynflow::PersistenceAdapters::Memory.new
131
+ # end
132
+ #end
133
+ #
134
+ #class SimpleFileStorageTest < MiniTest::Unit::TestCase
135
+ # include PersistenceAdapterTest
136
+ #
137
+ # def storage_path
138
+ # "#{File.dirname(__FILE__)}/simple_file_storage"
139
+ # end
140
+ #
141
+ # def setup
142
+ # Dir.mkdir storage_path
143
+ # end
144
+ #
145
+ # def storage
146
+ # @storage ||= begin
147
+ # Dynflow::PersistenceAdapters::SimpleFileStorage.new storage_path
148
+ # end
149
+ # end
150
+ #
151
+ # def teardown
152
+ # FileUtils.rm_rf storage_path
153
+ # end
154
+ #end
155
+ #
156
+ #require 'dynflow/persistence_adapters/active_record'
157
+ #
158
+ #class ActiveRecordTest < MiniTest::Unit::TestCase
159
+ # include PersistenceAdapterTest
160
+ #
161
+ # def setup
162
+ # ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
163
+ # ::ActiveRecord::Migrator.migrate Dynflow::PersistenceAdapters::ActiveRecord.migrations_path
164
+ # end
165
+ #
166
+ # def storage
167
+ # @storage ||= begin
168
+ # Dynflow::PersistenceAdapters::ActiveRecord.new
169
+ # end
170
+ # end
171
+ #end
172
+
173
+
data/test/test_helper.rb CHANGED
@@ -1,4 +1,319 @@
1
- require 'test/unit'
1
+ require 'bundler/setup'
2
+ require 'minitest/autorun'
2
3
  require 'minitest/spec'
4
+
5
+ if ENV['RM_INFO']
6
+ require 'minitest/reporters'
7
+ MiniTest::Reporters.use!
8
+ end
9
+
3
10
  require 'dynflow'
11
+ require 'dynflow/testing'
4
12
  require 'pry'
13
+
14
+ class TestExecutionLog
15
+
16
+ include Enumerable
17
+
18
+ def initialize
19
+ @log = []
20
+ end
21
+
22
+ def <<(action)
23
+ @log << [action.action_class, action.input]
24
+ end
25
+
26
+ def log
27
+ @log
28
+ end
29
+
30
+ def each(&block)
31
+ @log.each(&block)
32
+ end
33
+
34
+ def size
35
+ @log.size
36
+ end
37
+
38
+ def self.setup
39
+ @run, @finalize = self.new, self.new
40
+ end
41
+
42
+ def self.teardown
43
+ @run, @finalize = nil, nil
44
+ end
45
+
46
+ def self.run
47
+ @run || []
48
+ end
49
+
50
+ def self.finalize
51
+ @finalize || []
52
+ end
53
+
54
+ end
55
+
56
+ # To be able to stop a process in some step and perform assertions while paused
57
+ class TestPause
58
+
59
+ def self.setup
60
+ @pause = Dynflow::Future.new
61
+ @ready = Dynflow::Future.new
62
+ end
63
+
64
+ def self.teardown
65
+ @pause = nil
66
+ @ready = nil
67
+ end
68
+
69
+ # to be called from action
70
+ def self.pause
71
+ if !@pause
72
+ raise 'the TestPause class was not setup'
73
+ elsif @ready.ready?
74
+ raise 'you can pause only once'
75
+ else
76
+ @ready.resolve(true)
77
+ @pause.wait
78
+ end
79
+ end
80
+
81
+ # in the block perform assertions
82
+ def self.when_paused
83
+ if @pause
84
+ @ready.wait # wait till we are paused
85
+ yield
86
+ @pause.resolve(true) # resume the run
87
+ else
88
+ raise 'the TestPause class was not setup'
89
+ end
90
+ end
91
+ end
92
+
93
+ module WorldInstance
94
+ def self.world
95
+ @world ||= create_world
96
+ end
97
+
98
+ def self.remote_world
99
+ return @remote_world if @remote_world
100
+ @listener, @remote_world = create_remote_world world
101
+ @remote_world
102
+ end
103
+
104
+ def self.logger_adapter
105
+ action_logger = Logger.new($stderr).tap do |logger|
106
+ logger.level = Logger::FATAL
107
+ logger.progname = 'action'
108
+ end
109
+ dynflow_logger = Logger.new($stderr).tap do |logger|
110
+ logger.level = Logger::WARN
111
+ logger.progname = 'dynflow'
112
+ end
113
+ Dynflow::LoggerAdapters::Delegator.new(action_logger, dynflow_logger)
114
+ end
115
+
116
+ def self.create_world
117
+ Dynflow::SimpleWorld.new logger_adapter: logger_adapter,
118
+ auto_terminate: false
119
+ end
120
+
121
+ def self.create_remote_world(world)
122
+ @counter ||= 0
123
+ socket_path = Dir.tmpdir + "/dynflow_remote_#{@counter+=1}"
124
+ listener = Dynflow::Listeners::Socket.new world, socket_path
125
+ world = Dynflow::SimpleWorld.new(logger_adapter: logger_adapter) do |remote_world|
126
+ { persistence_adapter: world.persistence.adapter,
127
+ executor: Dynflow::Executors::RemoteViaSocket.new(remote_world, socket_path),
128
+ auto_terminate: false }
129
+ end
130
+ return listener, world
131
+ end
132
+
133
+ def self.terminate
134
+ remote_world.terminate.wait if @remote_world
135
+ world.terminate.wait if @world
136
+
137
+ @remote_world = @world = nil
138
+ end
139
+
140
+ def world
141
+ WorldInstance.world
142
+ end
143
+
144
+ def remote_world
145
+ WorldInstance.remote_world
146
+ end
147
+ end
148
+
149
+ # ensure there are no unresolved Futures at the end or being GCed
150
+ future_tests =-> do
151
+ future_creations = {}
152
+ non_ready_futures = {}
153
+
154
+ MiniTest.after_run do
155
+ WorldInstance.terminate
156
+ futures = ObjectSpace.each_object(Dynflow::Future).select { |f| !f.ready? }
157
+ unless futures.empty?
158
+ raise "there are unready futures:\n" +
159
+ futures.map { |f| "#{f}\n#{future_creations[f.object_id]}" }.join("\n")
160
+ end
161
+ end
162
+
163
+ Dynflow::Future.singleton_class.send :define_method, :new do |*args, &block|
164
+ super(*args, &block).tap do |f|
165
+ future_creations[f.object_id] = caller(3)
166
+ non_ready_futures[f.object_id] = true
167
+ end
168
+ end
169
+
170
+ set_method = Dynflow::Future.instance_method :set
171
+ Dynflow::Future.send :define_method, :set do |*args|
172
+ begin
173
+ set_method.bind(self).call *args
174
+ ensure
175
+ non_ready_futures.delete self.object_id
176
+ end
177
+ end
178
+
179
+ MiniTest.after_run do
180
+ unless non_ready_futures.empty?
181
+ unified = non_ready_futures.each_with_object({}) do |(id, _), h|
182
+ backtrace_first = future_creations[id][0]
183
+ h[backtrace_first] ||= []
184
+ h[backtrace_first] << id
185
+ end
186
+ raise("there were #{non_ready_futures.size} non_ready_futures:\n" +
187
+ unified.map do |backtrace, ids|
188
+ "--- #{ids.size}: #{ids}\n#{future_creations[ids.first].join("\n")}"
189
+ end.join("\n"))
190
+ end
191
+ end
192
+
193
+ # time out all futures by default
194
+ default_timeout = 8
195
+ wait_method = Dynflow::Future.instance_method(:wait)
196
+
197
+ Dynflow::Future.class_eval do
198
+ define_method :wait do |timeout = nil|
199
+ wait_method.bind(self).call(timeout || default_timeout)
200
+ end
201
+ end
202
+
203
+ end.call
204
+
205
+ module PlanAssertions
206
+
207
+ def inspect_flow(execution_plan, flow)
208
+ out = ""
209
+ inspect_subflow(out, execution_plan, flow, "")
210
+ out
211
+ end
212
+
213
+ def inspect_plan_steps(execution_plan)
214
+ out = ""
215
+ inspect_plan_step(out, execution_plan, execution_plan.root_plan_step, "")
216
+ out
217
+ end
218
+
219
+ def assert_planning_success(execution_plan)
220
+ plan_steps = execution_plan.steps.values.find_all do |step|
221
+ step.is_a? Dynflow::ExecutionPlan::Steps::PlanStep
222
+ end
223
+ plan_steps.all? { |plan_step| plan_step.state.must_equal :success, plan_step.error }
224
+ end
225
+
226
+ def assert_run_flow(expected, execution_plan)
227
+ assert_planning_success(execution_plan)
228
+ inspect_flow(execution_plan, execution_plan.run_flow).chomp.must_equal dedent(expected).chomp
229
+ end
230
+
231
+ def assert_finalize_flow(expected, execution_plan)
232
+ assert_planning_success(execution_plan)
233
+ inspect_flow(execution_plan, execution_plan.finalize_flow).chomp.must_equal dedent(expected).chomp
234
+ end
235
+
236
+ def assert_run_flow_equal(expected_plan, execution_plan)
237
+ expected = inspect_flow(expected_plan, expected_plan.run_flow)
238
+ current = inspect_flow(execution_plan, execution_plan.run_flow)
239
+ assert_equal expected, current
240
+ end
241
+
242
+ def assert_steps_equal(expected, current)
243
+ current.id.must_equal expected.id
244
+ current.class.must_equal expected.class
245
+ current.state.must_equal expected.state
246
+ current.action_class.must_equal expected.action_class
247
+ current.action_id.must_equal expected.action_id
248
+
249
+ if expected.respond_to?(:children)
250
+ current.children.must_equal(expected.children)
251
+ end
252
+ end
253
+
254
+ def assert_plan_steps(expected, execution_plan)
255
+ inspect_plan_steps(execution_plan).chomp.must_equal dedent(expected).chomp
256
+ end
257
+
258
+ def assert_finalized(action_class, input)
259
+ assert_executed(:finalize, action_class, input)
260
+ end
261
+
262
+ def assert_executed(phase, action_class, input)
263
+ log = TestExecutionLog.send(phase).log
264
+
265
+ found_log = log.any? do |(logged_action_class, logged_input)|
266
+ action_class == logged_action_class && input == logged_input
267
+ end
268
+
269
+ unless found_log
270
+ message = ["#{action_class} with input #{input.inspect} not executed in #{phase} phase"]
271
+ message << "following actions were executed:"
272
+ log.each do |(logged_action_class, logged_input)|
273
+ message << "#{logged_action_class} #{logged_input.inspect}"
274
+ end
275
+ raise message.join("\n")
276
+ end
277
+ end
278
+
279
+ def inspect_subflow(out, execution_plan, flow, prefix)
280
+ case flow
281
+ when Dynflow::Flows::Atom
282
+ out << prefix
283
+ out << flow.step_id.to_s << ': '
284
+ step = execution_plan.steps[flow.step_id]
285
+ out << step.action_class.to_s[/\w+\Z/]
286
+ out << "(#{step.state})"
287
+ out << ' '
288
+ action = execution_plan.world.persistence.load_action(step)
289
+ out << action.input.inspect
290
+ unless step.state == :pending
291
+ out << ' --> '
292
+ out << action.output.inspect
293
+ end
294
+ out << "\n"
295
+ else
296
+ out << prefix << flow.class.name << "\n"
297
+ flow.sub_flows.each do |sub_flow|
298
+ inspect_subflow(out, execution_plan, sub_flow, prefix + ' ')
299
+ end
300
+ end
301
+ out
302
+ end
303
+
304
+ def inspect_plan_step(out, execution_plan, plan_step, prefix)
305
+ out << prefix
306
+ out << plan_step.action_class.to_s[/\w+\Z/]
307
+ out << "\n"
308
+ plan_step.children.each do |sub_step_id|
309
+ sub_step = execution_plan.steps[sub_step_id]
310
+ inspect_plan_step(out, execution_plan, sub_step, prefix + ' ')
311
+ end
312
+ out
313
+ end
314
+
315
+ def dedent(string)
316
+ dedent = string.scan(/^ */).map { |spaces| spaces.size }.min
317
+ string.lines.map { |line| line[dedent..-1] }.join
318
+ end
319
+ end