diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4e63c45e..fed4af2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,7 @@ jobs: - '8.0' - '8.1' - '8.2' + - '8.3' steps: - name: Checkout code uses: actions/checkout@v3 diff --git a/CHANGES.txt b/CHANGES.txt index 5f741368..e75e9697 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +7.3.0 (May 14, 2024) + - Added support for targeting rules based on semantic versions (https://semver.org/). + - Added the logic to handle correctly when the SDK receives an unsupported Matcher type. + - Updated dependencies to allow `symfony/yaml` 7. + 7.2.1 (March 6, 2024) - Fix error on duplicated file flagSetsValidator. diff --git a/composer.json b/composer.json index 16f5a3f0..c22f0e41 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "php": ">=7.3", "psr/log": "1 - 3", "predis/predis": "^2.0", - "symfony/yaml": "^5.3|^6.0" + "symfony/yaml": "^5.3|^6.0|^7.0" }, "require-dev": { diff --git a/src/SplitIO/Exception/SemverParseException.php b/src/SplitIO/Exception/SemverParseException.php new file mode 100644 index 00000000..da1a2749 --- /dev/null +++ b/src/SplitIO/Exception/SemverParseException.php @@ -0,0 +1,7 @@ + ConditionTypeEnum::WHITELIST, + 'matcherGroup' => array( + 'combiner' => CombinerEnum::_AND, + 'matchers' => array( + array( + 'matcherType' => Matcher::ALL_KEYS, + 'negate' => false, + 'userDefinedSegmentMatcherData' => null, + 'whitelistMatcherData' => null + ) + ) + ), + 'partitions' => array( + array( + 'treatment' => TreatmentEnum::CONTROL, + 'size' => 100 + ) + ), + 'label' => ImpressionLabel::UNSUPPORTED_MATCHER + )); + } + /** * @return array|null */ diff --git a/src/SplitIO/Grammar/Condition/Matcher.php b/src/SplitIO/Grammar/Condition/Matcher.php index c49c8e9f..2ab7719f 100644 --- a/src/SplitIO/Grammar/Condition/Matcher.php +++ b/src/SplitIO/Grammar/Condition/Matcher.php @@ -1,11 +1,17 @@ startTarget = Semver::build($data['start']); + $this->endTarget = Semver::build($data['end']); + } + } + + /** + * + * @param mixed $key + */ + protected function evalKey($key) + { + if ($key == null || !is_string($key) || $this->startTarget == null || $this->endTarget == null) { + return false; + } + + $keySemver = Semver::build($key); + if ($keySemver == null) { + return false; + } + + $result = SemverComparer::do($keySemver, $this->startTarget) >= 0 + && SemverComparer::do($keySemver, $this->endTarget) <= 0; + + SplitApp::logger()->debug($this->startTarget->getVersion() . " <= " + . $keySemver->getVersion() . " <= " . $this->endTarget->getVersion() + . " | Result: " . $result); + + return $result; + } +} diff --git a/src/SplitIO/Grammar/Condition/Matcher/EqualToSemver.php b/src/SplitIO/Grammar/Condition/Matcher/EqualToSemver.php new file mode 100644 index 00000000..5e8c4949 --- /dev/null +++ b/src/SplitIO/Grammar/Condition/Matcher/EqualToSemver.php @@ -0,0 +1,42 @@ +toCompare = Semver::build($toCompare); + } + + /** + * + * @param mixed $key + */ + protected function evalKey($key) + { + if ($key == null || $this->toCompare == null || !is_string($key)) { + return false; + } + + $keySemver = Semver::build($key); + if ($keySemver == null) { + return false; + } + + $result = SemverComparer::equals($this->toCompare, $keySemver); + + SplitApp::logger()->debug($this->toCompare->getVersion() . " == " + . $keySemver->getVersion() . " | Result: " . $result); + + return $result; + } +} diff --git a/src/SplitIO/Grammar/Condition/Matcher/GreaterThanOrEqualToSemver.php b/src/SplitIO/Grammar/Condition/Matcher/GreaterThanOrEqualToSemver.php new file mode 100644 index 00000000..6fe0074e --- /dev/null +++ b/src/SplitIO/Grammar/Condition/Matcher/GreaterThanOrEqualToSemver.php @@ -0,0 +1,42 @@ +target = Semver::build($toCompare); + } + + /** + * + * @param mixed $key + */ + protected function evalKey($key) + { + if ($key == null || $this->target == null || !is_string($key)) { + return false; + } + + $keySemver = Semver::build($key); + if ($keySemver == null) { + return false; + } + + $result = SemverComparer::do($keySemver, $this->target) >= 0; + + SplitApp::logger()->debug($this->target->getVersion() . " >= " + . $keySemver->getVersion() . " | Result: " . $result); + + return $result; + } +} diff --git a/src/SplitIO/Grammar/Condition/Matcher/InListSemver.php b/src/SplitIO/Grammar/Condition/Matcher/InListSemver.php new file mode 100644 index 00000000..8eaf391d --- /dev/null +++ b/src/SplitIO/Grammar/Condition/Matcher/InListSemver.php @@ -0,0 +1,51 @@ +targetList = array(); + parent::__construct(Matcher::IN_LIST_SEMVER, $negate, $attribute); + + if (is_array($targetList)) { + foreach ($targetList as $item) { + $toAdd = Semver::build($item); + + if ($toAdd != null) { + array_push($this->targetList, $toAdd); + } + } + } + } + + /** + * + * @param mixed $key + */ + protected function evalKey($key) + { + if ($key == null || !is_string($key) || count($this->targetList) == 0) { + return false; + } + + $keySemver = Semver::build($key); + if ($keySemver == null) { + return false; + } + + foreach ($this->targetList as $item) { + if (SemverComparer::equals($keySemver, $item)) { + return true; + } + } + + return false; + } +} diff --git a/src/SplitIO/Grammar/Condition/Matcher/LessThanOrEqualToSemver.php b/src/SplitIO/Grammar/Condition/Matcher/LessThanOrEqualToSemver.php new file mode 100644 index 00000000..4b537bac --- /dev/null +++ b/src/SplitIO/Grammar/Condition/Matcher/LessThanOrEqualToSemver.php @@ -0,0 +1,42 @@ +target = Semver::build($toCompare); + } + + /** + * + * @param mixed $key + */ + protected function evalKey($key) + { + if ($key == null || $this->target == null || !is_string($key)) { + return false; + } + + $keySemver = Semver::build($key); + if ($keySemver == null) { + return false; + } + + $result = SemverComparer::do($keySemver, $this->target) <= 0; + + SplitApp::logger()->debug($this->target->getVersion() . " <= " + . $keySemver->getVersion() . " | Result: " . $result); + + return $result; + } +} diff --git a/src/SplitIO/Grammar/Semver/Semver.php b/src/SplitIO/Grammar/Semver/Semver.php new file mode 100644 index 00000000..da5cc9b2 --- /dev/null +++ b/src/SplitIO/Grammar/Semver/Semver.php @@ -0,0 +1,136 @@ +error($e->getMessage()); + return null; + } + } + + public function getMajor() + { + return $this->major; + } + + public function getMinor() + { + return $this->minor; + } + + public function getPatch() + { + return $this->patch; + } + + public function getVersion() + { + return $this->version; + } + + public function isStable() + { + return (bool) $this->isStable; + } + + public function getPreRelease() + { + return $this->preRelease; + } + + private function __construct($version) + { + $vWithoutMetadata = $this->setAndRemoveMetadataIfExists($version); + $vWithoutPreRelease = $this->setAndRemovePreReleaseIfExists($vWithoutMetadata); + $this->setMajorMinorAndPatch($vWithoutPreRelease); + $this->version = $this->setVersion(); + } + + private function setAndRemoveMetadataIfExists($version) + { + $index = strpos($version, self::METADATA_DELIMITER); + if ($index === false) { + return $version; + } + + $this->metadata = substr($version, $index + 1); + if ($this->metadata === null || $this->metadata === '') { + throw new SemverParseException("Unable to convert to Semver, incorrect medatada"); + } + + return substr($version, 0, $index); + } + + private function setAndRemovePreReleaseIfExists($vWithoutMetadata) + { + $index = strpos($vWithoutMetadata, self::PRERELEASE_DELIMITER); + if ($index === false) { + $this->isStable = true; + return $vWithoutMetadata; + } + + $preReleaseData = substr($vWithoutMetadata, $index + 1); + $this->preRelease = explode(self::VALUE_DELIMITER, $preReleaseData); + if ($this->preRelease === null || array_reduce($this->preRelease, function ($carry, $item) { + return $carry || $item === null || $item === ''; + }, false)) { + throw new SemverParseException("Unable to convert to Semver, incorrect pre release data"); + } + + return substr($vWithoutMetadata, 0, $index); + } + + private function setMajorMinorAndPatch($version) + { + $vParts = explode(self::VALUE_DELIMITER, $version); + + if (count($vParts) !== 3 || !is_numeric($vParts[0]) || !is_numeric($vParts[1]) || !is_numeric($vParts[2])) { + throw new SemverParseException("Unable to convert to Semver, incorrect format: " . $version); + } + + $this->major = (int)$vParts[0]; + $this->minor = (int)$vParts[1]; + $this->patch = (int)$vParts[2]; + } + + private function setVersion() + { + $toReturn = $this->major . self::VALUE_DELIMITER . $this->minor . self::VALUE_DELIMITER . $this->patch; + + if ($this->preRelease != null && count($this->preRelease) > 0) { + foreach ($this->preRelease as $index => $item) { + if (is_numeric($item)) { + $this->preRelease[$index] = (int)$this->preRelease[$index]; + } + } + + $toReturn = $toReturn . self::PRERELEASE_DELIMITER . implode(self::VALUE_DELIMITER, $this->preRelease); + } + + if ($this->metadata != null && $this->metadata != "") { + $toReturn = $toReturn . self::METADATA_DELIMITER . $this->metadata; + } + + return $toReturn; + } +} diff --git a/src/SplitIO/Grammar/Semver/SemverComparer.php b/src/SplitIO/Grammar/Semver/SemverComparer.php new file mode 100644 index 00000000..bdb48f0d --- /dev/null +++ b/src/SplitIO/Grammar/Semver/SemverComparer.php @@ -0,0 +1,59 @@ +getVersion(), $toCompare->getVersion()) == 0) { + return true; + } + + return false; + } + + public static function do($version, $toCompare) + { + if (self::equals($version, $toCompare)) { + return 0; + } + + // Compare major, minor, and patch versions numerically + if ($version->getMajor() !== $toCompare->getMajor()) { + return $version->getMajor() > $toCompare->getMajor() ? 1 : -1; + } + + if ($version->getMinor() !== $toCompare->getMinor()) { + return $version->getMinor() > $toCompare->getMinor() ? 1 : -1; + } + + if ($version->getPatch() !== $toCompare->getPatch()) { + return $version->getPatch() > $toCompare->getPatch() ? 1 : -1; + } + + if (!$version->isStable() && $toCompare->isStable()) { + return -1; + } elseif ($version->isStable() && !$toCompare->isStable()) { + return 1; + } + + // Compare pre-release versions lexically + $vPreRelease = $version->getPreRelease(); + $tcPreRelease = $toCompare->getPreRelease(); + $minLength = min(count($vPreRelease), count($tcPreRelease)); + for ($i = 0; $i < $minLength; $i++) { + if ($vPreRelease[$i] == $tcPreRelease[$i]) { + continue; + } + + if (is_numeric($vPreRelease[$i]) && is_numeric($tcPreRelease[$i])) { + return $vPreRelease[$i] > $tcPreRelease[$i] ? 1 : -1; + } + + return strcmp($vPreRelease[$i], $tcPreRelease[$i]); + } + + // Compare lengths of pre-release versions + return count($vPreRelease) <=> count($tcPreRelease); + } +} diff --git a/src/SplitIO/Grammar/Split.php b/src/SplitIO/Grammar/Split.php index 4f88c481..6d6cf385 100644 --- a/src/SplitIO/Grammar/Split.php +++ b/src/SplitIO/Grammar/Split.php @@ -1,8 +1,8 @@ info("Constructing Feature Flag: ".$this->name); if (isset($split['conditions']) && is_array($split['conditions'])) { - $this->conditions = array(); - foreach ($split['conditions'] as $condition) { - $this->conditions[] = new Condition($condition); + try { + $this->conditions = array(); + foreach ($split['conditions'] as $condition) { + $this->conditions[] = new Condition($condition); + } + } catch (\Exception $e) { + SplitApp::logger()->debug($e->getMessage()); + $this->conditions = array(Condition::getDefaultCondition()); } } } diff --git a/src/SplitIO/Sdk/Impressions/ImpressionLabel.php b/src/SplitIO/Sdk/Impressions/ImpressionLabel.php index 1bf7c2a3..0aff7d73 100644 --- a/src/SplitIO/Sdk/Impressions/ImpressionLabel.php +++ b/src/SplitIO/Sdk/Impressions/ImpressionLabel.php @@ -48,4 +48,11 @@ class ImpressionLabel * Label: exception */ const EXCEPTION = "exception"; + + /** + * Condition: unsupported matcher + * Treatment: control + * Label: targeting rule type unsupported by sdk + */ + const UNSUPPORTED_MATCHER = "targeting rule type unsupported by sdk"; } diff --git a/src/SplitIO/Version.php b/src/SplitIO/Version.php index fdd63142..6637d136 100644 --- a/src/SplitIO/Version.php +++ b/src/SplitIO/Version.php @@ -3,5 +3,5 @@ class Version { - const CURRENT = '7.2.1'; + const CURRENT = '7.3.0'; } diff --git a/tests/Suite/Matchers/MatchersTest.php b/tests/Suite/Matchers/MatchersTest.php index f0a1c57a..9989ffc5 100644 --- a/tests/Suite/Matchers/MatchersTest.php +++ b/tests/Suite/Matchers/MatchersTest.php @@ -1,18 +1,13 @@ assertEquals($meth->invoke($matcher2, 'ff'), false); } + public function testUnsupportedMatcher() + { + $condition = array( + 'matcherType' => 'NEW_MATCHER_TYPE', + ); + + $this->expectException('\SplitIO\Exception\UnsupportedMatcherException'); + Matcher::factory($condition); + } + + public function testEqualToSemverMatcher() + { + $this->setupSplitApp(); + + $condition = array( + 'matcherType' => 'EQUAL_TO_SEMVER', + 'stringMatcherData' => '2.2.2' + ); + + $matcher = Matcher::factory($condition); + $this->assertTrue($matcher->evaluate('2.2.2')); + $this->assertFalse($matcher->evaluate('1.1.1')); + $this->assertFalse($matcher->evaluate(null)); + $this->assertFalse($matcher->evaluate('')); + + $condition = array( + 'matcherType' => 'EQUAL_TO_SEMVER', + 'stringMatcherData' => null + ); + + $matcher = Matcher::factory($condition); + $this->assertFalse($matcher->evaluate('2.2.2')); + } + + public function testGreaterThanOrEqualToSemverMatcher() + { + $this->setupSplitApp(); + + $condition = array( + 'matcherType' => 'GREATER_THAN_OR_EQUAL_TO_SEMVER', + 'stringMatcherData' => '2.2.2' + ); + + $matcher = Matcher::factory($condition); + $this->assertTrue($matcher->evaluate('2.2.2')); + $this->assertTrue($matcher->evaluate('2.2.3')); + $this->assertTrue($matcher->evaluate('2.3.2')); + $this->assertTrue($matcher->evaluate('3.2.2')); + $this->assertFalse($matcher->evaluate('1.2.2')); + $this->assertFalse($matcher->evaluate('2.1.2')); + $this->assertFalse($matcher->evaluate('2.2.2-rc.1')); + $this->assertFalse($matcher->evaluate(null)); + $this->assertFalse($matcher->evaluate('')); + + $condition = array( + 'matcherType' => 'GREATER_THAN_OR_EQUAL_TO_SEMVER', + 'stringMatcherData' => null + ); + + $matcher = Matcher::factory($condition); + $this->assertFalse($matcher->evaluate('2.2.2')); + } + + public function testLessThanOrEqualToSemverMatcher() + { + $this->setupSplitApp(); + + $condition = array( + 'matcherType' => 'LESS_THAN_OR_EQUAL_TO_SEMVER', + 'stringMatcherData' => '2.2.2' + ); + + $matcher = Matcher::factory($condition); + $this->assertTrue($matcher->evaluate('1.2.2')); + $this->assertTrue($matcher->evaluate('2.1.2')); + $this->assertTrue($matcher->evaluate('2.2.2-rc.1')); + $this->assertTrue($matcher->evaluate('0.2.2')); + $this->assertTrue($matcher->evaluate('2.2.1')); + $this->assertTrue($matcher->evaluate('2.2.2')); + $this->assertFalse($matcher->evaluate('2.2.3')); + $this->assertFalse($matcher->evaluate('2.3.2')); + $this->assertFalse($matcher->evaluate('3.2.2')); + $this->assertFalse($matcher->evaluate(null)); + $this->assertFalse($matcher->evaluate('')); + + $condition = array( + 'matcherType' => 'GREATER_THAN_OR_EQUAL_TO_SEMVER', + 'stringMatcherData' => null + ); + + $matcher = Matcher::factory($condition); + $this->assertFalse($matcher->evaluate('2.2.2')); + } + + public function testBetweenSemverMatcher() + { + $this->setupSplitApp(); + + $condition = array( + 'matcherType' => 'BETWEEN_SEMVER', + 'betweenStringMatcherData' => array( + 'start' => '11.11.11', + 'end' => '22.22.22', + ) + ); + + $matcher = Matcher::factory($condition); + $this->assertTrue($matcher->evaluate('20.2.2')); + $this->assertTrue($matcher->evaluate('16.5.6')); + $this->assertTrue($matcher->evaluate('19.0.1')); + $this->assertFalse($matcher->evaluate(null)); + $this->assertFalse($matcher->evaluate('')); + $this->assertFalse($matcher->evaluate('22.22.25')); + $this->assertFalse($matcher->evaluate('10.0.0')); + + $condition = array( + 'matcherType' => 'BETWEEN_SEMVER', + 'betweenStringMatcherData' => null + ); + + $matcher = Matcher::factory($condition); + $this->assertFalse($matcher->evaluate('2.2.2')); + } + + public function testInListSemverMatcher() + { + $this->setupSplitApp(); + + $condition = array( + 'matcherType' => 'IN_LIST_SEMVER', + 'whitelistMatcherData' => array( + 'whitelist' => array( + '1.1.1', + '2.2.2', + '3.3.3', + ) + ) + ); + + $matcher = Matcher::factory($condition); + $this->assertTrue($matcher->evaluate('1.1.1')); + $this->assertTrue($matcher->evaluate('2.2.2')); + $this->assertTrue($matcher->evaluate('3.3.3')); + $this->assertFalse($matcher->evaluate(null)); + $this->assertFalse($matcher->evaluate('')); + $this->assertFalse($matcher->evaluate('4.22.25')); + $this->assertFalse($matcher->evaluate('10.0.0')); + + $condition = array( + 'matcherType' => 'IN_LIST_SEMVER', + 'whitelistMatcherData' => null + ); + + $matcher = Matcher::factory($condition); + $this->assertFalse($matcher->evaluate('2.2.2')); + } + public static function tearDownAfterClass(): void { Utils\Utils::cleanCache(); diff --git a/tests/Suite/Sdk/SdkClientTest.php b/tests/Suite/Sdk/SdkClientTest.php index 07407920..6bba6ac9 100644 --- a/tests/Suite/Sdk/SdkClientTest.php +++ b/tests/Suite/Sdk/SdkClientTest.php @@ -191,7 +191,8 @@ private function validateLastImpression( $key, $treatment, $machineName = 'unknown', - $machineIP = 'unknown' + $machineIP = 'unknown', + $label = '' ) { $raw = $redisClient->rpop(ImpressionCache::IMPRESSIONS_QUEUE_KEY); $parsed = json_decode($raw, true); @@ -200,6 +201,10 @@ private function validateLastImpression( $this->assertEquals($parsed['i']['t'], $treatment); $this->assertEquals($parsed['m']['i'], $machineIP); $this->assertEquals($parsed['m']['n'], $machineName); + + if ($label != '') { + $this->assertEquals($parsed['i']['r'], $label); + } } public function testSplitManager() @@ -236,6 +241,135 @@ public function testSplitManager() $this->assertEquals('["set_a","set_b","set_c"]', json_encode($split_views["flagsets_feature"]->getSets())); } + public function testClientSemverMatchers() + { + Di::set(Di::KEY_FACTORY_TRACKER, false); + //Testing version string + $this->assertTrue(is_string(\SplitIO\version())); + + $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")); + + $redisClient = ReflectiveTools::clientFromCachePool(Di::getCache()); + + //Assertions EqualToSemver + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'equal_to_semver_flag', array('version' => '34.56.89-rc.1+meta'))); + $this->validateLastImpression($redisClient, 'equal_to_semver_flag', 'user1', 'v1'); + $this->assertEquals('off', $splitSdk->getTreatment('user2', 'equal_to_semver_flag', array('version' => '34.56.89'))); + $this->validateLastImpression($redisClient, 'equal_to_semver_flag', 'user2', 'off'); + + //Assertions GreaterThanOrEqualToSemver + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'gtoet_semver_flag', array('version' => '34.56.89-rc.12.2.3.4+meta'))); + $this->validateLastImpression($redisClient, 'gtoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'gtoet_semver_flag', array('version' => '34.56.89-rc.12.3.3.4+meta'))); + $this->validateLastImpression($redisClient, 'gtoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'gtoet_semver_flag', array('version' => '34.56.89-rc.12.2.1.4+meta'))); + $this->validateLastImpression($redisClient, 'gtoet_semver_flag', 'user1', 'off'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'gtoet_semver_flag', array('version' => '34.56.89'))); + $this->validateLastImpression($redisClient, 'gtoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'gtoet_semver_flag', array('version' => '34.57.89'))); + $this->validateLastImpression($redisClient, 'gtoet_semver_flag', 'user1', 'v1'); + + //Assertions LessThanOrEqualToSemver + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'ltoet_semver_flag', array('version' => '11.22.33'))); + $this->validateLastImpression($redisClient, 'ltoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'ltoet_semver_flag', array('version' => '11.1.33'))); + $this->validateLastImpression($redisClient, 'ltoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'ltoet_semver_flag', array('version' => '11.1.3'))); + $this->validateLastImpression($redisClient, 'ltoet_semver_flag', 'user1', 'v1'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'ltoet_semver_flag', array('version' => '11.22.34'))); + $this->validateLastImpression($redisClient, 'ltoet_semver_flag', 'user1', 'off'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'ltoet_semver_flag', array('version' => '11.22.33-rc.1'))); + $this->validateLastImpression($redisClient, 'ltoet_semver_flag', 'user1', 'v1'); + + //Assertions BetweenSemver + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '6.9.0'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '8.0.0-rc.22'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '9.0.0+metadata'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '10.4.4-rc.1+metadata'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'v1'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '10.4.7-rc.1+metadata'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'off'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'between_semver_flag', array('version' => '1.4.7'))); + $this->validateLastImpression($redisClient, 'between_semver_flag', 'user1', 'off'); + + //Assertions InListSemver + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '6.7.8'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '1.1.1-alpha'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'v1'); + $this->assertEquals('v1', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '2.2.2+meta'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'v1'); + + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '2.2.2'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'off'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '1.1.1'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'off'); + $this->assertEquals('off', $splitSdk->getTreatment('user1', 'inlist_semver_flag', array('version' => '8.6.0-rc.2'))); + $this->validateLastImpression($redisClient, 'inlist_semver_flag', 'user1', 'off'); + + // Assertions UnSupported + $this->assertEquals('control', $splitSdk->getTreatment('user1', 'semver_demo_test', array('version' => '2.2.2'))); + $this->validateLastImpression($redisClient, 'semver_demo_test', 'user1', 'control', 'unknown', 'unknown', 'targeting rule type unsupported by sdk'); + } + + public function testClientWithUnsupportedMatcher() + { + Di::set(Di::KEY_FACTORY_TRACKER, false); + //Testing version string + $this->assertTrue(is_string(\SplitIO\version())); + + $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")); + + $redisClient = ReflectiveTools::clientFromCachePool(Di::getCache()); + + //Assertions + $this->assertEquals('control', $splitSdk->getTreatment('user1', 'unsupported_matcher')); + $this->validateLastImpression($redisClient, 'unsupported_matcher', 'user1', 'control', 'unknown', 'unknown', 'targeting rule type unsupported by sdk'); + } + public function testClient() { Di::set(Di::KEY_FACTORY_TRACKER, false); diff --git a/tests/Suite/Sdk/files/splitChanges.json b/tests/Suite/Sdk/files/splitChanges.json index 59e8a287..e6f7cd15 100644 --- a/tests/Suite/Sdk/files/splitChanges.json +++ b/tests/Suite/Sdk/files/splitChanges.json @@ -438,6 +438,354 @@ ] } ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "unsupported_matcher", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + }, + { + "treatment": "off", + "size": 0 + } + ] + }, + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "matcherType": "WRONG_MATCHER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "whitelisted_user" + ] + } + } + ] + }, + "partitions": [ + { + "treatment": "on", + "size": 100 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "equal_to_semver_flag", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "stringMatcherData": "34.56.89-rc.1+meta" + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "gtoet_semver_flag", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "GREATER_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "stringMatcherData": "34.56.89-rc.12.2.3.4+meta" + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "ltoet_semver_flag", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "LESS_THAN_OR_EQUAL_TO_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "stringMatcherData": "11.22.33" + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "between_semver_flag", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "BETWEEN_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "betweenStringMatcherData": { + "start": "5.0.0", + "end": "10.4.6" + } + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ] + } + ] + }, + { + "orgId": null, + "environment": null, + "trafficTypeId": null, + "trafficTypeName": null, + "name": "inlist_semver_flag", + "seed": -1222652054, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "sets": [], + "configurations": { + "on": "{\"size\":15,\"test\":20}", + "of": "{\"size\":15,\"defTreatment\":true}" + }, + "conditions": [ + { + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": { + "trafficType": "user", + "attribute": "version" + }, + "matcherType": "IN_LIST_SEMVER", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": { + "whitelist": [ + "6.7.8", + "2.2.2+meta", + "1.1.1-alpha", + "8.6.0-rc.1", + "9.6.0-beta.1" + ] + } + } + ] + }, + "partitions": [ + { + "treatment": "v1", + "size": 100 + }, + { + "treatment": "v2", + "size": 0 + } + ] + } + ] + }, + { + "changeNumber": 1715263640336, + "trafficTypeName": "user", + "name": "semver_demo_test", + "trafficAllocation": 100, + "trafficAllocationSeed": -109105559, + "seed": 1836161355, + "status": "ACTIVE", + "killed": false, + "defaultTreatment": "off", + "algo": 2, + "conditions": [ + { + "conditionType": "WHITELIST", + "matcherGroup": { + "combiner": "AND", + "matchers": [ + { + "keySelector": null, + "matcherType": "ALL_KEYS", + "negate": false, + "userDefinedSegmentMatcherData": null, + "whitelistMatcherData": null, + "unaryNumericMatcherData": null, + "betweenMatcherData": null, + "dependencyMatcherData": null, + "booleanMatcherData": null, + "stringMatcherData": null, + "betweenStringMatcherData": null + } + ] + }, + "partitions": [ + { + "treatment": "control", + "size": 100 + } + ], + "label": "targeting rule type unsupported by sdk" + } + ], + "configurations": {}, + "sets": [] } ], "since": -1, diff --git a/tests/Suite/Semver/ComparerTests.php b/tests/Suite/Semver/ComparerTests.php new file mode 100644 index 00000000..93574f61 --- /dev/null +++ b/tests/Suite/Semver/ComparerTests.php @@ -0,0 +1,109 @@ +assertTrue(SemverComparer::do($semver1, $semver2) >= 0); + $this->assertTrue(SemverComparer::do($semver1, $semver1) >= 0); + $this->assertTrue(SemverComparer::do($semver2, $semver2) >= 0); + $this->assertFalse(SemverComparer::do($semver2, $semver1) >= 0); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } + + public function testLessThanOrEqualTo() + { + $handle = fopen(__DIR__."/../../files/valid-semantic-versions.csv", "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $_line = explode(',', $line); + + $v1 = str_replace("\n","",$_line[0]); + $v2 = str_replace("\n","",$_line[1]); + + $semver1 = Semver::build($v1); + $semver2 = Semver::build($v2); + + $this->assertFalse(SemverComparer::do($semver1, $semver2) <= 0); + $this->assertTrue(SemverComparer::do($semver2, $semver1) <= 0); + $this->assertTrue(SemverComparer::do($semver1, $semver1) <= 0); + $this->assertTrue(SemverComparer::do($semver2, $semver2) <= 0); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } + + public function testEquals() + { + $handle = fopen(__DIR__."/../../files/equal-to-semver.csv", "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $_line = explode(',', $line); + + $c1 = str_replace("\n","",$_line[0]); + $c2 = str_replace("\n","",$_line[1]); + $c3 = str_replace("\n","",$_line[2]); + + $semver1 = Semver::build($c1); + $semver2 = Semver::build($c2); + $expected = (bool) $c3; + + $this->assertEquals($expected, SemverComparer::equals($semver1, $semver2), $semver1->getVersion() . " - " . $semver2->getVersion() . " | " . $expected); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } + + public function testBetween() + { + $handle = fopen(__DIR__."/../../files/between-semver.csv", "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $_line = explode(',', $line); + + $c1 = str_replace("\n","",$_line[0]); + $c2 = str_replace("\n","",$_line[1]); + $c3 = str_replace("\n","",$_line[2]); + $c4 = str_replace("\n","",$_line[3]); + + $semver1 = Semver::build($c1); + $semver2 = Semver::build($c2); + $semver3 = Semver::build($c3); + + $result = SemverComparer::do($semver2, $semver1) >= 0 && SemverComparer::do($semver2, $semver3) <= 0; + + $this->assertEquals((bool) $c4, $result); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } +} \ No newline at end of file diff --git a/tests/Suite/Semver/SemverTests.php b/tests/Suite/Semver/SemverTests.php new file mode 100644 index 00000000..e536d66e --- /dev/null +++ b/tests/Suite/Semver/SemverTests.php @@ -0,0 +1,77 @@ + 'redis', + 'host' => "localhost", + 'port' => 6379, + 'timeout' => 881 + ); + $options = array('prefix' => TEST_PREFIX); + $sdkConfig = array( + 'log' => array('adapter' => 'stdout', 'level' => 'info'), + 'cache' => array('adapter' => 'predis', + 'parameters' => $parameters, + 'options' => $options + ) + ); + + $splitFactory = \SplitIO\Sdk::factory('apikey', $sdkConfig); + $splitFactory->client(); + } + + public function testValidSemver() + { + $this->setupSplitApp(); + + $handle = fopen(__DIR__."/../../files/valid-semantic-versions.csv", "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $_line = explode(',', $line); + + $v1 = str_replace("\n","",$_line[0]); + $v2 = str_replace("\n","",$_line[1]); + + $semver1 = Semver::build($v1); + $semver2 = Semver::build($v2); + + $this->assertNotNull($semver1, $v1); + $this->assertNotNull($semver2, $v2); + $this->assertEquals($v1, $semver1->getVersion()); + $this->assertEquals($v2, $semver2->getVersion()); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } + + public function testInvalidSemver() + { + $this->setupSplitApp(); + + $handle = fopen(__DIR__."/../../files/invalid-semantic-versions.csv", "r"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + $_line = explode(',', $line); + + $v1 = str_replace("\n","",$_line[0]); + + $this->assertNull(Semver::build($v1)); + } + + fclose($handle); + } else { + $this->assertTrue(false, "Sample Data not found"); + } + } +} \ No newline at end of file diff --git a/tests/files/between-semver.csv b/tests/files/between-semver.csv new file mode 100644 index 00000000..67bfebf5 --- /dev/null +++ b/tests/files/between-semver.csv @@ -0,0 +1,17 @@ +1.1.1,2.2.2,3.3.3,1 +1.1.1-rc.1,1.1.1-rc.2,1.1.1-rc.3,1 +1.0.0-alpha,1.0.0-alpha.1,1.0.0-alpha.beta,1 +1.0.0-alpha.1,1.0.0-alpha.beta,1.0.0-beta,1 +1.0.0-alpha.beta,1.0.0-beta,1.0.0-beta.2,1 +1.0.0-beta,1.0.0-beta.2,1.0.0-beta.11,1 +1.0.0-beta.2,1.0.0-beta.11,1.0.0-rc.1,1 +1.0.0-beta.11,1.0.0-rc.1,1.0.0,1 +1.1.2,1.1.3,1.1.4,1 +1.2.1,1.3.1,1.4.1,1 +2.0.0,3.0.0,4.0.0,1 +2.2.2,2.2.3-rc1,2.2.3,1 +2.2.2,2.3.2-rc100,2.3.3,1 +1.0.0-rc.1+build.1,1.2.3-beta,1.2.3-rc.1+build.123,1 +3.3.3,3.3.3-alpha,3.3.4,0 +2.2.2-rc.1,2.2.2+metadata,2.2.2-rc.10,0 +1.1.1-rc.1,1.1.1-rc.3,1.1.1-rc.2,0 diff --git a/tests/files/equal-to-semver.csv b/tests/files/equal-to-semver.csv new file mode 100644 index 00000000..90c1f4d0 --- /dev/null +++ b/tests/files/equal-to-semver.csv @@ -0,0 +1,8 @@ +1.1.1,1.1.1,1 +2.2.2,2.2.2+metadata,0 +1.1.1,1.1.1-rc.1,0 +88.88.88,88.88.88,1 +1.2.3----RC-SNAPSHOT.12.9.1--.12,1.2.3----RC-SNAPSHOT.12.9.1--.12,1 +10.2.3-DEV-SNAPSHOT,10.2.3-SNAPSHOT-123,0 +00.01.002-0003.00004,0.1.2-3.4,1 +00.01.002-0003.0000z,0.1.2-3.z,0 \ No newline at end of file diff --git a/tests/files/invalid-semantic-versions.csv b/tests/files/invalid-semantic-versions.csv new file mode 100644 index 00000000..f4eb1932 --- /dev/null +++ b/tests/files/invalid-semantic-versions.csv @@ -0,0 +1,26 @@ +1.alpha.2 +1.2 +1 ++invalid +-invalid +-invalid+invalid +-invalid.01 +alpha +alpha.beta +alpha.beta.1 +alpha.1 +alpha+beta +alpha_beta +alpha. +alpha.. +beta +-alpha. +1.2.3.DEV +1.2-SNAPSHOT +1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788 +1.2-RC-SNAPSHOT +-1.0.3-gamma+b7718 ++justmeta +99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12 +1.1.1+ +1.1.1- \ No newline at end of file diff --git a/tests/files/valid-semantic-versions.csv b/tests/files/valid-semantic-versions.csv new file mode 100644 index 00000000..65c43e0c --- /dev/null +++ b/tests/files/valid-semantic-versions.csv @@ -0,0 +1,20 @@ +1.0.0,1.0.0-rc.1 +1.1.2,1.1.1 +1.1.0-rc.1,1.0.0-beta.11 +1.0.0-beta.11,1.0.0-beta.2 +1.0.0-beta.2,1.0.0-beta +1.0.0-beta,1.0.0-alpha.beta +1.0.0-alpha.beta,1.0.0-alpha.1 +1.0.0-alpha.1,1.0.0-alpha +2.2.2-rc.2+metadata-lalala,2.2.2-rc.1.2 +1.2.3,0.0.4 +1.1.2+meta,1.1.2-prerelease+meta +1.0.0-beta,1.0.0-alpha +1.0.0-alpha0.valid,1.0.0-alpha.0valid +1.0.0-rc.1+build.1,1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay +10.2.3-DEV-SNAPSHOT,1.2.3-SNAPSHOT-123 +1.1.1-rc2,1.0.0-0A.is.legal +1.2.3----RC-SNAPSHOT.12.9.1--.12+788,1.2.3----R-S.12.9.1--.12+meta +1.2.3----RC-SNAPSHOT.12.9.1--.12.88,1.2.3----RC-SNAPSHOT.12.9.1--.12 +1.1.1-alpha.beta.rc.build.java.pr.support.10,1.1.1-alpha.beta.rc.build.java.pr.support +9223372036854775807.9223372036854775807.9223372036854775807,9223372036854775807.9223372036854775807.9223372036854775806 \ No newline at end of file