Skip to content

Commit 2fba187

Browse files
author
Michael Ng
authored
refac(decision): moved various components around. (#111)
1 parent 27ddd56 commit 2fba187

13 files changed

+489
-304
lines changed

optimizely/decision/composite_experiment_service.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,12 @@
1818
package decision
1919

2020
import (
21-
"github.com/optimizely/go-sdk/optimizely/decision/evaluator"
22-
"github.com/optimizely/go-sdk/optimizely/decision/reasons"
2321
"github.com/optimizely/go-sdk/optimizely/entities"
2422
)
2523

2624
// CompositeExperimentService bridges together the various experiment decision services that ship by default with the SDK
2725
type CompositeExperimentService struct {
28-
audienceTreeEvaluator evaluator.TreeEvaluator
29-
experimentServices []ExperimentService
26+
experimentServices []ExperimentService
3027
}
3128

3229
// NewCompositeExperimentService creates a new instance of the CompositeExperimentService
@@ -35,7 +32,6 @@ func NewCompositeExperimentService() *CompositeExperimentService {
3532
// 1. Bucketing
3633
// @TODO(mng): Prepend forced variation and whitelisting services
3734
return &CompositeExperimentService{
38-
audienceTreeEvaluator: evaluator.NewMixedTreeEvaluator(),
3935
experimentServices: []ExperimentService{
4036
NewExperimentBucketerService(),
4137
},
@@ -46,19 +42,8 @@ func NewCompositeExperimentService() *CompositeExperimentService {
4642
func (s CompositeExperimentService) GetDecision(decisionContext ExperimentDecisionContext, userContext entities.UserContext) (ExperimentDecision, error) {
4743

4844
experimentDecision := ExperimentDecision{}
49-
experiment := decisionContext.Experiment
5045

51-
// Determine if user can be part of the experiment
52-
if experiment.AudienceConditionTree != nil {
53-
condTreeParams := entities.NewTreeParameters(&userContext, decisionContext.ProjectConfig.GetAudienceMap())
54-
evalResult := s.audienceTreeEvaluator.Evaluate(experiment.AudienceConditionTree, condTreeParams)
55-
if !evalResult {
56-
experimentDecision.Reason = reasons.FailedAudienceTargeting
57-
return experimentDecision, nil
58-
}
59-
}
60-
61-
// User passed targeting (or the experiment is untargeted), so run through the various decision services
46+
// Run through the various decision services until we get a decision
6247
for _, experimentService := range s.experimentServices {
6348
if decision, err := experimentService.GetDecision(decisionContext, userContext); decision.Variation != nil {
6449
return decision, err

optimizely/decision/composite_experiment_service_test.go

Lines changed: 64 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,33 @@ package decision
1919
import (
2020
"testing"
2121

22-
"github.com/stretchr/testify/assert"
22+
"github.com/stretchr/testify/suite"
2323

2424
"github.com/optimizely/go-sdk/optimizely/entities"
2525
)
2626

27-
func TestCompositeExperimentServiceGetDecision(t *testing.T) {
28-
mockProjectConfig := new(mockProjectConfig)
29-
testDecisionContext := ExperimentDecisionContext{
27+
type CompositeExperimentTestSuite struct {
28+
suite.Suite
29+
mockConfig *mockProjectConfig
30+
mockExperimentService *MockExperimentDecisionService
31+
mockExperimentService2 *MockExperimentDecisionService
32+
testDecisionContext ExperimentDecisionContext
33+
}
34+
35+
func (s *CompositeExperimentTestSuite) SetupTest() {
36+
s.mockConfig = new(mockProjectConfig)
37+
s.mockExperimentService = new(MockExperimentDecisionService)
38+
s.mockExperimentService2 = new(MockExperimentDecisionService)
39+
40+
// Setup test data
41+
s.testDecisionContext = ExperimentDecisionContext{
3042
Experiment: &testExp1111,
31-
ProjectConfig: mockProjectConfig,
43+
ProjectConfig: s.mockConfig,
3244
}
45+
}
3346

47+
func (s *CompositeExperimentTestSuite) TestGetDecision() {
48+
// test that we return out of the decision making and the next one does not get called
3449
testUserContext := entities.UserContext{
3550
ID: "test_user_1",
3651
}
@@ -39,102 +54,67 @@ func TestCompositeExperimentServiceGetDecision(t *testing.T) {
3954
expectedExperimentDecision := ExperimentDecision{
4055
Variation: &expectedVariation,
4156
}
42-
// test that we return out of the decision making and the next one doesn't get called
43-
mockExperimentDecisionService := new(MockExperimentDecisionService)
44-
mockExperimentDecisionService.On("GetDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
57+
s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
4558

46-
mockExperimentDecisionService2 := new(MockExperimentDecisionService)
4759
compositeExperimentService := &CompositeExperimentService{
48-
experimentServices: []ExperimentService{mockExperimentDecisionService, mockExperimentDecisionService2},
60+
experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2},
61+
}
62+
decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext)
63+
s.Equal(expectedExperimentDecision, decision)
64+
s.NoError(err)
65+
s.mockExperimentService.AssertExpectations(s.T())
66+
s.mockExperimentService2.AssertNotCalled(s.T(), "GetDecision")
67+
68+
}
69+
70+
func (s *CompositeExperimentTestSuite) TestGetDecisionFallthrough() {
71+
// test that we move onto the next decision service if no decision is made
72+
testUserContext := entities.UserContext{
73+
ID: "test_user_1",
4974
}
50-
decision, err := compositeExperimentService.GetDecision(testDecisionContext, testUserContext)
51-
mockExperimentDecisionService.AssertExpectations(t)
52-
mockExperimentDecisionService2.AssertNotCalled(t, "GetDecision")
5375

54-
// test that we move on to the next decision service if no decision is made
55-
mockExperimentDecisionService = new(MockExperimentDecisionService)
56-
expectedExperimentDecision = ExperimentDecision{}
57-
mockExperimentDecisionService.On("GetDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
76+
expectedVariation := testExp1111.Variations["2222"]
77+
expectedExperimentDecision := ExperimentDecision{}
78+
s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
5879

59-
mockExperimentDecisionService2 = new(MockExperimentDecisionService)
6080
expectedExperimentDecision2 := ExperimentDecision{
6181
Variation: &expectedVariation,
6282
}
63-
mockExperimentDecisionService2.On("GetDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil)
83+
s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil)
6484

65-
compositeExperimentService = &CompositeExperimentService{
66-
experimentServices: []ExperimentService{mockExperimentDecisionService, mockExperimentDecisionService2},
85+
compositeExperimentService := &CompositeExperimentService{
86+
experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2},
6787
}
68-
decision, err = compositeExperimentService.GetDecision(testDecisionContext, testUserContext)
88+
decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext)
6989

70-
assert.NoError(t, err)
71-
assert.Equal(t, expectedExperimentDecision2, decision)
72-
mockExperimentDecisionService.AssertExpectations(t)
73-
mockExperimentDecisionService2.AssertExpectations(t)
90+
s.NoError(err)
91+
s.Equal(expectedExperimentDecision2, decision)
92+
s.mockExperimentService.AssertExpectations(s.T())
93+
s.mockExperimentService2.AssertExpectations(s.T())
94+
}
7495

96+
func (s *CompositeExperimentTestSuite) TestGetDecisionNoDecisionsMade() {
7597
// test when no decisions are made
76-
mockExperimentDecisionService = new(MockExperimentDecisionService)
77-
expectedExperimentDecision = ExperimentDecision{}
78-
mockExperimentDecisionService.On("GetDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
79-
80-
mockExperimentDecisionService2 = new(MockExperimentDecisionService)
81-
expectedExperimentDecision2 = ExperimentDecision{}
82-
mockExperimentDecisionService2.On("GetDecision", testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil)
83-
84-
compositeExperimentService = &CompositeExperimentService{
85-
experimentServices: []ExperimentService{mockExperimentDecisionService, mockExperimentDecisionService2},
98+
testUserContext := entities.UserContext{
99+
ID: "test_user_1",
86100
}
87-
decision, err = compositeExperimentService.GetDecision(testDecisionContext, testUserContext)
101+
expectedExperimentDecision := ExperimentDecision{}
102+
s.mockExperimentService.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
88103

89-
assert.NoError(t, err)
90-
assert.Equal(t, expectedExperimentDecision2, decision)
91-
mockExperimentDecisionService.AssertExpectations(t)
92-
mockExperimentDecisionService2.AssertExpectations(t)
93-
}
104+
expectedExperimentDecision2 := ExperimentDecision{}
105+
s.mockExperimentService2.On("GetDecision", s.testDecisionContext, testUserContext).Return(expectedExperimentDecision2, nil)
94106

95-
func TestCompositeExperimentServiceGetDecisionTargeting(t *testing.T) {
96-
testUserContext := entities.UserContext{
97-
ID: "test_user",
98-
}
99-
testAudienceMap := map[string]entities.Audience{
100-
"5555": testAudience5555,
107+
compositeExperimentService := &CompositeExperimentService{
108+
experimentServices: []ExperimentService{s.mockExperimentService, s.mockExperimentService2},
101109
}
102-
mockProjectConfig := new(mockProjectConfig)
103-
mockProjectConfig.On("GetAudienceMap").Return(testAudienceMap)
110+
decision, err := compositeExperimentService.GetDecision(s.testDecisionContext, testUserContext)
104111

105-
testExperimentDecisionContext := ExperimentDecisionContext{
106-
Experiment: &testExp1112,
107-
ProjectConfig: mockProjectConfig,
108-
}
109-
testCondTreeParams := entities.NewTreeParameters(&testUserContext, testAudienceMap)
110-
111-
// Test user fails targeting
112-
mockAudienceTreeEvaluator := new(MockAudienceTreeEvaluator)
113-
mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, testCondTreeParams).Return(false)
114-
mockExperimentDecisionService := new(MockExperimentDecisionService)
115-
testCompositeExperimentService := &CompositeExperimentService{
116-
audienceTreeEvaluator: mockAudienceTreeEvaluator,
117-
experimentServices: []ExperimentService{mockExperimentDecisionService},
118-
}
119-
decision, _ := testCompositeExperimentService.GetDecision(testExperimentDecisionContext, testUserContext)
120-
assert.Nil(t, decision.Variation)
121-
mockAudienceTreeEvaluator.AssertExpectations(t)
122-
mockExperimentDecisionService.AssertNotCalled(t, "GetDecision")
112+
s.NoError(err)
113+
s.Equal(expectedExperimentDecision2, decision)
114+
s.mockExperimentService.AssertExpectations(s.T())
115+
s.mockExperimentService2.AssertExpectations(s.T())
116+
}
123117

124-
// Test user passes targeting, moves on to children decision services
125-
expectedExperimentDecision := ExperimentDecision{
126-
Variation: &testExp1112Var2222,
127-
}
128-
mockAudienceTreeEvaluator = new(MockAudienceTreeEvaluator)
129-
mockAudienceTreeEvaluator.On("Evaluate", testExp1112.AudienceConditionTree, testCondTreeParams).Return(true)
130-
mockExperimentDecisionService = new(MockExperimentDecisionService)
131-
mockExperimentDecisionService.On("GetDecision", testExperimentDecisionContext, testUserContext).Return(expectedExperimentDecision, nil)
132-
testCompositeExperimentService = &CompositeExperimentService{
133-
audienceTreeEvaluator: mockAudienceTreeEvaluator,
134-
experimentServices: []ExperimentService{mockExperimentDecisionService},
135-
}
136-
decision, _ = testCompositeExperimentService.GetDecision(testExperimentDecisionContext, testUserContext)
137-
assert.Equal(t, decision, expectedExperimentDecision)
138-
mockAudienceTreeEvaluator.AssertExpectations(t)
139-
mockExperimentDecisionService.AssertExpectations(t)
118+
func TestCompositeExperimentTestSuite(t *testing.T) {
119+
suite.Run(t, new(CompositeExperimentTestSuite))
140120
}

optimizely/decision/composite_feature_service.go

Lines changed: 10 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,65 +28,27 @@ var cfLogger = logging.GetLogger("CompositeFeatureService")
2828

2929
// CompositeFeatureService is the default out-of-the-box feature decision service
3030
type CompositeFeatureService struct {
31-
featureExperimentService ExperimentService
32-
rolloutDecisionService FeatureService
31+
featureServices []FeatureService
3332
}
3433

3534
// NewCompositeFeatureService returns a new instance of the CompositeFeatureService
3635
func NewCompositeFeatureService() *CompositeFeatureService {
3736
return &CompositeFeatureService{
38-
featureExperimentService: NewFeatureExperimentService(),
39-
rolloutDecisionService: NewRolloutService(),
37+
featureServices: []FeatureService{
38+
NewFeatureExperimentService(),
39+
NewRolloutService(),
40+
},
4041
}
4142
}
4243

4344
// GetDecision returns a decision for the given feature and user context
4445
func (f CompositeFeatureService) GetDecision(decisionContext FeatureDecisionContext, userContext entities.UserContext) (FeatureDecision, error) {
45-
feature := decisionContext.Feature
46-
47-
// Check if user is bucketed in feature experiment
48-
if f.featureExperimentService != nil && len(feature.FeatureExperiments) > 0 {
49-
// @TODO this can be improved by getting group ID first and determining experiment and then bucketing in experiment
50-
for _, experiment := range feature.FeatureExperiments {
51-
featureExperiment := experiment
52-
experimentDecisionContext := ExperimentDecisionContext{
53-
Experiment: &featureExperiment,
54-
ProjectConfig: decisionContext.ProjectConfig,
55-
}
56-
57-
experimentDecision, err := f.featureExperimentService.GetDecision(experimentDecisionContext, userContext)
58-
// Variation not nil means we got a decision and should return it
59-
if experimentDecision.Variation != nil {
60-
featureDecision := FeatureDecision{
61-
Experiment: experiment,
62-
Decision: experimentDecision.Decision,
63-
Variation: experimentDecision.Variation,
64-
Source: FeatureTest,
65-
}
66-
67-
cfLogger.Debug(fmt.Sprintf(
68-
`Decision made for feature test with key "%s" for user "%s" with the following reason: "%s".`,
69-
feature.Key,
70-
userContext.ID,
71-
featureDecision.Reason,
72-
))
73-
return featureDecision, err
74-
}
46+
for _, featureDecisionService := range f.featureServices {
47+
featureDecision, err := featureDecisionService.GetDecision(decisionContext, userContext)
48+
if featureDecision.Variation != nil {
49+
return featureDecision, err
7550
}
7651
}
7752

78-
featureDecisionContext := FeatureDecisionContext{
79-
Feature: feature,
80-
ProjectConfig: decisionContext.ProjectConfig,
81-
}
82-
featureDecision, err := f.rolloutDecisionService.GetDecision(featureDecisionContext, userContext)
83-
featureDecision.Source = Rollout
84-
cfLogger.Debug(fmt.Sprintf(
85-
`Decision made for feature rollout with key "%s" for user "%s" with the following reason: "%s".`,
86-
feature.Key,
87-
userContext.ID,
88-
featureDecision.Reason,
89-
))
90-
91-
return featureDecision, err
53+
return FeatureDecision{}, fmt.Errorf("no decision was made for feature %s", decisionContext.Feature.Key)
9254
}

0 commit comments

Comments
 (0)