|
1801 | 1801 | end |
1802 | 1802 | end |
1803 | 1803 | end |
| 1804 | + |
| 1805 | + describe 'holdout experiments field and mapping' do |
| 1806 | + let(:config_with_experiment_holdouts) do |
| 1807 | + config_body_with_experiments = config_body.dup |
| 1808 | + config_body_with_experiments['holdouts'] = [ |
| 1809 | + { |
| 1810 | + 'id' => 'holdout_exp_1', |
| 1811 | + 'key' => 'local_holdout_1', |
| 1812 | + 'status' => 'Running', |
| 1813 | + 'audiences' => [], |
| 1814 | + 'includedFlags' => [], |
| 1815 | + 'excludedFlags' => [], |
| 1816 | + 'experiments' => ['exp_123', 'exp_456'], |
| 1817 | + 'variations' => [], |
| 1818 | + 'trafficAllocation' => [] |
| 1819 | + }, |
| 1820 | + { |
| 1821 | + 'id' => 'holdout_exp_2', |
| 1822 | + 'key' => 'local_holdout_2', |
| 1823 | + 'status' => 'Running', |
| 1824 | + 'audiences' => [], |
| 1825 | + 'includedFlags' => [], |
| 1826 | + 'excludedFlags' => [], |
| 1827 | + 'experiments' => ['exp_789'], |
| 1828 | + 'variations' => [], |
| 1829 | + 'trafficAllocation' => [] |
| 1830 | + }, |
| 1831 | + { |
| 1832 | + 'id' => 'holdout_global', |
| 1833 | + 'key' => 'global_holdout', |
| 1834 | + 'status' => 'Running', |
| 1835 | + 'audiences' => [], |
| 1836 | + 'includedFlags' => [], |
| 1837 | + 'excludedFlags' => [], |
| 1838 | + 'variations' => [], |
| 1839 | + 'trafficAllocation' => [] |
| 1840 | + } |
| 1841 | + ] |
| 1842 | + |
| 1843 | + Optimizely::DatafileProjectConfig.new( |
| 1844 | + JSON.dump(config_body_with_experiments), |
| 1845 | + logger, |
| 1846 | + error_handler |
| 1847 | + ) |
| 1848 | + end |
| 1849 | + |
| 1850 | + describe '#get_holdouts_for_experiment' do |
| 1851 | + it 'should return holdouts targeting a specific experiment' do |
| 1852 | + holdouts = config_with_experiment_holdouts.get_holdouts_for_experiment('exp_123') |
| 1853 | + expect(holdouts.length).to eq(1) |
| 1854 | + expect(holdouts.first['id']).to eq('holdout_exp_1') |
| 1855 | + expect(holdouts.first['key']).to eq('local_holdout_1') |
| 1856 | + end |
| 1857 | + |
| 1858 | + it 'should return multiple holdouts if experiment is targeted by multiple holdouts' do |
| 1859 | + # Add another holdout targeting exp_123 |
| 1860 | + config_body_multi = config_body.dup |
| 1861 | + config_body_multi['holdouts'] = [ |
| 1862 | + { |
| 1863 | + 'id' => 'holdout_1', |
| 1864 | + 'key' => 'local_holdout_1', |
| 1865 | + 'status' => 'Running', |
| 1866 | + 'audiences' => [], |
| 1867 | + 'includedFlags' => [], |
| 1868 | + 'excludedFlags' => [], |
| 1869 | + 'experiments' => ['exp_shared'], |
| 1870 | + 'variations' => [], |
| 1871 | + 'trafficAllocation' => [] |
| 1872 | + }, |
| 1873 | + { |
| 1874 | + 'id' => 'holdout_2', |
| 1875 | + 'key' => 'local_holdout_2', |
| 1876 | + 'status' => 'Running', |
| 1877 | + 'audiences' => [], |
| 1878 | + 'includedFlags' => [], |
| 1879 | + 'excludedFlags' => [], |
| 1880 | + 'experiments' => ['exp_shared'], |
| 1881 | + 'variations' => [], |
| 1882 | + 'trafficAllocation' => [] |
| 1883 | + } |
| 1884 | + ] |
| 1885 | + |
| 1886 | + config = Optimizely::DatafileProjectConfig.new( |
| 1887 | + JSON.dump(config_body_multi), |
| 1888 | + logger, |
| 1889 | + error_handler |
| 1890 | + ) |
| 1891 | + |
| 1892 | + holdouts = config.get_holdouts_for_experiment('exp_shared') |
| 1893 | + expect(holdouts.length).to eq(2) |
| 1894 | + expect(holdouts.map { |h| h['id'] }).to contain_exactly('holdout_1', 'holdout_2') |
| 1895 | + end |
| 1896 | + |
| 1897 | + it 'should return empty array for experiment not targeted by any holdout' do |
| 1898 | + holdouts = config_with_experiment_holdouts.get_holdouts_for_experiment('exp_not_exists') |
| 1899 | + expect(holdouts).to eq([]) |
| 1900 | + end |
| 1901 | + |
| 1902 | + it 'should not return global holdouts (without experiments field)' do |
| 1903 | + holdouts = config_with_experiment_holdouts.get_holdouts_for_experiment('exp_random') |
| 1904 | + expect(holdouts).to eq([]) |
| 1905 | + |
| 1906 | + # Verify global holdout exists but is not returned |
| 1907 | + global_holdout = config_with_experiment_holdouts.get_holdout('holdout_global') |
| 1908 | + expect(global_holdout).not_to be_nil |
| 1909 | + expect(global_holdout['experiments']).to eq([]) |
| 1910 | + end |
| 1911 | + |
| 1912 | + it 'should handle holdout targeting multiple experiments' do |
| 1913 | + holdouts_exp1 = config_with_experiment_holdouts.get_holdouts_for_experiment('exp_123') |
| 1914 | + holdouts_exp2 = config_with_experiment_holdouts.get_holdouts_for_experiment('exp_456') |
| 1915 | + |
| 1916 | + expect(holdouts_exp1.length).to eq(1) |
| 1917 | + expect(holdouts_exp2.length).to eq(1) |
| 1918 | + expect(holdouts_exp1.first['id']).to eq('holdout_exp_1') |
| 1919 | + expect(holdouts_exp2.first['id']).to eq('holdout_exp_1') |
| 1920 | + expect(holdouts_exp1.first['id']).to eq(holdouts_exp2.first['id']) |
| 1921 | + end |
| 1922 | + end |
| 1923 | + |
| 1924 | + describe '#local_holdout?' do |
| 1925 | + it 'should return true for holdouts with experiments array' do |
| 1926 | + holdout = config_with_experiment_holdouts.get_holdout('holdout_exp_1') |
| 1927 | + expect(config_with_experiment_holdouts.local_holdout?(holdout)).to be true |
| 1928 | + end |
| 1929 | + |
| 1930 | + it 'should return false for holdouts without experiments' do |
| 1931 | + holdout = config_with_experiment_holdouts.get_holdout('holdout_global') |
| 1932 | + expect(config_with_experiment_holdouts.local_holdout?(holdout)).to be false |
| 1933 | + end |
| 1934 | + |
| 1935 | + it 'should return false for holdouts with empty experiments array' do |
| 1936 | + config_body_empty = config_body.dup |
| 1937 | + config_body_empty['holdouts'] = [ |
| 1938 | + { |
| 1939 | + 'id' => 'holdout_empty', |
| 1940 | + 'key' => 'empty_holdout', |
| 1941 | + 'status' => 'Running', |
| 1942 | + 'audiences' => [], |
| 1943 | + 'includedFlags' => [], |
| 1944 | + 'excludedFlags' => [], |
| 1945 | + 'experiments' => [], |
| 1946 | + 'variations' => [], |
| 1947 | + 'trafficAllocation' => [] |
| 1948 | + } |
| 1949 | + ] |
| 1950 | + |
| 1951 | + config = Optimizely::DatafileProjectConfig.new( |
| 1952 | + JSON.dump(config_body_empty), |
| 1953 | + logger, |
| 1954 | + error_handler |
| 1955 | + ) |
| 1956 | + |
| 1957 | + holdout = config.get_holdout('holdout_empty') |
| 1958 | + expect(config.local_holdout?(holdout)).to be false |
| 1959 | + end |
| 1960 | + end |
| 1961 | + |
| 1962 | + describe 'experiments field parsing and defaults' do |
| 1963 | + it 'should parse experiments field from datafile' do |
| 1964 | + holdout = config_with_experiment_holdouts.get_holdout('holdout_exp_1') |
| 1965 | + expect(holdout['experiments']).to eq(['exp_123', 'exp_456']) |
| 1966 | + end |
| 1967 | + |
| 1968 | + it 'should default experiments to empty array if not provided' do |
| 1969 | + config_body_no_experiments = config_body.dup |
| 1970 | + config_body_no_experiments['holdouts'] = [ |
| 1971 | + { |
| 1972 | + 'id' => 'holdout_no_exp', |
| 1973 | + 'key' => 'holdout_without_experiments', |
| 1974 | + 'status' => 'Running', |
| 1975 | + 'audiences' => [], |
| 1976 | + 'includedFlags' => [], |
| 1977 | + 'excludedFlags' => [], |
| 1978 | + 'variations' => [], |
| 1979 | + 'trafficAllocation' => [] |
| 1980 | + } |
| 1981 | + ] |
| 1982 | + |
| 1983 | + config = Optimizely::DatafileProjectConfig.new( |
| 1984 | + JSON.dump(config_body_no_experiments), |
| 1985 | + logger, |
| 1986 | + error_handler |
| 1987 | + ) |
| 1988 | + |
| 1989 | + holdout = config.get_holdout('holdout_no_exp') |
| 1990 | + expect(holdout['experiments']).to eq([]) |
| 1991 | + end |
| 1992 | + |
| 1993 | + it 'should build experiment_holdouts_map correctly' do |
| 1994 | + expect(config_with_experiment_holdouts.experiment_holdouts_map).to be_a(Hash) |
| 1995 | + expect(config_with_experiment_holdouts.experiment_holdouts_map.keys).to include('exp_123', 'exp_456', 'exp_789') |
| 1996 | + expect(config_with_experiment_holdouts.experiment_holdouts_map['exp_123'].first['id']).to eq('holdout_exp_1') |
| 1997 | + end |
| 1998 | + |
| 1999 | + it 'should not include non-running holdouts in experiment mapping' do |
| 2000 | + config_body_inactive = config_body.dup |
| 2001 | + config_body_inactive['holdouts'] = [ |
| 2002 | + { |
| 2003 | + 'id' => 'holdout_inactive', |
| 2004 | + 'key' => 'inactive_holdout', |
| 2005 | + 'status' => 'Paused', |
| 2006 | + 'audiences' => [], |
| 2007 | + 'includedFlags' => [], |
| 2008 | + 'excludedFlags' => [], |
| 2009 | + 'experiments' => ['exp_inactive'], |
| 2010 | + 'variations' => [], |
| 2011 | + 'trafficAllocation' => [] |
| 2012 | + } |
| 2013 | + ] |
| 2014 | + |
| 2015 | + config = Optimizely::DatafileProjectConfig.new( |
| 2016 | + JSON.dump(config_body_inactive), |
| 2017 | + logger, |
| 2018 | + error_handler |
| 2019 | + ) |
| 2020 | + |
| 2021 | + holdouts = config.get_holdouts_for_experiment('exp_inactive') |
| 2022 | + expect(holdouts).to eq([]) |
| 2023 | + end |
| 2024 | + end |
| 2025 | + |
| 2026 | + describe 'backward compatibility' do |
| 2027 | + it 'should work with datafiles without experiments field' do |
| 2028 | + config_body_legacy = config_body.dup |
| 2029 | + config_body_legacy['holdouts'] = [ |
| 2030 | + { |
| 2031 | + 'id' => 'holdout_legacy', |
| 2032 | + 'key' => 'legacy_holdout', |
| 2033 | + 'status' => 'Running', |
| 2034 | + 'audiences' => [], |
| 2035 | + 'includedFlags' => [], |
| 2036 | + 'excludedFlags' => [], |
| 2037 | + 'variations' => [], |
| 2038 | + 'trafficAllocation' => [] |
| 2039 | + } |
| 2040 | + ] |
| 2041 | + |
| 2042 | + config = Optimizely::DatafileProjectConfig.new( |
| 2043 | + JSON.dump(config_body_legacy), |
| 2044 | + logger, |
| 2045 | + error_handler |
| 2046 | + ) |
| 2047 | + |
| 2048 | + holdout = config.get_holdout('holdout_legacy') |
| 2049 | + expect(holdout).not_to be_nil |
| 2050 | + expect(holdout['experiments']).to eq([]) |
| 2051 | + expect(config.local_holdout?(holdout)).to be false |
| 2052 | + end |
| 2053 | + |
| 2054 | + it 'should handle mix of holdouts with and without experiments field' do |
| 2055 | + config_body_mixed = config_body.dup |
| 2056 | + config_body_mixed['holdouts'] = [ |
| 2057 | + { |
| 2058 | + 'id' => 'holdout_with_exp', |
| 2059 | + 'key' => 'holdout_1', |
| 2060 | + 'status' => 'Running', |
| 2061 | + 'audiences' => [], |
| 2062 | + 'includedFlags' => [], |
| 2063 | + 'excludedFlags' => [], |
| 2064 | + 'experiments' => ['exp_1'], |
| 2065 | + 'variations' => [], |
| 2066 | + 'trafficAllocation' => [] |
| 2067 | + }, |
| 2068 | + { |
| 2069 | + 'id' => 'holdout_without_exp', |
| 2070 | + 'key' => 'holdout_2', |
| 2071 | + 'status' => 'Running', |
| 2072 | + 'audiences' => [], |
| 2073 | + 'includedFlags' => [], |
| 2074 | + 'excludedFlags' => [], |
| 2075 | + 'variations' => [], |
| 2076 | + 'trafficAllocation' => [] |
| 2077 | + } |
| 2078 | + ] |
| 2079 | + |
| 2080 | + config = Optimizely::DatafileProjectConfig.new( |
| 2081 | + JSON.dump(config_body_mixed), |
| 2082 | + logger, |
| 2083 | + error_handler |
| 2084 | + ) |
| 2085 | + |
| 2086 | + holdout1 = config.get_holdout('holdout_with_exp') |
| 2087 | + holdout2 = config.get_holdout('holdout_without_exp') |
| 2088 | + |
| 2089 | + expect(config.local_holdout?(holdout1)).to be true |
| 2090 | + expect(config.local_holdout?(holdout2)).to be false |
| 2091 | + |
| 2092 | + holdouts = config.get_holdouts_for_experiment('exp_1') |
| 2093 | + expect(holdouts.length).to eq(1) |
| 2094 | + expect(holdouts.first['id']).to eq('holdout_with_exp') |
| 2095 | + end |
| 2096 | + end |
| 2097 | + end |
1804 | 2098 | end |
0 commit comments