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,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