dynflow 0.8.35 → 0.8.36

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,37 +1,66 @@
1
1
  module Dynflow
2
2
  module Serializers
3
+ # @abstract
4
+ # Used to serialize and deserialize arguments for storage in a database.
5
+ # Used by {DelayedPlan} to store arguments which should be passed into
6
+ # the {Dynflow::Action}'s #plan method when the plan is executed.
3
7
  class Abstract
4
8
 
5
9
  attr_reader :args, :serialized_args
6
10
 
11
+ # @param args [Array] arguments to be serialized
12
+ # @param serialized_args [nil, Array] arguments in their serialized form
7
13
  def initialize(args, serialized_args = nil)
8
14
  @args = args
9
15
  @serialized_args = serialized_args
10
16
  end
11
17
 
12
- def args
18
+ # Retrieves the arguments
19
+ #
20
+ # @raise [RuntimeError] if the deserialized arguments are not available
21
+ # @return [Array] the arguments
22
+ def args!
13
23
  raise "@args not set" if @args.nil?
14
24
  return @args
15
25
  end
16
26
 
17
- def serialized_args
27
+ # Retrieves the arguments in the serialized form
28
+ #
29
+ # @raise [RuntimeError] if the serialized arguments are not available
30
+ # @return [Array] the serialized arguments
31
+ def serialized_args!
18
32
  raise "@serialized_args not set" if @serialized_args.nil?
19
33
  return @serialized_args
20
34
  end
21
35
 
36
+ # Converts arguments into their serialized form, iterates over deserialized
37
+ # arguments, applying {#serialize} to each of them
38
+ #
39
+ # @raise [RuntimeError] if the deserialized arguments are not available
40
+ # @return [Array] the serialized arguments
22
41
  def perform_serialization!
23
- @serialized_args = args.map { |arg| serialize arg }
42
+ @serialized_args = args!.map { |arg| serialize arg }
24
43
  end
25
44
 
45
+ # Converts arguments into their deserialized form, iterates over serialized
46
+ # arguments, applying {#deserialize} to each of them
47
+ #
48
+ # @raise [RuntimeError] if the serialized arguments are not available
49
+ # @return [Array] the deserialized arguments
26
50
  def perform_deserialization!
27
- raise "@serialized_args not set" if @serialized_args.nil?
28
- @args = serialized_args.map { |arg| deserialize arg }
51
+ @args = serialized_args!.map { |arg| deserialize arg }
29
52
  end
30
53
 
54
+ # Converts an argument into it serialized form
55
+ #
56
+ # @param arg the argument to be serialized
31
57
  def serialize(arg)
32
58
  raise NotImplementedError
33
59
  end
34
60
 
61
+ # Converts a serialized argument into its deserialized form
62
+ #
63
+ # @param arg the argument to be deserialized
35
64
  def deserialize(arg)
36
65
  raise NotImplementedError
37
66
  end
@@ -1,3 +1,3 @@
1
1
  module Dynflow
2
- VERSION = '0.8.35'
2
+ VERSION = '0.8.36'.freeze
3
3
  end
@@ -69,6 +69,7 @@ module Dynflow
69
69
  plan = world.plan(ParentAction, 20)
70
70
  future = world.execute plan.id
71
71
  wait_for { future.completed? }
72
+ plan = world.persistence.load_execution_plan(plan.id)
72
73
  action = plan.entry_action
73
74
 
74
75
  action.output[:batch_count].must_equal action.total_count / action.batch_size
@@ -79,6 +80,7 @@ module Dynflow
79
80
  plan = world.plan(ParentAction, 20)
80
81
  future = world.execute plan.id
81
82
  wait_for { future.completed? }
83
+ plan = world.persistence.load_execution_plan(plan.id)
82
84
  action = plan.entry_action
83
85
  action.output[:batch_count].must_equal 1
84
86
  future.value.state.must_equal :paused
@@ -97,6 +99,7 @@ module Dynflow
97
99
  plan = world.plan(ParentAction, 10)
98
100
  future = world.execute plan.id
99
101
  wait_for { future.completed? }
102
+ plan = world.persistence.load_execution_plan(plan.id)
100
103
  action = plan.entry_action
101
104
  action.send(:can_spawn_next_batch?).must_equal false
102
105
  action.current_batch.must_be :empty?
@@ -146,6 +146,7 @@ module Dynflow
146
146
  wait_for { plan.sub_plans_count == total }
147
147
  world.event(plan.id, plan.steps.values.last.id, ::Dynflow::Action::Cancellable::Cancel)
148
148
  wait_for { triggered.completed? }
149
+ plan = world.persistence.load_execution_plan(plan.id)
149
150
  plan.entry_action.output[:failed_count].must_equal total
150
151
  world.throttle_limiter.core.ask!(:running).max.must_be :<=, 0
151
152
  end
@@ -154,10 +155,13 @@ module Dynflow
154
155
  it 'calculates time interval correctly' do
155
156
  world.stub :clock, klok do
156
157
  total = 10
157
- get_interval = ->(plan) { plan.entry_action.input[:concurrency_control][:time][:meta][:interval] }
158
+ get_interval = ->(plan) do
159
+ plan = world.persistence.load_execution_plan(plan.id)
160
+ plan.entry_action.input[:concurrency_control][:time][:meta][:interval]
161
+ end
158
162
 
159
163
  plan = world.plan(ParentAction, total, 1, 10)
160
- future = world.execute(plan.id)
164
+ world.execute(plan.id)
161
165
  wait_for { plan.sub_plans_count == total }
162
166
  wait_for { klok.progress; plan.sub_plans.all? { |sub| successful? sub } }
163
167
  # 10 tasks over 10 seconds, one task at a time, 1 task every second
@@ -0,0 +1,86 @@
1
+ require_relative 'test_helper'
2
+
3
+ module Dynflow
4
+ class ExecutionPlan
5
+ describe Hooks do
6
+ include PlanAssertions
7
+
8
+ let(:world) { WorldFactory.create_world }
9
+
10
+ class Flag
11
+ class << self
12
+ def raise!
13
+ @raised = true
14
+ end
15
+
16
+ def raised?
17
+ @raised
18
+ end
19
+
20
+ def lower!
21
+ @raised = false
22
+ end
23
+ end
24
+ end
25
+
26
+ module FlagHook
27
+ def raise_flag(_execution_plan)
28
+ Flag.raise!
29
+ end
30
+
31
+ def controlled_failure(_execution_plan)
32
+ Flag.raise!
33
+ raise "A controlled failure"
34
+ end
35
+ end
36
+
37
+ class ActionWithHooks < ::Dynflow::Action
38
+ include FlagHook
39
+
40
+ execution_plan_hooks.use :raise_flag, :on => :success
41
+ end
42
+
43
+ class ActionOnStop < ::Dynflow::Action
44
+ include FlagHook
45
+
46
+ execution_plan_hooks.use :controlled_failure, :on => :stopped
47
+ end
48
+
49
+ class Inherited < ActionWithHooks; end
50
+ class Overriden < ActionWithHooks
51
+ execution_plan_hooks.do_not_use :raise_flag
52
+ end
53
+
54
+ before { Flag.lower! }
55
+
56
+ it 'runs the on_success hook' do
57
+ refute Flag.raised?
58
+ plan = world.trigger(ActionWithHooks)
59
+ plan.finished.wait!
60
+ assert Flag.raised?
61
+ end
62
+
63
+ it 'does not alter the execution plan when exception happens in the hook' do
64
+ refute Flag.raised?
65
+ plan = world.plan(ActionOnStop)
66
+ plan = world.execute(plan.id).wait!.value
67
+ assert Flag.raised?
68
+ plan.result.must_equal :success
69
+ end
70
+
71
+ it 'inherits the hooks when subclassing' do
72
+ refute Flag.raised?
73
+ plan = world.trigger(Inherited)
74
+ plan.finished.wait!
75
+ assert Flag.raised?
76
+ end
77
+
78
+ it 'can override the hooks from the child' do
79
+ refute Flag.raised?
80
+ plan = world.trigger(Overriden)
81
+ plan.finished.wait!
82
+ refute Flag.raised?
83
+ end
84
+ end
85
+ end
86
+ end
@@ -221,8 +221,11 @@ module Dynflow
221
221
  result.started_at.wont_be_nil
222
222
  result.ended_at.wont_be_nil
223
223
  result.execution_time.must_be :<, result.real_time
224
- result.execution_time.must_equal(
225
- result.steps.inject(0) { |sum, (_, step)| sum + step.execution_time })
224
+
225
+ step_sum = result.steps.values.map(&:execution_time).reduce(:+)
226
+
227
+ # Storing floats can lead to slight deviations, 1ns precision should be enough
228
+ result.execution_time.must_be_close_to step_sum, 0.000_001
226
229
 
227
230
  plan_step = result.steps[1]
228
231
  plan_step.started_at.wont_be_nil
@@ -115,6 +115,9 @@ module Dynflow
115
115
  end
116
116
 
117
117
  describe 'serializers' do
118
+ let(:args) { %w(arg1 arg2) }
119
+ let(:serialized_serializer) { Dynflow::Serializers::Noop.new(nil, args) }
120
+ let(:deserialized_serializer) { Dynflow::Serializers::Noop.new(args, nil) }
118
121
  let(:save_and_load) do
119
122
  ->(thing) { MultiJson.load(MultiJson.dump(thing)) }
120
123
  end
@@ -134,6 +137,40 @@ module Dynflow
134
137
  input = [1, 2.0, 'three', ['four-1', 'four-2'], { 'five' => 5 }]
135
138
  simulated_use.call(Dynflow::Serializers::Noop, input).must_equal input
136
139
  end
140
+
141
+ it 'args! raises if not deserialized' do
142
+ proc { serialized_serializer.args! }.must_raise RuntimeError
143
+ deserialized_serializer.args! # Must not raise
144
+ end
145
+
146
+ it 'serialized_args! raises if not serialized' do
147
+ proc { deserialized_serializer.serialized_args! }.must_raise RuntimeError
148
+ serialized_serializer.serialized_args! # Must not raise
149
+ end
150
+
151
+ it 'performs_serialization!' do
152
+ deserialized_serializer.perform_serialization!
153
+ deserialized_serializer.serialized_args!.must_equal args
154
+ end
155
+
156
+ it 'performs_deserialization!' do
157
+ serialized_serializer.perform_deserialization!
158
+ serialized_serializer.args.must_equal args
159
+ end
160
+ end
161
+
162
+ describe 'delayed plan' do
163
+ let(:args) { %w(arg1 arg2) }
164
+ let(:serializer) { Dynflow::Serializers::Noop.new(nil, args) }
165
+ let(:delayed_plan) do
166
+ Dynflow::DelayedPlan.new(Dynflow::World.allocate, 'an uuid', nil, nil, serializer)
167
+ end
168
+
169
+ it "allows access to serializer's args" do
170
+ serializer.args.must_be :nil?
171
+ delayed_plan.args.must_equal args
172
+ serializer.args.must_equal args
173
+ end
137
174
  end
138
175
  end
139
176
  end
@@ -6,21 +6,31 @@ module Dynflow
6
6
  describe 'persistence adapters' do
7
7
 
8
8
  let :execution_plans_data do
9
- [{ id: 'plan1', :label => 'test1', state: 'paused' },
10
- { id: 'plan2', :label => 'test2', state: 'stopped' },
11
- { id: 'plan3', :label => 'test3', state: 'paused' },
12
- { id: 'plan4', :label => 'test4', state: 'paused' }]
9
+ [{ id: 'plan1', :label => 'test1', root_plan_step_id: 1, class: 'Dynflow::ExecutionPlan', state: 'paused' },
10
+ { id: 'plan2', :label => 'test2', root_plan_step_id: 1, class: 'Dynflow::ExecutionPlan', state: 'stopped' },
11
+ { id: 'plan3', :label => 'test3', root_plan_step_id: 1, class: 'Dynflow::ExecutionPlan', state: 'paused' },
12
+ { id: 'plan4', :label => 'test4', root_plan_step_id: 1, class: 'Dynflow::ExecutionPlan', state: 'paused' }]
13
13
  end
14
14
 
15
15
  let :action_data do
16
- { id: 1, caller_execution_plan_id: nil, caller_action_id: nil }
16
+ {
17
+ id: 1,
18
+ caller_execution_plan_id: nil,
19
+ caller_action_id: nil,
20
+ class: 'Dynflow::Action',
21
+ input: {key: 'value'},
22
+ output: {something: 'else'},
23
+ plan_step_id: 1,
24
+ run_step_id: 2,
25
+ finalize_step_id: 3
26
+ }
17
27
  end
18
28
 
19
29
  let :step_data do
20
30
  { id: 1,
21
31
  state: 'success',
22
- started_at: '2015-02-24 10:00',
23
- ended_at: '2015-02-24 10:01',
32
+ started_at: Time.now.utc - 60,
33
+ ended_at: Time.now.utc - 30,
24
34
  real_time: 1.1,
25
35
  execution_time: 0.1,
26
36
  action_id: 1,
@@ -30,13 +40,15 @@ module Dynflow
30
40
 
31
41
  def prepare_plans
32
42
  execution_plans_data.map do |h|
33
- h.merge result: nil, started_at: (Time.now-20).to_s, ended_at: (Time.now-10).to_s,
43
+ h.merge result: nil, started_at: Time.now.utc - 20, ended_at: Time.now.utc - 10,
34
44
  real_time: 0.0, execution_time: 0.0
35
- end.tap do |plans|
36
- plans.each { |plan| adapter.save_execution_plan(plan[:id], plan) }
37
45
  end
38
46
  end
39
47
 
48
+ def prepare_and_save_plans
49
+ prepare_plans.each { |plan| adapter.save_execution_plan(plan[:id], plan) }
50
+ end
51
+
40
52
  def format_time(time)
41
53
  time.strftime('%Y-%m-%d %H:%M:%S')
42
54
  end
@@ -46,18 +58,38 @@ module Dynflow
46
58
  end
47
59
 
48
60
  def prepare_step(plan)
49
- adapter.save_step(plan, step_data[:id], step_data)
61
+ step = step_data.dup
62
+ step[:execution_plan_uuid] = plan
63
+ step
64
+ end
65
+
66
+ def prepare_and_save_step(plan)
67
+ step = prepare_step(plan)
68
+ adapter.save_step(plan, step[:id], step)
50
69
  end
51
70
 
52
71
  def prepare_plans_with_actions
53
- prepare_plans.each do |plan|
72
+ prepare_and_save_plans.each do |plan|
54
73
  prepare_action(plan[:id])
55
74
  end
56
75
  end
57
76
 
58
77
  def prepare_plans_with_steps
59
78
  prepare_plans_with_actions.map do |plan|
60
- prepare_step(plan[:id])
79
+ prepare_and_save_step(plan[:id])
80
+ end
81
+ end
82
+
83
+ def assert_equal_attributes!(original, loaded)
84
+ original.each do |key, value|
85
+ loaded_value = loaded[key.to_s]
86
+ if value.is_a?(Time)
87
+ loaded_value.inspect.must_equal value.inspect
88
+ elsif value.is_a?(Hash)
89
+ assert_equal_attributes!(value, loaded_value)
90
+ else
91
+ loaded[key.to_s].must_equal value
92
+ end
61
93
  end
62
94
  end
63
95
 
@@ -69,7 +101,7 @@ module Dynflow
69
101
  end
70
102
  describe '#find_execution_plans' do
71
103
  it 'supports pagination' do
72
- prepare_plans
104
+ prepare_and_save_plans
73
105
  if adapter.pagination?
74
106
  loaded_plans = adapter.find_execution_plans(page: 0, per_page: 1)
75
107
  loaded_plans.map { |h| h[:id] }.must_equal ['plan1']
@@ -80,7 +112,7 @@ module Dynflow
80
112
  end
81
113
 
82
114
  it 'supports ordering' do
83
- prepare_plans
115
+ prepare_and_save_plans
84
116
  if adapter.ordering_by.include?('state')
85
117
  loaded_plans = adapter.find_execution_plans(order_by: 'state')
86
118
  loaded_plans.map { |h| h[:id] }.must_equal %w(plan1 plan3 plan4 plan2)
@@ -91,7 +123,7 @@ module Dynflow
91
123
  end
92
124
 
93
125
  it 'supports filtering' do
94
- prepare_plans
126
+ prepare_and_save_plans
95
127
  if adapter.ordering_by.include?('state')
96
128
  loaded_plans = adapter.find_execution_plans(filters: { label: ['test1'] })
97
129
  loaded_plans.map { |h| h[:id] }.must_equal ['plan1']
@@ -130,7 +162,7 @@ module Dynflow
130
162
  end
131
163
 
132
164
  it 'supports filtering' do
133
- prepare_plans
165
+ prepare_and_save_plans
134
166
  if adapter.ordering_by.include?('state')
135
167
  loaded_plans = adapter.find_execution_plan_counts(filters: { label: ['test1'] })
136
168
  loaded_plans.must_equal 1
@@ -165,10 +197,12 @@ module Dynflow
165
197
  describe '#load_execution_plan and #save_execution_plan' do
166
198
  it 'serializes/deserializes the plan data' do
167
199
  -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
168
- prepare_plans
169
- adapter.load_execution_plan('plan1')[:id].must_equal 'plan1'
170
- adapter.load_execution_plan('plan1')['id'].must_equal 'plan1'
171
- adapter.load_execution_plan('plan1').keys.size.must_equal 8
200
+ plan = prepare_and_save_plans.first
201
+ loaded_plan = adapter.load_execution_plan('plan1')
202
+ loaded_plan[:id].must_equal 'plan1'
203
+ loaded_plan['id'].must_equal 'plan1'
204
+
205
+ assert_equal_attributes!(plan, loaded_plan)
172
206
 
173
207
  adapter.save_execution_plan('plan1', nil)
174
208
  -> { adapter.load_execution_plan('plan1') }.must_raise KeyError
@@ -214,37 +248,60 @@ module Dynflow
214
248
 
215
249
  describe '#load_action and #save_action' do
216
250
  it 'serializes/deserializes the action data' do
217
- prepare_plans
251
+ prepare_and_save_plans
252
+ action = action_data.dup
218
253
  action_id = action_data[:id]
219
254
  -> { adapter.load_action('plan1', action_id) }.must_raise KeyError
220
255
 
221
256
  prepare_action('plan1')
222
257
  loaded_action = adapter.load_action('plan1', action_id)
223
258
  loaded_action[:id].must_equal action_id
224
- loaded_action.must_equal(Utils.stringify_keys(action_data))
259
+
260
+ assert_equal_attributes!(action, loaded_action)
225
261
 
226
262
  adapter.save_action('plan1', action_id, nil)
227
263
  -> { adapter.load_action('plan1', action_id) }.must_raise KeyError
228
264
 
229
265
  adapter.save_execution_plan('plan1', nil)
230
266
  end
267
+
268
+ it 'allow to retrieve specific attributes using #load_actions_attributes' do
269
+ prepare_and_save_plans
270
+ prepare_action('plan1')
271
+ loaded_data = adapter.load_actions_attributes('plan1', [:id, :run_step_id]).first
272
+ loaded_data.keys.count.must_equal 2
273
+ loaded_data[:id].must_equal action_data[:id]
274
+ loaded_data[:run_step_id].must_equal action_data[:run_step_id]
275
+ end
276
+
277
+ it 'allows to load actions in bulk using #load_actions' do
278
+ prepare_and_save_plans
279
+ prepare_action('plan1')
280
+ action = action_data.dup
281
+ loaded_actions = adapter.load_actions('plan1', [1])
282
+ loaded_actions.count.must_equal 1
283
+ loaded_action = loaded_actions.first
284
+
285
+ assert_equal_attributes!(action, loaded_action)
286
+ end
231
287
  end
232
288
 
233
289
  describe '#load_step and #save_step' do
234
290
  it 'serializes/deserializes the step data' do
235
291
  prepare_plans_with_actions
236
292
  step_id = step_data[:id]
237
- prepare_step('plan1')
293
+ prepare_and_save_step('plan1')
238
294
  loaded_step = adapter.load_step('plan1', step_id)
239
295
  loaded_step[:id].must_equal step_id
240
- loaded_step.must_equal(Utils.stringify_keys(step_data))
296
+
297
+ assert_equal_attributes!(step_data, loaded_step)
241
298
  end
242
299
  end
243
300
 
244
301
  describe '#find_past_delayed_plans' do
245
302
  it 'finds plans with start_before in past' do
246
- start_time = Time.now
247
- prepare_plans
303
+ start_time = Time.now.utc
304
+ prepare_and_save_plans
248
305
  adapter.save_delayed_plan('plan1', :execution_plan_uuid => 'plan1', :start_at => format_time(start_time + 60),
249
306
  :start_before => format_time(start_time - 60))
250
307
  adapter.save_delayed_plan('plan2', :execution_plan_uuid => 'plan2', :start_at => format_time(start_time - 60))
@@ -264,15 +321,16 @@ module Dynflow
264
321
  it_acts_as_persistence_adapter
265
322
 
266
323
  it 'allows inspecting the persisted content' do
267
- plans = prepare_plans
324
+ plans = prepare_and_save_plans
268
325
 
269
326
  plans.each do |original|
270
327
  stored = adapter.to_hash.fetch(:execution_plans).find { |ep| ep[:uuid].strip == original[:id] }
271
- stored.each { |k, v| stored[k] = v.to_s if v.is_a? Time }
272
328
  adapter.class::META_DATA.fetch(:execution_plan).each do |name|
273
329
  value = original.fetch(name.to_sym)
274
330
  if value.nil?
275
331
  stored.fetch(name.to_sym).must_be_nil
332
+ elsif value.is_a?(Time)
333
+ stored.fetch(name.to_sym).inspect.must_equal value.inspect
276
334
  else
277
335
  stored.fetch(name.to_sym).must_equal value
278
336
  end
@@ -296,6 +354,63 @@ module Dynflow
296
354
  assert_equal [], adapter.pull_envelopes(executor_world_id)
297
355
  end
298
356
 
357
+ it 'supports reading data saved prior to normalization' do
358
+ db = adapter.send(:db)
359
+ # Prepare records for saving
360
+ plan = prepare_plans.first
361
+ step_data = prepare_step(plan[:id])
362
+
363
+ # We used to store times as strings
364
+ plan[:started_at] = format_time plan[:started_at]
365
+ plan[:ended_at] = format_time plan[:ended_at]
366
+ step_data[:started_at] = format_time step_data[:started_at]
367
+ step_data[:ended_at] = format_time step_data[:ended_at]
368
+
369
+ plan_record = adapter.send(:prepare_record, :execution_plan, plan.merge(:uuid => plan[:id]))
370
+ action_record = adapter.send(:prepare_record, :action, action_data.dup)
371
+ step_record = adapter.send(:prepare_record, :step, step_data)
372
+
373
+ # Insert the records
374
+ db[:dynflow_execution_plans].insert plan_record.merge(:uuid => plan[:id])
375
+ db[:dynflow_actions].insert action_record.merge(:execution_plan_uuid => plan[:id], :id => action_data[:id])
376
+ db[:dynflow_steps].insert step_record.merge(:execution_plan_uuid => plan[:id], :id => step_data[:id])
377
+
378
+ # Load the saved records
379
+ loaded_plan = adapter.load_execution_plan(plan[:id])
380
+ loaded_action = adapter.load_action(plan[:id], action_data[:id])
381
+ loaded_step = adapter.load_step(plan[:id], step_data[:id])
382
+
383
+ # Test
384
+ assert_equal_attributes!(plan, loaded_plan)
385
+ assert_equal_attributes!(action_data, loaded_action)
386
+ assert_equal_attributes!(step_data, loaded_step)
387
+ end
388
+
389
+ it 'support updating data saved prior to normalization' do
390
+ db = adapter.send(:db)
391
+ plan = prepare_plans.first
392
+ plan_data = plan.dup
393
+ plan[:started_at] = format_time plan[:started_at]
394
+ plan[:ended_at] = format_time plan[:ended_at]
395
+ plan_record = adapter.send(:prepare_record, :execution_plan, plan.merge(:uuid => plan[:id]))
396
+
397
+ # Save the plan the old way
398
+ db[:dynflow_execution_plans].insert plan_record.merge(:uuid => plan[:id])
399
+
400
+ # Update and save the plan
401
+ plan_data[:state] = 'stopped'
402
+ plan_data[:result] = 'success'
403
+ adapter.save_execution_plan(plan[:id], plan_data)
404
+
405
+ # Check the plan has the changed columns populated
406
+ raw_plan = db[:dynflow_execution_plans].where(:uuid => 'plan1').first
407
+ raw_plan[:state].must_equal 'stopped'
408
+ raw_plan[:result].must_equal 'success'
409
+
410
+ # Load the plan and assert it doesn't read attributes from data
411
+ loaded_plan = adapter.load_execution_plan(plan[:id])
412
+ assert_equal_attributes!(plan_data, loaded_plan)
413
+ end
299
414
  end
300
415
  end
301
416
  end