Skip to content

Commit

Permalink
Update the stacktrace files and fixes #2
Browse files Browse the repository at this point in the history
  • Loading branch information
Austin Kregel committed Dec 1, 2018
1 parent 786d446 commit 83aa4d9
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 85 deletions.
147 changes: 62 additions & 85 deletions src/Stacktrace.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand All @@ -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");

Expand All @@ -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)) {
Expand Down
48 changes: 48 additions & 0 deletions tests/StacktraceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Kregel\ExceptionProbe\Tests;

use Kregel\ExceptionProbe\Codeframe;
use Kregel\ExceptionProbe\Stacktrace;
use PHPUnit\Framework\TestCase;

Expand All @@ -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);
}
}

Expand All @@ -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()
Expand All @@ -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);
}
}

0 comments on commit 83aa4d9

Please sign in to comment.