@@ -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
7173function 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
7779function 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
98160function 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