diff --git a/.gitattributes b/.gitattributes index f4c293c..def7917 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ /bin/coveralls.sh export-ignore +/bin/composer-set-config.php export-ignore /.coveralls.yml export-ignore /.gitattributes export-ignore /.gitignore export-ignore diff --git a/.travis.yml b/.travis.yml index 1357196..60661b9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,7 @@ install: - mv .git ~.git dist: trusty +sudo: false php: - 5.4 @@ -27,10 +28,30 @@ matrix: - php: 5.3 dist: precise + - php: hhvm + env: HHVM_PHP7=1 + + - php: hhvm-3.12 + env: HHVM_PHP7=1 + + - php: hhvm-3.15 + env: HHVM_PHP7=1 + + - php: hhvm-3.18 + env: HHVM_PHP7=1 + + - php: hhvm-nightly + env: HHVM_PHP7=1 + +before_script: + - if [ ${HHVM_PHP7+x} ]; then bin/composer-set-config.php php 7.0; fi + - if [ ${HHVM_PHP7+x} ]; then export EXTRA_CONFIG='-d hhvm.php7.all=1'; else export EXTRA_CONFIG=''; fi + script: - - php -r "echo phpversion() . PHP_EOL;" - - travis_wait composer update --prefer-lowest --no-interaction && php -d error_reporting=$(php -r "var_export(E_ALL & ~E_DEPRECATED);") vendor/bin/phpunit - - travis_wait composer update --no-interaction && vendor/bin/phpunit + - php ${EXTRA_CONFIG} -r "echo phpversion() . PHP_EOL;" + + - if [ ${HHVM_PHP7+x} ]; then echo "Do not test the lowest dependencies on HHVM with PHP7 support"; else travis_wait composer update --prefer-lowest --no-interaction && php -d hhvm.jit=0 -d error_reporting=$(php -r "var_export(E_ALL & ~E_DEPRECATED);") vendor/bin/phpunit; fi + - travis_wait composer update --no-interaction && php -d hhvm.jit=0 ${EXTRA_CONFIG} vendor/bin/phpunit after_script: - mv ~.git .git diff --git a/CHANGELOG.md b/CHANGELOG.md index 042482d..155910d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.10.0 + +* Removed `Awesomite\StackTrace\Arguments\Values\MultipleValues` +* Added tests for HHVM with flag `hhvm.php7.all=1` +* `Awesomite\StackTrace\StackTrace::unserialize` throws `Awesomite\StackTrace\Exceptions\LogicException` instead of `LogicException` + ## 0.9.2 * Fixed phpdoc for `Awesomite\StackTrace\Arguments\ArgumentsInterface::getIterator` diff --git a/bin/composer-set-config.php b/bin/composer-set-config.php new file mode 100755 index 0000000..f3bff1f --- /dev/null +++ b/bin/composer-set-config.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +function->getReflection()->getParameters() as $parameter) { $declaration = new Declaration($parameter); - if ($declaration->isVariadic()) { - $result[] = new Argument($declaration, $arguments ? new MultipleValues($arguments) : null); - $arguments = array(); - continue; - } - if ($arguments) { $result[] = new Argument($declaration, array_shift($arguments)); continue; diff --git a/src/Arguments/Values/MultipleValues.php b/src/Arguments/Values/MultipleValues.php deleted file mode 100644 index 0f0ccfd..0000000 --- a/src/Arguments/Values/MultipleValues.php +++ /dev/null @@ -1,67 +0,0 @@ -values = $values; - $this->limit = $limit; - } - - public function isRealValueReadable() - { - return false; - } - - public function getRealValue() - { - throw new CannotRestoreValueException('Cannot restore value!'); - } - - public function __toString() - { - return $this->getDump(); - } - - public function dump() - { - $limit = $this->limit; - echo 'array(' . count($this->values) . ') {' . "\n"; - foreach ($this->values as $key => $value) { - $valDump = str_replace("\n", "\n ", $value->getDump()); - $valDump = substr($valDump, 0, -2); - echo " [{$key}] => \n {$valDump}"; - if (!--$limit) { - if (count($this->values) > $this->limit) { - echo " (...)\n"; - } - break; - } - } - echo '}' . "\n"; - } - - public function getDump() - { - ob_start(); - $this->dump(); - $result = ob_get_contents(); - ob_end_clean(); - - return $result; - } -} diff --git a/src/StackTrace.php b/src/StackTrace.php index 460a2cd..d334811 100644 --- a/src/StackTrace.php +++ b/src/StackTrace.php @@ -5,6 +5,7 @@ use Awesomite\Iterators\CallbackIterator; use Awesomite\StackTrace\Arguments\Values\DeserializedValue; use Awesomite\StackTrace\Arguments\Values\Value; +use Awesomite\StackTrace\Exceptions\LogicException; use Awesomite\StackTrace\SourceCode\File; use Awesomite\StackTrace\Steps\Step; use Awesomite\StackTrace\Steps\StepInterface; @@ -17,8 +18,8 @@ */ class StackTrace implements StackTraceInterface { - const VERSION = '0.9.2'; - const CONSTRAINTS_VERSION = '>=0.1.0 <0.10.0'; + const VERSION = '0.10.0'; + const CONSTRAINTS_VERSION = '>=0.1.0 <0.11.0'; private $arrayStackTrace; @@ -92,7 +93,7 @@ public function unserialize($serialized) $data = unserialize($serialized); if (!Semver::satisfies($data['__version'], static::CONSTRAINTS_VERSION)) { $message = 'Cannot use incompatible version to unserialize stack trace (serialized by: %s, current: %s).'; - throw new \LogicException(sprintf($message, $data['__version'], static::VERSION)); + throw new LogicException(sprintf($message, $data['__version'], static::VERSION)); } $this->arrayStackTrace = $data['steps']; $this->unserialized = true; @@ -160,32 +161,38 @@ public function setVarDumper(VarDumperInterface $varDumper) */ public function convertStep(array $step, $toSerialize = false) { - $result = array(); - foreach ($step as $key => $value) { - $result[$key] = $value; - } - if ($this->withoutArgs) { - $result['args'] = array(); - } else if (empty($result[Constants::KEY_ARGS_CONVERTED]) && isset($result['args'])) { - $result['args'] = $this->convertArgs($result['args']); - $result[Constants::KEY_ARGS_CONVERTED] = true; + $step['args'] = array(); + } else if (empty($step[Constants::KEY_ARGS_CONVERTED]) && isset($step['args'])) { + $maxArgs = null; + if (version_compare(PHP_VERSION, '5.6') >= 0) { + $fakeStep = new Step($step); + $reflectionFn = $fakeStep->hasCalledFunction() && $fakeStep->getCalledFunction()->hasReflection() + ? $fakeStep->getCalledFunction()->getReflection() + : null; + if (!is_null($reflectionFn) && $reflectionFn->isVariadic()) { + $maxArgs = count($reflectionFn->getParameters()); + } + } + + $step['args'] = $this->convertArgs($step['args'], $maxArgs); + $step[Constants::KEY_ARGS_CONVERTED] = true; } if ( !$toSerialize - && !isset($result[Constants::KEY_FILE_OBJECT]) - && isset($result['file']) - && isset($this->files[$result['file']]) + && !isset($step[Constants::KEY_FILE_OBJECT]) + && isset($step['file']) + && isset($this->files[$step['file']]) ) { - $result[Constants::KEY_FILE_OBJECT] = $this->files[$result['file']]; + $step[Constants::KEY_FILE_OBJECT] = $this->files[$step['file']]; } - if (isset($result['object'])) { - unset($result['object']); + if (isset($step['object'])) { + unset($step['object']); } - return $result; + return $step; } private function getVarDumper() @@ -197,19 +204,32 @@ private function getVarDumper() return $this->varDumper; } - private function convertArgs(array $args) + /** + * @param array $inputArgs + * @param int|null $maxArgs + * @return array + */ + private function convertArgs(array $inputArgs, $maxArgs) { /** - * input has to be copied to different array, - * because array $args returned by debug_backtrace function contains references from PHP 7.0 + * debug_backtrace()[$x]['args'] can contain references */ - $result = array(); + $args = array(); + foreach ($inputArgs as $key => $value) { + $args[$key] = $value; + } + + if (!is_null($maxArgs) && $maxArgs <= count($args)) { + $preparedCopy = $args; + $args = array_slice($preparedCopy, 0, $maxArgs - 1); + $args[] = array_slice($preparedCopy, $maxArgs - 1); + } foreach ($args as $key => $value) { - $result[$key] = $this->convertArg($value); + $args[$key] = $this->convertArg($value); } - return $result; + return $args; } private function convertArg($value) diff --git a/src/StackTraceFactory.php b/src/StackTraceFactory.php index c050ec3..d9105d2 100644 --- a/src/StackTraceFactory.php +++ b/src/StackTraceFactory.php @@ -6,6 +6,8 @@ class StackTraceFactory { + private static $rootExceptionClass = null; + /** * @param int $stepLimit * @param bool $ignoreArgs @@ -45,14 +47,36 @@ public function create($stepLimit = 0, $ignoreArgs = false) */ public function createByThrowable($exception, $stepLimit = 0, $ignoreArgs = false) { - $exceptionClass = version_compare(PHP_VERSION, '7.0') >= 0 ? '\Throwable' : '\Exception'; - if (!$exception instanceof $exceptionClass) { - throw new InvalidArgumentException("Argument should be an instance of {$exceptionClass}!"); + $exceptionClass = $this->getRootExceptionClass(); + if (!is_object($exception) || !$exception instanceof $exceptionClass) { + throw new InvalidArgumentException(sprintf( + "Expected argument of type %s, %s given", + $exceptionClass, + is_object($exception) ? get_class($exception) : gettype($exception) + )); } return $this->createBy($exception, $stepLimit, $ignoreArgs); } + /** + * HHVM still does not support \Throwable interface + * + * @return null|string + */ + private function getRootExceptionClass() + { + if (is_null(self::$rootExceptionClass)) { + $reflection = new \ReflectionClass('\Exception'); + $throwableExists = interface_exists('\Throwable', false); + self::$rootExceptionClass = $throwableExists && $reflection->implementsInterface('\Throwable') + ? '\Throwable' + : '\Exception'; + } + + return self::$rootExceptionClass; + } + /** * @param \Exception|\Throwable $exception * @param int $stepLimit diff --git a/tests/Arguments/ArgumentsTest.php b/tests/Arguments/ArgumentsTest.php index 618912a..9b8dd19 100644 --- a/tests/Arguments/ArgumentsTest.php +++ b/tests/Arguments/ArgumentsTest.php @@ -23,6 +23,7 @@ class ArgumentsTest extends BaseTestCase public function testAll(Arguments $arguments, $count) { $this->assertSame($count, count($arguments)); + $this->assertSame(count($arguments), count(iterator_to_array($arguments))); foreach ($arguments as $argument) { $this->assertTrue($argument instanceof ArgumentInterface); } @@ -60,7 +61,7 @@ private function providerVariadic() )); return array( - $this->createByRawValues(array('1', '2', '3'), $function), + $this->createByRawValues(array(array('1', '2', '3')), $function), 1 ); } diff --git a/tests/Arguments/Values/MultipleValuesTest.php b/tests/Arguments/Values/MultipleValuesTest.php deleted file mode 100644 index 9d8041c..0000000 --- a/tests/Arguments/Values/MultipleValuesTest.php +++ /dev/null @@ -1,119 +0,0 @@ -assertSame(false, $value->isRealValueReadable()); - } - - /** - * @dataProvider providerObject - * - * @expectedException \Awesomite\StackTrace\Arguments\Values\CannotRestoreValueException - * @expectedExceptionMessage Cannot restore value! - * - * @param MultipleValues $multipleValues - */ - public function testGetRealValue(MultipleValues $multipleValues) - { - $multipleValues->getRealValue(); - } - - /** - * @dataProvider providerObject - * - * @param MultipleValues $value - */ - public function testToString(MultipleValues $value) - { - $this->assertSame($value->__toString(), $value->getDump()); - } - - /** - * @dataProvider providerObject - * - * @param MultipleValues $value - */ - public function testDump(MultipleValues $value) - { - $this->expectOutputString($value->getDump()); - $value->dump(); - } - - /** - * @dataProvider providerObject - * - * @param MultipleValues $value - * @param string $expectedDump - */ - public function testGetDump(MultipleValues $value, $expectedDump) - { - $this->assertSame($expectedDump, $value->getDump()); - } - - public function providerObject() - { - $subData = range(0, 11); - $subDataArg = array(); - foreach ($subData as $value) { - $subDataArg[] = new Value($value); - } - array_unshift( - $subDataArg, - new MultipleValues( - array(new Value(5)) - ) - ); - $subValue = new MultipleValues($subDataArg, 5); - - $multipleValue = new MultipleValues(array( - new Value(1), - new Value(false), - new Value(array()), - $subValue, - )); - $expectedDump = << - int(1) - [1] => - bool(false) - [2] => - array(0) { - } - [3] => - array(13) { - [0] => - array(1) { - [0] => - int(5) - } - [1] => - int(0) - [2] => - int(1) - [3] => - int(2) - [4] => - int(3) - (...) - } -} - -DUMP; - - return array( - array($multipleValue, $expectedDump), - ); - } -} diff --git a/tests/Listeners/TestListener.php b/tests/Listeners/TestListener.php index 6c0d2ec..8e6ba91 100644 --- a/tests/Listeners/TestListener.php +++ b/tests/Listeners/TestListener.php @@ -15,6 +15,11 @@ class TestListener extends BridgeTestListener private $messages = array(); + public function __construct() + { + $this->getConsoleOutput()->writeln(sprintf('PHP %s', phpversion())); + } + public function __destruct() { $output = $this->getConsoleOutput(); diff --git a/tests/StackTraceTest.php b/tests/StackTraceTest.php index b06b2d6..95d2863 100644 --- a/tests/StackTraceTest.php +++ b/tests/StackTraceTest.php @@ -2,6 +2,7 @@ namespace Awesomite\StackTrace; +use Awesomite\StackTrace\Arguments\ArgumentInterface; use Awesomite\StackTrace\Steps\StepInterface; use Awesomite\VarDumper\LightVarDumper; @@ -107,4 +108,89 @@ public function testCannotUnserialize() $string = 'C:31:"Awesomite\StackTrace\StackTrace":81:{a:3:{s:5:"steps";a:0:{}s:13:"filesContents";a:0:{}s:9:"__version";s:7:"999.0.0";}}'; unserialize($string); } + + public function testVariadic() + { + if (version_compare(PHP_VERSION, '5.6') >= 0) { + $stackTraceVariadic = new StackTraceVariadic($this); + $stackTraceVariadic->handleTest(); + } else { + $this->assertTrue(true); + } + } + + public function testSemiVariadic() + { + $this->handleSemiVariadic(1); + $this->handleSemiVariadic(1, 2); + $this->handleSemiVariadic(1, 2, 3); + } + + private function handleSemiVariadic() + { + $factory = new StackTraceFactory(); + $stackTrace = $factory->create(2); + /** @var StepInterface[] $steps */ + $steps = iterator_to_array($stackTrace->getIterator()); + $step = $steps[1]; + /** @var ArgumentInterface[] $args */ + $args = iterator_to_array($step->getArguments()); + $this->assertSame(count(func_get_args()), count($args)); + foreach ($args as $argument) { + $this->assertTrue($argument->hasValue()); + $this->assertFalse($argument->hasDeclaration()); + } + } + + /** + * There is an option to change value of passed argument usign debug_backtrace() function. + * Class StackTrace should not change any value passed by reference. + * + * @see testChangeReference + */ + public function testDoNotChangeReferences() + { + $original = 'original'; + $copy = $original; + $this->doNotChangeReferences($original); + $this->assertSame($copy, $original); + } + + private function doNotChangeReferences(&$reference) + { + /** + * debug_backtrace()[$x]['args'] can contain references + */ + $factory = new StackTraceFactory(); + $stackTrace = $factory->create(2); + foreach ($stackTrace as $step) { + } + } + + public function testChangeReference() + { + /** + * HHVM does not allow to change value of reference using debug_backtrace() + */ + if (defined('HHVM_VERSION')) { + $this->assertTrue(true); + return; + } + + $original = 'original'; + $copy = $original; + $this->changeReference($original); + $this->assertNotSame($copy, $original); + } + + private function changeReference(&$reference) + { + $this->modifyArgsInStackTrace(); + } + + private function modifyArgsInStackTrace() + { + $stackTrace = debug_backtrace(); + $stackTrace[1]['args'][0] = 'I\'m a hacker!'; + } } diff --git a/tests/StackTraceVariadic.php b/tests/StackTraceVariadic.php new file mode 100644 index 0000000..9cb7305 --- /dev/null +++ b/tests/StackTraceVariadic.php @@ -0,0 +1,53 @@ +testCase = $testCase; + } + + public function handleTest() + { + $this->testVariadic(1, 2); + $this->testVariadic(1, 2, 3); + $this->testVariadic(1, 2, 3, 4); + $this->testVariadic(1, 2, 3, 4, 5); + } + + private function testVariadic($first, $second, ...$third) + { + $factory = new StackTraceFactory(); + $stackTrace = $factory->create(2); + /** @var StepInterface[] $steps */ + $steps = iterator_to_array($stackTrace->getIterator()); + $step = $steps[1]; + /** @var ArgumentInterface[] $args */ + $args = iterator_to_array($step->getArguments()); + $this->testCase->assertSame(3, count($args)); + /** + * @see https://travis-ci.org/awesomite/stack-trace/builds/239418547 + * Bug in older HHVM versions: + * Variadic parameter is missing in debug_backtrace()[$x]['args'] + */ + if (!defined('HHVM_VERSION') || version_compare(HHVM_VERSION, '3.9') >= 0) { + $this->testCase->assertSame(empty($third), !$args[2]->hasValue()); + $this->testCase->assertTrue($args[2]->hasDeclaration()); + } + + for ($i = 0; $i < 2; $i++) { + $this->testCase->assertTrue($args[$i]->hasValue()); + $this->testCase->assertTrue($args[$i]->hasDeclaration()); + } + } +} diff --git a/tests/SyntaxTest.php b/tests/SyntaxTest.php index 4f99049..f8287f8 100644 --- a/tests/SyntaxTest.php +++ b/tests/SyntaxTest.php @@ -7,9 +7,6 @@ */ class SyntaxTest extends BaseTestCase { - /** - * @runInSeparateProcess - */ public function testSyntax() { $delimiter = DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR; @@ -21,10 +18,31 @@ public function testSyntax() $iterator = new \RecursiveIteratorIterator($directory); $regex = new \RegexIterator($iterator, '/^.+\.php$/', \RecursiveRegexIterator::GET_MATCH); $counter = 0; + $toSkip = $this->getToSkip(); foreach ($regex as $file) { + foreach ($toSkip as $pattern) { + if (preg_match($pattern, $file[0])) { + continue 2; + } + } $counter++; require_once $file[0]; } $this->assertGreaterThan(0, $counter); } + + /** + * Returns array of patterns + * + * @return array + */ + private function getToSkip() + { + $result = array(); + + $subpath = implode(DIRECTORY_SEPARATOR, array('src', 'Exceptions', 'StackTraceException.php')); + $result[] = '#' . preg_quote($subpath, '#') . '#'; + + return $result; + } } diff --git a/tests/hhvm-polyfill/StackTraceException.php.polyfill b/tests/hhvm-polyfill/StackTraceException.php.polyfill new file mode 100644 index 0000000..70fd20b --- /dev/null +++ b/tests/hhvm-polyfill/StackTraceException.php.polyfill @@ -0,0 +1,10 @@ += 0; + +if ($isHhvm && $isPhp7 && !interface_exists('\Throwable', false)) { + /** + * @internal + */ + interface Throwable + { + } + + require_once __DIR__ . DIRECTORY_SEPARATOR . 'StackTraceException.php.polyfill'; +}