1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-24 18:13:00 +01:00

Simplifying the Request FSM

FSM is now passed to a client as a callable.
FSM now requires an adapter and message factory rather than a function.
FSM now is merged with RequestFsm to simplify the API (at least for now)
Removed unnecessary circular reference from client/FSM.
This commit is contained in:
Michael Dowling 2014-10-11 16:01:20 -07:00
parent 286525b387
commit 22389c611c
7 changed files with 196 additions and 400 deletions

View File

@ -32,7 +32,7 @@ class Client implements ClientInterface
/** @var array Default request options */ /** @var array Default request options */
private $defaults; private $defaults;
/** @var Fsm Request state machine */ /** @var callable Request state machine */
private $fsm; private $fsm;
/** /**
@ -63,7 +63,10 @@ class Client implements ClientInterface
* - message_factory: Factory used to create request and response object * - message_factory: Factory used to create request and response object
* - defaults: Default request options to apply to each request * - defaults: Default request options to apply to each request
* - emitter: Event emitter used for request events * - emitter: Event emitter used for request events
* - fsm: (internal use only) The request finite state machine. * - fsm: (internal use only) The request finite state machine. A
* function that accepts a transaction and optional final state. The
* function is responsible for transitioning a request through its
* lifecycle events.
*/ */
public function __construct(array $config = []) public function __construct(array $config = [])
{ {
@ -77,12 +80,15 @@ class Client implements ClientInterface
$this->messageFactory = isset($config['message_factory']) $this->messageFactory = isset($config['message_factory'])
? $config['message_factory'] ? $config['message_factory']
: new MessageFactory(); : new MessageFactory();
$adapter = isset($config['adapter'])
? $config['adapter'] if (isset($config['fsm'])) {
: self::getDefaultAdapter(); $this->fsm = $config['fsm'];
$this->fsm = isset($config['fsm']) } else {
? $config['fsm'] $this->fsm = new RequestFsm(
: $this->createDefaultFsm($adapter, $this->messageFactory); isset($config['adapter']) ? $config['adapter'] : self::getDefaultAdapter(),
$this->messageFactory
);
}
} }
/** /**
@ -227,11 +233,12 @@ class Client implements ClientInterface
public function send(RequestInterface $request) public function send(RequestInterface $request)
{ {
$trans = new Transaction($this, $request); $trans = new Transaction($this, $request);
$fn = $this->fsm;
// Ensure a future response is returned if one was requested. // Ensure a future response is returned if one was requested.
if ($request->getConfig()->get('future')) { if ($request->getConfig()->get('future')) {
try { try {
$this->fsm->run($trans); $fn($trans);
// Turn the normal response into a future if needed. // Turn the normal response into a future if needed.
return $trans->response instanceof FutureInterface return $trans->response instanceof FutureInterface
? $trans->response ? $trans->response
@ -242,7 +249,7 @@ class Client implements ClientInterface
} }
} else { } else {
try { try {
$this->fsm->run($trans); $fn($trans);
return $trans->response instanceof FutureInterface return $trans->response instanceof FutureInterface
? $trans->response->wait() ? $trans->response->wait()
: $trans->response; : $trans->response;
@ -368,23 +375,6 @@ class Client implements ClientInterface
return $this->defaults['headers']; return $this->defaults['headers'];
} }
private function createDefaultFsm(
callable $adapter,
MessageFactoryInterface $mf
) {
return new RequestFsm(function (Transaction $t) use ($adapter, $mf) {
$t->response = FutureResponse::proxy(
$adapter(RingBridge::prepareRingRequest($t)),
function ($value) use ($t) {
RingBridge::completeRingResponse(
$t, $value, $this->messageFactory, $this->fsm
);
return $t->response;
}
);
});
}
/** /**
* @deprecated Use {@see GuzzleHttp\Pool} instead. * @deprecated Use {@see GuzzleHttp\Pool} instead.
* @see GuzzleHttp\Pool * @see GuzzleHttp\Pool

View File

@ -1,128 +0,0 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Exception\StateException;
/**
* Provides a basic finite state machine that transitions transaction objects
* through state transitions provided in the constructor.
*
* As states transition, any exceptions thrown in the state are caught and
* passed to the corresponding error state if available. If no error state is
* available, then the exception is thrown. If a
* {@see GuzzleHttp\Exception\StateException} is thrown, then the exception
* is thrown immediately without allowing any further transitions.
*
* States can return true or false/null. When a state returns true, it tells
* the FSM to transition to the defined "intercept" state. If no intercept
* state is defined then a StateException is thrown. If a state returns
* false/null, then the FSM transitions to the "complete" state if one is
* defined.
*/
class Fsm
{
private $states;
private $initialState;
private $maxTransitions;
/**
* The states array is an associative array of associative arrays
* describing each state transition. Each key of the outer array is a state
* name, and each value is an associative array that can contain the
* following key value pairs:
*
* - transition: A callable that is invoked when entering the state. If
* the callable throws an exception then the FSM transitions to the
* error state. Otherwise, the FSM transitions to the success state.
* - success: The state to transition to when no error is raised. If not
* present, then this is a terminal state.
* - intercept: The state to transition to if the state is intercepted.
* You can intercept states by returning true in a transition function.
* - error: The state to transition to when an error is raised. If not
* present and an exception occurs, then the exception is thrown.
*
* @param string $initialState The initial state of the FSM
* @param array $states Associative array of state transitions.
* @param int $maxTransitions The maximum number of allows transitions
* before failing. This is basically a
* fail-safe to prevent infinite loops.
*/
public function __construct(
$initialState,
array $states,
$maxTransitions = 200
) {
$this->states = $states;
$this->initialState = $initialState;
$this->maxTransitions = $maxTransitions;
}
/**
* Runs the state machine until a terminal state is entered or the
* optionally supplied $finalState is entered.
*
* @param Transaction $trans Transaction being transitioned.
* @param string $finalState The state to stop on. If unspecified,
* runs until a terminal state is found.
*
* @throws \Exception if a terminal state throws an exception.
*/
public function run(Transaction $trans, $finalState = null)
{
$trans->_transitionCount = 1;
if (!$trans->state) {
$trans->state = $this->initialState;
}
while ($trans->state !== $finalState) {
if (!isset($this->states[$trans->state])) {
throw new StateException("Invalid state: {$trans->state}");
} elseif (++$trans->_transitionCount > $this->maxTransitions) {
throw new StateException('Too many state transitions were '
. ' encountered ({$trans->_transitionCount}). This likely '
. 'means that a combination of event listeners are in an '
. 'infinite loop.');
}
$state = $this->states[$trans->state];
try {
// Call the transition function if available.
if (isset($state['transition'])) {
// Handles transitioning to the "intercept" state.
if ($state['transition']($trans)) {
if (isset($state['intercept'])) {
$trans->state = $state['intercept'];
continue;
}
throw new StateException('Invalid intercept state '
. 'transition from ' . $trans->state);
}
}
if (isset($state['success'])) {
// Transition to the success state
$trans->state = $state['success'];
} else {
// Break: this is a terminal state with no transition.
break;
}
} catch (StateException $e) {
// State exceptions are thrown no matter what.
throw $e;
} catch (\Exception $e) {
$trans->exception = $e;
// Terminal error states throw the exception.
if (!isset($state['error'])) {
throw $e;
}
// Transition to the error state.
$trans->state = $state['error'];
}
}
}
}

View File

@ -5,53 +5,123 @@ use GuzzleHttp\Event\BeforeEvent;
use GuzzleHttp\Event\ErrorEvent; use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Event\CompleteEvent; use GuzzleHttp\Event\CompleteEvent;
use GuzzleHttp\Event\EndEvent; use GuzzleHttp\Event\EndEvent;
use GuzzleHttp\Exception\StateException;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\FutureResponse;
use GuzzleHttp\Message\MessageFactoryInterface;
use GuzzleHttp\Ring\Future\FutureInterface; use GuzzleHttp\Ring\Future\FutureInterface;
/** /**
* Responsible for transitioning requests through lifecycle events. * Responsible for transitioning requests through lifecycle events.
*/ */
class RequestFsm extends Fsm class RequestFsm
{ {
private $sendFn; private $adapter;
private $mf;
private $maxTransitions;
public function __construct(callable $sendFn) private $states = [
{ // When a mock intercepts the emitted "before" event, then we
$this->sendFn = $sendFn; // transition to the "complete" intercept state.
parent::__construct('before', [ 'before' => [
// When a mock intercepts the emitted "before" event, then we 'success' => 'send',
// transition to the "complete" intercept state. 'intercept' => 'complete',
'before' => [ 'error' => 'error'
'success' => 'send', ],
'intercept' => 'complete', // The complete and error events are handled using the "then" of
'error' => 'error', // the Guzzle-Ring request, so we exit the FSM.
'transition' => [$this, 'beforeTransition'] 'send' => ['error' => 'error'],
], 'complete' => [
// The complete and error events are handled using the "then" of 'success' => 'end',
// the Guzzle-Ring request, so we exit the FSM. 'intercept' => 'before',
'send' => [ 'error' => 'error'
'error' => 'error', ],
'transition' => $this->sendFn 'error' => [
], 'success' => 'complete',
'complete' => [ 'intercept' => 'before',
'success' => 'end', 'error' => 'end'
'intercept' => 'before', ],
'error' => 'error', 'end' => []
'transition' => [$this, 'completeTransition'] ];
],
'error' => [ public function __construct(
'success' => 'complete', callable $adapter,
'intercept' => 'before', MessageFactoryInterface $messageFactory,
'error' => 'end', $maxTransitions = 200
'transition' => [$this, 'ErrorTransition'] ) {
], $this->mf = $messageFactory;
'end' => [ $this->maxTransitions = $maxTransitions;
'transition' => [$this, 'endTransition'] $this->adapter = $adapter;
]
]);
} }
protected function beforeTransition(Transaction $trans) /**
* Runs the state machine until a terminal state is entered or the
* optionally supplied $finalState is entered.
*
* @param Transaction $trans Transaction being transitioned.
* @param string $finalState The state to stop on. If unspecified,
* runs until a terminal state is found.
*
* @throws \Exception if a terminal state throws an exception.
*/
public function __invoke(Transaction $trans, $finalState = null)
{
$trans->_transitionCount = 1;
if (!$trans->state) {
$trans->state = 'before';
}
while ($trans->state !== $finalState) {
if (!isset($this->states[$trans->state])) {
throw new StateException("Invalid state: {$trans->state}");
} elseif (++$trans->_transitionCount > $this->maxTransitions) {
throw new StateException('Too many state transitions were '
. 'encountered ({$trans->_transitionCount}). This likely '
. 'means that a combination of event listeners are in an '
. 'infinite loop.');
}
$state = $this->states[$trans->state];
try {
/** @var callable $fn */
$fn = [$this, $trans->state];
if ($fn($trans)) {
// Handles transitioning to the "intercept" state.
if (isset($state['intercept'])) {
$trans->state = $state['intercept'];
continue;
}
throw new StateException('Invalid intercept state '
. 'transition from ' . $trans->state);
}
if (isset($state['success'])) {
// Transition to the success state
$trans->state = $state['success'];
} else {
// Break: this is a terminal state with no transition.
break;
}
} catch (StateException $e) {
// State exceptions are thrown no matter what.
throw $e;
} catch (\Exception $e) {
$trans->exception = $e;
// Terminal error states throw the exception.
if (!isset($state['error'])) {
throw $e;
}
// Transition to the error state.
$trans->state = $state['error'];
}
}
}
private function before(Transaction $trans)
{ {
$trans->request->getEmitter()->emit('before', new BeforeEvent($trans)); $trans->request->getEmitter()->emit('before', new BeforeEvent($trans));
@ -61,6 +131,18 @@ class RequestFsm extends Fsm
return (bool) $trans->response; return (bool) $trans->response;
} }
private function send(Transaction $trans)
{
$fn = $this->adapter;
$trans->response = FutureResponse::proxy(
$fn(RingBridge::prepareRingRequest($trans)),
function ($value) use ($trans) {
RingBridge::completeRingResponse($trans, $value, $this->mf, $this);
return $trans->response;
}
);
}
/** /**
* Emits the error event and ensures that the exception is set and is an * Emits the error event and ensures that the exception is set and is an
* instance of RequestException. If the error event is not intercepted, * instance of RequestException. If the error event is not intercepted,
@ -69,7 +151,7 @@ class RequestFsm extends Fsm
* to the "before" event. Otherwise, when no retries, and the exception is * to the "before" event. Otherwise, when no retries, and the exception is
* intercepted, transition to the "complete" event. * intercepted, transition to the "complete" event.
*/ */
protected function errorTransition(Transaction $trans) private function error(Transaction $trans)
{ {
// Convert non-request exception to a wrapped exception // Convert non-request exception to a wrapped exception
if (!($trans->exception instanceof RequestException)) { if (!($trans->exception instanceof RequestException)) {
@ -96,7 +178,7 @@ class RequestFsm extends Fsm
* Emits a complete event, and if a request is marked for a retry during * Emits a complete event, and if a request is marked for a retry during
* the complete event, then the "before" state is transitioned to. * the complete event, then the "before" state is transitioned to.
*/ */
protected function completeTransition(Transaction $trans) private function complete(Transaction $trans)
{ {
// Futures will have their own end events emitted when dereferenced. // Futures will have their own end events emitted when dereferenced.
if ($trans->response instanceof FutureInterface) { if ($trans->response instanceof FutureInterface) {
@ -113,7 +195,7 @@ class RequestFsm extends Fsm
/** /**
* Emits the "end" event and throws an exception if one is present. * Emits the "end" event and throws an exception if one is present.
*/ */
protected function endTransition(Transaction $trans) private function end(Transaction $trans)
{ {
// Futures will have their own end events emitted when dereferenced, // Futures will have their own end events emitted when dereferenced,
// but still emit, even for futures, when an exception is present. // but still emit, even for futures, when an exception is present.

View File

@ -78,13 +78,13 @@ class RingBridge
* @param Transaction $trans Owns request and response. * @param Transaction $trans Owns request and response.
* @param array $response Ring response array * @param array $response Ring response array
* @param MessageFactoryInterface $messageFactory Creates response objects. * @param MessageFactoryInterface $messageFactory Creates response objects.
* @param Fsm $fsm State machine. * @param callable $fsm Request FSM function.
*/ */
public static function completeRingResponse( public static function completeRingResponse(
Transaction $trans, Transaction $trans,
array $response, array $response,
MessageFactoryInterface $messageFactory, MessageFactoryInterface $messageFactory,
Fsm $fsm callable $fsm
) { ) {
$trans->state = 'complete'; $trans->state = 'complete';
$trans->transferInfo = isset($response['transfer_info']) $trans->transferInfo = isset($response['transfer_info'])
@ -118,7 +118,7 @@ class RingBridge
} }
// Complete the lifecycle of the request. // Complete the lifecycle of the request.
$fsm->run($trans); $fsm($trans);
} }
/** /**

View File

@ -1,174 +0,0 @@
<?php
namespace GuzzleHttp\Tests;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\StateException;
use GuzzleHttp\Transaction;
use GuzzleHttp\Fsm;
class FsmTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \RuntimeException
*/
public function testValidatesStateNames()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
(new Fsm('foo', []))->run(new Transaction($client, $request));
}
public function testTransitionsThroughStates()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$c = [];
$fsm = new Fsm('begin', [
'begin' => [
'success' => 'end',
'transition' => function (Transaction $trans) use ($t, &$c) {
$this->assertSame($t, $trans);
$c[] = 'begin';
}
],
'end' => [
'transition' => function (Transaction $trans) use ($t, &$c) {
$this->assertSame($t, $trans);
$c[] = 'end';
}
],
]);
$fsm->run($t);
$this->assertEquals(['begin', 'end'], $c);
}
public function testTransitionsThroughErrorStates()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$c = [];
$fsm = new Fsm('begin', [
'begin' => [
'success' => 'end',
'error' => 'error',
'transition' => function (Transaction $trans) use ($t, &$c) {
$c[] = 'begin';
throw new \OutOfBoundsException();
}
],
'error' => [
'success' => 'end',
'error' => 'end',
'transition' => function (Transaction $trans) use ($t, &$c) {
$c[] = 'error';
$this->assertInstanceOf('OutOfBoundsException', $t->exception);
$trans->exception = null;
}
],
'end' => [
'transition' => function (Transaction $trans) use ($t, &$c) {
$c[] = 'end';
}
],
]);
$fsm->run($t);
$this->assertEquals(['begin', 'error', 'end'], $c);
$this->assertNull($t->exception);
}
public function testThrowsTerminalErrors()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$fsm = new Fsm('begin', [
'begin' => [
'transition' => function (Transaction $trans) use ($t) {
throw new \OutOfBoundsException();
}
]
]);
try {
$fsm->run($t);
$this->fail('Did not throw');
} catch (\OutOfBoundsException $e) {
$this->assertSame($e, $t->exception);
}
}
/**
* @expectedException \RuntimeException
* @expectedExceptionMessage Too many state transitions
*/
public function testThrowsWhenTooManyTransitions()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$fsm = new Fsm('begin', ['begin' => ['success' => 'begin']], 10);
$fsm->run($t);
}
/**
* @expectedExceptionMessage Foo
* @expectedException \GuzzleHttp\Exception\StateException
*/
public function testThrowsWhenStateException()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$fsm = new Fsm('begin', [
'begin' => [
'transition' => function () use ($request) {
throw new StateException('Foo');
},
'error' => 'not_there'
]
]);
$fsm->run($t);
}
public function testCanInterceptTransitionStates()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$called = false;
$fsm = new Fsm('begin', [
'begin' => [
'transition' => function () { return true; },
'intercept' => 'end'
],
'end' => [
'transition' => function () use (&$called) { $called = true; }
]
]);
$fsm->run($t);
$this->assertTrue($called);
}
/**
* @expectedExceptionMessage Invalid intercept state transition from begin
* @expectedException \GuzzleHttp\Exception\StateException
*/
public function testEnsuresInterceptStatesAreDefined()
{
$client = new Client();
$request = $client->createRequest('GET', 'http://httpbin.org');
$t = new Transaction($client, $request);
$fsm = new Fsm('begin', [
'begin' => [
'transition' => function () { return true; }
]
]);
$fsm->run($t);
}
}

View File

@ -2,6 +2,7 @@
namespace GuzzleHttp\Tests; namespace GuzzleHttp\Tests;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Message\MessageFactory;
use GuzzleHttp\Message\Response; use GuzzleHttp\Message\Response;
use GuzzleHttp\RequestFsm; use GuzzleHttp\RequestFsm;
use GuzzleHttp\Subscriber\Mock; use GuzzleHttp\Subscriber\Mock;
@ -19,21 +20,28 @@ use React\Promise\Deferred;
class RequestFsmTest extends \PHPUnit_Framework_TestCase class RequestFsmTest extends \PHPUnit_Framework_TestCase
{ {
private $mf;
public function setup()
{
$this->mf = new MessageFactory();
}
public function testEmitsBeforeEventInTransition() public function testEmitsBeforeEventInTransition()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com')); $t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$c = false; $c = false;
$t->request->getEmitter()->on('before', function (BeforeEvent $e) use (&$c) { $t->request->getEmitter()->on('before', function (BeforeEvent $e) use (&$c) {
$c = true; $c = true;
}); });
$fsm->run($t, 'send'); $fsm($t, 'send');
$this->assertTrue($c); $this->assertTrue($c);
} }
public function testEmitsCompleteEventInTransition() public function testEmitsCompleteEventInTransition()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com')); $t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$t->response = new Response(200); $t->response = new Response(200);
$t->state = 'complete'; $t->state = 'complete';
@ -41,13 +49,13 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) { $t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) {
$c = true; $c = true;
}); });
$fsm->run($t, 'end'); $fsm($t, 'end');
$this->assertTrue($c); $this->assertTrue($c);
} }
public function testDoesNotEmitCompleteForFuture() public function testDoesNotEmitCompleteForFuture()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com')); $t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$deferred = new Deferred(); $deferred = new Deferred();
$t->response = new FutureResponse($deferred->promise()); $t->response = new FutureResponse($deferred->promise());
@ -56,13 +64,13 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) { $t->request->getEmitter()->on('complete', function (CompleteEvent $e) use (&$c) {
$c = true; $c = true;
}); });
$fsm->run($t, 'end'); $fsm($t, 'end');
$this->assertFalse($c); $this->assertFalse($c);
} }
public function testDoesNotEmitEndForFuture() public function testDoesNotEmitEndForFuture()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$t = new Transaction(new Client(), new Request('GET', 'http://foo.com')); $t = new Transaction(new Client(), new Request('GET', 'http://foo.com'));
$deferred = new Deferred(); $deferred = new Deferred();
$t->response = new FutureResponse($deferred->promise()); $t->response = new FutureResponse($deferred->promise());
@ -71,7 +79,7 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('end', function (EndEvent $e) use (&$c) { $t->request->getEmitter()->on('end', function (EndEvent $e) use (&$c) {
$c = true; $c = true;
}); });
$fsm->run($t); $fsm($t);
$this->assertFalse($c); $this->assertFalse($c);
} }
@ -87,7 +95,7 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
public function testTransitionsThroughErrorsInBefore() public function testTransitionsThroughErrorsInBefore()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$client = new Client(); $client = new Client();
$request = $client->createRequest('GET', 'http://ewfewwef.com'); $request = $client->createRequest('GET', 'http://ewfewwef.com');
$t = new Transaction($client, $request); $t = new Transaction($client, $request);
@ -97,7 +105,7 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
throw new \Exception('foo'); throw new \Exception('foo');
}); });
try { try {
$fsm->run($t, 'send'); $fsm($t, 'send');
$this->fail('did not throw'); $this->fail('did not throw');
} catch (RequestException $e) { } catch (RequestException $e) {
$this->assertContains('foo', $t->exception->getMessage()); $this->assertContains('foo', $t->exception->getMessage());
@ -125,7 +133,7 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
public function testTransitionsThroughErrorInterception() public function testTransitionsThroughErrorInterception()
{ {
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, $this->mf);
$client = new Client(); $client = new Client();
$request = $client->createRequest('GET', 'http://ewfewwef.com'); $request = $client->createRequest('GET', 'http://ewfewwef.com');
$t = new Transaction($client, $request); $t = new Transaction($client, $request);
@ -134,10 +142,10 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$t->request->getEmitter()->on('error', function (ErrorEvent $e) { $t->request->getEmitter()->on('error', function (ErrorEvent $e) {
$e->intercept(new Response(200)); $e->intercept(new Response(200));
}); });
$fsm->run($t, 'send'); $fsm($t, 'send');
$t->response = new Response(404); $t->response = new Response(404);
$t->state = 'complete'; $t->state = 'complete';
$fsm->run($t); $fsm($t);
$this->assertEquals(200, $t->response->getStatusCode()); $this->assertEquals(200, $t->response->getStatusCode());
$this->assertNull($t->exception); $this->assertNull($t->exception);
$this->assertEquals(['before', 'complete', 'error', 'complete', 'end'], $calls); $this->assertEquals(['before', 'complete', 'error', 'complete', 'end'], $calls);
@ -158,4 +166,27 @@ class RequestFsmTest extends \PHPUnit_Framework_TestCase
$calls[] = 'end'; $calls[] = 'end';
}, RequestEvents::EARLY); }, RequestEvents::EARLY);
} }
/**
* @expectedException \GuzzleHttp\Exception\RequestException
* @expectedExceptionMessage Too many state transitions
*/
public function testDetectsInfiniteLoops()
{
$client = new Client([
'fsm' => $fsm = new RequestFsm(
function () {},
new MessageFactory(),
3
)
]);
$request = $client->createRequest('GET', 'http://foo.com:123');
$request->getEmitter()->on('before', function () {
throw new \Exception('foo');
});
$request->getEmitter()->on('error', function ($e) {
$e->retry();
});
$client->send($request);
}
} }

View File

@ -12,7 +12,6 @@ use GuzzleHttp\Message\Response;
use GuzzleHttp\Ring\Client\MockAdapter; use GuzzleHttp\Ring\Client\MockAdapter;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Event\ErrorEvent; use GuzzleHttp\Event\ErrorEvent;
use GuzzleHttp\Fsm;
use GuzzleHttp\RequestFsm; use GuzzleHttp\RequestFsm;
class RingBridgeTest extends \PHPUnit_Framework_TestCase class RingBridgeTest extends \PHPUnit_Framework_TestCase
@ -26,7 +25,7 @@ class RingBridgeTest extends \PHPUnit_Framework_TestCase
$request->getConfig()->set('foo', 'bar'); $request->getConfig()->set('foo', 'bar');
$trans = new Transaction(new Client(), $request); $trans = new Transaction(new Client(), $request);
$factory = new MessageFactory(); $factory = new MessageFactory();
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, new MessageFactory());
$r = RingBridge::prepareRingRequest($trans, $factory, $fsm); $r = RingBridge::prepareRingRequest($trans, $factory, $fsm);
$this->assertEquals('http', $r['scheme']); $this->assertEquals('http', $r['scheme']);
$this->assertEquals('1.1', $r['version']); $this->assertEquals('1.1', $r['version']);
@ -48,7 +47,7 @@ class RingBridgeTest extends \PHPUnit_Framework_TestCase
$request = new Request('GET', 'http://httpbin.org'); $request = new Request('GET', 'http://httpbin.org');
$trans = new Transaction(new Client(), $request); $trans = new Transaction(new Client(), $request);
$factory = new MessageFactory(); $factory = new MessageFactory();
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, new MessageFactory());
$r = RingBridge::prepareRingRequest($trans, $factory, $fsm); $r = RingBridge::prepareRingRequest($trans, $factory, $fsm);
$this->assertNull($r['query_string']); $this->assertNull($r['query_string']);
$this->assertEquals('/', $r['uri']); $this->assertEquals('/', $r['uri']);
@ -117,7 +116,7 @@ class RingBridgeTest extends \PHPUnit_Framework_TestCase
}); });
$f = new MessageFactory(); $f = new MessageFactory();
$res = ['status' => 200, 'headers' => []]; $res = ['status' => 200, 'headers' => []];
$fsm = new RequestFsm(function () {}); $fsm = new RequestFsm(function () {}, new MessageFactory());
RingBridge::completeRingResponse($trans, $res, $f, $fsm); RingBridge::completeRingResponse($trans, $res, $f, $fsm);
$this->assertInstanceOf( $this->assertInstanceOf(
'GuzzleHttp\Message\ResponseInterface', 'GuzzleHttp\Message\ResponseInterface',
@ -203,16 +202,12 @@ class RingBridgeTest extends \PHPUnit_Framework_TestCase
{ {
$trans = new Transaction(new Client(), new Request('GET', 'http://f.co')); $trans = new Transaction(new Client(), new Request('GET', 'http://f.co'));
$f = new MessageFactory(); $f = new MessageFactory();
$called = false; $fsm = new RequestFsm(function () {}, new MessageFactory());
$fsm = new Fsm('foo', [ try {
'error' => [ RingBridge::completeRingResponse($trans, [], $f, $fsm);
'transition' => function ($trans) use (&$called) { } catch (RequestException $e) {
$called = true; $this->assertSame($trans->request, $e->getRequest());
$this->assertInstanceOf('GuzzleHttp\Exception\RequestException', $trans->exception); $this->assertContains('Guzzle-Ring', $e->getMessage());
} }
]
]);
RingBridge::completeRingResponse($trans, [], $f, $fsm);
$this->assertTrue($called);
} }
} }