Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit a73acdb

Browse files
authored
Improved stack trace output. (#203)
1 parent 19c73bd commit a73acdb

File tree

7 files changed

+189
-10
lines changed

7 files changed

+189
-10
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
namespace Some\Really\Long\Name\That\Looks\Dumb;
3+
4+
describe('Exception trimming', function () {
5+
it('should not display irrelevent stack frames', function () {
6+
new SomeClass();
7+
});
8+
9+
it('should render assertion exceptions', function () {
10+
assert('true === false', 'should implode the universe');
11+
});
12+
});
13+
14+
class SomeClass
15+
{
16+
public function __construct()
17+
{
18+
someFunction();
19+
}
20+
}
21+
22+
function someFunction()
23+
{
24+
new SomeOtherClass();
25+
}
26+
27+
class SomeOtherClass
28+
{
29+
public function __construct()
30+
{
31+
throw new \Exception('You done goofed.');
32+
}
33+
}

specs/assert-exception.spec.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
use Peridot\AssertException;
4+
5+
describe('AssertException', function () {
6+
context('::handle()', function () {
7+
it('should throw an exception', function () {
8+
$exception = null;
9+
try {
10+
AssertException::handle('/path/to/file', 111, '', 'You done goofed.');
11+
} catch (AssertException $e) {
12+
$exception = $e;
13+
}
14+
assert(!is_null($exception), 'exception should have been thrown');
15+
assert($exception->getFile() === '/path/to/file', 'exception should have the correct filename');
16+
assert($exception->getLine() === 111, 'exception should have the correct line number');
17+
assert($exception->getMessage() === 'You done goofed.', 'exception should have the correct message');
18+
assert($exception->getTrace() === [], 'exception should have an empty trace');
19+
});
20+
21+
it('should support assertions with an expression', function () {
22+
$exception = null;
23+
try {
24+
AssertException::handle('/path/to/file', 111, 'expression', 'You done goofed.');
25+
} catch (AssertException $e) {
26+
$exception = $e;
27+
}
28+
assert($exception->getMessage() === 'expression You done goofed.', 'exception should have the correct message');
29+
});
30+
});
31+
});

specs/spec-reporter.spec.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@
4848

4949
$exception = null;
5050
try {
51-
throw new Exception("ooops" . PHP_EOL . "nextline");
51+
$expectedTrace = preg_quote(sprintf('%s:%d', __FILE__, __LINE__ + 1), '~') . '.*';
52+
new ExceptionThrower($expectedTrace);
5253
} catch (Exception $e) {
5354
$exception = $e;
5455
}
5556
$this->exception = $exception;
57+
$this->expectedTrace = '~' . $expectedTrace . '~s';
5658

5759
$this->emitter->emit('test.passed', [new Test('passing test', function() {})]);
5860
$this->emitter->emit('test.failed', [new Test('failing test', function() {}), $this->exception]);
@@ -81,9 +83,33 @@
8183
it('should display exception stacks and messages', function() {
8284
$expectedExceptionMessage = " ooops" . PHP_EOL . " nextline";
8385
assert(strstr($this->contents, $expectedExceptionMessage) !== false, "should include exception message");
84-
$trace = preg_replace('/^#/m', " #", $this->exception->getTraceAsString());
85-
assert(strstr($this->contents, $trace) !== false, "should include exception stack");
86+
assert(preg_match($this->expectedTrace, $this->contents), 'should include exception stack');
8687
});
8788
});
8889

8990
});
91+
92+
function throwException(&$pattern)
93+
{
94+
$pattern = preg_quote(sprintf('%s:%d', __FILE__, __LINE__ + 1), '~') . '.*noFilename.*' . $pattern;
95+
$exception = new Exception('ooops' . PHP_EOL . 'nextline');
96+
97+
$reflector = new ReflectionClass('Exception');
98+
$traceProperty = $reflector->getProperty('trace');
99+
$traceProperty->setAccessible(true);
100+
101+
$trace = $traceProperty->getValue($exception);
102+
array_unshift($trace, ['function' => 'noFilename']);
103+
$traceProperty->setValue($exception, $trace);
104+
105+
throw $exception;
106+
}
107+
108+
class ExceptionThrower
109+
{
110+
public function __construct(&$pattern)
111+
{
112+
$pattern = preg_quote(sprintf('%s:%d', __FILE__, __LINE__ + 1), '~') . '.*' . $pattern;
113+
throwException($pattern);
114+
}
115+
}

specs/suiteloader.spec.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
describe('->getTests()', function() {
1919
it("should return file paths matching *.spec.php recursively", function() {
2020
$tests = $this->loader->getTests($this->fixtures);
21-
assert(count($tests) == 5, "suite loader should have loaded 5 specs");
21+
assert(count($tests) == 6, "suite loader should have loaded 6 specs");
2222
});
2323

2424
it("should return single file if it exists", function() {

src/AssertException.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
namespace Peridot;
3+
4+
use Exception;
5+
use ReflectionClass;
6+
7+
/**
8+
* Represents a failed assert() call.
9+
*
10+
* @package Peridot
11+
*/
12+
class AssertException extends Exception
13+
{
14+
/**
15+
* Handle a failed assert() call.
16+
*
17+
* @param string $file
18+
* @param int $line
19+
* @param string $expression
20+
* @param string $description
21+
*
22+
* @return self
23+
*/
24+
public static function handle($file, $line, $expression, $description)
25+
{
26+
throw new self($file, $line, $expression, $description);
27+
}
28+
29+
/**
30+
* Construct a new assert exception.
31+
*
32+
* @param string $file
33+
* @param int $line
34+
* @param string $expression
35+
* @param string $description
36+
*/
37+
public function __construct($file, $line, $expression, $description)
38+
{
39+
if ($expression) {
40+
$message = sprintf('%s %s', $expression, $description);
41+
} else {
42+
$message = $description;
43+
}
44+
45+
parent::__construct($message);
46+
47+
$this->file = $file;
48+
$this->line = $line;
49+
50+
$reflector = new ReflectionClass('Exception');
51+
$traceProperty = $reflector->getProperty('trace');
52+
$traceProperty->setAccessible(true);
53+
$traceProperty->setValue($this, array());
54+
}
55+
}

src/Dsl.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,4 @@ function afterEach(callable $fn)
126126
* Change default assert behavior to throw exceptions
127127
*/
128128
assert_options(ASSERT_WARNING, false);
129-
assert_options(ASSERT_CALLBACK, function ($script, $line, $message, $description) {
130-
throw new Exception($description);
131-
});
129+
assert_options(ASSERT_CALLBACK, ['Peridot\AssertException', 'handle']);

src/Reporter/AbstractBaseReporter.php

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ public function footer()
183183
for ($i = 0; $i < $errorCount; $i++) {
184184
list($test, $error) = $this->errors[$i];
185185
$this->outputError($i + 1, $test, $error);
186+
$this->output->writeln('');
186187
}
187188
}
188189

@@ -191,7 +192,7 @@ public function footer()
191192
*
192193
* @param int $errorNumber
193194
* @param TestInterface $test
194-
* @param $exception - an exception like interface with ->getMessage(), ->getTraceAsString()
195+
* @param $exception - an exception like interface with ->getMessage(), ->getTrace()
195196
*/
196197
protected function outputError($errorNumber, TestInterface $test, $exception)
197198
{
@@ -200,8 +201,32 @@ protected function outputError($errorNumber, TestInterface $test, $exception)
200201
$message = sprintf(" %s", str_replace(PHP_EOL, PHP_EOL . " ", $exception->getMessage()));
201202
$this->output->writeln($this->color('error', $message));
202203

203-
$trace = preg_replace('/^#/m', " #", $exception->getTraceAsString());
204-
$this->output->writeln($this->color('muted', $trace));
204+
$location = sprintf(' at %s:%d', $exception->getFile(), $exception->getLine());
205+
$this->output->writeln($location);
206+
207+
$this->outputTrace($exception->getTrace());
208+
}
209+
210+
/**
211+
* Output a stack trace.
212+
*
213+
* @param array $trace
214+
*/
215+
protected function outputTrace(array $trace)
216+
{
217+
foreach ($trace as $index => $entry) {
218+
if (isset($entry['class'])) {
219+
$function = $entry['class'] . $entry['type'] . $entry['function'];
220+
} else {
221+
$function = $entry['function'];
222+
}
223+
224+
if (strncmp($function, 'Peridot\\', 8) === 0) {
225+
break;
226+
}
227+
228+
$this->output->writeln($this->color('muted', $this->renderTraceEntry($index, $entry, $function)));
229+
}
205230
}
206231

207232
/**
@@ -309,4 +334,15 @@ private function isTtyTerminal(StreamOutput $output)
309334
* @return void
310335
*/
311336
abstract public function init();
337+
338+
private function renderTraceEntry($index, array $entry, $function)
339+
{
340+
if (isset($entry['file'])) {
341+
$location = sprintf(' (%s:%d)', $entry['file'], $entry['line']);
342+
} else {
343+
$location = '';
344+
}
345+
346+
return sprintf(' #%d %s%s', $index, $function, $location);
347+
}
312348
}

0 commit comments

Comments
 (0)