-
Notifications
You must be signed in to change notification settings - Fork 20
feat(ForcedDecisions): add forced-decisions APIs to OptimizelyUserContext #285
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
e9f3ad3
482628c
97acb39
c097e89
d23fcd0
3abb3e5
dd50814
973f24d
4747186
3b3070c
68d918e
30a808b
14a7288
8c289b6
6917f4d
9723d7a
86a099b
d5583d7
b7b8fb7
a46516e
b62c547
7df6dd0
d1e8e68
965c6bf
63e86b7
44229f0
99c1769
77626e0
5287e54
147773e
d418ea2
13ca784
aefdf34
58d40bf
484dc21
7d420c4
e89f4a0
ac5fd97
7570f48
a01050b
9efc42a
9da8ee5
4783824
8fa3198
5eb9922
0381512
874157e
f3ec5fb
a139aa9
ba19e30
9e4409d
044ad4a
a073b79
a401aa9
1106c44
692ee26
cbbe452
19eb3da
e72faee
97b52a9
7b76612
5288bb1
4323773
86f8ea5
8240f6e
82a8d93
87a1fc3
af39695
2500dd7
8b32b39
b209680
758f5d0
01a90ca
7ea219e
9e17cd8
03002e2
f4e348b
a061f72
4427c9f
0838c14
6793b0b
0211a2d
5ff593e
8ef2a93
85ca51d
5edcc18
a18f774
c9db119
c9acbdb
964bba5
d78a22b
3bd0991
b685bbd
87fe232
059fafd
80abfa3
9c87d71
f0f8ee8
9afc59a
e4c190c
07b69f2
9e427fa
1a3aab3
3bd0bb0
ba225f9
6f563bb
c7312ec
0db66fd
8bce727
69a2f51
10eaf22
f608a70
d5c621b
5511a91
375d748
5be1a05
48b15ef
4bca717
0b669eb
6478ed6
d359a0f
cdf53de
b6170cd
0d46ef7
de95a2c
0063e26
2fc7a47
85b00ff
0dccc30
a829f60
e43898f
0ac3dbe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,22 +18,34 @@ public class Assertions | |
{ | ||
#region Basic asserts | ||
|
||
public static void AreEquivalent(IEnumerable<string> expected, IEnumerable<string> actual) | ||
public static bool HasItems(IEnumerable<object> expected, IEnumerable<object> actual, bool allowNull = true) | ||
{ | ||
if (allowNull && expected == null && actual == null) | ||
return false; | ||
|
||
Assert.AreEqual(expected.Count(), actual.Count()); | ||
var zipped = expected.Zip(actual, (e, a) => | ||
|
||
return true; | ||
} | ||
|
||
public static void AreEquivalent(IEnumerable<string> expected, IEnumerable<string> actual) | ||
{ | ||
if (HasItems(expected, actual, false)) | ||
{ | ||
return new | ||
var zipped = expected.Zip(actual, (e, a) => | ||
{ | ||
Expected = e, | ||
Actual = a | ||
return new | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why we can't compare here instead of adding anonymous type object and then comparing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately the output of Zip is an IEnumerable, meaning we still need some sort of return. We could assert in here, but still need to return some enumerable. |
||
{ | ||
Expected = e, | ||
Actual = a | ||
}; | ||
}).ToList(); | ||
|
||
foreach (var z in zipped) | ||
{ | ||
Assert.AreEqual(z.Expected, z.Actual); | ||
}; | ||
}).ToList(); | ||
|
||
foreach (var z in zipped) | ||
{ | ||
Assert.AreEqual(z.Expected, z.Actual); | ||
}; | ||
} | ||
} | ||
|
||
public static void AreEquivalent(Dictionary<string, string> expected, Dictionary<string, string> actual) | ||
|
@@ -96,6 +108,7 @@ public static void AreEqual(FeatureDecision expected, FeatureDecision actual) | |
#region FeatureFlags | ||
public static void AreEquivalent(Dictionary<string, FeatureFlag> expected, Dictionary<string, FeatureFlag> actual) | ||
{ | ||
Assert.AreEqual(expected.Count, actual.Count); | ||
var zipped = expected.Zip(actual, (e, a) => | ||
{ | ||
return new | ||
|
@@ -139,6 +152,7 @@ public static void AreEqual(FeatureVariable expected, FeatureVariable actual) | |
|
||
public static void AreEquivalent(Dictionary<string, FeatureVariable> expected, Dictionary<string, FeatureVariable> actual) | ||
{ | ||
Assert.AreEqual(expected.Count, actual.Count); | ||
var zipped = expected.Zip(actual, (e, a) => | ||
{ | ||
return new | ||
|
@@ -244,22 +258,30 @@ public static void AreEquivalent(IEnumerable<Variation> expected, IEnumerable<Va | |
#region FeatureVariableUsage | ||
public static void AreEquivalent(IEnumerable<FeatureVariableUsage> expected, IEnumerable<FeatureVariableUsage> actual) | ||
{ | ||
Assert.AreEqual(expected.Count(), actual.Count()); | ||
expected.Zip(actual, (e, a) => | ||
if (HasItems(expected, actual)) | ||
{ | ||
return new | ||
expected.Zip(actual, (e, a) => | ||
{ | ||
Expected = e, | ||
Actual = a | ||
}; | ||
}).ToList().ForEach((item) => | ||
{ | ||
AreEqual(item.Expected, item.Actual); | ||
}); | ||
return new | ||
{ | ||
Expected = e, | ||
Actual = a | ||
}; | ||
}).ToList().ForEach((item) => | ||
{ | ||
AreEqual(item.Expected, item.Actual); | ||
}); | ||
} | ||
} | ||
|
||
public static void AreEquivalent(Dictionary<string, FeatureVariableUsage> expected, Dictionary<string, FeatureVariableUsage> actual) | ||
{ | ||
if (expected == null && actual == null) | ||
{ | ||
return; | ||
} | ||
|
||
Assert.AreEqual(expected.Count(), actual.Count()); | ||
expected.Zip(actual, (e, a) => | ||
{ | ||
return new | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -186,6 +186,78 @@ public void TestDecide() | |
Assert.AreEqual(decision.Reasons.Length, 0); | ||
} | ||
|
||
[Test] | ||
public void SetForcedDecisionSetsValue() | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var user = Optimizely.CreateUserContext(UserID); | ||
|
||
var result = user.SetForcedDecision("flag", "variation"); | ||
|
||
Assert.IsTrue(result); | ||
} | ||
|
||
[Test] | ||
public void SetForcedDecisionReturnsFalseAndLogErrorForNullConfig() | ||
{ | ||
var optly = new Optimizely(new FallbackProjectConfigManager(null)); | ||
|
||
|
||
var user = optly.CreateUserContext(UserID); | ||
var result = user.SetForcedDecision("flag", "variation"); | ||
|
||
Assert.IsFalse(result); | ||
//should assert logger is called | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
[Test] | ||
public void GetForcedDecisionsReturnsValueWithRuleKey() | ||
{ | ||
var user = Optimizely.CreateUserContext(UserID); | ||
|
||
user.SetForcedDecision("flag", "rule", "variation"); | ||
|
||
var result = user.GetForcedDecision("flag", "rule"); | ||
|
||
Assert.AreEqual("variation", result); | ||
} | ||
|
||
[Test] | ||
|
||
public void GetForcedDecisionReturnsValueWithoutRuleKey() | ||
{ | ||
var user = Optimizely.CreateUserContext(UserID); | ||
|
||
user.SetForcedDecision("flag", "variation"); | ||
|
||
var result = user.GetForcedDecision("flag", null); | ||
|
||
Assert.AreEqual("variation", result); | ||
} | ||
|
||
[Test] | ||
public void FindForcedDecisionReturnsValueWithoutRuleKey() | ||
{ | ||
var user = Optimizely.CreateUserContext(UserID); | ||
|
||
user.SetForcedDecision("flag", "variation"); | ||
|
||
var result = user.FindForcedDecision( 10000 "flag", null); | ||
|
||
Assert.AreEqual("variation", result); | ||
} | ||
|
||
[Test] | ||
public void FindForcedDecisionReturnsValueWithRuleKey() | ||
{ | ||
var user = Optimizely.CreateUserContext(UserID); | ||
|
||
user.SetForcedDecision("flag", "rule", "variation"); | ||
|
||
var result = user.FindForcedDecision("flag", "rule"); | ||
|
||
Assert.AreEqual("variation", result); | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we have tests for F-to-D and E-to-D conflicts, like set(flag1, var1) and set(flag1, rule1, var2)? |
||
[Test] | ||
public void DecideInvalidFlagKey() | ||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,26 @@ namespace OptimizelySDK | |
/// </summary> | ||
public class OptimizelyUserContext | ||
{ | ||
private class ForcedDecision | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private string flagKey; | ||
private string ruleKey; | ||
private string variationKey; | ||
|
||
public ForcedDecision(string flagKey, string ruleKey, string variationKey) | ||
{ | ||
this.flagKey = flagKey; | ||
this.ruleKey = ruleKey; | ||
this.variationKey = variationKey; | ||
} | ||
|
||
public string FlagKey { get { return flagKey; } set { this.flagKey = value; } } | ||
|
||
public string RuleKey { get { return ruleKey; } set { this.ruleKey = value; } } | ||
|
||
public string VariationKey { get { return variationKey; } set { this.variationKey = value; } } | ||
} | ||
|
||
private ILogger Logger; | ||
private IErrorHandler ErrorHandler; | ||
private object mutex = new object(); | ||
|
@@ -37,6 +57,9 @@ public class OptimizelyUserContext | |
// Optimizely object to be used. | ||
private Optimizely Optimizely; | ||
|
||
private Dictionary<string, Dictionary<string, ForcedDecision>> ForcedDecisionsMap = new Dictionary<string, Dictionary<string, ForcedDecision>>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When Copy() is called, this ForcedDicisionsMap should be also copied thread-safe to a new clone just like attributes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also consider lazy initialization only when setForcedDecision is called for the first time. It's because most users may not use this feature at all and it'll help to minimize cloning overhead. |
||
private Dictionary<string, ForcedDecision> ForcedDecisionsMapWithNoRuleKey = new Dictionary<string, ForcedDecision>(); | ||
|
||
public OptimizelyUserContext(Optimizely optimizely, string userId, UserAttributes userAttributes, IErrorHandler errorHandler, ILogger logger) | ||
{ | ||
ErrorHandler = errorHandler; | ||
|
@@ -189,5 +212,99 @@ public virtual void TrackEvent(string eventName, | |
{ | ||
Optimizely.Track(eventName, UserId, Attributes, eventTags); | ||
} | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="flagKey"></param> | ||
/// <param name="variationKey"></param> | ||
/// <returns></returns> | ||
|
||
public bool SetForcedDecision(string flagKey, string variationKey) | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return SetForcedDecision(flagKey, nul 179B l, variationKey); | ||
} | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="flagKey"></param> | ||
/// <param name="ruleKey"></param> | ||
/// <param name="variationKey"></param> | ||
/// <returns></returns> | ||
public bool SetForcedDecision(string flagKey, string ruleKey, string variationKey) | ||
{ | ||
if (Optimizely.GetOptimizelyConfig() == null) | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Logger.Log(LogLevel.ERROR, "Optimizely config is null"); | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return false; | ||
} | ||
|
||
if(ruleKey == null) | ||
{ | ||
if (ForcedDecisionsMapWithNoRuleKey.TryGetValue(flagKey, out var value)) | ||
{ | ||
value.VariationKey = variationKey; | ||
} | ||
else | ||
{ | ||
ForcedDecisionsMapWithNoRuleKey.Add(flagKey, new ForcedDecision(flagKey, null, variationKey)); | ||
} | ||
} | ||
else | ||
{ | ||
Dictionary<string, ForcedDecision> forcedDecision = new Dictionary<string, ForcedDecision>(); | ||
forcedDecision.Add(ruleKey, new ForcedDecision(flagKey, ruleKey, variationKey)); | ||
ForcedDecisionsMap.Add(flagKey, forcedDecision); | ||
} | ||
return true; | ||
} | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="String"></param> | ||
/// <param name=""></param> | ||
/// <param name="ruleKey"></param> | ||
/// <returns></returns> | ||
public string GetForcedDecision(string flagKey, string ruleKey) | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (Optimizely.GetOptimizelyConfig() == null) | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
Logger.Log(LogLevel.ERROR, "Optimizely SDK not ready."); | ||
return null; | ||
} | ||
|
||
return FindForcedDecision(flagKey, ruleKey); | ||
} | ||
|
||
/// <summary> | ||
/// | ||
/// </summary> | ||
/// <param name="flagKey"></param> | ||
/// <param name="ruleKey"></param> | ||
/// <returns></returns> | ||
public string FindForcedDecision(string flagKey, string ruleKey) | ||
{ | ||
string variationKey = null; | ||
if (ruleKey != null) | ||
dustin-sier marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
if (ForcedDecisionsMap.Count > 0 && ForcedDecisionsMap.ContainsKey(flagKey)) | ||
{ | ||
if (ForcedDecisionsMap.TryGetValue(flagKey, out var forcedDecision) && forcedDecision.TryGetValue(ruleKey, out var forcedDecision2)) | ||
{ | ||
variationKey = forcedDecision2.VariationKey; | ||
} | ||
} | ||
} | ||
else | ||
{ | ||
if (ForcedDecisionsMapWithNoRuleKey.Count > 0 && ForcedDecisionsMapWithNoRuleKey.TryGetValue(flagKey, out var forcedDecision)) | ||
{ | ||
variationKey = forcedDecision.VariationKey; | ||
} | ||
4366 | } | |
return variationKey; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will give exception before assertion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure, but HasItems is asserting objects, doesn't seem OK to me.