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,160 @@
|
|
|
1
|
+
require 'sequel/no_core_ext' # to avoid sequel ~> 3.0 coliding with ActiveRecord
|
|
2
|
+
require 'multi_json'
|
|
3
|
+
|
|
4
|
+
module Dynflow
|
|
5
|
+
module PersistenceAdapters
|
|
6
|
+
|
|
7
|
+
Sequel.extension :migration
|
|
8
|
+
|
|
9
|
+
class Sequel < Abstract
|
|
10
|
+
include Algebrick::TypeCheck
|
|
11
|
+
|
|
12
|
+
attr_reader :db
|
|
13
|
+
|
|
14
|
+
def pagination?
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def filtering_by
|
|
19
|
+
META_DATA.fetch :execution_plan
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def ordering_by
|
|
23
|
+
META_DATA.fetch :execution_plan
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
META_DATA = { execution_plan: %w(state result started_at ended_at real_time execution_time),
|
|
27
|
+
action: [],
|
|
28
|
+
step: %w(state started_at ended_at real_time execution_time action_id) }
|
|
29
|
+
|
|
30
|
+
def initialize(db_path)
|
|
31
|
+
@db = initialize_db db_path
|
|
32
|
+
migrate_db
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def find_execution_plans(options = {})
|
|
36
|
+
data_set = filter(order(paginate(table(:execution_plan), options), options), options)
|
|
37
|
+
|
|
38
|
+
data_set.map do |record|
|
|
39
|
+
HashWithIndifferentAccess.new(MultiJson.load(record[:data]))
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def load_execution_plan(execution_plan_id)
|
|
44
|
+
load :execution_plan, uuid: execution_plan_id
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def save_execution_plan(execution_plan_id, value)
|
|
48
|
+
save :execution_plan, { uuid: execution_plan_id }, value
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def load_step(execution_plan_id, step_id)
|
|
52
|
+
load :step, execution_plan_uuid: execution_plan_id, id: step_id
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def save_step(execution_plan_id, step_id, value)
|
|
56
|
+
save :step, { execution_plan_uuid: execution_plan_id, id: step_id }, value
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def load_action(execution_plan_id, action_id)
|
|
60
|
+
load :action, execution_plan_uuid: execution_plan_id, id: action_id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def save_action(execution_plan_id, action_id, value)
|
|
64
|
+
save :action, { execution_plan_uuid: execution_plan_id, id: action_id }, value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def to_hash
|
|
68
|
+
{ execution_plans: table(:execution_plan).all,
|
|
69
|
+
steps: table(:step).all,
|
|
70
|
+
actions: table(:action).all }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
TABLES = { execution_plan: :dynflow_execution_plans,
|
|
76
|
+
action: :dynflow_actions,
|
|
77
|
+
step: :dynflow_steps }
|
|
78
|
+
|
|
79
|
+
def table(which)
|
|
80
|
+
db[TABLES.fetch(which)]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def initialize_db(db_path)
|
|
84
|
+
::Sequel.connect db_path
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.migrations_path
|
|
88
|
+
File.expand_path('../sequel_migrations', __FILE__)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def migrate_db
|
|
92
|
+
::Sequel::Migrator.run(db, self.class.migrations_path, table: 'dynflow_schema_info')
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def save(what, condition, value)
|
|
96
|
+
table = table(what)
|
|
97
|
+
existing_record = table.first condition
|
|
98
|
+
|
|
99
|
+
if value
|
|
100
|
+
value = value.with_indifferent_access
|
|
101
|
+
record = existing_record || condition
|
|
102
|
+
record[:data] = MultiJson.dump Type!(value, Hash)
|
|
103
|
+
meta_data = META_DATA.fetch(what).inject({}) { |h, k| h.update k.to_sym => value.fetch(k) }
|
|
104
|
+
record.merge! meta_data
|
|
105
|
+
record.each { |k, v| record[k] = v.to_s if v.is_a? Symbol }
|
|
106
|
+
|
|
107
|
+
if existing_record
|
|
108
|
+
table.where(condition).update(record)
|
|
109
|
+
else
|
|
110
|
+
table.insert record
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
else
|
|
114
|
+
existing_record and table.where(condition).delete
|
|
115
|
+
end
|
|
116
|
+
value
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def load(what, condition)
|
|
120
|
+
table = table(what)
|
|
121
|
+
if (record = table.first(condition.symbolize_keys))
|
|
122
|
+
HashWithIndifferentAccess.new MultiJson.load(record[:data])
|
|
123
|
+
else
|
|
124
|
+
raise KeyError, "searching: #{what} by: #{condition.inspect}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def paginate(data_set, options)
|
|
129
|
+
page = Integer(options[:page] || 0)
|
|
130
|
+
per_page = Integer(options[:per_page] || 20)
|
|
131
|
+
|
|
132
|
+
if page
|
|
133
|
+
data_set.limit per_page, per_page * page
|
|
134
|
+
else
|
|
135
|
+
data_set
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def order(data_set, options)
|
|
140
|
+
order_by = (options[:order_by] || :started_at).to_s
|
|
141
|
+
unless META_DATA.fetch(:execution_plan).include? order_by
|
|
142
|
+
raise ArgumentError, "unknown column #{order_by.inspect}"
|
|
143
|
+
end
|
|
144
|
+
order_by = order_by.to_sym
|
|
145
|
+
data_set.order_by options[:desc] ? ::Sequel.desc(order_by) : order_by
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def filter(data_set, options)
|
|
149
|
+
filters = Type! options[:filters], NilClass, Hash
|
|
150
|
+
return data_set if filters.nil?
|
|
151
|
+
|
|
152
|
+
unless (unknown = filters.keys - META_DATA.fetch(:execution_plan)).empty?
|
|
153
|
+
raise ArgumentError, "unkown columns: #{unknown.inspect}"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
data_set.where filters.symbolize_keys
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Sequel.migration do
|
|
2
|
+
up do
|
|
3
|
+
create_table(:dynflow_execution_plans) do
|
|
4
|
+
column :uuid, String, primary_key: true, size: 36, fixed: true
|
|
5
|
+
index :uuid, :unique => true
|
|
6
|
+
|
|
7
|
+
column :data, String, text: true
|
|
8
|
+
|
|
9
|
+
column :state, String
|
|
10
|
+
column :result, String
|
|
11
|
+
column :started_at, Time
|
|
12
|
+
column :ended_at, Time
|
|
13
|
+
column :real_time, Float
|
|
14
|
+
column :execution_time, Float
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
create_table(:dynflow_actions) do
|
|
18
|
+
foreign_key :execution_plan_uuid, :dynflow_execution_plans, type: String, size: 36, fixed: true
|
|
19
|
+
index :execution_plan_uuid
|
|
20
|
+
column :id, Fixnum
|
|
21
|
+
primary_key [:execution_plan_uuid, :id]
|
|
22
|
+
index [:execution_plan_uuid, :id], :unique => true
|
|
23
|
+
|
|
24
|
+
column :data, String, text: true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
create_table(:dynflow_steps) do
|
|
28
|
+
foreign_key :execution_plan_uuid, :dynflow_execution_plans, type: String, size: 36, fixed: true
|
|
29
|
+
index :execution_plan_uuid
|
|
30
|
+
column :id, Fixnum
|
|
31
|
+
primary_key [:execution_plan_uuid, :id]
|
|
32
|
+
index [:execution_plan_uuid, :id], :unique => true
|
|
33
|
+
column :action_id, Fixnum
|
|
34
|
+
foreign_key [:execution_plan_uuid, :action_id], :dynflow_actions
|
|
35
|
+
index [:execution_plan_uuid, :action_id]
|
|
36
|
+
|
|
37
|
+
column :data, String, text: true
|
|
38
|
+
|
|
39
|
+
column :state, String
|
|
40
|
+
column :started_at, Time
|
|
41
|
+
column :ended_at, Time
|
|
42
|
+
column :real_time, Float
|
|
43
|
+
column :execution_time, Float
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
down do
|
|
48
|
+
drop_table(:dynflow_steps)
|
|
49
|
+
drop_table(:dynflow_actions)
|
|
50
|
+
drop_table(:dynflow_execution_plans)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
class Serializable
|
|
3
|
+
def self.from_hash(hash, *args)
|
|
4
|
+
check_class_key_present hash
|
|
5
|
+
hash[:class].constantize.new_from_hash(hash, *args)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def to_hash
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @api private
|
|
13
|
+
def self.new_from_hash(hash, *args)
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
# new ...
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.check_class_matching(hash)
|
|
19
|
+
check_class_key_present hash
|
|
20
|
+
unless self.to_s == hash[:class]
|
|
21
|
+
raise ArgumentError, "class mismatch #{hash[:class]} != #{self}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.check_class_key_present(hash)
|
|
26
|
+
raise ArgumentError, 'missing :class' unless hash[:class]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method :check_class_matching, :check_class_key_present
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def recursive_to_hash(value)
|
|
34
|
+
case value
|
|
35
|
+
when Numeric, String, Symbol, TrueClass, FalseClass, NilClass
|
|
36
|
+
value
|
|
37
|
+
when Array
|
|
38
|
+
value.map { |v| recursive_to_hash v }
|
|
39
|
+
when Hash
|
|
40
|
+
value.inject({}) { |h, (k, v)| h.update k => recursive_to_hash(v) }
|
|
41
|
+
else
|
|
42
|
+
value.to_hash
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.string_to_time(string)
|
|
47
|
+
return if string.nil?
|
|
48
|
+
_, year, month, day, hour, min, sec = */(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})/.match(string)
|
|
49
|
+
Time.new(year.to_i, month.to_i, day.to_i, hour.to_i, min.to_i, sec.to_i)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def time_to_str(time)
|
|
53
|
+
return if time.nil?
|
|
54
|
+
Type! time, Time
|
|
55
|
+
time.strftime '%Y-%m-%d %H:%M:%S'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def self.hash_to_error(hash)
|
|
59
|
+
return nil if hash.nil?
|
|
60
|
+
ExecutionPlan::Steps::Error.from_hash(hash)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private_class_method :string_to_time, :hash_to_error
|
|
64
|
+
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
class SimpleWorld < World
|
|
3
|
+
def initialize(options_hash = {})
|
|
4
|
+
super options_hash
|
|
5
|
+
at_exit { self.terminate.wait } if options[:auto_terminate]
|
|
6
|
+
# we can check consistency here because SimpleWorld doesn't expect
|
|
7
|
+
# remote executor being in place.
|
|
8
|
+
self.consistency_check
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def default_options
|
|
12
|
+
super.merge(pool_size: 5,
|
|
13
|
+
persistence_adapter: PersistenceAdapters::Sequel.new('sqlite:/'),
|
|
14
|
+
transaction_adapter: TransactionAdapters::None.new,
|
|
15
|
+
auto_terminate: true)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Stateful
|
|
3
|
+
def self.included(base)
|
|
4
|
+
base.extend ClassMethods
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module ClassMethods
|
|
8
|
+
def states
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def state_transitions
|
|
13
|
+
raise NotImplementedError
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def states
|
|
18
|
+
self.class.states
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def state_transitions
|
|
22
|
+
self.class.state_transitions
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attr_reader :state
|
|
26
|
+
|
|
27
|
+
def state=(state)
|
|
28
|
+
set_state state, false
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def set_state(state, skip_transition_check)
|
|
32
|
+
state = state.to_sym if state.is_a?(String) && states.map(&:to_s).include?(state)
|
|
33
|
+
raise "unknown state #{state}" unless states.include? state
|
|
34
|
+
unless self.state.nil? || skip_transition_check || state_transitions.fetch(self.state).include?(state)
|
|
35
|
+
raise "invalid state transition #{self.state} >> #{state} in #{self}"
|
|
36
|
+
end
|
|
37
|
+
@state = state
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Testing
|
|
3
|
+
extend Algebrick::TypeCheck
|
|
4
|
+
|
|
5
|
+
def self.logger_adapter
|
|
6
|
+
@logger_adapter ||= LoggerAdapters::Simple.new $stdout, 0
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def self.logger_adapter=(adapter)
|
|
10
|
+
Type! adapter, LoggerAdapters::Abstract
|
|
11
|
+
@logger_adapter = adapter
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.get_id
|
|
15
|
+
@last_id ||= 0
|
|
16
|
+
@last_id += 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'dynflow/testing/mimic'
|
|
20
|
+
require 'dynflow/testing/managed_clock'
|
|
21
|
+
require 'dynflow/testing/dummy_world'
|
|
22
|
+
require 'dynflow/testing/dummy_executor'
|
|
23
|
+
require 'dynflow/testing/dummy_execution_plan'
|
|
24
|
+
require 'dynflow/testing/dummy_step'
|
|
25
|
+
require 'dynflow/testing/dummy_planned_action'
|
|
26
|
+
require 'dynflow/testing/assertions'
|
|
27
|
+
require 'dynflow/testing/factories'
|
|
28
|
+
|
|
29
|
+
include Assertions
|
|
30
|
+
include Factories
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Testing
|
|
3
|
+
module Assertions
|
|
4
|
+
# assert that +assert_actioned_plan+ was planned by +action+ with arguments +plan_input+
|
|
5
|
+
# alternatively plan-input can be asserted with +block+
|
|
6
|
+
def assert_action_planed_with(action, planned_action_class, *plan_input, &block)
|
|
7
|
+
found_classes = assert_action_planed(action, planned_action_class)
|
|
8
|
+
found = found_classes.select do |a|
|
|
9
|
+
if plan_input.empty?
|
|
10
|
+
block.call a.plan_input
|
|
11
|
+
else
|
|
12
|
+
a.plan_input == plan_input
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
assert(!found.empty?,
|
|
17
|
+
"Action #{planned_action_class} with plan_input #{plan_input} was not planned, there were only #{found_classes.map(&:plan_input)}")
|
|
18
|
+
found
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# assert that +assert_actioned_plan+ was planned by +action+
|
|
22
|
+
def assert_action_planed(action, planned_action_class)
|
|
23
|
+
Type! action, Dynflow::Action::PlanPhase
|
|
24
|
+
Match! action.state, :success
|
|
25
|
+
found = action.execution_plan.planned_plan_steps.
|
|
26
|
+
select { |a| a.is_a?(planned_action_class) }
|
|
27
|
+
|
|
28
|
+
assert(!found.empty?, "Action #{planned_action_class} was not planned")
|
|
29
|
+
found
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# assert that +action+ has run-phase planned
|
|
33
|
+
def assert_run_phase(action, input = nil, &block)
|
|
34
|
+
Type! action, Dynflow::Action::PlanPhase
|
|
35
|
+
Match! action.state, :success
|
|
36
|
+
action.execution_plan.planned_run_steps.must_include action
|
|
37
|
+
action.input.must_equal input.stringify_keys if input
|
|
38
|
+
block.call input if block
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# refute that +action+ has run-phase planned
|
|
42
|
+
def refute_run_phase(action)
|
|
43
|
+
Type! action, Dynflow::Action::PlanPhase
|
|
44
|
+
Match! action.state, :success
|
|
45
|
+
action.execution_plan.planned_run_steps.wont_include action
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# assert that +action+ has finalize-phase planned
|
|
49
|
+
def assert_finalize_phase(action)
|
|
50
|
+
Type! action, Dynflow::Action::PlanPhase
|
|
51
|
+
Match! action.state, :success
|
|
52
|
+
action.execution_plan.planned_finalize_steps.must_include action
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# refute that +action+ has finalize-phase planned
|
|
56
|
+
def refute_finalize_phase(action)
|
|
57
|
+
Type! action, Dynflow::Action::PlanPhase
|
|
58
|
+
Match! action.state, :success
|
|
59
|
+
action.execution_plan.planned_finalize_steps.wont_include action
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Dynflow
|
|
2
|
+
module Testing
|
|
3
|
+
class DummyExecutionPlan
|
|
4
|
+
extend Mimic
|
|
5
|
+
mimic! ExecutionPlan
|
|
6
|
+
|
|
7
|
+
attr_reader :id, :planned_plan_steps, :planned_run_steps, :planned_finalize_steps
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@id = Testing.get_id
|
|
11
|
+
@planned_plan_steps = []
|
|
12
|
+
@planned_run_steps = []
|
|
13
|
+
@planned_finalize_steps = []
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def world
|
|
17
|
+
@world ||= DummyWorld.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add_run_step(action)
|
|
21
|
+
@planned_run_steps << action
|
|
22
|
+
action
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_finalize_step(action)
|
|
26
|
+
@planned_finalize_steps << action
|
|
27
|
+
action
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def add_plan_step(klass, action)
|
|
31
|
+
@planned_plan_steps << action = DummyPlannedAction.new(klass)
|
|
32
|
+
action
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def switch_flow(*args, &block)
|
|
36
|
+
block.call
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|