@@ -2291,6 +2291,173 @@ public void testDecide_withDefaultDecideOptions() throws IOException {
2291
2291
assertTrue (decision .getReasons ().size () > 0 );
2292
2292
}
2293
2293
2294
+ // Holdout Tests
2295
+
2296
+ private OptimizelyClient createOptimizelyClientWithHoldouts (Context context ) throws IOException {
2297
+ String holdoutDatafile = loadRawResource (context , R .raw .holdouts_project_config );
2298
+ OptimizelyManager optimizelyManager = OptimizelyManager .builder (testProjectId ).build (context );
2299
+ optimizelyManager .initialize (context , holdoutDatafile );
2300
+ return optimizelyManager .getOptimizely ();
2301
+ }
2302
+
2303
+ @ Test
2304
+ public void testDecide_withHoldout () throws IOException {
2305
+ assumeTrue (datafileVersion == Integer .parseInt (ProjectConfig .Version .V4 .toString ()));
2306
+
2307
+ Context context = InstrumentationRegistry .getInstrumentation ().getTargetContext ();
2308
+ OptimizelyClient optimizelyClient = createOptimizelyClientWithHoldouts (context );
2309
+
2310
+ String flagKey = "boolean_feature" ;
2311
+ String userId = "user123" ;
2312
+ String variationKey = "ho_off_key" ;
2313
+ String ruleKey = "basic_holdout" ;
2314
+
2315
+ Map <String , Object > attributes = new HashMap <>();
2316
+ attributes .put ("$opt_bucketing_id" , "ppid160000" ); // deterministic bucketing into basic_holdout
2317
+ attributes .put ("nationality" , "English" ); // non-reserved attribute
2318
+
2319
+ OptimizelyUserContext userContext = optimizelyClient .createUserContext (userId , attributes );
2320
+ OptimizelyDecision decision = userContext .decide (flagKey , Collections .singletonList (OptimizelyDecideOption .INCLUDE_REASONS ));
2321
+
2322
+ // Validate holdout decision
2323
+ assertEquals (flagKey , decision .getFlagKey ());
2324
+ assertEquals (variationKey , decision .getVariationKey ());
2325
+ assertEquals (ruleKey , decision .getRuleKey ());
2326
+ assertFalse (decision .getEnabled ());
2327
+ assertTrue (decision .getVariables ().toMap ().isEmpty ());
2328
+ assertTrue ("Expected holdout reason" , decision .getReasons ().stream ()
2329
+ .anyMatch (reason -> reason .contains ("holdout" )));
2330
+ }
2331
+
2332
+ @ Test
2333
+ public void testDecideForKeys_withHoldout () throws IOException {
2334
+ assumeTrue (datafileVersion == Integer .parseInt (ProjectConfig .Version .V4 .toString ()));
2335
+
2336
+ Context context = InstrumentationRegistry .getInstrumentation ().getTargetContext ();
2337
+ OptimizelyClient optimizelyClient = createOptimizelyClientWithHoldouts (context );
2338
+
2339
+ String userId = "user123" ;
2340
+ String variationKey = "ho_off_key" ;
2341
+ String ruleKey = "basic_holdout" ;
2342
+
2343
+ Map <String , Object > attributes = new HashMap <>();
2344
+ attributes .put ("$opt_bucketing_id" , "ppid160000" ); // deterministic bucketing into basic_holdout
2345
+
2346
+ List <String > flagKeys = Arrays .asList (
2347
+ "boolean_feature" ,
2348
+ "double_single_variable_feature" ,
2349
+ "integer_single_variable_feature"
2350
+ );
2351
+
2352
+ OptimizelyUserContext userContext = optimizelyClient .createUserContext (userId , attributes );
2353
+ Map <String , OptimizelyDecision > decisions = userContext .decideForKeys (flagKeys , Collections .singletonList (OptimizelyDecideOption .INCLUDE_REASONS ));
2354
+
2355
+ assertEquals (3 , decisions .size ());
2356
+
2357
+ for (String flagKey : flagKeys ) {
2358
+ OptimizelyDecision decision = decisions .get (flagKey );
2359
+ assertNotNull ("Missing decision for flag " + flagKey , decision );
2360
+ assertEquals (flagKey , decision .getFlagKey ());
2361
+ assertEquals (variationKey , decision .getVariationKey ());
2362
+ assertEquals (ruleKey , decision .getRuleKey ());
2363
+ assertFalse (decision .getEnabled ());
2364
+ assertTrue ("Expected holdout reason for flag " + flagKey , decision .getReasons ().stream ()
2365
+ .anyMatch (reason -> reason .contains ("holdout" )));
2366
+ }
2367
+ }
2368
+
2369
+ @ Test
2370
+ public void testDecideAll_withHoldout () throws IOException {
2371
+ assumeTrue (datafileVersion == Integer .parseInt (ProjectConfig .Version .V4 .toString ()));
2372
+
2373
+ Context context = InstrumentationRegistry .getInstrumentation ().getTargetContext ();
2374
+ OptimizelyClient optimizelyClient = createOptimizelyClientWithHoldouts (context );
2375
+
2376
+ String userId = "user123" ;
2377
+ String variationKey = "ho_off_key" ;
2378
+
2379
+ Map <String , Object > attributes = new HashMap <>();
2380
+ // ppid120000 buckets user into holdout_included_flags (selective holdout)
2381
+ attributes .put ("$opt_bucketing_id" , "ppid120000" );
2382
+
2383
+ // Flags INCLUDED in holdout_included_flags (only these should be holdout decisions)
2384
+ List <String > includedInHoldout = Arrays .asList (
2385
+ "boolean_feature" ,
2386
+ "double_single_variable_feature" ,
2387
+ "integer_single_variable_feature"
2388
+ );
2389
+
2390
+ OptimizelyUserContext userContext = optimizelyClient .createUserContext (userId , attributes );
2391
+ Map <String , OptimizelyDecision > decisions = userContext .decideAll (Arrays .asList (
2392
+ OptimizelyDecideOption .INCLUDE_REASONS ,
2393
+ OptimizelyDecideOption .DISABLE_DECISION_EVENT
2394
+ ));
2395
+
2396
+ assertTrue ("Should have multiple decisions" , decisions .size () > 0 );
2397
+
2398
+ String expectedReason = "User (" + userId + ") is in variation (" + variationKey + ") of holdout (holdout_included_flags)." ;
2399
+
2400
+ int holdoutCount = 0 ;
2401
+ for (Map .Entry <String , OptimizelyDecision > entry : decisions .entrySet ()) {
2402
+ String flagKey = entry .getKey ();
2403
+ OptimizelyDecision decision = entry .getValue ();
2404
+ assertNotNull ("Missing decision for flag " + flagKey , decision );
2405
+
2406
+ if (includedInHoldout .contains (flagKey )) {
2407
+ // Should be holdout decision
2408
+ assertEquals (variationKey , decision .getVariationKey ());
2409
+ assertFalse (decision .getEnabled ());
2410
+ assertTrue ("Expected holdout reason for flag " + flagKey , decision .getReasons ().contains (expectedReason ));
2411
+ holdoutCount ++;
2412
+ } else {
2413
+ // Should NOT be a holdout decision
2414
+ assertFalse ("Non-included flag should not have holdout reason: " + flagKey ,
2415
+ decision .getReasons ().contains (expectedReason ));
2416
+ }
2417
+ }
2418
+ assertEquals ("Expected exactly the included flags to be in holdout" , includedInHoldout .size (), holdoutCount );
2419
+ }
2420
+
2421
+ @ Test
2422
+ public void testDecisionNotificationHandler_withHoldout () throws IOException {
2423
+ assumeTrue (datafileVersion == Integer .parseInt (ProjectConfig .Version .V4 .toString ()));
2424
+
2425
+ Context context = InstrumentationRegistry .getInstrumentation ().getTargetContext ();
2426
+ OptimizelyClient optimizelyClient = createOptimizelyClientWithHoldouts (context );
2427
+
2428
+ String flagKey = "boolean_feature" ;
2429
+ String userId = "user123" ;
2430
+ String variationKey = "ho_off_key" ;
2431
+ String ruleKey = "basic_holdout" ;
2432
+
2433
+ Map <String , Object > attributes = new HashMap <>();
2434
+ attributes .put ("$opt_bucketing_id" , "ppid160000" ); // deterministic bucketing into basic_holdout
2435
+ attributes .put ("nationality" , "English" ); // non-reserved attribute
2436
+
2437
+ final boolean [] listenerCalled = {false };
2438
+ optimizelyClient .addDecisionNotificationHandler (decisionNotification -> {
2439
+ assertEquals ("FLAG" , decisionNotification .getType ());
2440
+ assertEquals (userId , decisionNotification .getUserId ());
2441
+ assertEquals (attributes , decisionNotification .getAttributes ());
2442
+
2443
+ Map <String , ?> info = decisionNotification .getDecisionInfo ();
2444
+ assertEquals (flagKey , info .get ("flagKey" ));
2445
+ assertEquals (variationKey , info .get ("variationKey" ));
2446
+ assertEquals (false , info .get ("enabled" ));
2447
+ assertEquals (ruleKey , info .get ("ruleKey" ));
2448
+ assertTrue (((Map <?, ?>) info .get ("variables" )).isEmpty ());
2449
+
2450
+ listenerCalled [0 ] = true ;
2451
+ });
2452
+
2453
+ OptimizelyUserContext userContext = optimizelyClient .createUserContext (userId , attributes );
2454
+ OptimizelyDecision decision = userContext .decide (flagKey , Collections .singletonList (OptimizelyDecideOption .INCLUDE_REASONS ));
2455
+
2456
+ assertTrue ("Decision notification handler should have been called" , listenerCalled [0 ]);
2457
+ assertEquals (variationKey , decision .getVariationKey ());
2458
+ assertFalse (decision .getEnabled ());
2459
+ }
2460
+
2294
2461
// Utils
2295
2462
2296
2463
private boolean compareJsonStrings (String str1 , String str2 ) {
0 commit comments