diff --git a/src/LaunchDarkly/EvaluationReason.php b/src/LaunchDarkly/EvaluationReason.php index 0e76f164..51f00fe7 100644 --- a/src/LaunchDarkly/EvaluationReason.php +++ b/src/LaunchDarkly/EvaluationReason.php @@ -100,9 +100,9 @@ public static function off() * Creates a new instance of the FALLTHROUGH reason. * @return EvaluationReason */ - public static function fallthrough() + public static function fallthrough($inExperiment = false) { - return new EvaluationReason(self::FALLTHROUGH); + return new EvaluationReason(self::FALLTHROUGH, null, null, null, null, $inExperiment); } /** @@ -118,9 +118,9 @@ public static function targetMatch() * Creates a new instance of the RULE_MATCH reason. * @return EvaluationReason */ - public static function ruleMatch($ruleIndex, $ruleId) + public static function ruleMatch($ruleIndex, $ruleId, $inExperiment = false) { - return new EvaluationReason(self::RULE_MATCH, null, $ruleIndex, $ruleId); + return new EvaluationReason(self::RULE_MATCH, null, $ruleIndex, $ruleId, null, $inExperiment); } /** @@ -141,13 +141,14 @@ public static function error($errorKind) return new EvaluationReason(self::ERROR, $errorKind); } - private function __construct($kind, $errorKind = null, $ruleIndex = null, $ruleId = null, $prerequisiteKey = null) + private function __construct($kind, $errorKind = null, $ruleIndex = null, $ruleId = null, $prerequisiteKey = null, $inExperiment = null) { $this->_kind = $kind; $this->_errorKind = $errorKind; $this->_ruleIndex = $ruleIndex; $this->_ruleId = $ruleId; $this->_prerequisiteKey = $prerequisiteKey; + $this->_inExperiment = $inExperiment; } /** @@ -199,6 +200,16 @@ public function getPrerequisiteKey() return $this->_prerequisiteKey; } + /** + * Returns true if the evaluation resulted in an experiment rollout *and* served + * one of the variations in the experiment. Otherwise it returns false. + * @return boolean + */ + public function isInExperiment() + { + return !is_null($this->_inExperiment) && $this->_inExperiment; + } + /** * Returns a simple string representation of this object. */ @@ -235,6 +246,9 @@ public function jsonSerialize() if ($this->_prerequisiteKey !== null) { $ret['prerequisiteKey'] = $this->_prerequisiteKey; } + if ($this->_inExperiment !== null && $this->_inExperiment) { + $ret['inExperiment'] = $this->_inExperiment; + } return $ret; } } diff --git a/src/LaunchDarkly/FeatureFlag.php b/src/LaunchDarkly/FeatureFlag.php index 2a65d21f..3d0e97e0 100644 --- a/src/LaunchDarkly/FeatureFlag.php +++ b/src/LaunchDarkly/FeatureFlag.php @@ -236,10 +236,18 @@ private function getOffValue($reason) */ private function getValueForVariationOrRollout($r, $user, $reason) { - $index = $r->variationIndexForUser($user, $this->_key, $this->_salt); + $seed = $r->getRollout() ? $r->getRollout()->getSeed() : null; + list($index, $inExperiment) = $r->variationIndexForUser($user, $this->_key, $this->_salt, $seed); if ($index === null) { return new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); } + if ($inExperiment) { + if ($reason->getKind() === EvaluationReason::FALLTHROUGH) { + $reason = EvaluationReason::fallthrough(true); + } elseif ($reason->getKind() === EvaluationReason::RULE_MATCH) { + $reason = EvaluationReason::ruleMatch($reason->getRuleIndex(), $reason->getRuleId(), true); + } + } return $this->getVariation($index, $reason); } diff --git a/src/LaunchDarkly/Impl/EventFactory.php b/src/LaunchDarkly/Impl/EventFactory.php index 4a3651f7..a3cc5716 100644 --- a/src/LaunchDarkly/Impl/EventFactory.php +++ b/src/LaunchDarkly/Impl/EventFactory.php @@ -148,6 +148,9 @@ private static function contextKind($user) private static function isExperiment($flag, $reason) { if ($reason) { + if ($reason->isInExperiment()) { + return true; + } switch ($reason->getKind()) { case 'RULE_MATCH': $i = $reason->getRuleIndex(); diff --git a/src/LaunchDarkly/Rollout.php b/src/LaunchDarkly/Rollout.php index 30f08def..10d8ef52 100644 --- a/src/LaunchDarkly/Rollout.php +++ b/src/LaunchDarkly/Rollout.php @@ -11,15 +11,23 @@ */ class Rollout { + const KIND_EXPERIMENT = 'experiment'; + /** @var WeightedVariation[] */ private $_variations = array(); /** @var string */ private $_bucketBy = null; + /** @var string */ + private $_kind = null; + /** @var int|null */ + private $_seed = null; - protected function __construct(array $variations, $bucketBy) + protected function __construct(array $variations, $bucketBy, $kind = null, $seed = null) { $this->_variations = $variations; $this->_bucketBy = $bucketBy; + $this->_kind = $kind; + $this->_seed = $seed; } public static function getDecoder() @@ -27,7 +35,10 @@ public static function getDecoder() return function ($v) { return new Rollout( array_map(WeightedVariation::getDecoder(), $v['variations']), - isset($v['bucketBy']) ? $v['bucketBy'] : null); + isset($v['bucketBy']) ? $v['bucketBy'] : null, + isset($v['kind']) ? $v['kind'] : null, + isset($v['seed']) ? $v['seed'] : null + ); }; } @@ -46,4 +57,20 @@ public function getBucketBy() { return $this->_bucketBy; } + + /** + * @return int|null + */ + public function getSeed() + { + return $this->_seed; + } + + /** + * @return boolean + */ + public function isExperiment() + { + return $this->_kind === self::KIND_EXPERIMENT; + } } diff --git a/src/LaunchDarkly/SegmentRule.php b/src/LaunchDarkly/SegmentRule.php index adc06117..0d692fc6 100644 --- a/src/LaunchDarkly/SegmentRule.php +++ b/src/LaunchDarkly/SegmentRule.php @@ -48,7 +48,7 @@ public function matchesUser($user, $segmentKey, $segmentSalt) } // All of the clauses are met. See if the user buckets in $bucketBy = ($this->_bucketBy === null) ? "key" : $this->_bucketBy; - $bucket = VariationOrRollout::bucketUser($user, $segmentKey, $bucketBy, $segmentSalt); + $bucket = VariationOrRollout::bucketUser($user, $segmentKey, $bucketBy, $segmentSalt, null); $weight = $this->_weight / 100000.0; return $bucket < $weight; } diff --git a/src/LaunchDarkly/VariationOrRollout.php b/src/LaunchDarkly/VariationOrRollout.php index 60b7524e..05146122 100644 --- a/src/LaunchDarkly/VariationOrRollout.php +++ b/src/LaunchDarkly/VariationOrRollout.php @@ -54,31 +54,32 @@ public function getRollout() * @param $user LDUser * @param $_key string * @param $_salt string - * @return int|null + * @return array(int|null, boolean) */ public function variationIndexForUser($user, $_key, $_salt) { if ($this->_variation !== null) { - return $this->_variation; + return array($this->_variation, false); } $rollout = $this->_rollout; if ($rollout === null) { - return null; + return array(null, false); } $variations = $rollout->getVariations(); if ($variations) { $bucketBy = $this->_rollout->getBucketBy() === null ? "key" : $this->_rollout->getBucketBy(); - $bucket = self::bucketUser($user, $_key, $bucketBy, $_salt); + $bucket = self::bucketUser($user, $_key, $bucketBy, $_salt, $rollout->getSeed()); $sum = 0.0; foreach ($variations as $wv) { $sum += $wv->getWeight() / 100000.0; if ($bucket < $sum) { - return $wv->getVariation(); + return array($wv->getVariation(), $this->_rollout->isExperiment() && !$wv->isUntracked()); } } - return $variations[count($variations) - 1]->getVariation(); + $lastVariation = $variations[count($variations) - 1]; + return array($lastVariation->getVariation(), $this->_rollout->isExperiment() && !$lastVariation->isUntracked()); } - return null; + return array(null, false); } /** @@ -86,9 +87,10 @@ public function variationIndexForUser($user, $_key, $_salt) * @param $_key string * @param $attr string * @param $_salt string + * @param $seed int|null * @return float */ - public static function bucketUser($user, $_key, $attr, $_salt) + public static function bucketUser($user, $_key, $attr, $_salt, $seed) { $userValue = $user->getValueForEvaluation($attr); $idHash = null; @@ -98,10 +100,15 @@ public static function bucketUser($user, $_key, $attr, $_salt) } if (is_string($userValue)) { $idHash = $userValue; + if (isset($seed)) { + $prefix = (string) $seed; + } else { + $prefix = $_key . "." . $_salt; + } if ($user->getSecondary() !== null) { $idHash = $idHash . "." . strval($user->getSecondary()); } - $hash = substr(sha1($_key . "." . $_salt . "." . $idHash), 0, 15); + $hash = substr(sha1($prefix . "." . $idHash), 0, 15); $longVal = base_convert($hash, 16, 10); $result = $longVal / self::$LONG_SCALE; diff --git a/src/LaunchDarkly/WeightedVariation.php b/src/LaunchDarkly/WeightedVariation.php index b4df7ee0..3efa07c8 100644 --- a/src/LaunchDarkly/WeightedVariation.php +++ b/src/LaunchDarkly/WeightedVariation.php @@ -15,17 +15,24 @@ class WeightedVariation private $_variation = null; /** @var int */ private $_weight = null; + /** @var boolean */ + private $_untracked = false; - private function __construct($variation, $weight) + private function __construct($variation, $weight, $untracked) { $this->_variation = $variation; $this->_weight = $weight; + $this->_untracked = $untracked; } public static function getDecoder() { return function ($v) { - return new WeightedVariation($v['variation'], $v['weight']); + return new WeightedVariation( + $v['variation'], + $v['weight'], + isset($v['untracked']) ? $v['untracked'] : false + ); }; } @@ -44,4 +51,12 @@ public function getWeight() { return $this->_weight; } + + /** + * @return boolean + */ + public function isUntracked() + { + return $this->_untracked; + } } diff --git a/tests/EvaluationReasonTest.php b/tests/EvaluationReasonTest.php index 759dc3ec..3d53d4dc 100644 --- a/tests/EvaluationReasonTest.php +++ b/tests/EvaluationReasonTest.php @@ -21,6 +21,22 @@ public function testFallthroughReasonSerialization() $this->assertEquals('FALLTHROUGH', (string)$reason); } + public function testFallthroughReasonNotInExperimentSerialization() + { + $reason = EvaluationReason::fallthrough(false); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'FALLTHROUGH'), json_decode($json, true)); + $this->assertEquals('FALLTHROUGH', (string)$reason); + } + + public function testFallthroughReasonInExperimentSerialization() + { + $reason = EvaluationReason::fallthrough(true); + $json = json_encode($reason); + $this->assertEquals(array('kind' => 'FALLTHROUGH', 'inExperiment' => true), json_decode($json, true)); + $this->assertEquals('FALLTHROUGH', (string)$reason); + } + public function testTargetMatchReasonSerialization() { $reason = EvaluationReason::targetMatch(); @@ -33,8 +49,32 @@ public function testRuleMatchReasonSerialization() { $reason = EvaluationReason::ruleMatch(0, 'id'); $json = json_encode($reason); - $this->assertEquals(array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'), - json_decode($json, true)); + $this->assertEquals( + array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'), + json_decode($json, true) + ); + $this->assertEquals('RULE_MATCH(0,id)', (string)$reason); + } + + public function testRuleMatchReasonNotInExperimentSerialization() + { + $reason = EvaluationReason::ruleMatch(0, 'id', false); + $json = json_encode($reason); + $this->assertEquals( + array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id'), + json_decode($json, true) + ); + $this->assertEquals('RULE_MATCH(0,id)', (string)$reason); + } + + public function testRuleMatchReasonInExperimentSerialization() + { + $reason = EvaluationReason::ruleMatch(0, 'id', true); + $json = json_encode($reason); + $this->assertEquals( + array('kind' => 'RULE_MATCH', 'ruleIndex' => 0, 'ruleId' => 'id', 'inExperiment' => true), + json_decode($json, true) + ); $this->assertEquals('RULE_MATCH(0,id)', (string)$reason); } @@ -42,8 +82,10 @@ public function testPrerequisiteFailedReasonSerialization() { $reason = EvaluationReason::prerequisiteFailed('key'); $json = json_encode($reason); - $this->assertEquals(array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'), - json_decode($json, true)); + $this->assertEquals( + array('kind' => 'PREREQUISITE_FAILED', 'prerequisiteKey' => 'key'), + json_decode($json, true) + ); $this->assertEquals('PREREQUISITE_FAILED(key)', (string)$reason); } diff --git a/tests/EventFactoryTest.php b/tests/EventFactoryTest.php new file mode 100644 index 00000000..7c99dace --- /dev/null +++ b/tests/EventFactoryTest.php @@ -0,0 +1,107 @@ + array( + 'variations' => array( + array('variation' => 0, 'weight' => 10000, 'untracked' => false), + array('variation' => 1, 'weight' => 20000, 'untracked' => false), + array('variation' => 0, 'weight' => 70000, 'untracked' => true) + ), + 'kind' => 'experiment', + // seed here carefully chosen so users fall into different variations + 'seed' => 61 + ), + 'clauses' => [] + + ); + + $flag = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => $vr, + 'variations' => array('fall', 'off', 'on'), + 'salt' => 'saltyA', + 'trackEvents' => $trackEvents + ); + $decodedFlag = call_user_func(FeatureFlag::getDecoder(), $flag); + + return $decodedFlag; + } + + public function testTrackEventFalse() + { + $ef = new EventFactory(false); + + $flag = $this->buildFlag(false); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $detail = new EvaluationDetail('off', 1, EvaluationReason::fallthrough()); + + $result = $ef->newEvalEvent($flag, $user, $detail, null); + + $this->assertFalse(isset($result['trackEvents'])); + } + + public function testTrackEventTrue() + { + $ef = new EventFactory(false); + + $flag = $this->buildFlag(true); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $detail = new EvaluationDetail('off', 1, EvaluationReason::fallthrough()); + + $result = $ef->newEvalEvent($flag, $user, $detail, null); + + $this->assertTrue($result['trackEvents']); + } + + public function testTrackEventTrueWhenTrackEventsFalseButExperimentFallthroughReason() + { + $ef = new EventFactory(false); + + $flag = $this->buildFlag(false); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $detail = new EvaluationDetail('off', 1, EvaluationReason::fallthrough(true)); + + $result = $ef->newEvalEvent($flag, $user, $detail, null); + + $this->assertTrue($result['trackEvents']); + } + + public function testTrackEventTrueWhenTrackEventsFalseButExperimentRuleMatchReason() + { + $ef = new EventFactory(false); + + $flag = $this->buildFlag(false); + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + + $detail = new EvaluationDetail('off', 1, EvaluationReason::ruleMatch(1, 'something', true)); + + $result = $ef->newEvalEvent($flag, $user, $detail, null); + + $this->assertTrue($result['trackEvents']); + } +} diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 28cd7e41..87342824 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -620,7 +620,7 @@ public function testRolloutSelectsBucket() // First verify that with our test inputs, the bucket value will be greater than zero and less than 100000, // so we can construct a rollout whose second bucket just barely contains that value - $bucketValue = floor(VariationOrRollout::bucketUser($user, $flagKey, "key", $salt) * 100000); + $bucketValue = floor(VariationOrRollout::bucketUser($user, $flagKey, "key", $salt, null) * 100000); self::assertGreaterThan(0, $bucketValue); self::assertLessThan(100000, $bucketValue); @@ -659,7 +659,7 @@ public function testRolloutSelectsLastBucketIfBucketValueEqualsTotalWeight() $flagKey = 'flagkey'; $salt = 'salt'; - $bucketValue = floor(VariationOrRollout::bucketUser($user, $flagKey, "key", $salt) * 100000); + $bucketValue = floor(VariationOrRollout::bucketUser($user, $flagKey, "key", $salt, null) * 100000); // We'll construct a list of variations that stops right at the target bucket value $rollout = array( diff --git a/tests/RolloutRandomizationConsistencyTest.php b/tests/RolloutRandomizationConsistencyTest.php new file mode 100644 index 00000000..58ca956f --- /dev/null +++ b/tests/RolloutRandomizationConsistencyTest.php @@ -0,0 +1,156 @@ + array( + 'variations' => array( + array('variation' => 0, 'weight' => 10000, 'untracked' => false), + array('variation' => 1, 'weight' => 20000, 'untracked' => false), + array('variation' => 0, 'weight' => 70000, 'untracked' => true) + ), + 'kind' => 'experiment', + // seed here carefully chosen so users fall into different variations + 'seed' => 61 + ), + 'clauses' => [] + + ); + + $flag = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => array(), + 'offVariation' => 1, + 'fallthrough' => $vr, + 'variations' => array('fall', 'off', 'on'), + 'salt' => 'saltyA' + ); + $decodedFlag = call_user_func(FeatureFlag::getDecoder(), $flag); + + return $decodedFlag; + } + + public function testVariationIndexForUser() + { + $flag = $this->buildFlag(); + $eventFactory = new EventFactory(false); + + $evaluationReasonInExperiment = EvaluationReason::fallthrough(true); + $evaluationReasonNotInExperiment = EvaluationReason::fallthrough(false); + + $expectedEvalResult1 = new EvalResult( + new EvaluationDetail('fall', 0, $evaluationReasonInExperiment), + [] + ); + + $expectedEvalResult2 = new EvalResult( + new EvaluationDetail('off', 1, $evaluationReasonInExperiment), + [] + ); + + $expectedEvalResult3 = new EvalResult( + new EvaluationDetail('fall', 0, $evaluationReasonNotInExperiment), + [] + ); + + $ub1 = new LDUserBuilder('userKeyA'); + $user1 = $ub1->build(); + $result1 = $flag->evaluate($user1, null, $eventFactory); + $this->assertEquals($expectedEvalResult1, $result1); + + $ub2 = new LDUserBuilder('userKeyB'); + $user2 = $ub2->build(); + $result2 = $flag->evaluate($user2, null, $eventFactory); + $this->assertEquals($expectedEvalResult2, $result2); + + $ub3 = new LDUserBuilder('userKeyC'); + $user3 = $ub3->build(); + $result3 = $flag->evaluate($user3, null, $eventFactory); + $this->assertEquals($expectedEvalResult3, $result3); + } + + public function testBucketUserByKey() + { + $vr = array('rollout' => array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + )); + + $decodedVr = call_user_func(VariationOrRollout::getDecoder(), $vr); + + $ub1 = new LDUserBuilder('userKeyA'); + $user1 = $ub1->build(); + $point1 = $decodedVr->bucketUser($user1, 'hashKey', 'key', 'saltyA', null); + $difference1 = abs($point1 - 0.42157587); + $this->assertTrue($difference1 <= 0.0000001); + + $ub2 = new LDUserBuilder('userKeyB'); + $user2 = $ub2->build(); + $point2 = $decodedVr->bucketUser($user2, 'hashKey', 'key', 'saltyA', null); + $difference2 = abs($point2 - 0.6708485); + $this->assertTrue($difference2 <= 0.0000001); + + $ub3 = new LDUserBuilder('userKeyC'); + $user3 = $ub3->build(); + $point3 = $decodedVr->bucketUser($user3, 'hashKey', 'key', 'saltyA', null); + $difference3 = abs($point3 - 0.10343106); + $this->assertTrue($difference3 <= 0.0000001); + } + + public function testBucketUserBySeed() + { + $seed = 61; + $vr = array('rollout' => array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + )); + + $decodedVr = call_user_func(VariationOrRollout::getDecoder(), $vr); + + $ub1 = new LDUserBuilder('userKeyA'); + $user1 = $ub1->build(); + $point1 = $decodedVr->bucketUser($user1, 'hashKey', 'key', 'saltyA', $seed); + $difference1 = abs($point1 - 0.09801207); + $this->assertTrue($difference1 <= 0.0000001); + + $ub2 = new LDUserBuilder('userKeyB'); + $user2 = $ub2->build(); + $point2 = $decodedVr->bucketUser($user2, 'hashKey', 'key', 'saltyA', $seed); + $difference2 = abs($point2 - 0.14483777); + $this->assertTrue($difference2 <= 0.0000001); + + $ub3 = new LDUserBuilder('userKeyC'); + $user3 = $ub3->build(); + $point3 = $decodedVr->bucketUser($user3, 'hashKey', 'key', 'saltyA', $seed); + $difference3 = abs($point3 - 0.9242641); + $this->assertTrue($difference3 <= 0.0000001); + } +} diff --git a/tests/RolloutTest.php b/tests/RolloutTest.php new file mode 100644 index 00000000..2c2a2275 --- /dev/null +++ b/tests/RolloutTest.php @@ -0,0 +1,42 @@ + array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ), + 'kind' => $kind, + 'seed' => $seed + ); + $decodedRollout = call_user_func(Rollout::getDecoder(), $rollout); + + $this->assertEquals(count($decodedRollout->getVariations()), 2); + $this->assertEquals($decodedRollout->isExperiment(), true); + $this->assertEquals($decodedRollout->getSeed(), $seed); + } + + public function testRolloutDefaultProperties() + { + $rollout = array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + ); + $decodedRollout = call_user_func(Rollout::getDecoder(), $rollout); + + $this->assertEquals(count($decodedRollout->getVariations()), 2); + $this->assertEquals($decodedRollout->isExperiment(), false); + $this->assertEquals($decodedRollout->getSeed(), null); + } +} diff --git a/tests/VariationOrRolloutTest.php b/tests/VariationOrRolloutTest.php new file mode 100644 index 00000000..0a5da0a3 --- /dev/null +++ b/tests/VariationOrRolloutTest.php @@ -0,0 +1,78 @@ + array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + )); + + $decodedVr = call_user_func(VariationOrRollout::getDecoder(), $vr); + + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + $key = 'flag-key'; + $attr = 'key'; + $salt = 'testing123'; + $userPoint1 = $decodedVr->bucketUser($user, $key, $attr, $salt, null); + $userPoint2 = $decodedVr->bucketUser($user, $key, $attr, $salt, $seed); + + $this->assertNotEquals($userPoint1, $userPoint2); + } + + public function testDifferentSaltsProduceDifferentAssignment() + { + $seed1 = 357; + $seed2 = 13; + $vr = array('rollout' => array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + )); + + $decodedVr = call_user_func(VariationOrRollout::getDecoder(), $vr); + + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + $key = 'flag-key'; + $attr = 'key'; + $salt = 'testing123'; + $userPoint1 = $decodedVr->bucketUser($user, $key, $attr, $salt, $seed1); + $userPoint2 = $decodedVr->bucketUser($user, $key, $attr, $salt, $seed2); + + $this->assertNotEquals($userPoint1, $userPoint2); + } + + public function testSameSeedIsDeterministic() + { + $seed = 357; + $vr = array('rollout' => array( + 'variations' => array( + array('variation' => 1, 'weight' => 50000), + array('variation' => 2, 'weight' => 50000) + ) + )); + + $decodedVr = call_user_func(VariationOrRollout::getDecoder(), $vr); + + $ub = new LDUserBuilder('userkey'); + $user = $ub->build(); + $key = 'flag-key'; + $attr = 'key'; + $salt = 'testing123'; + $userPoint1 = $decodedVr->bucketUser($user, $key, $attr, $salt, $seed); + $userPoint2 = $decodedVr->bucketUser($user, $key, $attr, $salt, $seed); + + $this->assertEquals($userPoint1, $userPoint2); + } +} diff --git a/tests/WeightedVariationTest.php b/tests/WeightedVariationTest.php new file mode 100644 index 00000000..6bd9ab32 --- /dev/null +++ b/tests/WeightedVariationTest.php @@ -0,0 +1,41 @@ + $variationId, + 'weight' => $weight, + 'untracked' => $untracked + ); + $decodedVariation = call_user_func(WeightedVariation::getDecoder(), $variation); + + $this->assertEquals($decodedVariation->getVariation(), $variationId); + $this->assertEquals($decodedVariation->getWeight(), $weight); + $this->assertEquals($decodedVariation->isUntracked(), $untracked); + } + + public function testWeightedVariationUntrackedDefault() + { + $variationId = 2; + $weight = 35700; + + $variation = array( + 'variation' => $variationId, + 'weight' => $weight + ); + $decodedVariation = call_user_func(WeightedVariation::getDecoder(), $variation); + + $this->assertEquals($decodedVariation->getVariation(), $variationId); + $this->assertEquals($decodedVariation->getWeight(), $weight); + $this->assertEquals($decodedVariation->isUntracked(), false); + } +}