You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2022/11/09 19:04:25 UTC

[unomi] branch refactor-control-groups updated: UNOMI-690, UNOMI-696: Add new control group integration test and cover the feature with strong integration tests

This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch refactor-control-groups
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/refactor-control-groups by this push:
     new 2e0633389 UNOMI-690, UNOMI-696: Add new control group integration test and cover the feature with strong integration tests
2e0633389 is described below

commit 2e0633389aa469082243749e6a78c0bfca1b6390
Author: Kevan <ke...@jahia.com>
AuthorDate: Wed Nov 9 20:04:12 2022 +0100

    UNOMI-690, UNOMI-696: Add new control group integration test and cover the feature with strong integration tests
---
 .../org/apache/unomi/itests/ContextServletIT.java  | 235 +++++++++++++++++++--
 .../resources/personalization-control-group.json   |  49 +++++
 .../personalization-no-control-group.json          |  45 ++++
 .../PersonalizationServiceImpl.java                |   4 +-
 .../sorts/ControlGroupPersonalizationStrategy.java |   4 +-
 5 files changed, 323 insertions(+), 14 deletions(-)

diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
index 8c4d97eda..c27a926d7 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -17,7 +17,6 @@
 
 package org.apache.unomi.itests;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.http.client.methods.HttpPost;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
@@ -25,14 +24,7 @@ import org.apache.unomi.api.*;
 import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.segments.Scoring;
 import org.apache.unomi.api.segments.Segment;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.EventService;
-import org.apache.unomi.api.services.ProfileService;
-import org.apache.unomi.api.services.ScopeService;
-import org.apache.unomi.api.services.SegmentService;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
-import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.schema.api.SchemaService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -40,11 +32,8 @@ import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerSuite;
-import org.ops4j.pax.exam.util.Filter;
 
-import javax.inject.Inject;
 import java.io.File;
-import java.io.IOException;
 import java.net.URI;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -64,7 +53,6 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
 
 /**
  * Created by Ron Barabash on 5/4/2020.
@@ -80,7 +68,9 @@ public class ContextServletIT extends BaseIT {
     private final static String TEST_EVENT_TYPE_SCHEMA = "schemas/events/test-event-type.json";
     private final static String FLOAT_PROPERTY_EVENT_TYPE = "floatPropertyType";
     private final static String FLOAT_PROPERTY_EVENT_TYPE_SCHEMA = "schemas/events/float-property-type.json";
+    private final static String TEST_SESSION_ID = "dummy-session";
     private final static String TEST_PROFILE_ID = "test-profile-id";
+    private final static String TEST_PROFILE_FIRST_NAME = "contextServletIT_profile";
 
     private final static String SEGMENT_ID = "test-segment-id";
     private final static int SEGMENT_NUMBER_OF_DAYS = 30;
@@ -107,6 +97,7 @@ public class ContextServletIT extends BaseIT {
         segmentService.setSegmentDefinition(segment);
 
         profile = new Profile(TEST_PROFILE_ID);
+        profile.setProperty("firstName", TEST_PROFILE_FIRST_NAME);
         profileService.save(profile);
 
         keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required time", () -> profileService.load(TEST_PROFILE_ID),
@@ -132,6 +123,7 @@ public class ContextServletIT extends BaseIT {
         TestUtils.removeAllSessions(definitionsService, persistenceService);
         TestUtils.removeAllProfiles(definitionsService, persistenceService);
         profileService.delete(profile.getItemId(), false);
+        removeItems(Session.class);
         segmentService.removeSegmentDefinition(SEGMENT_ID, false);
 
         // cleanup schemas
@@ -664,4 +656,223 @@ public class ContextServletIT extends BaseIT {
 
         segmentService.removeScoringDefinition(scoring.getItemId(), false);
     }
+
+    @Test
+    public void test_no_ControlGroup() throws Exception {
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                null,
+                null);
+    }
+
+    @Test
+    public void test_in_ControlGroup_profile_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+    }
+
+    @Test
+    public void test_in_ControlGroup_session_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                null,
+                true);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                null,
+                true);
+    }
+
+    @Test
+    public void test_out_ControlGroup_profile_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                false,
+                null);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                false,
+                null);
+    }
+
+    @Test
+    public void test_out_ControlGroup_session_stored() throws Exception {
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                null,
+                false);
+
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                null,
+                false);
+    }
+
+    @Test
+    public void test_advanced_ControlGroup_test() throws Exception {
+        // STEP 1: start with no control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                null,
+                null);
+
+        // STEP 2: then enable control group stored in profile
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "100.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                true,
+                null);
+
+        // STEP 3: then re disable control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                /* We can see we still have old control group check stored in the profile */ true,
+                null);
+
+        // STEP 4: then re-enable control group, but session scoped this time, with a 0 percentage
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "0.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                /* We can see we still have old control group check stored in the profile */ true,
+                /* And now we also have a status saved in the session */ false);
+
+        // STEP 5: then re-enable control group, but profile scoped this time, with a 0 percentage
+        //         We should be in control group because of the STEP 2, the current profile already contains a persisted status for the perso.
+        //         So even if current config is 0, old check already flagged current profile to be in the control group.
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("false", "0.0"),
+                Arrays.asList("first-name-missing", "no-condition"),
+                true,
+                true,
+                /* We can see we still have old control group check stored in the profile */ true,
+                /*  We can see we still have old control group check stored in the session too */ false);
+
+        // STEP 6: then re-enable control group, but session scoped this time, with a 100 percentage
+        //         We should not be in control group because of the STEP 4, the current session already contains a persisted status for the perso.
+        //         So even if current config is 100, old check already flagged current profile to not be in the control group.
+        performPersonalizationWithControlGroup(
+                generateControlGroupConfig("true", "100.0"),
+                Collections.singletonList("no-condition"),
+                true,
+                false,
+                /* We can see we still have old control group check stored in the profile */ true,
+                /*  We can see we still have old control group check stored in the session too */ false);
+
+        // STEP 7: then re disable control group
+        performPersonalizationWithControlGroup(
+                null,
+                Collections.singletonList("no-condition"),
+                false,
+                false,
+                /* We can see we still have old control group check stored in the profile */ true,
+                /*  We can see we still have old control group check stored in the session too */ false);
+    }
+
+    private void performPersonalizationWithControlGroup(Map<String, String> controlGroupConfig, List<String> expectedVariants,
+                                                        boolean expectedControlGroupInfoInPersoResult, boolean expectedControlGroupValueInPersoResult,
+                                                        Boolean expectedControlGroupValueInProfile, Boolean expectedControlGroupValueInSession) throws Exception {
+        // Test normal personalization should not have control group info in response
+
+        HttpPost request = new HttpPost(getFullUrl(CONTEXT_URL));
+        if (controlGroupConfig != null) {
+            request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-control-group.json", controlGroupConfig), ContentType.APPLICATION_JSON));
+        } else {
+            request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-no-control-group.json", null), ContentType.APPLICATION_JSON));
+        }
+
+        TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request);
+        ContextResponse contextResponse = response.getContextResponse();
+
+        // Check variants
+        List<String> variants = contextResponse.getPersonalizations().get("perso-control-group");
+        assertEquals("Invalid response code", 200, response.getStatusCode());
+        assertEquals("Perso should contains the good number of variants", expectedVariants.size(), variants.size());
+        for (int i = 0; i < expectedVariants.size(); i++) {
+            assertEquals("Variant is not the expected one", expectedVariants.get(i), variants.get(i));
+        }
+        PersonalizationResult personalizationResult = contextResponse.getPersonalizationResults().get("perso-control-group");
+        variants = personalizationResult.getContentIds();
+        assertEquals("Perso should contains the good number of variants", expectedVariants.size(), variants.size());
+        for (int i = 0; i < expectedVariants.size(); i++) {
+            assertEquals("Variant is not the expected one", expectedVariants.get(i), variants.get(i));
+        }
+        // Check control group info
+        assertEquals("Perso result should contains control group info", expectedControlGroupInfoInPersoResult, personalizationResult.getAdditionalResultInfos().containsKey(PersonalizationResult.ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP));
+        assertEquals("Perso should not be in control group then", expectedControlGroupValueInPersoResult, contextResponse.getPersonalizationResults().get("perso-control-group").isInControlGroup());
+
+        // Check control group state on profile
+        keepTrying("Incorrect control group on profile",
+                () -> profileService.load(TEST_PROFILE_ID), storedProfile -> expectedControlGroupValueInProfile == getPersistedControlGroupStatus(storedProfile, "perso-control-group"),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        // Check control group state on session
+        keepTrying("Incorrect control group status on session",
+                () -> persistenceService.load(TEST_SESSION_ID, Session.class), storedSession -> expectedControlGroupValueInSession == getPersistedControlGroupStatus(storedSession, "perso-control-group"),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
+
+    private Boolean getPersistedControlGroupStatus(SystemPropertiesItem systemPropertiesItem, String personalizationId) {
+        if(systemPropertiesItem.getSystemProperties() != null && systemPropertiesItem.getSystemProperties().containsKey("personalizationStrategyStatus")) {
+            List<Map<String, Object>> personalizationStrategyStatus = (List<Map<String, Object>>) systemPropertiesItem.getSystemProperties().get("personalizationStrategyStatus");
+            for (Map<String, Object> strategyStatus : personalizationStrategyStatus) {
+                if (personalizationId.equals(strategyStatus.get("personalizationId"))) {
+                    return strategyStatus.containsKey("inControlGroup") && ((boolean) strategyStatus.get("inControlGroup"));
+                }
+            }
+        }
+        return null;
+    }
+
+    private Map<String, String> generateControlGroupConfig(String storeInSession, String percentage) {
+        Map<String, String> controlGroupConfig = new HashMap<>();
+        controlGroupConfig.put("storeInSession", storeInSession);
+        controlGroupConfig.put("percentage", percentage);
+        return controlGroupConfig;
+    }
 }
diff --git a/itests/src/test/resources/personalization-control-group.json b/itests/src/test/resources/personalization-control-group.json
new file mode 100644
index 000000000..e7b07eb2b
--- /dev/null
+++ b/itests/src/test/resources/personalization-control-group.json
@@ -0,0 +1,49 @@
+{
+  "source": {
+    "itemId": "CMSServer",
+    "itemType": "custom",
+    "scope": "acme",
+    "properties": {}
+  },
+  "requireSegments": true,
+  "requiredProfileProperties": [
+    "*"
+  ],
+  "requiredSessionProperties": [
+    "*"
+  ],
+  "personalizations": [
+    {
+      "id": "perso-control-group",
+      "strategy": "matching-first",
+      "strategyOptions": {
+        "controlGroup": {
+          "storeInSession": ###storeInSession###,
+          "percentage": ###percentage###
+        }
+      },
+      "contents": [
+        {
+          "id": "first-name-missing",
+          "filters": [
+            {
+              "condition": {
+                "type": "profilePropertyCondition",
+                "parameterValues": {
+                  "propertyName": "properties.firstName",
+                  "comparisonOperator": "missing"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "no-condition",
+          "filters": null
+        }
+      ]
+    }
+  ],
+  "sessionId": "dummy-session",
+  "profileId": "test-profile-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization-no-control-group.json b/itests/src/test/resources/personalization-no-control-group.json
new file mode 100644
index 000000000..790dbe71b
--- /dev/null
+++ b/itests/src/test/resources/personalization-no-control-group.json
@@ -0,0 +1,45 @@
+{
+  "source": {
+    "itemId": "CMSServer",
+    "itemType": "custom",
+    "scope": "acme",
+    "properties": {}
+  },
+  "requireSegments": true,
+  "requiredProfileProperties": [
+    "*"
+  ],
+  "requiredSessionProperties": [
+    "*"
+  ],
+  "personalizations": [
+    {
+      "id": "perso-control-group",
+      "strategy": "matching-first",
+      "strategyOptions": {
+      },
+      "contents": [
+        {
+          "id": "first-name-missing",
+          "filters": [
+            {
+              "condition": {
+                "type": "profilePropertyCondition",
+                "parameterValues": {
+                  "propertyName": "properties.firstName",
+                  "comparisonOperator": "missing"
+                }
+              }
+            }
+          ]
+        },
+        {
+          "id": "no-condition",
+          "filters": null
+        }
+      ]
+    }
+  ],
+  "sessionId": "dummy-session",
+  "profileId": "test-profile-id"
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
index 9fef8ba2c..494b8db66 100644
--- a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java
@@ -98,10 +98,12 @@ public class PersonalizationServiceImpl implements PersonalizationService {
 
             // Execute the original strategy
             PersonalizationResult originalStrategyResult = strategy.personalizeList(profile, session, personalizationRequest);
-            // even if control group is false, profile or session could have been modified
+            // merge original strategy result with previous controlGroup hook in case it's needed
             if (controlGroupStrategyResult != null) {
                 originalStrategyResult.addChanges(controlGroupStrategyResult.getChangeType());
+                originalStrategyResult.getAdditionalResultInfos().putAll(controlGroupStrategyResult.getAdditionalResultInfos());
             }
+
             return originalStrategyResult;
         }
 
diff --git a/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
index 3361d5182..3297b334c 100644
--- a/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
+++ b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java
@@ -47,7 +47,9 @@ public class ControlGroupPersonalizationStrategy implements PersonalizationStrat
         if (personalizationRequest.getStrategyOptions() != null && personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) {
             Map<String, Object> controlGroupMap = (Map<String, Object>) personalizationRequest.getStrategyOptions().get(CONTROL_GROUP_CONFIG);
 
-            return controlGroupMap.containsKey(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) ?
+            return controlGroupMap.containsKey(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) &&
+                    controlGroupMap.get(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) instanceof Boolean &&
+                    ((Boolean) controlGroupMap.get(CONTROL_GROUP_CONFIG_STORE_IN_SESSION)) ?
                     getPersonalizationResultForControlGroup(personalizationRequest, session, controlGroupMap, EventService.SESSION_UPDATED) :
                     getPersonalizationResultForControlGroup(personalizationRequest, profile, controlGroupMap, EventService.PROFILE_UPDATED);
         }