diff --git a/CHANGES.txt b/CHANGES.txt index 972a8855..f3338d24 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -2,6 +2,16 @@ - BREAKING CHANGE: Removed support from versions older than PHP 7.4. - BREAKING CHANGE: Added support for Multiple Factory Instantiation. +7.2.0 (Jan 24, 2024) + - Added support for Flag Sets on the SDK, which enables grouping feature flags and interacting with the group rather than individually (more details in our documentation): + - Added new variations of the get treatment methods to support evaluating flags in given flag set/s. + - getTreatmentsByFlagSet and getTreatmentsByFlagSets + - getTreatmentWithConfigByFlagSets and getTreatmentsWithConfigByFlagSets + - Added `defaultTreatment` and `sets` properties to the `SplitView` object returned by the `split` and `splits` methods of the SDK manager. + +7.1.8 (Jul 24, 2023) + - Fixed input validation for empty keys. + 7.1.7 (May 16, 2023) - Updated terminology on the SDKs codebase to be more aligned with current standard without causing a breaking change. The core change is the term split for feature flag on things like logs and phpdoc comments. - Fixed php 8.2 warnings in code. diff --git a/src/SplitIO/Component/Cache/Pool.php b/src/SplitIO/Component/Cache/Pool.php index 88e22744..43bdf504 100644 --- a/src/SplitIO/Component/Cache/Pool.php +++ b/src/SplitIO/Component/Cache/Pool.php @@ -83,4 +83,9 @@ public function expireKey($key, $ttl) { return $this->adapter->expireKey($key, $ttl); } + + public function sMembers($key) + { + return $this->adapter->sMembers($key); + } } diff --git a/src/SplitIO/Component/Cache/SplitCache.php b/src/SplitIO/Component/Cache/SplitCache.php index 138a5171..6eae5403 100644 --- a/src/SplitIO/Component/Cache/SplitCache.php +++ b/src/SplitIO/Component/Cache/SplitCache.php @@ -9,6 +9,8 @@ class SplitCache implements SplitCacheInterface const KEY_TRAFFIC_TYPE_CACHED = 'SPLITIO.trafficType.{trafficTypeName}'; + const KEY_FLAG_SET_CACHED = 'SPLITIO.flagSet.{set}'; + /** * @var \SplitIO\Component\Cache\Pool */ @@ -37,6 +39,11 @@ private static function getCacheKeyForSplit($splitName) return str_replace('{splitName}', $splitName, self::KEY_SPLIT_CACHED_ITEM); } + private static function getCacheKeyForFlagSet($flagSet) + { + return str_replace('{set}', $flagSet, self::KEY_FLAG_SET_CACHED); + } + private static function getSplitNameFromCacheKey($key) { $cacheKeyPrefix = self::getCacheKeyForSplit(''); @@ -87,6 +94,22 @@ public function getSplitNames() return array_map([self::class, 'getSplitNameFromCacheKey'], $splitKeys); } + /** + * @param array(string) List of flag set names + * @return array(string) List of all feature flag names by flag sets + */ + public function getNamesByFlagSets($flagSets) + { + $toReturn = array(); + if (empty($flagSets)) { + return $toReturn; + } + foreach ($flagSets as $flagSet) { + $toReturn[$flagSet] = $this->cache->sMembers(self::getCacheKeyForFlagSet($flagSet)); + } + return $toReturn; + } + /** * @return array(string) List of all split JSON strings */ diff --git a/src/SplitIO/Component/Cache/SplitCacheInterface.php b/src/SplitIO/Component/Cache/SplitCacheInterface.php index a667e48c..9ec14ffc 100644 --- a/src/SplitIO/Component/Cache/SplitCacheInterface.php +++ b/src/SplitIO/Component/Cache/SplitCacheInterface.php @@ -13,4 +13,10 @@ public function getChangeNumber(); * @return string JSON representation */ public function getSplit($splitName); + + /** + * @param array(string) List of flag set names + * @return array(string) List of all feature flag names by flag sets + */ + public function getNamesByFlagSets($flagSets); } diff --git a/src/SplitIO/Component/Cache/Storage/Adapter/CacheStorageAdapterInterface.php b/src/SplitIO/Component/Cache/Storage/Adapter/CacheStorageAdapterInterface.php index fbbfb44d..5d12ea70 100644 --- a/src/SplitIO/Component/Cache/Storage/Adapter/CacheStorageAdapterInterface.php +++ b/src/SplitIO/Component/Cache/Storage/Adapter/CacheStorageAdapterInterface.php @@ -37,4 +37,10 @@ public function rightPushQueue($queueName, $item); * @return boolean */ public function expireKey($key, $ttl); + + /** + * @param string $key + * @return mixed + */ + public function sMembers($key); } diff --git a/src/SplitIO/Component/Cache/Storage/Adapter/PRedis.php b/src/SplitIO/Component/Cache/Storage/Adapter/PRedis.php index e5a4a680..1c6b8dca 100644 --- a/src/SplitIO/Component/Cache/Storage/Adapter/PRedis.php +++ b/src/SplitIO/Component/Cache/Storage/Adapter/PRedis.php @@ -184,6 +184,15 @@ public function isOnList($key, $value) return $this->client->sIsMember($key, $value); } + /** + * @param string $key + * @return mixed + */ + public function sMembers($key) + { + return $this->client->smembers($key); + } + public function getKeys($pattern = '*') { $keys = $this->client->keys($pattern); diff --git a/src/SplitIO/Component/Cache/Storage/Adapter/SafeRedisWrapper.php b/src/SplitIO/Component/Cache/Storage/Adapter/SafeRedisWrapper.php index 5f26405e..a1dc5e96 100644 --- a/src/SplitIO/Component/Cache/Storage/Adapter/SafeRedisWrapper.php +++ b/src/SplitIO/Component/Cache/Storage/Adapter/SafeRedisWrapper.php @@ -123,4 +123,20 @@ public function expireKey($key, $ttl) return false; } } + + /** + * @param string $key + * @return mixed + */ + public function sMembers($key) + { + try { + return $this->cacheAdapter->sMembers($key); + } catch (\Exception $e) { + Context::getLogger()->critical("An error occurred performing SMEMBERS for " . $key); + Context::getLogger()->critical($e->getMessage()); + Context::getLogger()->critical($e->getTraceAsString()); + return array(); + } + } } diff --git a/src/SplitIO/Grammar/Split.php b/src/SplitIO/Grammar/Split.php index 78dfc6ba..56354122 100644 --- a/src/SplitIO/Grammar/Split.php +++ b/src/SplitIO/Grammar/Split.php @@ -31,6 +31,7 @@ class Split private $trafficAllocationSeed = null; private $configurations = null; + private $sets = null; public function __construct(array $split) { @@ -50,6 +51,7 @@ public function __construct(array $split) $split['trafficAllocationSeed'] : null; $this->configurations = isset($split['configurations']) && count($split['configurations']) > 0 ? $split['configurations'] : null; + $this->sets = isset($split['sets']) ? $split['sets'] : array(); SplitApp::logger()->info("Constructing Feature Flag: ".$this->name); @@ -167,4 +169,12 @@ public function getConfigurations() { return $this->configurations; } + + /** + * @return array|null + */ + public function getSets() + { + return $this->sets; + } } diff --git a/src/SplitIO/Metrics.php b/src/SplitIO/Metrics.php index bbbd3b82..099496f6 100644 --- a/src/SplitIO/Metrics.php +++ b/src/SplitIO/Metrics.php @@ -5,11 +5,6 @@ class Metrics { - const MNAME_SDK_GET_TREATMENT = 'sdk.getTreatment'; - const MNAME_SDK_GET_TREATMENT_WITH_CONFIG = 'sdk.getTreatmentWithConfig'; - const MNAME_SDK_GET_TREATMENTS = 'sdk.getTreatments'; - const MNAME_SDK_GET_TREATMENTS_WITH_CONFIG = 'sdk.getTreatmentsWithConfig'; - public static function startMeasuringLatency() { return Latency::startMeasuringLatency(); diff --git a/src/SplitIO/Sdk/Client.php b/src/SplitIO/Sdk/Client.php index bb65c354..9e2ac24c 100644 --- a/src/SplitIO/Sdk/Client.php +++ b/src/SplitIO/Sdk/Client.php @@ -1,7 +1,6 @@ TreatmentEnum::CONTROL, 'config' => null); @@ -156,7 +155,7 @@ private function doEvaluation($operation, $metricName, $key, $featureFlagName, $ $bucketingKey ); - $this->registerData($impression, $attributes, $metricName, $result['latency']); + $this->registerData($impression, $attributes); return array( 'treatment' => $result['treatment'], 'config' => $result['config'], @@ -177,7 +176,7 @@ private function doEvaluation($operation, $metricName, $key, $featureFlagName, $ ImpressionLabel::EXCEPTION, $bucketingKey ); - $this->registerData($impression, $attributes, $metricName); + $this->registerData($impression, $attributes); } catch (\Exception $e) { SplitApp::logger()->critical( "An error occurred when attempting to log impression for " . @@ -196,7 +195,6 @@ public function getTreatment($key, $featureName, array $attributes = null) try { $result = $this->doEvaluation( 'getTreatment', - Metrics::MNAME_SDK_GET_TREATMENT, $key, $featureName, $attributes @@ -216,7 +214,6 @@ public function getTreatmentWithConfig($key, $featureFlagName, array $attributes try { return $this->doEvaluation( 'getTreatmentWithConfig', - Metrics::MNAME_SDK_GET_TREATMENT_WITH_CONFIG, $key, $featureFlagName, $attributes @@ -261,7 +258,7 @@ private function doInputValidationForTreatments($key, $featureFlagNames, array $ ); } - private function registerData($impressions, $attributes, $metricName, $latency = null) + private function registerData($impressions, $attributes) { try { if (is_null($impressions) || (is_array($impressions) && 0 == count($impressions))) { @@ -291,7 +288,7 @@ private function registerData($impressions, $attributes, $metricName, $latency = * * @return mixed */ - private function doEvaluationForTreatments($operation, $metricName, $key, $featureFlagNames, $attributes) + private function doEvaluationForTreatments($operation, $key, $featureFlagNames, $attributes) { $inputValidation = $this->doInputValidationForTreatments($key, $featureFlagNames, $attributes, $operation); if (is_null($inputValidation)) { @@ -306,35 +303,19 @@ private function doEvaluationForTreatments($operation, $metricName, $key, $featu $featureFlags = $inputValidation['featureFlagNames']; try { - $result = array(); - $impressions = array(); $evaluationResults = $this->evaluator->evaluateFeatures( $matchingKey, $bucketingKey, $featureFlags, $attributes ); - foreach ($evaluationResults['evaluations'] as $featureFlag => $evalResult) { - if (InputValidator::isSplitFound($evalResult['impression']['label'], $featureFlag, $operation)) { - // Creates impression - $impressions[] = $this->createImpression( - $matchingKey, - $featureFlag, - $evalResult['treatment'], - $evalResult['impression']['changeNumber'], - $evalResult['impression']['label'], - $bucketingKey - ); - $result[$featureFlag] = array( - 'treatment' => $evalResult['treatment'], - 'config' => $evalResult['config'], - ); - } else { - $result[$featureFlag] = array('treatment' => TreatmentEnum::CONTROL, 'config' => null); - } - } - $this->registerData($impressions, $attributes, $metricName, $evaluationResults['latency']); - return $result; + return $this->processEvaluations( + $matchingKey, + $bucketingKey, + $operation, + $attributes, + $evaluationResults['evaluations'] + ); } catch (\Exception $e) { SplitApp::logger()->critical($operation . ' method is throwing exceptions'); SplitApp::logger()->critical($e->getMessage()); @@ -355,7 +336,6 @@ function ($feature) { }, $this->doEvaluationForTreatments( 'getTreatments', - Metrics::MNAME_SDK_GET_TREATMENTS, $key, $featureFlagNames, $attributes @@ -376,7 +356,6 @@ public function getTreatmentsWithConfig($key, $featureFlagNames, array $attribut try { return $this->doEvaluationForTreatments( 'getTreatmentsWithConfig', - Metrics::MNAME_SDK_GET_TREATMENTS_WITH_CONFIG, $key, $featureFlagNames, $attributes @@ -442,4 +421,158 @@ public function track($key, $trafficType, $eventType, $value = null, $properties return false; } + + public function getTreatmentsByFlagSets($key, $flagSets, array $attributes = null) + { + try { + return array_map( + function ($feature) { + return $feature['treatment']; + }, + $this->doEvaluationByFlagSets( + 'getTreatmentsByFlagSets', + $key, + $flagSets, + $attributes + ) + ); + } catch (\Exception $e) { + SplitApp::logger()->critical('getTreatmentsByFlagSets method is throwing exceptions'); + return array(); + } + } + + public function getTreatmentsWithConfigByFlagSets($key, $flagSets, array $attributes = null) + { + try { + return $this->doEvaluationByFlagSets( + 'getTreatmentsWithConfigByFlagSets', + $key, + $flagSets, + $attributes + ); + } catch (\Exception $e) { + SplitApp::logger()->critical('getTreatmentsWithConfigByFlagSets method is throwing exceptions'); + return array(); + } + } + + public function getTreatmentsByFlagSet($key, $flagSet, array $attributes = null) + { + try { + return array_map( + function ($feature) { + return $feature['treatment']; + }, + $this->doEvaluationByFlagSets( + 'getTreatmentsByFlagSet', + $key, + array($flagSet), + $attributes + ) + ); + } catch (\Exception $e) { + SplitApp::logger()->critical('getTreatmentsByFlagSet method is throwing exceptions'); + return array(); + } + } + + public function getTreatmentsWithConfigByFlagSet($key, $flagSet, array $attributes = null) + { + try { + return $this->doEvaluationByFlagSets( + 'getTreatmentsWithConfigByFlagSet', + $key, + array($flagSet), + $attributes + ); + } catch (\Exception $e) { + SplitApp::logger()->critical('getTreatmentsWithConfigByFlagSet method is throwing exceptions'); + return array(); + } + } + + private function doInputValidationByFlagSets($key, $flagSets, array $attributes = null, $operation) + { + $key = InputValidator::validateKey($key, $operation); + if (is_null($key) || !InputValidator::validAttributes($attributes, $operation)) { + return null; + } + + $sets = FlagSetsValidator::areValid($flagSets, $operation); + if (is_null($sets)) { + return null; + } + + return array( + 'matchingKey' => $key['matchingKey'], + 'bucketingKey' => $key['bucketingKey'], + 'flagSets' => $sets, + ); + } + + private function doEvaluationByFlagSets($operation, $key, $flagSets, $attributes) + { + $inputValidation = $this->doInputValidationByFlagSets($key, $flagSets, $attributes, $operation); + if (is_null($inputValidation)) { + return array(); + } + + $matchingKey = $inputValidation['matchingKey']; + $bucketingKey = $inputValidation['bucketingKey']; + $flagSets = $inputValidation['flagSets']; + + try { + $evaluationResults = $this->evaluator->evaluateFeaturesByFlagSets( + $matchingKey, + $bucketingKey, + $flagSets, + $attributes + ); + return $this->processEvaluations( + $matchingKey, + $bucketingKey, + $operation, + $attributes, + $evaluationResults['evaluations'] + ); + } catch (\Exception $e) { + SplitApp::logger()->critical($operation . ' method is throwing exceptions'); + SplitApp::logger()->critical($e->getMessage()); + SplitApp::logger()->critical($e->getTraceAsString()); + } + return array(); + } + + private function processEvaluations( + $matchingKey, + $bucketingKey, + $operation, + $attributes, + $evaluations + ) { + $result = array(); + $impressions = array(); + foreach ($evaluations as $featureFlagName => $evalResult) { + if (InputValidator::isSplitFound($evalResult['impression']['label'], $featureFlagName, $operation)) { + // Creates impression + $impressions[] = $this->createImpression( + $matchingKey, + $featureFlagName, + $evalResult['treatment'], + $evalResult['impression']['changeNumber'], + $evalResult['impression']['label'], + $bucketingKey + ); + $result[$featureFlagName] = array( + 'treatment' => $evalResult['treatment'], + 'config' => $evalResult['config'], + ); + } else { + $result[$featureFlagName] = array('treatment' => TreatmentEnum::CONTROL, 'config' => null); + } + } + $this->registerData($impressions, $attributes); + return $result; + } } diff --git a/src/SplitIO/Sdk/ClientInterface.php b/src/SplitIO/Sdk/ClientInterface.php index b070116e..813da077 100644 --- a/src/SplitIO/Sdk/ClientInterface.php +++ b/src/SplitIO/Sdk/ClientInterface.php @@ -146,6 +146,124 @@ public function getTreatments($key, $featureFlagNames, array $attributes = null) */ public function getTreatmentsWithConfig($key, $featureFlagNames, array $attributes = null); + /** + * Returns an associative array which each key will be + * the treatment result and the config for each + * feature associated with flag sets passed as parameter. + * The set of treatments for a feature can be configured + * on the Split web console and the config for + * that treatment. + *
+ * The sdk returns the default treatment of this feature if: + *
+ * This method does not throw any exceptions. + * It also never returns null. + * + * @param $key + * @param $flagSets + * @param $attributes + * @return array + */ + public function getTreatmentsWithConfigByFlagSets($key, $flagSets, array $attributes = null); + + /** + * Returns an associative array which each key will be + * the treatment result and the config for each + * feature associated with flag sets passed as parameter. + * The set of treatments for a feature can be configured + * on the Split web console and the config for + * that treatment. + *
+ * The sdk returns the default treatment of this feature if: + *
+ * This method does not throw any exceptions. + * It also never returns null. + * + * @param $key + * @param $flagSets + * @param $attributes + * @return array + */ + public function getTreatmentsByFlagSets($key, $flagSets, array $attributes = null); + + /** + * Returns an associative array which each key will be + * the treatment result for each feature associated with + * flag set passed as parameter. + * The set of treatments for a feature can be configured + * on the Split web console. + * This method returns the string 'control' if: + *
+ * The sdk returns the default treatment of this feature if: + *
+ * This method does not throw any exceptions. + * It also never returns null. + * + * @param $key + * @param $flagSet + * @param $attributes + * @return array + */ + public function getTreatmentsByFlagSet($key, $flagSet, array $attributes = null); + + /** + * Returns an associative array which each key will be + * the treatment result and the config for each + * feature associated with flag sets passed as parameter. + * The set of treatments for a feature can be configured + * on the Split web console and the config for + * that treatment. + *
+ * The sdk returns the default treatment of this feature if: + *
+ * This method does not throw any exceptions. + * It also never returns null. + * + * @param $key + * @param $flagSet + * @param $attributes + * @return array + */ + public function getTreatmentsWithConfigByFlagSet($key, $flagSet, array $attributes = null); + /** * A short-hand for *
diff --git a/src/SplitIO/Sdk/Evaluator.php b/src/SplitIO/Sdk/Evaluator.php index ac958320..6853850c 100644 --- a/src/SplitIO/Sdk/Evaluator.php +++ b/src/SplitIO/Sdk/Evaluator.php @@ -29,7 +29,6 @@ public function __construct(SplitCache $splitCache, SegmentCache $segmentCache) $this->segmentCache = $segmentCache; } - private function fetchSplit($featureName) { $splitCachedItem = $this->splitCache->getSplit($featureName); @@ -55,6 +54,25 @@ private function fetchSplits($featureNames) return $toReturn; } + private function fetchFeatureFlagNamesByFlagSets($flagSets) + { + $namesByFlagSets = $this->splitCache->getNamesByFlagSets($flagSets); + $toReturn = array(); + + foreach ($namesByFlagSets as $flagSet => $flagNames) { + if (empty($flagNames)) { + SplitApp::logger()->warning("you passed $flagSet Flag Set that does not contain" . + 'cached feature flag names, please double check what Flag Sets are in use in the' . + 'Split user interface.'); + continue; + } + + array_push($toReturn, ...$flagNames); + } + + return array_values(array_unique($toReturn)); + } + public function evaluateFeature($matchingKey, $bucketingKey, $featureName, array $attributes = null) { $timeStart = Metrics::startMeasuringLatency(); @@ -78,6 +96,15 @@ public function evaluateFeatures($matchingKey, $bucketingKey, array $featureName return $toReturn; } + public function evaluateFeaturesByFlagSets($matchingKey, $bucketingKey, array $flagSets, array $attributes = null) + { + $timeStart = Metrics::startMeasuringLatency(); + $featureFlagNames = $this->fetchFeatureFlagNamesByFlagSets($flagSets); + $toReturn = $this->evaluateFeatures($matchingKey, $bucketingKey, $featureFlagNames, $attributes); + $toReturn['latency'] = Metrics::calculateLatency($timeStart); + return $toReturn; + } + private function evalTreatment($key, $bucketingKey, $split, array $attributes = null) { $context = array( diff --git a/src/SplitIO/Sdk/LocalhostClient.php b/src/SplitIO/Sdk/LocalhostClient.php index 2af44546..139446a1 100644 --- a/src/SplitIO/Sdk/LocalhostClient.php +++ b/src/SplitIO/Sdk/LocalhostClient.php @@ -257,4 +257,28 @@ public function track($key, $trafficType, $eventType, $value = null, $properties { return true; } + + public function getTreatmentsWithConfigByFlagSets($key, $flagSets, array $attributes = null) + { + // no-op + return array(); + } + + public function getTreatmentsByFlagSets($key, $flagSets, array $attributes = null) + { + // no-op + return array(); + } + + public function getTreatmentsWithConfigByFlagSet($key, $flagSet, array $attributes = null) + { + // no-op + return array(); + } + + public function getTreatmentsByFlagSet($key, $flagSet, array $attributes = null) + { + // no-op + return array(); + } } diff --git a/src/SplitIO/Sdk/Manager/LocalhostSplitManager.php b/src/SplitIO/Sdk/Manager/LocalhostSplitManager.php index 0e0e4b83..0d138faa 100644 --- a/src/SplitIO/Sdk/Manager/LocalhostSplitManager.php +++ b/src/SplitIO/Sdk/Manager/LocalhostSplitManager.php @@ -95,7 +95,9 @@ public function split($featureFlagName) false, $this->splits[$featureFlagName]["treatments"], 0, - $configs + $configs, + null, + array() ); } diff --git a/src/SplitIO/Sdk/Manager/SplitManager.php b/src/SplitIO/Sdk/Manager/SplitManager.php index 51613df1..44554aa1 100644 --- a/src/SplitIO/Sdk/Manager/SplitManager.php +++ b/src/SplitIO/Sdk/Manager/SplitManager.php @@ -65,7 +65,6 @@ private static function parseSplitView($splitRepresentation) } $split = new Split(json_decode($splitRepresentation, true)); - $configs = !is_null($split->getConfigurations()) ? $split->getConfigurations() : new StdClass; return new SplitView( @@ -74,7 +73,9 @@ private static function parseSplitView($splitRepresentation) $split->killed(), $split->getTreatments(), $split->getChangeNumber(), - $configs + $configs, + $split->getDefaultTratment(), + $split->getSets() ); } } diff --git a/src/SplitIO/Sdk/Manager/SplitView.php b/src/SplitIO/Sdk/Manager/SplitView.php index 493bf068..558635db 100644 --- a/src/SplitIO/Sdk/Manager/SplitView.php +++ b/src/SplitIO/Sdk/Manager/SplitView.php @@ -9,6 +9,8 @@ class SplitView private $treatments; private $changeNumber; private $configs; + private $defaultTreatment; + private $sets; /** * SplitView constructor. @@ -18,15 +20,27 @@ class SplitView * @param $treatments * @param $changeNumber * @param $configurations + * @param $defaultTreatment + * @param $sets */ - public function __construct($name, $trafficType, $killed, $treatments, $changeNumber, $configs) - { + public function __construct( + $name, + $trafficType, + $killed, + $treatments, + $changeNumber, + $configs, + $defaultTreatment, + $sets + ) { $this->name = $name; $this->trafficType = $trafficType; $this->killed = $killed; $this->treatments = $treatments; $this->changeNumber = $changeNumber; $this->configs = $configs; + $this->defaultTreatment = $defaultTreatment; + $this->sets = $sets; } @@ -125,4 +139,36 @@ public function setConfigs($configs) { $this->configs = $configs; } + + /** + * @param mixed $defaultTreatment + */ + public function setDefaultTreatment($defaultTreatment) + { + $this->defaultTreatment = $defaultTreatment; + } + + /** + * @return mixed + */ + public function getDefaultTreatment() + { + return $this->defaultTreatment; + } + + /** + * @param mixed $sets + */ + public function setSets($sets) + { + $this->sets = $sets; + } + + /** + * @return mixed + */ + public function getSets() + { + return $this->sets; + } } diff --git a/src/SplitIO/Sdk/Validator/FlagSetsValidator.php b/src/SplitIO/Sdk/Validator/FlagSetsValidator.php new file mode 100644 index 00000000..69893d84 --- /dev/null +++ b/src/SplitIO/Sdk/Validator/FlagSetsValidator.php @@ -0,0 +1,62 @@ +error($operation . ': FlagSets must be a non-empty list.'); + return array(); + } + + $sanitized = []; + foreach ($flagSets as $flagSet) { + $sanitizedFlagSet = self::sanitize($flagSet, $operation); + if (!is_null($sanitizedFlagSet)) { + array_push($sanitized, $sanitizedFlagSet); + } + } + + return array_values(array_unique($sanitized)); + } + + private static function sanitize($flagSet, $operation) + { + if ($flagSet == null) { + return null; + } + + if (!is_string($flagSet)) { + SplitApp::logger()->error($operation . ': FlagSet must be a string and not null. ' . + $flagSet . ' was discarded.'); + return null; + } + + $trimmed = trim($flagSet); + if ($trimmed !== $flagSet) { + SplitApp::logger()->warning($operation . ': Flag Set name "' . $flagSet . + '" has extra whitespace, trimming.'); + } + $toLowercase = strtolower($trimmed); + if ($toLowercase !== $trimmed) { + SplitApp::logger()->warning($operation . ': Flag Set name "' . $flagSet . + '" should be all lowercase - converting string to lowercase.'); + } + if (!preg_match(REG_EXP_FLAG_SET, $toLowercase)) { + SplitApp::logger()->warning($operation . ': you passed "' . $flagSet . + '", Flag Set must adhere to the regular expressions {' .REG_EXP_FLAG_SET . + '} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "' . $flagSet . '" was discarded.'); + return null; + } + + return $toLowercase; + } +} diff --git a/src/SplitIO/Sdk/Validator/InputValidator.php b/src/SplitIO/Sdk/Validator/InputValidator.php index 27f2f07e..9862da6c 100644 --- a/src/SplitIO/Sdk/Validator/InputValidator.php +++ b/src/SplitIO/Sdk/Validator/InputValidator.php @@ -67,7 +67,7 @@ private static function checkIsNull($value, $name, $nameType, $operation) private static function checkIsEmpty($value, $name, $nameType, $operation) { $trimmed = trim($value); - if (empty($trimmed)) { + if (0 == strlen($trimmed)) { SplitApp::logger()->critical($operation . ": you passed an empty " . $name . ", " . $nameType . " must be a non-empty string."); return true; @@ -262,7 +262,7 @@ function ($featureFlagName) use ($operation) { ) ) ); - if (empty($filteredArray)) { + if (0 == count($filteredArray)) { SplitApp::logger()->critical($operation . ': featureFlagNames must be a non-empty array.'); return null; } diff --git a/src/SplitIO/Version.php b/src/SplitIO/Version.php index 97f8446b..2f4d6399 100644 --- a/src/SplitIO/Version.php +++ b/src/SplitIO/Version.php @@ -3,5 +3,5 @@ class Version { - const CURRENT = '8.0.0-rc3'; + const CURRENT = '8.0.0-rc4'; } diff --git a/tests/Suite/Attributes/files/splitChanges.json b/tests/Suite/Attributes/files/splitChanges.json index 63dc2398..0460ef22 100644 --- a/tests/Suite/Attributes/files/splitChanges.json +++ b/tests/Suite/Attributes/files/splitChanges.json @@ -10,6 +10,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -57,6 +58,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "on", + "sets": [], "conditions": [ { "matcherGroup": { @@ -130,6 +132,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -187,6 +190,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -233,6 +237,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -273,6 +278,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -311,6 +317,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -353,6 +360,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -395,6 +403,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -437,6 +446,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -478,6 +488,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -519,6 +530,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -560,6 +572,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -601,6 +614,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -642,6 +656,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -683,6 +698,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -724,6 +740,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -765,6 +782,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -806,6 +824,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -847,6 +866,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -888,6 +908,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -929,6 +950,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -970,6 +992,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1011,6 +1034,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1052,6 +1076,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1093,6 +1118,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1134,6 +1160,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "on", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1176,6 +1203,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1244,6 +1272,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1286,6 +1315,7 @@ "status": "ACTIVE", "killed": true, "defaultTreatment": "defTreatment", + "sets": [], "conditions": [ { "matcherGroup": { @@ -1328,6 +1358,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { diff --git a/tests/Suite/DynamicConfigurations/EvaluatorTest.php b/tests/Suite/DynamicConfigurations/EvaluatorTest.php index b57db811..c1dfbfd3 100644 --- a/tests/Suite/DynamicConfigurations/EvaluatorTest.php +++ b/tests/Suite/DynamicConfigurations/EvaluatorTest.php @@ -172,4 +172,51 @@ public function testSplitWithConfigurationsButKilledWithConfigsOnDefault() $redisClient->del('SPLITIO.split.mysplittest4'); } + + public function testEvaluateFeaturesByFlagSets() + { + $parameters = array('scheme' => 'redis', 'host' => REDIS_HOST, 'port' => REDIS_PORT, 'timeout' => 881); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Initializing the SDK instance. + $factory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $cachePool = ReflectiveTools::cacheFromFactory($factory); + $redisClient = ReflectiveTools::clientFromFactory($factory); + + $redisClient->del('SPLITIO.flagSet.set_1'); + $redisClient->del('SPLITIO.flagSet.set_2'); + $redisClient->del('SPLITIO.split.mysplittest'); + $redisClient->del('SPLITIO.split.mysplittest2'); + $redisClient->del('SPLITIO.split.mysplittest4'); + + $redisClient->set('SPLITIO.split.mysplittest', $this->split1); + $redisClient->set('SPLITIO.split.mysplittest2', $this->split2); + $redisClient->set('SPLITIO.split.mysplittest4', $this->split4); + $redisClient->sadd('SPLITIO.flagSet.set_1', 'mysplittest2'); + $redisClient->sadd('SPLITIO.flagSet.set_2', 'mysplittest2'); + $redisClient->sadd('SPLITIO.flagSet.set_2', 'mysplittest4'); + $redisClient->sadd('SPLITIO.flagSet.set_5', 'mysplittest'); + + $segmentCache = new SegmentCache($cachePool); + $splitCache = new SplitCache($cachePool); + $evaluator = new Evaluator($splitCache, $segmentCache); + + $result = $evaluator->evaluateFeaturesByFlagSets('test', '', ['set_1', 'set_2', 'set_3']); + + $this->assertEquals('on', $result['evaluations']['mysplittest2']['treatment']); + $this->assertEquals('killed', $result['evaluations']['mysplittest4']['treatment']); + $this->assertFalse(array_key_exists('mysplittest', $result['evaluations'])); + $this->assertGreaterThan(0, $result['latency']); + + $redisClient->del('SPLITIO.flagSet.set_1'); + $redisClient->del('SPLITIO.flagSet.set_2'); + $redisClient->del('SPLITIO.split.mysplittest'); + $redisClient->del('SPLITIO.split.mysplittest2'); + $redisClient->del('SPLITIO.split.mysplittest4'); + } } diff --git a/tests/Suite/InputValidation/FlagSetsValidatorTest.php b/tests/Suite/InputValidation/FlagSetsValidatorTest.php new file mode 100644 index 00000000..fa6f5214 --- /dev/null +++ b/tests/Suite/InputValidation/FlagSetsValidatorTest.php @@ -0,0 +1,136 @@ +getMockBuilder('\SplitIO\Component\Log\Logger') + ->disableOriginalConstructor() + ->setMethods(array('warning', 'debug', 'error', 'info', 'critical', 'emergency', + 'alert', 'notice', 'write', 'log')) + ->getMock(); + + ReflectiveTools::overrideLogger($logger); + + return $logger; + } + + public function testAreValidWithEmptyArray() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('error') + ->with($this->equalTo('test: FlagSets must be a non-empty list.')); + + $result = FlagSetsValidator::areValid([], "test"); + $this->assertEquals(0, count($result)); + } + + public function testAreValidWithWhitespaces() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('warning') + ->with($this->equalTo('test: Flag Set name " set_1 " has extra whitespace, trimming.')); + + $result = FlagSetsValidator::areValid([" set_1 "], "test"); + $this->assertEquals(1, count($result)); + } + + public function testAreValidWithUppercases() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('warning') + ->with($this->equalTo('test: Flag Set name "SET_1" should be all lowercase - converting string to lowercase.')); + + $result = FlagSetsValidator::areValid(["SET_1"], "test"); + $this->assertEquals(1, count($result)); + } + + public function testAreValidWithIncorrectCharacters() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('warning') + ->with($this->equalTo('test: you passed "set-2", Flag Set must adhere to the regular expressions {/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. "set-2" was discarded.')); + + $result = FlagSetsValidator::areValid(["set-2"], "test"); + $this->assertEquals(0, count($result)); + } + + public function testAreValidWithFlagSetToLong() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('warning') + ->with($this->equalTo('test: you passed "set_123123123123123123123123123123123123123123123123", Flag Set must adhere to the regular expressions {/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and have a max length of 50 characters. "set_123123123123123123123123123123123123123123123123" was discarded.')); + + $result = FlagSetsValidator::areValid(["set_123123123123123123123123123123123123123123123123"], "test"); + $this->assertEquals(0, count($result)); + } + + public function testAreValidWithFlagSetDupiclated() + { + $result = FlagSetsValidator::areValid(["set_4", "set_1", "SET_1", "set_2", " set_2 ", "set_3", "set_3"], "test"); + $this->assertEquals(4, count($result)); + $this->assertEquals("set_4", $result[0]); + $this->assertEquals("set_1", $result[1]); + $this->assertEquals("set_2", $result[2]); + $this->assertEquals("set_3", $result[3]); + } + + public function testAreValidWithIncorrectTypes() + { + $logger = $this->getMockedLogger(); + + $logger->expects($this->once()) + ->method('error') + ->with($this->equalTo('test: FlagSet must be a string and not null. 123 was discarded.')); + + $result = FlagSetsValidator::areValid([null, 123, "set_1", "SET_1"], "test"); + $this->assertEquals(1, count($result)); + } + + public function testAreValidConsecutive() + { + $logger = $this->getMockedLogger(); + + $logger + ->expects($this->exactly(6)) + ->method('warning') + ->withConsecutive( + ['test: Flag Set name " A " has extra whitespace, trimming.'], + ['test: Flag Set name " A " should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name "@FAIL" should be all lowercase - converting string to lowercase.'], + ['test: you passed "@FAIL", Flag Set must adhere to the regular expressions ' . + '{/^[a-z0-9][_a-z0-9]{0,49}$/} This means a Flag Set must start with a letter or number, be in lowercase, alphanumeric and ' . + 'have a max length of 50 characters. "@FAIL" was discarded.'], + ['test: Flag Set name "TEST" should be all lowercase - converting string to lowercase.'], + ['test: Flag Set name " a" has extra whitespace, trimming.'], + ); + $logger + ->expects($this->exactly(2)) + ->method('error') + ->withConsecutive( + ['test: FlagSets must be a non-empty list.'], + ['test: FlagSets must be a non-empty list.'] + ); + + $this->assertEquals(['a', 'test'], FlagSetsValidator::areValid([' A ', '@FAIL', 'TEST'], 'test')); + $this->assertEquals(array(), FlagSetsValidator::areValid([], 'test')); + $this->assertEquals(array(), FlagSetsValidator::areValid(['some' => 'some1'], 'test')); + $this->assertEquals(['a', 'test'], FlagSetsValidator::areValid(['a', 'test', ' a'], 'test')); + } +} diff --git a/tests/Suite/InputValidation/GetTreatmentValidationTest.php b/tests/Suite/InputValidation/GetTreatmentValidationTest.php index a4f3ba8f..2c83e812 100644 --- a/tests/Suite/InputValidation/GetTreatmentValidationTest.php +++ b/tests/Suite/InputValidation/GetTreatmentValidationTest.php @@ -57,6 +57,7 @@ public function testGetTreatmentWithEmptyMatchingKeyObject() $splitSdk = $this->getFactoryClient(); $this->assertEquals('control', $splitSdk->getTreatment(new Key('', 'some_bucketing_key'), 'some_feature')); + $this->assertNotEquals('control', $splitSdk->getTreatment(new Key("0", 'some_bucketing_key'), 'some_feature')); } public function testGetTreatmentWithWrongTypeMatchingKeyObject() diff --git a/tests/Suite/Redis/CacheInterfacesTest.php b/tests/Suite/Redis/CacheInterfacesTest.php index 8e94bb65..6767b1a2 100644 --- a/tests/Suite/Redis/CacheInterfacesTest.php +++ b/tests/Suite/Redis/CacheInterfacesTest.php @@ -61,11 +61,14 @@ public function testSplitCacheInterface() $splitChanges = json_decode($splitChanges, true); $splits = $splitChanges['splits']; $split = $splits[0]; - $splitName = $split['name']; + $flagSets = array('set_a', 'set_b'); $this->assertEquals(strlen(json_encode($split)), strlen($splitCache->getSplit($splitName))); $this->assertEquals($splitChanges['till'], $splitCache->getChangeNumber()); + $result = $splitCache->getNamesByFlagSets($flagSets); + $this->assertEquals(2, count($result['set_a'])); + $this->assertEquals(2, count($result['set_b'])); } /** diff --git a/tests/Suite/Redis/SafeRedisWrapperTest.php b/tests/Suite/Redis/SafeRedisWrapperTest.php index 320f2999..b60b9c8d 100644 --- a/tests/Suite/Redis/SafeRedisWrapperTest.php +++ b/tests/Suite/Redis/SafeRedisWrapperTest.php @@ -1,9 +1,6 @@ getMockBuilder('\Predis\Client') ->disableOriginalConstructor() @@ -49,5 +46,6 @@ public function testAllMethodsException() $this->assertEquals(array(), $safeRedisWrapper->getKeys("some")); $this->assertEquals(0, $safeRedisWrapper->rightPushQueue("some", "another")); $this->assertEquals(false, $safeRedisWrapper->expireKey("some", 12345)); + $this->assertEquals(array(), $safeRedisWrapper->sMembers("key")); } } diff --git a/tests/Suite/Sdk/SdkClientTest.php b/tests/Suite/Sdk/SdkClientTest.php index 765fcc99..ddd27fed 100644 --- a/tests/Suite/Sdk/SdkClientTest.php +++ b/tests/Suite/Sdk/SdkClientTest.php @@ -4,15 +4,13 @@ use \stdClass; use ReflectionClass; use SplitIO\Test\Suite\Redis\ReflectiveTools; +use SplitIO\Component\Cache\SegmentCache; +use SplitIO\Component\Cache\SplitCache; use SplitIO\Component\Cache\ImpressionCache; use SplitIO\Component\Cache\EventsCache; use SplitIO\Component\Cache\Storage\Adapter\PRedis; use SplitIO\Component\Cache\Pool; -use SplitIO\Component\Cache\SegmentCache; -use SplitIO\Component\Cache\SplitCache; use SplitIO\Sdk\Client; - -use SplitIO\Test\Suite\Sdk\Helpers\CustomLogger; use SplitIO\Test\Utils; class SdkClientTest extends \PHPUnit\Framework\TestCase @@ -207,6 +205,40 @@ private function validateLastImpression( $this->assertEquals($parsed['m']['n'], $machineName); } + public function testSplitManager() + { + ReflectiveTools::resetContext(); + $parameters = array( + 'scheme' => 'redis', + 'host' => REDIS_HOST, + 'port' => REDIS_PORT, + 'timeout' => 881, + ); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Populating the cache. + Utils\Utils::addSplitsInCache(file_get_contents(__DIR__."/files/splitChanges.json")); + + //Initializing the SDK instance. + $splitFactory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $splitManager = $splitFactory->manager(); + + //Assertions + $split_view = $splitManager->split("flagsets_feature"); + $this->assertEquals("flagsets_feature", $split_view->getName()); + $this->assertEquals('off', $split_view->getDefaultTreatment()); + $this->assertEquals('["set_a","set_b","set_c"]', json_encode($split_view->getSets())); + + $split_views = $splitManager->splits(); + $this->assertEquals('off', $split_views["flagsets_feature"]->getDefaultTreatment()); + $this->assertEquals('["set_a","set_b","set_c"]', json_encode($split_views["flagsets_feature"]->getSets())); + } + public function testClient() { ReflectiveTools::resetContext(); @@ -746,6 +778,189 @@ public function testMultipleInstantiationNotOverrideIP() $this->validateLastImpression($redisClient, 'sample_feature', 'user1', 'on', 'ip-1-2-3-4', '1.2.3.4'); } + public function testGetTreatmentsWithConfigByFlagSets() + { + ReflectiveTools::resetContext(); + $parameters = array('scheme' => 'redis', 'host' => REDIS_HOST, 'port' => REDIS_PORT, 'timeout' => 881); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Initializing the SDK instance. + $splitFactory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $splitSdk = $splitFactory->client(); + + //Populating the cache. + Utils\Utils::addSplitsInCache(file_get_contents(__DIR__."/files/splitChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentEmployeesChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentHumanBeignsChanges.json")); + + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSets('user1', array('set_a', null, 'invalid-set', 'set_a', null, 'set_b'), null); + + //Assertions + $this->assertEquals(2, count(array_keys($treatmentResult))); + + $this->assertEquals('on', $treatmentResult['flagsets_feature']["treatment"]); + $this->assertEquals("{\"size\":15,\"test\":20}", $treatmentResult['flagsets_feature']["config"]); + $this->assertEquals('off', $treatmentResult['boolean_test']["treatment"]); + $this->assertNull($treatmentResult['boolean_test']["config"]); + + //Check impressions + $redisClient = ReflectiveTools::clientFromFactory($splitFactory); + $this->validateLastImpression($redisClient, 'boolean_test', 'user1', 'off'); + $this->validateLastImpression($redisClient, 'flagsets_feature', 'user1', 'on'); + + // With Incorrect Values + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSets('user1', array(null, 123, ""=>"", "fake_name", "###", ["set"], ["set"=>"set"]), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // Empty array + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSets('user1', array(), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // null + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSets('user1', null, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + } + + public function testGetTreatmentsByFlagSets() + { + ReflectiveTools::resetContext(); + $parameters = array('scheme' => 'redis', 'host' => REDIS_HOST, 'port' => REDIS_PORT, 'timeout' => 881); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Initializing the SDK instance. + $splitFactory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $splitSdk = $splitFactory->client(); + + //Populating the cache. + Utils\Utils::addSplitsInCache(file_get_contents(__DIR__."/files/splitChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentEmployeesChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentHumanBeignsChanges.json")); + + $treatmentResult = $splitSdk->getTreatmentsByFlagSets('user1', array('set_a', null, 'invalid-set', 'set_a', null, 'set_b'), null); + + //Assertions + $this->assertEquals(2, count(array_keys($treatmentResult))); + + $this->assertEquals('on', $treatmentResult['flagsets_feature']); + $this->assertEquals('off', $treatmentResult['boolean_test']); + + //Check impressions + $redisClient = ReflectiveTools::clientFromFactory($splitFactory); + $this->validateLastImpression($redisClient, 'boolean_test', 'user1', 'off'); + $this->validateLastImpression($redisClient, 'flagsets_feature', 'user1', 'on'); + + // With Incorrect Values + $treatmentResult = $splitSdk->getTreatmentsByFlagSets('user1', array(null, 123, "set"=>"set", "fake_name", "###", ["set"], ["set"=>"set"]), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // Empty array + $treatmentResult = $splitSdk->getTreatmentsByFlagSets('user1', array(), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // null + $treatmentResult = $splitSdk->getTreatmentsByFlagSets('user1', null, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + } + + public function testGetTreatmentsWithConfigByFlagSet() + { + ReflectiveTools::resetContext(); + $parameters = array('scheme' => 'redis', 'host' => REDIS_HOST, 'port' => REDIS_PORT, 'timeout' => 881); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Initializing the SDK instance. + $splitFactory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $splitSdk = $splitFactory->client(); + + //Populating the cache. + Utils\Utils::addSplitsInCache(file_get_contents(__DIR__."/files/splitChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentEmployeesChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentHumanBeignsChanges.json")); + + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSet('user1', 'set_a', null); + + //Assertions + $this->assertEquals(1, count(array_keys($treatmentResult))); + + $this->assertEquals('on', $treatmentResult['flagsets_feature']["treatment"]); + $this->assertEquals("{\"size\":15,\"test\":20}", $treatmentResult['flagsets_feature']["config"]); + + //Check impressions + $redisClient = ReflectiveTools::clientFromFactory($splitFactory); + $this->validateLastImpression($redisClient, 'flagsets_feature', 'user1', 'on'); + + // With Incorrect Values + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSet('user1', 123, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // Empty array + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSet('user1', array(), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // null + $treatmentResult = $splitSdk->getTreatmentsWithConfigByFlagSet('user1', null, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + } + + public function testGetTreatmentsByFlagSet() + { + ReflectiveTools::resetContext(); + $parameters = array('scheme' => 'redis', 'host' => REDIS_HOST, 'port' => REDIS_PORT, 'timeout' => 881); + $options = array('prefix' => TEST_PREFIX); + + $sdkConfig = array( + 'log' => array('adapter' => 'stdout'), + 'cache' => array('adapter' => 'predis', 'parameters' => $parameters, 'options' => $options) + ); + + //Initializing the SDK instance. + $splitFactory = \SplitIO\Sdk::factory('asdqwe123456', $sdkConfig); + $splitSdk = $splitFactory->client(); + + //Populating the cache. + Utils\Utils::addSplitsInCache(file_get_contents(__DIR__."/files/splitChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentEmployeesChanges.json")); + Utils\Utils::addSegmentsInCache(file_get_contents(__DIR__."/files/segmentHumanBeignsChanges.json")); + + $treatmentResult = $splitSdk->getTreatmentsByFlagSet('user1', 'set_a', null); + + //Assertions + $this->assertEquals(1, count(array_keys($treatmentResult))); + + $this->assertEquals('on', $treatmentResult['flagsets_feature']); + + //Check impressions + $redisClient = ReflectiveTools::clientFromFactory($splitFactory); + $this->validateLastImpression($redisClient, 'flagsets_feature', 'user1', 'on'); + + // With Incorrect Values + $treatmentResult = $splitSdk->getTreatmentsByFlagSet('user1', 123, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // Empty array + $treatmentResult = $splitSdk->getTreatmentsByFlagSet('user1', array(), null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + + // null + $treatmentResult = $splitSdk->getTreatmentsByFlagSet('user1', null, null); + $this->assertEquals(0, count(array_keys($treatmentResult))); + } + public static function tearDownAfterClass(): void { Utils\Utils::cleanCache(); diff --git a/tests/Suite/Sdk/files/splitChanges.json b/tests/Suite/Sdk/files/splitChanges.json index 84548e58..59e8a287 100644 --- a/tests/Suite/Sdk/files/splitChanges.json +++ b/tests/Suite/Sdk/files/splitChanges.json @@ -10,6 +10,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -69,6 +70,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -128,6 +130,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -164,6 +167,7 @@ "status": "ACTIVE", "killed": true, "defaultTreatment": "defTreatment", + "sets": [], "configurations": { "off": "{\"size\":15,\"test\":20}", "defTreatment": "{\"size\":15,\"defTreatment\":true}" @@ -204,6 +208,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "configurations": { "on": "{\"size\":15,\"test\":20}" }, @@ -266,6 +271,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -305,6 +311,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -341,6 +348,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": ["set_b", "set_c"], "conditions": [ { "matcherGroup": { @@ -366,6 +374,70 @@ ] } ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "flagsets_feature", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": ["set_a", "set_b", "set_c"], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "whitelisted_user" + ] + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ] + } + ] } ], "since": -1, diff --git a/tests/Suite/Sdk/files/splitReadOnly.json b/tests/Suite/Sdk/files/splitReadOnly.json index 748dd155..9f540ead 100644 --- a/tests/Suite/Sdk/files/splitReadOnly.json +++ b/tests/Suite/Sdk/files/splitReadOnly.json @@ -10,6 +10,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { diff --git a/tests/Suite/Sdk/files/splitil.json b/tests/Suite/Sdk/files/splitil.json index 0753555f..2b7f689b 100644 --- a/tests/Suite/Sdk/files/splitil.json +++ b/tests/Suite/Sdk/files/splitil.json @@ -10,6 +10,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { diff --git a/tests/Utils/Utils.php b/tests/Utils/Utils.php index e2be6ec3..df602407 100644 --- a/tests/Utils/Utils.php +++ b/tests/Utils/Utils.php @@ -7,6 +7,7 @@ public static function addSplitsInCache($splitChanges) { $splitKey = "SPLITIO.split."; $tillKey = "SPLITIO.splits.till"; + $flagSetKey = "SPLITIO.flagSet."; $predis = new \Predis\Client([ 'host' => REDIS_HOST, @@ -23,6 +24,11 @@ public static function addSplitsInCache($splitChanges) foreach ($splits as $split) { $splitName = $split['name']; $predis->set($splitKey . $splitName, json_encode($split)); + + $sets = $split['sets']; + foreach ($sets as $set) { + $predis->sadd($flagSetKey . $set, $splitName); + } } $till = -1; if (isset($splitChanges['till'])) { diff --git a/tests/files/algoSplits.json b/tests/files/algoSplits.json index 67dac317..37e78bba 100644 --- a/tests/files/algoSplits.json +++ b/tests/files/algoSplits.json @@ -11,6 +11,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -71,6 +72,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { @@ -108,6 +110,7 @@ "status": "ACTIVE", "killed": true, "defaultTreatment": "defTreatment", + "sets": [], "conditions": [ { "matcherGroup": { @@ -144,6 +147,7 @@ "status": "ACTIVE", "killed": false, "defaultTreatment": "off", + "sets": [], "conditions": [ { "matcherGroup": { diff --git a/tests/files/splitChanges.json b/tests/files/splitChanges.json index 35b44a35..30ab4981 100644 --- a/tests/files/splitChanges.json +++ b/tests/files/splitChanges.json @@ -9,6 +9,72 @@ "seed": 301711069, "status": "ACTIVE", "killed": false, + "sets": ["set_a", "set_b", "set_c"], + "configurations": { + "on": "{\"size\":15,\"test\":20}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "WHITELIST", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "fake_user_id_6", + "fake_user_id_7999" + ] + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "IN_SEGMENT", + "negate": false, + "userDefinedSegmentMatcherData": { + "segmentName": "employees" + }, + "whitelistMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 80 + }, + { + "treatment": "control", + "size": 20 + } + ] + } + ] + }, + { + "orgId": "bf083ab0-b402-11e5-b7d5-024293b5d101", + "environment": "bf9d9ce0-b402-11e5-b7d5-024293b5d101", + "name": "sample_feature_2", + "trafficTypeId": "u", + "trafficTypeName": "User", + "seed": 301711069, + "status": "ACTIVE", + "killed": false, + "sets": ["set_a", "set_b", "set_c"], "configurations": { "on": "{\"size\":15,\"test\":20}" },