diff --git a/src/Stacktrace.php b/src/Stacktrace.php index b89757b..c910661 100755 --- a/src/Stacktrace.php +++ b/src/Stacktrace.php @@ -4,36 +4,34 @@ class Stacktrace { + protected const REGEX_STACK_PART = '/#\d+ (.*)\((\d+)\): (.*)/'; + protected const REGEX_STACK_MESSAGE = '/(.*)in (\/.*:\d+$)/'; + protected const REGEX_STACK_LOOKBACK = '/(?<=(\.php))/'; + /** + * @const int The number of lines of code we want to grab. + */ + public const NUMBER_OF_LINES_TO_DISPLAY = 5; + /** + * @var array + */ + public $message; /** * @var string */ protected $stacktrace; - /** * @var array */ protected $brokenStackTrace; - /** * @var array */ protected $brokenMap; - - /** - * @var array - */ - public $message; - /** * @var Codeframe[] */ protected $codeFrames; - /** - * @const int The number of lines of code we want to grab. - */ - public const NUMBER_OF_LINES_TO_DISPLAY = 5; - /** * Stacktrace constructor. * @param $stackTrace @@ -60,88 +58,29 @@ public function parse(?string $stackTrace = null): array return $this->breakUpTheStacks()->getFilesFromBrokenMap()->codeFrames; } - /** - * We need to parse the lines that have a file in them, the lines that don't have a file in them, and a stack trace message. - * @return $this - */ - protected function breakUpTheStacks() - { - $this->brokenStackTrace = explode("\n", $this->stacktrace); - - $stack = array_values(array_filter($this->brokenStackTrace, function ($input) { - return stripos($input, '#') !== false; - })); - - $this->message = array_values(array_filter(array_diff($this->brokenStackTrace, $stack), function ($input) { - return stripos($input, 'stack trace') === false; - })); - - foreach($this->message as $message) { - [$message, $otherFile] = explode(' in /', $message); - } - - $this->message = $message; - - $otherFile = preg_split('/(?<=[\:])/', $otherFile, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - - array_unshift($stack, sprintf('#00 /%s(%d): ', trim($otherFile[0], ':'), $otherFile[1])); - - $this->brokenMap = array_map(function ($frame) { - return preg_split('/(?<=[\):?])/', $frame, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - }, $stack); - - return $this; - } - /** * Convert the frame into a CodeFrame which includes relative code. * @return $this */ protected function getFilesFromBrokenMap() { - $this->codeFrames = array_values(array_filter(array_map(function ($line) { - if (count($line) == 1) { - return; - } - - [$mainFrame, $_, $frame] = $line; - - // We only want to parse the files that are PHP files for now... We don't care about other files... - if (stripos($mainFrame, '.php') !== false) { - [$_, $line, $file] = $this->parseFrame($mainFrame); + $mapParts = array_filter($this->brokenMap); - if (!$this->validateTheFile($file)) { - return; - } - - $linesOfCode = $this->getTheCodeFromTheFile($file, $line); - - return new Codeframe($file, $line, $linesOfCode, trim($frame)); + $this->codeFrames = array_values(array_map(function ($frame) { + if (count($frame) === 3) { + [$file, $linesOfCode, $line] = $frame; } - }, $this->brokenMap))); - - return $this; - } - /** - * Break the frame of a stack trace into three pars, the frame, the line number, and the file itself. - * @param $mainFrame - * @return array - */ - protected function parseFrame($mainFrame): array - { - $splitFrame = preg_split('/(?<=(\.php))/', $mainFrame, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - [$frame, $line] = $splitFrame; + if (!$this->isValidFile($file)) { + return new Codeframe($file, 0, [], trim($line)); + } - $realFrame = $frame; - if (count($splitFrame) > 2) { - $realFrame = $splitFrame[2]; - } + $codes = $this->getTheCodeFromTheFile($file, (int) $linesOfCode); - [$file] = array_values(array_filter(preg_split('/#\d+\s/', $frame, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY))); - $lineNumber = (int) str_replace(['(', ')'], '', $line); + return new Codeframe($file, (int) $linesOfCode, $codes, trim($line)); + }, $mapParts)); - return [$realFrame, $lineNumber, trim($file)]; + return $this; } /** @@ -151,7 +90,7 @@ protected function parseFrame($mainFrame): array * @param int $currentLine * @return array */ - protected function getTheCodeFromTheFile($file, $lineNumber, $currentLine = 0): array + protected function getTheCodeFromTheFile(string $file, int $lineNumber, int $currentLine = 0): array { $handle = fopen($file, "r"); @@ -176,12 +115,50 @@ protected function getTheCodeFromTheFile($file, $lineNumber, $currentLine = 0): return $linesOfCode; } + /** + * We need to parse the lines that have a file in them, the lines that don't have a file in them, and a stack trace message. + * @return $this + */ + protected function breakUpTheStacks() + { + $this->brokenStackTrace = explode("\n", $this->stacktrace); + + $stack = array_values(array_filter($this->brokenStackTrace, function ($input) { + return stripos($input, '#') !== false; + })); + + $newMessage = array_values(array_filter(array_diff($this->brokenStackTrace, $stack), function ($input) { + return stripos($input, 'stack trace') === false; + })); + + if (empty($newMessage)) { + $this->message = ''; + } else { + $newMessage = $newMessage[0]; + preg_match_all(static::REGEX_STACK_MESSAGE, $newMessage, $match); + + $this->message = $match[1][0]; + } + + $this->brokenMap = array_map(function ($frame) { + preg_match_all(static::REGEX_STACK_PART, $frame, $matches); + + $parts = array_filter(array_map(function ($match) { + return $match[0] ?? null; + }, $matches)); + + return array_splice($parts, 1, count($parts)); + }, $stack); + + return $this; + } + /** * See if the file is empty, if it exists, or if it's readable. * @param $file * @return bool */ - protected function validateTheFile($file): bool + protected function isValidFile($file): bool { // If there's no file we can't do anything with it... if (empty($file)) { diff --git a/tests/StacktraceTest.php b/tests/StacktraceTest.php index 3df679c..993399a 100644 --- a/tests/StacktraceTest.php +++ b/tests/StacktraceTest.php @@ -6,6 +6,7 @@ namespace Kregel\ExceptionProbe\Tests; +use Kregel\ExceptionProbe\Codeframe; use Kregel\ExceptionProbe\Stacktrace; use PHPUnit\Framework\TestCase; @@ -32,6 +33,24 @@ public function testWeCanParseFromAnException() } catch (\Exception $exception) { $array = $this->stacktrace->parse($exception->getTraceAsString()); $this->assertTrue(is_array($array)); + + $firstCodeframe = $array[0]; + $this->assertInstanceOf(Codeframe::class, $firstCodeframe); + $this->assertTrue(stripos($firstCodeframe->file, 'tests/StacktraceTests.php') !== true); + $this->assertSame('Kregel\ExceptionProbe\Tests\TestClassException->handle()', $firstCodeframe->frame); + + $codeLine = $firstCodeframe->code[32]; + $this->assertSame(' (new TestClassException())->handle();' . "\n", $codeLine); + + $secondsCodeframe = $array[1]; + $this->assertInstanceOf(Codeframe::class, $secondsCodeframe); + $this->assertTrue(stripos($secondsCodeframe->file, 'vendor/phpunit/phpunit/src/Framework/TestCase.php') !== true); + $this->assertSame('Kregel\ExceptionProbe\Tests\StacktraceTest->testWeCanParseFromAnException()', $secondsCodeframe->frame); + + $lastFrame = $array[8]; + $this->assertInstanceOf(Codeframe::class, $lastFrame); + $this->assertTrue(stripos($lastFrame->file, 'vendor/phpunit/phpunit/src/TextUI/Command.php') !== true); + $this->assertSame('PHPUnit\TextUI\TestRunner->doRun(Object(PHPUnit\Framework\TestSuite), Array, true)', $lastFrame->frame); } } @@ -40,6 +59,13 @@ public function testWeGetAnInvalidFile() $exceptionString = '#0 /some/fake/file.php(31): Kregel\ExceptionProbe\Tests\TestClassException->handle()'; $array = $this->stacktrace->parse($exceptionString); $this->assertTrue(is_array($array)); + $this->assertTrue(!empty($array)); + + $firstCodeframe = $array[0]; + $this->assertInstanceOf(Codeframe::class, $firstCodeframe); + $this->assertSame('/some/fake/file.php', $firstCodeframe->file); + $this->assertSame('Kregel\ExceptionProbe\Tests\TestClassException->handle()', $firstCodeframe->frame); + $this->assertCount(0, $firstCodeframe->code); } public function testWeGetAnUnWritableFile() @@ -48,4 +74,26 @@ public function testWeGetAnUnWritableFile() $array = $this->stacktrace->parse($exceptionString); $this->assertTrue(is_array($array)); } + + public function testWeThrowAnException() + { + $this->expectException(\InvalidArgumentException::class); + $exceptionString = ''; + $array = $this->stacktrace->parse($exceptionString); + $this->assertTrue(is_array($array)); + } + + public function testWeCanHandleLaravelBasedExceptions() + { + $exceptionString = "Symfony\Component\Debug\Exception\FatalThrowableError: Too few arguments to function App\Domain\Service\GitHub::__construct(), 1 passed in /home/austinkregel/Sites/lager/app/Jobs/RefreshGithubData.php on line 44 and exactly 2 expected in /home/austinkregel/Sites/lager/app/Domain/Service/GitHub.php:29\nStack trace:\n#0 /home/austinkregel/Sites/lager/app/Jobs/RefreshGithubData.php(44): App\Domain\Service\GitHub->__construct(Object(App\Social))\n#1 [internal function]: App\Jobs\RefreshGithubData->handle()\n#2 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(29): call_user_func_array(Array, Array)\n#3 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(87): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()\n#4 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(31): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))\n#5 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/Container.php(572): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)\n#6 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(94): Illuminate\Container\Container->call(Array)\n#7 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(116): Illuminate\Bus\Dispatcher->Illuminate\Bus\{closure}(Object(App\Jobs\RefreshGithubData))\n#8 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php(104): Illuminate\Pipeline\Pipeline->Illuminate\Pipeline\{closure}(Object(App\Jobs\RefreshGithubData))\n#9 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php(98): Illuminate\Pipeline\Pipeline->then(Object(Closure))\n#10 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php(49): Illuminate\Bus\Dispatcher->dispatchNow(Object(App\Jobs\RefreshGithubData), false)\n#11 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php(83): Illuminate\Queue\CallQueuedHandler->call(Object(Illuminate\Queue\Jobs\RedisJob), Array)\n#12 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(327): Illuminate\Queue\Jobs\Job->fire()\n#13 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(277): Illuminate\Queue\Worker->process('redis', Object(Illuminate\Queue\Jobs\RedisJob), Object(Illuminate\Queue\WorkerOptions))\n#14 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Worker.php(118): Illuminate\Queue\Worker->runJob(Object(Illuminate\Queue\Jobs\RedisJob), 'redis', Object(Illuminate\Queue\WorkerOptions))\n#15 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(102): Illuminate\Queue\Worker->daemon('redis', 'changelager', Object(Illuminate\Queue\WorkerOptions))\n#16 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Queue/Console/WorkCommand.php(86): Illuminate\Queue\Console\WorkCommand->runWorker('redis', 'changelager')\n#17 /home/austinkregel/Sites/lager/vendor/laravel/horizon/src/Console/WorkCommand.php(46): Illuminate\Queue\Console\WorkCommand->handle()\n#18 [internal function]: Laravel\Horizon\Console\WorkCommand->handle()\n#19 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(29): call_user_func_array(Array, Array)\n#20 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(87): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()\n#21 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(31): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))\n#22 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Container/Container.php(572): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)\n#23 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Console/Command.php(183): Illuminate\Container\Container->call(Array)\n#24 /home/austinkregel/Sites/lager/vendor/symfony/console/Command/Command.php(255): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))\n#25 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Console/Command.php(170): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))\n#26 /home/austinkregel/Sites/lager/vendor/symfony/console/Application.php(893): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#27 /home/austinkregel/Sites/lager/vendor/symfony/console/Application.php(262): Symfony\Component\Console\Application->doRunCommand(Object(Laravel\Horizon\Console\WorkCommand), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#28 /home/austinkregel/Sites/lager/vendor/symfony/console/Application.php(145): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#29 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Console/Application.php(89): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#30 /home/austinkregel/Sites/lager/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(122): Illuminate\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#31 /home/austinkregel/Sites/lager/artisan(37): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))\n#32 {main}"; + $array = $this->stacktrace->parse($exceptionString); + $this->assertTrue(is_array($array)); + $this->assertTrue(!empty($array)); + + $firstCodeframe = $array[0]; + $this->assertInstanceOf(Codeframe::class, $firstCodeframe); + $this->assertSame('/home/austinkregel/Sites/lager/app/Jobs/RefreshGithubData.php', $firstCodeframe->file); + $this->assertSame('App\Domain\Service\GitHub->__construct(Object(App\Social))', $firstCodeframe->frame); + $this->assertCount(0, $firstCodeframe->code); + } }