├── LICENSE.md ├── README.md ├── composer.json └── src ├── Configuration.php ├── DataSourceConfiguration.php ├── Exceptions ├── ErrorHandler.php ├── InvalidJsonPathException.php ├── InvalidRegexException.php ├── InvalidRequestValidationException.php ├── InvalidStepException.php └── VoltTestException.php ├── Extractors ├── CookieExtractor.php ├── Extractor.php ├── HeaderExtractor.php ├── HtmlExtractor.php ├── JsonExtractor.php └── RegexExtractor.php ├── Platform.php ├── ProcessManager.php ├── Request.php ├── Scenario.php ├── Step.php ├── TestResult.php ├── TestableProcessManager.php ├── Validators ├── StatusValidator.php └── Validator.php └── VoltTest.php /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 VoltTest PHP SDK 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VoltTest PHP SDK 2 | 3 | VoltTest is a high-performance PHP performance testing SDK powered by a Golang engine. 4 | It combines PHP’s simplicity with Go’s speed and concurrency, allowing you to define, run, and analyze tests with an intuitive API while leveraging Go for efficient load generation. 5 | 6 | ## Features 7 | - [x] **Multiple Scenario Support with Weights** – Run different test scenarios with custom weight distributions. 8 | - [x] **Data Provider for Virtual Users** - Assign dynamic data to virtual users for realistic test simulations. 9 | - [x] **Extract Data from Requests** – Capture and reuse response data in subsequent requests. 10 | - [x] **Request Customization & Response Validation** – Modify headers, payloads, and assert results. 11 | - [x] **Think Time & Ramp-Up Configuration** – Simulate real-user behavior. 12 | - [x] **Detailed Reports & Distributed Execution** – Scale tests and analyze results. 13 | - [x] **Debug Requests** - Inspect and troubleshoot request/response payloads easily. 14 | - [ ] **Cloud Execution** – Seamless cloud-based testing in progress. 15 | 16 | 17 | ## Architecture 18 | VoltTest PHP SDK works as a bridge between your PHP application and the VoltTest Engine (written in Go). When you run a test: 19 | 20 | Your PHP code defines the test scenarios and configurations 21 | The SDK transforms these into a format the Go engine understands 22 | The Go engine executes the actual load testing 23 | Results are streamed back to your PHP application for analysis 24 | 25 | This architecture provides several benefits: 26 | 27 | Write tests in PHP while getting Go's performance benefits 28 | True parallel execution of virtual users 29 | Minimal resource footprint during test execution 30 | Accurate timing and metrics collection 31 | 32 | ## Documentation 33 | 34 | For detailed documentation, visit [https://php.volt-test.com](https://php.volt-test.com) 35 | 36 | ## Requirements 37 | 38 | - PHP 8.0 or higher 39 | - ext-json 40 | - ext-pcntl 41 | - ext-curl 42 | 43 | ## License 44 | 45 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 46 | 47 | For more examples and detailed documentation, visit [https://php.volt-test.com](https://php.volt-test.com) 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "volt-test/php-sdk", 3 | "description": "Volt Test PHP SDK - A performance testing tool for PHP applications", 4 | "type": "library", 5 | "version": "1.1.0", 6 | "keywords": [ 7 | "volt-test", 8 | "php-sdk", 9 | "performance-testing", 10 | "load-testing", 11 | "stress-testing", 12 | "http" 13 | ], 14 | "homepage": "https://php.volt-test.com", 15 | "license": "MIT", 16 | "support": { 17 | "issues": "https://github.com/volt-test/php-sdk/issues", 18 | "source": "https://github.com/volt-test/php-sdk/" 19 | }, 20 | "authors": [ 21 | { 22 | "name": "elwafa", 23 | "email": "islam@volt-test.com" 24 | } 25 | ], 26 | "require": { 27 | "php": "^8.0", 28 | "ext-json": "*", 29 | "ext-curl": "*", 30 | "ext-pcntl": "*" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "VoltTest\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Tests\\": "tests/" 40 | } 41 | }, 42 | "scripts": { 43 | "post-install-cmd": [ 44 | "VoltTest\\Platform::installBinary" 45 | ], 46 | "post-update-cmd": [ 47 | "VoltTest\\Platform::installBinary" 48 | ], 49 | "volt-test": [ 50 | "VoltTest\\Platform::installBinary" 51 | ], 52 | "test": "phpunit", 53 | "cs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --diff", 54 | "cs-check": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix --dry-run --diff", 55 | "analyze": "phpstan analyze", 56 | "check": [ 57 | "@cs-check", 58 | "@analyze", 59 | "@test" 60 | ] 61 | }, 62 | "config": { 63 | "bin-dir": "vendor/bin" 64 | }, 65 | "require-dev": { 66 | "phpunit/phpunit": "^11.5", 67 | "phpstan/phpstan": "^1.10", 68 | "friendsofphp/php-cs-fixer": "^3.51" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Configuration.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->description = $description; 27 | $this->virtualUsers = 1; 28 | $this->duration = ''; 29 | $this->rampUp = ''; 30 | $this->target = [ 31 | 'url' => 'https://example.com', 32 | 'idle_timeout' => '30s', 33 | ]; 34 | } 35 | 36 | public function toArray(): array 37 | { 38 | $array = [ 39 | 'name' => $this->name, 40 | 'description' => $this->description, 41 | 'virtual_users' => $this->virtualUsers, 42 | 'target' => $this->target, 43 | 'http_debug' => $this->httpDebug, 44 | ]; 45 | if (trim($this->rampUp) !== '') { 46 | $array['ramp_up'] = $this->rampUp; 47 | } 48 | if (trim($this->duration) !== '') { 49 | $array['duration'] = $this->duration; 50 | } 51 | 52 | return $array; 53 | } 54 | 55 | public function setVirtualUsers(int $count): self 56 | { 57 | if ($count < 1) { 58 | throw new VoltTestException('Virtual users count must be at least 1'); 59 | } 60 | $this->virtualUsers = $count; 61 | 62 | return $this; 63 | } 64 | 65 | public function setDuration(string $duration): self 66 | { 67 | if (! preg_match('/^\d+[smh]$/', $duration)) { 68 | throw new VoltTestException('Invalid duration format. Use [s|m|h]'); 69 | } 70 | $this->duration = $duration; 71 | 72 | return $this; 73 | } 74 | 75 | public function setRampUp(string $rampUp): self 76 | { 77 | if (! preg_match('/^\d+[smh]$/', $rampUp)) { 78 | throw new VoltTestException('Invalid ramp-up format. Use [s|m|h]'); 79 | } 80 | $this->rampUp = $rampUp; 81 | 82 | return $this; 83 | } 84 | 85 | public function setTarget(string $idleTimeout = '30s'): self 86 | { 87 | if (! preg_match('/^\d+[smh]$/', $idleTimeout)) { 88 | throw new VoltTestException('Invalid idle timeout format. Use [s|m|h]'); 89 | } 90 | $this->target['idle_timeout'] = $idleTimeout; 91 | 92 | return $this; 93 | } 94 | 95 | public function setHttpDebug(bool $httpDebug): self 96 | { 97 | $this->httpDebug = $httpDebug; 98 | 99 | return $this; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/DataSourceConfiguration.php: -------------------------------------------------------------------------------- 1 | dataSource = $dataSource; 18 | $this->mode = $mode; 19 | $this->hasHeader = $hasHeader; 20 | } 21 | 22 | /** 23 | * @throws VoltTestException 24 | */ 25 | public function toArray(): array 26 | { 27 | $this->validate(); 28 | 29 | return [ 30 | 'data_source' => realpath($this->dataSource), 31 | 'data_format' => 'csv', 32 | 'has_header' => $this->hasHeader, 33 | 'mode' => $this->mode, 34 | ]; 35 | } 36 | 37 | public function validate(): void 38 | { 39 | if (! file_exists($this->dataSource)) { 40 | throw new VoltTestException("Data source file '{$this->dataSource}' does not exist"); 41 | } 42 | 43 | if (! in_array($this->mode, ['sequential', 'random','unique'])) { 44 | throw new VoltTestException('Invalid data source mode. Use "sequential", "random" or "unique"'); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exceptions/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 16 | $this->selector = $selector; 17 | } 18 | 19 | public function toArray(): array 20 | { 21 | if (! $this->validate()) { 22 | throw new VoltTestException('Invalid Cookie Extractor'); 23 | } 24 | 25 | return [ 26 | 'variable_name' => $this->variableName, 27 | 'selector' => $this->selector, 28 | 'type' => 'cookie', 29 | ]; 30 | } 31 | 32 | public function validate(): bool 33 | { 34 | return trim($this->selector) !== '' && trim($this->variableName) !== ''; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Extractors/Extractor.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 16 | $this->selector = $selector; 17 | } 18 | 19 | public function toArray(): array 20 | { 21 | if (! $this->validate()) { 22 | throw new VoltTestException('Invalid Header Extractor'); 23 | } 24 | 25 | return [ 26 | 'variable_name' => $this->variableName, 27 | 'selector' => $this->selector, 28 | 'type' => 'header', 29 | ]; 30 | } 31 | 32 | public function validate(): bool 33 | { 34 | if (trim($this->variableName) === '') { 35 | return false; 36 | } 37 | 38 | // Check for empty or whitespace-only selector 39 | if (trim($this->selector) === '') { 40 | return false; 41 | } 42 | 43 | // Header names should match RFC 7230 requirements 44 | // Only alphanumeric characters, hyphens, and underscores are allowed 45 | // Must not start with a number and no spaces allowed 46 | return (bool) preg_match('/^[a-zA-Z][a-zA-Z0-9_-]*$/', $this->selector); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Extractors/HtmlExtractor.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 19 | $this->selector = $selector; 20 | $this->attribute = $attribute; 21 | } 22 | 23 | public function toArray(): array 24 | { 25 | if (! $this->validate()) { 26 | throw new VoltTestException('Invalid HTML Extractor'); 27 | } 28 | 29 | return [ 30 | 'variable_name' => $this->variableName, 31 | 'selector' => $this->selector, 32 | 'attribute' => $this->attribute, 33 | 'type' => 'html', 34 | ]; 35 | } 36 | 37 | public function validate(): bool 38 | { 39 | if (trim($this->variableName) === '') { 40 | return false; 41 | } 42 | 43 | // Check for empty or whitespace-only selector 44 | if (trim($this->selector) === '') { 45 | return false; 46 | } 47 | 48 | // Check for empty or whitespace-only attribute 49 | if ($this->attribute !== null && trim($this->attribute) === '') { 50 | return false; 51 | } 52 | 53 | return true; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Extractors/JsonExtractor.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 16 | $this->selector = '$.' . $jsonPath; 17 | } 18 | 19 | /* 20 | * Convert the object to an array 21 | * @return array 22 | * @throws InvalidJsonPathException 23 | * 24 | * */ 25 | public function toArray(): array 26 | { 27 | $this->validate(); 28 | 29 | return [ 30 | 'variable_name' => $this->variableName, 31 | 'selector' => $this->selector, 32 | 'type' => 'json', 33 | ]; 34 | } 35 | 36 | /* 37 | * Validate the selector and the content 38 | * @return bool 39 | * @throws InvalidJsonPathException 40 | * */ 41 | public function validate(): bool 42 | { 43 | // Validate the selector data 44 | if (empty($this->selector) || $this->selector === '$.') { 45 | throw new InvalidJsonPathException('JSON path cannot be empty'); 46 | } 47 | // Validate the selector ex: $.meta.token or $.data[0].name 48 | // Validate the selector follows proper JSON path format 49 | // Should start with $ followed by dot and valid path segments 50 | $pattern = '/^\$(\.[a-zA-Z0-9_]+|\[[0-9]+\])*$/'; 51 | if (! preg_match($pattern, $this->selector)) { 52 | throw new InvalidJsonPathException('Invalid JSON path'); 53 | } 54 | 55 | return true; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Extractors/RegexExtractor.php: -------------------------------------------------------------------------------- 1 | variableName = $variableName; 16 | $this->selector = $selector; 17 | } 18 | 19 | /* 20 | * Convert the object to an array 21 | * @return array 22 | * @throws InvalidRegexException 23 | */ 24 | public function toArray(): array 25 | { 26 | if (! $this->validate()) { 27 | throw new InvalidRegexException('Invalid regex selector or variable name'); 28 | } 29 | 30 | return [ 31 | 'variable_name' => $this->variableName, 32 | 'selector' => $this->selector, 33 | 'type' => 'regex', 34 | ]; 35 | } 36 | 37 | /* 38 | * Validate the selector and the content 39 | * @return bool 40 | * */ 41 | public function validate(): bool 42 | { 43 | return trim($this->selector) !== '' && trim($this->variableName) !== ''; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Platform.php: -------------------------------------------------------------------------------- 1 | 'volt-test-linux-amd64', 13 | 'linux-arm64' => 'volt-test-linux-arm64', 14 | 'darwin-amd64' => 'volt-test-darwin-amd64', 15 | 'darwin-arm64' => 'volt-test-darwin-arm64', 16 | ]; 17 | 18 | private static function getVendorDir(): string 19 | { 20 | // First try using Composer's environment variable 21 | if (getenv('COMPOSER_VENDOR_DIR')) { 22 | return rtrim(getenv('COMPOSER_VENDOR_DIR'), '/\\'); 23 | } 24 | 25 | // Then try using Composer's home directory 26 | if (getenv('COMPOSER_HOME')) { 27 | $vendorDir = rtrim(getenv('COMPOSER_HOME'), '/\\') . '/vendor'; 28 | if (is_dir($vendorDir)) { 29 | return $vendorDir; 30 | } 31 | } 32 | 33 | // Try to find vendor directory relative to current file 34 | $paths = [ 35 | __DIR__ . '/../../../', // From src/VoltTest to vendor 36 | __DIR__ . '/vendor/', // Direct vendor subdirectory 37 | dirname(__DIR__, 2) . '/vendor/', // Two levels up 38 | dirname(__DIR__, 3) . '/vendor/', // Three levels up 39 | dirname(__DIR__) . '/vendor/', 40 | ]; 41 | 42 | foreach ($paths as $path) { 43 | if (file_exists($path . 'autoload.php')) { 44 | return rtrim($path, '/\\'); 45 | } 46 | } 47 | 48 | // If running as a Composer script, use the working directory 49 | if (getenv('COMPOSER')) { 50 | $path = getcwd() . '/vendor'; 51 | if (is_dir($path)) { 52 | return $path; 53 | } 54 | } 55 | 56 | throw new \RuntimeException( 57 | 'Could not locate Composer vendor directory. ' . 58 | 'Please ensure you are installing through Composer.' 59 | ); 60 | } 61 | 62 | private static function getBinaryDir(): string 63 | { 64 | return self::getVendorDir() . '/bin'; 65 | } 66 | 67 | private static function getCurrentVersion(): string 68 | { 69 | return self::ENGINE_CURRENT_VERSION; 70 | } 71 | 72 | private static function detectPlatform($testing = false): string 73 | { 74 | if ($testing === true) { 75 | return 'unsupported-platform'; 76 | } 77 | $os = strtolower(PHP_OS); 78 | $arch = php_uname('m'); 79 | 80 | if (strpos($os, 'win') === 0) { 81 | $os = 'windows'; 82 | } elseif (strpos($os, 'darwin') === 0) { 83 | $os = 'darwin'; 84 | } elseif (strpos($os, 'linux') === 0) { 85 | $os = 'linux'; 86 | } 87 | 88 | if ($arch === 'x86_64') { 89 | $arch = 'amd64'; 90 | } elseif ($arch === 'arm64' || $arch === 'aarch64') { 91 | $arch = 'arm64'; 92 | } 93 | 94 | return "$os-$arch"; 95 | } 96 | 97 | public static function installBinary($testing = false): void 98 | { 99 | $platform = self::detectPlatform($testing); 100 | 101 | if (! array_key_exists($platform, self::SUPPORTED_PLATFORMS)) { 102 | throw new \RuntimeException("Platform $platform is not supported"); 103 | } 104 | 105 | $version = self::getCurrentVersion(); 106 | $binaryName = self::SUPPORTED_PLATFORMS[$platform]; 107 | $downloadUrl = sprintf('%s/%s/%s', self::BASE_DOWNLOAD_URL, $version, $binaryName); 108 | 109 | $binDir = self::getBinaryDir(); 110 | $targetFile = $binDir . '/' . self::BINARY_NAME; 111 | if (PHP_OS_FAMILY === 'Windows') { 112 | $targetFile .= '.exe'; 113 | } 114 | 115 | if (! is_dir($binDir)) { 116 | if (! mkdir($binDir, 0755, true)) { 117 | throw new \RuntimeException("Failed to create directory: $binDir"); 118 | } 119 | } 120 | 121 | echo "Downloading VoltTest binary $version for platform: $platform\n"; 122 | 123 | $tempFile = tempnam(sys_get_temp_dir(), 'volt-test-download-'); 124 | if ($tempFile === false) { 125 | throw new \RuntimeException("Failed to create temporary file"); 126 | } 127 | 128 | try { 129 | $ch = curl_init($downloadUrl); 130 | if ($ch === false) { 131 | throw new \RuntimeException("Failed to initialize cURL"); 132 | } 133 | 134 | $fp = fopen($tempFile, 'w'); 135 | if ($fp === false) { 136 | curl_close($ch); 137 | 138 | throw new \RuntimeException("Failed to open temporary file for writing"); 139 | } 140 | 141 | curl_setopt_array($ch, [ 142 | CURLOPT_FILE => $fp, 143 | CURLOPT_FOLLOWLOCATION => true, 144 | CURLOPT_SSL_VERIFYPEER => true, 145 | CURLOPT_FAILONERROR => true, 146 | CURLOPT_TIMEOUT => 300, 147 | CURLOPT_CONNECTTIMEOUT => 30, 148 | CURLOPT_HTTPHEADER => ['User-Agent: volt-test-php-sdk'], 149 | ]); 150 | 151 | if (curl_exec($ch) === false) { 152 | throw new \RuntimeException("Download failed: " . curl_error($ch)); 153 | } 154 | 155 | $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 156 | if ($httpCode !== 200) { 157 | throw new \RuntimeException("HTTP request failed with status $httpCode"); 158 | } 159 | 160 | curl_close($ch); 161 | fclose($fp); 162 | 163 | if (! file_exists($tempFile) || filesize($tempFile) === 0) { 164 | throw new \RuntimeException("Downloaded file is empty or missing"); 165 | } 166 | 167 | if (! rename($tempFile, $targetFile)) { 168 | throw new \RuntimeException("Failed to move downloaded binary to: $targetFile"); 169 | } 170 | 171 | if (! chmod($targetFile, 0755)) { 172 | throw new \RuntimeException("Failed to set executable permissions on binary"); 173 | } 174 | 175 | file_put_contents($binDir . '/.volt-test-version', $version); 176 | 177 | echo "Successfully installed VoltTest binary $version to: $targetFile\n"; 178 | } catch (\Exception $e) { 179 | if (file_exists($tempFile)) { 180 | unlink($tempFile); 181 | } 182 | 183 | throw $e; 184 | } 185 | } 186 | 187 | public static function getBinaryPath(): string 188 | { 189 | $binDir = self::getBinaryDir(); 190 | $binaryName = self::BINARY_NAME; 191 | if (PHP_OS_FAMILY === 'Windows') { 192 | $binaryName .= '.exe'; 193 | } 194 | 195 | $binaryPath = $binDir . '/' . $binaryName; 196 | $versionFile = $binDir . '/.volt-test-version'; 197 | 198 | $needsInstall = true; 199 | 200 | if (file_exists($binaryPath) && file_exists($versionFile)) { 201 | try { 202 | $currentVersion = trim(file_get_contents($versionFile)); 203 | $latestVersion = self::getCurrentVersion(); 204 | $needsInstall = $currentVersion !== $latestVersion; 205 | } catch (\Exception $e) { 206 | $needsInstall = true; 207 | } 208 | } 209 | 210 | if ($needsInstall) { 211 | self::installBinary(); 212 | } 213 | 214 | return $binaryPath; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/ProcessManager.php: -------------------------------------------------------------------------------- 1 | binaryPath = $binaryPath; 17 | if (function_exists('pcntl_async_signals')) { 18 | pcntl_async_signals(true); 19 | pcntl_signal(SIGINT, [$this, 'handleSignal']); 20 | pcntl_signal(SIGTERM, [$this, 'handleSignal']); 21 | } 22 | } 23 | 24 | public function handleSignal(int $signal): void 25 | { 26 | if ($this->currentProcess && is_resource($this->currentProcess)) { 27 | // Send SIGTERM to the process 28 | proc_terminate($this->currentProcess, SIGTERM); 29 | 30 | // Wait a bit for it to exit gracefully 31 | sleep(1); 32 | 33 | // Capture final output before closing the process 34 | $output = ''; 35 | if ($this->pipes && isset($this->pipes[1]) && is_resource($this->pipes[1])) { 36 | stream_set_blocking($this->pipes[1], false); // Ensure non-blocking mode 37 | $output = stream_get_contents($this->pipes[1]); 38 | fclose($this->pipes[1]); 39 | } 40 | 41 | // Ensure the process is closed 42 | proc_close($this->currentProcess); 43 | $this->currentProcess = null; 44 | 45 | // Print the final output 46 | if (!empty($output)) { 47 | echo "\n$output\n"; 48 | } 49 | } 50 | 51 | exit(0); 52 | } 53 | 54 | public function execute(array $config, bool $streamOutput): string 55 | { 56 | [$success, $process, $pipes] = $this->openProcess(); 57 | $this->currentProcess = $process; 58 | $this->pipes = $pipes; 59 | 60 | if (! $success || ! is_array($pipes)) { 61 | throw new RuntimeException('Failed to start process of volt test'); 62 | } 63 | 64 | try { 65 | $this->writeInput($pipes[0], json_encode($config, JSON_PRETTY_PRINT)); 66 | fclose($pipes[0]); 67 | 68 | $output = $this->handleProcess($pipes, $streamOutput); 69 | 70 | // Store stderr content before closing 71 | $stderrContent = ''; 72 | if (isset($pipes[2]) && is_resource($pipes[2])) { 73 | rewind($pipes[2]); 74 | $stderrContent = stream_get_contents($pipes[2]); 75 | } 76 | 77 | // Clean up pipes 78 | foreach ($pipes as $pipe) { 79 | if (is_resource($pipe)) { 80 | fclose($pipe); 81 | } 82 | } 83 | 84 | if (is_resource($process)) { 85 | $exitCode = $this->closeProcess($process); 86 | $this->currentProcess = null; 87 | if ($exitCode !== 0) { 88 | echo "\nError: " . trim($stderrContent) . "\n"; 89 | 90 | return ''; 91 | } 92 | } 93 | 94 | return $output; 95 | } finally { 96 | foreach ($pipes as $pipe) { 97 | if (is_resource($pipe)) { 98 | fclose($pipe); 99 | } 100 | } 101 | if (is_resource($process)) { 102 | $this->closeProcess($process); 103 | $this->currentProcess = null; 104 | } 105 | } 106 | } 107 | 108 | protected function openProcess(): array 109 | { 110 | $pipes = []; 111 | $process = proc_open( 112 | $this->binaryPath, 113 | [ 114 | 0 => ['pipe', 'r'], 115 | 1 => ['pipe', 'w'], 116 | 2 => ['pipe', 'w'], 117 | ], 118 | $pipes, 119 | null, 120 | null, 121 | ['bypass_shell' => true] 122 | ); 123 | 124 | if (! is_resource($process)) { 125 | return [false, null, []]; 126 | } 127 | 128 | return [true, $process, $pipes]; 129 | } 130 | 131 | private function handleProcess(array $pipes, bool $streamOutput): string 132 | { 133 | $output = ''; 134 | 135 | // Set non-blocking mode for stdout and stderr 136 | stream_set_blocking($pipes[1], false); 137 | stream_set_blocking($pipes[2], false); 138 | 139 | while (true) { 140 | $read = array_filter($pipes, 'is_resource'); 141 | if (empty($read)) { 142 | break; 143 | } 144 | 145 | $write = null; 146 | $except = null; 147 | 148 | if (stream_select($read, $write, $except, 1) === false) { 149 | break; 150 | } 151 | 152 | foreach ($read as $pipe) { 153 | $type = array_search($pipe, $pipes, true); 154 | $data = fread($pipe, 4096); 155 | 156 | if ($data === false || $data === '') { 157 | if (feof($pipe)) { 158 | fclose($pipe); 159 | unset($pipes[$type]); 160 | continue; 161 | } 162 | } 163 | 164 | if ($type === 1) { // stdout 165 | $output .= $data; 166 | if ($streamOutput) { 167 | echo $data; 168 | } 169 | } elseif ($type === 2 && $streamOutput) { // stderr 170 | fwrite(STDERR, $data); 171 | } 172 | } 173 | } 174 | 175 | return $output; 176 | } 177 | 178 | protected function writeInput($pipe, string $input): void 179 | { 180 | if (is_resource($pipe)) { 181 | fwrite($pipe, $input); 182 | } 183 | } 184 | 185 | protected function closeProcess($process): int 186 | { 187 | if (! is_resource($process)) { 188 | return -1; 189 | } 190 | 191 | $status = proc_get_status($process); 192 | if ($status['running']) { 193 | proc_terminate($process); 194 | } 195 | 196 | 197 | return proc_close($process); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/Request.php: -------------------------------------------------------------------------------- 1 | method = $method; 32 | 33 | return $this; 34 | } 35 | 36 | /* 37 | * Sets the URL for the request with validation 38 | * @param string $url 39 | * @return self 40 | * @throws \InvalidRequestValidationException 41 | * */ 42 | public function setUrl(string $url): self 43 | { 44 | // confirm it start with https:// or http:// 45 | if (! preg_match('/^https?:\/\//', $url)) { 46 | throw new InvalidRequestValidationException('URL should start with http:// or https://'); 47 | } 48 | // confirm it is a valid URL 49 | if (! filter_var($url, FILTER_VALIDATE_URL)) { 50 | throw new InvalidRequestValidationException('Invalid URL provided'); 51 | } 52 | $this->url = $url; 53 | 54 | return $this; 55 | } 56 | 57 | /* 58 | * Sets the body for the request with validation 59 | * @param string $body 60 | * @return self 61 | * @throws InvalidRequestValidationException 62 | * */ 63 | public function setBody(string $body): self 64 | { 65 | if (in_array($this->method, ['GET', 'HEAD', 'OPTIONS'])) { 66 | throw new InvalidRequestValidationException(sprintf(('%s method should not have a body'), $this->method)); 67 | } 68 | $this->body = $body; 69 | 70 | return $this; 71 | } 72 | 73 | /* 74 | * Adds a header to the request with validation 75 | * @param string $name 76 | * @param string $value 77 | * @return self 78 | * @throws InvalidRequestValidationException 79 | * */ 80 | public function addHeader(string $name, string $value): self 81 | { 82 | // Validate header name format (RFC 7230) 83 | if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $name)) { 84 | throw new InvalidRequestValidationException( 85 | 'Invalid header name. Header names should only contain printable US-ASCII characters except delimiters' 86 | ); 87 | } 88 | 89 | // Validate header value 90 | if (preg_match('/[\r\n]/', $value)) { 91 | throw new InvalidRequestValidationException('Header value cannot contain CR or LF characters'); 92 | } 93 | $this->headers[$name] = $value; 94 | 95 | return $this; 96 | } 97 | 98 | /* 99 | * Converts the request to an array 100 | * @return array 101 | * */ 102 | public function toArray(): array 103 | { 104 | $array = [ 105 | 'method' => $this->method, 106 | 'url' => $this->url, 107 | 'body' => $this->body, 108 | ]; 109 | if (count($this->headers) > 0) { 110 | $array['header'] = $this->headers; 111 | } 112 | 113 | return $array; 114 | } 115 | 116 | /** 117 | * Validates the entire request 118 | * @return bool 119 | * @throws InvalidRequestValidationException 120 | */ 121 | public function validate(): bool 122 | { 123 | // Validate required fields 124 | if (empty($this->url)) { 125 | throw new InvalidRequestValidationException('URL is required'); 126 | } 127 | 128 | // Validate content type header if body is present 129 | if (! empty($this->body)) { 130 | $hasContentType = false; 131 | foreach ($this->headers as $name => $value) { 132 | if (strtolower($name) === 'content-type') { 133 | $hasContentType = true; 134 | 135 | break; 136 | } 137 | } 138 | 139 | if (! $hasContentType) { 140 | throw new InvalidRequestValidationException('Content-Type header is required when body is present'); 141 | } 142 | } 143 | 144 | return true; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Scenario.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->description = $description; 27 | $this->dataSourceConfiguration = null; 28 | } 29 | 30 | public function step(string $name): Step 31 | { 32 | $step = new Step($name); 33 | $this->steps[] = $step; 34 | 35 | return $step; 36 | } 37 | 38 | public function setWeight(int $weight): self 39 | { 40 | $this->weight = $weight; 41 | 42 | return $this; 43 | } 44 | 45 | public function setThinkTime(string $thinkTime): self 46 | { 47 | if (! preg_match('/^\d+[smh]$/', $thinkTime)) { 48 | throw new VoltTestException('Invalid think time format. Use [s|m|h]'); 49 | } 50 | $this->thinkTime = $thinkTime; 51 | 52 | return $this; 53 | } 54 | 55 | public function getWeight(): int 56 | { 57 | return $this->weight; 58 | } 59 | 60 | public function autoHandleCookies(): self 61 | { 62 | $this->autoHandleCookies = true; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @throws VoltTestException 69 | */ 70 | public function setDataSourceConfiguration(DataSourceConfiguration $dataSourceConfiguration): self 71 | { 72 | if (! is_null($this->dataSourceConfiguration)) { 73 | throw new VoltTestException('Data source configuration already set'); 74 | } 75 | $this->dataSourceConfiguration = $dataSourceConfiguration; 76 | $this->dataSourceConfiguration->validate(); 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @throws VoltTestException 83 | */ 84 | public function toArray(): array 85 | { 86 | $array = [ 87 | 'name' => $this->name, 88 | 'description' => $this->description, 89 | 'weight' => $this->weight, 90 | 'steps' => array_map(function (Step $step) { 91 | return $step->toArray(); 92 | }, $this->steps), 93 | 'auto_handle_cookies' => $this->autoHandleCookies, 94 | ]; 95 | if (trim($this->thinkTime) !== '') { 96 | $array['think_time'] = $this->thinkTime; 97 | } 98 | if (! is_null($this->dataSourceConfiguration)) { 99 | $array['data_config'] = $this->dataSourceConfiguration->toArray(); 100 | } 101 | 102 | return $array; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Step.php: -------------------------------------------------------------------------------- 1 | name = $name; 37 | $this->request = new Request(); 38 | } 39 | 40 | /** 41 | * @throws InvalidRequestValidationException 42 | */ 43 | public function get(string $url): self 44 | { 45 | $this->request->setMethod('GET')->setUrl($url); 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @throws InvalidRequestValidationException 52 | */ 53 | public function post(string $url, string $body = ''): self 54 | { 55 | $this->request->setMethod('POST')->setUrl($url)->setBody($body); 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * @throws InvalidRequestValidationException 62 | */ 63 | public function delete(string $url): self 64 | { 65 | $this->request->setMethod('DELETE')->setUrl($url); 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * @throws InvalidRequestValidationException 72 | */ 73 | public function patch(string $url, string $body = ''): self 74 | { 75 | $this->request->setMethod('PATCH')->setUrl($url)->setBody($body); 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * @throws InvalidRequestValidationException 82 | */ 83 | public function put(string $url, string $body = ''): self 84 | { 85 | $this->request->setMethod('PUT')->setUrl($url)->setBody($body); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @throws InvalidRequestValidationException 92 | */ 93 | public function head(string $url): self 94 | { 95 | $this->request->setMethod('HEAD')->setUrl($url); 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * @throws InvalidRequestValidationException 102 | */ 103 | public function options(string $url): self 104 | { 105 | $this->request->setMethod('OPTIONS')->setUrl($url); 106 | 107 | return $this; 108 | } 109 | 110 | /** 111 | * @throws InvalidRequestValidationException 112 | */ 113 | public function header(string $name, string $value): self 114 | { 115 | $this->request->addHeader($name, $value); 116 | 117 | return $this; 118 | } 119 | 120 | public function extractFromCookie(string $variableName, string $selector): self 121 | { 122 | $cookieExtractor = new CookieExtractor($variableName, $selector); 123 | if (! $cookieExtractor->validate()) { 124 | throw new VoltTestException( 125 | sprintf( 126 | 'Invalid regex pattern provided: "%s". Step: "%s". Variable: "%s".', 127 | $selector, 128 | $this->name, 129 | $variableName 130 | ) 131 | ); 132 | } 133 | $this->extracts[] = $cookieExtractor; 134 | 135 | return $this; 136 | } 137 | 138 | /* 139 | * Extracts a value from the response header 140 | * @param string $variableName The name of the variable to store the extracted value 141 | * @param string $selector The regex pattern to use to extract the value 142 | * @return self 143 | * @throws VoltTestException 144 | * */ 145 | public function extractFromHeader(string $variableName, string $selector): self 146 | { 147 | $headerExtractor = new HeaderExtractor($variableName, $selector); 148 | if (! $headerExtractor->validate()) { 149 | throw new VoltTestException( 150 | sprintf( 151 | 'Invalid regex pattern provided: "%s". Step: "%s". Variable: "%s".', 152 | $selector, 153 | $this->name, 154 | $variableName 155 | ) 156 | ); 157 | } 158 | $this->extracts[] = $headerExtractor; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * Extracts a value from the response json body 165 | * @param string $variableName The name of the variable to store the extracted value 166 | * @param string $jsonPath The json path to use to extract the value 167 | * @return self 168 | * @throws InvalidJsonPathException 169 | */ 170 | public function extractFromJson(string $variableName, string $jsonPath): self 171 | { 172 | $jsonExtractor = new JsonExtractor($variableName, $jsonPath); 173 | $jsonExtractor->validate(); 174 | $this->extracts[] = $jsonExtractor; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Extracts a value using a regex pattern 181 | * @param string $variableName The name of the variable to store the extracted value 182 | * @param string $selector The regex pattern to use to extract the value 183 | * @return self 184 | * @throws InvalidRegexException 185 | */ 186 | public function extractFromRegex(string $variableName, string $selector): self 187 | { 188 | $regexExtractor = new RegexExtractor($variableName, $selector); 189 | $regexExtractor->validate(); 190 | $this->extracts[] = $regexExtractor; 191 | 192 | return $this; 193 | } 194 | 195 | 196 | public function extractFromHtml(string $variableName, string $selector, ?string $attribute = null): self 197 | { 198 | $htmlExtractor = new HtmlExtractor($variableName, $selector, $attribute); 199 | $this->extracts[] = $htmlExtractor; 200 | return $this; 201 | } 202 | 203 | public function validateStatus(string $name, int $expected): self 204 | { 205 | $this->validations[] = new StatusValidator($name, $expected); 206 | 207 | return $this; 208 | } 209 | 210 | public function setThinkTime(string $thinkTime): self 211 | { 212 | if (! preg_match('/^\d+[smh]$/', $thinkTime)) { 213 | throw new VoltTestException('Invalid think time format. Use [s|m|h]'); 214 | } 215 | $this->thinkTime = $thinkTime; 216 | 217 | return $this; 218 | } 219 | 220 | public function toArray(): array 221 | { 222 | $array = [ 223 | 'name' => $this->name, 224 | 'request' => $this->request->toArray(), 225 | 'extract' => array_map(function (Extractor $extract) { 226 | return $extract->toArray(); 227 | }, $this->extracts), 228 | 'validate' => array_map(function (Validator $validate) { 229 | return $validate->toArray(); 230 | }, $this->validations), 231 | ]; 232 | if (trim($this->thinkTime) !== '') { 233 | $array['think_time'] = $this->thinkTime; 234 | } 235 | 236 | return $array; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/TestResult.php: -------------------------------------------------------------------------------- 1 | rawOutput = $output; 14 | $this->parseOutput(); 15 | } 16 | 17 | private function parseOutput(): void 18 | { 19 | // Initialize default values 20 | $this->parsedResults = [ 21 | 'duration' => '0', 22 | 'totalRequests' => 0, 23 | 'successRate' => 0.0, 24 | 'requestsPerSecond' => 0.0, 25 | 'successRequests' => 0, 26 | 'failedRequests' => 0, 27 | 'responseTime' => [ 28 | 'min' => null, 29 | 'max' => null, 30 | 'avg' => null, 31 | 'median' => null, 32 | 'p95' => null, 33 | 'p99' => null, 34 | ], 35 | ]; 36 | 37 | if (empty($this->rawOutput)) { 38 | return; 39 | } 40 | 41 | // Parse main metrics 42 | $this->parseMainMetrics(); 43 | 44 | // Parse response time metrics if present 45 | if (strpos($this->rawOutput, 'Response Time:') !== false) { 46 | $this->parseResponseTimeMetrics(); 47 | } 48 | } 49 | 50 | private function parseMainMetrics(): void 51 | { 52 | // Duration 53 | if (preg_match('/Duration:\s+([\d.]+(?:ms|s|m|hr))/', $this->rawOutput, $matches)) { 54 | $this->parsedResults['duration'] = $matches[1]; 55 | } 56 | 57 | // Total Requests 58 | if (preg_match('/Total Reqs:\s+(\d+)/', $this->rawOutput, $matches)) { 59 | $this->parsedResults['totalRequests'] = (int)$matches[1]; 60 | } 61 | 62 | // Success Rate 63 | if (preg_match('/Success Rate:\s+([\d.]+)%/', $this->rawOutput, $matches)) { 64 | $this->parsedResults['successRate'] = (float)$matches[1]; 65 | } 66 | 67 | // Requests per Second 68 | if (preg_match('/Req\/sec:\s+([\d.]+)/', $this->rawOutput, $matches)) { 69 | $this->parsedResults['requestsPerSecond'] = (float)$matches[1]; 70 | } 71 | 72 | // Success Requests 73 | if (preg_match('/Success Requests:\s+(\d+)/', $this->rawOutput, $matches)) { 74 | $this->parsedResults['successRequests'] = (int)$matches[1]; 75 | } 76 | 77 | // Failed Requests 78 | if (preg_match('/Failed Requests:\s+(\d+)/', $this->rawOutput, $matches)) { 79 | $this->parsedResults['failedRequests'] = (int)$matches[1]; 80 | } 81 | } 82 | 83 | private function parseResponseTimeMetrics(): void 84 | { 85 | $metrics = [ 86 | 'min' => 'Min:\s+([^\n]+)', 87 | 'max' => 'Max:\s+([^\n]+)', 88 | 'avg' => 'Avg:\s+([^\n]+)', 89 | 'median' => 'Median:\s+([^\n]+)', 90 | 'p95' => 'P95:\s+([^\n]+)', 91 | 'p99' => 'P99:\s+([^\n]+)', 92 | ]; 93 | 94 | foreach ($metrics as $key => $pattern) { 95 | if (preg_match('/' . $pattern . '/', $this->rawOutput, $matches)) { 96 | $this->parsedResults['responseTime'][$key] = trim($matches[1]); 97 | } 98 | } 99 | } 100 | 101 | public function getDuration(): string 102 | { 103 | return $this->parsedResults['duration']; 104 | } 105 | 106 | public function getTotalRequests(): int 107 | { 108 | return $this->parsedResults['totalRequests']; 109 | } 110 | 111 | public function getSuccessRate(): float 112 | { 113 | return $this->parsedResults['successRate']; 114 | } 115 | 116 | public function getRequestsPerSecond(): float 117 | { 118 | return $this->parsedResults['requestsPerSecond']; 119 | } 120 | 121 | public function getSuccessRequests(): int 122 | { 123 | return $this->parsedResults['successRequests']; 124 | } 125 | 126 | public function getFailedRequests(): int 127 | { 128 | return $this->parsedResults['failedRequests']; 129 | } 130 | 131 | public function getMinResponseTime(): ?string 132 | { 133 | return $this->parsedResults['responseTime']['min']; 134 | } 135 | 136 | public function getMaxResponseTime(): ?string 137 | { 138 | return $this->parsedResults['responseTime']['max']; 139 | } 140 | 141 | public function getAvgResponseTime(): ?string 142 | { 143 | return $this->parsedResults['responseTime']['avg']; 144 | } 145 | 146 | public function getMedianResponseTime(): ?string 147 | { 148 | return $this->parsedResults['responseTime']['median']; 149 | } 150 | 151 | public function getP95ResponseTime(): ?string 152 | { 153 | return $this->parsedResults['responseTime']['p95']; 154 | } 155 | 156 | public function getP99ResponseTime(): ?string 157 | { 158 | return $this->parsedResults['responseTime']['p99']; 159 | } 160 | 161 | public function getRawOutput(): string 162 | { 163 | return $this->rawOutput; 164 | } 165 | 166 | /** 167 | * Returns all parsed metrics as an array 168 | * @return array 169 | */ 170 | public function getAllMetrics(): array 171 | { 172 | return $this->parsedResults; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/TestableProcessManager.php: -------------------------------------------------------------------------------- 1 | mockOutput = $output; 30 | } 31 | 32 | public function setMockStderr(string $stderr): void 33 | { 34 | $this->mockStderr = $stderr; 35 | } 36 | 37 | public function setMockExitCode(int $exitCode): void 38 | { 39 | $this->mockExitCode = $exitCode; 40 | } 41 | 42 | public function setFailProcessStart(bool $fail): void 43 | { 44 | $this->failProcessStart = $fail; 45 | } 46 | 47 | public function wasProcessStarted(): bool 48 | { 49 | return $this->processStarted; 50 | } 51 | 52 | public function wasProcessClosed(): bool 53 | { 54 | return $this->processClosed; 55 | } 56 | 57 | public function wasProcessCompleted(): bool 58 | { 59 | return $this->processCompleted; 60 | } 61 | 62 | public function getWritternInput(): string 63 | { 64 | return $this->writtenInput; 65 | } 66 | 67 | public function wereResourcesCleaned(): bool 68 | { 69 | return $this->resourcedCleaned; 70 | } 71 | 72 | protected function openProcess(): array 73 | { 74 | if ($this->failProcessStart) { 75 | return [false, null, []]; 76 | } 77 | 78 | $this->processStarted = true; 79 | 80 | $pipes = [ 81 | 0 => fopen('php://temp', 'r+'), // stdin 82 | 1 => fopen('php://temp', 'w+'), // stdout 83 | 2 => fopen('php://temp', 'w+'), // stderr 84 | ]; 85 | 86 | if (in_array(false, $pipes, true)) { 87 | return [false, null, []]; 88 | } 89 | 90 | // Write mock output and error to pipes 91 | fwrite($pipes[1], $this->mockOutput); 92 | fseek($pipes[1], 0); 93 | 94 | fwrite($pipes[2], $this->mockStderr); 95 | fseek($pipes[2], 0); 96 | 97 | // Mock a process resource 98 | $process = tmpfile(); 99 | 100 | return [true, $process, $pipes]; 101 | } 102 | 103 | protected function writeInput($pipe, string $input): void 104 | { 105 | $this->writtenInput = $input; 106 | parent::writeInput($pipe, $input); 107 | } 108 | 109 | protected function closeProcess($process): int 110 | { 111 | $this->processClosed = true; 112 | $this->resourcedCleaned = true; 113 | $this->processCompleted = true; 114 | 115 | return $this->mockExitCode; 116 | } 117 | 118 | protected function getProcessStatus($process): array 119 | { 120 | if ($this->isRunning) { 121 | $this->isRunning = false; 122 | 123 | return ['running' => true]; 124 | } 125 | 126 | $this->processCompleted = true; 127 | 128 | return ['running' => false]; 129 | } 130 | 131 | public function getWrittenInput(): string 132 | { 133 | return $this->writtenInput; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Validators/StatusValidator.php: -------------------------------------------------------------------------------- 1 | name = $name; 14 | $this->expected = $expected; 15 | } 16 | 17 | public function toArray(): array 18 | { 19 | return [ 20 | 'name' => $this->name, 21 | 'expected' => $this->expected, 22 | 'type' => 'status_code', 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Validators/Validator.php: -------------------------------------------------------------------------------- 1 | config = new Configuration($name, $description); 20 | $this->processManager = new ProcessManager(Platform::getBinaryPath()); 21 | } 22 | 23 | /** 24 | * Set the number of virtual users 25 | * @param int $count 26 | * @return $this 27 | * @throws VoltTestException 28 | */ 29 | public function setVirtualUsers(int $count): self 30 | { 31 | if ($count < 1) { 32 | throw new VoltTestException('Virtual users count must be at least 1'); 33 | } 34 | $this->config->setVirtualUsers($count); 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Set the test duration 41 | * @param string $duration 42 | * @return $this 43 | * @throws VoltTestException 44 | */ 45 | public function setDuration(string $duration): self 46 | { 47 | if (! preg_match('/^\d+[smh]$/', $duration)) { 48 | throw new VoltTestException('Invalid duration format. Use [s|m|h]'); 49 | } 50 | $this->config->setDuration($duration); 51 | 52 | return $this; 53 | } 54 | 55 | /* 56 | * Set the ramp-up time for every virtual user to start 57 | * @param string $rampUp 58 | * @return $this 59 | * @throws VoltTestException 60 | * */ 61 | public function setRampUp(string $rampUp): self 62 | { 63 | if (! preg_match('/^\d+[smh]$/', $rampUp)) { 64 | throw new VoltTestException('Invalid ramp-up format. Use [s|m|h]'); 65 | } 66 | $this->config->setRampUp($rampUp); 67 | 68 | return $this; 69 | } 70 | 71 | public function setHttpDebug(bool $httpDebug): self 72 | { 73 | $this->config->setHttpDebug($httpDebug); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Set the target URL and idle timeout 80 | * @param string $idleTimeout Default is 30s (30 seconds) example: 1s (1 second), 1m (1 minute), 1h (1 hour) 81 | * @throws VoltTestException 82 | */ 83 | public function setTarget(string $idleTimeout): self 84 | { 85 | if (! preg_match('/^\d+[smh]$/', $idleTimeout)) { 86 | throw new VoltTestException('Invalid idle timeout format. Use [s|m|h]'); 87 | } 88 | $this->config->setTarget($idleTimeout); 89 | 90 | return $this; 91 | } 92 | 93 | public function scenario(string $name, string $description = ''): Scenario 94 | { 95 | $scenario = new Scenario($name, $description); 96 | $this->scenarios[] = $scenario; 97 | 98 | return $scenario; 99 | } 100 | 101 | public function run(bool $streamOutput = false): TestResult 102 | { 103 | $config = $this->prepareConfig(); 104 | 105 | $output = $this->processManager->execute($config, $streamOutput); 106 | 107 | return new TestResult($output); 108 | } 109 | 110 | private function prepareConfig(): array 111 | { 112 | $config = $this->config->toArray(); 113 | $config['scenarios'] = array_map(function (Scenario $scenario) { 114 | return $scenario->toArray(); 115 | }, $this->scenarios); 116 | 117 | $config['weights'] = array_map(function (Scenario $scenario) { 118 | return $scenario->getWeight(); 119 | }, $this->scenarios); 120 | 121 | return $config; 122 | } 123 | } 124 | --------------------------------------------------------------------------------