From 446ab49c5fa5cb19963e165959bee91836feb560 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 3 Nov 2023 22:15:35 -0400 Subject: [PATCH] feat: standalone stress tests --- composer.json | 2 +- src/Blocks/ResponseDuration.php | 12 +-- src/Factory.php | 10 ++- src/ResultPrinters/Progress.php | 128 ++++++++++++++++++++++++++++++++ src/Run.php | 114 +--------------------------- src/ValueObjects/Result.php | 105 +------------------------- src/ValueObjects/Url.php | 8 ++ stress/nunomaduro.com.php | 9 +++ 8 files changed, 168 insertions(+), 220 deletions(-) create mode 100644 src/ResultPrinters/Progress.php create mode 100644 stress/nunomaduro.com.php diff --git a/composer.json b/composer.json index 9cc34f2..4da3859 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,7 @@ "license": "MIT", "require": { "php": "^8.2", - "pestphp/pest": "^2.24.1", + "pestphp/pest": "^2.24.2", "pestphp/pest-plugin": "^2.1.1", "ext-curl": "*" }, diff --git a/src/Blocks/ResponseDuration.php b/src/Blocks/ResponseDuration.php index f00e151..292ceb6 100644 --- a/src/Blocks/ResponseDuration.php +++ b/src/Blocks/ResponseDuration.php @@ -28,9 +28,9 @@ public function value(): string { $array = $this->result->toArray(); - $duration = $array['metrics']['req_connecting']['values']['avg'] - + $array['metrics']['req_tls_handshaking']['values']['avg'] - + $array['metrics']['req_duration']['values']['avg']; + $duration = $array['metrics']['http_req_connecting']['values']['avg'] + + $array['metrics']['http_req_tls_handshaking']['values']['avg'] + + $array['metrics']['http_req_duration']['values']['avg']; return sprintf('%4.2f ms', $duration); } @@ -42,9 +42,9 @@ public function color(): string { $array = $this->result->toArray(); - $duration = $array['metrics']['req_connecting']['values']['avg'] - + $array['metrics']['req_tls_handshaking']['values']['avg'] - + $array['metrics']['req_duration']['values']['avg']; + $duration = $array['metrics']['http_req_connecting']['values']['avg'] + + $array['metrics']['http_req_tls_handshaking']['values']['avg'] + + $array['metrics']['http_req_duration']['values']['avg']; return match (true) { $duration < 200 => 'green', diff --git a/src/Factory.php b/src/Factory.php index 7981080..ffab802 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -7,6 +7,7 @@ use Pest\Stressless\Fluent\WithOptions; use Pest\Stressless\ValueObjects\Result; use Pest\Stressless\ValueObjects\Url; +use Pest\TestSuite; /** * @internal @@ -91,7 +92,7 @@ public function dd(): never { $this->dump(); - exit(1); + exit(0); } /** @@ -137,4 +138,11 @@ public function __get(string $name): mixed { return $this->{$name}(); // @phpstan-ignore-line } + + public function __destruct() + { + if (! $this->result instanceof Result && TestSuite::getInstance()->test === null) { + $this->dd(); + } + } } diff --git a/src/ResultPrinters/Progress.php b/src/ResultPrinters/Progress.php new file mode 100644 index 0000000..61946ba --- /dev/null +++ b/src/ResultPrinters/Progress.php @@ -0,0 +1,128 @@ +url->domain(); + + render(<< + $date + + Running stress test on $domain + + HTML); + + sleep(1); + + $tail = new Process(['tail', '-f', $this->session->progressPath()]); + + $tail + ->setTty(false) + ->setTimeout(null) + ->start(); + + /** @var array $points */ + $points = []; + + $buffer = ''; + $lastTime = null; + while ($this->process->isRunning()) { + $output = $tail->getIncrementalOutput(); + + $output = $buffer.$output; + $buffer = ''; + + $lines = explode("\n", $output); + + foreach ($lines as $line) { + if (str_starts_with($line, '{"metric":"http_req_duration","type":"Point"')) { + /** @var array{data: array{time: string, value: float}}|null $point */ + $point = json_decode($line, true, 512, JSON_THROW_ON_ERROR); + + if (is_array($point)) { + $currentTime = substr($point['data']['time'], 0, 19); + if ($lastTime !== $currentTime) { + $this->printCurrentPoints($points); + $points = []; + + $lastTime = $currentTime; + } + + $points[] = $point; + } else { + $buffer .= $line; + } + } + } + + usleep(100000); // 100ms + } + } + + /** + * Prints the current points. + * + * @param array $points + */ + private function printCurrentPoints(array $points): void + { + static $maxResponseTime; + + if ($points !== []) { + $average = array_sum(array_map(fn ($point): float => $point['data']['value'], $points)) / count($points); + $average = round($average, 2); + + $time = substr($points[0]['data']['time'], 11, 8); + + $width = max(0, terminal()->width()); + $width = $width - 4 - strlen($time); + + if ($maxResponseTime === null) { + $maxResponseTime = max($average * 3, 1000); + } + + $greenDots = (int) (($average * $width) / $maxResponseTime); + + $greenDots = str_repeat('█', $greenDots); + + render(<< + + {$time}│ + $greenDots + + + {$average}ms + + HTML); + } + } +} diff --git a/src/Run.php b/src/Run.php index e7a94e4..431f9e3 100644 --- a/src/Run.php +++ b/src/Run.php @@ -6,15 +6,13 @@ use Pest\Exceptions\ShouldNotHappen; use Pest\Stressless\ResultPrinters\Blocks; +use Pest\Stressless\ResultPrinters\Progress; use Pest\Stressless\ValueObjects\Binary; use Pest\Stressless\ValueObjects\Result; use Pest\Stressless\ValueObjects\Url; use RuntimeException; use Symfony\Component\Process\Process; -use function Termwind\render; -use function Termwind\terminal; - /** * @internal */ @@ -51,7 +49,7 @@ public function start(): Result $process->start(); if ($this->verbose) { - $this->tailProgress($process, $session->progressPath()); + (new Progress($process, $session, $this->url))->tail(); } $process->wait(); @@ -68,7 +66,7 @@ public function start(): Result $metrics = json_decode($summary, true, 512, JSON_THROW_ON_ERROR); assert(is_array($metrics)); - $result = new Result($metrics); + $result = new Result($metrics); // @phpstan-ignore-line if ($this->verbose) { $blocks = new Blocks(); @@ -80,110 +78,4 @@ public function start(): Result return $result; } - - private function tailProgress(Process $process, string $progressPath): void - { - $date = date('H:i:s'); - $url = str_starts_with($this->url, 'http') ? $this->url : 'https://'.$this->url; - $url = explode('//', (string) $url)[1]; - - render(<< - $date - - Running stress test on $url - - HTML); - - sleep(1); - - $tail = new Process(['tail', '-f', $progressPath]); - - $tail - ->setTty(false) - ->setTimeout(null) - ->start(); - - $points = []; - - $buffer = ''; - $lastTime = null; - while ($process->isRunning()) { - $output = $tail->getIncrementalOutput(); - - if (empty($output)) { - continue; - } - - $output = $buffer.$output; - $buffer = ''; - - $lines = explode("\n", $output); - - foreach ($lines as $line) { - if (str_starts_with($line, '{"metric":"http_req_duration","type":"Point"')) { - $decodedLine = json_decode($line, true, 512, JSON_THROW_ON_ERROR); - - if (is_array($decodedLine)) { - $currentTime = substr((string) $decodedLine['data']['time'], 0, 19); - if ($lastTime !== $currentTime) { - $this->printCurrentPoints($points); - $points = []; - - $lastTime = $currentTime; - } - - $points[] = $decodedLine; - } else { - $buffer .= $line; - } - } - } - - usleep(100000); // 100ms - } - } - - private function printCurrentPoints(array $points): void - { - static $maxResponseTime; - - if ($points !== []) { - $average = array_sum(array_map(fn ($point) => $point['data']['value'], $points)) / count($points); - $average = round($average, 2); - - // only time - $time = substr((string) $points[0]['data']['time'], 11, 8); - - $width = max(0, terminal()->width()); - $width = $width - 4 - strlen($time); - - if ($maxResponseTime === null) { - $maxResponseTime = max($average * 3, 1000); - } - - $greenDots = (int) (($average * $width) / $maxResponseTime); - - $greenDots = str_repeat('█', $greenDots); - - render(<< - - {$time}│ - $greenDots - - - {$average}ms - - HTML); - } - } - - /** - * Destroys the run instance. - */ - public function __destruct() - { - // - } } diff --git a/src/ValueObjects/Result.php b/src/ValueObjects/Result.php index 1ddbb9b..5f8d1d1 100644 --- a/src/ValueObjects/Result.php +++ b/src/ValueObjects/Result.php @@ -7,15 +7,11 @@ /** * @internal * - * @property-read float $averageResponseTime - * @property-read float $minResponseTime - * @property-read float $medianResponseTime - * @property-read float $percentile90ResponseTime - * @property-read float $percentile95ResponseTime - * @property-read float $maxResponseTime - * @property-read int $failedRequests + * @property-read float $successRate + * @property-read float $failureRate + * @property-read int $requests * @property-read int $successfulRequests - * @property-read int $totalRequests + * @property-read int $failedRequests */ final readonly class Result { @@ -275,99 +271,6 @@ public function failedRequests(): int return $this->array['metrics']['http_req_failed']['values']['passes']; } - /** - * Returns the average request tls handshaking. - */ - public function averageRequestTlsHandshaking(): float - { - return $this->array['metrics']['http_req_tls_handshaking']['values']['avg']; - } - - /** - * Returns the average request connecting. - */ - public function averageRequestConnecting(): float - { - return $this->array['metrics']['http_req_connecting']['values']['avg']; - } - - /** - * Returns the average request duration. - */ - public function averageRequestDuration(): float - { - return $this->array['metrics']['http_req_duration']['values']['avg']; - } - - /** - * Returns the average request waiting. - */ - public function averageRequestWaiting(): float - { - return $this->array['metrics']['http_req_waiting']['values']['avg']; - } - - /** - * Returns the median response time. - */ - public function minResponseTime(): float - { - return $this->array['metrics']['http_req_duration']['values']['min']; - } - - /** - * Returns the median response time. - */ - public function medianResponseTime(): float - { - return $this->array['metrics']['http_req_duration']['values']['med']; - } - - /** - * Returns the 90th percentile response time. - */ - public function percentile90ResponseTime(): float - { - return $this->array['metrics']['http_req_duration']['values']['p(90)']; - } - - /** - * Returns the 95th percentile response time. - */ - public function percentile95ResponseTime(): float - { - return $this->array['metrics']['http_req_duration']['values']['p(95)']; - } - - /** - * Returns the maximum response time. - */ - public function maxResponseTime(): float - { - return $this->array['metrics']['http_req_duration']['values']['max']; - } - - public function totalRequests(): int - { - return $this->array['metrics']['http_reqs']['values']['count']; - } - - /** - * Returns the average request waiting time. - */ - public function getAverageRequestSending(): float - { - return $this->array['metrics']['http_req_sending']['values']['avg']; - } - - /** - * Returns the data received rate - */ - public function getAverageDataReceived(): float - { - return $this->array['metrics']['data_received']['values']['avg']; - } - /** * Proxies the properties to methods. */ diff --git a/src/ValueObjects/Url.php b/src/ValueObjects/Url.php index a09f524..1ea5352 100644 --- a/src/ValueObjects/Url.php +++ b/src/ValueObjects/Url.php @@ -24,6 +24,14 @@ public function __construct(string $url) $this->url = str_starts_with($url, 'http') ? $url : 'https://'.$url; } + /** + * Gets the domain of the URL. + */ + public function domain(): string + { + return explode('//', $this->url)[1]; + } + /** * The string representation of the URL. */ diff --git a/stress/nunomaduro.com.php b/stress/nunomaduro.com.php new file mode 100644 index 0000000..729f875 --- /dev/null +++ b/stress/nunomaduro.com.php @@ -0,0 +1,9 @@ +with(2)->concurrentRequests() + ->for(3)->seconds();