Skip to content

Commit

Permalink
Fixed JSON tests for existing and sticky bucket
Browse files Browse the repository at this point in the history
  • Loading branch information
Norhaven committed Dec 16, 2024
1 parent 8018c77 commit 0abda2d
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using GrowthBook.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;

Expand All @@ -24,14 +27,47 @@ public class StickyBucketTestCase
[TestPropertyIndex(4)]
public JToken ExpectedResult { get; set; }
[TestPropertyIndex(5)]
public StickyAssignmentsDocument[] ExpectedAssignmentDocs { get; set; } = [];
public Dictionary<string, StickyAssignmentsDocument> ExpectedAssignmentDocs { get; set; } = [];
}

[Theory]
[MemberData(nameof(GetMappedTestsInCategory), typeof(StickyBucketTestCase))]
public void Run(StickyBucketTestCase testCase)
{
var service = new InMemoryStickyBucketService();

testCase.Context.StickyBucketService = service;
testCase.Context.StickyBucketAssignmentDocs = testCase.PreExistingAssignmentDocs.ToDictionary(x => x.FormattedAttribute);

// NOTE: Existing sticky bucket JSON tests in the JS SDK load this into the service up front
// but I wonder if that's correct because without that any assignment doc that exists
// other than those will not be stored and some of these test cases will fail.

foreach (var document in testCase.PreExistingAssignmentDocs)
{
service.SaveAssignments(document);
}

var gb = new GrowthBook(testCase.Context);
#warning Complete this.

var result = gb.EvalFeature(testCase.FeatureName);

var actualResult = JToken.Parse(JsonConvert.SerializeObject(result.ExperimentResult));

if (testCase.ExpectedResult is JObject obj)
{
foreach (var property in obj.Properties())
{
actualResult[property.Name].ToString().Should().Be(property.Value.ToString());
}
}
else
{
actualResult.ToString().Should().Be(testCase.ExpectedResult.ToString());
}

var storedDocuments = service.GetAllAssignments(testCase.ExpectedAssignmentDocs.Keys);

storedDocuments.Should().BeEquivalentTo(testCase.ExpectedAssignmentDocs, "because those should have been stored correctly");
}
}
34 changes: 19 additions & 15 deletions GrowthBook/GrowthBook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -486,7 +486,7 @@ private ExperimentResult RunExperiment(Experiment experiment, string featureId)

if (hasFallback)
{
(_, hashValue) = Attributes.GetHashAttributeAndValue(experiment.FallbackAttribute);
(hashAttribute, hashValue) = Attributes.GetHashAttributeAndValue(experiment.FallbackAttribute);
}
else
{
Expand Down Expand Up @@ -548,24 +548,24 @@ private ExperimentResult RunExperiment(Experiment experiment, string featureId)
_logger.LogDebug("Aborting experiment, associated conditions have prohibited participation");
return GetExperimentResult(experiment, featureId: featureId);
}
}

if (experiment.ParentConditions != null)
if (experiment.ParentConditions != null)
{
foreach (var parentCondition in experiment.ParentConditions)
{
foreach (var parentCondition in experiment.ParentConditions)
{
var parentResult = EvaluateFeature(featureId);
var parentResult = EvaluateFeature(parentCondition.Id);

if (parentResult.Source == FeatureResult.SourceId.CyclicPrerequisite)
{
return GetExperimentResult(experiment, featureId: featureId);
}
if (parentResult.Source == FeatureResult.SourceId.CyclicPrerequisite)
{
return GetExperimentResult(experiment, featureId: featureId);
}

var evaluationObject = new JObject { ["value"] = parentResult.Value };
var evaluationObject = new JObject { ["value"] = parentResult.Value };

if (!_conditionEvaluator.EvalCondition(evaluationObject, parentCondition.Condition ?? new JObject(), _savedGroups))
{
return GetExperimentResult(experiment, featureId: featureId);
}
if (!_conditionEvaluator.EvalCondition(evaluationObject, parentCondition.Condition ?? new JObject(), _savedGroups))
{
return GetExperimentResult(experiment, featureId: featureId);
}
}
}
Expand Down Expand Up @@ -753,7 +753,11 @@ private ExperimentResult GetExperimentResult(Experiment experiment, int variatio
HashUsed = hashUsed,
HashValue = hashValue,
Value = experiment.Variations is null ? null : experiment.Variations[variationIndex],
VariationId = variationIndex
VariationId = variationIndex,
Name = meta?.Name,
Passthrough = meta?.Passthrough ?? false,
Bucket = bucketHash ?? 0d,
StickyBucketUsed = wasStickyBucketUsed
};

result.Name = meta?.Name;
Expand Down
2 changes: 1 addition & 1 deletion GrowthBook/Services/IStickyBucketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ public interface IStickyBucketService
{
StickyAssignmentsDocument GetAssignments(string attributeName, string attributeValue);
void SaveAssignments(StickyAssignmentsDocument document);
IDictionary<string, StickyAssignmentsDocument> GetAllAssignments(IDictionary<string, string> attributes);
IDictionary<string, StickyAssignmentsDocument> GetAllAssignments(IEnumerable<string> attributes);
}
}
6 changes: 3 additions & 3 deletions GrowthBook/Services/InMemoryStickyBucketService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ public class InMemoryStickyBucketService : IStickyBucketService
{
private readonly IDictionary<string, StickyAssignmentsDocument> _cachedDocuments = new Dictionary<string, StickyAssignmentsDocument>();

public IDictionary<string, StickyAssignmentsDocument> GetAllAssignments(IDictionary<string, string> attributes)
public IDictionary<string, StickyAssignmentsDocument> GetAllAssignments(IEnumerable<string> attributes)
{
var assignments = from pair in attributes
let existingDoc = _cachedDocuments.TryGetValue(pair.Key, out var doc) ? doc : null
var assignments = from name in attributes
let existingDoc = _cachedDocuments.TryGetValue(name, out var doc) ? doc : null
where existingDoc != null
select (Attribute: existingDoc.FormattedAttribute, Document: existingDoc);

Expand Down
4 changes: 2 additions & 2 deletions GrowthBook/StickyAssignmentsDocument.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ public class StickyAssignmentsDocument

public string AttributeName { get; set; }
public string AttributeValue { get; set; }
public IDictionary<string, string> StickyAssignments { get; set; }
public IDictionary<string, string> Assignments { get; set; } = new Dictionary<string, string>();

public StickyAssignmentsDocument() { }

public StickyAssignmentsDocument(string attributeName, string attributeValue, IDictionary<string, string> stickyAssignments = default)
{
AttributeName = attributeName;
AttributeValue = attributeValue;
StickyAssignments = stickyAssignments ?? new Dictionary<string, string>();
Assignments = stickyAssignments ?? new Dictionary<string, string>();
}
}
}
13 changes: 6 additions & 7 deletions GrowthBook/Utilities/ExperimentUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,15 +253,14 @@ private static Regex GetUrlRegex(UrlPattern pattern)
public static (StickyAssignmentsDocument Document, bool IsChanged) GenerateStickyBucketAssignment(IStickyBucketService stickyBucketService, string attributeName, string attributeValue, IDictionary<string, string> assignments)
{
var existingDocument = stickyBucketService is null ? new StickyAssignmentsDocument(attributeName, attributeValue) : stickyBucketService.GetAssignments(attributeName, attributeValue);
var newAssignments = new Dictionary<string, string>(existingDocument.StickyAssignments);
var newAssignments = new Dictionary<string, string>(existingDocument?.Assignments ?? new Dictionary<string, string>());

newAssignments.MergeWith(new[] { assignments });

var isChanged = JsonConvert.SerializeObject(existingDocument) != JsonConvert.SerializeObject(newAssignments);
var document = new StickyAssignmentsDocument(attributeName, attributeValue, newAssignments);

existingDocument.StickyAssignments = newAssignments;

return (existingDocument, isChanged);
return (document, isChanged);
}

public static StickyBucketVariation GetStickyBucketVariation(Experiment experiment, int bucketVersion, int minBucketVersion, IList<VariationMeta> meta, JObject attributes, IDictionary<string, StickyAssignmentsDocument> document)
Expand Down Expand Up @@ -304,7 +303,7 @@ private static IDictionary<string, string> GetStickyBucketAssignments(JObject at
(var hashAttributeWithoutFallback, var hashValueWithoutFallback) = attributes.GetHashAttributeAndValue(hashAttribute, default);
var hashKey = new StickyAssignmentsDocument(hashAttributeWithoutFallback, hashValueWithoutFallback);

(var hashAttributeWithFallback, var hashValueWithFallback) = attributes.GetHashAttributeAndValue(default, fallbackAttribute);
(var hashAttributeWithFallback, var hashValueWithFallback) = attributes.GetHashAttributeAndValue(fallbackAttribute, default);
var fallbackKey = new StickyAssignmentsDocument(hashAttributeWithFallback, hashValueWithFallback);

var pendingAssignments = new List<IDictionary<string, string>>();
Expand All @@ -313,12 +312,12 @@ private static IDictionary<string, string> GetStickyBucketAssignments(JObject at

if (fallbackKey.HasValue && stickyAssignmentDocs.TryGetValue(fallbackKey.FormattedAttribute, out var fallbackDocument))
{
pendingAssignments.Add(fallbackDocument.StickyAssignments);
pendingAssignments.Add(fallbackDocument.Assignments);
}

if (stickyAssignmentDocs.TryGetValue(hashKey.FormattedAttribute, out var document))
{
pendingAssignments.Add(document.StickyAssignments);
pendingAssignments.Add(document.Assignments);
}

return mergedAssignments.MergeWith(pendingAssignments);
Expand Down

0 comments on commit 0abda2d

Please sign in to comment.