Dustin Wilson
6 months ago
commit
7e912be190
15 changed files with 3393 additions and 0 deletions
@ -0,0 +1,77 @@ |
|||
# Catcher-specific |
|||
/test*.html |
|||
/test*.php |
|||
/test/ |
|||
/test/* |
|||
|
|||
# General |
|||
*.DS_Store |
|||
.AppleDouble |
|||
.LSOverride |
|||
|
|||
# Icon must end with two \r |
|||
Icon |
|||
|
|||
|
|||
# Thumbnails |
|||
._* |
|||
|
|||
# Files that might appear in the root of a volume |
|||
.DocumentRevisions-V100 |
|||
.fseventsd |
|||
.Spotlight-V100 |
|||
.TemporaryItems |
|||
.Trashes |
|||
.VolumeIcon.icns |
|||
.com.apple.timemachine.donotpresent |
|||
|
|||
# Directories potentially created on remote AFP share |
|||
.AppleDB |
|||
.AppleDesktop |
|||
Network Trash Folder |
|||
Temporary Items |
|||
.apdisk |
|||
|
|||
# Windows thumbnail cache files |
|||
Thumbs.db |
|||
ehthumbs.db |
|||
ehthumbs_vista.db |
|||
|
|||
# Dump file |
|||
*.stackdump |
|||
|
|||
# Folder config file |
|||
Desktop.ini |
|||
|
|||
# Recycle Bin used on file shares |
|||
$RECYCLE.BIN/ |
|||
|
|||
# Windows Installer files |
|||
*.cab |
|||
*.msi |
|||
*.msm |
|||
*.msp |
|||
|
|||
# Windows shortcuts |
|||
*.lnk |
|||
|
|||
*~ |
|||
|
|||
# temporary files which can be created if a process still has a handle open of a deleted file |
|||
.fuse_hidden* |
|||
|
|||
# KDE directory preferences |
|||
.directory |
|||
|
|||
# Linux trash folder which might appear on any partition or disk |
|||
.Trash-* |
|||
|
|||
# .nfs files are created when an open file is removed but is still being accessed |
|||
.nfs* |
|||
|
|||
/vendor/ |
|||
/vendor-bin/*/vendor |
|||
/tests/html5lib-tests |
|||
/tests/.phpunit.cache |
|||
/tests/coverage |
|||
cachegrind.out.* |
@ -0,0 +1,4 @@ |
|||
Project leads |
|||
------------- |
|||
Dustin Wilson https://dustinwilson.com/ |
|||
J. King https://jkingweb.ca/ |
@ -0,0 +1,22 @@ |
|||
Copyright (c) 2022 Dustin Wilson, J. King |
|||
|
|||
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. |
@ -0,0 +1,87 @@ |
|||
[a]: https://code.mensbeam.com/MensBeam/Catcher |
|||
[b]: https://packagist.org/packages/aws/aws-sdk-php |
|||
[c]: https://github.com/symfony/yaml |
|||
[d]: https://www.php.net/manual/en/function.pcntl-fork.php |
|||
[e]: https://www.php.net/manual/en/function.print-r.php |
|||
[f]: https://github.com/symfony/var-exporter |
|||
[g]: https://github.com/php-fig/log |
|||
|
|||
# AWSSNSHandler # |
|||
|
|||
_AWSSNSHandler_ is a Throwable handler for use in [_Catcher_][a], a Throwable and error handling library for PHP. It sends throwables and errors to Amazon SNS topics. Right now _AWSSNSHandler_ only supports sending to email topics. SMS messages aren't supported at this time. |
|||
|
|||
|
|||
## Requirements ## |
|||
|
|||
* PHP >= 8.1 |
|||
* [mensbeam/catcher][a] ^2.1.0 |
|||
* [aws/aws-sdk-php][b] ^3.283 |
|||
|
|||
|
|||
## Installation ## |
|||
|
|||
```shell |
|||
composer require mensbeam/catcher-awssnshandler |
|||
``` |
|||
|
|||
|
|||
## Usage ## |
|||
|
|||
For most use cases this library requires no configuration and little effort to integrate into non-complex environments: |
|||
|
|||
```php |
|||
use MensBeam\Catcher, |
|||
MensBeam\Catcher\AWSSNSHandler, |
|||
Aws\Sns\SnsClient; |
|||
|
|||
$client = new SnsClient([ |
|||
'version' => 'latest', |
|||
'region' => 'us-west-2', |
|||
'credentials' => [ |
|||
'key' => 'AKIAFIMBZAFZZQL42RMH', |
|||
'secret' => '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' |
|||
] |
|||
]); |
|||
$catcher = new Catcher(new AWSSNSHandler($client, 'arn:aws:sns:us-west-2:701867229025:ook_eek')); |
|||
``` |
|||
|
|||
That's it. It will automatically register Catcher as an exception, error, and shutdown handler and use `AWSSNSHandler` as its sole handler. Like other _Catcher_ handlers, _AWSSNSHandler_ can be configured with a logger. When logging it behaves identically to _JSONHandler_. See the [_Catcher_][a] documentation for more info on how to configure a logger. |
|||
|
|||
## Documentation ## |
|||
|
|||
### MensBeam\Catcher\AWSSNSHandler ### |
|||
|
|||
```php |
|||
namespace MensBeam\Catcher; |
|||
use Aws\Sns\SnsClient; |
|||
|
|||
|
|||
class AWSSNSHandler extends JSONHandler { |
|||
protected SnsClient $client; |
|||
protected string $topicARN; |
|||
|
|||
|
|||
public function __construct(SnsClient $client, string $topicARN, array $options = []); |
|||
|
|||
public function getClient(): SnsClient; |
|||
public function setClient(SnsClient $client): void; |
|||
public function getTopicARN(): string; |
|||
public function setTopicARN(string $topicARN): void; |
|||
} |
|||
``` |
|||
|
|||
#### MensBeam\Catcher\AWSSNSHandler::getClient #### |
|||
|
|||
Returns the `Aws\Sns\SnsClient` the handler uses |
|||
|
|||
#### MensBeam\Catcher\AWSSNSHandler::getTopicARN #### |
|||
|
|||
Returns the AWS SNS topic ARN the handler sends messages to |
|||
|
|||
#### MensBeam\Catcher\AWSSNSHandler::setClient #### |
|||
|
|||
Replaces the `Aws\Sns\SnsClient` the handler uses with one specified |
|||
|
|||
#### MensBeam\Catcher\AWSSNSHandler::setTopicARN #### |
|||
|
|||
Replaces the AWS SNS topic ARN the handler sends messages to with one specified |
@ -0,0 +1,32 @@ |
|||
{ |
|||
"name": "mensbeam/catcher-awsnshandler", |
|||
"description": "Amazon AWS SNS Message Handler for MensBeam's Catcher", |
|||
"type": "library", |
|||
"license": "MIT", |
|||
"autoload": { |
|||
"psr-4": { |
|||
"MensBeam\\Catcher\\": "lib/" |
|||
} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": { |
|||
"MensBeam\\Catcher\\AWSSNSHandler\\Test\\": "tests/lib/" |
|||
} |
|||
}, |
|||
"authors": [ |
|||
{ |
|||
"name": "Dustin Wilson", |
|||
"email": "dustin@dustinwilson.com" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": ">=8.1", |
|||
"mensbeam/catcher": "^2.1.2", |
|||
"aws/aws-sdk-php": "^3.283" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "^10", |
|||
"phake/phake": "^4.4", |
|||
"psr/log": "^3.0" |
|||
} |
|||
} |
File diff suppressed because it is too large
@ -0,0 +1,66 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher; |
|||
use Aws\Sns\SnsClient; |
|||
|
|||
|
|||
class AWSSNSHandler extends JSONHandler { |
|||
protected SnsClient $client; |
|||
protected string $topicARN; |
|||
|
|||
|
|||
|
|||
|
|||
public function __construct(SnsClient $client, string $topicARN, array $options = []) { |
|||
$this->client = $client; |
|||
$this->topicARN = $topicARN; |
|||
parent::__construct($options); |
|||
} |
|||
|
|||
|
|||
|
|||
|
|||
public function getClient(): SnsClient { |
|||
return $this->client; |
|||
} |
|||
|
|||
public function getTopicARN(): string { |
|||
return $this->topicARN; |
|||
} |
|||
|
|||
public function setClient(SnsClient $client): void { |
|||
$this->client = $client; |
|||
} |
|||
|
|||
public function setTopicARN(string $topicARN): void { |
|||
$this->topicARN = $topicARN; |
|||
} |
|||
|
|||
|
|||
protected function handleCallback(array $output): array { |
|||
return $output; |
|||
} |
|||
|
|||
protected function invokeCallback(): void { |
|||
foreach ($this->outputBuffer as $o) { |
|||
if (($o['code'] & self::OUTPUT) === 0) { |
|||
if ($o['code'] & self::LOG) { |
|||
$this->serializeOutputThrowable($o); |
|||
} |
|||
|
|||
continue; |
|||
} |
|||
|
|||
$this->client->publish([ |
|||
'Message' => $this->serializeOutputThrowable($o), |
|||
'TopicArn' => $this->topicARN |
|||
]); |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,38 @@ |
|||
#!/usr/bin/env php |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
$dir = ini_get('extension_dir'); |
|||
$php = escapeshellarg(\PHP_BINARY); |
|||
$code = escapeshellarg(__DIR__ . '/lib'); |
|||
|
|||
|
|||
array_shift($argv); |
|||
foreach ($argv as $k => $v) { |
|||
if (in_array($v, ['--coverage', '--coverage-html'])) { |
|||
$argv[$k] = '--coverage-html tests/coverage'; |
|||
} |
|||
} |
|||
|
|||
$cmd = [ |
|||
$php, |
|||
'-d opcache.enable_cli=0', |
|||
]; |
|||
|
|||
if (!extension_loaded('xdebug')) { |
|||
$cmd[] = '-d zend_extension=xdebug.so'; |
|||
} |
|||
|
|||
$cmd = implode(' ', [ |
|||
...$cmd, |
|||
'-d xdebug.mode=coverage,develop,trace', |
|||
escapeshellarg(__DIR__ . '/vendor/bin/phpunit'), |
|||
'--configuration tests/phpunit.xml', |
|||
...$argv, |
|||
'--display-deprecations' |
|||
]); |
|||
passthru($cmd); |
@ -0,0 +1,18 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
|
|||
ini_set('memory_limit', '2G'); |
|||
ini_set('zend.assertions', '1'); |
|||
ini_set('assert.exception', 'true'); |
|||
error_reporting(\E_ALL); |
|||
define('CWD', dirname(__DIR__)); |
|||
require_once CWD . '/vendor/autoload.php'; |
|||
|
|||
if (function_exists('xdebug_set_filter')) { |
|||
if (defined('XDEBUG_PATH_INCLUDE')) { |
|||
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_INCLUDE, [ CWD . '/lib/' ]); |
|||
} else { |
|||
xdebug_set_filter(\XDEBUG_FILTER_CODE_COVERAGE, \XDEBUG_PATH_WHITELIST, [ CWD . '/lib/' ]); |
|||
} |
|||
} |
@ -0,0 +1,126 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
use MensBeam\Catcher\{ |
|||
Error, |
|||
Handler, |
|||
ThrowableController, |
|||
AWSSNSHandler |
|||
}; |
|||
use Aws\Sns\SnsClient, |
|||
Psr\Log\LoggerInterface, |
|||
Phake; |
|||
|
|||
|
|||
/** @covers \MensBeam\Catcher\AWSSNSHandler */ |
|||
class TestAWSSNSHandler extends \PHPUnit\Framework\TestCase { |
|||
protected ?SnsClient $client = null; |
|||
protected ?Handler $handler = null; |
|||
|
|||
|
|||
public function setUp(): void { |
|||
parent::setUp(); |
|||
|
|||
$this->client = Phake::mock(SnsClient::class); |
|||
Phake::when($this->client)->publish()->thenReturn(true); |
|||
|
|||
$this->handler = new AWSSNSHandler($this->client, 'ook', [ |
|||
'outputBacktrace' => true, |
|||
'silent' => true |
|||
]); |
|||
} |
|||
|
|||
public function testGettingSettingProps() { |
|||
$c = $this->handler->getClient(); |
|||
$this->assertTrue($c instanceof SnsClient); |
|||
$c2 = Phake::mock(SnsClient::class); |
|||
Phake::when($c2)->publish()->thenReturn(true); |
|||
$this->handler->setClient($c2); |
|||
$this->assertNotEquals($c, $this->handler->getClient()); |
|||
$this->assertTrue($this->handler->getClient() instanceof SnsClient); |
|||
|
|||
$this->assertSame('ook', $this->handler->getTopicARN()); |
|||
$this->handler->setTopicARN('eek'); |
|||
$this->assertSame('eek', $this->handler->getTopicARN()); |
|||
} |
|||
|
|||
/** @dataProvider provideInvocationTests */ |
|||
public function testInvocation(\Throwable $throwable, bool $silent, bool $log, ?string $logMethodName, ?array $ignore, int $line): void { |
|||
$this->handler->setOption('outputToStderr', false); |
|||
|
|||
if (!$silent) { |
|||
$this->handler->setOption('silent', false); |
|||
} |
|||
if ($log) { |
|||
$l = Phake::mock(LoggerInterface::class); |
|||
$this->handler->setOption('logger', $l); |
|||
} |
|||
if ($ignore !== null) { |
|||
$this->handler->setOption('ignore', $ignore); |
|||
} |
|||
|
|||
$o = $this->handler->handle(new ThrowableController($throwable)); |
|||
$c = $o['class'] ?? null; |
|||
|
|||
$h = $this->handler; |
|||
$h(); |
|||
$u = $h->getLastOutputThrowable(); |
|||
|
|||
if ($ignore === null) { |
|||
$this->assertNotNull($u); |
|||
$this->assertEquals($c, $u['class']); |
|||
$this->assertEquals(__FILE__, $u['file']); |
|||
$this->assertEquals($line, $u['line']); |
|||
|
|||
if (!$silent) { |
|||
Phake::verify($this->client, Phake::times(1))->publish; |
|||
} |
|||
} else { |
|||
$this->assertNull($u); |
|||
} |
|||
|
|||
if ($log) { |
|||
Phake::verify($l, Phake::times((int)(count($ignore ?? []) === 0)))->$logMethodName; |
|||
} |
|||
} |
|||
|
|||
|
|||
public static function provideHandlingTests(): iterable { |
|||
$options = [ |
|||
[ new \Exception('Ook!'), Handler::BUBBLES | Handler::EXIT, [ 'forceExit' => true ] ], |
|||
[ new \Error('Ook!'), Handler::BUBBLES ], |
|||
[ new Error('Ook!', \E_ERROR, '/dev/null', 42, new \Error('Eek!')), Handler::BUBBLES | Handler::NOW, [ 'forceOutputNow' => true ] ], |
|||
[ new \Exception('Ook!'), Handler::BUBBLES, [ 'logger' => Phake::mock(LoggerInterface::class), 'logWhenSilent' => false ] ], |
|||
[ new \Error('Ook!'), Handler::BUBBLES | Handler::LOG, [ 'forceOutputNow' => true, 'logger' => Phake::mock(LoggerInterface::class) ] ] |
|||
]; |
|||
|
|||
foreach ($options as $o) { |
|||
$o[1] |= Handler::NOW; |
|||
yield $o; |
|||
} |
|||
} |
|||
|
|||
public static function provideInvocationTests(): iterable { |
|||
$options = [ |
|||
[ new \Exception('Ook!'), false, true, 'critical', null ], |
|||
[ new \Exception('Ook!'), false, true, 'critical', [ \Exception::class ] ], |
|||
[ new \Error('Ook!'), true, false, null, null ], |
|||
[ new \Error('Ook!'), true, false, null, [ \Error::class ] ], |
|||
[ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', null ], |
|||
[ new Error('Ook!', \E_ERROR, __FILE__, __LINE__), false, true, 'error', [ \E_ERROR ] ], |
|||
[ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', null ], |
|||
[ new \Exception(message: 'Ook!', previous: new \Error(message: 'Eek!', previous: new \ParseError('Ack!'))), true, true, 'critical', [ \Exception::class ] ] |
|||
]; |
|||
|
|||
$l = count($options); |
|||
foreach ($options as $k => $o) { |
|||
yield [ ...$o, __LINE__ - 4 - $l + $k ]; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,11 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
|
|||
class Error extends \Error {} |
@ -0,0 +1,31 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
|
|||
|
|||
class ErrorHandlingTestCase extends \PHPUnit\Framework\TestCase { |
|||
protected ?Error $lastError = null; |
|||
|
|||
|
|||
public function setUp(): void { |
|||
set_error_handler([ $this, 'handleError' ]); |
|||
} |
|||
|
|||
public function tearDown(): void { |
|||
restore_error_handler(); |
|||
} |
|||
|
|||
public function handleError(int $code, string $message, string $file, int $line): void { |
|||
$e = new Error($message, $code); |
|||
$this->lastError = $e; |
|||
if (in_array($code, [ \E_ERROR, \E_PARSE, \E_CORE_ERROR, \E_COMPILE_ERROR, \E_USER_ERROR, \E_RECOVERABLE_ERROR ])) { |
|||
throw $e; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
use org\bovigo\vfs\vfsStream, |
|||
Psr\Log\LoggerInterface, |
|||
Psr\Log\LoggerTrait; |
|||
|
|||
|
|||
class FailLogger implements LoggerInterface { |
|||
use LoggerTrait; |
|||
|
|||
public function log($level, string|\Stringable $message, array $context = []): void { |
|||
$v = vfsStream::setup('ook'); |
|||
$d = vfsStream::newDirectory('ook', 0777)->at($v); |
|||
file_put_contents($d->url(), $message); |
|||
|
|||
echo "Ook!\n"; |
|||
} |
|||
|
|||
public function error(string|\Stringable $message, array $context = []): void { |
|||
trigger_error('Ook!', \E_USER_ERROR); |
|||
} |
|||
} |
@ -0,0 +1,66 @@ |
|||
<?php |
|||
/** |
|||
* @license MIT |
|||
* Copyright 2022 Dustin Wilson, et al. |
|||
* See LICENSE and AUTHORS files for details |
|||
*/ |
|||
|
|||
declare(strict_types=1); |
|||
namespace MensBeam\Catcher\Test; |
|||
use MensBeam\Catcher\Handler; |
|||
|
|||
|
|||
class TestingHandler extends Handler { |
|||
public array $output = []; |
|||
|
|||
protected ?string $_name = null; |
|||
// Could just use silent option instead, but we need to test Handler::SILENT |
|||
protected bool $_print = false; |
|||
// When printing output, print it as JSON |
|||
protected bool $_printJSON = true; |
|||
|
|||
|
|||
protected function handleCallback(array $output): array { |
|||
$output['code'] = (\PHP_SAPI === 'cli') ? $output['code'] | self::NOW : $output['code']; |
|||
return $output; |
|||
} |
|||
|
|||
protected function invokeCallback(): void { |
|||
foreach ($this->outputBuffer as $o) { |
|||
if ($o['code'] & self::LOG) { |
|||
$this->log($o['controller']->getThrowable(), json_encode([ |
|||
'class' => $o['class'], |
|||
'code' => $o['code'], |
|||
'file' => $o['file'], |
|||
'line' => $o['line'], |
|||
'message' => $o['message'] |
|||
])); |
|||
} |
|||
|
|||
if (($o['code'] & self::OUTPUT) === 0) { |
|||
continue; |
|||
} |
|||
|
|||
$o = $this->cleanOutputThrowable($o); |
|||
|
|||
if ($this->_print) { |
|||
if (!$this->_printJSON) { |
|||
$oo = ''; |
|||
foreach ($o['frames'] as $f) { |
|||
if (!isset($f['args'])) { |
|||
continue; |
|||
} |
|||
$oo .= $this->serializeArgs($f['args']) . "\n"; |
|||
} |
|||
$oo = rtrim($oo); |
|||
} else { |
|||
$oo = json_encode($o, \JSON_THROW_ON_ERROR); |
|||
} |
|||
|
|||
$this->print($oo); |
|||
} |
|||
|
|||
$this->output[] = $o; |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,22 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd" |
|||
beStrictAboutOutputDuringTests="true" |
|||
beStrictAboutTestsThatDoNotTestAnything="true" |
|||
bootstrap="bootstrap.php" |
|||
cacheDirectory=".phpunit.cache" |
|||
colors="true" |
|||
executionOrder="defects" |
|||
requireCoverageMetadata="true" |
|||
> |
|||
<testsuites> |
|||
<testsuite name="Main"> |
|||
<directory prefix="Test" suffix=".php">./cases</directory> |
|||
</testsuite> |
|||
</testsuites> |
|||
<coverage/> |
|||
<source> |
|||
<include> |
|||
<directory suffix=".php">../lib</directory> |
|||
</include> |
|||
</source> |
|||
</phpunit> |
Loading…
Reference in new issue