Skip to content

Feat: Add test command #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,43 @@ unleash_symfony_client:
fetching_enabled: false
```

## Test command

If you need to quickly test what will your flags evaluate to, you can use the built-in command `unleash:test-flag`.

The command is documented and here's the output of `./bin/console unleash:test-flag --help`:

```
Description:
Check the status of an Unleash feature

Usage:
unleash:test-flag [options] [--] <flag>

Arguments:
flag The name of the feature flag to check the result for

Options:
-f, --force When this flag is present, fresh results without cache will be forced
--user-id=USER-ID [Context] Provide the current user's ID
--ip-address=IP-ADDRESS [Context] Provide the current IP address
--session-id=SESSION-ID [Context] Provide the current session ID
--hostname=HOSTNAME [Context] Provide the current hostname
--environment=ENVIRONMENT [Context] Provide the current environment
--current-time=CURRENT-TIME [Context] Provide the current date and time
--custom-context=CUSTOM-CONTEXT [Context] Custom context values in the format [contextName]=[contextValue], for example: myCustomContextField=someValue (multiple values allowed)
--expected=EXPECTED For use in testing, if this option is present, the exit code will be either 0 or 1 depending on whether the expectation matches the result
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-e, --env=ENV The Environment name. [default: "dev"]
--no-debug Switch off debug mode.
--profile Enables profiling (requires debug).
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
```

## Configuration reference

This is the autogenerated config dump (by running `php bin/console config:dump unleash_symfony_client`):
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
},
"config": {
"allow-plugins": {
"php-http/discovery": true
"php-http/discovery": true,
"phpstan/extension-installer": false
}
}
}
171 changes: 171 additions & 0 deletions src/Command/TestFlagCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
<?php

namespace Unleash\Client\Bundle\Command;

use LogicException;
use Psr\SimpleCache\CacheInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Unleash\Client\Configuration\Context;
use Unleash\Client\Configuration\UnleashContext;
use Unleash\Client\Unleash;

final class TestFlagCommand extends Command
{
public function __construct(
string $name,
private readonly Unleash $unleash,
private readonly CacheInterface $cache,
) {
parent::__construct($name);
}

protected function configure(): void
{
$this
->setDescription('Check the status of an Unleash feature')
->addArgument(
name: 'flag',
mode: InputArgument::REQUIRED,
description: 'The name of the feature flag to check the result for',
)
->addOption(
name: 'force',
shortcut: 'f',
mode: InputOption::VALUE_NONE,
description: 'When this flag is present, fresh results without cache will be forced',
)
->addOption(
name: 'user-id',
mode: InputOption::VALUE_REQUIRED,
description: "[Context] Provide the current user's ID",
default: null,
)
->addOption(
name: 'ip-address',
mode: InputOption::VALUE_REQUIRED,
description: '[Context] Provide the current IP address',
default: null,
)
->addOption(
name: 'session-id',
mode: InputOption::VALUE_REQUIRED,
description: '[Context] Provide the current session ID',
default: null,
)
->addOption(
name: 'hostname',
mode: InputOption::VALUE_REQUIRED,
description: '[Context] Provide the current hostname',
default: null,
)
->addOption(
name: 'environment',
mode: InputOption::VALUE_REQUIRED,
description: '[Context] Provide the current environment',
default: null,
)
->addOption(
name: 'current-time',
mode: InputOption::VALUE_REQUIRED,
description: '[Context] Provide the current date and time',
default: null,
)
->addOption(
'custom-context',
mode: InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
description: '[Context] Custom context values in the format [contextName]=[contextValue], for example: myCustomContextField=someValue',
default: null,
)
->addOption( // must use positional arguments, because $suggestedValues is not a real argument
'expected',
null,
InputOption::VALUE_REQUIRED,
'For use in testing, if this option is present, the exit code will be either 0 or 1 depending on whether the expectation matches the result',
null,
['true', 'false'], // suggested values
)
;
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$flagName = $input->getArgument('flag');
assert(is_string($flagName));

if ($input->getOption('force')) {
$this->cache->clear();
}

$result = $this->unleash->isEnabled(
$flagName,
$this->createContext($input),
);

$expected = $input->getOption('expected');
if ($expected !== null) {
$expected = $expected === 'true';
}
$success = ($expected === null && $result) || ($expected !== null && $result === $expected);
$message = "The feature flag '{$flagName}' evaluated to: " . ($result ? 'true' : 'false');

$success
? $io->success($message)
: $io->error($message)
;

return $expected === null
? Command::SUCCESS
: (
$result === $expected
? Command::SUCCESS
: Command::FAILURE
)
;
}

private function createContext(InputInterface $input): Context
{
$customContextInput = $input->getOption('custom-context');
assert(is_array($customContextInput));

$customContext = [];
foreach ($customContextInput as $item) {
if (!fnmatch('*=*', $item)) {
throw new LogicException('The value must be a key=value pair.');
}
[$key, $value] = explode('=', $item);
$customContext[trim($key)] = trim($value);
}

$userId = $input->getOption('user-id');
$ipAddress = $input->getOption('ip-address');
$sessionId = $input->getOption('session-id');
$hostname = $input->getOption('hostname');
$environment = $input->getOption('environment');
$currentTime = $input->getOption('current-time');

assert($userId === null || is_string($userId));
assert($ipAddress === null || is_string($ipAddress));
assert($sessionId === null || is_string($sessionId));
assert($hostname === null || is_string($hostname));
assert($environment === null || is_string($environment));
assert($currentTime === null || is_string($currentTime));

return new UnleashContext(
currentUserId: $userId,
ipAddress: $ipAddress,
sessionId: $sessionId,
customContext: $customContext,
hostname: $hostname,
environment: $environment,
currentTime: $currentTime,
);
}
}
9 changes: 9 additions & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,12 @@ services:
- '@event_dispatcher'
tags:
- kernel.event_subscriber

unleash.client.command.test_flag:
class: Unleash\Client\Bundle\Command\TestFlagCommand
arguments:
$name: 'unleash:test-flag'
$unleash: '@unleash.client.unleash'
$cache: '@unleash.client.internal.cache'
tags:
- console.command