diff --git a/CHANGES b/CHANGES index 70559aa..295c576 100644 --- a/CHANGES +++ b/CHANGES @@ -1,5 +1,10 @@ +1.2.0 (Sep 19, 2023): +- Add support for Client/GetTreatment(s)WithConfig operations. +- Add support for Manager operations. +- Add default arguments for value & properties in .track() +- Enforce status code validation on rpc responses. + 1.1.0 (Sep 6, 2023): -==================== - Fix issue with datetime attributes on php7 - Remove unit tests from autoload - Add support for .track() diff --git a/examples/manager.php b/examples/manager.php new file mode 100644 index 0000000..bf91e73 --- /dev/null +++ b/examples/manager.php @@ -0,0 +1,21 @@ + [ + 'address' => '../../splitd/splitd.sock', + 'type' => 'unix-stream', + ], + 'logging' => [ + 'level' => \Psr\Log\LogLevel::DEBUG, + ], +]); + +$manager = $factory->manager(); +$names = $manager->splitNames(); +print_r($names); +var_dump($manager->split($names[0])); +var_dump($manager->splits()); diff --git a/examples/test.php b/examples/treatment.php similarity index 100% rename from examples/test.php rename to examples/treatment.php diff --git a/examples/treatmentWithConfig.php b/examples/treatmentWithConfig.php new file mode 100644 index 0000000..5480a23 --- /dev/null +++ b/examples/treatmentWithConfig.php @@ -0,0 +1,37 @@ +getKey() + . " feat=" . $i->getFeature() + . " treatment=" . $i->getTreatment() + . " label=" . $i->getLabel() + . " cn=" . $i->getChangeNumber() + . " #attrs=" . (($a == null) ? 0 : count($a)) . "\n"; + } +} + +$factory = Factory::withConfig([ + 'transfer' => [ + 'address' => '../../splitd/splitd.sock', + 'type' => 'unix-stream', + ], + 'logging' => [ + 'level' => \Psr\Log\LogLevel::INFO, + ], + 'utils' => [ + 'impressionListener' => new CustomListener(), + ], +]); + +$client = $factory->client(); +print_r($client->getTreatmentWithConfig("key", null, "feature1", ['age' => 22])); +print_r($client->getTreatmentsWithConfig("key", null, ["feature1", "feature2"], ['age' => 22])); diff --git a/src/Client.php b/src/Client.php index 8cb7988..3e7c2cd 100644 --- a/src/Client.php +++ b/src/Client.php @@ -26,7 +26,7 @@ public function __construct(Manager $manager, LoggerInterface $logger, ?Impressi $this->inputValidator = new InputValidator($logger); } - public function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes): string + public function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): string { try { list($treatment, $ilData) = $this->lm->getTreatment($key, $bucketingKey, $feature, $attributes); @@ -38,7 +38,7 @@ public function getTreatment(string $key, ?string $bucketingKey, string $feature } } - public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array { try { $results = $this->lm->getTreatments($key, $bucketingKey, $features, $attributes); @@ -51,11 +51,46 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature return $toReturn; } catch (\Exception $exc) { $this->logger->error($exc); - return array_reduce($features, function ($r, $k) { $r[$k] = "control"; return $r; }, []); + return array_reduce($features, function ($r, $k) { + $r[$k] = "control"; + return $r; + }, []); } } - public function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool + public function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes = null): array + { + try { + list($treatment, $ilData, $config) = $this->lm->getTreatmentWithConfig($key, $bucketingKey, $feature, $attributes); + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + return ['treatment' => $treatment, 'config' => $config]; + } catch (\Exception $exc) { + $this->logger->error($exc); + return "control"; + } + } + + public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes = null): array + { + try { + $results = $this->lm->getTreatmentsWithConfig($key, $bucketingKey, $features, $attributes); + $toReturn = []; + foreach ($results as $feature => $result) { + list($treatment, $ilData, $config) = $result; + $toReturn[$feature] = ['treatment' => $treatment, 'config' => $config]; + $this->handleListener($key, $bucketingKey, $feature, $attributes, $treatment, $ilData); + } + return $toReturn; + } catch (\Exception $exc) { + $this->logger->error($exc); + return array_reduce($features, function ($r, $k) { + $r[$k] = ['treatment' => 'control', 'config' => null]; + return $r; + }, []); + } + } + + public function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool { try { $properties = $this->inputValidator->validProperties($properties); diff --git a/src/ClientInterface.php b/src/ClientInterface.php index 0002a1b..e62f045 100644 --- a/src/ClientInterface.php +++ b/src/ClientInterface.php @@ -5,6 +5,8 @@ interface ClientInterface { function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes): string; + function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array; function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; - function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool; + function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; + function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool; } diff --git a/src/Config/Fallback.php b/src/Config/Fallback.php new file mode 100644 index 0000000..6a8ad29 --- /dev/null +++ b/src/Config/Fallback.php @@ -0,0 +1,53 @@ +disable = $disable; + $this->customUserClient = $client; + $this->customUserManager = $manager; + } + + public function disable(): bool + { + return $this->disable; + } + + public function client(): ?ClientInterface + { + return $this->customUserClient; + } + + public function manager(): ?ManagerInterface + { + return $this->customUserManager; + } + + public static function fromArray(array $config): Fallback + { + $d = self::default(); + return new Fallback( + $config['disable'] ?? $d->disable(), + $config['client'] ?? $d->client(), + $config['manager'] ?? $d->manager() + ); + } + + public static function default(): Fallback + { + return new Fallback(false, new AlwaysControlClient(), new AlwaysEmptyManager()); + } +} diff --git a/src/Config/Main.php b/src/Config/Main.php index 52029d3..3eaa44b 100644 --- a/src/Config/Main.php +++ b/src/Config/Main.php @@ -7,13 +7,15 @@ class Main private /*Transfer*/ $transfer; private /*Serialization*/ $serialization; private /*Logging*/ $logging; + private /*Fallback*/ $fallback; private /*Utils*/ $utils; - private function __construct(Transfer $transfer, Serialization $serialization, Logging $logging, Utils $utils) + private function __construct(Transfer $transfer, Serialization $serialization, Logging $logging, Fallback $fallback, Utils $utils) { $this->transfer = $transfer; $this->serialization = $serialization; $this->logging = $logging; + $this->fallback = $fallback; $this->utils = $utils; } @@ -32,6 +34,11 @@ public function logging(): Logging return $this->logging; } + public function fallback(): Fallback + { + return $this->fallback; + } + public function utils(): Utils { return $this->utils; @@ -43,6 +50,7 @@ public static function fromArray(array $config): Main Transfer::fromArray($config['transfer'] ?? []), Serialization::fromArray($config['serialization'] ?? []), Logging::fromArray($config['logging'] ?? []), + Fallback::fromArray($config['fallback'] ?? []), Utils::fromArray($config['utils'] ?? []), ); } @@ -53,6 +61,7 @@ public static function default(): Main Transfer::default(), Serialization::default(), Logging::default(), + Fallback::default(), Utils::default(), ); } diff --git a/src/Factory.php b/src/Factory.php index 8fa1441..43373c9 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -28,13 +28,42 @@ public static function default(): Factory return new Factory(Config\Main::default()); } - public static function withConfig(array $config): Factory + public static function withConfig(array $config): FactoryInterface { - return new Factory(Config\Main::fromArray($config)); + try { + return new Factory(Config\Main::fromArray($config)); + } catch (\Exception $e) { + + try { + $parsedConfig = Config\Main::fromArray($config); + if ($parsedConfig->fallback()->disable()) { // fallback disabled, re-throw + throw new Fallback\FallbackDisabledException($e); + } + + $logger = Helpers::getLogger($parsedConfig->logging()); + $logger->error(sprintf("error instantiating a factory with supplied config (%s). will return a fallback.", $e->getMessage())); + $logger->debug($e); + return new Fallback\GenericFallbackFactory($parsedConfig->fallback()->client(), $parsedConfig->fallback()->manager()); + } catch (Fallback\FallbackDisabledException $e) { + // client wants to handle exception himself. re-throw it; + throw $e->wrapped(); + } catch (\Exception $e) { + // This branch is virtually unreachable (hence untestable) unless we introduce a bug. + // it's basically a safeguard to prevent the customer app from crashing if we do. + $logger = Helpers::getLogger(Config\Logging::default()); + $logger->error(sprintf("error parsing supplied factory config config (%s). will return a fallback.", $e->getMessage())); + return new Fallback\GenericFallbackFactory(new Fallback\AlwaysControlClient(), new Fallback\AlwaysEmptyManager()); + } + } } public function client(): ClientInterface { return new Client($this->linkManager, $this->logger, $this->config->utils()->impressionListener()); } + + public function manager(): ManagerInterface + { + return new Manager($this->linkManager, $this->logger); + } }; diff --git a/src/FactoryInterface.php b/src/FactoryInterface.php index 759d740..93a63d9 100644 --- a/src/FactoryInterface.php +++ b/src/FactoryInterface.php @@ -5,4 +5,5 @@ interface FactoryInterface { public function client(): ClientInterface; + public function manager(): ManagerInterface; }; diff --git a/src/Fallback/AlwaysControlClient.php b/src/Fallback/AlwaysControlClient.php new file mode 100644 index 0000000..2020e3e --- /dev/null +++ b/src/Fallback/AlwaysControlClient.php @@ -0,0 +1,39 @@ + 'control', 'config' => null]; + } + + public function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + { + return array_reduce($features, function ($carry, $item) { + $carry[$item] = "control"; + return $carry; + }, []); + } + + public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + { + return array_reduce($features, function ($carry, $item) { + $carry[$item] = ['treatment' => 'control', 'config' => null]; + return $carry; + }, []); + } + + public function track(string $key, string $trafficType, string $eventType, ?float $value = null, ?array $properties = null): bool + { + return false; + } +} diff --git a/src/Fallback/AlwaysEmptyManager.php b/src/Fallback/AlwaysEmptyManager.php new file mode 100644 index 0000000..23f816e --- /dev/null +++ b/src/Fallback/AlwaysEmptyManager.php @@ -0,0 +1,24 @@ +wrapped = $wrapped; + } + + public function wrapped(): \Exception + { + return $this->wrapped; + } +} diff --git a/src/Fallback/GenericFallbackFactory.php b/src/Fallback/GenericFallbackFactory.php new file mode 100644 index 0000000..4ec1048 --- /dev/null +++ b/src/Fallback/GenericFallbackFactory.php @@ -0,0 +1,29 @@ +client = $client; + $this->manager = $manager; + } + public function client(): ClientInterface + { + return $this->client; + } + + public function manager(): ManagerInterface + { + return $this->manager; + } +} diff --git a/src/Link/Consumer/Manager.php b/src/Link/Consumer/Manager.php index 3e44de4..cdba27e 100644 --- a/src/Link/Consumer/Manager.php +++ b/src/Link/Consumer/Manager.php @@ -2,10 +2,16 @@ namespace SplitIO\ThinSdk\Link\Consumer; +use \SplitIO\ThinSdk\SplitView; + interface Manager { function getTreatment(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array; + function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array; function getTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; + function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array; function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool; + function splitNames(): array; + function split(string $splitName): ?SplitView; + function splits(): array; } - diff --git a/src/Link/Consumer/V1Manager.php b/src/Link/Consumer/V1Manager.php index d5251a5..f9abf16 100644 --- a/src/Link/Consumer/V1Manager.php +++ b/src/Link/Consumer/V1Manager.php @@ -4,8 +4,10 @@ use \SplitIO\ThinSdk\Link\Protocol; use \SplitIO\ThinSdk\Link\Protocol\V1\RPC; +use \SplitIO\ThinSdk\Link\Protocol\V1\SplitViewResult; use \SplitIO\ThinSdk\Link\Transfer; use \SplitIO\ThinSdk\Link\Serialization; +use \SplitIO\ThinSdk\SplitView; use \SplitIO\ThinSdk\Config\Utils as UtilsConfig; @@ -42,6 +44,16 @@ public function getTreatment(string $key, ?string $bucketingKey, string $feature { $result = Protocol\V1\TreatmentResponse::fromRaw( $this->rpcWithReconnect(RPC::forTreatment($key, $bucketingKey, $feature, $attributes)) + ); + $result->ensureSuccess(); + + return [$result->getEvaluationResult()->getTreatment(), $result->getEvaluationResult()->getImpressionListenerData()]; + } + + public function getTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): array + { + $result = Protocol\V1\TreatmentResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentWithConfig($key, $bucketingKey, $feature, $attributes)) )->getEvaluationResult(); return [$result->getTreatment(), $result->getImpressionListenerData(), $result->getConfig()]; @@ -52,6 +64,25 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature $response = Protocol\V1\TreatmentsResponse::fromRaw( $this->rpcWithReconnect(RPC::forTreatments($key, $bucketingKey, $features, $attributes)) ); + $response->ensureSuccess(); + + $results = []; + foreach ($features as $idx => $feature) { + $result = $response->getEvaluationResult($idx); + $results[$feature] = $result == null + ? ["control", null] + : [$result->getTreatment(), $result->getImpressionListenerdata()]; + } + + return $results; + } + + public function getTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): array + { + $response = Protocol\V1\TreatmentsResponse::fromRaw( + $this->rpcWithReconnect(RPC::forTreatmentsWithConfig($key, $bucketingKey, $features, $attributes)) + ); + $response->ensureSuccess(); $results = []; foreach ($features as $idx => $feature) { @@ -66,18 +97,41 @@ public function getTreatments(string $key, ?string $bucketingKey, array $feature public function track(string $key, string $trafficType, string $eventType, ?float $value, ?array $properties): bool { - return Protocol\V1\TrackResponse::fromRaw( + $response = Protocol\V1\TrackResponse::fromRaw( $this->rpcWithReconnect(RPC::forTrack($key, $trafficType, $eventType, $value, $properties)) - )->getSuccess(); + ); + $response->ensureSuccess(); + return $response->getEventQueued(); + } + + public function splitNames(): array + { + $response = Protocol\V1\SplitNamesResponse::fromRaw($this->rpcWithReconnect(RPC::forSplitNames())); + $response->ensureSuccess(); + return $response->getSplitNames(); + } + + public function split(string $splitName): ?SplitView + { + $response = Protocol\V1\SplitResponse::fromRaw($this->rpcWithReconnect(RPC::forSplit($splitName))); + $response->ensureSuccess(); + return self::splitResultToView($response->getView()); } + public function splits(): array + { + $response = Protocol\V1\SplitsResponse::fromRaw($this->rpcWithReconnect(RPC::forSplits())); + $response->ensureSuccess(); + return array_map([self::class, 'splitResultToView'], $response->getViews()); + } private function register(string $id, bool $impressionFeedback) { // this is performed without retries to avoid an endless loop, // since register should occur only once per connection. if it fails, // it's not worth retrying for this single evaluation, and probably better off to just return 'control'. - return $this->performRPC(RPC::forRegister($id, new Protocol\V1\RegisterFlags($impressionFeedback))); + $response = Protocol\V1\RegisterResponse::fromRaw($this->performRPC(RPC::forRegister($id, new Protocol\V1\RegisterFlags($impressionFeedback)))); + $response->ensureSuccess(); } private function rpcWithReconnect(RPC $rpc): array @@ -100,4 +154,16 @@ private function performRPC(RPC $rpc): array $this->conn->sendMessage($this->serializer->serialize($rpc)); return $this->serializer->deserialize($this->conn->readMessage()); } + + private static function splitResultToView(SplitViewResult $res): SplitView + { + return new SplitView( + $res->getName(), + $res->getTrafficType(), + $res->getKilled(), + $res->getTreatments(), + $res->getChangeNumber(), + $res->getConfigs() + ); + } }; diff --git a/src/Link/Protocol/V1/ImpressionListenerData.php b/src/Link/Protocol/V1/ImpressionListenerData.php index 055b398..0b62018 100644 --- a/src/Link/Protocol/V1/ImpressionListenerData.php +++ b/src/Link/Protocol/V1/ImpressionListenerData.php @@ -35,10 +35,7 @@ public function getTimestamp(): int public static function fromRaw(/*mixed*/$raw)/*: mixed*/ { - if (!is_array($raw)) { - throw new \InvalidArgumentException("TreatmentResponse must be parsed from an array. Got a " . gettype($raw)); - } - + Enforce::isArray($raw); return new ImpressionListenerData(Enforce::isString($raw['l']), Enforce::isInt($raw['c']), enforce::isInt($raw['m'])); } } diff --git a/src/Link/Protocol/V1/OpCode.php b/src/Link/Protocol/V1/OpCode.php index 90ffb70..e8c105a 100644 --- a/src/Link/Protocol/V1/OpCode.php +++ b/src/Link/Protocol/V1/OpCode.php @@ -29,5 +29,7 @@ class OpCode extends Enum private const Track = 0x80; + private const SplitNames = 0xA0; + private const Split = 0xA1; + private const Splits = 0xA2; } - diff --git a/src/Link/Protocol/V1/OperationFailureException.php b/src/Link/Protocol/V1/OperationFailureException.php new file mode 100644 index 0000000..ee90720 --- /dev/null +++ b/src/Link/Protocol/V1/OperationFailureException.php @@ -0,0 +1,7 @@ +getValue() => $key, - TreatmentArgs::BUCKETING_KEY()->getValue() => $bucketingKey, - TreatmentArgs::FEATURE()->getValue() => $feature, - TreatmentArgs::ATTRIBUTES()->getValue() => ($attributes != null && count($attributes) > 0) - ? $attributes - : null, - ) - ); + return self::_forTreatment($key, $bucketingKey, $feature, $attributes, false); + } + + public static function forTreatmentWithConfig(string $key, ?string $bucketingKey, string $feature, ?array $attributes): RPC + { + return self::_forTreatment($key, $bucketingKey, $feature, $attributes, true); } public static function forTreatments(string $key, ?string $bucketingKey, array $features, ?array $attributes): RPC { - return new RPC( - Version::V1(), - OpCode::Treatments(), - array( - TreatmentsArgs::KEY()->getValue() => $key, - TreatmentsArgs::BUCKETING_KEY()->getValue() => $bucketingKey, - TreatmentsArgs::FEATURES()->getValue() => $features, - TreatmentsArgs::ATTRIBUTES()->getValue() => ($attributes != null && count($attributes) > 0) - ? $attributes - : null, - ) - ); + return self::_forTreatments($key, $bucketingKey, $features, $attributes, false); + } + + public static function forTreatmentsWithConfig(string $key, ?string $bucketingKey, array $features, ?array $attributes): RPC + { + return self::_forTreatments($key, $bucketingKey, $features, $attributes, true); } public static function forTrack( @@ -99,6 +87,21 @@ public static function forTrack( ); } + public static function forSplitNames(): RPC + { + return new RPC(Version::V1(), OpCode::SplitNames(), []); + } + + public static function forSplit(string $splitName): RPC + { + return new RPC(Version::V1(), OpCode::Split(), [SplitArgs::SPLIT_NAME()->getValue() => $splitName]); + } + + public static function forSplits(): RPC + { + return new RPC(Version::V1(), OpCode::Splits(), []); + } + function getSerializable() /* : mixed */ { return array( @@ -107,4 +110,32 @@ function getSerializable() /* : mixed */ "a" => $this->getArgs(), ); } + + private static function _forTreatment(string $k, ?string $bk, string $f, ?array $a, bool $includeConfig): RPC + { + return new RPC( + Version::V1(), + $includeConfig ? OpCode::TreatmentWithConfig() : OpCode::Treatment(), + array( + TreatmentArgs::KEY()->getValue() => $k, + TreatmentArgs::BUCKETING_KEY()->getValue() => $bk, + TreatmentArgs::FEATURE()->getValue() => $f, + TreatmentArgs::ATTRIBUTES()->getValue() => ($a != null && count($a) > 0) ? $a : null, + ) + ); + } + + public static function _forTreatments(string $k, ?string $bk, array $f, ?array $a, bool $includeConfig): RPC + { + return new RPC( + Version::V1(), + $includeConfig ? OpCode::TreatmentsWithConfig() : OpCode::Treatments(), + [ + TreatmentsArgs::KEY()->getValue() => $k, + TreatmentsArgs::BUCKETING_KEY()->getValue() => $bk, + TreatmentsArgs::FEATURES()->getValue() => $f, + TreatmentsArgs::ATTRIBUTES()->getValue() => ($a != null && count($a) > 0) ? $a : null, + ] + ); + } } diff --git a/src/Link/Protocol/V1/RegisterResponse.php b/src/Link/Protocol/V1/RegisterResponse.php new file mode 100644 index 0000000..adb48ab --- /dev/null +++ b/src/Link/Protocol/V1/RegisterResponse.php @@ -0,0 +1,15 @@ +result; } - static abstract function fromRaw(/*mixed*/ $raw)/*: mixed*/; + public function ensureSuccess(): void + { + if ($this->result != Result::Ok()) { + throw new OperationFailureException("operation failed with status code: " . $this->result->getKey()); + } + } } diff --git a/src/Link/Protocol/V1/SplitArgs.php b/src/Link/Protocol/V1/SplitArgs.php new file mode 100644 index 0000000..4f2e180 --- /dev/null +++ b/src/Link/Protocol/V1/SplitArgs.php @@ -0,0 +1,17 @@ +splitNames = $splitNames; + } + + public function getSplitNames(): array + { + return $this->splitNames; + } + + public static function fromRaw(/*array*/$raw): SplitNamesResponse + { + Enforce::isArray($raw); + $payload = Enforce::isArray($raw['p']); + return new SplitNamesResponse(Result::from(Enforce::isInt($raw['s'])), Enforce::isArray($payload['n'])); + } +} diff --git a/src/Link/Protocol/V1/SplitResponse.php b/src/Link/Protocol/V1/SplitResponse.php new file mode 100644 index 0000000..cc3eb70 --- /dev/null +++ b/src/Link/Protocol/V1/SplitResponse.php @@ -0,0 +1,32 @@ +splitView = $splitView; + } + + public function getView(): ?SplitViewResult + { + return $this->splitView; + } + + public static function fromRaw(/*array*/$raw): SplitResponse + { + Enforce::isArray($raw); + return new SplitResponse( + Result::from(Enforce::isInt($raw['s'])), + isset($raw['p']) ? SplitViewResult::fromRaw(Enforce::isArray($raw['p'])) : null + ); + } +} diff --git a/src/Link/Protocol/V1/SplitViewResult.php b/src/Link/Protocol/V1/SplitViewResult.php new file mode 100644 index 0000000..c73c927 --- /dev/null +++ b/src/Link/Protocol/V1/SplitViewResult.php @@ -0,0 +1,66 @@ +name = $name; + $this->trafficType = $trafficType; + $this->killed = $killed; + $this->treatments = $treatments; + $this->changeNumber = $changeNumber; + $this->configs = $configs; + } + + public function getName(): string + { + return $this->name; + } + + public function getTrafficType(): string + { + return $this->trafficType; + } + + public function getKilled(): bool + { + return $this->killed; + } + + public function getTreatments(): array + { + return $this->treatments; + } + + public function getChangeNumber(): int + { + return $this->changeNumber; + } + + public function getConfigs(): ?array + { + return $this->configs; + } + public static function fromRaw(array $raw): SplitViewResult + { + return new SplitViewResult( + Enforce::isString($raw['n']), + Enforce::isString($raw['t']), + Enforce::isBool($raw['k']), + Enforce::isArray($raw['s']), + Enforce::isInt($raw['c']), + isset($raw['f']) ? Enforce::isArray($raw['f']) : null + ); + } +} diff --git a/src/Link/Protocol/V1/SplitsResponse.php b/src/Link/Protocol/V1/SplitsResponse.php new file mode 100644 index 0000000..964dd1a --- /dev/null +++ b/src/Link/Protocol/V1/SplitsResponse.php @@ -0,0 +1,35 @@ +splitViews = $splitViews; + } + + public function getViews(): array + { + return $this->splitViews; + } + + public static function fromRaw(/*array*/$raw): SplitsResponse + { + Enforce::isArray($raw); + $payload = Enforce::isArray($raw['p']); + return new SplitsResponse( + Result::from(Enforce::isInt($raw['s'])), + array_map(function ($e) { + return SplitViewResult::fromRaw(Enforce::isArray($e)); + }, Enforce::isArray($payload['s'])) + ); + } +} diff --git a/src/Link/Protocol/V1/TrackResponse.php b/src/Link/Protocol/V1/TrackResponse.php index c8a3389..f5459b5 100644 --- a/src/Link/Protocol/V1/TrackResponse.php +++ b/src/Link/Protocol/V1/TrackResponse.php @@ -8,25 +8,26 @@ class TrackResponse extends Response { - private $success; + private $eventQueued; - public function __construct(Result $status, bool $success) + public function __construct(Result $status, bool $eventQueued) { parent::__construct($status); - $this->success = $success; + $this->eventQueued = $eventQueued; } - public function getSuccess(): bool + public function getEventQueued(): bool { - return $this->success; + return $this->eventQueued; } - public static function fromRaw(/*mixed*/ $raw)/*: mixed*/ + public static function fromRaw(/*mixed*/$raw)/*: mixed*/ { $raw = Enforce::isArray($raw); $payload = Enforce::isArray($raw['p']); return new TrackResponse( Result::from(Enforce::isInt($raw['s'])), - Enforce::isBool($payload['s'])); + Enforce::isBool($payload['s']) + ); } } diff --git a/src/Link/Protocol/V1/TreatmentsResponse.php b/src/Link/Protocol/V1/TreatmentsResponse.php index 5df7f6c..fd64de2 100644 --- a/src/Link/Protocol/V1/TreatmentsResponse.php +++ b/src/Link/Protocol/V1/TreatmentsResponse.php @@ -28,10 +28,7 @@ public function getEvaluationResult(int $index): ?EvaluationResult public static function fromRaw(/*mixed*/$raw)/*: mixed*/ { - if (!is_array($raw)) { - throw new \InvalidArgumentException("TreatmentResponse must be parsed from an array. Got a " . gettype($raw)); - } - + Enforce::isArray($raw); return new TreatmentsResponse( Result::from(Enforce::isInt($raw['s'])), array_map( diff --git a/src/Link/Transfer/UnixPacket.php b/src/Link/Transfer/UnixPacket.php index 8a09983..2351d22 100644 --- a/src/Link/Transfer/UnixPacket.php +++ b/src/Link/Transfer/UnixPacket.php @@ -4,7 +4,7 @@ class UnixPacket implements RawConnection { - const DEFAULT_RECEIVE_BUFFER_SIZE = 64 * 1024; // 64k + const DEFAULT_RECEIVE_BUFFER_SIZE = 212992; // linux default private /*string*/ $targetSockFN; private /*\Socket*/ $sock; @@ -33,20 +33,25 @@ public function __construct(string $targetSockFN, array $options = array()) } $curr = @socket_get_option($this->sock, SOL_SOCKET, SO_SNDBUF); if ($curr != $options['sendBufferSize']) { - throw new ConnectionException(sprintf("send-buffer allocation failed. Expected=%d, Actual=%d", - $options['sendBufferSize'], $curr)); + throw new ConnectionException(sprintf( + "send-buffer allocation failed. Expected=%d, Actual=%d", + $options['sendBufferSize'], + $curr + )); } - } if (isset($options['recvBufferSize'])) { if (!@socket_set_option($this->sock, SOL_SOCKET, SO_RCVBUF, $options['recvBufferSize'])) { throw new ConnectionException("cannot allocate requested receive-buffer size. please check your OS config"); - $curr = @socket_get_option($this->sock, SOL_SOCKET, SO_RCVBUF); - if ($curr != $options['recvBufferSize']) { - throw new ConnectionException(sprintf("receive-buffer allocation failed. Expected=%d, Actual=%d", - $options['recvBufferSize'], $curr)); - } + $curr = @socket_get_option($this->sock, SOL_SOCKET, SO_RCVBUF); + if ($curr != $options['recvBufferSize']) { + throw new ConnectionException(sprintf( + "receive-buffer allocation failed. Expected=%d, Actual=%d", + $options['recvBufferSize'], + $curr + )); + } } $this->maxRecvSize = $options['recvBufferSize']; } @@ -86,5 +91,4 @@ public function __destruct() { @socket_close($this->sock); } - } diff --git a/src/Manager.php b/src/Manager.php new file mode 100644 index 0000000..1221932 --- /dev/null +++ b/src/Manager.php @@ -0,0 +1,53 @@ +link = $lm; + $this->logger = $logger; + } + public function splitNames(): array + { + try { + return $this->link->splitNames(); + } catch (\Exception $exc) { + $this->logger->error("failed to fetch split names: " . $exc->getMessage()); + $this->logger->debug("full trace:" . $exc); + } + return []; + } + + public function split(string $name): ?SplitView + { + try { + return $this->link->split($name); + } catch (\Exception $exc) { + $this->logger->error("failed to fetch split information: " . $exc->getMessage()); + $this->logger->debug("full trace:" . $exc); + } + return null; + } + + public function splits(): array + { + try { + return $this->link->splits(); + } catch (\Exception $exc) { + $this->logger->error("failed to fetch all splits information: " . $exc->getMessage()); + $this->logger->debug("full trace:" . $exc); + } + return []; + } +} diff --git a/src/ManagerInterface.php b/src/ManagerInterface.php new file mode 100644 index 0000000..0e36313 --- /dev/null +++ b/src/ManagerInterface.php @@ -0,0 +1,12 @@ +name = $name; + $this->trafficType = $trafficType; + $this->killed = $killed; + $this->treatments = $treatments; + $this->changeNumber = $changeNumber; + $this->configs = $configs; + } + + public function getName(): string + { + return $this->name; + } + + public function getTrafficType(): string + { + return $this->trafficType; + } + + public function getKilled(): bool + { + return $this->killed; + } + + public function getTreatments(): array + { + return $this->treatments; + } + + public function getChangeNumber(): int + { + return $this->changeNumber; + } + + public function getConfigs(): ?array + { + return $this->configs; + } +} diff --git a/src/Version.php b/src/Version.php index bd729e0..ea47932 100644 --- a/src/Version.php +++ b/src/Version.php @@ -4,5 +4,5 @@ class Version { - const CURRENT = '1.1.0'; + const CURRENT = '1.2.0'; } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index e87592f..83c0bed 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -1,6 +1,6 @@ createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentWithConfig') + ->with('someKey', 'someBuck', 'someFeature', ['someAttr' => 123]) + ->willReturn(['on', new ImpressionListenerData('lab1', 123, 123456), '{"a": 1}']); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->once()) + ->method('accept') + ->with(new Impression('someKey', 'someBuck', 'someFeature', 'on', 'lab1', 123, 123456), ['someAttr' => 123]); + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + ['treatment' => 'on', 'config' => '{"a": 1}'], + $client->getTreatmentWithConfig('someKey', 'someBuck', 'someFeature', ['someAttr' => 123]) + ); + } + + public function testGetTreatmentsWithConfigAndListener() + { + $manager = $this->createMock(Manager::class); + $manager->expects($this->once())->method('getTreatmentsWithConfig') + ->with('someKey', 'someBuck', ['someFeature1', 'someFeature2', 'someFeature3'], ['someAttr' => 123]) + ->willReturn([ + 'someFeature1' => ['on', new ImpressionListenerData('lab1', 123, 123456), null], + 'someFeature2' => ['off', new ImpressionListenerData('lab1', 124, 123457), null], + 'someFeature3' => ['n/a', new ImpressionListenerData('lab1', 125, 123458), '{"a": 2}'], + ]); + + $ilMock = $this->createMock(ImpressionListener::class); + $ilMock->expects($this->exactly(3)) + ->method('accept') + ->withConsecutive( + [new Impression('someKey', 'someBuck', 'someFeature1', 'on', 'lab1', 123, 123456), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature2', 'off', 'lab1', 124, 123457), ['someAttr' => 123]], + [new Impression('someKey', 'someBuck', 'someFeature3', 'n/a', 'lab1', 125, 123458), ['someAttr' => 123]] + ); + + + $client = new Client($manager, $this->logger, $ilMock); + $this->assertEquals( + [ + 'someFeature1' => ['treatment' => 'on', 'config' => null], + 'someFeature2' => ['treatment' => 'off', 'config' => null], + 'someFeature3' => ['treatment' => 'n/a', 'config' => '{"a": 2}'] + ], + $client->getTreatmentsWithConfig('someKey', 'someBuck', ['someFeature1', 'someFeature2', 'someFeature3'], ['someAttr' => 123]) + ); + } + public function testGetTreatmentExceptionReturnsControl() { $manager = $this->createMock(Manager::class); diff --git a/tests/Config/FallbackTest.php b/tests/Config/FallbackTest.php new file mode 100644 index 0000000..6c96bbe --- /dev/null +++ b/tests/Config/FallbackTest.php @@ -0,0 +1,38 @@ +assertEquals(false, $cfg->disable()); + $this->assertEquals(new AlwaysControlClient(), $cfg->client()); + $this->assertEquals(new AlwaysEmptyManager(), $cfg->manager()); + } + + public function testConfigParsing() + { + $clientMock = $this->createMock(ClientInterface::class); + $managerMock = $this->createMock(ManagerInterface::class); + $cfg = Fallback::fromArray([ + 'disable' => true, + 'client' => $clientMock, + 'manager' => $managerMock, + ]); + + $this->assertEquals(true, $cfg->disable()); + $this->assertEquals($clientMock, $cfg->client()); + $this->assertEquals($managerMock, $cfg->manager()); + } +} diff --git a/tests/FactoryTest.php b/tests/FactoryTest.php new file mode 100644 index 0000000..094504c --- /dev/null +++ b/tests/FactoryTest.php @@ -0,0 +1,104 @@ +extendWith(new TimestampExtension(), new DateTimeExtension()); + + $socketServerRC = new SocketServerRemoteControl(); + + try { + $serverAddress = sys_get_temp_dir() . "/php_thin_client_tests_seqpacket.sock"; + $socketServerRC->start(SocketServerRemoteControl::UNIX_STREAM, $serverAddress, 1, [ + [ + 'expects' => $packer->pack(RPC::forRegister("someId", new RegisterFlags(false))->getSerializable()), + 'returns' => $packer->pack(['s' => 0x01]), + ], + ]); + + $socketServerRC->awaitServerReady(); + + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $factory = Factory::withConfig([ + 'transfer' => [ + 'address' => $serverAddress, + 'type' => 'unix-stream', + ], + 'logging' => ['psr-instance' => $logger], + ]); + + $this->assertEquals('SplitIO\ThinSdk\Client', get_class($factory->client())); + } finally { + $socketServerRC->shutdown(); + } + } + + public function testGetFactoryLinkErrorWithFallback() + { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $factory = Factory::withConfig([ + 'transfer' => [ + 'address' => '/non/existant/file', + 'type' => 'unix-stream', + ], + 'logging' => ['psr-instance' => $logger], + ]); + $this->assertEquals(new AlwaysControlClient(), $factory->client()); + $this->assertEquals(new AlwaysEmptyManager(), $factory->manager()); + } + + public function testGetFactoryLinkErrorNoFallback() + { + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $this->expectExceptionMessage('failed to connect to remote socket /non/existant/file: No such file or directory'); + Factory::withConfig([ + 'transfer' => [ + 'address' => '/non/existant/file', + 'type' => 'unix-stream', + ], + 'logging' => ['psr-instance' => $logger], + 'fallback' => ['disable' => true], + ]); + } + + public function testGetFactoryLinkErrorCustomFallback() + { + $client = $this->createMock(ClientInterface::class); + $manager = $this->createMock(ManagerInterface::class); + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $factory = Factory::withConfig([ + 'transfer' => [ + 'address' => '/non/existant/file', + 'type' => 'unix-stream', + ], + 'logging' => ['psr-instance' => $logger], + 'fallback' => ['client' => $client, 'manager' => $manager], + ]); + $this->assertEquals($client, $factory->client()); + $this->assertEquals($manager, $factory->manager()); + } +} diff --git a/tests/Fallback/AlwaysEmptyManagerTest.php b/tests/Fallback/AlwaysEmptyManagerTest.php new file mode 100644 index 0000000..17750a8 --- /dev/null +++ b/tests/Fallback/AlwaysEmptyManagerTest.php @@ -0,0 +1,30 @@ +assertEquals([], $m->splitNames()); + } + + public function testGetTreatments() + { + $m = new AlwaysEmptyManager(); + $this->assertEquals(null, $m->split('some')); + $this->assertEquals(null, $m->split('')); + } + + public function testTrack() + { + $m = new AlwaysEmptyManager(); + $this->assertEquals([], $m->splits()); + } +} diff --git a/tests/Fallback/AwaysControlClientTest.php b/tests/Fallback/AwaysControlClientTest.php new file mode 100644 index 0000000..9989663 --- /dev/null +++ b/tests/Fallback/AwaysControlClientTest.php @@ -0,0 +1,38 @@ +assertEquals("control", $c->getTreatment("key", null, "feature", null)); + $this->assertEquals("control", $c->getTreatment("key", "buck", "feature", ['a' => 1])); + $this->assertEquals("control", $c->getTreatment("", null, "", null)); + } + + public function testGetTreatments() + { + $c = new AlwaysControlClient(); + $this->assertEquals( + ["f1" => "control", "f2" => "control"], + $c->getTreatments("key", null, ["f1", "f2"], null) + ); + $this->assertEquals([], $c->getTreatments("key", null, [], null)); + } + + public function testTrack() + { + $c = new AlwaysControlClient(); + $this->assertEquals(false, $c->track("k", "tt", "et", null, null)); + $this->assertEquals(false, $c->track("k", "tt", "et", 1.23, null)); + $this->assertEquals(false, $c->track("k", "tt", "et", null, [])); + $this->assertEquals(false, $c->track("k", "tt", "et", null, ['a' => 1])); + } +} diff --git a/tests/Fallback/TestGenericFallbackFactory.php b/tests/Fallback/TestGenericFallbackFactory.php new file mode 100644 index 0000000..50b8492 --- /dev/null +++ b/tests/Fallback/TestGenericFallbackFactory.php @@ -0,0 +1,23 @@ +createMock(ClientInterface::class); + $manager = $this->createMock(ManagerInterface::class); + $factory = new GenericFallbackFactory($client, $manager); + + $this->assertEquals($client, $factory->client()); + $this->assertEquals($manager, $factory->manager()); + } +} diff --git a/tests/Link/Consumer/V1ManagerTest.php b/tests/Link/Consumer/V1ManagerTest.php index 9892e4d..7d5e219 100644 --- a/tests/Link/Consumer/V1ManagerTest.php +++ b/tests/Link/Consumer/V1ManagerTest.php @@ -13,6 +13,7 @@ use SplitIO\ThinSdk\Link\Transfer\ConnectionException; use SplitIO\ThinSdk\Link\Serialization\SerializerFactory; use SplitIO\ThinSdk\Link\Serialization\Serializer; +use \SplitIO\ThinSdk\SplitView; use PHPUnit\Framework\TestCase; @@ -29,46 +30,70 @@ public function setUp(): void public function testHappyExchangeNoImpListener(): void { $connMock = $this->createMock(RawConnection::class); - $connMock->expects($this->exactly(3)) + $connMock->expects($this->exactly(5)) ->method('sendMessage') - ->withConsecutive(['serializedRegister'], ['serializedTreatment'], ['serializedTreatments']); - $connMock->expects($this->exactly(3)) + ->withConsecutive( + ['serializedRegister'], + ['serializedTreatment'], + ['serializedTreatments'], + ['serializedTreatmentWithConfig'], + ['serializedTreatmentsWithConfig'] + ); + $connMock->expects($this->exactly(5)) ->method('readMessage') - ->willReturnOnConsecutiveCalls('serializedRegisterResp', 'serializedTreatmentResp', 'serializedTreatmentsResp'); + ->willReturnOnConsecutiveCalls( + 'serializedRegisterResp', + 'serializedTreatmentResp', + 'serializedTreatmentsResp', + 'serilaizedTreatmentWithCnfigResp', + 'serializedTreatmentsWithConfig' + ); $connFactoryMock = $this->createMock(ConnectionFactory::class); $connFactoryMock->expects($this->once())->method('create')->willReturn($connMock); $serializerMock = $this->createMock(Serializer::class); - $serializerMock->expects($this->exactly(3)) + $serializerMock->expects($this->exactly(5)) ->method('serialize') ->withConsecutive( [RPC::forRegister('someId', new RegisterFlags(false))], [RPC::forTreatment("k", "b", "f", ["a" => 1])], [RPC::forTreatments("k", "b", ["f1", "f2", "f3"], ["a" => 1])], + [RPC::forTreatmentWithConfig("k", "b", "f", ["a" => 1])], + [RPC::forTreatmentsWithConfig("k", "b", ["f1", "f2", "f3"], ["a" => 1])], ) - ->willReturnOnConsecutiveCalls('serializedRegister', 'serializedTreatment', 'serializedTreatments'); + ->willReturnOnConsecutiveCalls( + 'serializedRegister', + 'serializedTreatment', + 'serializedTreatments', + 'serializedTreatmentWithConfig', + 'serializedTreatmentsWithConfig' + ); - $serializerMock->expects($this->exactly(3)) + $serializerMock->expects($this->exactly(5)) ->method('deserialize') ->withConsecutive(['serializedRegisterResp'], ['serializedTreatmentResp'], ['serializedTreatmentsResp']) ->willReturnOnConsecutiveCalls( ['s' => 0x01], ['s' => 0x01, 'p' => ['t' => 'on']], - ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off']]]] + ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off']]]], + ['s' => 0x01, 'p' => ['t' => 'on', 'c' => '{"a": 1}']], + ['s' => 0x01, 'p' => ['r' => [['t' => 'on'], ['t' => 'on'], ['t' => 'off', 'c' => '{"a": 2}']]]] ); $serializerFactoryMock = $this->createMock(SerializerFactory::class); $serializerFactoryMock->expects($this->once())->method('create')->willReturn($serializerMock); $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); + $this->assertEquals(['on', null], $v1Manager->getTreatment("k", "b", "f", ["a" => 1])); $this->assertEquals( - ['on', null, null], - $v1Manager->getTreatment("k", "b", "f", ["a" => 1]) + ['f1' => ['on', null], 'f2' => ['on', null], 'f3' => ['off', null]], + $v1Manager->getTreatments('k', 'b', ['f1', 'f2', 'f3'], ['a' => 1]) ); + $this->assertEquals(['on', null, '{"a": 1}'], $v1Manager->getTreatmentWithConfig("k", "b", "f", ["a" => 1])); $this->assertEquals( - ['f1' => ['on', null, null], 'f2' => ['on', null, null], 'f3' => ['off', null, null]], - $v1Manager->getTreatments('k', 'b', ['f1', 'f2', 'f3'], ['a' => 1]) + ['f1' => ['on', null, null], 'f2' => ['on', null, null], 'f3' => ['off', null, '{"a": 2}']], + $v1Manager->getTreatmentsWithConfig('k', 'b', ['f1', 'f2', 'f3'], ['a' => 1]) ); } @@ -111,14 +136,14 @@ public function testHappyExchangeWithImpListener(): void $ilMock = $this->createMock(ImpressionListener::class); $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::fromArray(['impressionListener' => $ilMock]), $this->logger); $this->assertEquals( - ['on', new ImpressionListenerData('lab1', 123, 1234), null], + ['on', new ImpressionListenerData('lab1', 123, 1234)], $v1Manager->getTreatment("k", "b", "f", ["a" => 1]) ); $this->assertEquals( [ - 'f1' => ['on', new ImpressionListenerData('lab1', 123, 1234), null], - 'f2' => ['on', new ImpressionListenerData('lab2', 124, 1235), null], - 'f3' => ['off', new ImpressionListenerData('lab3', 125, 1236), null], + 'f1' => ['on', new ImpressionListenerData('lab1', 123, 1234)], + 'f2' => ['on', new ImpressionListenerData('lab2', 124, 1235)], + 'f3' => ['off', new ImpressionListenerData('lab3', 125, 1236)], ], $v1Manager->getTreatments('k', 'b', ['f1', 'f2', 'f3'], ['a' => 1]) ); @@ -198,10 +223,7 @@ public function testPostRegisterRPCsAreRetried(): void $serializerFactoryMock->expects($this->once())->method('create')->willReturn($serializerMock); $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); - $this->assertEquals( - ['on', null, null], - $v1Manager->getTreatment("k", "b", "f", ["a" => 1]) - ); + $this->assertEquals(['on', null], $v1Manager->getTreatment("k", "b", "f", ["a" => 1])); } public function test2FailuresCrash(): void @@ -257,7 +279,7 @@ public function test2FailuresCrash(): void $v1Manager->getTreatment("k", "b", "f", ["a" => 1]); } - public function testTrackHappyPath(): void + public function testTrack(): void { $connMock = $this->createMock(RawConnection::class); $connMock->expects($this->exactly(2)) @@ -294,6 +316,146 @@ public function testTrackHappyPath(): void $this->assertEquals(true, $v1Manager->track('k', 'tt', 'et', 1.25, ['a' => 1])); } + public function testSplitNames(): void + { + $connMock = $this->createMock(RawConnection::class); + $connMock->expects($this->exactly(2)) + ->method('sendMessage') + ->withConsecutive(['serializedRegister'], ['serializedSplitNames']); + $connMock->expects($this->exactly(2)) + ->method('readMessage') + ->willReturnOnConsecutiveCalls('serializedRegisterResp', 'serializedSplitNamesResp'); -} + $connFactoryMock = $this->createMock(ConnectionFactory::class); + $connFactoryMock->expects($this->once())->method('create')->willReturn($connMock); + + $serializerMock = $this->createMock(Serializer::class); + $serializerMock->expects($this->exactly(2)) + ->method('serialize') + ->withConsecutive( + [RPC::forRegister('someId', new RegisterFlags(false))], + [RPC::forSplitNames()], + ) + ->willReturnOnConsecutiveCalls('serializedRegister', 'serializedSplitNames'); + + $serializerMock->expects($this->exactly(2)) + ->method('deserialize') + ->withConsecutive(['serializedRegisterResp'], ['serializedSplitNamesResp']) + ->willReturnOnConsecutiveCalls( + ['s' => 0x01], + ['s' => 0x01, 'p' => ['n' => ['s1', 's2']]], + ); + $serializerFactoryMock = $this->createMock(SerializerFactory::class); + $serializerFactoryMock->expects($this->once())->method('create')->willReturn($serializerMock); + + $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); + $this->assertEquals(['s1', 's2'], $v1Manager->splitNames()); + } + + public function testSplit(): void + { + $connMock = $this->createMock(RawConnection::class); + $connMock->expects($this->exactly(2)) + ->method('sendMessage') + ->withConsecutive(['serializedRegister'], ['serializedSplit']); + $connMock->expects($this->exactly(2)) + ->method('readMessage') + ->willReturnOnConsecutiveCalls('serializedRegisterResp', 'serializedSplitResp'); + + $connFactoryMock = $this->createMock(ConnectionFactory::class); + $connFactoryMock->expects($this->once())->method('create')->willReturn($connMock); + + $serializerMock = $this->createMock(Serializer::class); + $serializerMock->expects($this->exactly(2)) + ->method('serialize') + ->withConsecutive( + [RPC::forRegister('someId', new RegisterFlags(false))], + [RPC::forSplit('someName')], + ) + ->willReturnOnConsecutiveCalls('serializedRegister', 'serializedSplit'); + + $serializerMock->expects($this->exactly(2)) + ->method('deserialize') + ->withConsecutive(['serializedRegisterResp'], ['serializedSplitResp']) + ->willReturnOnConsecutiveCalls( + ['s' => 0x01], + ['s' => 0x01, 'p' => [ + 'n' => 'someName', + 't' => 'someTrafficType', + 'k' => true, + 's' => ['on', 'off'], + 'c' => 123, + 'f' => ['on' => 'some'], + ]], + ); + + $serializerFactoryMock = $this->createMock(SerializerFactory::class); + $serializerFactoryMock->expects($this->once())->method('create')->willReturn($serializerMock); + + $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); + $this->assertEquals( + new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, ['on' => 'some']), + $v1Manager->split('someName') + ); + } + public function testSplits(): void + { + $connMock = $this->createMock(RawConnection::class); + $connMock->expects($this->exactly(2)) + ->method('sendMessage') + ->withConsecutive(['serializedRegister'], ['serializedSplits']); + $connMock->expects($this->exactly(2)) + ->method('readMessage') + ->willReturnOnConsecutiveCalls('serializedRegisterResp', 'serializedSplitsResp'); + + $connFactoryMock = $this->createMock(ConnectionFactory::class); + $connFactoryMock->expects($this->once())->method('create')->willReturn($connMock); + + $serializerMock = $this->createMock(Serializer::class); + $serializerMock->expects($this->exactly(2)) + ->method('serialize') + ->withConsecutive( + [RPC::forRegister('someId', new RegisterFlags(false))], + [RPC::forSplits()], + ) + ->willReturnOnConsecutiveCalls('serializedRegister', 'serializedSplits'); + + $serializerMock->expects($this->exactly(2)) + ->method('deserialize') + ->withConsecutive(['serializedRegisterResp'], ['serializedSplitsResp']) + ->willReturnOnConsecutiveCalls( + ['s' => 0x01], + ['s' => 0x01, 'p' => ['s' => [ + [ + 'n' => 'someName', + 't' => 'someTrafficType', + 'k' => true, + 's' => ['on', 'off'], + 'c' => 123, + 'f' => ['on' => 'some'], + ], + [ + 'n' => 'someName2', + 't' => 'someTrafficType', + 'k' => false, + 's' => ['on', 'off'], + 'c' => 124, + 'f' => null, + ], + ]]], + ); + + $serializerFactoryMock = $this->createMock(SerializerFactory::class); + $serializerFactoryMock->expects($this->once())->method('create')->willReturn($serializerMock); + + $v1Manager = new V1Manager($connFactoryMock, $serializerFactoryMock, Utils::default(), $this->logger); + $this->assertEquals( + [ + new SplitView('someName', 'someTrafficType', true, ['on', 'off'], 123, ['on' => 'some']), + new SplitView('someName2', 'someTrafficType', false, ['on', 'off'], 124, null), + ], + $v1Manager->splits() + ); + } +} diff --git a/tests/Link/Protocol/V1/SplitNamesResponseTest.php b/tests/Link/Protocol/V1/SplitNamesResponseTest.php new file mode 100644 index 0000000..9c82618 --- /dev/null +++ b/tests/Link/Protocol/V1/SplitNamesResponseTest.php @@ -0,0 +1,41 @@ + 0x01, 'p' => ['n' => ["s1", "s2"]]]; + $this->assertEquals(new SplitNamesResponse(Result::Ok(), ["s1", "s2"]), SplitNamesResponse::fromRaw($raw)); + + $raw = ['s' => 0x01, 'p' => ['n' => []]]; + $this->assertEquals(new SplitNamesResponse(Result::Ok(), []), SplitNamesResponse::fromRaw($raw)); + } + + public function testParsingNonIntStatus(): void + { + $this->expectExceptionMessageMatches("/^expected an int .*/"); + SplitNamesResponse::fromRaw(['s' => [], 'p' => []]); + } + + public function testParsingNonArrayPayload(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + SplitNamesResponse::fromRaw(['s' => 0x01, 'p' => 1]); + } + + public function testParsingNonArraySplitList(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + SplitNamesResponse::fromRaw(['s' => 0x01, 'p' => ['n' => 3]]); + } +} diff --git a/tests/Link/Protocol/V1/SplitResponseTest.php b/tests/Link/Protocol/V1/SplitResponseTest.php new file mode 100644 index 0000000..829b148 --- /dev/null +++ b/tests/Link/Protocol/V1/SplitResponseTest.php @@ -0,0 +1,51 @@ + 0x01, 'p' => [ + 'n' => 'someName', + 't' => 'someTrafficType', + 'k' => true, + 's' => ['on', 'off'], + 'c' => 123, + 'f' => ['on' => 'some'], + ]]; + $this->assertEquals( + new SplitResponse(Result::Ok(), new SplitViewResult("someName", "someTrafficType", true, ['on', 'off'], 123, ['on' => 'some'])), + SplitResponse::fromRaw($raw) + ); + + $this->assertEquals(new SplitResponse(Result::Ok(), null), SplitResponse::fromRaw(['s' => 0x01, 'p' => null])); + } + + public function testParsingNonIntStatus(): void + { + $this->expectExceptionMessageMatches("/^expected an int .*/"); + SplitResponse::fromRaw(['s' => [], 'p' => []]); + } + + public function testParsingNonArrayPayload(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + SplitResponse::fromRaw(['s' => 0x01, 'p' => 1]); + } + + public function testParsingNonArraySplitList(): void + { + $this->expectExceptionMessageMatches("/^expected a string .*/"); + SplitResponse::fromRaw(['s' => 0x01, 'p' => ['n' => 3]]); + } +} diff --git a/tests/Link/Protocol/V1/SplitsResponseTest.php b/tests/Link/Protocol/V1/SplitsResponseTest.php new file mode 100644 index 0000000..fb20ae5 --- /dev/null +++ b/tests/Link/Protocol/V1/SplitsResponseTest.php @@ -0,0 +1,53 @@ + 0x01, + 'p' => ['s' => [ + ['n' => 's1', 't' => 'someTrafficType', 'k' => true, 's' => ['on', 'off'], 'c' => 123, 'f' => ['on' => 'some']], + ['n' => 's2', 't' => 'someTrafficType', 'k' => false, 's' => ['on', 'off'], 'c' => 124, 'f' => null], + ]] + ]; + $this->assertEquals( + new SplitsResponse(Result::Ok(), [ + new SplitViewResult("s1", "someTrafficType", true, ['on', 'off'], 123, ['on' => 'some']), + new SplitViewResult("s2", "someTrafficType", false, ['on', 'off'], 124, null), + ]), + SplitsResponse::fromRaw($raw) + ); + + $this->assertEquals(new SplitsResponse(Result::Ok(), []), SplitsResponse::fromRaw(['s' => 0x01, 'p' => ['s' => []]])); + } + + public function testParsingNonIntStatus(): void + { + $this->expectExceptionMessageMatches("/^expected an int .*/"); + SplitsResponse::fromRaw(['s' => [], 'p' => []]); + } + + public function testParsingNonArrayPayload(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + SplitsResponse::fromRaw(['s' => 0x01, 'p' => 1]); + } + + public function testParsingNonArraySplitList(): void + { + $this->expectExceptionMessageMatches("/^expected an array .*/"); + SplitsResponse::fromRaw(['s' => 0x01, 'p' => ['s' => 3]]); + } +} diff --git a/tests/ManagerTest.php b/tests/ManagerTest.php new file mode 100644 index 0000000..91a8dd9 --- /dev/null +++ b/tests/ManagerTest.php @@ -0,0 +1,91 @@ +logger = $this->createStub(LoggerInterface::class); + } + + public function testSplitNamesOk() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('splitNames') + ->with() + ->willReturn(['s1', 's2']); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals(['s1', 's2'], $manager->splitNames()); + } + + public function testSplitNamesReturnsEmptyOnException() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('splitNames') + ->will($this->throwException(new \Exception('sarasa'))); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals([], $manager->splitNames()); + } + + public function testSplitOk() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('split') + ->with('s1') + ->willReturn(new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula'])); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals(new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), $manager->split('s1')); + } + + public function testSplitReturnNullOnException() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('split') + ->with('s1') + ->will($this->throwException(new \Exception('sarasa'))); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals(null, $manager->split('s1')); + } + + public function testSplitsOk() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('splits') + ->with() + ->willReturn([ + new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), + new SplitView('s2', 'tt', false, ['on', 'off'], 124, ['on' => 'frula']), + ]); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals([ + new SplitView('s1', 'tt', true, ['on', 'off'], 123, ['on' => 'frula']), + new SplitView('s2', 'tt', false, ['on', 'off'], 124, ['on' => 'frula']), + ], $manager->splits()); + } + + public function testSplitsReturnsEmptyOnException() + { + $lm = $this->createMock(LinkManager::class); + $lm->expects($this->once())->method('splits') + ->will($this->throwException(new \Exception('sarasa'))); + + $manager = new Manager($lm, $this->logger); + $this->assertEquals([], $manager->splits()); + } +}