Skip to content

Commit 855fd20

Browse files
committed
port
1 parent c8e325d commit 855fd20

14 files changed

+539
-115
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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,9 +655,13 @@ $ vendor/bin/featurevisor test \
655655
--quiet|verbose \
656656
--onlyFailures \
657657
--keyPattern="myFeatureKey" \
658-
--assertionPattern="#1"
658+
--assertionPattern="#1" \
659+
--with-tags \
660+
--with-scopes
659661
```
660662

663+
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`.
664+
661665
### Benchmark
662666

663667
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
}

src/Conditions.php

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,8 @@ public static function conditionIsMatched($condition, array $context, callable $
8787
if (count($notConditions) === 0) {
8888
return true;
8989
}
90-
foreach ($notConditions as $subCondition) {
91-
if (self::conditionIsMatched($subCondition, $context, $getRegex)) {
92-
return false;
93-
}
94-
}
95-
return true;
90+
// JS SDK semantics: "not" negates the entire AND group.
91+
return !self::conditionIsMatched(['and' => $notConditions], $context, $getRegex);
9692
}
9793

9894
$attribute = $condition['attribute'] ?? '';
@@ -121,14 +117,14 @@ public static function conditionIsMatched($condition, array $context, callable $
121117
(is_string($contextValueFromPath) || is_numeric($contextValueFromPath) || $contextValueFromPath === null)
122118
) {
123119
// in / notIn (where condition value is an array)
120+
if (!self::pathExists($context, $attribute)) {
121+
return false;
122+
}
124123
$valueInContext = $contextValueFromPath;
125124

126125
if ($operator === 'in') {
127126
return in_array($valueInContext, $value);
128-
} elseif (
129-
$operator === 'notIn' &&
130-
self::pathExists($context, $attribute)
131-
) {
127+
} elseif ($operator === 'notIn') {
132128
return !in_array($valueInContext, $value);
133129
}
134130

src/DatafileReader.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,9 @@ public function allConditionsAreMatched($conditions, array $context): bool
190190
return false;
191191
}
192192
if (isset($conditions['not']) && is_array($conditions['not'])) {
193-
foreach ($conditions['not'] as $subCondition) {
194-
if ($this->allConditionsAreMatched($subCondition, $context)) {
195-
return false;
196-
}
197-
}
198-
return true;
193+
return $this->allConditionsAreMatched([
194+
'and' => $conditions['not'],
195+
], $context) === false;
199196
}
200197
// If it's a plain array, treat as AND (all must match)
201198
if (array_keys($conditions) === range(0, count($conditions) - 1)) {

0 commit comments

Comments
 (0)