dynflow 0.8.35 → 0.8.36

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.
@@ -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