From 914e91829c9385b7243e7ea58ddb1b13d069752a Mon Sep 17 00:00:00 2001 From: James Rosewell Date: Thu, 17 Sep 2026 00:37:26 +0100 Subject: [PATCH] FIX: Build the failure to match example's pipeline on every run The example kept the pipeline it built in a file and reused it on the next run, so a run with one resource key answered with the properties of whichever key had written the file. Running the example with a key entitled to IsMobile still ended in 'Property ismobile not found in data for element device', listing the properties of the earlier key. The file name was also built without a separator, so it landed beside the examples folder rather than in it. The example now builds its pipeline each run and the comment says what a site does instead, which is to keep the pipeline and throw it away when the resource key changes. tests/ExampleTests.php runs the example as its own process, checks it answers and checks it leaves no serialized pipeline behind. The test fails before the change, naming the file left in examples/, and passes after it. --- examples/cloud/failureToMatch.php | 20 +++----- tests/ExampleTests.php | 83 +++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 12 deletions(-) diff --git a/examples/cloud/failureToMatch.php b/examples/cloud/failureToMatch.php index f8642bb..163f34d 100644 --- a/examples/cloud/failureToMatch.php +++ b/examples/cloud/failureToMatch.php @@ -87,18 +87,14 @@ // Next we build the pipeline. We could additionally add extra engines and/or // flowElements here before building. -// To stop having to construct the pipeline -// and re-make cloud API requests used during construction on every page load, -// we recommend caching the serialized pipeline to a database or disk. -// Below we are using PHP's serialize and writing to a file if it doesn't exist - -$serializedPipelineFile = __DIR__ . "failure_to_match_pipeline.pipeline"; -if(!file_exists($serializedPipelineFile)){ - $pipeline = $builder->build(); - file_put_contents($serializedPipelineFile, serialize($pipeline)); -} else { - $pipeline = unserialize(file_get_contents($serializedPipelineFile)); -} +// A site keeps the pipeline for the life of the process, or serializes it, +// so that the cloud requests made while it is built are not made on every +// page load. Anything kept that way has to be thrown away when the resource +// key changes, because a pipeline carries the properties of the key it was +// built with. This example builds it on every run, so the answer always +// belongs to the key the example was given. + +$pipeline = $builder->build(); // Here we create a function that checks if a supplied User-Agent is a // mobile device diff --git a/tests/ExampleTests.php b/tests/ExampleTests.php index bc4e577..8f91de1 100644 --- a/tests/ExampleTests.php +++ b/tests/ExampleTests.php @@ -174,6 +174,89 @@ static function ($line) { return strpos($line, "\t") === 0; } } } + /** + * The failure to match example is a script rather than a class, so it + * is run as its own process. + * + * It used to keep the pipeline it built in a file beside the examples + * and reuse it on the next run, so a run with one resource key answered + * with the properties of whichever key had run first. The file name was + * also built without a separator, so it landed outside the folder the + * example lives in. The example builds its pipeline on every run now, + * and this checks that it answers and leaves nothing behind. + */ + public function testFailureToMatchLeavesNoPipelineBehind() + { + $key = $this->getResourceKey(); + $examples = __DIR__ . '/../examples'; + + foreach ($this->pipelineFilesUnder($examples) as $stale) { + unlink($stale); + } + + $output = $this->runScript( + $examples . '/cloud/failureToMatch.php', + $key + ); + + $this->assertStringContainsString( + 'represent a mobile device', + $output + ); + + $this->assertSame( + [], + $this->pipelineFilesUnder($examples), + 'The example left a serialized pipeline behind, and the next run ' + . 'would answer from whatever resource key wrote it' + ); + } + + /** + * Runs a PHP script with the resource key in its environment and + * returns everything it wrote. + */ + private function runScript(string $script, string $key): string + { + $environment = getenv(); + $environment[Constants::RESOURCE_ENV_VAR] = $key; + + $process = proc_open( + [PHP_BINARY, $script], + [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], + $pipes, + null, + $environment + ); + $this->assertIsResource($process, 'Could not start ' . $script); + + $output = stream_get_contents($pipes[1]) + . stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + proc_close($process); + + return $output; + } + + /** + * Every serialized pipeline file in the examples folder, in the folders + * below it, and beside it, because a name built without a separator + * lands beside the folder rather than in it. + * + * @return string[] + */ + private function pipelineFilesUnder(string $folder): array + { + $found = array_merge( + (array) glob($folder . '/*.pipeline'), + (array) glob($folder . '/*/*.pipeline'), + (array) glob(dirname($folder) . '/*.pipeline') + ); + + return array_values(array_filter($found, 'is_file')); + } + private function getResourceKey() { $resourceKey = ResourceKeys::find(Constants::RESOURCE_ENV_VAR);