@@ -1375,3 +1375,162 @@ def test_get_variation_from_key_by_experiment_id_missing(self):
13751375 variation = project_config .get_variation_from_key_by_experiment_id (experiment_id , variation_key )
13761376
13771377 self .assertIsNone (variation )
1378+
1379+
1380+ class HoldoutConfigTest (base .BaseTest ):
1381+ def setUp (self ):
1382+ base .BaseTest .setUp (self )
1383+
1384+ # Create config with holdouts
1385+ config_body_with_holdouts = self .config_dict_with_features .copy ()
1386+
1387+ # Use correct feature flag IDs from the datafile
1388+ boolean_feature_id = '91111' # boolean_single_variable_feature
1389+ multi_variate_feature_id = '91114' # test_feature_in_experiment_and_rollout
1390+
1391+ config_body_with_holdouts ['holdouts' ] = [
1392+ {
1393+ 'id' : 'holdout_1' ,
1394+ 'key' : 'global_holdout' ,
1395+ 'status' : 'Running' ,
1396+ 'includedFlags' : [],
1397+ 'excludedFlags' : [boolean_feature_id ]
1398+ },
1399+ {
1400+ 'id' : 'holdout_2' ,
1401+ 'key' : 'specific_holdout' ,
1402+ 'status' : 'Running' ,
1403+ 'includedFlags' : [multi_variate_feature_id ],
1404+ 'excludedFlags' : []
1405+ },
1406+ {
1407+ 'id' : 'holdout_3' ,
1408+ 'key' : 'inactive_holdout' ,
1409+ 'status' : 'Inactive' ,
1410+ 'includedFlags' : [boolean_feature_id ],
1411+ 'excludedFlags' : []
1412+ }
1413+ ]
1414+
1415+ self .config_json_with_holdouts = json .dumps (config_body_with_holdouts )
1416+ opt_obj = optimizely .Optimizely (self .config_json_with_holdouts )
1417+ self .config_with_holdouts = opt_obj .config_manager .get_config ()
1418+
1419+ def test_get_holdouts_for_flag__non_existent_flag (self ):
1420+ """ Test that get_holdouts_for_flag returns empty array for non-existent flag. """
1421+
1422+ holdouts = self .config_with_holdouts .get_holdouts_for_flag ('non_existent_flag' )
1423+ self .assertEqual ([], holdouts )
1424+
1425+ def test_get_holdouts_for_flag__returns_global_and_specific_holdouts (self ):
1426+ """ Test that get_holdouts_for_flag returns global holdouts that do not exclude the flag
1427+ and specific holdouts that include the flag. """
1428+
1429+ holdouts = self .config_with_holdouts .get_holdouts_for_flag ('test_feature_in_experiment_and_rollout' )
1430+ self .assertEqual (2 , len (holdouts ))
1431+
1432+ global_holdout = next ((h for h in holdouts if h ['key' ] == 'global_holdout' ), None )
1433+ self .assertIsNotNone (global_holdout )
1434+ self .assertEqual ('holdout_1' , global_holdout ['id' ])
1435+
1436+ specific_holdout = next ((h for h in holdouts if h ['key' ] == 'specific_holdout' ), None )
1437+ self .assertIsNotNone (specific_holdout )
1438+ self .assertEqual ('holdout_2' , specific_holdout ['id' ])
1439+
1440+ def test_get_holdouts_for_flag__excludes_global_holdouts_for_excluded_flags (self ):
1441+ """ Test that get_holdouts_for_flag does not return global holdouts that exclude the flag. """
1442+
1443+ holdouts = self .config_with_holdouts .get_holdouts_for_flag ('boolean_single_variable_feature' )
1444+ self .assertEqual (0 , len (holdouts ))
1445+
1446+ global_holdout = next ((h for h in holdouts if h ['key' ] == 'global_holdout' ), None )
1447+ self .assertIsNone (global_holdout )
1448+
1449+ def test_get_holdouts_for_flag__caches_results (self ):
1450+ """ Test that get_holdouts_for_flag caches results for subsequent calls. """
1451+
1452+ holdouts1 = self .config_with_holdouts .get_holdouts_for_flag ('test_feature_in_experiment_and_rollout' )
1453+ holdouts2 = self .config_with_holdouts .get_holdouts_for_flag ('test_feature_in_experiment_and_rollout' )
1454+
1455+ # Should be the same object (cached)
1456+ self .assertIs (holdouts1 , holdouts2 )
1457+ self .assertEqual (2 , len (holdouts1 ))
1458+
1459+ def test_get_holdouts_for_flag__returns_only_global_for_non_targeted_flags (self ):
1460+ """ Test that get_holdouts_for_flag returns only global holdouts for flags not specifically targeted. """
1461+
1462+ holdouts = self .config_with_holdouts .get_holdouts_for_flag ('test_feature_in_rollout' )
1463+
1464+ # Should only include global holdout (not excluded and no specific targeting)
1465+ self .assertEqual (1 , len (holdouts ))
1466+ self .assertEqual ('global_holdout' , holdouts [0 ]['key' ])
1467+
1468+ def test_get_holdout__returns_holdout_for_valid_id (self ):
1469+ """ Test that get_holdout returns holdout when valid ID is provided. """
1470+
1471+ holdout = self .config_with_holdouts .get_holdout ('holdout_1' )
1472+ self .assertIsNotNone (holdout )
1473+ self .assertEqual ('holdout_1' , holdout ['id' ])
1474+ self .assertEqual ('global_holdout' , holdout ['key' ])
1475+ self .assertEqual ('Running' , holdout ['status' ])
1476+
1477+ def test_get_holdout__returns_holdout_regardless_of_status (self ):
1478+ """ Test that get_holdout returns holdout regardless of status when valid ID is provided. """
1479+
1480+ holdout = self .config_with_holdouts .get_holdout ('holdout_2' )
1481+ self .assertIsNotNone (holdout )
1482+ self .assertEqual ('holdout_2' , holdout ['id' ])
1483+ self .assertEqual ('specific_holdout' , holdout ['key' ])
1484+ self .assertEqual ('Running' , holdout ['status' ])
1485+
1486+ def test_get_holdout__returns_none_for_non_existent_id (self ):
1487+ """ Test that get_holdout returns None for non-existent holdout ID. """
1488+
1489+ holdout = self .config_with_holdouts .get_holdout ('non_existent_holdout' )
1490+ self .assertIsNone (holdout )
1491+
1492+ def test_get_holdout__logs_error_when_not_found (self ):
1493+ """ Test that get_holdout logs error when holdout is not found. """
1494+
1495+ with mock .patch .object (self .config_with_holdouts , 'logger' ) as mock_logger :
1496+ result = self .config_with_holdouts .get_holdout ('invalid_holdout_id' )
1497+
1498+ self .assertIsNone (result )
1499+ mock_logger .error .assert_called_once_with ('Holdout with ID "invalid_holdout_id" not found.' )
1500+
1501+ def test_get_holdout__does_not_log_when_found (self ):
1502+ """ Test that get_holdout does not log when holdout is found. """
1503+
1504+ with mock .patch .object (self .config_with_holdouts , 'logger' ) as mock_logger :
1505+ result = self .config_with_holdouts .get_holdout ('holdout_1' )
1506+
1507+ self .assertIsNotNone (result )
1508+ mock_logger .error .assert_not_called ()
1509+
1510+ def test_holdout_initialization__categorizes_holdouts_properly (self ):
1511+ """ Test that holdouts are properly categorized during initialization. """
1512+
1513+ self .assertIn ('holdout_1' , self .config_with_holdouts .holdout_id_map )
1514+ self .assertIn ('holdout_2' , self .config_with_holdouts .holdout_id_map )
1515+ self .assertIn ('holdout_1' , self .config_with_holdouts .global_holdouts )
1516+
1517+ # Use correct feature flag IDs
1518+ boolean_feature_id = '91111'
1519+ multi_variate_feature_id = '91114'
1520+
1521+ self .assertIn (multi_variate_feature_id , self .config_with_holdouts .included_holdouts )
1522+ self .assertTrue (len (self .config_with_holdouts .included_holdouts [multi_variate_feature_id ]) > 0 )
1523+ self .assertNotIn (boolean_feature_id , self .config_with_holdouts .included_holdouts )
1524+
1525+ self .assertIn (boolean_feature_id , self .config_with_holdouts .excluded_holdouts )
1526+ self .assertTrue (len (self .config_with_holdouts .excluded_holdouts [boolean_feature_id ]) > 0 )
1527+
1528+ def test_holdout_initialization__only_processes_running_holdouts (self ):
1529+ """ Test that only running holdouts are processed during initialization. """
1530+
1531+ self .assertNotIn ('holdout_3' , self .config_with_holdouts .holdout_id_map )
1532+ self .assertNotIn ('holdout_3' , self .config_with_holdouts .global_holdouts )
1533+
1534+ boolean_feature_id = '91111'
1535+ included_for_boolean = self .config_with_holdouts .included_holdouts .get (boolean_feature_id )
1536+ self .assertIsNone (included_for_boolean )
0 commit comments