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);
}