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.
- data/.gitignore +6 -0
- data/.travis.yml +9 -0
- data/Gemfile +0 -10
- data/MIT-LICENSE +1 -1
- data/README.md +99 -37
- data/Rakefile +2 -6
- data/doc/images/logo.png +0 -0
- data/dynflow.gemspec +10 -1
- data/examples/generate_work_for_daemon.rb +24 -0
- data/examples/orchestrate.rb +121 -0
- data/examples/run_daemon.rb +17 -0
- data/examples/web_console.rb +29 -0
- data/lib/dynflow.rb +27 -6
- data/lib/dynflow/action.rb +185 -77
- data/lib/dynflow/action/cancellable_polling.rb +18 -0
- data/lib/dynflow/action/finalize_phase.rb +18 -0
- data/lib/dynflow/action/flow_phase.rb +44 -0
- data/lib/dynflow/action/format.rb +46 -0
- data/lib/dynflow/action/missing.rb +26 -0
- data/lib/dynflow/action/plan_phase.rb +85 -0
- data/lib/dynflow/action/polling.rb +49 -0
- data/lib/dynflow/action/presenter.rb +51 -0
- data/lib/dynflow/action/progress.rb +62 -0
- data/lib/dynflow/action/run_phase.rb +43 -0
- data/lib/dynflow/action/suspended.rb +21 -0
- data/lib/dynflow/clock.rb +133 -0
- data/lib/dynflow/daemon.rb +29 -0
- data/lib/dynflow/execution_plan.rb +285 -33
- data/lib/dynflow/execution_plan/dependency_graph.rb +29 -0
- data/lib/dynflow/execution_plan/output_reference.rb +52 -0
- data/lib/dynflow/execution_plan/steps.rb +12 -0
- data/lib/dynflow/execution_plan/steps/abstract.rb +121 -0
- data/lib/dynflow/execution_plan/steps/abstract_flow_step.rb +52 -0
- data/lib/dynflow/execution_plan/steps/error.rb +33 -0
- data/lib/dynflow/execution_plan/steps/finalize_step.rb +23 -0
- data/lib/dynflow/execution_plan/steps/plan_step.rb +81 -0
- data/lib/dynflow/execution_plan/steps/run_step.rb +21 -0
- data/lib/dynflow/executors.rb +9 -0
- data/lib/dynflow/executors/abstract.rb +32 -0
- data/lib/dynflow/executors/parallel.rb +88 -0
- data/lib/dynflow/executors/parallel/core.rb +119 -0
- data/lib/dynflow/executors/parallel/execution_plan_manager.rb +120 -0
- data/lib/dynflow/executors/parallel/flow_manager.rb +48 -0
- data/lib/dynflow/executors/parallel/pool.rb +102 -0
- data/lib/dynflow/executors/parallel/running_steps_manager.rb +63 -0
- data/lib/dynflow/executors/parallel/sequence_cursor.rb +97 -0
- data/lib/dynflow/executors/parallel/sequential_manager.rb +81 -0
- data/lib/dynflow/executors/parallel/work_queue.rb +44 -0
- data/lib/dynflow/executors/parallel/worker.rb +30 -0
- data/lib/dynflow/executors/remote_via_socket.rb +38 -0
- data/lib/dynflow/executors/remote_via_socket/core.rb +150 -0
- data/lib/dynflow/flows.rb +13 -0
- data/lib/dynflow/flows/abstract.rb +36 -0
- data/lib/dynflow/flows/abstract_composed.rb +104 -0
- data/lib/dynflow/flows/atom.rb +36 -0
- data/lib/dynflow/flows/concurrence.rb +28 -0
- data/lib/dynflow/flows/sequence.rb +13 -0
- data/lib/dynflow/future.rb +173 -0
- data/lib/dynflow/listeners.rb +7 -0
- data/lib/dynflow/listeners/abstract.rb +13 -0
- data/lib/dynflow/listeners/serialization.rb +41 -0
- data/lib/dynflow/listeners/socket.rb +88 -0
- data/lib/dynflow/logger_adapters.rb +8 -0
- data/lib/dynflow/logger_adapters/abstract.rb +30 -0
- data/lib/dynflow/logger_adapters/delegator.rb +13 -0
- data/lib/dynflow/logger_adapters/formatters.rb +8 -0
- data/lib/dynflow/logger_adapters/formatters/abstract.rb +33 -0
- data/lib/dynflow/logger_adapters/formatters/exception.rb +15 -0
- data/lib/dynflow/logger_adapters/simple.rb +59 -0
- data/lib/dynflow/micro_actor.rb +102 -0
- data/lib/dynflow/persistence.rb +53 -0
- data/lib/dynflow/persistence_adapters.rb +6 -0
- data/lib/dynflow/persistence_adapters/abstract.rb +56 -0
- data/lib/dynflow/persistence_adapters/sequel.rb +160 -0
- data/lib/dynflow/persistence_adapters/sequel_migrations/001_initial.rb +52 -0
- data/lib/dynflow/serializable.rb +66 -0
- data/lib/dynflow/simple_world.rb +18 -0
- data/lib/dynflow/stateful.rb +40 -0
- data/lib/dynflow/testing.rb +32 -0
- data/lib/dynflow/testing/assertions.rb +64 -0
- data/lib/dynflow/testing/dummy_execution_plan.rb +40 -0
- data/lib/dynflow/testing/dummy_executor.rb +29 -0
- data/lib/dynflow/testing/dummy_planned_action.rb +18 -0
- data/lib/dynflow/testing/dummy_step.rb +19 -0
- data/lib/dynflow/testing/dummy_world.rb +33 -0
- data/lib/dynflow/testing/factories.rb +83 -0
- data/lib/dynflow/testing/managed_clock.rb +23 -0
- data/lib/dynflow/testing/mimic.rb +38 -0
- data/lib/dynflow/transaction_adapters.rb +9 -0
- data/lib/dynflow/transaction_adapters/abstract.rb +26 -0
- data/lib/dynflow/transaction_adapters/active_record.rb +27 -0
- data/lib/dynflow/transaction_adapters/none.rb +12 -0
- data/lib/dynflow/version.rb +1 -1
- data/lib/dynflow/web_console.rb +277 -0
- data/lib/dynflow/world.rb +168 -0
- data/test/action_test.rb +89 -11
- data/test/clock_test.rb +59 -0
- data/test/code_workflow_example.rb +382 -0
- data/test/execution_plan_test.rb +195 -64
- data/test/executor_test.rb +692 -0
- data/test/persistance_adapters_test.rb +173 -0
- data/test/test_helper.rb +316 -1
- data/test/testing_test.rb +148 -0
- data/test/web_console_test.rb +38 -0
- data/web/assets/javascripts/application.js +25 -0
- data/web/assets/stylesheets/application.css +101 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.css +1109 -0
- data/web/assets/vendor/bootstrap/css/bootstrap-responsive.min.css +9 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.css +6167 -0
- data/web/assets/vendor/bootstrap/css/bootstrap.min.css +9 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings-white.png +0 -0
- data/web/assets/vendor/bootstrap/img/glyphicons-halflings.png +0 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.js +2280 -0
- data/web/assets/vendor/bootstrap/js/bootstrap.min.js +6 -0
- data/web/assets/vendor/google-code-prettify/lang-basic.js +3 -0
- data/web/assets/vendor/google-code-prettify/prettify.css +1 -0
- data/web/assets/vendor/google-code-prettify/prettify.js +30 -0
- data/web/assets/vendor/google-code-prettify/run_prettify.js +34 -0
- data/web/assets/vendor/jquery/jquery.js +9807 -0
- data/web/views/flow.erb +19 -0
- data/web/views/flow_step.erb +31 -0
- data/web/views/index.erb +39 -0
- data/web/views/layout.erb +20 -0
- data/web/views/plan_step.erb +11 -0
- data/web/views/show.erb +54 -0
- metadata +250 -11
- data/examples/events.rb +0 -71
- data/examples/workflow.rb +0 -140
- data/lib/dynflow/bus.rb +0 -168
- data/lib/dynflow/dispatcher.rb +0 -36
- data/lib/dynflow/logger.rb +0 -34
- data/lib/dynflow/step.rb +0 -234
- data/test/bus_test.rb +0 -150
|
@@ -0,0 +1,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 '
|
|
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
|