%PDF- %PDF-
Direktori : /home/vacivi36/vacivitta.com.br/vendor/nexusphp/tachycardia/src/ |
Current File : /home/vacivi36/vacivitta.com.br/vendor/nexusphp/tachycardia/src/Tachycardia.php |
<?php declare(strict_types=1); /** * This file is part of Nexus Tachycardia. * * (c) 2021 John Paul E. Balandan, CPA <paulbalandan@gmail.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace Nexus\PHPUnit\Extension; use Nexus\PHPUnit\Extension\Util\GithubMonitor; use Nexus\PHPUnit\Extension\Util\Parser; use Nexus\PHPUnit\Extension\Util\TimeState; use PHPUnit\Runner\AfterLastTestHook; use PHPUnit\Runner\AfterSuccessfulTestHook; use PHPUnit\Runner\BeforeFirstTestHook; final class Tachycardia implements AfterLastTestHook, AfterSuccessfulTestHook, BeforeFirstTestHook { /** * Whether this extension will monitor slow tests for * rendering later in console. * * This can be controlled by the `TACHYCARDIA_MONITOR` * environment variable. * * @var bool */ private $monitor = true; /** * Whether this extension will monitor slow tests * for inline annotations later in Github Actions. * * This can be controlled by the `TACHYCARDIA_MONITOR_GA` * environment variable. * * @var bool */ private $monitorForGa = false; /** * Default time limit in seconds for each test method. This can be * overridden by providing inline annotations to the doc blocks of * the test methods you wish to override the time limit. * * @var float */ private $timeLimit = 1.00; /** * Number of reportable slow tests in console output. * * @var int */ private $reportable = 10; /** * Degree of precision in the decimals of reported times. * * @var int */ private $precision = 4; /** * Whether to tabulate the results instead of printing plainly. * * @var bool */ private $tabulate = false; /** * Collection of tests which are slow. * * @var array<array{'label':string, 'time':float, 'limit':float}> */ private $slowTests = []; /** * Instance of TimeState. * * @var null|\Nexus\PHPUnit\Extension\Util\TimeState */ private $timeState; /** * Internal count of test suites run. Returning to 0 means the tests * finished running. * * @var int */ private $suites = 0; /** * The current test case being profiled wrapped as a TestCase object. * * @var \Nexus\PHPUnit\Extension\Util\TestCase */ private $testCase; /** * Constructor. * * @param array<string, mixed> $options * * @phpstan-param array{ * 'timeLimit'?:float, * 'reportable'?:int, * 'precision'?:int, * 'tabulate'?:bool, * 'collectBare'?:bool, * } $options */ public function __construct(array $options = []) { $this->monitor = getenv('TACHYCARDIA_MONITOR') !== 'disabled'; $this->monitorForGa = getenv('TACHYCARDIA_MONITOR_GA') === 'enabled'; $this->timeLimit = $options['timeLimit'] ?? 1.00; $this->reportable = $options['reportable'] ?? 10; $this->precision = $options['precision'] ?? 4; $this->tabulate = $options['tabulate'] ?? false; if ($options['collectBare'] ?? false) { $this->timeState = new TimeState(); } } /** * Collects details of successful test runs and picks those * running over the time limits. * * @param string $test Complete name of the test method * @param float $time PHPUnit time in seconds */ public function executeAfterSuccessfulTest(string $test, float $time): void { if (! $this->monitor && ! $this->monitorForGa) { return; } $this->testCase = Parser::getInstance()->parseTest($test); if (isset($this->timeState)) { /** @var float $time */ $time = $this->timeState->find($test, $time); } $label = $this->testCase->getTestName(); $limit = $this->parseTimeLimit(); if (! $this->isProfilingDisabled() && $time >= $limit) { $this->slowTests[] = compact('label', 'time', 'limit'); } } public function executeBeforeFirstTest(): void { if (! $this->monitor && ! $this->monitorForGa) { return; } ++$this->suites; } public function executeAfterLastTest(): void { if (! $this->monitor && ! $this->monitorForGa) { return; } --$this->suites; if (0 === $this->suites && $this->hasSlowTests()) { usort($this->slowTests, static function ($a, $b): int { return $b['time'] <=> $a['time']; }); if ($this->monitor) { $this->render(); } if ($this->monitorForGa && GithubMonitor::runningInGithubActions()) { $monitor = new GithubMonitor($this); echo "\n"; $monitor->defibrillate(); } } } /** * Whether the test suite run has slow tests recorded. */ public function hasSlowTests(): bool { return [] !== $this->slowTests; } /** * Returns the associative array of details of slow tests. * * @return array<array<mixed>> */ public function getSlowTests(): array { return $this->slowTests; } /** * Retrieves the current precision for time presentation. */ public function getPrecision(): int { return $this->precision; } /** * Outputs the slow tests profiling into the console. * * This can be either via plain rendering or using * console tables. */ public function render(): void { $this->renderHeader(); if ($this->tabulate) { $this->renderAsTable(); } else { $this->renderAsPlain(); } $this->renderFooter(); } private function renderHeader(): void { $slow = $this->getReportable(); printf( "\n\n%s identified %s %s:\n", $this->color(self::class, 'green'), 1 === $slow ? 'this' : 'these', $this->color(sprintf('%s slow %s', 1 === $slow ? 'sole' : $slow, 1 === $slow ? 'test' : 'tests'), 'yellow'), ); } private function renderAsTable(): void { $slows = []; $max = ['label' => 9, 'time' => 13, 'limit' => 10]; for ($index = 0; $index < $this->getReportable(); ++$index) { ['label' => $label, 'time' => $time, 'limit' => $limit] = $this->slowTests[$index]; $label = addslashes($label); $time = $this->formTime($time); $limit = $this->formTime($limit); // collect the max length for each column $max['label'] = max($max['label'], \strlen($label)); $max['time'] = max($max['time'], \strlen($time)); $max['limit'] = max($max['limit'], \strlen($limit)); $slows[] = compact('label', 'time', 'limit'); } foreach ($slows as $i => $row) { foreach ($max as $key => $length) { $slows[$i][$key] = $row[$key] . str_repeat(' ', $length - \strlen($row[$key])); } } $table = '+'; foreach ($max as $length) { $table .= str_repeat('-', $length + 2) . '+'; } $table .= "\n"; $body = $footer = $table; $table .= sprintf( "| %s | %s | %s |\n", $this->color('Test Case', 'green') . str_repeat(' ', $max['label'] - 9), $this->color('Time Consumed', 'green') . str_repeat(' ', $max['time'] - 13), $this->color('Time Limit', 'green') . str_repeat(' ', $max['limit'] - 10), ); $table .= $body; foreach ($slows as ['label' => $label, 'time' => $time, 'limit' => $limit]) { $table .= sprintf("| %s | %s | %s |\n", $label, $time, $limit); } $table .= $footer; echo $table; } private function renderAsPlain(): void { for ($index = 0; $index < $this->getReportable(); ++$index) { ['label' => $label, 'time' => $time, 'limit' => $limit] = $this->slowTests[$index]; printf( "%s Took %s from %s limit to run %s\n", $this->color("\xE2\x9A\xA0", 'yellow'), $this->color(number_format($time, $this->precision) . 's', 'yellow'), $this->color(number_format($limit, $this->precision) . 's', 'yellow'), $this->color(addslashes($label), 'green'), ); } } private function renderFooter(): void { $hiddenTests = max(\count($this->slowTests) - $this->reportable, 0); if ($hiddenTests > 0) { printf("...and %s hidden from view.\n", $this->color(sprintf('%s more %s', $hiddenTests, 1 === $hiddenTests ? 'test' : 'tests'), 'yellow')); } } /** * Gets the count of reportable slow tests. */ private function getReportable(): int { return min($this->reportable, \count($this->slowTests)); } /** * Gets the time limit appropriate for the test method. * * Order of precedence: * - method time limit * - class time limit * - default time limit */ private function parseTimeLimit(): float { if ($this->testCase->hasMethodAnnotation('timeLimit')) { $timeLimit = $this->testCase->getMethodAnnotation('timeLimit')[0]; if (is_numeric($timeLimit)) { return (float) $timeLimit; } } if ($this->testCase->hasClassAnnotation('timeLimit')) { $timeLimit = $this->testCase->getClassAnnotation('timeLimit')[0]; if (is_numeric($timeLimit)) { return (float) $timeLimit; } } return $this->timeLimit; } /** * Whether a test case is disabled for profiling, i.e., to * be skipped for analysis. * * Order of precedence * - method @noTimeLimit * - class @noTimeLimit */ private function isProfilingDisabled(): bool { return $this->testCase->hasMethodAnnotation('noTimeLimit') || $this->testCase->hasClassAnnotation('noTimeLimit'); } private function color(string $text, string $color): string { static $colors = [ 'green' => ['open' => 32, 'close' => 39], 'yellow' => ['open' => 33, 'close' => 39], 'bright_green' => ['open' => 92, 'close' => 39], 'bright_yellow' => ['open' => 93, 'close' => 39], ]; return sprintf( "\033[%sm%s\033[%sm", $colors[$color]['open'], $text, $colors[$color]['close'], ); } /** * Takes a timestamp given in `$seconds` and returns a string * in HH:MM:SS form. */ private function formTime(float $seconds): string { $second = fmod($seconds, 60); $second = number_format($second, $this->precision); if (preg_match('/^(\d+)(\.\d+)?/', $second, $matches) === 1) { $second = str_pad($matches[1], 2, '0', STR_PAD_LEFT) . ($matches[2] ?? ''); } $minute = '00'; $hour = '00'; if ($seconds > 60) { $minute = str_pad((string) floor(($seconds % 3600) / 60), 2, '0', STR_PAD_LEFT); } if ($seconds > 3600) { $hour = str_pad((string) floor(($seconds % 86400) / 3600), 2, '0', STR_PAD_LEFT); } return sprintf('%s:%s:%s', $hour, $minute, $second); } }