From 66e7cb0e262745aa55d41b7a259cf8b49dd61543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20S=C3=A1gi-Kaz=C3=A1r?= Date: Tue, 20 Feb 2018 09:08:17 +0100 Subject: [PATCH] Add SQS driver --- .travis.yml | 3 + README.md | 1 + composer.json | 4 +- src/Sqs/Driver.php | 275 ++++++++++++++++++++++++ src/Sqs/LICENSE | 19 ++ src/Sqs/README.md | 48 +++++ src/Sqs/Tests/DriverIntegrationTest.php | 258 ++++++++++++++++++++++ src/Sqs/Tests/DriverTest.php | 110 ++++++++++ src/Sqs/composer.json | 47 ++++ src/Sqs/phpunit.xml.dist | 29 +++ 10 files changed, 793 insertions(+), 1 deletion(-) create mode 100644 src/Sqs/Driver.php create mode 100644 src/Sqs/LICENSE create mode 100644 src/Sqs/README.md create mode 100644 src/Sqs/Tests/DriverIntegrationTest.php create mode 100644 src/Sqs/Tests/DriverTest.php create mode 100644 src/Sqs/composer.json create mode 100644 src/Sqs/phpunit.xml.dist diff --git a/.travis.yml b/.travis.yml index 03f98cd..ec338c6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,9 @@ services: env: global: - TEST_COMMAND="composer test-ci" + - secure: "nk2Nm/xFmdp/sakc1q0/rEH/yUVvEzMSbh1trRMp6ldHCzscBTYFnVFrnwf8G6qpaYR604sQV3yujEE1r9B8DZ4mDsHD8WbOtUyaNLINeUnL2uEHXyukE76Bf3hjkSLvKdpriQDZA5xvbfXUBYoh81CdmT9OaQnUtblMe5BN93xB7VsNB68+eCiPK0IUFKTby59rm6ewVB4kanljtvwOunuEfZXvqdfg7HtArN1tWxd8Bn58KaLvYlndFXtLsTlhygnmJAbKd4Mk6H+OVd5Z3Qq3UXaKFBh7ovrob/aWHBLFLGhr6Ga1palh7d2sdO4wCn8dVfbTlvVRvcqxQmwBHUoGOr1/918caN3ZWjEF21kmX8BgK+lHOvexrAPo15jOL6ZXYhJY4zAAZmMXHUAYHxM0UUNPd3KywhdFan3PNsVCYgldcpsU6unnP9PKv/upm+holWnUvd7fgUrZtcnD2Q8lAihkxF7HJrFttqnm0qaipbOFp9flwxskkQbE0qAZZm16GuCw0Yfky3h9YyQ7nIOjWlF9t8/Z4d1VxrwTcNc8Zba4vM5OdvB9qVRrc0ZJVj2z/PeEPJOO0ytUBO/Y+hz10iReGLHtZZukR6RDaF0XlGVwynBPIS6Hu54t3S30jg/DYu8e0jwlb2rP/+LpFpv+ALUzl95a3SWhqgVsjCc=" + - secure: "kQj2YVCU3Hc4R7CVGKfWl3jieP1vEjAkxbUyUCpNC37IYrXujajKdyXbcWBXUeDa++HfICdlBikPdfh/mPC+/WozaBNzoOIFTOEN5zmE9WPLtnB1d7QB5zTnQBX9rb59il+bYfgm8IL2iwK3mupryP8Kspt4nMjVjsEz4sxtqe/XR23RuEgf4UFPqZEZdKSjnwGiUuERvRderkcpROAu7/6X79OFnbjZLDgT/j+eCQypvYXievn700DnWCwRaCBcqB3DepkrppfiXmZLRAjKUgMRVCi0ILfpoUF7rK3aS8hLC0YlIYjmc62HT+KP16NP9htXxZ+DNCH5OEmZjR8pRli3is1RFqeviM+v/Mx4mEpYkTNcoJygE8blFareudQu6ofwrAT/W6q/7LFtjVA2F6wc8CLztZawaqTBxdIhRdZj5q8FTt7lfJaVPsk5/dimz75iinH1opOJPkpBU2rbg3erPWxBz4tvlq3cEfvS/P7y/OFQ1eCEDl2Wi8Y+kSYcEpDDplAg6WGS2KEMx40bVPSZgZ0H0omnchnwuZuJBoSgoSjcSRvUv21ljttCyCVQJfxHpxoLNk6N5BOsTIi5AXu0grIvGiyoNuO9zqLKzuxdYTMqPmLAqLJL0k/R0HzRyL8q2H3UecEaC0etFOvCV/DT5kQLZSjyVSMRfAJ1sL8=" + - secure: "QM3I9ZP1U1eo32iIb/I8x4J4sYlx1W0U7dhvA+2vPoU67JWg/CkBTDBZmNTZl7qP+/TRXO+QlKoClkwBX/CsthEYuuqN35cxs3tVEEMBsnoK3oLxgWG6RmyrHmHd5Jv5JIJucuugcfq3bevVRlqRUJM+uPvO9PvbwPyXxYsMudroXZRhS9a7ZXtkdDh1A2rWJBgakuXHy+rlq8nUnEf3yiKFLxVe9SJdzVVXYeEVthiTe65R8R5AAXMR/UQtdO3CD1/g5Jk2kNXDJKDTUjuRqBo1s2ACX22Eg1eGK//KDR9agMAj8L4thTlJoZMz0q8vqP7FezUvPSN+5GAL8LUy5ODAqO37fqtSmoXWSbcHndNtCnHVTucOe1l0NLLHlTozW//iaK8JknaTwj4gUe9utKsVDaRwAIrkhX6TG0qZtSm375O4sGbgTMRHYNpsZQWkyojC7uAgpBk6RSgz9zvR0db+oDxZTbI/BGR7qkJB6eYiV0Gg8q4NM86D33HGO8XRB7sUjwmJouK+zZVOu4dimwQmuXbFWdXKlJDn3klxBZnPZDK4PIYCWJLZjYDwv6C987H8MMu9oU6ehzaq8RDYwyIoPJmMunc3xqXis8puR66aU2toGC0OeezK2DUI0PFyGktcdLKZQPpUX8tJ5DtOCvMq6pRtQ9Ga5+5298kxDeA=" branches: except: diff --git a/README.md b/README.md index 75d9c84..15411ab 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ $ composer require bernard/drivers ## Drivers +- [Amazon SQS](src/Sqs) - [AMQP](src/Amqp) - [Iron MQ](src/IronMQ) - [Pheanstalk](src/Pheanstalk) diff --git a/composer.json b/composer.json index e6e2ea2..f2a1d9b 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,12 @@ "bernard/ironmq-driver": "self.version", "bernard/pheanstalk-driver": "self.version", "bernard/predis-driver": "self.version", + "bernard/queue-interop-driver": "self.version", "bernard/redis-driver": "self.version", - "bernard/queue-interop-driver": "self.version" + "bernard/sqs-driver": "self.version" }, "require-dev": { + "aws/aws-sdk-php": "^3.20", "ext-redis": "*", "iron-io/iron_mq": "^4.0", "pda/pheanstalk": "^3.0", diff --git a/src/Sqs/Driver.php b/src/Sqs/Driver.php new file mode 100644 index 0000000..52d04f5 --- /dev/null +++ b/src/Sqs/Driver.php @@ -0,0 +1,275 @@ +sqs = $sqs; + $this->queueUrls = $queueUrls; + } + + /** + * {@inheritdoc} + */ + public function listQueues() + { + $result = $this->sqs->listQueues(); + + // TODO: drop this as it can easily get inconsistent? + if (!$queueUrls = $result->get('QueueUrls')) { + return array_keys($this->queueUrls); + } + + foreach ($queueUrls as $queueUrl) { + if (in_array($queueUrl, $this->queueUrls)) { + continue; + } + + $queueName = current(array_reverse(explode('/', $queueUrl))); + $this->queueUrls[$queueName] = $queueUrl; + } + + return array_keys($this->queueUrls); + } + + /** + * {@inheritdoc} + * + * @see http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sqs-2012-11-05.html#createqueue + * + * @throws SqsException + */ + public function createQueue($queueName) + { + if ($this->queueExists($queueName)) { + return; + } + + $parameters = [ + 'QueueName' => $queueName, + ]; + + if ($this->isFifoQueue($queueName)) { + $parameters['Attributes'] = [ + 'FifoQueue' => 'true', + ]; + } + + $result = $this->sqs->createQueue($parameters); + + $this->queueUrls[$queueName] = $result['QueueUrl']; + } + + /** + * @param string $queueName + * + * @return bool + * + * @throws SqsException + */ + private function queueExists($queueName) + { + try { + $this->resolveUrl($queueName); + + return true; + } catch (\InvalidArgumentException $exception) { + return false; + } catch (SqsException $exception) { + if ($previousException = $exception->getPrevious()) { + switch ($previousException->getCode()) { + case self::AWS_SQS_EXCEPTION_BAD_REQUEST: + case self::AWS_SQS_EXCEPTION_NOT_FOUND: + return false; + } + } + + throw $exception; + } + } + + /** + * @param string $queueName + * + * @return bool + */ + private function isFifoQueue($queueName) + { + return $this->endsWith($queueName, self::AWS_SQS_FIFO_SUFFIX); + } + + /** + * @param string $haystack + * @param string $needle + * + * @return bool + */ + private function endsWith($haystack, $needle) + { + $length = strlen($needle); + if ($length === 0) { + return true; + } + + return substr($haystack, -$length) === $needle; + } + + /** + * {@inheritdoc} + */ + public function countMessages($queueName) + { + $queueUrl = $this->resolveUrl($queueName); + + $result = $this->sqs->getQueueAttributes([ + 'QueueUrl' => $queueUrl, + 'AttributeNames' => ['ApproximateNumberOfMessages'], + ]); + + if (isset($result['Attributes']['ApproximateNumberOfMessages'])) { + return (int) $result['Attributes']['ApproximateNumberOfMessages']; + } + + return 0; + } + + /** + * {@inheritdoc} + */ + public function pushMessage($queueName, $message) + { + $queueUrl = $this->resolveUrl($queueName); + + $parameters = [ + 'QueueUrl' => $queueUrl, + 'MessageBody' => $message, + ]; + + if ($this->isFifoQueue($queueName)) { + $parameters['MessageGroupId'] = __METHOD__; + $parameters['MessageDeduplicationId'] = md5($message); + } + + $this->sqs->sendMessage($parameters); + } + + /** + * {@inheritdoc} + */ + public function popMessage($queueName, $duration = 5) + { + if ($message = $this->cache->pop($queueName)) { + return $message; + } + + $queueUrl = $this->resolveUrl($queueName); + + $result = $this->sqs->receiveMessage([ + 'QueueUrl' => $queueUrl, + 'MaxNumberOfMessages' => $this->prefetch, + 'WaitTimeSeconds' => $duration, + ]); + + if (!$result || !$messages = $result->get('Messages')) { + return [null, null]; + } + + foreach ($messages as $message) { + $this->cache->push($queueName, [$message['Body'], $message['ReceiptHandle']]); + } + + return $this->cache->pop($queueName); + } + + /** + * {@inheritdoc} + */ + public function acknowledgeMessage($queueName, $receipt) + { + $queueUrl = $this->resolveUrl($queueName); + + $this->sqs->deleteMessage([ + 'QueueUrl' => $queueUrl, + 'ReceiptHandle' => $receipt, + ]); + } + + /** + * {@inheritdoc} + */ + public function peekQueue($queueName, $index = 0, $limit = 20) + { + return []; + } + + /** + * {@inheritdoc} + * + * @see http://docs.aws.amazon.com/aws-sdk-php/v3/api/api-sqs-2012-11-05.html#deletequeue + */ + public function removeQueue($queueName) + { + $queueUrl = $this->resolveUrl($queueName); + + $this->sqs->deleteQueue([ + 'QueueUrl' => $queueUrl, + ]); + } + + /** + * {@inheritdoc} + */ + public function info() + { + return [ + 'prefetch' => $this->prefetch, + ]; + } + + /** + * AWS works with queue URLs rather than queue names. Returns either queue URL (if queue exists) for given name or null if not. + * + * @param string $queueName + * + * @return mixed + * + * @throws SqsException + */ + private function resolveUrl($queueName) + { + if (isset($this->queueUrls[$queueName])) { + return $this->queueUrls[$queueName]; + } + + $result = $this->sqs->getQueueUrl(['QueueName' => $queueName]); + + if ($result && $queueUrl = $result->get('QueueUrl')) { + return $this->queueUrls[$queueName] = $queueUrl; + } + + throw new \InvalidArgumentException('Queue "'.$queueName.'" cannot be resolved to an url.'); + } +} diff --git a/src/Sqs/LICENSE b/src/Sqs/LICENSE new file mode 100644 index 0000000..0fc6ac5 --- /dev/null +++ b/src/Sqs/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Henrik Bjornskov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Sqs/README.md b/src/Sqs/README.md new file mode 100644 index 0000000..8f1c9c7 --- /dev/null +++ b/src/Sqs/README.md @@ -0,0 +1,48 @@ +# Amazon SQS driver + +[![Latest Version](https://img.shields.io/github/release/bernardphp/sqs-driver.svg?style=flat-square)](https://github.com/bernardphp/sqs-driver/releases) + +**[Amazon SQS](https://aws.amazon.com/sqs/) driver for Bernard.** + + +## Install + +Via Composer + +```bash +$ composer require bernard/sqs-driver +``` + + +## Usage + +```php + [ + 'key' => 'your_access_key', + 'secret' => 'your_secret_key', + ], + 'region' => 'us-east-1', + 'version' => '2012-11-05' +]); + +$driver = new Driver($client); + +// or with prefetching +$driver = new Driver($client, [], 5); + +// or with aliased queue urls +$driver = new Driver($client, [ + 'queue-name' => 'queue-url', +]); +``` + + +## License + +The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/src/Sqs/Tests/DriverIntegrationTest.php b/src/Sqs/Tests/DriverIntegrationTest.php new file mode 100644 index 0000000..9967377 --- /dev/null +++ b/src/Sqs/Tests/DriverIntegrationTest.php @@ -0,0 +1,258 @@ +markTestSkipped('Missing SQS credentials'); + } + + $this->sqs = new SqsClient([ + 'credentials' => [ + 'key' => $accessKey, + 'secret' => $secretKey, + ], + 'region' => $region, + 'version' => '2012-11-05' + ]); + + $this->driver = new Driver($this->sqs); + } + + /** + * Creates a queue name for a topic, also saves the queue name in the local cleanup queue. + * It is necessary to use less deterministic names, because Amazon limits how queue names can be reused. + * (Have to wait 60 seconds after a queue is deleted) + * + * @param string $topic + * + * @return string + */ + private function queueName($topic) + { + // PHP version is added so tests can run in parallel in CI + return $this->queues[] = sprintf('bernard_%s_%d_%s', $topic, PHP_VERSION_ID, uniqid()); + } + + private function createQueue($topic) + { + $queueName = $this->queueName($topic); + + try { + $result = $this->sqs->createQueue(['QueueName' => $queueName]); + } catch (SqsException $e) { + if ($e->getAwsErrorCode() == 'AWS.SimpleQueueService.QueueDeletedRecently') { + $this->fail('Test executed too soon! Please try again later (with at least 60s delay)!'); + } + + throw $e; + } + + return [$queueName, $result['QueueUrl']]; + } + + public function tearDown() + { + foreach ($this->queues as $queue) { + $result = $this->sqs->getQueueUrl(['QueueName' => $queue]); + + if ($result) { + $this->sqs->deleteQueue(['QueueUrl' => $result->get('QueueUrl')]); + } + } + } + + /** + * @test + */ + public function it_lists_queues() + { + $queue = $this->createQueue('list'); + + $retries = 0; + $retryLimit = 60; + + // Wait for the queue to be created + while ($retries < $retryLimit) { + $queues = $this->driver->listQueues(); + + if (in_array($queue[0], $queues, true)) { + break; + } + + $retries++; + + sleep(1); + } + + $this->assertLessThan($retryLimit, $retries, 'Failed asserting queue creation within the retry limit'); + } + + /** + * @test + */ + public function it_creates_a_queue() + { + $queue = $this->queueName('create'); + + $this->driver->createQueue($queue); + + $result = $this->sqs->getQueueUrl(['QueueName' => $queue]); + $this->assertContains($queue, $result->get('QueueUrl')); + } + + /** + * @test + */ + public function it_counts_the_number_of_messages_in_a_queue() + { + $queue = $this->createQueue('count'); + + $this->sqs->sendMessageBatch([ + 'QueueUrl' => $queue[1], + 'Entries' => [ + [ + 'Id' => '1', + 'MessageBody' => self::MESSAGE, + ], + [ + 'Id' => '2', + 'MessageBody' => self::MESSAGE, + ], + ], + ]); + + $this->assertEquals(2, $this->driver->countMessages($queue[0])); + } + + /** + * @test + */ + public function it_pushes_a_message_to_a_queue() + { + $queue = $this->createQueue('push'); + + $this->driver->pushMessage($queue[0], self::MESSAGE); + + $result = $this->sqs->receiveMessage([ + 'QueueUrl' => $queue[1], + 'MaxNumberOfMessages' => 1, + 'WaitTimeSeconds' => 2, + ]); + + $messages = $result->get('Messages'); + + $this->assertCount(1, $messages); + $this->assertEquals(self::MESSAGE, $messages[0]['Body']); + } + + /** + * @test + */ + public function it_pops_messages_from_a_queue() + { + $queue = $this->createQueue('pop'); + + $this->sqs->sendMessage([ + 'QueueUrl' => $queue[1], + 'MessageBody' => self::MESSAGE, + ]); + + $message = $this->driver->popMessage($queue[0], 2); + + $this->assertEquals(self::MESSAGE, $message[0]); + $this->assertNotEmpty($message[1]); + } + + /** + * @test + */ + public function it_returns_an_empty_message_when_popping_messages_from_an_empty_queue() + { + $queue = $this->createQueue('pop_empty'); + + $this->assertEquals([null, null], $this->driver->popMessage($queue[0], 1)); + } + + /** + * @test + */ + public function it_acknowledges_a_message() + { + $queue = $this->createQueue('ack'); + + $this->sqs->sendMessage([ + 'QueueUrl' => $queue[1], + 'MessageBody' => self::MESSAGE, + ]); + + $result = $this->sqs->receiveMessage([ + 'QueueUrl' => $queue[1], + 'MaxNumberOfMessages' => 1, + 'WaitTimeSeconds' => 5, + ]); + + $messages = $result->get('Messages'); + + $this->assertCount(1, $messages); + + $this->driver->acknowledgeMessage($queue[0], $messages[0]['ReceiptHandle']); + + $result = $this->sqs->getQueueAttributes([ + 'QueueUrl' => $queue[1], + 'AttributeNames' => ['ApproximateNumberOfMessages'], + ]); + + $this->assertEquals(0, $result['Attributes']['ApproximateNumberOfMessages']); + } + + /** + * @test + */ + public function it_removes_a_queue() + { + $queue = $this->createQueue('remove'); + $this->queues = []; + + $this->driver->removeQueue($queue[0]); + + try { + $this->sqs->getQueueUrl(['QueueName' => $queue[0]]); + } catch (SqsException $e) { + $this->assertEquals($e->getAwsErrorCode(), 'AWS.SimpleQueueService.NonExistentQueue'); + } + } +} diff --git a/src/Sqs/Tests/DriverTest.php b/src/Sqs/Tests/DriverTest.php new file mode 100644 index 0000000..163650a --- /dev/null +++ b/src/Sqs/Tests/DriverTest.php @@ -0,0 +1,110 @@ +sqs = $this->prophesize(SqsClient::class); + + $this->driver = new Driver($this->sqs->reveal(), [self::QUEUE => self::URL]); + } + + /** + * @test + */ + public function it_is_a_driver() + { + $this->assertInstanceOf(\Bernard\Driver::class, $this->driver); + } + + /** + * @test + */ + public function it_lists_queues_from_the_internal_cache() + { + $result = $this->prophesize(ResultInterface::class); + $result->get('QueueUrls')->willReturn(null); + + $this->sqs->listQueues()->willReturn($result); + $this->sqs->createQueue(['QueueName' => self::QUEUE]); + + $queues = $this->driver->listQueues(); + + $this->assertContains(self::QUEUE, $queues); + } + + /** + * @test + */ + public function it_prefetches_messages_from_a_queue() + { + $result = $this->prophesize(ResultInterface::class); + $result->get('Messages')->willReturn([ + ['Body' => self::MESSAGE, 'ReceiptHandle' => '1'], + ['Body' => self::MESSAGE, 'ReceiptHandle' => '2'], + ]); + + $this->sqs->receiveMessage([ + 'QueueUrl' => self::URL, + 'MaxNumberOfMessages' => 10, + 'WaitTimeSeconds' => 5, + ])->willReturn($result); + + $driver = new Driver($this->sqs->reveal(), [self::QUEUE => self::URL], 10); + + $message = $driver->popMessage(self::QUEUE); + + $this->assertEquals([self::MESSAGE, '1'], $message); + + $message = $driver->popMessage(self::QUEUE, 10); + + $this->assertEquals([self::MESSAGE, '2'], $message); + } + + /** + * @test + */ + public function it_peeks_a_queue() + { + $this->assertEquals([], $this->driver->peekQueue(self::QUEUE)); + } + + /** + * @test + */ + public function it_exposes_info() + { + $this->assertEquals(['prefetch' => 2], $this->driver->info()); + } + + /** + * @test + */ + public function it_exposes_prefetch_info() + { + $driver = new Driver($this->sqs->reveal(), [self::QUEUE => 'url'], 10); + + $this->assertEquals(['prefetch' => 10], $driver->info()); + } +} diff --git a/src/Sqs/composer.json b/src/Sqs/composer.json new file mode 100644 index 0000000..a1745b6 --- /dev/null +++ b/src/Sqs/composer.json @@ -0,0 +1,47 @@ +{ + "name": "bernard/sqs-driver", + "description": "Amazon SQS driver for Bernard", + "license": "MIT", + "keywords": ["message queue", "message", "queue", "bernard", "amazon", "aws", "sqs"], + "homepage": "https://github.com/bernardphp/sqs-driver", + "authors": [ + { + "name": "Henrik Bjornskov" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "require": { + "php": "^5.6 || ^7.0", + "aws/aws-sdk-php": "^3.0", + "bernard/bernard": "dev-master" + }, + "require-dev": { + "phpunit/phpunit": "^5.7 || ^6.0 || ^7.0" + }, + "autoload": { + "psr-4": { + "Bernard\\Driver\\Sqs\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "config": { + "sort-packages": true + }, + "scripts": { + "clean": "rm -rf build/ vendor/", + "test": "vendor/bin/phpunit -v", + "test-integration": "vendor/bin/phpunit -v --group integration" + }, + "extra": { + "branch-alias": { + "dev-master": "0.1-dev" + } + }, + "prefer-stable": true, + "minimum-stability": "dev" +} diff --git a/src/Sqs/phpunit.xml.dist b/src/Sqs/phpunit.xml.dist new file mode 100644 index 0000000..7e07a2b --- /dev/null +++ b/src/Sqs/phpunit.xml.dist @@ -0,0 +1,29 @@ + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + + + + + integration + + +