From 51e8f84df7260614017f638060fef55c716f4d08 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Fri, 28 Dec 2018 14:17:53 +0100 Subject: [PATCH 01/18] Start consolidating common tests into Basic test collection --- tests/README.md | 32 +++++++++++++++++++ tests/basic/README.md | 16 ++++++++++ tests/basic/configuration.basic.xml | 16 ++++++++++ .../_files => basic/unit}/StatusTest.php | 0 tests/end-to-end/loggers/log-junit.phpt | 2 +- ...ation.phpt => testdox-colors-verbose.phpt} | 5 +-- tests/end-to-end/loggers/testdox-xml.phpt | 2 +- tests/end-to-end/loggers/testdox.phpt | 5 +-- 8 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 tests/README.md create mode 100644 tests/basic/README.md create mode 100644 tests/basic/configuration.basic.xml rename tests/{end-to-end/loggers/_files => basic/unit}/StatusTest.php (100%) rename tests/end-to-end/loggers/{testdox-status-colorization.phpt => testdox-colors-verbose.phpt} (68%) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 00000000000..ef8a96feab7 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,32 @@ +# PHPUnit self-tests + +This document contains the notes of projects contributors about testing the PHPUnit core and its integration with the most important dependencies. + +## Quick start + +There are two main ways to self-test PHPUnit. The first is to test _everything_ using the default configuration. Here's how to do that with pretty colors in a human-readable format: + +``` +cd /path/to/phpunit +./phpunit --testdox --colors=always --verbose +``` + +If you want to do a very quick check health-check of most basic use cases you can use the `basic` test collection: + +``` +./phpunit --testdox --colors=always --verbose -c tests/basic/configuration.basic.xml +``` + +The `basic` suite of tests puts the core system through its paces and covers most of the basic use cases of PHPUnit including happy flows and common exceptions. + +## Structure of the self-test collection + +Note: this section will change often while `tests/` is being refactored. + +- `configuration.xml`: the global configuration file which defines the internal `unit` and `end-to-end` tests suites +- `tests/` + - `_files`: specialized helper files; input/output samples + - `basic/`: fast tests covering all basics + - `configuration.basic.xml`: configuration file tailored for the `basic` suite + - `end-to-end/`: run PHPUnit as a seperate process and observe its behaviour via console messages and the filesystem + - `unit/`: unit tests for individual smallest components and integration tests of common usa cases diff --git a/tests/basic/README.md b/tests/basic/README.md new file mode 100644 index 00000000000..e41cf58faa1 --- /dev/null +++ b/tests/basic/README.md @@ -0,0 +1,16 @@ +# PHPUnit Basic Training + +The `basic` test suite is designed to put PHPUnit through its paces: +- basic test runner functionality + - configuration options + - test selection, grouping + - execution reordering and dependecy resolution are noticeable +- all test result statuses +- common fixture errors and exceptions +- common dataprovider errors and exceptions +- out-of-sequence test events are processed correctly +- events from isolated sandboxes are reported to the user +- execution order and reporting order are decoupled + +## References +- https://github.com/sebastianbergmann/phpunit/issues/3453 diff --git a/tests/basic/configuration.basic.xml b/tests/basic/configuration.basic.xml new file mode 100644 index 00000000000..b366eae49bf --- /dev/null +++ b/tests/basic/configuration.basic.xml @@ -0,0 +1,16 @@ + + + + + unit + integration + + + + + + + diff --git a/tests/end-to-end/loggers/_files/StatusTest.php b/tests/basic/unit/StatusTest.php similarity index 100% rename from tests/end-to-end/loggers/_files/StatusTest.php rename to tests/basic/unit/StatusTest.php diff --git a/tests/end-to-end/loggers/log-junit.phpt b/tests/end-to-end/loggers/log-junit.phpt index b0b90b4cdf4..0efd50c271c 100644 --- a/tests/end-to-end/loggers/log-junit.phpt +++ b/tests/end-to-end/loggers/log-junit.phpt @@ -7,7 +7,7 @@ $arguments = [ '--log-junit', 'php://stdout', 'StatusTest', - \realpath(__DIR__ . '/_files/StatusTest.php'), + \realpath(__DIR__ . '/../../basic/unit/StatusTest.php'), ]; \array_splice($_SERVER['argv'], 1, count($arguments), $arguments); diff --git a/tests/end-to-end/loggers/testdox-status-colorization.phpt b/tests/end-to-end/loggers/testdox-colors-verbose.phpt similarity index 68% rename from tests/end-to-end/loggers/testdox-status-colorization.phpt rename to tests/end-to-end/loggers/testdox-colors-verbose.phpt index 208d839bda1..9c645f02cb7 100644 --- a/tests/end-to-end/loggers/testdox-status-colorization.phpt +++ b/tests/end-to-end/loggers/testdox-colors-verbose.phpt @@ -1,9 +1,10 @@ --TEST-- -phpunit --testdox --colors=always --verbose RouterTest ../_files/StatusTest.php +phpunit --testdox --colors=always --verbose -c tests/basic/configuration.basic.xml --FILE-- Date: Sat, 29 Dec 2018 13:33:42 +0100 Subject: [PATCH 02/18] Add custom test result status messages --- tests/basic/unit/StatusTest.php | 35 ++++++++ .../loggers/_files/raw_output_StatusTest.txt | 47 ++++++++++- tests/end-to-end/loggers/log-junit.phpt | 84 +++++++++++++++---- tests/end-to-end/loggers/testdox-xml.phpt | 44 ++++++++-- 4 files changed, 186 insertions(+), 24 deletions(-) diff --git a/tests/basic/unit/StatusTest.php b/tests/basic/unit/StatusTest.php index 8f891dffc1d..e6c6dceaf31 100644 --- a/tests/basic/unit/StatusTest.php +++ b/tests/basic/unit/StatusTest.php @@ -47,4 +47,39 @@ public function testWarning(): void { throw new Warning; } + + public function testSuccessWithMessage(): void + { + $this->assertTrue(true, '"success with custom message"'); + } + + public function testFailureWithMessage(): void + { + $this->assertTrue(false, 'failure with custom message'); + } + + public function testErrorWithMessage(): void + { + throw new \RuntimeException('error with custom message'); + } + + public function testIncompleteWithMessage(): void + { + $this->markTestIncomplete('incomplete with custom message'); + } + + public function testSkippedWithMessage(): void + { + $this->markTestSkipped('skipped with custom message'); + } + + public function testRiskyWithMessage(): void + { + $this->markAsRisky('risky with custom message'); + } + + public function testWarningWithMessage(): void + { + throw new Warning('warning with custom message'); + } } diff --git a/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt b/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt index 42ace20c301..b16008a49bb 100644 --- a/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt +++ b/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt @@ -41,8 +41,53 @@ Runtime: %s │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:48 │ + ✔ Success with message  %f ms + ✘ Failure with message  %f ms + │ + │ failure with custom message%w + │ Failed asserting that false is true. + │ + │ %stests%ebasic%eunit%eStatusTest.php:58 + │ + + ✘ Error with message  %f ms + │ + │ RuntimeException: error with custom message + │ + │ %stests%ebasic%eunit%eStatusTest.php:63 + │ + + ∅ Incomplete with message  %f ms + │ + │ incomplete with custom message + │ + │ %stests%ebasic%eunit%eStatusTest.php:68 + │ + + → Skipped with message  %f ms + │ + │ skipped with custom message + │ + │ %stests%ebasic%eunit%eStatusTest.php:73 + │ + + ☢ Risky with message  %f ms + │ + │ This test did not perform any assertions%w + │%w + │ %stests%ebasic%eunit%eStatusTest.php:76%w + │%w + │ + + ✘ Warning with message  %f ms + │ + │ warning with custom message + │ + │ %stests%ebasic%eunit%eStatusTest.php:83 + │ + Time: %s, Memory: %s ERRORS! -Tests: 7, Assertions: 2, Errors: 1, Failures: 1, Warnings: 1, Skipped: 1, Incomplete: 1, Risky: 1. +Tests: 14, Assertions: 4, Errors: 2, Failures: 2, Warnings: 2, Skipped: 2, Incomplete: 2, Risky: 2. diff --git a/tests/end-to-end/loggers/log-junit.phpt b/tests/end-to-end/loggers/log-junit.phpt index 0efd50c271c..94af0a22b36 100644 --- a/tests/end-to-end/loggers/log-junit.phpt +++ b/tests/end-to-end/loggers/log-junit.phpt @@ -17,37 +17,70 @@ PHPUnit\TextUI\Command::main(); --EXPECTF-- PHPUnit %s by Sebastian Bergmann and contributors. -.FEISRW 7 / 7 (100%) +.FEISRW.FEISRW 14 / 14 (100%) - - - + + + vendor\project\StatusTest::testFailure Failed asserting that false is true. %s%eStatusTest.php:%d - + vendor\project\StatusTest::testError RuntimeException:%w %s%eStatusTest.php:%d - + - + - + Risky Test - + vendor\project\StatusTest::testWarning +%s%eStatusTest.php:%d + + + + + vendor\project\StatusTest::testFailureWithMessage +failure with custom message +Failed asserting that false is true. + +%s%eStatusTest.php:%d + + + + vendor\project\StatusTest::testErrorWithMessage +RuntimeException: error with custom message + +%s%eStatusTest.php:%d + + + + + + + + + + Risky Test + + + + vendor\project\StatusTest::testWarningWithMessage +warning with custom message + %s%eStatusTest.php:%d @@ -57,38 +90,59 @@ RuntimeException:%w Time: %s, Memory: %s -There was 1 error: +There were 2 errors: 1) vendor\project\StatusTest::testError RuntimeException:%w %s%eStatusTest.php:%d +2) vendor\project\StatusTest::testErrorWithMessage +RuntimeException: error with custom message + +%s%eStatusTest.php:%d + -- -There was 1 warning: +There were 2 warnings: 1) vendor\project\StatusTest::testWarning %s%eStatusTest.php:%d +2) vendor\project\StatusTest::testWarningWithMessage +warning with custom message + +%s%eStatusTest.php:%d + -- -There was 1 failure: +There were 2 failures: 1) vendor\project\StatusTest::testFailure Failed asserting that false is true. %s%eStatusTest.php:%d +2) vendor\project\StatusTest::testFailureWithMessage +failure with custom message +Failed asserting that false is true. + +%s%eStatusTest.php:%d + -- -There was 1 risky test: +There were 2 risky tests: 1) vendor\project\StatusTest::testRisky This test did not perform any assertions -%s:42 +%s%eStatusTest.php:%d + +2) vendor\project\StatusTest::testRiskyWithMessage +This test did not perform any assertions + +%s%eStatusTest.php:%d ERRORS! -Tests: 7, Assertions: 2, Errors: 1, Failures: 1, Warnings: 1, Skipped: 1, Incomplete: 1, Risky: 1. +Tests: 14, Assertions: 4, Errors: 2, Failures: 2, Warnings: 2, Skipped: 2, Incomplete: 2, Risky: 2. diff --git a/tests/end-to-end/loggers/testdox-xml.phpt b/tests/end-to-end/loggers/testdox-xml.phpt index 7b6ea867592..7cb6de14165 100644 --- a/tests/end-to-end/loggers/testdox-xml.phpt +++ b/tests/end-to-end/loggers/testdox-xml.phpt @@ -16,7 +16,7 @@ PHPUnit\TextUI\Command::main(); --EXPECTF-- PHPUnit %s by Sebastian Bergmann and contributors. -.FEISRW 7 / 7 (100%) +.FEISRW.FEISRW 14 / 14 (100%) @@ -25,43 +25,71 @@ PHPUnit %s by Sebastian Bergmann and contributors. + + + + + + + Time: %s, Memory: %s -There was 1 error: +There were 2 errors: 1) vendor\project\StatusTest::testError -RuntimeException:%s +RuntimeException:%w + +%s%eStatusTest.php:%d + +2) vendor\project\StatusTest::testErrorWithMessage +RuntimeException: error with custom message %s%eStatusTest.php:%d -- -There was 1 warning: +There were 2 warnings: 1) vendor\project\StatusTest::testWarning %s%eStatusTest.php:%d +2) vendor\project\StatusTest::testWarningWithMessage +warning with custom message + +%s%eStatusTest.php:%d + -- -There was 1 failure: +There were 2 failures: 1) vendor\project\StatusTest::testFailure Failed asserting that false is true. %s%eStatusTest.php:%d +2) vendor\project\StatusTest::testFailureWithMessage +failure with custom message +Failed asserting that false is true. + +%s%eStatusTest.php:%d + -- -There was 1 risky test: +There were 2 risky tests: 1) vendor\project\StatusTest::testRisky This test did not perform any assertions -%s:42 +%s%eStatusTest.php:%d + +2) vendor\project\StatusTest::testRiskyWithMessage +This test did not perform any assertions + +%s%eStatusTest.php:%d ERRORS! -Tests: 7, Assertions: 2, Errors: 1, Failures: 1, Warnings: 1, Skipped: 1, Incomplete: 1, Risky: 1. +Tests: 14, Assertions: 4, Errors: 2, Failures: 2, Warnings: 2, Skipped: 2, Incomplete: 2, Risky: 2. From 4848c9bf48b1ebc9a9bb4f6ac04edf5320473fee Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Mon, 7 Jan 2019 12:05:25 +0100 Subject: [PATCH 03/18] Visualize whitespace in verbose diff output --- src/Util/TestDox/CliTestDoxPrinter.php | 4 ++-- tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Util/TestDox/CliTestDoxPrinter.php b/src/Util/TestDox/CliTestDoxPrinter.php index cba54800ede..17ba12140fd 100644 --- a/src/Util/TestDox/CliTestDoxPrinter.php +++ b/src/Util/TestDox/CliTestDoxPrinter.php @@ -177,9 +177,9 @@ protected function colorizeMessageAndDiff(string $style, string $message): strin $throwable[] = $line; } else { if (\substr($line, 0, 1) === '-') { - $line = Color::colorize('fg-red', $line); + $line = Color::colorize('fg-red', Color::visualizeWhitespace($line, true)); } elseif (\substr($line, 0, 1) === '+') { - $line = Color::colorize('fg-green', $line); + $line = Color::colorize('fg-green', Color::visualizeWhitespace($line, true)); } elseif ($line === '@@ @@') { $line = Color::colorize('fg-cyan', $line); } diff --git a/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php b/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php index e7c3c20da7e..d4aff89abff 100644 --- a/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php +++ b/tests/unit/Util/TestDox/CliTestDoxPrinterColorTest.php @@ -43,8 +43,8 @@ public function testColorizesDiffInFailureMessage(): void $this->printer->endTest($this, 0.001); $this->assertStringContainsString(Color::colorize('bg-red,fg-white', 'some message'), $this->printer->getBuffer()); - $this->assertStringContainsString(Color::colorize('fg-red', '--- Expected'), $this->printer->getBuffer()); - $this->assertStringContainsString(Color::colorize('fg-green', '+++ Actual'), $this->printer->getBuffer()); + $this->assertStringContainsString(Color::colorize('fg-red', '---' . Color::dim('·') . 'Expected'), $this->printer->getBuffer()); + $this->assertStringContainsString(Color::colorize('fg-green', '+++' . Color::dim('·') . 'Actual'), $this->printer->getBuffer()); $this->assertStringContainsString(Color::colorize('fg-cyan', '@@ @@'), $this->printer->getBuffer()); } } From f099e537bc9d4ba1691cf5d7a4ca9e3b80cdbad8 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Mon, 7 Jan 2019 12:06:41 +0100 Subject: [PATCH 04/18] Test housekeeping after TestDox merging from master --- src/Util/Color.php | 3 +- tests/basic/unit/StatusTest.php | 5 +- .../loggers/_files/raw_output_StatusTest.txt | 39 ++++---- tests/end-to-end/loggers/debug.phpt | 99 ++++++++++++++++--- tests/end-to-end/loggers/testdox-xml.phpt | 28 +++--- 5 files changed, 127 insertions(+), 47 deletions(-) diff --git a/src/Util/Color.php b/src/Util/Color.php index dc88e4019c1..83e368385d7 100644 --- a/src/Util/Color.php +++ b/src/Util/Color.php @@ -93,8 +93,7 @@ public static function colorizePath(string $path, ?string $prevPath = null, bool $last = \count($path) - 1; $path[$last] = \preg_replace_callback( '/([\-_\.]+|phpt$)/', - function ($matches) - { + function ($matches) { return self::dim($matches[0]); }, $path[$last] diff --git a/tests/basic/unit/StatusTest.php b/tests/basic/unit/StatusTest.php index e6c6dceaf31..036ea1f93c4 100644 --- a/tests/basic/unit/StatusTest.php +++ b/tests/basic/unit/StatusTest.php @@ -12,6 +12,9 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Warning; +/** + * @testdox Test result status with and without message + */ class StatusTest extends TestCase { public function testSuccess(): void @@ -75,7 +78,7 @@ public function testSkippedWithMessage(): void public function testRiskyWithMessage(): void { - $this->markAsRisky('risky with custom message'); + // Custom messages not implemented for risky status } public function testWarningWithMessage(): void diff --git a/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt b/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt index b16008a49bb..4af93ec882e 100644 --- a/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt +++ b/tests/end-to-end/loggers/_files/raw_output_StatusTest.txt @@ -1,44 +1,45 @@ PHPUnit %s Sebastian Bergmann and contributors. Runtime: %s +Configuration: %stests%ebasic%econfiguration.basic.xml -vendor\project\Status +Test result status with and without message ✔ Success  %f ms ✘ Failure  %f ms │ │ Failed asserting that false is true. │ - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:24 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ✘ Error  %f ms │ │ RuntimeException: │ - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:29 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ∅ Incomplete  %f ms │ - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:34 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ↩ Skipped  %f ms │ - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:39 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ☢ Risky  %f ms │ │ This test did not perform any assertions%w │%w - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:42%w + │ %stests%ebasic%eunit%eStatusTest.php:%d%w │%w │ ⚠ Warning  %f ms │ - │ %stests%eend-to-end%eloggers%e_files%eStatusTest.php:48 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ✔ Success with message  %f ms @@ -47,43 +48,43 @@ Runtime: %s │ failure with custom message%w │ Failed asserting that false is true. │ - │ %stests%ebasic%eunit%eStatusTest.php:58 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ✘ Error with message  %f ms │ │ RuntimeException: error with custom message │ - │ %stests%ebasic%eunit%eStatusTest.php:63 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ ∅ Incomplete with message  %f ms │ │ incomplete with custom message │ - │ %stests%ebasic%eunit%eStatusTest.php:68 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ - → Skipped with message  %f ms - │ - │ skipped with custom message - │ - │ %stests%ebasic%eunit%eStatusTest.php:73 - │ + ↩ Skipped with message  %f ms + │ + │ skipped with custom message + │ + │ %stests%ebasic%eunit%eStatusTest.php:%d + │ ☢ Risky with message  %f ms │ │ This test did not perform any assertions%w │%w - │ %stests%ebasic%eunit%eStatusTest.php:76%w + │ %stests%ebasic%eunit%eStatusTest.php:%d%w │%w │ - ✘ Warning with message  %f ms + ⚠ Warning with message  %f ms │ │ warning with custom message │ - │ %stests%ebasic%eunit%eStatusTest.php:83 + │ %stests%ebasic%eunit%eStatusTest.php:%d │ Time: %s, Memory: %s diff --git a/tests/end-to-end/loggers/debug.phpt b/tests/end-to-end/loggers/debug.phpt index 92c92ca9a0c..108fbec650f 100644 --- a/tests/end-to-end/loggers/debug.phpt +++ b/tests/end-to-end/loggers/debug.phpt @@ -2,27 +2,104 @@ phpunit --debug BankAccountTest ../../_files/BankAccountTest.php --FILE-- - - - - - - - - - - - - - - + + + + + + + + + + + + + + From 977ad4ca8930516f884f07172d9f43080527d3ec Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Mon, 7 Jan 2019 17:42:12 +0100 Subject: [PATCH 05/18] Implement code location hints for PHPT SKIPIF section --- phpunit.xml | 1 + src/Framework/SkippedPHPTError.php | 14 +++++++ src/Runner/PhptTestCase.php | 42 +++++++++++++++++-- .../phpt-skip-location-hint-example.phpt | 12 ++++++ tests/end-to-end/phpt/skip-location-hint.phpt | 31 ++++++++++++++ 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/Framework/SkippedPHPTError.php create mode 100644 tests/end-to-end/_files/phpt-skip-location-hint-example.phpt create mode 100644 tests/end-to-end/phpt/skip-location-hint.phpt diff --git a/phpunit.xml b/phpunit.xml index 92b3e855560..673e3c9396b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -12,6 +12,7 @@ tests/end-to-end + tests/end-to-end/_files diff --git a/src/Framework/SkippedPHPTError.php b/src/Framework/SkippedPHPTError.php new file mode 100644 index 00000000000..41163dbb978 --- /dev/null +++ b/src/Framework/SkippedPHPTError.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework; + +class SkippedPHPTError extends SyntheticError implements SkippedTest +{ +} diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index bbc1a239750..2d94dcd6c3c 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -13,7 +13,7 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\IncompleteTestError; use PHPUnit\Framework\SelfDescribing; -use PHPUnit\Framework\SkippedTestError; +use PHPUnit\Framework\SkippedPHPTError; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestResult; use PHPUnit\Util\PHP\AbstractPhpProcess; @@ -328,7 +328,14 @@ private function runSkip(array &$sections, TestResult $result, array $settings): $message = \substr($skipMatch[1], 2); } - $result->addFailure($this, new SkippedTestError($message), 0); + $trace = \debug_backtrace(); + \array_unshift($trace, [ + 'file' => "{$this->filename}", + 'line' => $this->findOffsetForLineInSection($message, 'SKIPIF', $sections), + 'function' => 'PHPT_SKIPIF', + 'args' => [], + ]); + $result->addFailure($this, new SkippedPHPTError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); $result->endTest($this, 0); return true; @@ -374,10 +381,15 @@ private function parse(): array 'PHPDBG', ]; + $lineNr = 0; + foreach (\file($this->filename) as $line) { + $lineNr++; + if (\preg_match('/^--([_A-Z]+)--/', $line, $result)) { - $section = $result[1]; - $sections[$section] = ''; + $section = $result[1]; + $sections[$section] = ''; + $sections[$section . '_offset'] = $lineNr; continue; } @@ -587,4 +599,26 @@ private function stringifyIni(array $ini): array return $settings; } + + private function findOffsetForLineInSection(string $needle, string $sectionName, array $sections): int + { + $needle = \trim($needle); + + if (empty($needle) || !isset($sections[$sectionName])) { + return 0; + } + + $sectionOffset = $sections[$sectionName . '_offset'] ?? 0; + $offset = $sectionOffset; + + foreach (\explode(\PHP_EOL, $sections[$sectionName]) as $line) { + if (\strpos($line, $needle) !== false) { + return $offset; + } + $offset++; + } + + // String not found, show user the start of the named section + return $sectionOffset; + } } diff --git a/tests/end-to-end/_files/phpt-skip-location-hint-example.phpt b/tests/end-to-end/_files/phpt-skip-location-hint-example.phpt new file mode 100644 index 00000000000..1437dcc6609 --- /dev/null +++ b/tests/end-to-end/_files/phpt-skip-location-hint-example.phpt @@ -0,0 +1,12 @@ +--TEST-- +PHPT skip condition results in correct code location hint +--FILE-- + +--SKIPIF-- + +--EXPECT-- +Nothing to see here, move along diff --git a/tests/end-to-end/phpt/skip-location-hint.phpt b/tests/end-to-end/phpt/skip-location-hint.phpt new file mode 100644 index 00000000000..64b01ff9e13 --- /dev/null +++ b/tests/end-to-end/phpt/skip-location-hint.phpt @@ -0,0 +1,31 @@ +--TEST-- +PHPT skip condition results in correct code location hint +--FILE-- + Date: Mon, 7 Jan 2019 23:24:03 +0100 Subject: [PATCH 06/18] Implement code location hints for PHPT EXPECT, EXPECTF and EXPECTREGEX --- src/Framework/PHPTAssertionFailedError.php | 14 +++ ...ppedPHPTError.php => PHPTSkippedError.php} | 2 +- src/Runner/PhptTestCase.php | 106 ++++++++++++++---- ...pt-file-section-location-hint-example.phpt | 10 ++ ...skipif-section-location-hint-example.phpt} | 0 .../phpt/file-section-location-hint.phpt | 31 +++++ ...phpt => skipif-section-location-hint.phpt} | 6 +- 7 files changed, 146 insertions(+), 23 deletions(-) create mode 100644 src/Framework/PHPTAssertionFailedError.php rename src/Framework/{SkippedPHPTError.php => PHPTSkippedError.php} (80%) create mode 100644 tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt rename tests/end-to-end/_files/{phpt-skip-location-hint-example.phpt => phpt-skipif-section-location-hint-example.phpt} (100%) create mode 100644 tests/end-to-end/phpt/file-section-location-hint.phpt rename tests/end-to-end/phpt/{skip-location-hint.phpt => skipif-section-location-hint.phpt} (71%) diff --git a/src/Framework/PHPTAssertionFailedError.php b/src/Framework/PHPTAssertionFailedError.php new file mode 100644 index 00000000000..e1cdfff97af --- /dev/null +++ b/src/Framework/PHPTAssertionFailedError.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework; + +class PHPTAssertionFailedError extends SyntheticError +{ +} diff --git a/src/Framework/SkippedPHPTError.php b/src/Framework/PHPTSkippedError.php similarity index 80% rename from src/Framework/SkippedPHPTError.php rename to src/Framework/PHPTSkippedError.php index 41163dbb978..efb9ddb6f69 100644 --- a/src/Framework/SkippedPHPTError.php +++ b/src/Framework/PHPTSkippedError.php @@ -9,6 +9,6 @@ */ namespace PHPUnit\Framework; -class SkippedPHPTError extends SyntheticError implements SkippedTest +class PHPTSkippedError extends SyntheticError implements SkippedTest { } diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 2d94dcd6c3c..249f233a7b8 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -11,9 +11,11 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\IncompleteTestError; +use PHPUnit\Framework\PHPTAssertionFailedError; +use PHPUnit\Framework\PHPTSkippedError; use PHPUnit\Framework\SelfDescribing; -use PHPUnit\Framework\SkippedPHPTError; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestResult; use PHPUnit\Util\PHP\AbstractPhpProcess; @@ -175,7 +177,18 @@ public function run(TestResult $result = null): TestResult if ($xfail !== false) { $failure = new IncompleteTestError($xfail, 0, $e); + } else { + if ($e instanceof ExpectationFailedException) { + /** @var ExpectationFailedException $e */ + if ($e->getComparisonFailure()) { + $hint = $this->getLocationHintFromDiff($e->getComparisonFailure()->getDiff(), $sections); + $trace = \debug_backtrace(); + \array_unshift($trace, $hint); + $failure = new PHPTAssertionFailedError($e->getMessage(), 0, $trace[0]['file'], $trace[0]['line'], $trace); + } + } } + $result->addFailure($this, $failure, $time); } catch (Throwable $t) { $result->addError($this, $t, $time); @@ -328,14 +341,10 @@ private function runSkip(array &$sections, TestResult $result, array $settings): $message = \substr($skipMatch[1], 2); } + $hint = $this->getLocationHint($message, $sections, 'SKIPIF'); $trace = \debug_backtrace(); - \array_unshift($trace, [ - 'file' => "{$this->filename}", - 'line' => $this->findOffsetForLineInSection($message, 'SKIPIF', $sections), - 'function' => 'PHPT_SKIPIF', - 'args' => [], - ]); - $result->addFailure($this, new SkippedPHPTError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); + \array_unshift($trace, $hint); + $result->addFailure($this, new PHPTSkippedError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); $result->endTest($this, 0); return true; @@ -600,25 +609,84 @@ private function stringifyIni(array $ini): array return $settings; } - private function findOffsetForLineInSection(string $needle, string $sectionName, array $sections): int + private function getLocationHintFromDiff(string $message, array $sections): array + { + $needle = ''; + + if (\preg_match("/--- Expected[^']+^-'([^']*)'/m", $message, $matches)) { + $needle = $matches[1]; + } + + return $this->getLocationHint($needle, $sections); + } + + private function getLocationHint(string $needle, array $sections, ?string $sectionName = null): array { $needle = \trim($needle); - if (empty($needle) || !isset($sections[$sectionName])) { - return 0; + if (empty($needle)) { + return [ + 'file' => $this->filename, + 'line' => 0, + 'external' => false, + ]; } - $sectionOffset = $sections[$sectionName . '_offset'] ?? 0; - $offset = $sectionOffset; + if ($sectionName) { + $search = [$sectionName]; + } else { + $search = [ + // 'FILE', + 'EXPECT', + 'EXPECTF', + 'EXPECTREGEX', + ]; + } - foreach (\explode(\PHP_EOL, $sections[$sectionName]) as $line) { - if (\strpos($line, $needle) !== false) { - return $offset; + foreach ($search as $section) { + if (!isset($sections[$section])) { + continue; + } + + if (isset($sections[$section . '_EXTERNAL'])) { + return [ + 'file' => $sections[$section], + 'line' => 0, + 'external' => true, + ]; } - $offset++; + + $sectionOffset = $sections[$section . '_offset'] ?? 0; + $offset = $sectionOffset + 1; + + $lines = \explode(\PHP_EOL, $sections[$section]); + + foreach ($lines as $line) { + if (\strpos($line, $needle) !== false) { + return [ + 'file' => $this->filename, + 'line' => $offset, + 'external' => false, + ]; + } + $offset++; + } + } + + if ($sectionName) { + // String not found in specified section, show user the start of the named section + return [ + 'file' => $this->filename, + 'line' => $sectionOffset, + 'external' => false, + ]; } - // String not found, show user the start of the named section - return $sectionOffset; + // No section specified, show user start of code + return [ + 'file' => $this->filename, + 'line' => 0, + 'external' => false, + ]; } } diff --git a/tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt b/tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt new file mode 100644 index 00000000000..87f49c75826 --- /dev/null +++ b/tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt @@ -0,0 +1,10 @@ +--TEST-- +PHPT skip condition results in correct code location hint +--FILE-- + +--EXPECT-- +Nothing to see here, move along diff --git a/tests/end-to-end/_files/phpt-skip-location-hint-example.phpt b/tests/end-to-end/_files/phpt-skipif-section-location-hint-example.phpt similarity index 100% rename from tests/end-to-end/_files/phpt-skip-location-hint-example.phpt rename to tests/end-to-end/_files/phpt-skipif-section-location-hint-example.phpt diff --git a/tests/end-to-end/phpt/file-section-location-hint.phpt b/tests/end-to-end/phpt/file-section-location-hint.phpt new file mode 100644 index 00000000000..caaa3c9aaa1 --- /dev/null +++ b/tests/end-to-end/phpt/file-section-location-hint.phpt @@ -0,0 +1,31 @@ +--TEST-- +PHPT skip condition results in correct code location hint +--FILE-- + Date: Tue, 8 Jan 2019 00:31:00 +0100 Subject: [PATCH 07/18] Implement code location hints for PHPT *_EXTERNAL sections --- src/Runner/PhptTestCase.php | 35 ++++++++----------- tests/end-to-end/_files/expect_external.txt | 4 ++- ...expect-external-location-hint-example.phpt | 10 ++++++ ...=> phpt-expect-location-hint-example.phpt} | 0 ...=> phpt-skipif-location-hint-example.phpt} | 0 tests/end-to-end/_files/phpt_external.php | 4 ++- .../phpt/expect-external-location-hint.phpt | 31 ++++++++++++++++ ...on-hint.phpt => expect-location-hint.phpt} | 6 ++-- ...on-hint.phpt => skipif-location-hint.phpt} | 6 ++-- 9 files changed, 67 insertions(+), 29 deletions(-) create mode 100644 tests/end-to-end/_files/phpt-expect-external-location-hint-example.phpt rename tests/end-to-end/_files/{phpt-file-section-location-hint-example.phpt => phpt-expect-location-hint-example.phpt} (100%) rename tests/end-to-end/_files/{phpt-skipif-section-location-hint-example.phpt => phpt-skipif-location-hint-example.phpt} (100%) create mode 100644 tests/end-to-end/phpt/expect-external-location-hint.phpt rename tests/end-to-end/phpt/{file-section-location-hint.phpt => expect-location-hint.phpt} (69%) rename tests/end-to-end/phpt/{skipif-section-location-hint.phpt => skipif-location-hint.phpt} (71%) diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 249f233a7b8..90bcdc26e20 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -178,14 +178,12 @@ public function run(TestResult $result = null): TestResult if ($xfail !== false) { $failure = new IncompleteTestError($xfail, 0, $e); } else { - if ($e instanceof ExpectationFailedException) { + if ($e instanceof ExpectationFailedException && $e->getComparisonFailure()) { /** @var ExpectationFailedException $e */ - if ($e->getComparisonFailure()) { - $hint = $this->getLocationHintFromDiff($e->getComparisonFailure()->getDiff(), $sections); - $trace = \debug_backtrace(); - \array_unshift($trace, $hint); - $failure = new PHPTAssertionFailedError($e->getMessage(), 0, $trace[0]['file'], $trace[0]['line'], $trace); - } + $hint = $this->getLocationHintFromDiff($e->getComparisonFailure()->getDiff(), $sections); + $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + \array_unshift($trace, $hint); + $failure = new PHPTAssertionFailedError($e->getMessage(), 0, $trace[0]['file'], $trace[0]['line'], $trace); } } @@ -342,7 +340,7 @@ private function runSkip(array &$sections, TestResult $result, array $settings): } $hint = $this->getLocationHint($message, $sections, 'SKIPIF'); - $trace = \debug_backtrace(); + $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); \array_unshift($trace, $hint); $result->addFailure($this, new PHPTSkippedError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); $result->endTest($this, 0); @@ -461,8 +459,6 @@ private function parseExternal(array &$sections): void } $sections[$section] = \file_get_contents($testDirectory . $externalFilename); - - unset($sections[$section . '_EXTERNAL']); } } } @@ -626,9 +622,8 @@ private function getLocationHint(string $needle, array $sections, ?string $secti if (empty($needle)) { return [ - 'file' => $this->filename, + 'file' => \realpath($this->filename), 'line' => 0, - 'external' => false, ]; } @@ -649,10 +644,11 @@ private function getLocationHint(string $needle, array $sections, ?string $secti } if (isset($sections[$section . '_EXTERNAL'])) { + $file = \trim($sections[$section . '_EXTERNAL']); + return [ - 'file' => $sections[$section], - 'line' => 0, - 'external' => true, + 'file' => \realpath(\dirname($this->filename) . \DIRECTORY_SEPARATOR . $file), + 'line' => 1, ]; } @@ -664,9 +660,8 @@ private function getLocationHint(string $needle, array $sections, ?string $secti foreach ($lines as $line) { if (\strpos($line, $needle) !== false) { return [ - 'file' => $this->filename, + 'file' => \realpath($this->filename), 'line' => $offset, - 'external' => false, ]; } $offset++; @@ -676,17 +671,15 @@ private function getLocationHint(string $needle, array $sections, ?string $secti if ($sectionName) { // String not found in specified section, show user the start of the named section return [ - 'file' => $this->filename, + 'file' => \realpath($this->filename), 'line' => $sectionOffset, - 'external' => false, ]; } // No section specified, show user start of code return [ - 'file' => $this->filename, + 'file' => \realpath($this->filename), 'line' => 0, - 'external' => false, ]; } } diff --git a/tests/end-to-end/_files/expect_external.txt b/tests/end-to-end/_files/expect_external.txt index 5e1c309dae7..4e8779e82dc 100644 --- a/tests/end-to-end/_files/expect_external.txt +++ b/tests/end-to-end/_files/expect_external.txt @@ -1 +1,3 @@ -Hello World \ No newline at end of file +Hello World +This is line two +and this is line three diff --git a/tests/end-to-end/_files/phpt-expect-external-location-hint-example.phpt b/tests/end-to-end/_files/phpt-expect-external-location-hint-example.phpt new file mode 100644 index 00000000000..8475cb4de14 --- /dev/null +++ b/tests/end-to-end/_files/phpt-expect-external-location-hint-example.phpt @@ -0,0 +1,10 @@ +--TEST-- +PHPT skip condition results in correct code location hint +--FILE-- + +--EXPECT_EXTERNAL-- +../_files/expect_external.txt diff --git a/tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt b/tests/end-to-end/_files/phpt-expect-location-hint-example.phpt similarity index 100% rename from tests/end-to-end/_files/phpt-file-section-location-hint-example.phpt rename to tests/end-to-end/_files/phpt-expect-location-hint-example.phpt diff --git a/tests/end-to-end/_files/phpt-skipif-section-location-hint-example.phpt b/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt similarity index 100% rename from tests/end-to-end/_files/phpt-skipif-section-location-hint-example.phpt rename to tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt diff --git a/tests/end-to-end/_files/phpt_external.php b/tests/end-to-end/_files/phpt_external.php index f0bab63409f..3219572a232 100644 --- a/tests/end-to-end/_files/phpt_external.php +++ b/tests/end-to-end/_files/phpt_external.php @@ -7,4 +7,6 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -print 'Hello World'; +print 'Hello World' . \PHP_EOL; +print 'This is line two' . \PHP_EOL; +print 'and this is line three' . \PHP_EOL; diff --git a/tests/end-to-end/phpt/expect-external-location-hint.phpt b/tests/end-to-end/phpt/expect-external-location-hint.phpt new file mode 100644 index 00000000000..f986d2bad04 --- /dev/null +++ b/tests/end-to-end/phpt/expect-external-location-hint.phpt @@ -0,0 +1,31 @@ +--TEST-- +PHPT EXPECT_EXTERNAL results in correct code location hint +--FILE-- + Date: Tue, 8 Jan 2019 22:39:28 +0100 Subject: [PATCH 08/18] Housekeeping --- ...ppedError.php => SyntheticSkippedError.php} | 2 +- src/Runner/PhptTestCase.php | 4 ++-- tests/_files/RequirementsTest.php | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) rename src/Framework/{PHPTSkippedError.php => SyntheticSkippedError.php} (77%) diff --git a/src/Framework/PHPTSkippedError.php b/src/Framework/SyntheticSkippedError.php similarity index 77% rename from src/Framework/PHPTSkippedError.php rename to src/Framework/SyntheticSkippedError.php index efb9ddb6f69..186ca0ccd8b 100644 --- a/src/Framework/PHPTSkippedError.php +++ b/src/Framework/SyntheticSkippedError.php @@ -9,6 +9,6 @@ */ namespace PHPUnit\Framework; -class PHPTSkippedError extends SyntheticError implements SkippedTest +class SyntheticSkippedError extends SyntheticError implements SkippedTest { } diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 90bcdc26e20..510aba0603a 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -14,8 +14,8 @@ use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\IncompleteTestError; use PHPUnit\Framework\PHPTAssertionFailedError; -use PHPUnit\Framework\PHPTSkippedError; use PHPUnit\Framework\SelfDescribing; +use PHPUnit\Framework\SyntheticSkippedError; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestResult; use PHPUnit\Util\PHP\AbstractPhpProcess; @@ -342,7 +342,7 @@ private function runSkip(array &$sections, TestResult $result, array $settings): $hint = $this->getLocationHint($message, $sections, 'SKIPIF'); $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); \array_unshift($trace, $hint); - $result->addFailure($this, new PHPTSkippedError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); + $result->addFailure($this, new SyntheticSkippedError($message, 0, $trace[0]['file'], $trace[0]['line'], $trace), 0); $result->endTest($this, 0); return true; diff --git a/tests/_files/RequirementsTest.php b/tests/_files/RequirementsTest.php index cdff0db56f4..d3615595c24 100644 --- a/tests/_files/RequirementsTest.php +++ b/tests/_files/RequirementsTest.php @@ -183,6 +183,7 @@ public function testSpecificExtensionVersion(): void } /** + * @testdox PHP version operator less than * @requires PHP < 5.4 */ public function testPHPVersionOperatorLessThan(): void @@ -190,6 +191,7 @@ public function testPHPVersionOperatorLessThan(): void } /** + * @testdox PHP version operator less than or equals * @requires PHP <= 5.4 */ public function testPHPVersionOperatorLessThanEquals(): void @@ -197,6 +199,7 @@ public function testPHPVersionOperatorLessThanEquals(): void } /** + * @testdox PHP version operator greater than * @requires PHP > 99 */ public function testPHPVersionOperatorGreaterThan(): void @@ -204,6 +207,7 @@ public function testPHPVersionOperatorGreaterThan(): void } /** + * @testdox PHP version operator greater than or equals * @requires PHP >= 99 */ public function testPHPVersionOperatorGreaterThanEquals(): void @@ -211,6 +215,7 @@ public function testPHPVersionOperatorGreaterThanEquals(): void } /** + * @testdox PHP version operator equals * @requires PHP = 5.4 */ public function testPHPVersionOperatorEquals(): void @@ -218,6 +223,7 @@ public function testPHPVersionOperatorEquals(): void } /** + * @testdox PHP version operator double equals * @requires PHP == 5.4 */ public function testPHPVersionOperatorDoubleEquals(): void @@ -225,6 +231,7 @@ public function testPHPVersionOperatorDoubleEquals(): void } /** + * @testdox PHP version operator bang equals * @requires PHP != 99 */ public function testPHPVersionOperatorBangEquals(): void @@ -232,6 +239,7 @@ public function testPHPVersionOperatorBangEquals(): void } /** + * @testdox PHP version operator not equals * @requires PHP <> 99 */ public function testPHPVersionOperatorNotEquals(): void @@ -239,6 +247,7 @@ public function testPHPVersionOperatorNotEquals(): void } /** + * @testdox PHP version operator no space * @requires PHP >=99 */ public function testPHPVersionOperatorNoSpace(): void @@ -246,6 +255,7 @@ public function testPHPVersionOperatorNoSpace(): void } /** + * @testdox PHPUnit version operator less than * @requires PHPUnit < 1.0 */ public function testPHPUnitVersionOperatorLessThan(): void @@ -253,6 +263,7 @@ public function testPHPUnitVersionOperatorLessThan(): void } /** + * @testdox PHPUnit version operator less than equals * @requires PHPUnit <= 1.0 */ public function testPHPUnitVersionOperatorLessThanEquals(): void @@ -260,6 +271,7 @@ public function testPHPUnitVersionOperatorLessThanEquals(): void } /** + * @testdox PHPUnit version operator greater than * @requires PHPUnit > 99 */ public function testPHPUnitVersionOperatorGreaterThan(): void @@ -267,6 +279,7 @@ public function testPHPUnitVersionOperatorGreaterThan(): void } /** + * @testdox PHPUnit version operator greater than or equals * @requires PHPUnit >= 99 */ public function testPHPUnitVersionOperatorGreaterThanEquals(): void @@ -274,6 +287,7 @@ public function testPHPUnitVersionOperatorGreaterThanEquals(): void } /** + * @testdox PHPUnit version operator equals * @requires PHPUnit = 1.0 */ public function testPHPUnitVersionOperatorEquals(): void @@ -281,6 +295,7 @@ public function testPHPUnitVersionOperatorEquals(): void } /** + * @testdox PHPUnit version operator double equals * @requires PHPUnit == 1.0 */ public function testPHPUnitVersionOperatorDoubleEquals(): void @@ -288,6 +303,7 @@ public function testPHPUnitVersionOperatorDoubleEquals(): void } /** + * @testdox PHPUnit version operator bang equals * @requires PHPUnit != 99 */ public function testPHPUnitVersionOperatorBangEquals(): void @@ -295,6 +311,7 @@ public function testPHPUnitVersionOperatorBangEquals(): void } /** + * @testdox PHPUnit version operator not equals * @requires PHPUnit <> 99 */ public function testPHPUnitVersionOperatorNotEquals(): void @@ -302,6 +319,7 @@ public function testPHPUnitVersionOperatorNotEquals(): void } /** + * @testdox PHPUnit version operator no space * @requires PHPUnit >=99 */ public function testPHPUnitVersionOperatorNoSpace(): void From 664ac6771dc10f85e6bdb0e6e72a28c6d8cb5daa Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Tue, 8 Jan 2019 22:41:28 +0100 Subject: [PATCH 09/18] Implement code location hints for skipping with @requires annotation --- src/Framework/TestCase.php | 27 +++++++++- src/Util/Test.php | 107 +++++++++++++++++++++++-------------- 2 files changed, 93 insertions(+), 41 deletions(-) diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index f70f658a354..3e8c4d29b69 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -1644,10 +1644,35 @@ private function checkRequirements(): void ); if (!empty($missingRequirements)) { - $this->markTestSkipped(\implode(\PHP_EOL, $missingRequirements)); + $this->markTestSkippedWithLocationHint($missingRequirements); } } + private function markTestSkippedWithLocationHint(array $required): void + { + $file = null; + $line = null; + + while (\strpos($required[0], '__OFFSET') !== false) { + $offset = \explode('=', \array_shift($required)); + + if ($offset[0] === '__OFFSET_FILE') { + $file = $offset[1]; + } + + if ($offset[0] === '__OFFSET_LINE') { + $line = $offset[1]; + } + } + + if ($file && $line) { + $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + + throw new SyntheticSkippedError(\implode(\PHP_EOL, $required), 0, $file, $line, $trace); + } + $this->markTestSkipped(\implode(\PHP_EOL, $required)); + } + private function verifyMockObjects(): void { foreach ($this->mockObjects as $mockObject) { diff --git a/src/Util/Test.php b/src/Util/Test.php index 7ff953629ec..e212f478719 100644 --- a/src/Util/Test.php +++ b/src/Util/Test.php @@ -160,71 +160,82 @@ public static function getLinesToBeUsed(string $className, string $methodName): public static function getRequirements(string $className, string $methodName): array { $reflector = new ReflectionClass($className); - $docComment = $reflector->getDocComment(); + $requires = [ + '__OFFSET' => [ + '__FILE' => \realpath($reflector->getFileName()), + ], + ]; + $requires = self::parseRequirements($reflector->getDocComment(), $reflector->getStartLine(), $requires); + $reflector = new ReflectionMethod($className, $methodName); - $docComment .= "\n" . $reflector->getDocComment(); - $requires = []; - if ($count = \preg_match_all(self::REGEX_REQUIRES_OS, $docComment, $matches)) { - foreach (\range(0, $count - 1) as $i) { - $requires[$matches['name'][$i]] = $matches['value'][$i]; + return self::parseRequirements($reflector->getDocComment(), $reflector->getStartLine(), $requires); + } + + public static function parseRequirements(string $docComment, int $offset = 0, array $requires = []): array + { + // Split docblock into lines and rewind offset to start of docblock + $lines = \preg_split('/\r\n|\r|\n/', $docComment); + $offset -= \count($lines); + + foreach ($lines as $line) { + if (\preg_match(self::REGEX_REQUIRES_OS, $line, $matches)) { + $requires[$matches['name']] = $matches['value']; + $requires['__OFFSET'][$matches['name']] = $offset; } - } - if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION, $docComment, $matches)) { - foreach (\range(0, $count - 1) as $i) { - $requires[$matches['name'][$i]] = [ - 'version' => $matches['version'][$i], - 'operator' => $matches['operator'][$i], + if (\preg_match(self::REGEX_REQUIRES_VERSION, $line, $matches)) { + $requires[$matches['name']] = [ + 'version' => $matches['version'], + 'operator' => $matches['operator'], ]; + $requires['__OFFSET'][$matches['name']] = $offset; } - } - if ($count = \preg_match_all(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $docComment, $matches)) { - foreach (\range(0, $count - 1) as $i) { - if (!empty($requires[$matches['name'][$i]])) { + if (\preg_match(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $line, $matches)) { + if (!empty($requires[$matches['name']])) { continue; } try { $versionConstraintParser = new VersionConstraintParser; - $requires[$matches['name'][$i] . '_constraint'] = [ - 'constraint' => $versionConstraintParser->parse(\trim($matches['constraint'][$i])), + $requires[$matches['name'] . '_constraint'] = [ + 'constraint' => $versionConstraintParser->parse(\trim($matches['constraint'])), ]; + $requires['__OFFSET'][$matches['name'] . '_constraint'] = $offset; } catch (\PharIo\Version\Exception $e) { throw new Warning($e->getMessage(), $e->getCode(), $e); } } - } - - if ($count = \preg_match_all(self::REGEX_REQUIRES_SETTING, $docComment, $matches)) { - $requires['setting'] = []; - foreach (\range(0, $count - 1) as $i) { - $requires['setting'][$matches['setting'][$i]] = $matches['value'][$i]; + if (\preg_match(self::REGEX_REQUIRES_SETTING, $line, $matches)) { + if (!isset($requires['setting'])) { + $requires['setting'] = []; + } + $requires['setting'][$matches['setting']] = $matches['value']; + $requires['__OFFSET']['__SETTING_' . $matches['setting']] = $offset; } - } - if ($count = \preg_match_all(self::REGEX_REQUIRES, $docComment, $matches)) { - foreach (\range(0, $count - 1) as $i) { - $name = $matches['name'][$i] . 's'; + if (\preg_match(self::REGEX_REQUIRES, $line, $matches)) { + $name = $matches['name'] . 's'; if (!isset($requires[$name])) { $requires[$name] = []; } - $requires[$name][] = $matches['value'][$i]; + $requires[$name][] = $matches['value']; + $requires['__OFFSET'][$matches['name'] . '_' . $matches['value']] = $offset; - if ($name !== 'extensions' || empty($matches['version'][$i])) { - continue; + if ($name === 'extensions' && !empty($matches['version'])) { + $requires['extension_versions'][$matches['value']] = [ + 'version' => $matches['version'], + 'operator' => $matches['operator'], + ]; } - - $requires['extension_versions'][$matches['value'][$i]] = [ - 'version' => $matches['version'][$i], - 'operator' => $matches['operator'][$i], - ]; } + + $offset++; } return $requires; @@ -241,12 +252,14 @@ public static function getMissingRequirements(string $className, string $methodN { $required = static::getRequirements($className, $methodName); $missing = []; + $hint = null; if (!empty($required['PHP'])) { $operator = empty($required['PHP']['operator']) ? '>=' : $required['PHP']['operator']; if (!\version_compare(\PHP_VERSION, $required['PHP']['version'], $operator)) { $missing[] = \sprintf('PHP %s %s is required.', $operator, $required['PHP']['version']); + $hint = $hint ?? 'PHP'; } } elseif (!empty($required['PHP_constraint'])) { $version = new \PharIo\Version\Version(self::sanitizeVersionNumber(\PHP_VERSION)); @@ -256,6 +269,7 @@ public static function getMissingRequirements(string $className, string $methodN 'PHP version does not match the required constraint %s.', $required['PHP_constraint']['constraint']->asString() ); + $hint = $hint ?? 'PHP_constraint'; } } @@ -266,6 +280,7 @@ public static function getMissingRequirements(string $className, string $methodN if (!\version_compare($phpunitVersion, $required['PHPUnit']['version'], $operator)) { $missing[] = \sprintf('PHPUnit %s %s is required.', $operator, $required['PHPUnit']['version']); + $hint = $hint ?? 'PHPUnit'; } } elseif (!empty($required['PHPUnit_constraint'])) { $phpunitVersion = new \PharIo\Version\Version(self::sanitizeVersionNumber(Version::id())); @@ -275,11 +290,13 @@ public static function getMissingRequirements(string $className, string $methodN 'PHPUnit version does not match the required constraint %s.', $required['PHPUnit_constraint']['constraint']->asString() ); + $hint = $hint ?? 'PHPUnit_constraint'; } } if (!empty($required['OSFAMILY']) && $required['OSFAMILY'] !== (new OperatingSystem)->getFamily()) { $missing[] = \sprintf('Operating system %s is required.', $required['OSFAMILY']); + $hint = $hint ?? 'OSFAMILY'; } if (!empty($required['OS'])) { @@ -287,6 +304,7 @@ public static function getMissingRequirements(string $className, string $methodN if (!\preg_match($requiredOsPattern, \PHP_OS)) { $missing[] = \sprintf('Operating system matching %s is required.', $requiredOsPattern); + $hint = $hint ?? 'OS'; } } @@ -303,6 +321,7 @@ public static function getMissingRequirements(string $className, string $methodN } $missing[] = \sprintf('Function %s is required.', $function); + $hint = $hint ?? 'function_' . $function; } } @@ -310,6 +329,7 @@ public static function getMissingRequirements(string $className, string $methodN foreach ($required['setting'] as $setting => $value) { if (\ini_get($setting) != $value) { $missing[] = \sprintf('Setting "%s" must be "%s".', $setting, $value); + $hint = $hint ?? '__SETTING_' . $setting; } } } @@ -322,22 +342,29 @@ public static function getMissingRequirements(string $className, string $methodN if (!\extension_loaded($extension)) { $missing[] = \sprintf('Extension %s is required.', $extension); + $hint = $hint ?? 'extension_' . $extension; } } } if (!empty($required['extension_versions'])) { - foreach ($required['extension_versions'] as $extension => $required) { + foreach ($required['extension_versions'] as $extension => $req) { $actualVersion = \phpversion($extension); - $operator = empty($required['operator']) ? '>=' : $required['operator']; + $operator = empty($req['operator']) ? '>=' : $req['operator']; - if ($actualVersion === false || !\version_compare($actualVersion, $required['version'], $operator)) { - $missing[] = \sprintf('Extension %s %s %s is required.', $extension, $operator, $required['version']); + if ($actualVersion === false || !\version_compare($actualVersion, $req['version'], $operator)) { + $missing[] = \sprintf('Extension %s %s %s is required.', $extension, $operator, $req['version']); + $hint = $hint ?? 'extension_' . $extension; } } } + if ($hint && isset($required['__OFFSET'])) { + \array_unshift($missing, '__OFFSET_FILE=' . $required['__OFFSET']['__FILE']); + \array_unshift($missing, '__OFFSET_LINE=' . $required['__OFFSET'][$hint] ?? 1); + } + return $missing; } From 55dc77df00cb8cfe1d8857c29e53f1d1bde36722 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 11:56:34 +0100 Subject: [PATCH 10/18] Add unit test coverage for @requires-annotation location hints --- src/Util/Test.php | 2 + tests/unit/Util/TestTest.php | 458 +++++++++++++++++++++++++++++++---- 2 files changed, 417 insertions(+), 43 deletions(-) diff --git a/src/Util/Test.php b/src/Util/Test.php index e212f478719..c233af86604 100644 --- a/src/Util/Test.php +++ b/src/Util/Test.php @@ -194,6 +194,8 @@ public static function parseRequirements(string $docComment, int $offset = 0, ar if (\preg_match(self::REGEX_REQUIRES_VERSION_CONSTRAINT, $line, $matches)) { if (!empty($requires[$matches['name']])) { + $offset++; + continue; } diff --git a/tests/unit/Util/TestTest.php b/tests/unit/Util/TestTest.php index 40f220b1d0f..fd7ce355825 100644 --- a/tests/unit/Util/TestTest.php +++ b/tests/unit/Util/TestTest.php @@ -19,6 +19,12 @@ class TestTest extends TestCase { /** + * @var string + */ + private $fileRequirementsTest; + + /** + * @testdox Test::getRequirements() for $test * @dataProvider requirementsProvider * * @throws Warning @@ -36,33 +42,155 @@ public function testGetRequirements($test, $result): void public function requirementsProvider(): array { return [ - ['testOne', []], - ['testTwo', ['PHPUnit' => ['version' => '1.0', 'operator' => '']]], - ['testThree', ['PHP' => ['version' => '2.0', 'operator' => '']]], - ['testFour', [ - 'PHPUnit' => ['version' => '2.0', 'operator' => ''], - 'PHP' => ['version' => '1.0', 'operator' => ''], - ]], - ['testFive', ['PHP' => ['version' => '5.4.0RC6', 'operator' => '']]], - ['testSix', ['PHP' => ['version' => '5.4.0-alpha1', 'operator' => '']]], - ['testSeven', ['PHP' => ['version' => '5.4.0beta2', 'operator' => '']]], - ['testEight', ['PHP' => ['version' => '5.4-dev', 'operator' => '']]], - ['testNine', ['functions' => ['testFunc']]], - ['testTen', ['extensions' => ['testExt']]], - ['testEleven', [ - 'OS' => 'SunOS', - 'OSFAMILY' => 'Solaris', - ]], + [ + 'testOne', + ['__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + ]], + ], + + [ + 'testTwo', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 19, + ], + 'PHPUnit' => ['version' => '1.0', 'operator' => ''], + ], + ], + + [ + 'testThree', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 26, + ], + 'PHP' => ['version' => '2.0', 'operator' => ''], + ], + ], + + [ + 'testFour', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 33, + 'PHP' => 34, + ], + 'PHPUnit' => ['version' => '2.0', 'operator' => ''], + 'PHP' => ['version' => '1.0', 'operator' => ''], + ], + ], + + [ + 'testFive', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 41, + ], + 'PHP' => ['version' => '5.4.0RC6', 'operator' => ''], + ], + ], + + [ + 'testSix', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 48, + ], + 'PHP' => ['version' => '5.4.0-alpha1', 'operator' => ''], + ], + ], + + [ + 'testSeven', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 55, + ], + 'PHP' => ['version' => '5.4.0beta2', 'operator' => ''], + ], + ], + + [ + 'testEight', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 62, + ], + 'PHP' => ['version' => '5.4-dev', 'operator' => ''], + ], + ], + + [ + 'testNine', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'function_testFunc' => 69, + ], + 'functions' => ['testFunc'], + ], + ], + + [ + 'testTen', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExt' => 85, + ], + 'extensions' => ['testExt'], + ], + ], + + [ + 'testEleven', + [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'OS' => 92, + 'OSFAMILY' => 93, + ], + 'OS' => 'SunOS', + 'OSFAMILY' => 'Solaris', + ], + ], + [ 'testSpace', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_spl' => 171, + 'OS' => 172, + ], 'extensions' => ['spl'], 'OS' => '.*', ], ], + [ 'testAllPossibleRequirements', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 100, + 'PHPUnit' => 101, + 'OS' => 102, + 'function_testFuncOne' => 103, + 'function_testFunc2' => 104, + 'extension_testExtOne' => 105, + 'extension_testExt2' => 106, + 'extension_testExtThree' => 107, + '__SETTING_not_a_setting' => 108, + ], 'PHP' => ['version' => '99-dev', 'operator' => ''], 'PHPUnit' => ['version' => '9-dev', 'operator' => ''], 'OS' => 'DOESNOTEXIST', @@ -83,146 +211,255 @@ public function requirementsProvider(): array ], ], ], + ['testSpecificExtensionVersion', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExt' => 179, + ], 'extension_versions' => ['testExt' => ['version' => '1.8.0', 'operator' => '']], 'extensions' => ['testExt'], ], ], ['testPHPVersionOperatorLessThan', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 187, + ], 'PHP' => ['version' => '5.4', 'operator' => '<'], ], ], ['testPHPVersionOperatorLessThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 195, + ], 'PHP' => ['version' => '5.4', 'operator' => '<='], ], ], ['testPHPVersionOperatorGreaterThan', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 203, + ], 'PHP' => ['version' => '99', 'operator' => '>'], ], ], ['testPHPVersionOperatorGreaterThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 211, + ], 'PHP' => ['version' => '99', 'operator' => '>='], ], ], ['testPHPVersionOperatorEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 219, + ], 'PHP' => ['version' => '5.4', 'operator' => '='], ], ], ['testPHPVersionOperatorDoubleEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 227, + ], 'PHP' => ['version' => '5.4', 'operator' => '=='], ], ], ['testPHPVersionOperatorBangEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 235, + ], 'PHP' => ['version' => '99', 'operator' => '!='], ], ], ['testPHPVersionOperatorNotEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 243, + ], 'PHP' => ['version' => '99', 'operator' => '<>'], ], ], ['testPHPVersionOperatorNoSpace', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHP' => 251, + ], 'PHP' => ['version' => '99', 'operator' => '>='], ], ], ['testPHPUnitVersionOperatorLessThan', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 259, + ], 'PHPUnit' => ['version' => '1.0', 'operator' => '<'], ], ], ['testPHPUnitVersionOperatorLessThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 267, + ], 'PHPUnit' => ['version' => '1.0', 'operator' => '<='], ], ], ['testPHPUnitVersionOperatorGreaterThan', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 275, + ], 'PHPUnit' => ['version' => '99', 'operator' => '>'], ], ], ['testPHPUnitVersionOperatorGreaterThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 283, + ], 'PHPUnit' => ['version' => '99', 'operator' => '>='], ], ], ['testPHPUnitVersionOperatorEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 291, + ], 'PHPUnit' => ['version' => '1.0', 'operator' => '='], ], ], ['testPHPUnitVersionOperatorDoubleEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 299, + ], 'PHPUnit' => ['version' => '1.0', 'operator' => '=='], ], ], ['testPHPUnitVersionOperatorBangEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 307, + ], 'PHPUnit' => ['version' => '99', 'operator' => '!='], ], ], ['testPHPUnitVersionOperatorNotEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 315, + ], 'PHPUnit' => ['version' => '99', 'operator' => '<>'], ], ], ['testPHPUnitVersionOperatorNoSpace', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'PHPUnit' => 323, + ], 'PHPUnit' => ['version' => '99', 'operator' => '>='], ], ], ['testExtensionVersionOperatorLessThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 337, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '1.0', 'operator' => '<=']], ], ], ['testExtensionVersionOperatorGreaterThan', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 344, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '99', 'operator' => '>']], ], ], ['testExtensionVersionOperatorGreaterThanEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 351, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '99', 'operator' => '>=']], ], ], ['testExtensionVersionOperatorEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 358, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '1.0', 'operator' => '=']], ], ], ['testExtensionVersionOperatorDoubleEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 365, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '1.0', 'operator' => '==']], ], ], ['testExtensionVersionOperatorBangEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 372, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '99', 'operator' => '!=']], ], ], ['testExtensionVersionOperatorNotEquals', [ + '__OFFSET' => [ + '__FILE' => $this->getRequirementsTestClassFile(), + 'extension_testExtOne' => 379, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '99', 'operator' => '<>']], ], ], ['testExtensionVersionOperatorNoSpace', [ + '__OFFSET' => [ + '__FILE' => $this->fileRequirementsTest, + 'extension_testExtOne' => 386, + ], 'extensions' => ['testExtOne'], 'extension_versions' => ['testExtOne' => ['version' => '99', 'operator' => '>=']], ], @@ -231,6 +468,7 @@ public function requirementsProvider(): array } /** + * @testdox Test::getRequirements() with constraints for $test * @dataProvider requirementsWithVersionConstraintsProvider * * @throws Exception @@ -388,7 +626,20 @@ public function requirementsWithInvalidVersionConstraintsThrowsExceptionProvider public function testGetRequirementsMergesClassAndMethodDocBlocks(): void { + $reflector = new \ReflectionClass(\RequirementsClassDocBlockTest::class); + $file = $reflector->getFileName(); + $expectedAnnotations = [ + '__OFFSET' => [ + '__FILE' => $file, + 'PHP' => 21, + 'PHPUnit' => 22, + 'OS' => 23, + 'function_testFuncClass' => 15, + 'extension_testExtClass' => 16, + 'function_testFuncMethod' => 24, + 'extension_testExtMethod' => 25, + ], 'PHP' => ['version' => '5.4', 'operator' => ''], 'PHPUnit' => ['version' => '3.7', 'operator' => ''], 'OS' => 'WINNT', @@ -409,6 +660,7 @@ public function testGetRequirementsMergesClassAndMethodDocBlocks(): void } /** + * @testdox Test::getMissingRequirements() for $test * @dataProvider missingRequirementsProvider * * @throws Warning @@ -427,12 +679,34 @@ public function missingRequirementsProvider(): array { return [ ['testOne', []], - ['testNine', ['Function testFunc is required.']], - ['testTen', ['Extension testExt is required.']], - ['testAlwaysSkip', ['PHPUnit >= 1111111 is required.']], - ['testAlwaysSkip2', ['PHP >= 9999999 is required.']], - ['testAlwaysSkip3', ['Operating system matching /DOESNOTEXIST/i is required.']], + ['testNine', [ + '__OFFSET_LINE=69', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Function testFunc is required.', + ]], + ['testTen', [ + '__OFFSET_LINE=85', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExt is required.', + ]], + ['testAlwaysSkip', [ + '__OFFSET_LINE=143', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit >= 1111111 is required.', + ]], + ['testAlwaysSkip2', [ + '__OFFSET_LINE=150', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP >= 9999999 is required.', + ]], + ['testAlwaysSkip3', [ + '__OFFSET_LINE=157', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Operating system matching /DOESNOTEXIST/i is required.', + ]], ['testAllPossibleRequirements', [ + '__OFFSET_LINE=100', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), 'PHP >= 99-dev is required.', 'PHPUnit >= 9-dev is required.', 'Operating system matching /DOESNOTEXIST/i is required.', @@ -443,32 +717,120 @@ public function missingRequirementsProvider(): array 'Extension testExt2 is required.', 'Extension testExtThree >= 2.0 is required.', ]], - ['testPHPVersionOperatorLessThan', ['PHP < 5.4 is required.']], - ['testPHPVersionOperatorLessThanEquals', ['PHP <= 5.4 is required.']], - ['testPHPVersionOperatorGreaterThan', ['PHP > 99 is required.']], - ['testPHPVersionOperatorGreaterThanEquals', ['PHP >= 99 is required.']], - ['testPHPVersionOperatorNoSpace', ['PHP >= 99 is required.']], - ['testPHPVersionOperatorEquals', ['PHP = 5.4 is required.']], - ['testPHPVersionOperatorDoubleEquals', ['PHP == 5.4 is required.']], - ['testPHPUnitVersionOperatorLessThan', ['PHPUnit < 1.0 is required.']], - ['testPHPUnitVersionOperatorLessThanEquals', ['PHPUnit <= 1.0 is required.']], - ['testPHPUnitVersionOperatorGreaterThan', ['PHPUnit > 99 is required.']], - ['testPHPUnitVersionOperatorGreaterThanEquals', ['PHPUnit >= 99 is required.']], - ['testPHPUnitVersionOperatorEquals', ['PHPUnit = 1.0 is required.']], - ['testPHPUnitVersionOperatorDoubleEquals', ['PHPUnit == 1.0 is required.']], - ['testPHPUnitVersionOperatorNoSpace', ['PHPUnit >= 99 is required.']], - ['testExtensionVersionOperatorLessThan', ['Extension testExtOne < 1.0 is required.']], - ['testExtensionVersionOperatorLessThanEquals', ['Extension testExtOne <= 1.0 is required.']], - ['testExtensionVersionOperatorGreaterThan', ['Extension testExtOne > 99 is required.']], - ['testExtensionVersionOperatorGreaterThanEquals', ['Extension testExtOne >= 99 is required.']], - ['testExtensionVersionOperatorEquals', ['Extension testExtOne = 1.0 is required.']], - ['testExtensionVersionOperatorDoubleEquals', ['Extension testExtOne == 1.0 is required.']], - ['testExtensionVersionOperatorNoSpace', ['Extension testExtOne >= 99 is required.']], + ['testPHPVersionOperatorLessThan', [ + '__OFFSET_LINE=187', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP < 5.4 is required.', + ]], + ['testPHPVersionOperatorLessThanEquals', [ + '__OFFSET_LINE=195', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP <= 5.4 is required.', + ]], + ['testPHPVersionOperatorGreaterThan', [ + '__OFFSET_LINE=203', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP > 99 is required.', + ]], + ['testPHPVersionOperatorGreaterThanEquals', [ + '__OFFSET_LINE=211', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP >= 99 is required.', + ]], + ['testPHPVersionOperatorNoSpace', [ + '__OFFSET_LINE=251', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP >= 99 is required.', + ]], + ['testPHPVersionOperatorEquals', [ + '__OFFSET_LINE=219', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP = 5.4 is required.', + ]], + ['testPHPVersionOperatorDoubleEquals', [ + '__OFFSET_LINE=227', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHP == 5.4 is required.', + ]], + ['testPHPUnitVersionOperatorLessThan', [ + '__OFFSET_LINE=259', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit < 1.0 is required.', + ]], + ['testPHPUnitVersionOperatorLessThanEquals', [ + '__OFFSET_LINE=267', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit <= 1.0 is required.', + ]], + ['testPHPUnitVersionOperatorGreaterThan', [ + '__OFFSET_LINE=275', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit > 99 is required.', + ]], + ['testPHPUnitVersionOperatorGreaterThanEquals', [ + '__OFFSET_LINE=283', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit >= 99 is required.', + ]], + ['testPHPUnitVersionOperatorEquals', [ + '__OFFSET_LINE=291', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit = 1.0 is required.', + ]], + ['testPHPUnitVersionOperatorDoubleEquals', [ + '__OFFSET_LINE=299', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit == 1.0 is required.', + ]], + ['testPHPUnitVersionOperatorNoSpace', [ + '__OFFSET_LINE=323', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'PHPUnit >= 99 is required.', + ]], + ['testExtensionVersionOperatorLessThan', [ + '__OFFSET_LINE=330', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne < 1.0 is required.', + ]], + ['testExtensionVersionOperatorLessThanEquals', [ + '__OFFSET_LINE=337', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne <= 1.0 is required.', + ]], + ['testExtensionVersionOperatorGreaterThan', [ + '__OFFSET_LINE=344', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne > 99 is required.', + ]], + ['testExtensionVersionOperatorGreaterThanEquals', [ + '__OFFSET_LINE=351', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne >= 99 is required.', + ]], + ['testExtensionVersionOperatorEquals', [ + '__OFFSET_LINE=358', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne = 1.0 is required.', + ]], + ['testExtensionVersionOperatorDoubleEquals', [ + '__OFFSET_LINE=365', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne == 1.0 is required.', + ]], + ['testExtensionVersionOperatorNoSpace', [ + '__OFFSET_LINE=386', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), + 'Extension testExtOne >= 99 is required.', + ]], ['testVersionConstraintTildeMajor', [ + '__OFFSET_LINE=393', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), 'PHP version does not match the required constraint ~1.0.', 'PHPUnit version does not match the required constraint ~2.0.', ]], ['testVersionConstraintCaretMajor', [ + '__OFFSET_LINE=401', + '__OFFSET_FILE=' . $this->getRequirementsTestClassFile(), 'PHP version does not match the required constraint ^1.0.', 'PHPUnit version does not match the required constraint ^2.0.', ]], @@ -960,4 +1322,14 @@ public function testCoversAnnotationIncludesTraitsUsedByClass(): void ) ); } + + private function getRequirementsTestClassFile(): string + { + if (!$this->fileRequirementsTest) { + $reflector = new \ReflectionClass(\RequirementsTest::class); + $this->fileRequirementsTest = \realpath($reflector->getFileName()); + } + + return $this->fileRequirementsTest; + } } From b5e9e2acfe392e5503e0e2fb588a60b05468a9a7 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 12:12:33 +0100 Subject: [PATCH 11/18] Housekeeping: fix test description --- tests/end-to-end/loggers/debug.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end-to-end/loggers/debug.phpt b/tests/end-to-end/loggers/debug.phpt index 108fbec650f..ab20977edb7 100644 --- a/tests/end-to-end/loggers/debug.phpt +++ b/tests/end-to-end/loggers/debug.phpt @@ -1,5 +1,5 @@ --TEST-- -phpunit --debug BankAccountTest ../../_files/BankAccountTest.php +phpunit --debug -c tests/basic/configuration.basic.xml --FILE-- Date: Wed, 9 Jan 2019 12:27:32 +0100 Subject: [PATCH 12/18] Add end-to-end coverage of code location hints --- tests/requires-skip-code-location-hints.phpt | 120 +++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 tests/requires-skip-code-location-hints.phpt diff --git a/tests/requires-skip-code-location-hints.phpt b/tests/requires-skip-code-location-hints.phpt new file mode 100644 index 00000000000..0e280fda006 --- /dev/null +++ b/tests/requires-skip-code-location-hints.phpt @@ -0,0 +1,120 @@ +--TEST-- +phpunit --no-configuration RequirementsTest ../_files/RequirementsTest.php +--FILE-- + Date: Wed, 9 Jan 2019 12:36:21 +0100 Subject: [PATCH 13/18] Reduce visual clutter in colored Testdox caused by duration precision --- src/Util/TestDox/CliTestDoxPrinter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Util/TestDox/CliTestDoxPrinter.php b/src/Util/TestDox/CliTestDoxPrinter.php index 17ba12140fd..d572ffc2dd3 100644 --- a/src/Util/TestDox/CliTestDoxPrinter.php +++ b/src/Util/TestDox/CliTestDoxPrinter.php @@ -259,7 +259,7 @@ private function formatRuntime(float $time, string $color = ''): string $color = 'fg-magenta'; } - return Color::colorize($color, \sprintf(' %.2f ms', $time * 1000)); + return Color::colorize($color, \sprintf(' %d ms', ceil($time * 1000))); } private function printNonSuccessfulTestsSummary(int $numberOfExecutedTests): void From c9d68a258b06ebed4fab2282f4181fce34178997 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 13:02:09 +0100 Subject: [PATCH 14/18] Fix pathname matching --- tests/end-to-end/phpt/expect-location-hint.phpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end-to-end/phpt/expect-location-hint.phpt b/tests/end-to-end/phpt/expect-location-hint.phpt index 3dbd55dae4c..8a97c4c0258 100644 --- a/tests/end-to-end/phpt/expect-location-hint.phpt +++ b/tests/end-to-end/phpt/expect-location-hint.phpt @@ -25,7 +25,7 @@ There was 1 failure: 1) %stests%eend-to-end%e_files%ephpt-expect-location-hint-example.phpt Failed asserting that two strings are equal. -/Users/ewout/proj/phpunit/tests/end-to-end/_files/phpt-expect-location-hint-example.phpt:10 +%stests%eend-to-end%e_files%ephpt-expect-location-hint-example.phpt:10 FAILURES! Tests: 1, Assertions: 1, Failures: 1. From 11191a83cc07e96eae5791ee9a47481011cf3d96 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 14:14:05 +0100 Subject: [PATCH 15/18] CS/WS fix --- src/Util/TestDox/CliTestDoxPrinter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Util/TestDox/CliTestDoxPrinter.php b/src/Util/TestDox/CliTestDoxPrinter.php index d572ffc2dd3..b11f5f676d6 100644 --- a/src/Util/TestDox/CliTestDoxPrinter.php +++ b/src/Util/TestDox/CliTestDoxPrinter.php @@ -259,7 +259,7 @@ private function formatRuntime(float $time, string $color = ''): string $color = 'fg-magenta'; } - return Color::colorize($color, \sprintf(' %d ms', ceil($time * 1000))); + return Color::colorize($color, \sprintf(' %d ms', \ceil($time * 1000))); } private function printNonSuccessfulTestsSummary(int $numberOfExecutedTests): void From d6dd15bf949cc7b80d79ba9fc990b43631860d01 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 14:24:07 +0100 Subject: [PATCH 16/18] Kick CI to get new errors --- tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt b/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt index 1437dcc6609..b28316fc5ba 100644 --- a/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt +++ b/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt @@ -6,6 +6,7 @@ print "Nothing to see here, move along"; ?> --SKIPIF-- --EXPECT-- From d2b7e83d236ee5b3e56dd52a8d2355f58228f4f1 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Wed, 9 Jan 2019 14:34:42 +0100 Subject: [PATCH 17/18] Try to get Windows CI to agree on line offsets --- src/Runner/PhptTestCase.php | 2 +- tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 510aba0603a..2b254da2c79 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -655,7 +655,7 @@ private function getLocationHint(string $needle, array $sections, ?string $secti $sectionOffset = $sections[$section . '_offset'] ?? 0; $offset = $sectionOffset + 1; - $lines = \explode(\PHP_EOL, $sections[$section]); + $lines = \preg_split('/\r\n|\r|\n/', $sections[$section]); foreach ($lines as $line) { if (\strpos($line, $needle) !== false) { diff --git a/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt b/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt index b28316fc5ba..1437dcc6609 100644 --- a/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt +++ b/tests/end-to-end/_files/phpt-skipif-location-hint-example.phpt @@ -6,7 +6,6 @@ print "Nothing to see here, move along"; ?> --SKIPIF-- --EXPECT-- From 3e910b34ac70dd1196d086c002314f8ea5849941 Mon Sep 17 00:00:00 2001 From: Ewout Pieter den Ouden Date: Thu, 10 Jan 2019 16:06:36 +0100 Subject: [PATCH 18/18] Clean up. Bit of refactoring and new @testdox annotations --- src/Framework/Assert.php | 30 +++++++++++++++++++++++ src/Framework/TestCase.php | 27 +------------------- src/Runner/PhptTestCase.php | 8 +++--- tests/unit/Runner/TestSuiteSorterTest.php | 1 + 4 files changed, 36 insertions(+), 30 deletions(-) diff --git a/src/Framework/Assert.php b/src/Framework/Assert.php index 453abd9bbdc..4b08f1161e8 100644 --- a/src/Framework/Assert.php +++ b/src/Framework/Assert.php @@ -3000,6 +3000,12 @@ public static function markTestIncomplete(string $message = ''): void */ public static function markTestSkipped(string $message = ''): void { + if ($hint = self::detectLocationHint($message)) { + $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); + + throw new SyntheticSkippedError($hint['message'], 0, $hint['file'], $hint['line'], $trace); + } + throw new SkippedTestError($message); } @@ -3019,6 +3025,30 @@ public static function resetCount(): void self::$count = 0; } + private static function detectLocationHint(string $message): ?array + { + $hint = null; + $lines = \preg_split('/\r\n|\r|\n/', $message); + + while (\strpos($lines[0], '__OFFSET') !== false) { + $offset = \explode('=', \array_shift($lines)); + + if ($offset[0] === '__OFFSET_FILE') { + $hint['file'] = $offset[1]; + } + + if ($offset[0] === '__OFFSET_LINE') { + $hint['line'] = $offset[1]; + } + } + + if ($hint) { + $hint['message'] = \implode(\PHP_EOL, $lines); + } + + return $hint; + } + private static function isValidAttributeName(string $attributeName): bool { return \preg_match('/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/', $attributeName); diff --git a/src/Framework/TestCase.php b/src/Framework/TestCase.php index 3e8c4d29b69..f70f658a354 100644 --- a/src/Framework/TestCase.php +++ b/src/Framework/TestCase.php @@ -1644,35 +1644,10 @@ private function checkRequirements(): void ); if (!empty($missingRequirements)) { - $this->markTestSkippedWithLocationHint($missingRequirements); + $this->markTestSkipped(\implode(\PHP_EOL, $missingRequirements)); } } - private function markTestSkippedWithLocationHint(array $required): void - { - $file = null; - $line = null; - - while (\strpos($required[0], '__OFFSET') !== false) { - $offset = \explode('=', \array_shift($required)); - - if ($offset[0] === '__OFFSET_FILE') { - $file = $offset[1]; - } - - if ($offset[0] === '__OFFSET_LINE') { - $line = $offset[1]; - } - } - - if ($file && $line) { - $trace = \debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS); - - throw new SyntheticSkippedError(\implode(\PHP_EOL, $required), 0, $file, $line, $trace); - } - $this->markTestSkipped(\implode(\PHP_EOL, $required)); - } - private function verifyMockObjects(): void { foreach ($this->mockObjects as $mockObject) { diff --git a/src/Runner/PhptTestCase.php b/src/Runner/PhptTestCase.php index 2b254da2c79..5341c677178 100644 --- a/src/Runner/PhptTestCase.php +++ b/src/Runner/PhptTestCase.php @@ -647,8 +647,8 @@ private function getLocationHint(string $needle, array $sections, ?string $secti $file = \trim($sections[$section . '_EXTERNAL']); return [ - 'file' => \realpath(\dirname($this->filename) . \DIRECTORY_SEPARATOR . $file), - 'line' => 1, + 'file' => \realpath(\dirname($this->filename) . \DIRECTORY_SEPARATOR . $file), + 'line' => 1, ]; } @@ -660,8 +660,8 @@ private function getLocationHint(string $needle, array $sections, ?string $secti foreach ($lines as $line) { if (\strpos($line, $needle) !== false) { return [ - 'file' => \realpath($this->filename), - 'line' => $offset, + 'file' => \realpath($this->filename), + 'line' => $offset, ]; } $offset++; diff --git a/tests/unit/Runner/TestSuiteSorterTest.php b/tests/unit/Runner/TestSuiteSorterTest.php index 9f2114477b2..355dd729834 100644 --- a/tests/unit/Runner/TestSuiteSorterTest.php +++ b/tests/unit/Runner/TestSuiteSorterTest.php @@ -56,6 +56,7 @@ public function testThrowsExceptionWhenUsingInvalidOrderDefectsOption(): void } /** + * @testdox Empty TestSuite not affected (order=$order, resolve=$resolveDependencies, defects=$orderDefects) * @dataProvider suiteSorterOptionPermutationsProvider */ public function testShouldNotAffectEmptyTestSuite(int $order, bool $resolveDependencies, int $orderDefects): void