dynflow 0.7.7 → 0.7.8

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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  ![Gem version](https://img.shields.io/gem/v/dynflow.svg?style=flat)
7
7
  ![License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)
8
8
 
9
- **Note:** *There is a project page and documentation being build in url <http://http://dynflow.github.io/>.
9
+ **Note:** *There is a project page and documentation being build in url <http://dynflow.github.io/>.
10
10
  It's still work in progress but you may find useful information there. It'll eventually replace
11
11
  this README.*
12
12
 
@@ -4,5 +4,4 @@ gem 'rake'
4
4
  gem 'jekyll'
5
5
  gem 'pry'
6
6
  gem 'ruby-nuggets' # require by tags plugin
7
-
8
- gem 'pry'
7
+ gem 'therubyracer'
@@ -17,9 +17,10 @@ task :publish do
17
17
  system 'git fetch --all'
18
18
  system 'git reset --hard origin/master'
19
19
  end
20
- system 'jekyll build'
20
+ sh 'jekyll build'
21
21
  Dir.chdir(root + '/public') do
22
- system 'git ac -m Update'
22
+ system 'git add -A .'
23
+ system 'git commit -m Update'
23
24
  system 'git push'
24
25
  end
25
26
  end
@@ -371,9 +371,14 @@ module Dynflow
371
371
 
372
372
  # DSL for run phase
373
373
 
374
+ def suspended_action
375
+ phase! Run
376
+ @suspended_action ||= Action::Suspended.new(self)
377
+ end
378
+
374
379
  def suspend(&block)
375
380
  phase! Run
376
- block.call Action::Suspended.new self if block
381
+ block.call suspended_action if block
377
382
  throw SUSPEND, SUSPEND
378
383
  end
379
384
 
@@ -1,6 +1,12 @@
1
+ require 'dynflow/action/timeouts'
2
+
1
3
  module Dynflow
2
4
  module Action::Polling
3
5
 
6
+ def self.included(base)
7
+ base.send :include, Action::Timeouts
8
+ end
9
+
4
10
  Poll = Algebrick.atom
5
11
 
6
12
  def run(event = nil)
@@ -13,6 +19,9 @@ module Dynflow
13
19
  end
14
20
  when Poll
15
21
  poll_external_task_with_rescue
22
+ when Action::Timeouts::Timeout
23
+ process_timeout
24
+ suspend
16
25
  else
17
26
  raise "unrecognized event #{event}"
18
27
  end
@@ -0,0 +1,13 @@
1
+ module Dynflow
2
+ module Action::Timeouts
3
+ Timeout = Algebrick.atom
4
+
5
+ def process_timeout
6
+ fail("Timeout exceeded.")
7
+ end
8
+
9
+ def schedule_timeout(seconds)
10
+ world.clock.ping suspended_action, seconds, Timeout
11
+ end
12
+ end
13
+ end
@@ -34,6 +34,10 @@ module Dynflow
34
34
  end
35
35
  end
36
36
 
37
+ def delete_execution_plans(filters, batch_size = 1000)
38
+ adapter.delete_execution_plans(filters, batch_size)
39
+ end
40
+
37
41
  def load_execution_plan(id)
38
42
  execution_plan_hash = adapter.load_execution_plan(id)
39
43
  ExecutionPlan.new_from_hash(execution_plan_hash, @world)
@@ -30,12 +30,20 @@ module Dynflow
30
30
  # @option options [Integer] per_page the number of the items on page
31
31
  # @option options [Symbol] order_by name of the column to use for ordering
32
32
  # @option options [true, false] desc set to true if order should be descending
33
- # @option options [Hash{ Symbol => Object,Array<object> }] filters hash represents
33
+ # @option options [Hash{ String => Object,Array<object> }] filters hash represents
34
34
  # set of allowed values for a given key representing column
35
35
  def find_execution_plans(options = {})
36
36
  raise NotImplementedError
37
37
  end
38
38
 
39
+ # @param filters [Hash{ String => Object }] filters to determine
40
+ # what to delete
41
+ # @param batch_size the size of the chunks to iterate over when
42
+ # performing the deletion
43
+ def delete_execution_plans(filters, batch_size = 1000)
44
+ raise NotImplementedError
45
+ end
46
+
39
47
  def load_execution_plan(execution_plan_id)
40
48
  raise NotImplementedError
41
49
  end
@@ -36,13 +36,26 @@ module Dynflow
36
36
  end
37
37
 
38
38
  def find_execution_plans(options = {})
39
- data_set = filter(order(paginate(table(:execution_plan), options), options), options)
39
+ data_set = filter(order(paginate(table(:execution_plan), options), options), options[:filters])
40
40
 
41
41
  data_set.map do |record|
42
42
  HashWithIndifferentAccess.new(MultiJson.load(record[:data]))
43
43
  end
44
44
  end
45
45
 
46
+ def delete_execution_plans(filters, batch_size = 1000)
47
+ count = 0
48
+ filter(table(:execution_plan), filters).each_slice(batch_size) do |plans|
49
+ uuids = plans.map { |p| p.fetch(:uuid) }
50
+ @db.transaction do
51
+ table(:step).where(execution_plan_uuid: uuids).delete
52
+ table(:action).where(execution_plan_uuid: uuids).delete
53
+ count += table(:execution_plan).where(uuid: uuids).delete
54
+ end
55
+ end
56
+ return count
57
+ end
58
+
46
59
  def load_execution_plan(execution_plan_id)
47
60
  load :execution_plan, uuid: execution_plan_id
48
61
  end
@@ -68,9 +81,9 @@ module Dynflow
68
81
  end
69
82
 
70
83
  def to_hash
71
- { execution_plans: table(:execution_plan).all,
72
- steps: table(:step).all,
73
- actions: table(:action).all }
84
+ { execution_plans: table(:execution_plan).all.to_a,
85
+ steps: table(:step).all.to_a,
86
+ actions: table(:action).all.to_a }
74
87
  end
75
88
 
76
89
  private
@@ -148,10 +161,10 @@ module Dynflow
148
161
  data_set.order_by options[:desc] ? ::Sequel.desc(order_by) : order_by
149
162
  end
150
163
 
151
- def filter(data_set, options)
152
- filters = Type! options[:filters], NilClass, Hash
164
+ def filter(data_set, filters)
165
+ Type! filters, NilClass, Hash
153
166
  return data_set if filters.nil?
154
- unknown = filters.keys - META_DATA.fetch(:execution_plan) - %w[caller_execution_plan_id caller_action_id]
167
+ unknown = filters.keys - META_DATA.fetch(:execution_plan) - %w[uuid caller_execution_plan_id caller_action_id]
155
168
 
156
169
  if filters.key?('caller_action_id') && !filters.key?('caller_execution_plan_id')
157
170
  raise ArgumentError, "caller_action_id given but caller_execution_plan_id missing"
@@ -12,11 +12,15 @@ module Dynflow
12
12
  @events_to_process << [execution_plan_id, step_id, event, future]
13
13
  end
14
14
 
15
+ # returns true if some event was processed.
15
16
  def progress
16
17
  events = @events_to_process.dup
17
18
  clear
18
19
  events.each do |execution_plan_id, step_id, event, future|
19
20
  future.resolve true
21
+ if event && world.action.state != :suspended
22
+ return false
23
+ end
20
24
  world.action.execute event
21
25
  end
22
26
  end
@@ -97,8 +97,9 @@ module Dynflow
97
97
 
98
98
  def progress_action_time action
99
99
  Match! action.phase, Action::Run
100
- action.world.clock.progress
101
- action.world.executor.progress
100
+ if action.world.clock.progress
101
+ return action.world.executor.progress
102
+ end
102
103
  end
103
104
  end
104
105
  end
@@ -5,30 +5,29 @@ module Dynflow
5
5
  attr_reader :pending_pings
6
6
 
7
7
  include Algebrick::Types
8
- Timer = Algebrick.type do
9
- fields! who: Object, # to ping back
10
- when: type { variants Time, Numeric }, # to deliver
11
- what: Maybe[Object], # to send
12
- where: Symbol # it should be delivered, which method
13
- end
14
-
15
- module Timer
16
- include Clock::Timer
17
- end
18
8
 
19
9
  def initialize
20
10
  @pending_pings = []
21
11
  end
22
12
 
23
13
  def ping(who, time, with_what = nil, where = :<<)
14
+ time = current_time + time if time.is_a? Numeric
24
15
  with = with_what.nil? ? None : Some[Object][with_what]
25
- @pending_pings << Timer[who, time, with, where]
16
+ @pending_pings << Clock::Timer[who, time, with, where]
17
+ @pending_pings.sort!
26
18
  end
27
19
 
28
20
  def progress
29
- copy = @pending_pings.dup
30
- clear
31
- copy.each { |ping| ping.apply }
21
+ if next_ping = @pending_pings.shift
22
+ # we are testing an isolated system = we can move in time
23
+ # without actually waiting
24
+ @current_time = next_ping.when
25
+ next_ping.apply
26
+ end
27
+ end
28
+
29
+ def current_time
30
+ @current_time ||= Time.now
32
31
  end
33
32
 
34
33
  def clear
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.7.7'
2
+ VERSION = '0.7.8'
3
3
  end
@@ -172,85 +172,138 @@ module Dynflow
172
172
  end
173
173
  end
174
174
 
175
- let(:plan) do
176
- create_and_plan_action TestPollingAction, { task_args: 'do something' }
177
- end
178
-
179
- before do
180
- TestPollingAction.config = TestPollingAction::Config.new
175
+ class NonRunningExternalService < ExternalService
176
+ def poll(id)
177
+ return { message: 'nothing changed' }
178
+ end
181
179
  end
182
180
 
183
- def next_ping(action)
184
- action.world.clock.pending_pings.first
185
- end
181
+ class TestTimeoutAction < TestPollingAction
182
+ class Config < TestPollingAction::Config
183
+ def initialize
184
+ super
185
+ @external_service = NonRunningExternalService.new
186
+ end
187
+ end
186
188
 
187
- it 'initiates the external task' do
188
- action = run_action plan
189
+ def done?
190
+ self.state == :error
191
+ end
189
192
 
190
- action.output[:task][:task_id].must_equal 123
193
+ def invoke_external_task
194
+ schedule_timeout(5)
195
+ super
196
+ end
191
197
  end
192
198
 
193
- it 'polls till the task is done' do
194
- action = run_action plan
199
+ describe'without timeout' do
200
+ let(:plan) do
201
+ create_and_plan_action TestPollingAction, { task_args: 'do something' }
202
+ end
195
203
 
196
- 9.times { progress_action_time action }
197
- action.done?.must_equal false
198
- next_ping(action).wont_be_nil
199
- action.state.must_equal :suspended
204
+ before do
205
+ TestPollingAction.config = TestPollingAction::Config.new
206
+ end
200
207
 
201
- progress_action_time action
202
- action.done?.must_equal true
203
- next_ping(action).must_be_nil
204
- action.state.must_equal :success
205
- end
208
+ def next_ping(action)
209
+ action.world.clock.pending_pings.first
210
+ end
206
211
 
207
- it 'tries to poll for the old task when resuming' do
208
- action = run_action plan
209
- action.output[:task][:progress].must_equal 0
210
- run_action action
211
- action.output[:task][:progress].must_equal 10
212
- end
212
+ it 'initiates the external task' do
213
+ action = run_action plan
213
214
 
214
- it 'invokes the external task again when polling on the old one fails' do
215
- action = run_action plan
216
- action.world.silence_logger!
217
- action.external_service.will_fail
218
- action.output[:task][:progress].must_equal 0
219
- run_action action
220
- action.output[:task][:progress].must_equal 0
221
- end
215
+ action.output[:task][:task_id].must_equal 123
216
+ end
222
217
 
223
- it 'tolerates some failure while polling' do
224
- action = run_action plan
225
- action.external_service.will_fail
226
- action.world.silence_logger!
218
+ it 'polls till the task is done' do
219
+ action = run_action plan
227
220
 
228
- TestPollingAction.config.poll_max_retries = 3
229
- (1..2).each do |attempt|
230
- progress_action_time action
231
- action.poll_attempts[:failed].must_equal attempt
221
+ 9.times { progress_action_time action }
222
+ action.done?.must_equal false
232
223
  next_ping(action).wont_be_nil
233
224
  action.state.must_equal :suspended
225
+
226
+ progress_action_time action
227
+ action.done?.must_equal true
228
+ next_ping(action).must_be_nil
229
+ action.state.must_equal :success
234
230
  end
235
231
 
236
- progress_action_time action
237
- action.poll_attempts[:failed].must_equal 3
238
- next_ping(action).must_be_nil
239
- action.state.must_equal :error
240
- end
232
+ it 'tries to poll for the old task when resuming' do
233
+ action = run_action plan
234
+ action.output[:task][:progress].must_equal 0
235
+ run_action action
236
+ action.output[:task][:progress].must_equal 10
237
+ end
238
+
239
+ it 'invokes the external task again when polling on the old one fails' do
240
+ action = run_action plan
241
+ action.world.silence_logger!
242
+ action.external_service.will_fail
243
+ action.output[:task][:progress].must_equal 0
244
+ run_action action
245
+ action.output[:task][:progress].must_equal 0
246
+ end
247
+
248
+ it 'tolerates some failure while polling' do
249
+ action = run_action plan
250
+ action.external_service.will_fail
251
+ action.world.silence_logger!
252
+
253
+ TestPollingAction.config.poll_max_retries = 3
254
+ (1..2).each do |attempt|
255
+ progress_action_time action
256
+ action.poll_attempts[:failed].must_equal attempt
257
+ next_ping(action).wont_be_nil
258
+ action.state.must_equal :suspended
259
+ end
260
+
261
+ progress_action_time action
262
+ action.poll_attempts[:failed].must_equal 3
263
+ next_ping(action).must_be_nil
264
+ action.state.must_equal :error
265
+ end
241
266
 
242
- it 'allows increasing poll interval in a time' do
243
- TestPollingAction.config.poll_intervals = [1, 2]
244
- TestPollingAction.config.attempts_before_next_interval = 1
267
+ it 'allows increasing poll interval in a time' do
268
+ TestPollingAction.config.poll_intervals = [1, 2]
269
+ TestPollingAction.config.attempts_before_next_interval = 2
245
270
 
246
- action = run_action plan
247
- next_ping(action).when.must_equal 1
248
- progress_action_time action
249
- next_ping(action).when.must_equal 2
250
- progress_action_time action
251
- next_ping(action).when.must_equal 2
271
+ action = run_action plan
272
+ pings = []
273
+ pings << next_ping(action)
274
+ progress_action_time action
275
+ pings << next_ping(action)
276
+ progress_action_time action
277
+ pings << next_ping(action)
278
+ progress_action_time action
279
+ (pings[1].when - pings[0].when).must_be_close_to 1
280
+ (pings[2].when - pings[1].when).must_be_close_to 2
281
+ end
252
282
  end
253
283
 
284
+ describe 'with timeout' do
285
+ let(:plan) do
286
+ create_and_plan_action TestTimeoutAction, { task_args: 'do something' }
287
+ end
288
+
289
+ before do
290
+ TestTimeoutAction.config = TestTimeoutAction::Config.new
291
+ TestTimeoutAction.config.poll_intervals = [2]
292
+ end
293
+
294
+ it 'timesout' do
295
+ action = run_action plan
296
+ iterations = 0
297
+ while progress_action_time action
298
+ # we count the number of iterations till the timeout occurs
299
+ iterations += 1
300
+ end
301
+ action.state.must_equal :error
302
+ # two polls in 2 seconds intervals untill the 5 seconds
303
+ # timeout appears
304
+ iterations.must_equal 3
305
+ end
306
+ end
254
307
  end
255
308
 
256
309
  describe Action::WithSubPlans do
@@ -0,0 +1,191 @@
1
+ require_relative 'test_helper'
2
+ require 'fileutils'
3
+
4
+ module Dynflow
5
+ module PersistenceTest
6
+ describe 'persistence adapters' do
7
+
8
+ let :execution_plans_data do
9
+ [{ id: 'plan1', state: 'paused' },
10
+ { id: 'plan2', state: 'stopped' },
11
+ { id: 'plan3', state: 'paused' }]
12
+ end
13
+
14
+ let :action_data do
15
+ { id: 1, caller_execution_plan_id: nil, caller_action_id: nil }
16
+ end
17
+
18
+ let :step_data do
19
+ { id: 1,
20
+ state: 'success',
21
+ started_at: '2015-02-24 10:00',
22
+ ended_at: '2015-02-24 10:01',
23
+ real_time: 1.1,
24
+ execution_time: 0.1,
25
+ action_id: 1,
26
+ progress_done: 1,
27
+ progress_weight: 2.5 }
28
+ end
29
+
30
+ def prepare_plans
31
+ execution_plans_data.map do |h|
32
+ h.merge result: nil, started_at: (Time.now-20).to_s, ended_at: (Time.now-10).to_s,
33
+ real_time: 0.0, execution_time: 0.0
34
+ end.tap do |plans|
35
+ plans.each { |plan| adapter.save_execution_plan(plan[:id], plan) }
36
+ end
37
+ end
38
+
39
+ def prepare_action(plan)
40
+ adapter.save_action(plan, action_data[:id], action_data)
41
+ end
42
+
43
+ def prepare_step(plan)
44
+ adapter.save_step(plan, step_data[:id], step_data)
45
+ end
46
+
47
+ def prepare_plans_with_actions
48
+ prepare_plans.each do |plan|
49
+ prepare_action(plan[:id])
50
+ end
51
+ end
52
+
53
+ def prepare_plans_with_steps
54
+ prepare_plans_with_actions.map do |plan|
55
+ prepare_step(plan[:id])
56
+ end
57
+ end
58
+
59
+ def self.it_acts_as_persistence_adapter
60
+ before do
61
+ # the tests expect clean field
62
+ adapter.delete_execution_plans({})
63
+ end
64
+ describe '#find_execution_plans' do
65
+ it 'supports pagination' do
66
+ prepare_plans
67
+ if adapter.pagination?
68
+ loaded_plans = adapter.find_execution_plans(page: 0, per_page: 1)
69
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1']
70
+
71
+ loaded_plans = adapter.find_execution_plans(page: 1, per_page: 1)
72
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2']
73
+ end
74
+ end
75
+
76
+ it 'supports ordering' do
77
+ prepare_plans
78
+ if adapter.ordering_by.include?(:state)
79
+ loaded_plans = adapter.find_execution_plans(order_by: 'state')
80
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan3', 'plan2']
81
+
82
+ loaded_plans = adapter.find_execution_plans(order_by: 'state', desc: true)
83
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2', 'plan3', 'plan1']
84
+ end
85
+ end
86
+
87
+ it 'supports filtering' do
88
+ prepare_plans
89
+ if adapter.ordering_by.include?(:state)
90
+ loaded_plans = adapter.find_execution_plans(filters: { state: ['paused'] })
91
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan3']
92
+
93
+ loaded_plans = adapter.find_execution_plans(filters: { state: ['stopped'] })
94
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan2']
95
+
96
+ loaded_plans = adapter.find_execution_plans(filters: { state: [] })
97
+ loaded_plans.map { |h| h[:id] }.must_equal []
98
+
99
+ loaded_plans = adapter.find_execution_plans(filters: { state: ['stopped', 'paused'] })
100
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan2', 'plan3']
101
+
102
+ loaded_plans = adapter.find_execution_plans(filters: { 'state' => ['stopped', 'paused'] })
103
+ loaded_plans.map { |h| h[:id] }.must_equal ['plan1', 'plan2', 'plan3']
104
+ end
105
+ end
106
+ end
107
+
108
+ describe '#load_execution_plan and #save_execution_plan' do
109
+ it 'serializes/deserializes the plan data' do
110
+ -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
111
+ prepare_plans
112
+ adapter.load_execution_plan('plan1')[:id].must_equal 'plan1'
113
+ adapter.load_execution_plan('plan1')['id'].must_equal 'plan1'
114
+ adapter.load_execution_plan('plan1').keys.size.must_equal 7
115
+
116
+ adapter.save_execution_plan('plan1', nil)
117
+ -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
118
+ end
119
+ end
120
+
121
+ describe '#delete_execution_plans' do
122
+ it 'deletes selected execution plans, including steps and actions' do
123
+ prepare_plans_with_steps
124
+ adapter.delete_execution_plans('uuid' => 'plan1').must_equal 1
125
+ -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
126
+ -> { adapter.load_action('plan1', action_data[:id]) }.must_raise KeyError
127
+ -> { adapter.load_step('plan1', step_data[:id]) }.must_raise KeyError
128
+
129
+ # testing that no other plans where affected
130
+ adapter.load_execution_plan('plan2')
131
+ adapter.load_action('plan2', action_data[:id])
132
+ adapter.load_step('plan2', step_data[:id])
133
+
134
+ prepare_plans_with_steps
135
+ adapter.delete_execution_plans('state' => 'paused').must_equal 2
136
+ -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
137
+ adapter.load_execution_plan('plan2') # nothing raised
138
+ -> { adapter.load_execution_plan('plan3') }.must_raise KeyError
139
+ end
140
+ end
141
+
142
+ describe '#load_action and #save_action' do
143
+ it 'serializes/deserializes the action data' do
144
+ prepare_plans
145
+ action_id = action_data[:id]
146
+ -> { adapter.load_action('plan1', action_id) }.must_raise KeyError
147
+
148
+ prepare_action('plan1')
149
+ loaded_action = adapter.load_action('plan1', action_id)
150
+ loaded_action[:id].must_equal action_id
151
+ loaded_action.must_equal(action_data.stringify_keys)
152
+
153
+ adapter.save_action('plan1', action_id, nil)
154
+ -> { adapter.load_action('plan1', action_id) }.must_raise KeyError
155
+
156
+ adapter.save_execution_plan('plan1', nil)
157
+ end
158
+ end
159
+
160
+ describe '#load_step and #save_step' do
161
+ it 'serializes/deserializes the step data' do
162
+ prepare_plans_with_actions
163
+ step_id = step_data[:id]
164
+ prepare_step('plan1')
165
+ loaded_step = adapter.load_step('plan1', step_id)
166
+ loaded_step[:id].must_equal step_id
167
+ loaded_step.must_equal(step_data.stringify_keys)
168
+ end
169
+ end
170
+ end
171
+
172
+ describe Dynflow::PersistenceAdapters::Sequel do
173
+ let(:adapter) { Dynflow::PersistenceAdapters::Sequel.new 'sqlite:/' }
174
+
175
+ it_acts_as_persistence_adapter
176
+
177
+ it 'allows inspecting the persisted content' do
178
+ plans = prepare_plans
179
+
180
+ plans.each do |original|
181
+ stored = adapter.to_hash.fetch(:execution_plans).find { |ep| ep[:uuid].strip == original[:id] }
182
+ stored.each { |k, v| stored[k] = v.to_s if v.is_a? Time }
183
+ adapter.class::META_DATA.fetch(:execution_plan).each do |name|
184
+ stored.fetch(name.to_sym).must_equal original.fetch(name.to_sym)
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: 0.7.8
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2015-03-17 00:00:00.000000000 Z
13
+ date: 2015-05-18 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -348,6 +348,7 @@ files:
348
348
  - lib/dynflow/action/progress.rb
349
349
  - lib/dynflow/action/rescue.rb
350
350
  - lib/dynflow/action/suspended.rb
351
+ - lib/dynflow/action/timeouts.rb
351
352
  - lib/dynflow/action/with_sub_plans.rb
352
353
  - lib/dynflow/clock.rb
353
354
  - lib/dynflow/daemon.rb
@@ -432,7 +433,7 @@ files:
432
433
  - test/execution_plan_test.rb
433
434
  - test/executor_test.rb
434
435
  - test/middleware_test.rb
435
- - test/persistance_adapters_test.rb
436
+ - test/persistence_test.rb
436
437
  - test/remote_via_socket_test.rb
437
438
  - test/rescue_test.rb
438
439
  - test/support/code_workflow_example.rb
@@ -493,7 +494,7 @@ test_files:
493
494
  - test/execution_plan_test.rb
494
495
  - test/executor_test.rb
495
496
  - test/middleware_test.rb
496
- - test/persistance_adapters_test.rb
497
+ - test/persistence_test.rb
497
498
  - test/remote_via_socket_test.rb
498
499
  - test/rescue_test.rb
499
500
  - test/support/code_workflow_example.rb
@@ -1,173 +0,0 @@
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, caller_execution_plan_id: nil, caller_action_id: nil }
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.must_equal %w[id caller_execution_plan_id caller_action_id]
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::Spec
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
-