launchdarkly-server-sdk 5.5.7

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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,471 @@
1
+ require "spec_helper"
2
+
3
+
4
+ describe LaunchDarkly::LDClient do
5
+ subject { LaunchDarkly::LDClient }
6
+ let(:offline_config) { LaunchDarkly::Config.new({offline: true}) }
7
+ let(:offline_client) do
8
+ subject.new("secret", offline_config)
9
+ end
10
+ let(:null_data) { LaunchDarkly::NullUpdateProcessor.new }
11
+ let(:logger) { double().as_null_object }
12
+ let(:config) { LaunchDarkly::Config.new({ send_events: false, data_source: null_data, logger: logger }) }
13
+ let(:client) do
14
+ subject.new("secret", config)
15
+ end
16
+ let(:feature) do
17
+ data = File.read(File.join("spec", "fixtures", "feature.json"))
18
+ JSON.parse(data, symbolize_names: true)
19
+ end
20
+ let(:user) do
21
+ {
22
+ key: "user@test.com",
23
+ custom: {
24
+ groups: [ "microsoft", "google" ]
25
+ }
26
+ }
27
+ end
28
+ let(:user_without_key) do
29
+ { name: "Keyless Joe" }
30
+ end
31
+
32
+ def event_processor
33
+ client.instance_variable_get(:@event_processor)
34
+ end
35
+
36
+ describe '#variation' do
37
+ feature_with_value = { key: "key", on: false, offVariation: 0, variations: ["value"], version: 100,
38
+ trackEvents: true, debugEventsUntilDate: 1000 }
39
+
40
+ it "returns the default value if the client is offline" do
41
+ result = offline_client.variation("doesntmatter", user, "default")
42
+ expect(result).to eq "default"
43
+ end
44
+
45
+ it "returns the default value for an unknown feature" do
46
+ expect(client.variation("badkey", user, "default")).to eq "default"
47
+ end
48
+
49
+ it "queues a feature request event for an unknown feature" do
50
+ expect(event_processor).to receive(:add_event).with(hash_including(
51
+ kind: "feature", key: "badkey", user: user, value: "default", default: "default"
52
+ ))
53
+ client.variation("badkey", user, "default")
54
+ end
55
+
56
+ it "returns the value for an existing feature" do
57
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
58
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
59
+ expect(client.variation("key", user, "default")).to eq "value"
60
+ end
61
+
62
+ it "returns the default value if a feature evaluates to nil" do
63
+ empty_feature = { key: "key", on: false, offVariation: nil }
64
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
65
+ config.feature_store.upsert(LaunchDarkly::FEATURES, empty_feature)
66
+ expect(client.variation("key", user, "default")).to eq "default"
67
+ end
68
+
69
+ it "queues a feature request event for an existing feature" do
70
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
71
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
72
+ expect(event_processor).to receive(:add_event).with(hash_including(
73
+ kind: "feature",
74
+ key: "key",
75
+ version: 100,
76
+ user: user,
77
+ variation: 0,
78
+ value: "value",
79
+ default: "default",
80
+ trackEvents: true,
81
+ debugEventsUntilDate: 1000
82
+ ))
83
+ client.variation("key", user, "default")
84
+ end
85
+
86
+ it "queues a feature event for an existing feature when user is nil" do
87
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
88
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
89
+ expect(event_processor).to receive(:add_event).with(hash_including(
90
+ kind: "feature",
91
+ key: "key",
92
+ version: 100,
93
+ user: nil,
94
+ variation: nil,
95
+ value: "default",
96
+ default: "default",
97
+ trackEvents: true,
98
+ debugEventsUntilDate: 1000
99
+ ))
100
+ client.variation("key", nil, "default")
101
+ end
102
+
103
+ it "queues a feature event for an existing feature when user key is nil" do
104
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
105
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
106
+ bad_user = { name: "Bob" }
107
+ expect(event_processor).to receive(:add_event).with(hash_including(
108
+ kind: "feature",
109
+ key: "key",
110
+ version: 100,
111
+ user: bad_user,
112
+ variation: nil,
113
+ value: "default",
114
+ default: "default",
115
+ trackEvents: true,
116
+ debugEventsUntilDate: 1000
117
+ ))
118
+ client.variation("key", bad_user, "default")
119
+ end
120
+ end
121
+
122
+ describe '#variation_detail' do
123
+ feature_with_value = { key: "key", on: false, offVariation: 0, variations: ["value"], version: 100,
124
+ trackEvents: true, debugEventsUntilDate: 1000 }
125
+
126
+ it "returns the default value if the client is offline" do
127
+ result = offline_client.variation_detail("doesntmatter", user, "default")
128
+ expected = LaunchDarkly::EvaluationDetail.new("default", nil, { kind: 'ERROR', errorKind: 'CLIENT_NOT_READY' })
129
+ expect(result).to eq expected
130
+ end
131
+
132
+ it "returns the default value for an unknown feature" do
133
+ result = client.variation_detail("badkey", user, "default")
134
+ expected = LaunchDarkly::EvaluationDetail.new("default", nil, { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND'})
135
+ expect(result).to eq expected
136
+ end
137
+
138
+ it "queues a feature request event for an unknown feature" do
139
+ expect(event_processor).to receive(:add_event).with(hash_including(
140
+ kind: "feature", key: "badkey", user: user, value: "default", default: "default",
141
+ reason: { kind: 'ERROR', errorKind: 'FLAG_NOT_FOUND' }
142
+ ))
143
+ client.variation_detail("badkey", user, "default")
144
+ end
145
+
146
+ it "returns a value for an existing feature" do
147
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
148
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
149
+ result = client.variation_detail("key", user, "default")
150
+ expected = LaunchDarkly::EvaluationDetail.new("value", 0, { kind: 'OFF' })
151
+ expect(result).to eq expected
152
+ end
153
+
154
+ it "returns the default value if a feature evaluates to nil" do
155
+ empty_feature = { key: "key", on: false, offVariation: nil }
156
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
157
+ config.feature_store.upsert(LaunchDarkly::FEATURES, empty_feature)
158
+ result = client.variation_detail("key", user, "default")
159
+ expected = LaunchDarkly::EvaluationDetail.new("default", nil, { kind: 'OFF' })
160
+ expect(result).to eq expected
161
+ expect(result.default_value?).to be true
162
+ end
163
+
164
+ it "queues a feature request event for an existing feature" do
165
+ config.feature_store.init({ LaunchDarkly::FEATURES => {} })
166
+ config.feature_store.upsert(LaunchDarkly::FEATURES, feature_with_value)
167
+ expect(event_processor).to receive(:add_event).with(hash_including(
168
+ kind: "feature",
169
+ key: "key",
170
+ version: 100,
171
+ user: user,
172
+ variation: 0,
173
+ value: "value",
174
+ default: "default",
175
+ trackEvents: true,
176
+ debugEventsUntilDate: 1000,
177
+ reason: { kind: "OFF" }
178
+ ))
179
+ client.variation_detail("key", user, "default")
180
+ end
181
+ end
182
+
183
+ describe '#all_flags' do
184
+ let(:flag1) { { key: "key1", offVariation: 0, variations: [ 'value1' ] } }
185
+ let(:flag2) { { key: "key2", offVariation: 0, variations: [ 'value2' ] } }
186
+
187
+ it "returns flag values" do
188
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
189
+
190
+ result = client.all_flags({ key: 'userkey' })
191
+ expect(result).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
192
+ end
193
+
194
+ it "returns empty map for nil user" do
195
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
196
+
197
+ result = client.all_flags(nil)
198
+ expect(result).to eq({})
199
+ end
200
+
201
+ it "returns empty map for nil user key" do
202
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
203
+
204
+ result = client.all_flags({})
205
+ expect(result).to eq({})
206
+ end
207
+
208
+ it "returns empty map if offline" do
209
+ offline_config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
210
+
211
+ result = offline_client.all_flags(nil)
212
+ expect(result).to eq({})
213
+ end
214
+ end
215
+
216
+ describe '#all_flags_state' do
217
+ let(:flag1) { { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false } }
218
+ let(:flag2) { { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 } }
219
+
220
+ it "returns flags state" do
221
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
222
+
223
+ state = client.all_flags_state({ key: 'userkey' })
224
+ expect(state.valid?).to be true
225
+
226
+ values = state.values_map
227
+ expect(values).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
228
+
229
+ result = state.as_json
230
+ expect(result).to eq({
231
+ 'key1' => 'value1',
232
+ 'key2' => 'value2',
233
+ '$flagsState' => {
234
+ 'key1' => {
235
+ :variation => 0,
236
+ :version => 100
237
+ },
238
+ 'key2' => {
239
+ :variation => 1,
240
+ :version => 200,
241
+ :trackEvents => true,
242
+ :debugEventsUntilDate => 1000
243
+ }
244
+ },
245
+ '$valid' => true
246
+ })
247
+ end
248
+
249
+ it "can be filtered for only client-side flags" do
250
+ flag1 = { key: "server-side-1", offVariation: 0, variations: [ 'a' ], clientSide: false }
251
+ flag2 = { key: "server-side-2", offVariation: 0, variations: [ 'b' ], clientSide: false }
252
+ flag3 = { key: "client-side-1", offVariation: 0, variations: [ 'value1' ], clientSide: true }
253
+ flag4 = { key: "client-side-2", offVariation: 0, variations: [ 'value2' ], clientSide: true }
254
+ config.feature_store.init({ LaunchDarkly::FEATURES => {
255
+ flag1[:key] => flag1, flag2[:key] => flag2, flag3[:key] => flag3, flag4[:key] => flag4
256
+ }})
257
+
258
+ state = client.all_flags_state({ key: 'userkey' }, client_side_only: true)
259
+ expect(state.valid?).to be true
260
+
261
+ values = state.values_map
262
+ expect(values).to eq({ 'client-side-1' => 'value1', 'client-side-2' => 'value2' })
263
+ end
264
+
265
+ it "can omit details for untracked flags" do
266
+ future_time = (Time.now.to_f * 1000).to_i + 100000
267
+ flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
268
+ flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true }
269
+ flag3 = { key: "key3", version: 300, offVariation: 1, variations: [ 'x', 'value3' ], debugEventsUntilDate: future_time }
270
+
271
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2, 'key3' => flag3 } })
272
+
273
+ state = client.all_flags_state({ key: 'userkey' }, { details_only_for_tracked_flags: true })
274
+ expect(state.valid?).to be true
275
+
276
+ values = state.values_map
277
+ expect(values).to eq({ 'key1' => 'value1', 'key2' => 'value2', 'key3' => 'value3' })
278
+
279
+ result = state.as_json
280
+ expect(result).to eq({
281
+ 'key1' => 'value1',
282
+ 'key2' => 'value2',
283
+ 'key3' => 'value3',
284
+ '$flagsState' => {
285
+ 'key1' => {
286
+ :variation => 0
287
+ },
288
+ 'key2' => {
289
+ :variation => 1,
290
+ :version => 200,
291
+ :trackEvents => true
292
+ },
293
+ 'key3' => {
294
+ :variation => 1,
295
+ :version => 300,
296
+ :debugEventsUntilDate => future_time
297
+ }
298
+ },
299
+ '$valid' => true
300
+ })
301
+ end
302
+
303
+ it "returns empty state for nil user" do
304
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
305
+
306
+ state = client.all_flags_state(nil)
307
+ expect(state.valid?).to be false
308
+ expect(state.values_map).to eq({})
309
+ end
310
+
311
+ it "returns empty state for nil user key" do
312
+ config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
313
+
314
+ state = client.all_flags_state({})
315
+ expect(state.valid?).to be false
316
+ expect(state.values_map).to eq({})
317
+ end
318
+
319
+ it "returns empty state if offline" do
320
+ offline_config.feature_store.init({ LaunchDarkly::FEATURES => { 'key1' => flag1, 'key2' => flag2 } })
321
+
322
+ state = offline_client.all_flags_state({ key: 'userkey' })
323
+ expect(state.valid?).to be false
324
+ expect(state.values_map).to eq({})
325
+ end
326
+ end
327
+
328
+ describe '#secure_mode_hash' do
329
+ it "will return the expected value for a known message and secret" do
330
+ result = client.secure_mode_hash({key: :Message})
331
+ expect(result).to eq "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"
332
+ end
333
+ end
334
+
335
+ describe '#track' do
336
+ it "queues up an custom event" do
337
+ expect(event_processor).to receive(:add_event).with(hash_including(kind: "custom", key: "custom_event_name", user: user, data: 42))
338
+ client.track("custom_event_name", user, 42)
339
+ end
340
+
341
+ it "does not send an event, and logs a warning, if user is nil" do
342
+ expect(event_processor).not_to receive(:add_event)
343
+ expect(logger).to receive(:warn)
344
+ client.track("custom_event_name", nil, nil)
345
+ end
346
+
347
+ it "does not send an event, and logs a warning, if user key is nil" do
348
+ expect(event_processor).not_to receive(:add_event)
349
+ expect(logger).to receive(:warn)
350
+ client.track("custom_event_name", user_without_key, nil)
351
+ end
352
+ end
353
+
354
+ describe '#identify' do
355
+ it "queues up an identify event" do
356
+ expect(event_processor).to receive(:add_event).with(hash_including(kind: "identify", key: user[:key], user: user))
357
+ client.identify(user)
358
+ end
359
+
360
+ it "does not send an event, and logs a warning, if user is nil" do
361
+ expect(event_processor).not_to receive(:add_event)
362
+ expect(logger).to receive(:warn)
363
+ client.identify(nil)
364
+ end
365
+
366
+ it "does not send an event, and logs a warning, if user key is nil" do
367
+ expect(event_processor).not_to receive(:add_event)
368
+ expect(logger).to receive(:warn)
369
+ client.identify(user_without_key)
370
+ end
371
+ end
372
+
373
+ describe 'with send_events: false' do
374
+ let(:config) { LaunchDarkly::Config.new({offline: true, send_events: false, data_source: null_data}) }
375
+ let(:client) { subject.new("secret", config) }
376
+
377
+ it "uses a NullEventProcessor" do
378
+ ep = client.instance_variable_get(:@event_processor)
379
+ expect(ep).to be_a(LaunchDarkly::NullEventProcessor)
380
+ end
381
+ end
382
+
383
+ describe 'with send_events: true' do
384
+ let(:config_with_events) { LaunchDarkly::Config.new({offline: false, send_events: true, data_source: null_data}) }
385
+ let(:client_with_events) { subject.new("secret", config_with_events) }
386
+
387
+ it "does not use a NullEventProcessor" do
388
+ ep = client_with_events.instance_variable_get(:@event_processor)
389
+ expect(ep).not_to be_a(LaunchDarkly::NullEventProcessor)
390
+ end
391
+ end
392
+
393
+ describe "feature store data ordering" do
394
+ let(:dependency_ordering_test_data) {
395
+ {
396
+ LaunchDarkly::FEATURES => {
397
+ a: { key: "a", prerequisites: [ { key: "b" }, { key: "c" } ] },
398
+ b: { key: "b", prerequisites: [ { key: "c" }, { key: "e" } ] },
399
+ c: { key: "c" },
400
+ d: { key: "d" },
401
+ e: { key: "e" },
402
+ f: { key: "f" }
403
+ },
404
+ LaunchDarkly::SEGMENTS => {
405
+ o: { key: "o" }
406
+ }
407
+ }
408
+ }
409
+
410
+ class FakeFeatureStore
411
+ attr_reader :received_data
412
+
413
+ def init(all_data)
414
+ @received_data = all_data
415
+ end
416
+ end
417
+
418
+ class FakeUpdateProcessor
419
+ def initialize(store, data)
420
+ @store = store
421
+ @data = data
422
+ end
423
+
424
+ def start
425
+ @store.init(@data)
426
+ ev = Concurrent::Event.new
427
+ ev.set
428
+ ev
429
+ end
430
+
431
+ def stop
432
+ end
433
+
434
+ def initialized?
435
+ true
436
+ end
437
+ end
438
+
439
+ it "passes data set to feature store in correct order on init" do
440
+ store = FakeFeatureStore.new
441
+ data_source_factory = lambda { |sdk_key, config| FakeUpdateProcessor.new(config.feature_store,
442
+ dependency_ordering_test_data) }
443
+ config = LaunchDarkly::Config.new(send_events: false, feature_store: store, data_source: data_source_factory)
444
+ client = subject.new("secret", config)
445
+
446
+ data = store.received_data
447
+ expect(data).not_to be_nil
448
+ expect(data.count).to eq(2)
449
+
450
+ # Segments should always come first
451
+ expect(data.keys[0]).to be(LaunchDarkly::SEGMENTS)
452
+ expect(data.values[0].count).to eq(dependency_ordering_test_data[LaunchDarkly::SEGMENTS].count)
453
+
454
+ # Features should be ordered so that a flag always appears after its prerequisites, if any
455
+ expect(data.keys[1]).to be(LaunchDarkly::FEATURES)
456
+ flags_map = data.values[1]
457
+ flags_list = flags_map.values
458
+ expect(flags_list.count).to eq(dependency_ordering_test_data[LaunchDarkly::FEATURES].count)
459
+ flags_list.each_with_index do |item, item_index|
460
+ (item[:prerequisites] || []).each do |prereq|
461
+ prereq = flags_map[prereq[:key].to_sym]
462
+ prereq_index = flags_list.index(prereq)
463
+ if prereq_index > item_index
464
+ all_keys = (flags_list.map { |f| f[:key] }).join(", ")
465
+ raise "#{item[:key]} depends on #{prereq[:key]}, but #{item[:key]} was listed first; keys in order are [#{all_keys}]"
466
+ end
467
+ end
468
+ end
469
+ end
470
+ end
471
+ end