From 73637ad426be6a0dcf28728bfce012fbb9426896 Mon Sep 17 00:00:00 2001 From: Nuno Maduro Date: Fri, 3 Nov 2023 20:45:53 -0400 Subject: [PATCH] feat: refactors --- bin/run.js | 2 +- composer.json | 8 +- rector.php | 1 + src/Blocks/NetworkDuration.php | 57 +++ src/Blocks/ResponseDuration.php | 55 ++ src/Blocks/ServerDuration.php | 51 ++ src/Blocks/SuccessRate.php | 47 ++ src/Contracts/Block.php | 21 + src/Expectation.php | 2 + src/Factory.php | 47 +- src/Result.php | 303 ----------- src/ResultPrinters/Blocks.php | 176 +++++++ src/Run.php | 156 +++++- src/Session.php | 60 +++ src/{ => ValueObjects}/Binary.php | 4 +- src/ValueObjects/Result.php | 586 ++++++++++++++++++++++ src/ValueObjects/Url.php | 34 ++ tests/Expectations/FailedRequests.php | 23 + tests/Expectations/FailureRate.php | 21 + tests/Expectations/Requests.php | 21 + tests/Expectations/SuccessRate.php | 21 + tests/Expectations/SuccessfulRequests.php | 23 + tests/Feature/Requests.php | 17 - tests/Feature/ResponseTime.php | 13 - tests/Unit/Binary.php | 6 +- 25 files changed, 1398 insertions(+), 357 deletions(-) create mode 100644 src/Blocks/NetworkDuration.php create mode 100644 src/Blocks/ResponseDuration.php create mode 100644 src/Blocks/ServerDuration.php create mode 100644 src/Blocks/SuccessRate.php create mode 100644 src/Contracts/Block.php delete mode 100644 src/Result.php create mode 100644 src/ResultPrinters/Blocks.php create mode 100644 src/Session.php rename src/{ => ValueObjects}/Binary.php (89%) create mode 100644 src/ValueObjects/Result.php create mode 100644 src/ValueObjects/Url.php create mode 100644 tests/Expectations/FailedRequests.php create mode 100644 tests/Expectations/FailureRate.php create mode 100644 tests/Expectations/Requests.php create mode 100644 tests/Expectations/SuccessRate.php create mode 100644 tests/Expectations/SuccessfulRequests.php delete mode 100644 tests/Feature/Requests.php delete mode 100644 tests/Feature/ResponseTime.php diff --git a/bin/run.js b/bin/run.js index d022b90..1b4c9ae 100644 --- a/bin/run.js +++ b/bin/run.js @@ -8,6 +8,6 @@ export default () => { export function handleSummary(data) { return { - 'summary.json': JSON.stringify(data), + [__ENV.PEST_STRESS_TEST_SUMMARY_PATH]: JSON.stringify(data), }; } diff --git a/composer.json b/composer.json index ae7dca0..9cc34f2 100644 --- a/composer.json +++ b/composer.json @@ -13,16 +13,10 @@ "license": "MIT", "require": { "php": "^8.2", - "pestphp/pest": "@dev", + "pestphp/pest": "^2.24.1", "pestphp/pest-plugin": "^2.1.1", "ext-curl": "*" }, - "repositories": [ - { - "type": "path", - "url": "../pest" - } - ], "autoload": { "psr-4": { "Pest\\Stressless\\": "src/" diff --git a/rector.php b/rector.php index 589b8cd..7f2299b 100644 --- a/rector.php +++ b/rector.php @@ -10,6 +10,7 @@ return static function (RectorConfig $rectorConfig): void { $rectorConfig->paths([ __DIR__.'/src', + __DIR__.'/tests', ]); $rectorConfig->rules([ diff --git a/src/Blocks/NetworkDuration.php b/src/Blocks/NetworkDuration.php new file mode 100644 index 0000000..dfb2c68 --- /dev/null +++ b/src/Blocks/NetworkDuration.php @@ -0,0 +1,57 @@ +result->toArray(); + + $duration = $array['metrics']['http_req_connecting']['values']['avg'] + + $array['metrics']['http_req_tls_handshaking']['values']['avg'] + + $array['metrics']['http_req_duration']['values']['avg'] + - $array['metrics']['http_req_waiting']['values']['avg']; + + return sprintf('%4.2f ms', $duration); + } + + /** + * Gets the block color. + */ + public function color(): string + { + $array = $this->result->toArray(); + + $duration = $array['metrics']['http_req_connecting']['values']['avg'] + + $array['metrics']['http_req_tls_handshaking']['values']['avg'] + + $array['metrics']['http_req_duration']['values']['avg'] + - $array['metrics']['http_req_waiting']['values']['avg']; + + return match (true) { + $duration < 100 => 'green', + $duration < 200 => 'yellow', + default => 'red', + }; + } +} diff --git a/src/Blocks/ResponseDuration.php b/src/Blocks/ResponseDuration.php new file mode 100644 index 0000000..f00e151 --- /dev/null +++ b/src/Blocks/ResponseDuration.php @@ -0,0 +1,55 @@ +result->toArray(); + + $duration = $array['metrics']['req_connecting']['values']['avg'] + + $array['metrics']['req_tls_handshaking']['values']['avg'] + + $array['metrics']['req_duration']['values']['avg']; + + return sprintf('%4.2f ms', $duration); + } + + /** + * Gets the block color. + */ + 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']; + + return match (true) { + $duration < 200 => 'green', + $duration < 400 => 'yellow', + default => 'red', + }; + } +} diff --git a/src/Blocks/ServerDuration.php b/src/Blocks/ServerDuration.php new file mode 100644 index 0000000..c650b08 --- /dev/null +++ b/src/Blocks/ServerDuration.php @@ -0,0 +1,51 @@ +result->toArray(); + + $duration = $array['metrics']['http_req_waiting']['values']['avg']; + + return sprintf('%4.2f ms', $duration); + } + + /** + * Gets the block color. + */ + public function color(): string + { + $array = $this->result->toArray(); + + $duration = $array['metrics']['http_req_waiting']['values']['avg']; + + return match (true) { + $duration < 100 => 'green', + $duration < 200 => 'yellow', + default => 'red', + }; + } +} diff --git a/src/Blocks/SuccessRate.php b/src/Blocks/SuccessRate.php new file mode 100644 index 0000000..67752b2 --- /dev/null +++ b/src/Blocks/SuccessRate.php @@ -0,0 +1,47 @@ +result->toArray(); + + $percentage = (float) ($array['metrics']['http_req_failed']['values']['fails'] * 100 / $array['metrics']['http_reqs']['values']['count']); + + return sprintf('%4.1f %%', $percentage); + } + + /** + * Gets the block color. + */ + public function color(): string + { + $array = $this->result->toArray(); + + return $array['metrics']['http_req_failed']['values']['fails'] === $array['metrics']['http_reqs']['values']['count'] + ? 'green' + : 'red'; + } +} diff --git a/src/Contracts/Block.php b/src/Contracts/Block.php new file mode 100644 index 0000000..577bdff --- /dev/null +++ b/src/Contracts/Block.php @@ -0,0 +1,21 @@ +result = (new Run($this->url, $this->options))->start(); + return $this->result = (new Run( + new Url($this->url), + $this->options, + $this->verbose, + ))->start(); + } + + /** + * Specifies that the run should be verbose, and then exits. + */ + public function dd(): never + { + $this->dump(); + + exit(1); + } + + /** + * Specifies that the run should be verbose. + */ + public function dump(): self + { + $this->verbosely(); + + $this->run(); + + return $this; + } + + /** + * Specifies that the run should be verbose. + */ + public function verbosely(): self + { + $this->verbose = true; + + return $this; } /** @@ -80,7 +123,7 @@ public function run(): Result */ public function __call(string $name, array $arguments): mixed { - if (! $this->result instanceof \Pest\Stressless\Result) { + if (! $this->result instanceof Result) { $this->run(); } diff --git a/src/Result.php b/src/Result.php deleted file mode 100644 index aec6c43..0000000 --- a/src/Result.php +++ /dev/null @@ -1,303 +0,0 @@ -array['metrics']['http_req_duration']['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 failedRequests(): int - { - return $this->array['metrics']['http_req_failed']['values']['passes']; - } - - public function successfulRequests(): int - { - return $this->array['metrics']['http_req_failed']['values']['fails']; - } - - public function totalRequests(): int - { - return $this->array['metrics']['http_reqs']['values']['count']; - } - - /** - * Proxies the properties to methods. - */ - public function __get(string $name): mixed - { - return $this->{$name}(); // @phpstan-ignore-line - } -} diff --git a/src/ResultPrinters/Blocks.php b/src/ResultPrinters/Blocks.php new file mode 100644 index 0000000..237498b --- /dev/null +++ b/src/ResultPrinters/Blocks.php @@ -0,0 +1,176 @@ +%block_size% + %success_rate% + <%success_rate_color%>%block_size% + EOD; + + private const RESPONSE_DURATION = <<<'EOD' + <%response_time_color%>%block_size% + %response_time% + <%response_time_color%>%block_size% + EOD; + + private const NETWORK_DURATION = <<<'EOD' + <%ttfb_color%>%block_size% + %ttfb% + <%ttfb_color%>%block_size% + EOD; + + private const SERVER_DURATION = <<<'EOD' + <%server_duration_color%>%block_size% + %server_duration% + <%server_duration_color%>%block_size% + EOD; + + public function print(Result $result): void + { + render(<<<'HTML' +
+ + 0-49 + + 50-89 + + 90-100 +
+ HTML); + + $successRate = new SuccessRate($result); + $responseDuration = new ResponseDuration($result); + $networkDuration = new NetworkDuration($result); + $serverDuration = new ServerDuration($result); + + $templates = [ + '%success_rate%' => $successRate->value(), + '%success_rate_color%' => "bg={$successRate->color()}", + '%response_time%' => $responseDuration->value(), + '%response_time_color%' => "bg={$responseDuration->color()}", + '%ttfb%' => $networkDuration->value(), + '%ttfb_color%' => "bg={$networkDuration->color()}", + '%server_duration%' => $serverDuration->value(), + '%server_duration_color%' => "bg={$serverDuration->color()}", + '%subtitle%' => 'fg=white;options=bold;fg=white', + ]; + $disposition = self::ALL_BLOCKS_IN_ROW; + $spaceWidth = $this->getSpaceWidth(terminal()->width(), self::BLOCK_SIZE, $disposition); + + if (terminal()->width() < ((self::BLOCK_SIZE * $disposition) + 5 * $spaceWidth)) { + $disposition = self::TWO_BLOCKS_IN_ROW; + $spaceWidth = $this->getSpaceWidth(terminal()->width(), self::BLOCK_SIZE, $disposition); + } + + $templates = [...$templates, '%block_size%' => str_pad('', self::BLOCK_SIZE)]; + + $styleDefinition = clone Table::getStyleDefinition('compact'); + + $styleDefinition->setVerticalBorderChars( + str_pad('', (int) floor($spaceWidth / 2)), // outside + '' // inside + ); + + $styleDefinition->setPadType(STR_PAD_BOTH); + $styleDefinition->setCellRowContentFormat('%s'); + + $table = new Table(new ConsoleOutput()); + $table->setStyle($styleDefinition); + + $table->setColumnWidth(0, self::BLOCK_SIZE + $spaceWidth); + $table->setColumnWidth(1, self::BLOCK_SIZE + $spaceWidth); + $table->setColumnWidth(2, self::BLOCK_SIZE + $spaceWidth); + $table->setColumnWidth(3, self::BLOCK_SIZE + $spaceWidth); + + if ($disposition === self::ALL_BLOCKS_IN_ROW) { + $table->setRows([ + [ + strtr(self::SUCCESS_RATE, $templates), + strtr(self::RESPONSE_DURATION, $templates), + strtr(self::NETWORK_DURATION, $templates), + strtr(self::SERVER_DURATION, $templates), + ], + ['', '', '', ''], + [ + strtr('<%subtitle%> Success Rate ', $templates), + strtr('<%subtitle%> Response ', $templates), + strtr('<%subtitle%> Network ', $templates), + strtr('<%subtitle%> Server ', $templates), + ], + ]); + } + + if ($disposition === self::TWO_BLOCKS_IN_ROW) { + $table->setRows([ + [ + strtr(self::SUCCESS_RATE, $templates), + strtr(self::RESPONSE_DURATION, $templates), + ], + ['', ''], + [ + strtr('<%subtitle%> Success Rate ', $templates), + strtr('<%subtitle%> Response ', $templates), + ], + ['', ''], + [ + strtr(self::NETWORK_DURATION, $templates), + strtr(self::SERVER_DURATION, $templates), + ], + ['', ''], + [ + strtr('<%subtitle%> Network ', $templates), + strtr('<%subtitle%> Server ', $templates), + ], + ]); + } + + $table->render(); + } + + /** + * Total width of terminal - block size * disposition (4 or 2) / number of space block. + */ + private function getSpaceWidth(int $totalWidth, int $blockSize, int $disposition): int + { + $spaceWidth = (int) floor(($totalWidth - $blockSize * $disposition) / ($disposition + 1)); + + if ($spaceWidth > self::MAX_SPACEWIDTH) { + $spaceWidth = self::MAX_SPACEWIDTH; + } + + if ($spaceWidth < self::MIN_SPACEWIDTH) { + return self::MIN_SPACEWIDTH; + } + + return $spaceWidth; + } +} diff --git a/src/Run.php b/src/Run.php index fc00fc6..e7a94e4 100644 --- a/src/Run.php +++ b/src/Run.php @@ -4,8 +4,17 @@ namespace Pest\Stressless; +use Pest\Exceptions\ShouldNotHappen; +use Pest\Stressless\ResultPrinters\Blocks; +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 */ @@ -16,7 +25,7 @@ * * @param array $options */ - public function __construct(private string $url, private array $options) + public function __construct(private Url $url, private array $options, private bool $verbose) { // } @@ -26,28 +35,155 @@ public function __construct(private string $url, private array $options) */ public function start(): Result { - $basePath = dirname(__DIR__); - - $url = is_int(preg_match('/^https?:\/\//', $this->url)) ? $this->url : 'https://'.$this->url; + $session = new Session( + $basePath = dirname(__DIR__), + uniqid('pest', true), + ); $process = new Process([ - 'k6', 'run', 'run.js', + Binary::k6(), 'run', 'run.js', '--out', "json={$session->progressPath()}", ], $basePath.'/bin', [ 'PEST_STRESS_TEST_OPTIONS' => json_encode($this->options, JSON_THROW_ON_ERROR), - 'PEST_STRESS_TEST_URL' => $url, + 'PEST_STRESS_TEST_URL' => $this->url, + 'PEST_STRESS_TEST_SUMMARY_PATH' => $session->summaryPath(), ]); - $process->run(); + $process->start(); + + if ($this->verbose) { + $this->tailProgress($process, $session->progressPath()); + } + + $process->wait(); + if (! $process->isSuccessful()) { - dd($process->getErrorOutput()); + throw new ShouldNotHappen( + new RuntimeException(sprintf('The underlying K6 process failed with the following error: %s', $process->getErrorOutput())), + ); } - $summary = file_get_contents($basePath.'/bin/summary.json'); + $summary = file_get_contents($session->summaryPath()); assert(is_string($summary)); $metrics = json_decode($summary, true, 512, JSON_THROW_ON_ERROR); assert(is_array($metrics)); - return new Result($metrics); // @phpstan-ignore-line + $result = new Result($metrics); + + if ($this->verbose) { + $blocks = new Blocks(); + + $blocks->print($result); + } + + $session->clean(); + + 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/Session.php b/src/Session.php new file mode 100644 index 0000000..01e6ca4 --- /dev/null +++ b/src/Session.php @@ -0,0 +1,60 @@ +basePath, $this->id, 'summary'); + } + + /** + * Gets the session's progress file path. + */ + public function progressPath(): string + { + return sprintf(self::PATH, $this->basePath, $this->id, 'progress'); + } + + /** + * Cleans the session files, if any. + */ + public function clean(): void + { + if (file_exists($this->progressPath())) { + unlink($this->progressPath()); + } + if (file_exists($this->summaryPath())) { + unlink($this->summaryPath()); + } + } + + /** + * Destroys the session instance. + */ + public function __destruct() + { + $this->clean(); + } +} diff --git a/src/Binary.php b/src/ValueObjects/Binary.php similarity index 89% rename from src/Binary.php rename to src/ValueObjects/Binary.php index 4daea4d..3949ece 100644 --- a/src/Binary.php +++ b/src/ValueObjects/Binary.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Pest\Stressless; +namespace Pest\Stressless\ValueObjects; use RuntimeException; use Stringable; @@ -39,7 +39,7 @@ public static function k6(): self default => throw new RuntimeException('Unsupported OS.'), }; - return new self((string) realpath(__DIR__.'/../bin/'.$path)); + return new self((string) realpath(__DIR__.'/../../bin/'.$path)); } /** diff --git a/src/ValueObjects/Result.php b/src/ValueObjects/Result.php new file mode 100644 index 0000000..1ddbb9b --- /dev/null +++ b/src/ValueObjects/Result.php @@ -0,0 +1,586 @@ +array['metrics']['http_req_failed']['values']['fails']; + $totalRequests = $this->array['metrics']['http_reqs']['values']['count']; + + $percentage = (float) ($successfulRequests * 100 / $totalRequests); + + return min(max(0.00, $percentage), 100.00); + } + + /** + * Gets the rate of failed requests, as a percentage, between "0.00" and "100.00". + */ + public function failureRate(): float + { + return 100.00 - $this->successRate(); + } + + /** + * Gets the total number of requests. + */ + public function requests(): int + { + return $this->array['metrics']['http_reqs']['values']['count']; + } + + /** + * Gets the total number of successful requests. + */ + public function successfulRequests(): int + { + return $this->array['metrics']['http_req_failed']['values']['fails']; + } + + /** + * Gets the total number of failed requests. + */ + 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. + */ + public function __get(string $name): mixed + { + return $this->{$name}(); // @phpstan-ignore-line + } + + /** + * Returns the raw array. + * + * @return array{ + * root_group: array{ + * name: string, + * path: string, + * id: string + * }, + * options: array{ + * summaryTimeUnit: string, + * noColor: bool, + * summaryTrendStats: array{ + * 0: string, + * 1: string, + * 2: string, + * 3: string, + * 4: string, + * 5: string + * } + * }, + * state: array{ + * isStdOutTTY: bool, + * isStdErrTTY: bool, + * testRunDurationMs: float + * }, + * metrics: array{ + * http_req_duration: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * http_req_receiving: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * http_req_failed: array{ + * type: string, + * contains: string, + * values: array{ + * rate: int, + * passes: int, + * fails: int + * } + * }, + * http_req_waiting: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * iteration_duration: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * http_req_blocked: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * vus: array{ + * type: string, + * contains: string, + * values: array{ + * value: int, + * min: int, + * max: int + * } + * }, + * http_req_sending: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * http_req_connecting: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * data_sent: array{ + * type: string, + * contains: string, + * values: array{ + * count: int, + * rate: float + * } + * }, + * data_received: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * vus_max: array{ + * type: string, + * contains: string, + * values: array{ + * value: int, + * min: int, + * max: int + * } + * }, + * http_reqs: array{ + * type: string, + * contains: string, + * values: array{ + * count: int, + * rate: float + * } + * }, + * iterations: array{ + * type: string, + * contains: string, + * values: array{ + * count: int, + * rate: float + * } + * }, + * http_req_tls_handshaking: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * http_req_duration: array{ + * type: string, + * contains: string, + * values: array{ + * "p(95)": float, + * avg: float, + * min: float, + * med: float, + * max: float, + * "p(90)": float + * } + * }, + * } + * } + * / + * / + */ + public function toArray(): array + { + return $this->array; + } +} diff --git a/src/ValueObjects/Url.php b/src/ValueObjects/Url.php new file mode 100644 index 0000000..a09f524 --- /dev/null +++ b/src/ValueObjects/Url.php @@ -0,0 +1,34 @@ +url = str_starts_with($url, 'http') ? $url : 'https://'.$url; + } + + /** + * The string representation of the URL. + */ + public function __toString(): string + { + return $this->url; + } +} diff --git a/tests/Expectations/FailedRequests.php b/tests/Expectations/FailedRequests.php new file mode 100644 index 0000000..c300a5c --- /dev/null +++ b/tests/Expectations/FailedRequests.php @@ -0,0 +1,23 @@ +with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failedRequests())->toBe(0); +}); + +it('may fail', function (): void { + $result = stress('dummy-nunomaduro.com') + ->with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failedRequests()) + ->toBeGreaterThan(2) + ->toBe($result->requests()); +}); diff --git a/tests/Expectations/FailureRate.php b/tests/Expectations/FailureRate.php new file mode 100644 index 0000000..e8e8f68 --- /dev/null +++ b/tests/Expectations/FailureRate.php @@ -0,0 +1,21 @@ +with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failureRate())->toBe(0.0); +}); + +it('may fail', function (): void { + $result = stress('dummy-nunomaduro.com') + ->with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failureRate())->toBe(100.0); +}); diff --git a/tests/Expectations/Requests.php b/tests/Expectations/Requests.php new file mode 100644 index 0000000..e148e2e --- /dev/null +++ b/tests/Expectations/Requests.php @@ -0,0 +1,21 @@ +with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->requests())->toBeGreaterThan(2); +}); + +it('may fail', function (): void { + $result = stress('dummy-nunomaduro.com') + ->with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->requests())->toBeGreaterThan(0); +}); diff --git a/tests/Expectations/SuccessRate.php b/tests/Expectations/SuccessRate.php new file mode 100644 index 0000000..e8e8f68 --- /dev/null +++ b/tests/Expectations/SuccessRate.php @@ -0,0 +1,21 @@ +with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failureRate())->toBe(0.0); +}); + +it('may fail', function (): void { + $result = stress('dummy-nunomaduro.com') + ->with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->failureRate())->toBe(100.0); +}); diff --git a/tests/Expectations/SuccessfulRequests.php b/tests/Expectations/SuccessfulRequests.php new file mode 100644 index 0000000..69e2ef4 --- /dev/null +++ b/tests/Expectations/SuccessfulRequests.php @@ -0,0 +1,23 @@ +with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->successfulRequests()) + ->toBeGreaterThan(2) + ->toBe($result->requests()); +}); + +it('may fail', function (): void { + $result = stress('dummy-nunomaduro.com') + ->with(2)->concurrentRequests() + ->for(1)->second(); + + expect($result->successfulRequests())->toBe(0); +}); diff --git a/tests/Feature/Requests.php b/tests/Feature/Requests.php deleted file mode 100644 index e42943e..0000000 --- a/tests/Feature/Requests.php +++ /dev/null @@ -1,17 +0,0 @@ -with(2)->concurrentRequests() - ->for(2)->seconds(); - - expect($result->totalRequests()) - ->toBeInt() - ->toBeGreaterThan(0) - ->toBe($result->successfulRequests()) - ->and($result->failedRequests)->toBe(0); -}); diff --git a/tests/Feature/ResponseTime.php b/tests/Feature/ResponseTime.php deleted file mode 100644 index 8074a4c..0000000 --- a/tests/Feature/ResponseTime.php +++ /dev/null @@ -1,13 +0,0 @@ -with(10)->concurrentRequests() - ->for(2)->seconds(); - - expect($result->averageResponseTime())->toBeLessThan(1000); -}); diff --git a/tests/Unit/Binary.php b/tests/Unit/Binary.php index 8ef3648..1c4d7be 100644 --- a/tests/Unit/Binary.php +++ b/tests/Unit/Binary.php @@ -1,8 +1,10 @@ toBe(realpath(__DIR__.'/../../bin/k6-macos-arm64'));