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);