Skip to content

Commit 150380f

Browse files
[FSSDK-11544] Parsing holdout configuration from datafile + project config holdout impl. (#384)
1 parent d320001 commit 150380f

File tree

17 files changed

+1464
-220
lines changed

17 files changed

+1464
-220
lines changed

OptimizelySDK.Net35/OptimizelySDK.Net35.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@
8888
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
8989
<Link>Entity\Experiment.cs</Link>
9090
</Compile>
91+
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
92+
<Link>Entity\Holdout.cs</Link>
93+
</Compile>
94+
<Compile Include="..\OptimizelySDK\Entity\ExperimentCore.cs">
95+
<Link>Entity\ExperimentCore.cs</Link>
96+
</Compile>
9197
<Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs">
9298
<Link>Entity\FeatureDecision.cs</Link>
9399
</Compile>
@@ -215,6 +221,9 @@
215221
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
216222
<Link>Bucketing\ExperimentUtils</Link>
217223
</Compile>
224+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
225+
<Link>Utils\HoldoutConfig.cs</Link>
226+
</Compile>
218227
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
219228
<Link>Bucketing\UserProfileUtil</Link>
220229
</Compile>

OptimizelySDK.Net40/OptimizelySDK.Net40.csproj

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@
9090
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
9191
<Link>Entity\Experiment.cs</Link>
9292
</Compile>
93+
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
94+
<Link>Entity\Holdout.cs</Link>
95+
</Compile>
96+
<Compile Include="..\OptimizelySDK\Entity\ExperimentCore.cs">
97+
<Link>Entity\ExperimentCore.cs</Link>
98+
</Compile>
9399
<Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs">
94100
<Link>Entity\FeatureDecision.cs</Link>
95101
</Compile>
@@ -214,6 +220,9 @@
214220
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
215221
<Link>Bucketing\ExperimentUtils</Link>
216222
</Compile>
223+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
224+
<Link>Utils\HoldoutConfig.cs</Link>
225+
</Compile>
217226
<Compile Include="..\OptimizelySDK\Bucketing\UserProfileUtil.cs">
218227
<Link>Bucketing\UserProfileUtil</Link>
219228
</Compile>

OptimizelySDK.NetStandard16/OptimizelySDK.NetStandard16.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
<Compile Include="..\OptimizelySDK\Entity\Event.cs" />
2727
<Compile Include="..\OptimizelySDK\Entity\EventTags.cs" />
2828
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs" />
29+
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs" />
30+
<Compile Include="..\OptimizelySDK\Entity\ExperimentCore.cs" />
2931
<Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs" />
3032
<Compile Include="..\OptimizelySDK\Entity\ForcedVariation.cs" />
3133
<Compile Include="..\OptimizelySDK\Entity\Group.cs" />
@@ -64,6 +66,7 @@
6466
<Compile Include="..\OptimizelySDK\Utils\ControlAttributes.cs" />
6567
<Compile Include="..\OptimizelySDK\Utils\ExceptionExtensions.cs" />
6668
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs" />
69+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs" />
6770
<Compile Include="..\OptimizelySDK\Utils\ConditionParser.cs" />
6871
<Compile Include="..\OptimizelySDK\Utils\AttributeMatchTypes.cs" />
6972
<Compile Include="..\OptimizelySDK\Utils\DecisionInfoTypes.cs" />

OptimizelySDK.NetStandard20/OptimizelySDK.NetStandard20.csproj

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,12 @@
178178
<Compile Include="..\OptimizelySDK\Entity\Experiment.cs">
179179
<Link>Entity\Experiment.cs</Link>
180180
</Compile>
181-
<Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs">
181+
<Compile Include="..\OptimizelySDK\Entity\Holdout.cs">
182+
<Link>Entity\Holdout.cs</Link>
183+
</Compile>
184+
<Compile Include="..\OptimizelySDK\Entity\ExperimentCore.cs">
185+
<Link>Entity\ExperimentCore.cs</Link>
186+
</Compile> <Compile Include="..\OptimizelySDK\Entity\FeatureDecision.cs">
182187
<Link>Entity\FeatureDecision.cs</Link>
183188
</Compile>
184189
<Compile Include="..\OptimizelySDK\Entity\FeatureFlag.cs">
@@ -331,6 +336,9 @@
331336
<Compile Include="..\OptimizelySDK\Utils\ExperimentUtils.cs">
332337
<Link>Utils\ExperimentUtils.cs</Link>
333338
</Compile>
339+
<Compile Include="..\OptimizelySDK\Utils\HoldoutConfig.cs">
340+
<Link>Utils\HoldoutConfig.cs</Link>
341+
</Compile>
334342
<Compile Include="..\OptimizelySDK\Utils\Schema.cs">
335343
<Link>Utils\Schema.cs</Link>
336344
</Compile>
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
/*
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System;
18+
using System.IO;
19+
using Newtonsoft.Json;
20+
using Newtonsoft.Json.Linq;
21+
using NUnit.Framework;
22+
using OptimizelySDK.Entity;
23+
24+
namespace OptimizelySDK.Tests
25+
{
26+
[TestFixture]
27+
public class HoldoutTests
28+
{
29+
private JObject testData;
30+
31+
[SetUp]
32+
public void Setup()
33+
{
34+
// Load test data
35+
var testDataPath = Path.Combine(TestContext.CurrentContext.TestDirectory,
36+
"TestData", "HoldoutTestData.json");
37+
var jsonContent = File.ReadAllText(testDataPath);
38+
testData = JObject.Parse(jsonContent);
39+
}
40+
41+
[Test]
42+
public void TestHoldoutDeserialization()
43+
{
44+
// Test global holdout deserialization
45+
var globalHoldoutJson = testData["globalHoldout"].ToString();
46+
var globalHoldout = JsonConvert.DeserializeObject<Holdout>(globalHoldoutJson);
47+
48+
Assert.IsNotNull(globalHoldout);
49+
Assert.AreEqual("holdout_global_1", globalHoldout.Id);
50+
Assert.AreEqual("global_holdout", globalHoldout.Key);
51+
Assert.AreEqual("Running", globalHoldout.Status);
52+
Assert.IsNotNull(globalHoldout.Variations);
53+
Assert.AreEqual(1, globalHoldout.Variations.Length);
54+
Assert.IsNotNull(globalHoldout.TrafficAllocation);
55+
Assert.AreEqual(1, globalHoldout.TrafficAllocation.Length);
56+
Assert.IsNotNull(globalHoldout.IncludedFlags);
57+
Assert.AreEqual(0, globalHoldout.IncludedFlags.Length);
58+
Assert.IsNotNull(globalHoldout.ExcludedFlags);
59+
Assert.AreEqual(0, globalHoldout.ExcludedFlags.Length);
60+
}
61+
62+
[Test]
63+
public void TestHoldoutWithIncludedFlags()
64+
{
65+
var includedHoldoutJson = testData["includedFlagsHoldout"].ToString();
66+
var includedHoldout = JsonConvert.DeserializeObject<Holdout>(includedHoldoutJson);
67+
68+
Assert.IsNotNull(includedHoldout);
69+
Assert.AreEqual("holdout_included_1", includedHoldout.Id);
70+
Assert.AreEqual("included_holdout", includedHoldout.Key);
71+
Assert.IsNotNull(includedHoldout.IncludedFlags);
72+
Assert.AreEqual(2, includedHoldout.IncludedFlags.Length);
73+
Assert.Contains("flag_1", includedHoldout.IncludedFlags);
74+
Assert.Contains("flag_2", includedHoldout.IncludedFlags);
75+
Assert.IsNotNull(includedHoldout.ExcludedFlags);
76+
Assert.AreEqual(0, includedHoldout.ExcludedFlags.Length);
77+
}
78+
79+
[Test]
80+
public void TestHoldoutWithExcludedFlags()
81+
{
82+
var excludedHoldoutJson = testData["excludedFlagsHoldout"].ToString();
83+
var excludedHoldout = JsonConvert.DeserializeObject<Holdout>(excludedHoldoutJson);
84+
85+
Assert.IsNotNull(excludedHoldout);
86+
Assert.AreEqual("holdout_excluded_1", excludedHoldout.Id);
87+
Assert.AreEqual("excluded_holdout", excludedHoldout.Key);
88+
Assert.IsNotNull(excludedHoldout.IncludedFlags);
89+
Assert.AreEqual(0, excludedHoldout.IncludedFlags.Length);
90+
Assert.IsNotNull(excludedHoldout.ExcludedFlags);
91+
Assert.AreEqual(2, excludedHoldout.ExcludedFlags.Length);
92+
Assert.Contains("flag_3", excludedHoldout.ExcludedFlags);
93+
Assert.Contains("flag_4", excludedHoldout.ExcludedFlags);
94+
}
95+
96+
[Test]
97+
public void TestHoldoutWithEmptyFlags()
98+
{
99+
var globalHoldoutJson = testData["globalHoldout"].ToString();
100+
var globalHoldout = JsonConvert.DeserializeObject<Holdout>(globalHoldoutJson);
101+
102+
Assert.IsNotNull(globalHoldout);
103+
Assert.IsNotNull(globalHoldout.IncludedFlags);
104+
Assert.AreEqual(0, globalHoldout.IncludedFlags.Length);
105+
Assert.IsNotNull(globalHoldout.ExcludedFlags);
106+
Assert.AreEqual(0, globalHoldout.ExcludedFlags.Length);
107+
}
108+
109+
[Test]
110+
public void TestHoldoutEquality()
111+
{
112+
var holdoutJson = testData["globalHoldout"].ToString();
113+
var holdout1 = JsonConvert.DeserializeObject<Holdout>(holdoutJson);
114+
var holdout2 = JsonConvert.DeserializeObject<Holdout>(holdoutJson);
115+
116+
Assert.IsNotNull(holdout1);
117+
Assert.IsNotNull(holdout2);
118+
// Note: This test depends on how Holdout implements equality
119+
// If Holdout doesn't override Equals, this will test reference equality
120+
// You may need to implement custom equality logic for Holdout
121+
}
122+
123+
[Test]
124+
public void TestHoldoutStatusParsing()
125+
{
126+
var globalHoldoutJson = testData["globalHoldout"].ToString();
127+
var globalHoldout = JsonConvert.DeserializeObject<Holdout>(globalHoldoutJson);
128+
129+
Assert.IsNotNull(globalHoldout);
130+
Assert.AreEqual("Running", globalHoldout.Status);
131+
132+
// Test that the holdout is considered activated when status is "Running"
133+
// This assumes there's an IsActivated property or similar logic
134+
// Adjust based on actual Holdout implementation
135+
}
136+
137+
[Test]
138+
public void TestHoldoutVariationsDeserialization()
139+
{
140+
var holdoutJson = testData["includedFlagsHoldout"].ToString();
141+
var holdout = JsonConvert.DeserializeObject<Holdout>(holdoutJson);
142+
143+
Assert.IsNotNull(holdout);
144+
Assert.IsNotNull(holdout.Variations);
145+
Assert.AreEqual(1, holdout.Variations.Length);
146+
147+
var variation = holdout.Variations[0];
148+
Assert.AreEqual("var_2", variation.Id);
149+
Assert.AreEqual("treatment", variation.Key);
150+
Assert.AreEqual(true, variation.FeatureEnabled);
151+
}
152+
153+
[Test]
154+
public void TestHoldoutTrafficAllocationDeserialization()
155+
{
156+
var holdoutJson = testData["excludedFlagsHoldout"].ToString();
157+
var holdout = JsonConvert.DeserializeObject<Holdout>(holdoutJson);
158+
159+
Assert.IsNotNull(holdout);
160+
Assert.IsNotNull(holdout.TrafficAllocation);
161+
Assert.AreEqual(1, holdout.TrafficAllocation.Length);
162+
163+
var trafficAllocation = holdout.TrafficAllocation[0];
164+
Assert.AreEqual("var_3", trafficAllocation.EntityId);
165+
Assert.AreEqual(10000, trafficAllocation.EndOfRange);
166+
}
167+
168+
[Test]
169+
public void TestHoldoutNullSafety()
170+
{
171+
// Test that holdout can handle null/missing includedFlags and excludedFlags
172+
var minimalHoldoutJson = @"{
173+
""id"": ""test_holdout"",
174+
""key"": ""test_key"",
175+
""status"": ""Running"",
176+
""variations"": [],
177+
""trafficAllocation"": [],
178+
""audienceIds"": [],
179+
""audienceConditions"": []
180+
}";
181+
182+
var holdout = JsonConvert.DeserializeObject<Holdout>(minimalHoldoutJson);
183+
184+
Assert.IsNotNull(holdout);
185+
Assert.AreEqual("test_holdout", holdout.Id);
186+
Assert.AreEqual("test_key", holdout.Key);
187+
188+
// Verify that missing includedFlags and excludedFlags are handled properly
189+
// This depends on how the Holdout entity handles missing properties
190+
Assert.IsNotNull(holdout.IncludedFlags);
191+
Assert.IsNotNull(holdout.ExcludedFlags);
192+
}
193+
}
194+
}

OptimizelySDK.Tests/OptimizelySDK.Tests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,19 +119,24 @@
119119
<Compile Include="ValidEventDispatcher.cs"/>
120120
<Compile Include="ConfigTest\TestPollingProjectConfigManager.cs"/>
121121
<Compile Include="EntityTests\FeatureVariableTest.cs"/>
122+
<Compile Include="EntityTests\HoldoutTests.cs"/>
122123
<Compile Include="EventTests\EventEntitiesTest.cs"/>
123124
<Compile Include="EventTests\UserEventFactoryTest.cs"/>
124125
<Compile Include="EventTests\EventFactoryTest.cs"/>
125126
<Compile Include="EventTests\CanonicalEvent.cs"/>
126127
<Compile Include="OptimizelyFactoryTest.cs"/>
127128
<Compile Include="Utils\TestData.cs"/>
128129
<Compile Include="Utils\Reflection.cs"/>
130+
<Compile Include="UtilsTests\HoldoutConfigTests.cs"/>
129131
<Compile Include="ConfigTest\ProjectConfigProps.cs"/>
130132
<Compile Include="Utils\TestHttpProjectConfigManagerUtil.cs"/>
131133
</ItemGroup>
132134
<ItemGroup>
133135
<Content Include="App.config"/>
134136
<EmbeddedResource Include="OdpSegmentsDatafile.json"/>
137+
<Content Include="TestData\HoldoutTestData.json">
138+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
139+
</Content>
135140
</ItemGroup>
136141
<ItemGroup>
137142
<EmbeddedResource Include="IntegrationEmptyDatafile.json"/>

0 commit comments

Comments
 (0)