Skip to content

Commit a8f2643

Browse files
authored
feat: SDK parity (#13)
1 parent 0a6b887 commit a8f2643

15 files changed

+631
-131
lines changed

.editorconfig

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,7 @@ trim_trailing_whitespace = true
1212
[*.php]
1313
insert_final_newline = true
1414
indent_size = 4
15+
16+
[Makefile]
17+
indent_style = tab
18+
indent_size = 4

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ cache/
2323
.phpunit*
2424

2525
example-1
26+
27+
monorepo
28+
specs

Makefile

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.PHONY: install test setup-monorepo update-monorepo test-example-project
2+
3+
install:
4+
composer install
5+
6+
test:
7+
composer test
8+
9+
setup-monorepo:
10+
mkdir -p monorepo
11+
if [ ! -d "monorepo/.git" ]; then \
12+
git clone git@github.com:featurevisor/featurevisor.git monorepo; \
13+
else \
14+
(cd monorepo && git fetch origin main && git checkout main && git pull origin main); \
15+
fi
16+
(cd monorepo && make install && make build)
17+
18+
update-monorepo:
19+
(cd monorepo && git pull origin main)
20+
21+
test-example-project:
22+
./featurevisor test --projectDirectoryPath="./monorepo/examples/example-1"

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
2424
- [Set sticky afterwards](#set-sticky-afterwards)
2525
- [Setting datafile](#setting-datafile)
2626
- [Updating datafile](#updating-datafile)
27-
- [Interval-based update](#interval-based-update)
2827
- [Logging](#logging)
28+
- [Levels](#levels)
2929
- [Customizing levels](#customizing-levels)
3030
- [Handler](#handler)
3131
- [Events](#events)
@@ -347,18 +347,21 @@ The triggers for setting the datafile again can be:
347347
- a specific event in your application (like a user action), or
348348
- an event served via websocket or server-sent events (SSE)
349349

350-
### Interval-based update
351-
352-
Here's an example of using interval-based update:
353-
354-
@TODO
355-
356350
## Logging
357351

358352
By default, Featurevisor SDKs will print out logs to the console for `info` level and above.
359353
Featurevisor PHP-SDK by default uses [PSR-3 standard](https://www.php-fig.org/psr/psr-3/) simple implementation.
360354
You can also choose from many mature implementations like e.g. [Monolog](https://github.com/Seldaek/monolog)
361355

356+
### Levels
357+
358+
These are all the available log levels:
359+
360+
- `error`
361+
- `warning`
362+
- `info`
363+
- `debug`
364+
362365
### Customizing levels
363366

364367
If you choose `debug` level to make the logs more verbose, you can set it at the time of SDK initialization.
@@ -416,8 +419,6 @@ Featurevisor SDK implements a simple event emitter that allows you to listen to
416419

417420
You can listen to these events that can occur at various stages in your application:
418421

419-
@TODO: verify these events
420-
421422
### `datafile_set`
422423

423424
```php
@@ -515,8 +516,8 @@ $myCustomHook = [
515516
// rest of the properties below are all optional per hook
516517

517518
// before evaluation
518-
'before' => function (options) {
519-
$type = $options['type']; // `feature` | `variation` | `variable`
519+
'before' => function ($options) {
520+
$type = $options['type']; // `flag` | `variation` | `variable`
520521
$featureKey = $options['featureKey'];
521522
$variableKey = $options['variableKey']; // if type is `variable`
522523
$context = $options['context'];
@@ -655,9 +656,13 @@ $ vendor/bin/featurevisor test \
655656
--quiet|verbose \
656657
--onlyFailures \
657658
--keyPattern="myFeatureKey" \
658-
--assertionPattern="#1"
659+
--assertionPattern="#1" \
660+
--with-tags \
661+
--with-scopes
659662
```
660663

664+
If your assertions include `scope`, run tests with `--with-scopes` to evaluate against scoped datafiles generated on the fly via `npx featurevisor build --scope=<scopeName> --environment=<env> --json`.
665+
661666
### Benchmark
662667

663668
Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).

featurevisor

Lines changed: 118 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ $cliOptions = [
4444
'variation' => parseCliOption($argv, 'variation'),
4545
'verbose' => parseCliOption($argv, 'verbose'),
4646
'inflate' => parseCliOption($argv, 'inflate'),
47+
'withScopes' => parseCliOption($argv, 'withScopes'),
48+
'withTags' => parseCliOption($argv, 'withTags'),
4749
'rootDirectoryPath' => $cwd,
4850
'populateUuid' => array_reduce($argv, function($acc, $arg) {
4951
if (strpos($arg, '--populateUuid=') === 0) {
@@ -70,13 +72,13 @@ function executeCommand(string $command): string {
7072

7173
function getConfig(string $featurevisorProjectPath): array {
7274
echo "Getting config..." . PHP_EOL;
73-
$configOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor config --json)");
75+
$configOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor config --json)");
7476
return json_decode($configOutput, true);
7577
}
7678

7779
function getSegments(string $featurevisorProjectPath): array {
7880
echo "Getting segments..." . PHP_EOL;
79-
$segmentsOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor list --segments --json)");
81+
$segmentsOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor list --segments --json)");
8082
$segments = json_decode($segmentsOutput, true);
8183
$segmentsByKey = [];
8284
foreach ($segments as $segment) {
@@ -85,14 +87,74 @@ function getSegments(string $featurevisorProjectPath): array {
8587
return $segmentsByKey;
8688
}
8789

88-
function buildDatafiles(string $featurevisorProjectPath, array $environments): array {
89-
$datafilesByEnvironment = [];
90+
function buildSingleDatafile(
91+
string $featurevisorProjectPath,
92+
string $environment,
93+
?string $tag = null,
94+
?string $scope = null
95+
): array {
96+
$command = "(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor build --json";
97+
$command .= " --environment=" . escapeshellarg($environment);
98+
99+
if ($tag) {
100+
$command .= " --tag=" . escapeshellarg($tag);
101+
}
102+
103+
if ($scope) {
104+
$command .= " --scope=" . escapeshellarg($scope);
105+
}
106+
107+
$command .= ")";
108+
$output = executeCommand($command);
109+
110+
$decoded = json_decode($output, true);
111+
112+
return is_array($decoded) ? $decoded : [];
113+
}
114+
115+
function buildDatafiles(string $featurevisorProjectPath, array $config, array $cliOptions): array {
116+
$datafilesByKey = [];
117+
118+
$environments = $config['environments'] ?? [];
119+
$scopes = $config['scopes'] ?? [];
120+
$tags = $config['tags'] ?? [];
121+
90122
foreach ($environments as $environment) {
91123
echo "Building datafile for environment: $environment..." . PHP_EOL;
92-
$datafileOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor build --environment=$environment --json)");
93-
$datafilesByEnvironment[$environment] = json_decode($datafileOutput, true);
124+
$datafilesByKey[$environment] = buildSingleDatafile($featurevisorProjectPath, $environment);
125+
126+
if ($cliOptions['withScopes'] === true) {
127+
foreach ($scopes as $scope) {
128+
if (!isset($scope['name']) || !is_string($scope['name'])) {
129+
continue;
130+
}
131+
$scopeName = $scope['name'];
132+
echo "Building scoped datafile for environment: $environment, scope: $scopeName..." . PHP_EOL;
133+
$datafilesByKey[$environment . '-scope-' . $scopeName] = buildSingleDatafile(
134+
$featurevisorProjectPath,
135+
$environment,
136+
null,
137+
$scopeName
138+
);
139+
}
140+
}
141+
142+
if ($cliOptions['withTags'] === true) {
143+
foreach ($tags as $tag) {
144+
if (!is_string($tag)) {
145+
continue;
146+
}
147+
echo "Building tagged datafile for environment: $environment, tag: $tag..." . PHP_EOL;
148+
$datafilesByKey[$environment . '-tag-' . $tag] = buildSingleDatafile(
149+
$featurevisorProjectPath,
150+
$environment,
151+
$tag
152+
);
153+
}
154+
}
94155
}
95-
return $datafilesByEnvironment;
156+
157+
return $datafilesByKey;
96158
}
97159

98160
function getLoggerLevel(array $cliOptions): string {
@@ -121,7 +183,7 @@ function getTests(string $featurevisorProjectPath, array $cliOptions): array {
121183
$testsSuffix .= " --assertionPattern=" . $cliOptions['assertionPattern'];
122184
}
123185

124-
$testsOutput = executeCommand("(cd $featurevisorProjectPath && npx featurevisor list --tests --applyMatrix --json" . $testsSuffix . ")");
186+
$testsOutput = executeCommand("(cd " . escapeshellarg($featurevisorProjectPath) . " && npx featurevisor list --tests --applyMatrix --json" . $testsSuffix . ")");
125187
return json_decode($testsOutput, true);
126188
}
127189

@@ -130,8 +192,8 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
130192
$sticky = isset($assertion["sticky"]) ? $assertion["sticky"] : [];
131193

132194
// Update the SDK instance context and sticky values for this assertion
133-
$f->setContext($context);
134-
$f->setSticky($sticky);
195+
$f->setContext($context, true);
196+
$f->setSticky($sticky, true);
135197

136198
$hasError = false;
137199
$errors = "";
@@ -187,7 +249,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
187249
foreach ($expectedEvaluations["flag"] as $key => $expectedValue) {
188250
if ($actualEvaluation[$key] !== $expectedValue) {
189251
$hasError = true;
190-
$errors .= " ✘ expectedEvaluations.flag.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($evaluation[$key]) . PHP_EOL;
252+
$errors .= " ✘ expectedEvaluations.flag.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
191253
}
192254
}
193255
}
@@ -199,7 +261,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
199261
foreach ($expectedEvaluations["variation"] as $key => $expectedValue) {
200262
if ($actualEvaluation[$key] !== $expectedValue) {
201263
$hasError = true;
202-
$errors .= " ✘ expectedEvaluations.variation.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($evaluation[$key]) . PHP_EOL;
264+
$errors .= " ✘ expectedEvaluations.variation.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
203265
}
204266
}
205267
}
@@ -214,7 +276,7 @@ function testFeature(array $assertion, string $featureKey, $f, string $level): a
214276
foreach ($expectedEvaluation as $key => $expectedValue) {
215277
if ($actualEvaluation[$key] !== $expectedValue) {
216278
$hasError = true;
217-
$errors .= " ✘ expectedEvaluations.variables.$variableKey.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualValue[$key]) . PHP_EOL;
279+
$errors .= " ✘ expectedEvaluations.variables.$variableKey.$key: expected " . json_encode($expectedValue) . " but received " . json_encode($actualEvaluation[$key] ?? null) . PHP_EOL;
218280
}
219281
}
220282
}
@@ -288,7 +350,7 @@ function test(array $cliOptions) {
288350
$config = getConfig($featurevisorProjectPath);
289351
$environments = $config['environments'];
290352
$segmentsByKey = getSegments($featurevisorProjectPath);
291-
$datafilesByEnvironment = buildDatafiles($featurevisorProjectPath, $environments);
353+
$datafilesByKey = buildDatafiles($featurevisorProjectPath, $config, $cliOptions);
292354

293355
echo PHP_EOL;
294356

@@ -300,27 +362,6 @@ function test(array $cliOptions) {
300362
return;
301363
}
302364

303-
// Create SDK instances for each environment
304-
$sdkInstancesByEnvironment = [];
305-
foreach ($environments as $environment) {
306-
$datafile = $datafilesByEnvironment[$environment];
307-
$sdkInstancesByEnvironment[$environment] = Featurevisor::createInstance([
308-
'datafile' => $datafile,
309-
'logger' => Logger::create([
310-
'level' => $level,
311-
]),
312-
'hooks' => [
313-
[
314-
'name' => 'tester-hook',
315-
'bucketValue' => function ($options) {
316-
// This will be overridden per assertion if needed
317-
return $options["bucketValue"];
318-
}
319-
]
320-
]
321-
]);
322-
}
323-
324365
$passedTestsCount = 0;
325366
$failedTestsCount = 0;
326367
$passedAssertionsCount = 0;
@@ -337,29 +378,63 @@ function test(array $cliOptions) {
337378
$testResult = [];
338379

339380
if (isset($test["feature"])) {
340-
$environment = $assertion["environment"];
341-
$f = $sdkInstancesByEnvironment[$environment];
381+
$environment = $assertion["environment"] ?? null;
382+
if (!$environment || !isset($datafilesByKey[$environment])) {
383+
$testResult = [
384+
'hasError' => true,
385+
'errors' => " ✘ missing datafile for environment: " . json_encode($environment) . PHP_EOL,
386+
'duration' => 0
387+
];
388+
} else {
389+
$datafile = $datafilesByKey[$environment];
390+
391+
if (isset($assertion["scope"])) {
392+
$scopeDatafileKey = $environment . '-scope-' . $assertion["scope"];
393+
if ($cliOptions['withScopes'] === true && isset($datafilesByKey[$scopeDatafileKey])) {
394+
$datafile = $datafilesByKey[$scopeDatafileKey];
395+
} elseif ($cliOptions['withScopes'] !== true) {
396+
$scope = null;
397+
foreach ($config['scopes'] ?? [] as $scopeCandidate) {
398+
if (($scopeCandidate['name'] ?? null) === $assertion["scope"]) {
399+
$scope = $scopeCandidate;
400+
break;
401+
}
402+
}
403+
404+
if ($scope && isset($scope['context']) && is_array($scope['context'])) {
405+
$assertion['context'] = array_merge($scope['context'], $assertion['context'] ?? []);
406+
}
407+
}
408+
}
409+
410+
if (isset($assertion["tag"])) {
411+
$tagDatafileKey = $environment . '-tag-' . $assertion["tag"];
412+
if ($cliOptions['withTags'] === true && isset($datafilesByKey[$tagDatafileKey])) {
413+
$datafile = $datafilesByKey[$tagDatafileKey];
414+
}
415+
}
342416

343-
// If "at" parameter is provided, create a new SDK instance with the specific hook
344-
if (isset($assertion["at"])) {
345-
$datafile = $datafilesByEnvironment[$environment];
346417
$f = Featurevisor::createInstance([
347418
'datafile' => $datafile,
348419
'logger' => Logger::create([
349420
'level' => $level,
350421
]),
422+
'sticky' => $assertion['sticky'] ?? [],
351423
'hooks' => [
352424
[
353425
'name' => 'tester-hook',
354426
'bucketValue' => function ($options) use ($assertion) {
355-
return $assertion["at"] * 1000;
427+
if (isset($assertion["at"])) {
428+
return $assertion["at"] * 1000;
429+
}
430+
return $options["bucketValue"];
356431
}
357432
]
358433
]
359434
]);
360-
}
361435

362-
$testResult = testFeature($assertion, $test["feature"], $f, $level);
436+
$testResult = testFeature($assertion, $test["feature"], $f, $level);
437+
}
363438
} else if (isset($test["segment"])) {
364439
$testResult = testSegment($assertion, $segmentsByKey[$test["segment"]], $level);
365440
}

0 commit comments

Comments
 (0)