diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 064e6e159..c010aa5b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,37 @@ -Contributing to the LaunchDarkly SDK for PHP -================================================ +# Contributing to the LaunchDarkly Server-Side SDK for PHP -We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. \ No newline at end of file +LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. + +## Submitting bug reports and feature requests + +The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/php-server-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. + +## Submitting pull requests + +We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. + +## Build instructions + +### Prerequisites + +The project uses [Composer](https://getcomposer.org/). + +### Installing dependencies + +From the project root directory: + +``` +composer install +``` + +### Testing + +To run all unit tests: + +``` +phpunit +``` + +By default, the full unit test suite includes live tests of the integrations for Consul, DynamoDB, and Redis. Those tests expect you to have instances of all of those databases running locally. To skip them, set the environment variable `LD_SKIP_DATABASE_TESTS=1` before running the tests. + +It is preferable to run tests against all supported minor versions of PHP (as described in `README.md` under Requirements), or at least the lowest and highest versions, prior to submitting a pull request. However, LaunchDarkly's CI tests will run automatically against all supported versions. diff --git a/README.md b/README.md index 6c0303c92..020b6a742 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,34 @@ -LaunchDarkly SDK for PHP -=========================== +# LaunchDarkly Server-side SDK for PHP -[![Circle CI](https://circleci.com/gh/launchdarkly/php-client.svg?style=svg)](https://circleci.com/gh/launchdarkly/php-client) +[![Circle CI](https://img.shields.io/circleci/project/launchdarkly/php-server-sdk.png)](https://circleci.com/gh/launchdarkly/php-server-sdk) -Requirements ------------- -1. PHP 5.5 or higher. +## LaunchDarkly overview -Quick setup ------------ +[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves over 100 billion feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/docs/getting-started) using LaunchDarkly today! + +[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) -1. Install the PHP SDK and monolog for logging with [Composer](https://getcomposer.org/) +## Supported PHP versions - php composer.phar require launchdarkly/launchdarkly-php +This version of the LaunchDarkly SDK is compatible with PHP 5.5 and higher. -1. After installing, require Composer's autoloader: +## Getting started - require 'vendor/autoload.php'; +Refer to the [SDK reference guide](https://docs.launchdarkly.com/docs/php-sdk-reference) for instructions on getting started with using the SDK. -1. Create a new LDClient with your SDK key: +## Learn more - $client = new LaunchDarkly\LDClient("your_sdk_key"); - -Your first feature flag ------------------------ - -1. Create a new feature flag on your [dashboard](https://app.launchdarkly.com) - -2. In your application code, use the feature's key to check whether the flag is on for each user: - - $user = new LaunchDarkly\LDUser("user@test.com"); - if ($client->variation("your.flag.key", $user)) { - # application code to show the feature - } else { - # the code to run if the feature is off - } - -Fetching flags --------------- - -There are two distinct methods of integrating LaunchDarkly in a PHP environment. - -* [Guzzle Cache Middleware](https://github.com/Kevinrob/guzzle-cache-middleware) to request and cache HTTP responses in an in-memory array (default) -* [ld-relay](https://github.com/launchdarkly/ld-relay) to retrieve and store flags in Redis (recommended) - -We strongly recommend using the ld-relay. Per-flag caching (Guzzle method) is only intended for low-throughput environments. - -Using Guzzle -============ - -Require Guzzle as a dependency: - - php composer.phar require "guzzlehttp/guzzle:6.2.1" - php composer.phar require "kevinrob/guzzle-cache-middleware:1.4.1" - -It will then be used as the default way of fetching flags. - -With Guzzle, you could persist your cache somewhere other than the default in-memory store, like Memcached or Redis. You could then specify your cache when initializing the client with the [cache option](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/LDClient.php#L44). - - $client = new LaunchDarkly\LDClient("YOUR_SDK_KEY", array("cache" => $cacheStorage)); - - -Using LD-Relay -============== - -The LaunchDarkly Relay Proxy ([ld-relay](https://github.com/launchdarkly/ld-relay)) consumes the LaunchDarkly streaming API and can update a database cache operating in your production environment. The ld-relay offers many benefits such as performance and feature flag consistency. With PHP applications, we strongly recommend setting up ld-relay with a database store. The database can be Redis, Consul, or DynamoDB. (For more about using LaunchDarkly with databases, see the [SDK reference guide](https://docs.launchdarkly.com/v2.0/docs/using-a-persistent-feature-store).) - -1. Set up ld-relay in [daemon-mode](https://github.com/launchdarkly/ld-relay#redis-storage-and-daemon-mode) with Redis - -2. Add the necessary dependency for the chosen database. - - For Redis: - - php composer.phar require "predis/predis:1.0.*" - - For Consul: - - php composer.phar require "sensiolabs/consul-php-sdk:2.*" - - For DynamoDB: - - php composer.phar require "aws/aws-sdk-php:3.*" - -3. Create the LDClient with the appropriate parameters for the chosen database. These examples show all of the available options. - - For Redis: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\Redis::featureRequester(), - 'redis_host' => 'your.redis.host', // defaults to "localhost" if not specified - 'redis_port' => 6379, // defaults to 6379 if not specified - 'redis_timeout' => 5, // connection timeout in seconds; defaults to 5 - 'redis_prefix' => 'env1' // corresponds to the prefix setting in ld-relay - 'predis_client' => $myClient // use this if you have already configured a Predis client instance - ]); - - For Consul: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\Consul::featureRequester(), - 'consul_uri' => 'http://localhost:8500', // this is the default - 'consul_prefix' => 'env1', // corresponds to the prefix setting in ld-relay - 'consul_options' => array(), // you may pass any options supported by the Guzzle client - 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed - ]); - - For DynamoDB: - - $client = new LaunchDarkly\LDClient("your_sdk_key", [ - 'feature_requester' => LaunchDarkly\Integrations\DynamoDb::featureRequester(), - 'dynamodb_table' => 'your.table.name', // required - 'dynamodb_prefix' => 'env1', // corresponds to the prefix setting in ld-relay - 'dynamodb_options' => array(), // you may pass any options supported by the AWS SDK - 'apc_expiration' => 30 // expiration time for local caching, if you have apcu installed - ]); - -4. If you are using DynamoDB, you must create your table manually. It must have a partition key called "namespace", and a sort key called "key" (both strings). Note that by default the AWS SDK will attempt to get your AWS credentials and region from environment variables and/or local configuration files, but you may also specify them in `dynamodb_options`. - -5. If ld-relay is configured for [event forwarding](https://github.com/launchdarkly/ld-relay#event-forwarding), you can configure the LDClient to publish events to ld-relay instead of directly to `events.launchdarkly.com`. Using the `Guzzle` implementation of event publishing with ld-relay event forwarding can be an efficient alternative to the default `curl`-based event publishing. - - To forward events, add the following configuration properties to the configuration shown above: - - 'event_publisher' => LaunchDarkly\Integrations\Guzzle::eventPublisher(), - 'events_uri' => 'http://your-ldrelay-host:8030' - -Using flag data from a file ---------------------------- - -For testing purposes, the SDK can be made to read feature flag state from a file or files instead of connecting to LaunchDarkly. See [`LaunchDarkly\Integrations\Files`](https://github.com/launchdarkly/php-client/blob/master/src/LaunchDarkly/Integrations/Files.php) and ["Reading flags from a file"](https://docs.launchdarkly.com/docs/reading-flags-from-a-file). +Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/docs/php-sdk-reference). -Testing -------- +## Testing We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. -Learn more ------------ - -Check out our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](http://docs.launchdarkly.com/docs/php-sdk-reference). - -Contributing ------------- +## Contributing -We encourage pull-requests and other contributions from the community. We've also published an [SDK contributor's guide](http://docs.launchdarkly.com/docs/sdk-contributors-guide) that provides a detailed explanation of how our SDKs work. +We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. -About LaunchDarkly ------------------- +## About LaunchDarkly * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. diff --git a/composer.json b/composer.json index 6b89f748b..7d6ab72f6 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "launchdarkly", "launchdarkly php" ], - "homepage": "https://github.com/launchdarkly/php-client", + "homepage": "https://github.com/launchdarkly/php-server-sdk", "license": "Apache-2.0", "authors": [ { diff --git a/scripts/release.sh b/scripts/release.sh index 0babf1c15..fe523f7a3 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -10,7 +10,7 @@ # When done you should commit and push the changes made. set -uxe -echo "Starting php-client release (version update only)" +echo "Starting php-server-sdk release (version update only)" VERSION=$1 @@ -22,4 +22,4 @@ LDCLIENT_PHP_TEMP=./LDClient.php.tmp sed "s/const VERSION = '.*'/const VERSION = '${VERSION}'/g" $LDCLIENT_PHP > $LDCLIENT_PHP_TEMP mv $LDCLIENT_PHP_TEMP $LDCLIENT_PHP -echo "Done with php-client release (version update only)" +echo "Done with php-server-sdk release (version update only)" diff --git a/tests/ConsulFeatureRequesterTest.php b/tests/ConsulFeatureRequesterTest.php index 5a3abd6fa..fb5ff1542 100644 --- a/tests/ConsulFeatureRequesterTest.php +++ b/tests/ConsulFeatureRequesterTest.php @@ -15,8 +15,15 @@ class ConsulFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - $sf = new ServiceFactory(); - self::$kvClient = $sf->get('kv'); + if (!static::isSkipDatabaseTests()) { + $sf = new ServiceFactory(); + self::$kvClient = $sf->get('kv'); + } + } + + protected function isDatabaseTest() + { + return true; } protected function makeRequester() diff --git a/tests/DynamoDbFeatureRequesterTest.php b/tests/DynamoDbFeatureRequesterTest.php index e68ee342b..588c0ccea 100644 --- a/tests/DynamoDbFeatureRequesterTest.php +++ b/tests/DynamoDbFeatureRequesterTest.php @@ -15,8 +15,15 @@ class DynamoDbFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - self::$dynamoDbClient = new DynamoDbClient(self::makeDynamoDbOptions()); - self::createTableIfNecessary(); + if (!static::isSkipDatabaseTests()) { + self::$dynamoDbClient = new DynamoDbClient(self::makeDynamoDbOptions()); + self::createTableIfNecessary(); + } + } + + protected function isDatabaseTest() + { + return true; } private static function makeDynamoDbOptions() diff --git a/tests/FeatureFlagTest.php b/tests/FeatureFlagTest.php index 6546f1868..19c22e324 100644 --- a/tests/FeatureFlagTest.php +++ b/tests/FeatureFlagTest.php @@ -8,6 +8,54 @@ use LaunchDarkly\LDUserBuilder; use LaunchDarkly\Segment; +const RULE_ID = 'ruleid'; + +$defaultUser = (new LDUserBuilder('foo'))->build(); + +function makeBooleanFlagWithRules(array $rules) +{ + $flagJson = array( + 'key' => 'feature', + 'version' => 1, + 'deleted' => false, + 'on' => true, + 'targets' => array(), + 'prerequisites' => array(), + 'rules' => $rules, + 'offVariation' => 0, + 'fallthrough' => array('variation' => 0), + 'variations' => array(false, true), + 'salt' => '' + ); + return FeatureFlag::decode($flagJson); +} + +function makeBooleanFlagWithClauses($clauses) +{ + return makeBooleanFlagWithRules(array(array('clauses' => $clauses, 'variation' => 1))); +} + +function makeRuleMatchingUser($user, $ruleAttrs = array()) +{ + $clause = array('attribute' => 'key', 'op' => 'in', 'values' => array($user->getKey()), 'negate' => false); + return array_merge(array('id' => RULE_ID, 'clauses' => array($clause)), $ruleAttrs); +} + +function makeSegmentMatchClause($segmentKey) +{ + return array('attribute' => '', 'op' => 'segmentMatch', 'values' => array($segmentKey), 'negate' => false); +} + +// This is our way of verifying that the bucket value for a rollout is within 1.0 of the expected value. +function makeRolloutVariations($targetValue, $targetVariation, $otherVariation) +{ + return array( + array('weight' => $targetValue, 'variation' => $otherVariation), + array('weight' => 1, 'variation' => $targetVariation), + array('weight' => 100000 - ($targetValue + 1), 'variation' => $otherVariation) + ); +} + class FeatureFlagTest extends \PHPUnit_Framework_TestCase { private static $json1 = "{ @@ -498,59 +546,23 @@ public function testFlagMatchesUserFromTargets() self::assertEquals(array(), $result->getPrerequisiteEvents()); } - private function makeBooleanFlagWithRules(array $rules) - { - $flagJson = array( - 'key' => 'feature', - 'version' => 1, - 'deleted' => false, - 'on' => true, - 'targets' => array(), - 'prerequisites' => array(), - 'rules' => $rules, - 'offVariation' => 0, - 'fallthrough' => array('variation' => 0), - 'variations' => array(false, true), - 'salt' => '' - ); - return FeatureFlag::decode($flagJson); - } - public function testFlagMatchesUserFromRules() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => 1 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 1)))); - $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + $result = $flag->evaluate($defaultUser, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } public function testFlagReturnsErrorIfRuleVariationIsTooHigh() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => 999 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => 999)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -558,19 +570,10 @@ public function testFlagReturnsErrorIfRuleVariationIsTooHigh() public function testFlagReturnsErrorIfRuleVariationIsNegative() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'variation' => -1 - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('variation' => -1)))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -578,18 +581,10 @@ public function testFlagReturnsErrorIfRuleVariationIsNegative() public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ) - ) - )); - $ub = new LDUserBuilder('userkey'); - $user = $ub->build(); + global $defaultUser; + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array()))); - $result = $flag->evaluate($user, null); + $result = $flag->evaluate($defaultUser, null); $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); @@ -597,118 +592,154 @@ public function testFlagReturnsErrorIfRuleHasNoVariationOrRollout() public function testFlagReturnsErrorIfRuleHasRolloutWithNoVariations() { - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'rollout' => array('variations' => array()) - ) - )); + global $defaultUser; + $rollout = array('variations' => array()); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($defaultUser, array('rollout' => $rollout)))); + + $result = $flag->evaluate($defaultUser, null); + $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + self::assertEquals($detail, $result->getDetail()); + self::assertEquals(array(), $result->getPrerequisiteEvents()); + } + + public function testRolloutCalculationBucketsByUserKeyByDefault() + { $ub = new LDUserBuilder('userkey'); $user = $ub->build(); + $expectedBucketValue = 22464; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(null, null, EvaluationReason::error(EvaluationReason::MALFORMED_FLAG_ERROR)); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); + self::assertEquals($detail, $result->getDetail()); + } + + public function testRolloutCalculationCanBucketBySpecificAttribute() + { + $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); + $user = $ub->build(); + $expectedBucketValue = 95913; + $rollout = array( + 'salt' => '', + 'bucketBy' => 'name', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); + self::assertEquals($detail, $result->getDetail()); + } + + public function testRolloutCalculationIncludesSecondaryKey() + { + $ub = new LDUserBuilder('userkey'); + $ub->secondary('999'); + $user = $ub->build(); + $expectedBucketValue = 31179; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); + + $result = $flag->evaluate($user, null); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function testSecondaryKeyIsCoercedToStringForRolloutCalculation() - { - // We can't really verify that the rollout calculation works correctly, but we can at least - // make sure it doesn't error out if there's a non-string secondary value (ch35189) - $flag = $this->makeBooleanFlagWithRules(array( - array( - 'id' => 'ruleid', - 'clauses' => array( - array('attribute' => 'key', 'op' => 'in', 'values' => array('userkey'), 'negate' => false) - ), - 'rollout' => array( - 'salt' => '', - 'variations' => array( - array( - 'weight' => 100000, - 'variation' => 1 - ) - ) - ) - ) - )); + public function testRolloutCalculationCoercesSecondaryKeyToString() + { + // This should produce the same result as the previous test, and should not cause an error (ch35189). $ub = new LDUserBuilder('userkey'); $ub->secondary(999); $user = $ub->build(); + $expectedBucketValue = 31179; + $rollout = array( + 'salt' => '', + 'variations' => makeRolloutVariations($expectedBucketValue, 1, 0) + ); + $flag = makeBooleanFlagWithRules(array(makeRuleMatchingUser($user, array('rollout' => $rollout)))); $result = $flag->evaluate($user, null); - $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, 'ruleid')); + $detail = new EvaluationDetail(true, 1, EvaluationReason::ruleMatch(0, RULE_ID)); self::assertEquals($detail, $result->getDetail()); self::assertEquals(array(), $result->getPrerequisiteEvents()); } - public function clauseCanMatchBuiltInAttribute() + public function testClauseCanMatchBuiltInAttribute() { $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(true, $result->getValue()); + self::assertEquals(true, $result->getDetail()->getValue()); } - public function clauseCanMatchCustomAttribute() + public function testClauseCanMatchCustomAttribute() { $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); $ub->customAttribute('legs', 4); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(true, $result->getValue()); + self::assertEquals(true, $result->getDetail()->getValue()); } - public function clauseReturnsFalseForMissingAttribute() + public function testClauseReturnsFalseForMissingAttribute() { $clause = array('attribute' => 'legs', 'op' => 'in', 'values' => array('4'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } - public function clauseCanBeNegated() + public function testClauseCanBeNegated() { $clause = array('attribute' => 'name', 'op' => 'in', 'values' => array('Bob'), 'negate' => true); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } - public function clauseWithUnknownOperatorDoesNotMatch() + public function testClauseWithUnknownOperatorDoesNotMatch() { $clause = array('attribute' => 'name', 'op' => 'doesSomethingUnsupported', 'values' => array('Bob'), 'negate' => false); - $flag = $this->booleanFlagWithClauses(array($clause)); + $flag = makeBooleanFlagWithClauses(array($clause)); $ub = new LDUserBuilder('userkey'); + $ub->name('Bob'); $user = $ub->build(); $result = $flag->evaluate($user, null); - self::assertEquals(false, $result->getValue()); + self::assertEquals(false, $result->getDetail()->getValue()); } public function testSegmentMatchClauseRetrievesSegmentFromStore() { + global $defaultUser; $segmentJson = array( 'key' => 'segkey', 'version' => 1, 'deleted' => false, - 'included' => array('foo'), + 'included' => array($defaultUser->getKey()), 'excluded' => array(), 'rules' => array(), 'salt' => '' @@ -719,58 +750,22 @@ public function testSegmentMatchClauseRetrievesSegmentFromStore() $requester->key = 'segkey'; $requester->val = $segment; - $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); - - $ub = new LDUserBuilder('foo'); - $user = $ub->build(); + $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($user, $requester); + $result = $feature->evaluate($defaultUser, $requester); self::assertTrue($result->getDetail()->getValue()); } public function testSegmentMatchClauseFallsThroughWithNoErrorsIfSegmentNotFound() { + global $defaultUser; $requester = new MockFeatureRequesterForSegment(); - $feature = $this->makeBooleanFeatureWithSegmentMatch('segkey'); - - $ub = new LDUserBuilder('foo'); - $user = $ub->build(); + $feature = makeBooleanFlagWithClauses(array(makeSegmentMatchClause('segkey'))); - $result = $feature->evaluate($user, $requester); + $result = $feature->evaluate($defaultUser, $requester); self::assertFalse($result->getDetail()->getValue()); } - - private function booleanFlagWithClauses($clauses) - { - $featureJson = array( - 'key' => 'test', - 'version' => 1, - 'deleted' => false, - 'on' => true, - 'variations' => array(false, true), - 'fallthrough' => array('variation' => 0), - 'rules' => array( - array('clauses' => $clauses, 'variation' => 1) - ), - 'offVariation' => 0, - 'prerequisites' => array(), - 'targets' => array(), - 'salt' => '' - ); - return FeatureFlag::decode($featureJson); - } - - private function makeBooleanFeatureWithSegmentMatch($segmentKey) - { - $clause = array( - 'attribute' => '', - 'op' => 'segmentMatch', - 'values' => array($segmentKey), - 'negate' => false - ); - return $this->booleanFlagWithClauses(array($clause)); - } } diff --git a/tests/FeatureRequesterTestBase.php b/tests/FeatureRequesterTestBase.php index 430135527..be434852f 100644 --- a/tests/FeatureRequesterTestBase.php +++ b/tests/FeatureRequesterTestBase.php @@ -9,7 +9,21 @@ class FeatureRequesterTestBase extends \PHPUnit_Framework_TestCase { public function setUp() { - $this->deleteExistingData(); + if ($this->isDatabaseTest() && static::isSkipDatabaseTests()) { + $this->markTestSkipped('skipping database tests'); + } else { + $this->deleteExistingData(); + } + } + + protected static function isSkipDatabaseTests() + { + return isset($_ENV['LD_SKIP_DATABASE_TESTS']) && $_ENV['LD_SKIP_DATABASE_TESTS']; + } + + protected function isDatabaseTest() + { + return false; } protected function deleteExistingData() diff --git a/tests/RedisFeatureRequesterTest.php b/tests/RedisFeatureRequesterTest.php index a6e507b39..05ca388cd 100644 --- a/tests/RedisFeatureRequesterTest.php +++ b/tests/RedisFeatureRequesterTest.php @@ -14,7 +14,14 @@ class RedisFeatureRequesterTest extends FeatureRequesterTestBase public static function setUpBeforeClass() { - self::$predisClient = new Client(array()); + if (!static::isSkipDatabaseTests()) { + self::$predisClient = new Client(array()); + } + } + + protected function isDatabaseTest() + { + return true; } protected function makeRequester() diff --git a/tests/SegmentTest.php b/tests/SegmentTest.php index 60331d9dc..c9e6af39a 100644 --- a/tests/SegmentTest.php +++ b/tests/SegmentTest.php @@ -4,13 +4,32 @@ use LaunchDarkly\LDUserBuilder; use LaunchDarkly\Segment; +$defaultUser = (new LDUserBuilder('foo'))->build(); + +function makeSegmentMatchingUser($user, $ruleAttrs = array()) +{ + $clause = array('attribute' => 'key', 'op' => 'in', 'values' => array($user->getKey()), 'negate' => false); + $rule = array_merge(array('clauses' => array($clause)), $ruleAttrs); + $json = array( + 'key' => 'test', + 'included' => array(), + 'excluded' => array(), + 'salt' => 'salt', + 'rules' => array($rule), + 'version' => 1, + 'deleted' => false + ); + return Segment::decode($json); +} + class SegmentTest extends \PHPUnit_Framework_TestCase { public function testExplicitIncludeUser() { + global $defaultUser; $json = array( 'key' => 'test', - 'included' => array('foo'), + 'included' => array($defaultUser->getKey()), 'excluded' => array(), 'rules' => array(), 'salt' => 'salt', @@ -18,32 +37,32 @@ public function testExplicitIncludeUser() 'deleted' => false ); $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $this->assertTrue($segment->matchesUser($ub->build())); + $this->assertTrue($segment->matchesUser($defaultUser)); } public function testExplicitExcludeUser() { + global $defaultUser; $json = array( 'key' => 'test', 'included' => array(), - 'excluded' => array('foo'), + 'excluded' => array($defaultUser->getKey()), 'rules' => array(), 'salt' => 'salt', 'version' => 1, 'deleted' => false ); $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $this->assertFalse($segment->matchesUser($ub->build())); + $this->assertFalse($segment->matchesUser($defaultUser)); } public function testExplicitIncludePasPrecedence() { + global $defaultUser; $json = array( 'key' => 'test', - 'included' => array('foo'), - 'excluded' => array('foo'), + 'included' => array($defaultUser->getKey()), + 'excluded' => array($defaultUser->getKey()), 'rules' => array(), 'salt' => 'salt', 'version' => 1, @@ -56,60 +75,48 @@ public function testExplicitIncludePasPrecedence() public function testMatchingRuleWithFullRollout() { - $json = array( - 'key' => 'test', - 'included' => array(), - 'excluded' => array(), - 'salt' => 'salt', - 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => 'email', - 'op' => 'in', - 'values' => array('test@example.com'), - 'negate' => false - ) - ), - 'weight' => 100000 - ) - ), - 'version' => 1, - 'deleted' => false - ); - $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $ub->email('test@example.com'); - $this->assertTrue($segment->matchesUser($ub->build())); + global $defaultUser; + $segment = makeSegmentMatchingUser($defaultUser, array('weight' => 100000)); + $this->assertTrue($segment->matchesUser($defaultUser)); } public function testMatchingRuleWithZeroRollout() { - $json = array( - 'key' => 'test', - 'included' => array(), - 'excluded' => array(), - 'salt' => 'salt', - 'rules' => array( - array( - 'clauses' => array( - array( - 'attribute' => 'email', - 'op' => 'in', - 'values' => array('test@example.com'), - 'negate' => false - ) - ), - 'weight' => 0 - ) - ), - 'version' => 1, - 'deleted' => false - ); - $segment = Segment::decode($json); - $ub = new LDUserBuilder('foo'); - $ub->email('test@example.com'); - $this->assertFalse($segment->matchesUser($ub->build())); + global $defaultUser; + $segment = makeSegmentMatchingUser($defaultUser, array('weight' => 0)); + $this->assertFalse($segment->matchesUser($defaultUser)); + } + + public function testRolloutCalculationCanBucketByKey() + { + $user = (new LDUserBuilder('userkey'))->name('Bob')->build(); + $this->verifyRollout($user, 12551); + } + + public function testRolloutCalculationIncludesSecondaryKey() + { + $user = (new LDUserBuilder('userkey'))->secondary('999')->build(); + $this->verifyRollout($user, 81650); + } + + public function testRolloutCalculationCoercesSecondaryKeyToString() + { + $user = (new LDUserBuilder('userkey'))->secondary(999)->build(); + $this->verifyRollout($user, 81650); + } + + public function testRolloutCalculationCanBucketBySpecificAttribute() + { + $user = (new LDUserBuilder('userkey'))->name('Bob')->build(); + $this->verifyRollout($user, 61691, array('bucketBy' => 'name')); + } + + private function verifyRollout($user, $expectedBucketValue, $rolloutAttrs = array()) + { + $segment0 = makeSegmentMatchingUser($user, array_merge(array('weight' => $expectedBucketValue + 1), $rolloutAttrs)); + $this->assertTrue($segment0->matchesUser($user)); + $segment1 = makeSegmentMatchingUser($user, array_merge(array('weight' => $expectedBucketValue), $rolloutAttrs)); + $this->assertFalse($segment1->matchesUser($user)); } public function testMatchingRuleWithMultipleClauses()