From bc4afd106e80abc4b413fd622e11297c24f90bd6 Mon Sep 17 00:00:00 2001 From: Erin Millard Date: Fri, 18 Mar 2016 09:25:15 +1000 Subject: [PATCH] Allow multiple arguments to generator answer returns and throws. Closes #140. --- CHANGELOG.md | 3 +- doc/index.md | 91 ++++++++++++++++--- .../Answer/Builder/GeneratorAnswerBuilder.php | 42 +++++++++ .../GeneratorAnswerBuilderInterface.php | 2 + .../Builder/GeneratorAnswerBuilderTest.php | 64 ++++++++++++- .../Stub/StubWithGeneratorsTest.php | 9 ++ 6 files changed, 195 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4da8c8250..8cf4d1d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,13 @@ ## Next release -- **[NEW]** Implemented generator stubs ([#11]). +- **[NEW]** Implemented generator stubs ([#11], [#140]). - **[IMPROVED]** More default values for built-in return types ([#138], [#139]). [#11]: https://github.com/eloquent/phony/issues/11 [#138]: https://github.com/eloquent/phony/issues/138 [#139]: https://github.com/eloquent/phony/pull/139 +[#140]: https://github.com/eloquent/phony/issues/140 ## 0.8.0 (2016-02-12) diff --git a/doc/index.md b/doc/index.md index e50a769a5..a537309a4 100644 --- a/doc/index.md +++ b/doc/index.md @@ -65,6 +65,8 @@ - [Invoking callables] - [Stubbing generators] - [Yielding from a generator] + - [Yielding individual values from a generator] + - [Yielding multiple values from a generator] - [Returning values from a generator] - [Returning arguments from a generator] - [Returning the "self" value from a generator] @@ -2566,7 +2568,7 @@ Add a yielded value to the answer. *If no arguments are supplied, the stub will yield like `yield;`.* -*See [Yielding from a generator].* +*See [Yielding individual values from a generator].* @@ -2579,13 +2581,13 @@ Add a set of yielded values to the answer. *The `$values` argument can be an array, an iterator, or even another generator.* -*See [Yielding from a generator].* +*See [Yielding multiple values from a generator].* ---- -> *[stub][stub-api]* $generatorAnswer->[**returns**](#generatorAnswer.returns)($value = null) +> *[stub][stub-api]* $generatorAnswer->[**returns**](#generatorAnswer.returns)($value = null, ...$additionalValues) End the generator by returning a value. @@ -2620,7 +2622,7 @@ End the generator by returning the self value. ---- -> *[stub][stub-api]* $generatorAnswer->[**throws**](#generatorAnswer.throws)($exception = null) +> *[stub][stub-api]* $generatorAnswer->[**throws**](#generatorAnswer.throws)($exception = null, ...$additionalExceptions) End the generator by throwing an exception. @@ -3447,11 +3449,12 @@ echo json_encode($values); // outputs '[]' ``` The result of [`generates()`](#stub.generates) is a [generator answer]. This -object can be used to further customize the behavior of the generator to be -returned. See the subsequent headings for details of these customizations. +object can be used to further customize the behavior of the generator. See the +subsequent headings for details of these customizations. -When a method is called on the generator answer that "ends" the answer (by -returning or throwing), the original stub is returned, allowing continued +Certain methods, such as [`returns()`](#generatorAnswer.returns), or +[`throws()`](#generatorAnswer.throws), mark the "end" of generator answer. When +a generator answer is "ended", the original stub is returned, allowing continued stubbing in a fluent manner: ```php @@ -3478,7 +3481,7 @@ echo $resultC instanceof Generator ? 'true' : 'false'; // outputs 'true' #### Yielding from a generator Keys and values to be yielded can be passed directly to -[`generates()`](#stub.generates) as an array: +[`generates()`](#stub.generates) as any traversable value: ```php $stub = stub() @@ -3495,8 +3498,11 @@ echo json_encode($valuesA); // outputs '["a","b","c","d"]' echo json_encode($valuesB); // outputs '{"e":"f","g":"h"}' ``` -Alternatively, [`yields()`](#generatorAnswer.yields) can be used when yields -need to be interleaved with other actions: +##### Yielding individual values from a generator + +For more complicated generator behavior stubbing, +[`yields()`](#generatorAnswer.yields) can be used to interleave yields with +other actions: ```php $count = 0; @@ -3542,6 +3548,8 @@ $values = iterator_to_array($stub()); // consume the generator echo json_encode($values); // outputs '{"a":"b","0":"c","1":null}' ``` +##### Yielding multiple values from a generator + To yield a set of values from an array, an iterator, or another generator, use [`yieldsFrom()`](#generatorAnswer.yieldsFrom): @@ -3595,6 +3603,35 @@ iterator_to_array($generator); // consume the generator echo $generator->getReturn(); // outputs 'a' ``` +Calling [`returns()`](#generatorAnswer.returns) with multiple arguments allows +for easy specification of the generator return value on subsequent invocations. +For example, the two following stubs behave the same: + +```php +$stubA = stub() + ->generates()->returns('x', 'y'); + +$generatorA = $stubA(); +$generatorB = $stubA(); +iterator_to_array($generatorA); +iterator_to_array($generatorB); + +echo $generatorA->getReturn(); // outputs 'x' +echo $generatorB->getReturn(); // outputs 'y' + +$stubB = stub() + ->generates()->returns('x') + ->generates()->returns('y'); + +$generatorA = $stubB(); +$generatorB = $stubB(); +iterator_to_array($generatorA); +iterator_to_array($generatorB); + +echo $generatorA->getReturn(); // outputs 'x' +echo $generatorB->getReturn(); // outputs 'y' +``` + Note that attempting to return anything other than `null` will result in an exception unless the current runtime supports generator return expressions. For older runtimes, it is perfectly valid to call @@ -3631,7 +3668,7 @@ echo $generatorC->getReturn(); // outputs 'z' #### Returning the "self" value from a generator -The stub [self value] can be return from a generator by using +The stub [self value] can be returned from a generator by using [`returnsSelf()`](#generatorAnswer.returnsSelf) on any [generator answer]: ```php @@ -3664,6 +3701,34 @@ iterator_to_array($generatorA); // throws $exception iterator_to_array($generatorB); // throws a generic exception ``` +Calling [`throws()`](#generatorAnswer.throws) with multiple arguments allows for +easy specification of the thrown exception on subsequent invocations. For +example, the two following stubs behave the same: + +```php +$exceptionA = new RuntimeException('You done goofed.'); +$exceptionB = new RuntimeException('Consequences will never be the same.'); + +$stubA = stub() + ->generates()->throws($exceptionA, $exceptionB); + +$generatorA = $stubA(); +$generatorB = $stubA(); + +iterator_to_array($generatorA); // throws $exceptionA +iterator_to_array($generatorB); // throws $exceptionB + +$stubB = stub() + ->generates()->throws($exceptionA) + ->generates()->throws($exceptionB); + +$generatorA = $stubB(); +$generatorB = $stubB(); + +iterator_to_array($generatorA); // throws $exceptionA +iterator_to_array($generatorB); // throws $exceptionB +``` + #### Generator iterations that perform multiple actions Stubbed generators can perform multiple actions as part of a single iteration. @@ -7291,6 +7356,8 @@ For the full copyright and license information, please view the [LICENSE file]. [verifying values received by spies]: #verifying-values-received-by-spies [when to use the "equal to" matcher]: #when-to-use-the-equal-to-matcher [yielding from a generator]: #yielding-from-a-generator +[yielding individual values from a generator]: #yielding-individual-values-from-a-generator +[yielding multiple values from a generator]: #yielding-multiple-values-from-a-generator diff --git a/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php b/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php index 6dc335e14..1ccb16db3 100644 --- a/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php +++ b/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php @@ -337,12 +337,20 @@ public function yieldsFrom($values) * End the generator by returning a value. * * @param mixed $value The return value. + * @param mixed ...$additionalValues Additional return values for subsequent invocations. * * @return StubInterface The stub. * @throws RuntimeException If the current runtime does not support the supplied return value. */ public function returns($value = null) { + $argumentCount = func_num_args(); + $copies = array(); + + for ($i = 1; $i < $argumentCount; ++$i) { + $copies[$i] = clone $this; + } + if ( $value instanceof InstanceHandleInterface && $value->isAdaptable() @@ -363,6 +371,13 @@ public function returns($value = null) } // @codeCoverageIgnoreEnd + for ($i = 1; $i < $argumentCount; ++$i) { + $this->stub + ->doesWith($copies[$i]->answer(), array(), true, true, false); + + $copies[$i]->returns(func_get_arg($i)); + } + return $this->stub; } @@ -415,11 +430,19 @@ public function returnsSelf() * End the generator by throwing an exception. * * @param Exception|Error|string|null $exception The exception, or message, or null to throw a generic exception. + * @param Exception|Error|string ...$additionalExceptions Additional exceptions, or messages, for subsequent invocations. * * @return StubInterface The stub. */ public function throws($exception = null) { + $argumentCount = func_num_args(); + $copies = array(); + + for ($i = 1; $i < $argumentCount; ++$i) { + $copies[$i] = clone $this; + } + if (is_string($exception)) { $exception = new Exception($exception); } elseif ( @@ -433,6 +456,13 @@ public function throws($exception = null) $this->exception = $exception; + for ($i = 1; $i < $argumentCount; ++$i) { + $this->stub + ->doesWith($copies[$i]->answer(), array(), true, true, false); + + $copies[$i]->throws(func_get_arg($i)); + } + return $this->stub; } @@ -468,6 +498,18 @@ public function answer() // @codeCoverageIgnoreEnd } + /** + * Clone this builder. + */ + public function __clone() + { + // explicitly break references + foreach (get_object_vars($this) as $property => $value) { + unset($this->$property); + $this->$property = $value; + } + } + private $stub; private $isGeneratorReturnSupported; private $invocableInspector; diff --git a/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php b/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php index 6ce9d6796..e1e2b5d9c 100644 --- a/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php +++ b/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php @@ -146,6 +146,7 @@ public function yieldsFrom($values); * @api * * @param mixed $value The return value. + * @param mixed ...$additionalValues Additional return values for subsequent invocations. * * @return StubInterface The stub. * @throws RuntimeException If the current runtime does not support the supplied return value. @@ -181,6 +182,7 @@ public function returnsSelf(); * @api * * @param Exception|Error|string|null $exception The exception, or message, or null to throw a generic exception. + * @param Exception|Error|string ...$additionalExceptions Additional exceptions, or messages, for subsequent invocations. * * @return StubInterface The stub. */ diff --git a/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php b/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php index 00732dbde..6088cafd4 100644 --- a/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php +++ b/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php @@ -373,6 +373,20 @@ public function testReturnsWithValue() $this->markTestSkipped('Requires generator return values.'); } + $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c')); + + $generator = call_user_func($this->answer, $this->self, $this->arguments); + + $this->assertSame(array('a', 'b'), iterator_to_array($generator)); + $this->assertSame('c', $generator->getReturn()); + } + + public function testReturnsWithInstanceHandleValue() + { + if (!$this->featureDetector->isSupported('generator.return')) { + $this->markTestSkipped('Requires generator return values.'); + } + $adaptable = Phony::mock(); $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns($adaptable)); @@ -382,18 +396,25 @@ public function testReturnsWithValue() $this->assertSame($adaptable->mock(), $generator->getReturn()); } - public function testReturnsWithInstanceHandleValue() + public function testReturnsWithMulitpleValues() { if (!$this->featureDetector->isSupported('generator.return')) { $this->markTestSkipped('Requires generator return values.'); } - $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c')); + $this->stub->doesWith($this->answer, array(), true, true, false); - $generator = call_user_func($this->answer, $this->self, $this->arguments); + $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c', 'd')); + + $generator = call_user_func($this->stub); $this->assertSame(array('a', 'b'), iterator_to_array($generator)); $this->assertSame('c', $generator->getReturn()); + + $generator = call_user_func($this->stub); + + $this->assertSame(array('a', 'b'), iterator_to_array($generator)); + $this->assertSame('d', $generator->getReturn()); } public function testReturnsFailureValueNotSupported() @@ -563,6 +584,43 @@ public function testThrowsWithException() $this->assertSame($exception, $actual); } + public function testThrowsWithMultipleExceptions() + { + $this->stub->doesWith($this->answer, array(), true, true, false); + $exceptionA = new Exception('a'); + $exceptionB = new Exception('b'); + $this->subject->throws($exceptionA, $exceptionB); + $generator = call_user_func($this->stub); + $actual = null; + + try { + iterator_to_array($generator); + } catch (Exception $actual) { + } + + $this->assertSame($exceptionA, $actual); + + $generator = call_user_func($this->stub); + $actual = null; + + try { + iterator_to_array($generator); + } catch (Exception $actual) { + } + + $this->assertSame($exceptionB, $actual); + + $generator = call_user_func($this->stub); + $actual = null; + + try { + iterator_to_array($generator); + } catch (Exception $actual) { + } + + $this->assertSame($exceptionB, $actual); + } + public function testThrowsWithMessage() { $exception = new Exception(); diff --git a/test/suite-generators/Stub/StubWithGeneratorsTest.php b/test/suite-generators/Stub/StubWithGeneratorsTest.php index de575b5a2..a9682d84b 100644 --- a/test/suite-generators/Stub/StubWithGeneratorsTest.php +++ b/test/suite-generators/Stub/StubWithGeneratorsTest.php @@ -150,6 +150,13 @@ public function testGenerates() $this->assertInstanceOf('Generator', $generator); $this->assertSame($this->subject, $builder->returns()); $this->assertSame(array('a' => 'b', 0 => 'c'), $actual); + + $generator = call_user_func($this->subject); + $actual = iterator_to_array($generator); + + $this->assertInstanceOf('Generator', $generator); + $this->assertSame($this->subject, $builder->returns()); + $this->assertSame(array('a' => 'b', 0 => 'c'), $actual); } public function testGeneratesWithMultipleArguments() @@ -157,9 +164,11 @@ public function testGeneratesWithMultipleArguments() $builder = $this->subject->generates(array('a'), array('b')); $actualA = iterator_to_array(call_user_func($this->subject)); $actualB = iterator_to_array(call_user_func($this->subject)); + $actualC = iterator_to_array(call_user_func($this->subject)); $this->assertInstanceOf('Eloquent\Phony\Stub\Answer\Builder\GeneratorAnswerBuilderInterface', $builder); $this->assertSame(array('a'), $actualA); $this->assertSame(array('b'), $actualB); + $this->assertSame(array('b'), $actualC); } }