launchdarkly-server-sdk 5.5.7

Sign up to get free protection for your applications and to get access to all the features.
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