From 890de8da580aa84be0d7fe04755abb6fc352f741 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20N=C4=9Bmec?= Date: Fri, 20 Nov 2020 22:34:12 +0100 Subject: [PATCH 01/10] code coverage: improved coverage report template (#422) --- .travis.yml | 1 + src/CodeCoverage/Generators/template.phtml | 468 ++++++++++++++++++--- 2 files changed, 414 insertions(+), 55 deletions(-) diff --git a/.travis.yml b/.travis.yml index b9c764b5..8075f52f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,6 +61,7 @@ jobs: allow_failures: - stage: Static Analysis (informative) - stage: Code Coverage + - php: nightly dist: xenial diff --git a/src/CodeCoverage/Generators/template.phtml b/src/CodeCoverage/Generators/template.phtml index 4db2a3e3..0fb4e126 100644 --- a/src/CodeCoverage/Generators/template.phtml +++ b/src/CodeCoverage/Generators/template.phtml @@ -10,12 +10,12 @@ -

Code coverage  %

- $info): ?> -
+name); + + $currentFile = ''; + foreach ($keys as $key) { + $currentFile = $currentFile . ($currentFile !== '' ? '/' : '') . $key; + $arr = &$arr['files'][$key]; + + if (!isset($arr['name'])) { + $arr['name'] = $currentFile; + } + $arr['count'] = isset($arr['count']) ? $arr['count'] + 1 : 1; + $arr['coverage'] = isset($arr['coverage']) ? $arr['coverage'] + $info->coverage : $info->coverage; + } + $arr = $value; +} + +$jsonData = []; +$directories = []; +$allLinesCount = 0; +foreach ($files as $id => $info) { + $code = file_get_contents($info->file); + $lineCount = substr_count($code, "\n") + 1; + $digits = ceil(log10($lineCount)) + 1; + + $allLinesCount += $lineCount; + + $currentId = "F{$id}"; + assignArrayByPath($directories, $info, $currentId); + + $data = (array) $info; + $data['digits'] = $digits; + $data['lineCount'] = $lineCount; + $data['content'] = strtr(highlight_string($code, true), [ + '' => "", + '' => '', + '
' => '
', + ]); + $jsonData[$currentId] = $data; +} ?> + +

+ Code coverage  % + sources have lines of code in files +

+ + + +
+
- class ? " class='$info->class'" : '' ?>> - - - - + + + + + + +
coverage ?> %
name ?>
+  % + +
+
+ path  +
+
+
+ +
+ + + + + addClickListener('tab-item', function(e) { + e.preventDefault(); + let tabs = document.getElementsByClassName('tab-content'); + for (let i = 0; i < tabs.length; i++) { + tabs[i].style.display = 'none'; + } + let tabItems = document.getElementsByClassName('tab-item'); + for (let i = 0; i < tabItems.length; i++) { + tabItems[i].classList.remove('active'); + } + this.classList.add('active'); + + let id = this.href.split('#')[1]; + if (id === 'files') { + initFiles(); + } else { + initDirectories(); + } + + let el = document.getElementById(id); + if (el.style.display === 'block') { + el.style.display = 'none'; + } else { + el.style.display = 'block'; + } + }); + + document.addEventListener("DOMContentLoaded", function(event) { + initFiles(true); + + let el = document.getElementById(window.location.hash.replace(/^#|L\d+$/g, '')); + if (el) { + initFileContent(el); + el.style.display = 'block'; + } + }); + })(); + From 10a9b57a77704d54435d14c612a93366bd96cccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 3 Dec 2020 12:15:04 +0100 Subject: [PATCH 02/10] CodeCoverage: added version to array of engines --- src/CodeCoverage/Collector.php | 12 ++++++++---- src/Runner/CliTester.php | 4 +++- src/Runner/info.php | 4 +++- tests/CodeCoverage/Collector.phpt | 5 +++-- tests/Runner/PhpInterpreter.phpt | 6 +++--- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/CodeCoverage/Collector.php b/src/CodeCoverage/Collector.php index 1e80412d..585b6b52 100644 --- a/src/CodeCoverage/Collector.php +++ b/src/CodeCoverage/Collector.php @@ -30,9 +30,9 @@ class Collector public static function detectEngines(): array { return array_filter([ - extension_loaded('pcov') ? self::ENGINE_PCOV : null, - defined('PHPDBG_VERSION') ? self::ENGINE_PHPDBG : null, - extension_loaded('xdebug') ? self::ENGINE_XDEBUG : null, + extension_loaded('pcov') ? [self::ENGINE_PCOV, phpversion('pcov')] : null, + defined('PHPDBG_VERSION') ? [self::ENGINE_PHPDBG, PHPDBG_VERSION] : null, + extension_loaded('xdebug') ? [self::ENGINE_XDEBUG, phpversion('xdebug')] : null, ]); } @@ -52,7 +52,11 @@ public static function start(string $file, string $engine): void if (self::isStarted()) { throw new \LogicException('Code coverage collector has been already started.'); - } elseif (!in_array($engine, self::detectEngines(), true)) { + } elseif (!in_array( + $engine, + array_map(function (array $engineInfo) { return $engineInfo[0]; }, self::detectEngines()), + true + )) { throw new \LogicException("Code coverage engine '$engine' is not supported."); } diff --git a/src/Runner/CliTester.php b/src/Runner/CliTester.php index cbf96033..3b294f5a 100644 --- a/src/Runner/CliTester.php +++ b/src/Runner/CliTester.php @@ -236,8 +236,10 @@ private function prepareCodeCoverage(Runner $runner): string file_put_contents($this->options['--coverage'], ''); $file = realpath($this->options['--coverage']); + [$engine] = reset($engines); + $runner->setEnvironmentVariable(Environment::COVERAGE, $file); - $runner->setEnvironmentVariable(Environment::COVERAGE_ENGINE, $engine = reset($engines)); + $runner->setEnvironmentVariable(Environment::COVERAGE_ENGINE, $engine); if ($engine === CodeCoverage\Collector::ENGINE_PCOV && count($this->options['--coverage-src'])) { $runner->addPhpIniOption('pcov.directory', Helpers::findCommonDirectory($this->options['--coverage-src'])); diff --git a/src/Runner/info.php b/src/Runner/info.php index c00d4521..a2c2b012 100644 --- a/src/Runner/info.php +++ b/src/Runner/info.php @@ -36,7 +36,9 @@ 'PHP version' . ($isPhpDbg ? '; PHPDBG version' : '') => "$info->version ($info->sapi)" . ($isPhpDbg ? "; $info->phpDbgVersion" : ''), 'Loaded php.ini files' => count($info->iniFiles) ? implode(', ', $info->iniFiles) : '(none)', - 'Code coverage engines' => count($info->codeCoverageEngines) ? implode(', ', $info->codeCoverageEngines) : '(not available)', + 'Code coverage engines' => count($info->codeCoverageEngines) + ? implode(', ', array_map(function (array $engineInfo) { return sprintf('%s (%s)', ...$engineInfo); }, $info->codeCoverageEngines)) + : '(not available)', 'PHP temporary directory' => $info->tempDir == '' ? '(empty)' : $info->tempDir, 'Loaded extensions' => count($info->extensions) ? implode(', ', $info->extensions) : '(none)', ] as $title => $value) { diff --git a/tests/CodeCoverage/Collector.phpt b/tests/CodeCoverage/Collector.phpt index a6e4d3bb..595137b8 100644 --- a/tests/CodeCoverage/Collector.phpt +++ b/tests/CodeCoverage/Collector.phpt @@ -9,13 +9,14 @@ use Tester\FileMock; require __DIR__ . '/../bootstrap.php'; -$engines = array_filter(CodeCoverage\Collector::detectEngines(), function (string $engine) { +$engines = array_filter(CodeCoverage\Collector::detectEngines(), function (array $engineInfo) { + [$engine] = $engineInfo; return $engine !== CodeCoverage\Collector::ENGINE_PCOV; // PCOV needs system pcov.directory INI to be set }); if (count($engines) < 1) { Tester\Environment::skip('Requires Xdebug or PHPDB SAPI.'); } -$engine = reset($engines); +[$engine] = reset($engines); if (CodeCoverage\Collector::isStarted()) { Tester\Environment::skip('Requires running without --coverage.'); diff --git a/tests/Runner/PhpInterpreter.phpt b/tests/Runner/PhpInterpreter.phpt index 13e89b92..2516d628 100644 --- a/tests/Runner/PhpInterpreter.phpt +++ b/tests/Runner/PhpInterpreter.phpt @@ -18,15 +18,15 @@ Assert::same(strpos(PHP_SAPI, 'cgi') !== false, $interpreter->isCgi()); $count = 0; $engines = $interpreter->getCodeCoverageEngines(); if (defined('PHPDBG_VERSION')) { - Assert::contains(Tester\CodeCoverage\Collector::ENGINE_PHPDBG, $engines); + Assert::contains([Tester\CodeCoverage\Collector::ENGINE_PHPDBG, PHPDBG_VERSION], $engines); $count++; } if (extension_loaded('xdebug')) { - Assert::contains(Tester\CodeCoverage\Collector::ENGINE_XDEBUG, $engines); + Assert::contains([Tester\CodeCoverage\Collector::ENGINE_XDEBUG, phpversion('xdebug')], $engines); $count++; } if (extension_loaded('pcov')) { - Assert::contains(Tester\CodeCoverage\Collector::ENGINE_PCOV, $engines); + Assert::contains([Tester\CodeCoverage\Collector::ENGINE_PCOV, phpversion('pcov')], $engines); $count++; } Assert::count($count, $engines); From 652853ecfa478e459ae5b67bb26c8db3c8b7fdd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 3 Dec 2020 12:15:04 +0100 Subject: [PATCH 03/10] CodeCoverage: support Xdebug 3 (#424) --- src/Runner/CliTester.php | 6 +++++- tests/CodeCoverage/Collector.phpt | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Runner/CliTester.php b/src/Runner/CliTester.php index 3b294f5a..f0638f1d 100644 --- a/src/Runner/CliTester.php +++ b/src/Runner/CliTester.php @@ -236,11 +236,15 @@ private function prepareCodeCoverage(Runner $runner): string file_put_contents($this->options['--coverage'], ''); $file = realpath($this->options['--coverage']); - [$engine] = reset($engines); + [$engine, $version] = reset($engines); $runner->setEnvironmentVariable(Environment::COVERAGE, $file); $runner->setEnvironmentVariable(Environment::COVERAGE_ENGINE, $engine); + if ($engine === CodeCoverage\Collector::ENGINE_XDEBUG && version_compare($version, '3.0.0', '>=')) { + $runner->addPhpIniOption('xdebug.mode', ltrim(ini_get('xdebug.mode') . ',coverage', ',')); + } + if ($engine === CodeCoverage\Collector::ENGINE_PCOV && count($this->options['--coverage-src'])) { $runner->addPhpIniOption('pcov.directory', Helpers::findCommonDirectory($this->options['--coverage-src'])); } diff --git a/tests/CodeCoverage/Collector.phpt b/tests/CodeCoverage/Collector.phpt index 595137b8..29074d60 100644 --- a/tests/CodeCoverage/Collector.phpt +++ b/tests/CodeCoverage/Collector.phpt @@ -16,7 +16,14 @@ $engines = array_filter(CodeCoverage\Collector::detectEngines(), function (array if (count($engines) < 1) { Tester\Environment::skip('Requires Xdebug or PHPDB SAPI.'); } -[$engine] = reset($engines); +[$engine, $version] = reset($engines); + +if ($engine === CodeCoverage\Collector::ENGINE_XDEBUG + && version_compare($version, '3.0.0', '>=') + && strpos(ini_get('xdebug.mode'), 'coverage') === false +) { + Tester\Environment::skip('Requires xdebug.mode=coverage with Xdebug 3.'); +} if (CodeCoverage\Collector::isStarted()) { Tester\Environment::skip('Requires running without --coverage.'); From 82b649df4a0e8ecc436d0df67f557dcad7c08394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jir=CC=8Ci=CC=81=20Pudil?= Date: Sun, 14 Oct 2018 16:55:15 +0200 Subject: [PATCH 04/10] Snapshot testing --- src/Framework/Environment.php | 8 ++ src/Framework/Snapshot.php | 91 +++++++++++++++++++ src/Runner/CliTester.php | 4 + src/bootstrap.php | 1 + tests/Framework/.gitignore | 1 + tests/Framework/Snapshot.match.phpt | 21 +++++ tests/Framework/Snapshot.update.phpt | 43 +++++++++ .../Snapshot.match.existingSnapshot.phps | 1 + tests/Runner/Runner.snapshots.phpt | 80 ++++++++++++++++ tests/Runner/snapshots/.gitignore | 1 + tests/Runner/snapshots/update-snapshots.phptx | 7 ++ 11 files changed, 258 insertions(+) create mode 100644 src/Framework/Snapshot.php create mode 100644 tests/Framework/.gitignore create mode 100644 tests/Framework/Snapshot.match.phpt create mode 100644 tests/Framework/Snapshot.update.phpt create mode 100644 tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps create mode 100644 tests/Runner/Runner.snapshots.phpt create mode 100644 tests/Runner/snapshots/.gitignore create mode 100644 tests/Runner/snapshots/update-snapshots.phptx diff --git a/src/Framework/Environment.php b/src/Framework/Environment.php index 150b14b4..759dc1bf 100644 --- a/src/Framework/Environment.php +++ b/src/Framework/Environment.php @@ -30,6 +30,9 @@ class Environment /** Thread number when run tests in multi threads */ public const THREAD = 'NETTE_TESTER_THREAD'; + /** Should Tester update snapshots? */ + public const UPDATE_SNAPSHOTS = 'NETTE_TESTER_UPDATE_SNAPSHOTS'; + /** @var bool */ public static $checkAssertions = false; @@ -126,6 +129,11 @@ public static function setupErrors(): void self::removeOutputBuffers(); echo "\n", Dumper::color('white/red', "Fatal error: $error[message] in $error[file] on line $error[line]"), "\n"; } + } elseif (getenv(self::UPDATE_SNAPSHOTS) && Snapshot::$updatedSnapshots) { + self::removeOutputBuffers(); + echo "\nThe following snapshots were updated, please make sure they are correct:\n" + . implode("\n", Snapshot::$updatedSnapshots) . "\n"; + exit(Runner\Job::CODE_FAIL); } elseif (self::$checkAssertions && !Assert::$counter) { self::removeOutputBuffers(); echo "\n", Dumper::color('white/red', 'Error: This test forgets to execute an assertion.'), "\n"; diff --git a/src/Framework/Snapshot.php b/src/Framework/Snapshot.php new file mode 100644 index 00000000..5fa74ce5 --- /dev/null +++ b/src/Framework/Snapshot.php @@ -0,0 +1,91 @@ +options['--coverage']) { $coverageFile = $this->prepareCodeCoverage($runner); } + if ($this->options['--update-snapshots']) { + $runner->setEnvironmentVariable(Environment::UPDATE_SNAPSHOTS, '1'); + } if ($this->options['-o'] !== null) { ob_clean(); @@ -118,6 +121,7 @@ private function loadOptions(): CommandLine --colors [1|0] Enable or disable colors. --coverage Generate code coverage report to file. --coverage-src Path to source code. + --update-snapshots Create or update snapshot files. Tests with snapshot changes will be marked as skipped. -h | --help This help. XX diff --git a/src/bootstrap.php b/src/bootstrap.php index 065a4c7e..b18444e1 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -16,6 +16,7 @@ require __DIR__ . '/Framework/TestCase.php'; require __DIR__ . '/Framework/FileMutator.php'; require __DIR__ . '/Framework/Expect.php'; +require __DIR__ . '/Framework/Snapshot.php'; require __DIR__ . '/CodeCoverage/Collector.php'; require __DIR__ . '/Runner/Job.php'; diff --git a/tests/Framework/.gitignore b/tests/Framework/.gitignore new file mode 100644 index 00000000..373f5f3d --- /dev/null +++ b/tests/Framework/.gitignore @@ -0,0 +1 @@ +snapshots/ diff --git a/tests/Framework/Snapshot.match.phpt b/tests/Framework/Snapshot.match.phpt new file mode 100644 index 00000000..b6823cf5 --- /dev/null +++ b/tests/Framework/Snapshot.match.phpt @@ -0,0 +1,21 @@ + 42], 'existingSnapshot'); + +Assert::exception(function () { + Snapshot::match(['answer' => 43], 'existingSnapshot'); +}, AssertException::class, 'Snapshot existingSnapshot: %a% should be equal to %a%'); + +Assert::exception(function () { + Snapshot::match('value', 'nonExistingSnapshot'); +}, AssertException::class, "Missing snapshot file '%A%', use --update-snapshots option to generate it."); diff --git a/tests/Framework/Snapshot.update.phpt b/tests/Framework/Snapshot.update.phpt new file mode 100644 index 00000000..2e1ba1fa --- /dev/null +++ b/tests/Framework/Snapshot.update.phpt @@ -0,0 +1,43 @@ + 42], 'newSnapshot'); +Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps')); + +// existing + +file_put_contents( + Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps', + ' 43);' . PHP_EOL +); + +Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); +Snapshot::match(['answer' => 42], 'updatedSnapshot'); +Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); + +// Snapshot::$updatedSnapshots + +Assert::equal([ + Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Snapshot.update.newSnapshot.phps', + Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Snapshot.update.updatedSnapshot.phps', +], Snapshot::$updatedSnapshots); + +// reset the env variable so that the test does not fail due to updated snapshots +putenv(Environment::UPDATE_SNAPSHOTS . '=0'); diff --git a/tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps b/tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps new file mode 100644 index 00000000..a7d8f5b1 --- /dev/null +++ b/tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps @@ -0,0 +1 @@ + 42]; diff --git a/tests/Runner/Runner.snapshots.phpt b/tests/Runner/Runner.snapshots.phpt new file mode 100644 index 00000000..70fc6885 --- /dev/null +++ b/tests/Runner/Runner.snapshots.phpt @@ -0,0 +1,80 @@ +results[basename($test->getFile())] = [$test->getResult(), $test->message]; + } + + + public function begin(): void + { + } + + + public function end(): void + { + } +} + + +Tester\Helpers::purge(__DIR__ . '/snapshots/snapshots'); + + +// first run, without update -> fail + +$runner = new Tester\Runner\Runner(createInterpreter()); +$runner->paths[] = __DIR__ . '/snapshots/*.phptx'; +$runner->outputHandlers[] = $logger = new Logger; +$runner->run(); + +Assert::same(Test::FAILED, $logger->results['update-snapshots.phptx'][0]); +Assert::match( + "Failed: Missing snapshot file '%a%', use --update-snapshots option to generate it.\n%A%", + trim(Dumper::removeColors($logger->results['update-snapshots.phptx'][1])) +); + +// second run, with update -> skipped + +$runner = new Tester\Runner\Runner(createInterpreter()); +$runner->paths[] = __DIR__ . '/snapshots/*.phptx'; +$runner->outputHandlers[] = $logger = new Logger; +$runner->setEnvironmentVariable(Tester\Environment::UPDATE_SNAPSHOTS, '1'); +$runner->run(); + +Assert::same(Test::FAILED, $logger->results['update-snapshots.phptx'][0]); +Assert::match( + "The following snapshots were updated, please make sure they are correct:\n%a%.snapshot.phps", + trim(Dumper::removeColors($logger->results['update-snapshots.phptx'][1])) +); + +// third run, without update -> pass + +$runner = new Tester\Runner\Runner(createInterpreter()); +$runner->paths[] = __DIR__ . '/snapshots/*.phptx'; +$runner->outputHandlers[] = $logger = new Logger; +$runner->run(); + +Assert::same(Test::PASSED, $logger->results['update-snapshots.phptx'][0]); diff --git a/tests/Runner/snapshots/.gitignore b/tests/Runner/snapshots/.gitignore new file mode 100644 index 00000000..373f5f3d --- /dev/null +++ b/tests/Runner/snapshots/.gitignore @@ -0,0 +1 @@ +snapshots/ diff --git a/tests/Runner/snapshots/update-snapshots.phptx b/tests/Runner/snapshots/update-snapshots.phptx new file mode 100644 index 00000000..4790884e --- /dev/null +++ b/tests/Runner/snapshots/update-snapshots.phptx @@ -0,0 +1,7 @@ + Date: Thu, 25 Oct 2018 10:05:18 +0200 Subject: [PATCH 05/10] change cli usage description --- src/Runner/CliTester.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Runner/CliTester.php b/src/Runner/CliTester.php index ea618020..ccd2e6d2 100644 --- a/src/Runner/CliTester.php +++ b/src/Runner/CliTester.php @@ -121,7 +121,7 @@ private function loadOptions(): CommandLine --colors [1|0] Enable or disable colors. --coverage Generate code coverage report to file. --coverage-src Path to source code. - --update-snapshots Create or update snapshot files. Tests with snapshot changes will be marked as skipped. + --update-snapshots Create or update snapshot files. -h | --help This help. XX From 00c61f9d7b73d710dae7cae804eaf012933bcd4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jir=CC=8Ci=CC=81=20Pudil?= Date: Mon, 3 Dec 2018 09:53:43 +0100 Subject: [PATCH 06/10] add Assert::snapshot() and make Snapshot an instantiable representation of a single snapshot --- src/Framework/Assert.php | 34 ++++++++ src/Framework/Snapshot.php | 86 +++++++++++-------- tests/Framework/Assert.snapshot.phpt | 29 +++++++ tests/Framework/Assert.snapshot.update.phpt | 43 ++++++++++ tests/Framework/Snapshot.match.phpt | 21 ----- tests/Framework/Snapshot.update.phpt | 43 ---------- ...s => Assert.snapshot.anotherSnapshot.phps} | 0 .../Assert.snapshot.existingSnapshot.phps | 1 + tests/Runner/Runner.snapshots.phpt | 4 +- tests/Runner/snapshots/update-snapshots.phptx | 4 +- 10 files changed, 159 insertions(+), 106 deletions(-) create mode 100644 tests/Framework/Assert.snapshot.phpt create mode 100644 tests/Framework/Assert.snapshot.update.phpt delete mode 100644 tests/Framework/Snapshot.match.phpt delete mode 100644 tests/Framework/Snapshot.update.phpt rename tests/Framework/fixtures/{Snapshot.match.existingSnapshot.phps => Assert.snapshot.anotherSnapshot.phps} (100%) create mode 100644 tests/Framework/fixtures/Assert.snapshot.existingSnapshot.phps diff --git a/src/Framework/Assert.php b/src/Framework/Assert.php index 1ae8d16f..741a37d9 100644 --- a/src/Framework/Assert.php +++ b/src/Framework/Assert.php @@ -442,6 +442,40 @@ public static function matchFile(string $file, $actual, string $description = nu } + /** + * Compares value with a previously created snapshot. + */ + public static function snapshot(string $snapshotName, $actual, string $description = null): void + { + self::$counter++; + + $snapshot = new Snapshot($snapshotName); + if (!$snapshot->exists()) { + if (!$snapshot->canUpdate()) { + self::fail("Missing snapshot '$snapshotName', use --update-snapshots option to generate it."); + } + + $snapshot->update($actual); + } + + $expected = $snapshot->read(); + if ($expected !== $actual) { + if (!$snapshot->canUpdate()) { + self::fail( + self::describe( + "%1 should be %2 in snapshot '$snapshotName'", + $description + ), + $actual, + $expected + ); + } + + $snapshot->update($actual); + } + } + + /** * Assertion that fails. */ diff --git a/src/Framework/Snapshot.php b/src/Framework/Snapshot.php index 5fa74ce5..d690f872 100644 --- a/src/Framework/Snapshot.php +++ b/src/Framework/Snapshot.php @@ -11,71 +11,70 @@ /** - * Snapshot testing helper. + * Snapshot of a tested value. */ class Snapshot { + /** @var string */ public static $snapshotDir = 'snapshots'; + /** @var string[] */ public static $updatedSnapshots = []; + /** @var string[] */ + private static $usedNames = []; - /** - * Compares value with a previously created snapshot. - */ - public static function match($value, string $snapshotName): void - { - $updateSnapshots = (bool) getenv(Environment::UPDATE_SNAPSHOTS); - - $testFile = $_SERVER['argv'][0]; - $snapshotFile = self::getSnapshotFile($testFile, $snapshotName); + /** @var string */ + private $name; - if (!file_exists($snapshotFile)) { - if (!$updateSnapshots) { - Assert::fail("Missing snapshot file '$snapshotFile', use --update-snapshots option to generate it."); - } - self::write($snapshotFile, $value); + public function __construct(string $name) + { + if (!preg_match('/^[a-zA-Z0-9-_]+$/', $name)) { + throw new \Exception("Invalid snapshot name '$name'. Only alphanumeric characters, dash and underscore are allowed."); } - $snapshot = self::read($snapshotFile); + if (in_array($name, self::$usedNames, true)) { + throw new \Exception("Snapshot '$name' was already asserted, please use a different name."); + } - try { - Assert::equal($snapshot, $value, "Snapshot $snapshotName"); + $this->name = self::$usedNames[] = $name; + } - } catch (AssertException $e) { - if (!$updateSnapshots) { - throw $e; - } - self::write($snapshotFile, $value); - } + public function exists(): bool + { + return file_exists($this->getSnapshotFile()); } - private static function getSnapshotFile(string $testFile, string $snapshotName): string + public function read() { - $path = self::$snapshotDir . DIRECTORY_SEPARATOR . pathinfo($testFile, PATHINFO_FILENAME) . '.' . $snapshotName . '.phps'; - if (!preg_match('#/|\w:#A', self::$snapshotDir)) { - $path = dirname($testFile) . DIRECTORY_SEPARATOR . $path; - } - return $path; + $snapshotFile = $this->getSnapshotFile(); + set_error_handler(function ($errno, $errstr) use ($snapshotFile) { + throw new \Exception("Unable to read snapshot file '$snapshotFile': $errstr"); + }); + + $snapshotContents = include $snapshotFile; + + restore_error_handler(); + return $snapshotContents; } - private static function read(string $snapshotFile) + public function canUpdate(): bool { - $snapshotContents = @file_get_contents($snapshotFile); - if ($snapshotContents === false) { - throw new \Exception("Unable to read snapshot file '$snapshotFile'."); - } - - return eval(substr($snapshotContents, strlen('canUpdate()) { + throw new \Exception('Cannot update snapshot. Please run tests again with --update-snapshots.'); + } + + $snapshotFile = $this->getSnapshotFile(); $snapshotDirectory = dirname($snapshotFile); if (!is_dir($snapshotDirectory) && !mkdir($snapshotDirectory)) { throw new \Exception("Unable to create snapshot directory '$snapshotDirectory'."); @@ -88,4 +87,15 @@ private static function write(string $snapshotFile, $value): void self::$updatedSnapshots[] = $snapshotFile; } + + + private function getSnapshotFile(): string + { + $testFile = $_SERVER['argv'][0]; + $path = self::$snapshotDir . DIRECTORY_SEPARATOR . pathinfo($testFile, PATHINFO_FILENAME) . '.' . $this->name . '.phps'; + if (!preg_match('#/|\w:#A', self::$snapshotDir)) { + $path = dirname($testFile) . DIRECTORY_SEPARATOR . $path; + } + return $path; + } } diff --git a/tests/Framework/Assert.snapshot.phpt b/tests/Framework/Assert.snapshot.phpt new file mode 100644 index 00000000..2b23044f --- /dev/null +++ b/tests/Framework/Assert.snapshot.phpt @@ -0,0 +1,29 @@ + 42]); + +Assert::exception(function () { + Assert::snapshot('invalid / name', ['answer' => 42]); +}, \Exception::class, "Invalid snapshot name 'invalid / name'. Only alphanumeric characters, dash and underscore are allowed."); + +Assert::exception(function () { + Assert::snapshot('existingSnapshot', ['answer' => 42]); +}, \Exception::class, "Snapshot 'existingSnapshot' was already asserted, please use a different name."); + +Assert::exception(function () { + Assert::snapshot('anotherSnapshot', ['answer' => 43]); +}, AssertException::class, "%a% should be %a% in snapshot 'anotherSnapshot'"); + +Assert::exception(function () { + Assert::snapshot('nonExistingSnapshot', 'value'); +}, AssertException::class, "Missing snapshot 'nonExistingSnapshot', use --update-snapshots option to generate it."); diff --git a/tests/Framework/Assert.snapshot.update.phpt b/tests/Framework/Assert.snapshot.update.phpt new file mode 100644 index 00000000..3b7078ad --- /dev/null +++ b/tests/Framework/Assert.snapshot.update.phpt @@ -0,0 +1,43 @@ + 42]); +Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.newSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Assert.snapshot.update.newSnapshot.phps')); + +// existing + +file_put_contents( + Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps', + ' 43);' . PHP_EOL +); + +Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); +Assert::snapshot('updatedSnapshot', ['answer' => 42]); +Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); + +// Snapshot::$updatedSnapshots + +Assert::same([ + Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.newSnapshot.phps', + Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.updatedSnapshot.phps', +], Snapshot::$updatedSnapshots); + +// reset the env variable so that the test does not fail due to updated snapshots +putenv(Environment::UPDATE_SNAPSHOTS . '=0'); diff --git a/tests/Framework/Snapshot.match.phpt b/tests/Framework/Snapshot.match.phpt deleted file mode 100644 index b6823cf5..00000000 --- a/tests/Framework/Snapshot.match.phpt +++ /dev/null @@ -1,21 +0,0 @@ - 42], 'existingSnapshot'); - -Assert::exception(function () { - Snapshot::match(['answer' => 43], 'existingSnapshot'); -}, AssertException::class, 'Snapshot existingSnapshot: %a% should be equal to %a%'); - -Assert::exception(function () { - Snapshot::match('value', 'nonExistingSnapshot'); -}, AssertException::class, "Missing snapshot file '%A%', use --update-snapshots option to generate it."); diff --git a/tests/Framework/Snapshot.update.phpt b/tests/Framework/Snapshot.update.phpt deleted file mode 100644 index 2e1ba1fa..00000000 --- a/tests/Framework/Snapshot.update.phpt +++ /dev/null @@ -1,43 +0,0 @@ - 42], 'newSnapshot'); -Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps')); -Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.newSnapshot.phps')); - -// existing - -file_put_contents( - Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps', - ' 43);' . PHP_EOL -); - -Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); -Snapshot::match(['answer' => 42], 'updatedSnapshot'); -Assert::true(file_exists(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); -Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Snapshot.update.updatedSnapshot.phps')); - -// Snapshot::$updatedSnapshots - -Assert::equal([ - Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Snapshot.update.newSnapshot.phps', - Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Snapshot.update.updatedSnapshot.phps', -], Snapshot::$updatedSnapshots); - -// reset the env variable so that the test does not fail due to updated snapshots -putenv(Environment::UPDATE_SNAPSHOTS . '=0'); diff --git a/tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps b/tests/Framework/fixtures/Assert.snapshot.anotherSnapshot.phps similarity index 100% rename from tests/Framework/fixtures/Snapshot.match.existingSnapshot.phps rename to tests/Framework/fixtures/Assert.snapshot.anotherSnapshot.phps diff --git a/tests/Framework/fixtures/Assert.snapshot.existingSnapshot.phps b/tests/Framework/fixtures/Assert.snapshot.existingSnapshot.phps new file mode 100644 index 00000000..a7d8f5b1 --- /dev/null +++ b/tests/Framework/fixtures/Assert.snapshot.existingSnapshot.phps @@ -0,0 +1 @@ + 42]; diff --git a/tests/Runner/Runner.snapshots.phpt b/tests/Runner/Runner.snapshots.phpt index 70fc6885..2036cb69 100644 --- a/tests/Runner/Runner.snapshots.phpt +++ b/tests/Runner/Runner.snapshots.phpt @@ -52,11 +52,11 @@ $runner->run(); Assert::same(Test::FAILED, $logger->results['update-snapshots.phptx'][0]); Assert::match( - "Failed: Missing snapshot file '%a%', use --update-snapshots option to generate it.\n%A%", + "Failed: Missing snapshot '%a%', use --update-snapshots option to generate it.\n%A%", trim(Dumper::removeColors($logger->results['update-snapshots.phptx'][1])) ); -// second run, with update -> skipped +// second run, with update -> fail $runner = new Tester\Runner\Runner(createInterpreter()); $runner->paths[] = __DIR__ . '/snapshots/*.phptx'; diff --git a/tests/Runner/snapshots/update-snapshots.phptx b/tests/Runner/snapshots/update-snapshots.phptx index 4790884e..0e7a2c2d 100644 --- a/tests/Runner/snapshots/update-snapshots.phptx +++ b/tests/Runner/snapshots/update-snapshots.phptx @@ -1,7 +1,7 @@ Date: Fri, 6 Sep 2019 09:20:01 +0200 Subject: [PATCH 07/10] use platform-specific directory separator in tests --- tests/Framework/Assert.snapshot.phpt | 2 +- tests/Framework/Assert.snapshot.update.phpt | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/Framework/Assert.snapshot.phpt b/tests/Framework/Assert.snapshot.phpt index 2b23044f..6e000212 100644 --- a/tests/Framework/Assert.snapshot.phpt +++ b/tests/Framework/Assert.snapshot.phpt @@ -8,7 +8,7 @@ use Tester\Snapshot; require __DIR__ . '/../bootstrap.php'; -Snapshot::$snapshotDir = __DIR__ . '/fixtures'; +Snapshot::$snapshotDir = __DIR__ . DIRECTORY_SEPARATOR . 'fixtures'; Assert::snapshot('existingSnapshot', ['answer' => 42]); diff --git a/tests/Framework/Assert.snapshot.update.phpt b/tests/Framework/Assert.snapshot.update.phpt index 3b7078ad..66d9fba6 100644 --- a/tests/Framework/Assert.snapshot.update.phpt +++ b/tests/Framework/Assert.snapshot.update.phpt @@ -10,27 +10,27 @@ use Tester\Snapshot; require __DIR__ . '/../bootstrap.php'; putenv(Environment::UPDATE_SNAPSHOTS . '=1'); -Snapshot::$snapshotDir = __DIR__ . '/snapshots'; +Snapshot::$snapshotDir = __DIR__ . DIRECTORY_SEPARATOR . 'snapshots'; Helpers::purge(Snapshot::$snapshotDir); // newly created -Assert::false(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.newSnapshot.phps')); +Assert::false(file_exists(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.newSnapshot.phps')); Assert::snapshot('newSnapshot', ['answer' => 42]); -Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.newSnapshot.phps')); -Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Assert.snapshot.update.newSnapshot.phps')); +Assert::true(file_exists(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.newSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.newSnapshot.phps')); // existing file_put_contents( - Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps', + Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.updatedSnapshot.phps', ' 43);' . PHP_EOL ); -Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); +Assert::true(file_exists(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.updatedSnapshot.phps')); Assert::snapshot('updatedSnapshot', ['answer' => 42]); -Assert::true(file_exists(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); -Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . '/Assert.snapshot.update.updatedSnapshot.phps')); +Assert::true(file_exists(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.updatedSnapshot.phps')); +Assert::contains('42', file_get_contents(Snapshot::$snapshotDir . DIRECTORY_SEPARATOR . 'Assert.snapshot.update.updatedSnapshot.phps')); // Snapshot::$updatedSnapshots From 1e60f6925c256733bfc70698fdd3eb1d9785cdec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Wed, 15 Jan 2020 13:17:30 +0100 Subject: [PATCH 08/10] skip test that fails on appveyor due to a PHP bug --- tests/Framework/Assert.snapshot.update.phpt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/Framework/Assert.snapshot.update.phpt b/tests/Framework/Assert.snapshot.update.phpt index 66d9fba6..4244fb14 100644 --- a/tests/Framework/Assert.snapshot.update.phpt +++ b/tests/Framework/Assert.snapshot.update.phpt @@ -9,6 +9,12 @@ use Tester\Snapshot; require __DIR__ . '/../bootstrap.php'; +// https://bugs.php.net/bug.php?id=76801 +// fixed by https://github.com/php/php-src/pull/3965 in PHP 7.2.18 +if (strncasecmp(PHP_OS, 'win', 3) === 0 && strpos(PHP_BINARY, 'phpdbg') !== false && version_compare(PHP_VERSION, '7.2.18') < 0) { + Environment::skip('There is a bug in PHP :('); +} + putenv(Environment::UPDATE_SNAPSHOTS . '=1'); Snapshot::$snapshotDir = __DIR__ . DIRECTORY_SEPARATOR . 'snapshots'; Helpers::purge(Snapshot::$snapshotDir); From c514b2d270fc79b85f6da9711968238f5a0137be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Tue, 8 Sep 2020 12:58:47 +0200 Subject: [PATCH 09/10] mark Snapshot as internal --- src/Framework/Snapshot.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Framework/Snapshot.php b/src/Framework/Snapshot.php index d690f872..c9950226 100644 --- a/src/Framework/Snapshot.php +++ b/src/Framework/Snapshot.php @@ -12,6 +12,7 @@ /** * Snapshot of a tested value. + * @internal */ class Snapshot { From ac2120ed0fc47b7406a212046c4b8cb54be7e53a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pudil?= Date: Thu, 3 Dec 2020 12:39:27 +0100 Subject: [PATCH 10/10] fix cs --- tests/Framework/Assert.snapshot.update.phpt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Framework/Assert.snapshot.update.phpt b/tests/Framework/Assert.snapshot.update.phpt index 4244fb14..d35e723c 100644 --- a/tests/Framework/Assert.snapshot.update.phpt +++ b/tests/Framework/Assert.snapshot.update.phpt @@ -11,7 +11,11 @@ require __DIR__ . '/../bootstrap.php'; // https://bugs.php.net/bug.php?id=76801 // fixed by https://github.com/php/php-src/pull/3965 in PHP 7.2.18 -if (strncasecmp(PHP_OS, 'win', 3) === 0 && strpos(PHP_BINARY, 'phpdbg') !== false && version_compare(PHP_VERSION, '7.2.18') < 0) { +if ( + strncasecmp(PHP_OS, 'win', 3) === 0 + && strpos(PHP_BINARY, 'phpdbg') !== false + && version_compare(PHP_VERSION, '7.2.18') < 0 +) { Environment::skip('There is a bug in PHP :('); }