Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [ main, develop ]
branches: [ main, develop, release/* ]
pull_request:
branches: [ main, develop ]
branches: [ main, develop , release/* ]

jobs:
test:
Expand Down
116 changes: 116 additions & 0 deletions src/CloudClient.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace VoltTest;

use VoltTest\Exceptions\AuthenticationException;
use VoltTest\Exceptions\CloudConnectionException;
use VoltTest\Exceptions\CloudException;
use VoltTest\Exceptions\PlanLimitException;

class CloudClient
{
private const BASE_URL = 'https://cloud.volt-test.com';

private const USER_AGENT = 'volt-test-php-sdk';

private string $apiKey;

private string $baseUrl;

public function __construct(string $apiKey, ?string $baseUrl = null)
{
if (empty($apiKey)) {
throw new AuthenticationException('API key is required');
}

if (! str_starts_with($apiKey, 'vt_')) {
throw new AuthenticationException('API key must start with "vt_"');
}

$this->apiKey = $apiKey;
$this->baseUrl = $baseUrl ?? self::BASE_URL;
}

public function createTest(array $data): array
{
return $this->request('POST', '/api/v1/tests', $data);
}

public function startRun(string $testId): array
{
return $this->request('POST', '/api/v1/runs', ['test_id' => $testId]);
}

public function getRunStatus(string $runId): array
{
return $this->request('GET', '/api/v1/runs/' . $runId);
}

public function stopRun(string $runId): array
{
return $this->request('DELETE', '/api/v1/runs/' . $runId);
}

private function request(string $method, string $endpoint, ?array $body = null): array
{
$url = rtrim($this->baseUrl, '/') . $endpoint;

$ch = curl_init();

$headers = [
'Authorization: Bearer ' . $this->apiKey,
'Content-Type: application/json',
'Accept: application/json',
'User-Agent: ' . self::USER_AGENT,
];

curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => true,
]);

if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
} elseif ($method === 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}

$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);

if ($response === false) {
throw new CloudConnectionException('Failed to connect to VoltTest Cloud: ' . $curlError);
}

$decoded = json_decode($response, true) ?? [];

if ($httpCode === 401) {
$message = $decoded['error']['message'] ?? 'Invalid or expired API key';

throw new AuthenticationException($message);
}

if ($httpCode === 403) {
$message = $decoded['error']['message'] ?? 'Plan limit exceeded';

throw new PlanLimitException($message);
}

if ($httpCode >= 400) {
$message = $decoded['error']['message'] ?? "API request failed with status {$httpCode}";

throw new CloudException($message);
}

return $decoded;
}
}
46 changes: 46 additions & 0 deletions src/CloudRun.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace VoltTest;

class CloudRun
{
private const DASHBOARD_BASE_URL = 'https://volt-test.com';

private string $runId;

private string $testId;

private string $status;

public function __construct(string $runId, string $testId, string $status)
{
$this->runId = $runId;
$this->testId = $testId;
$this->status = $status;
}

public function getRunId(): string
{
return $this->runId;
}

public function getTestId(): string
{
return $this->testId;
}

public function getStatus(): string
{
return $this->status;
}

public function getDashboardUrl(): string
{
return self::DASHBOARD_BASE_URL . '/runs/' . $this->runId;
}

public function isSuccessful(): bool
{
return $this->status === 'completed';
}
}
56 changes: 51 additions & 5 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ class Configuration

private array $target;

private string $httpTimeout = '';

private bool $httpDebug = false;

/** @var Stage[] */
private array $stages = [];

public function __construct(string $name, string $description = '')
{
$this->name = $name;
Expand All @@ -38,15 +43,26 @@ public function toArray(): array
$array = [
'name' => $this->name,
'description' => $this->description,
'virtual_users' => $this->virtualUsers,
'target' => $this->target,
'http_debug' => $this->httpDebug,
];
if (trim($this->rampUp) !== '') {
$array['ramp_up'] = $this->rampUp;

if (count($this->stages) > 0) {
// Stages mode: omit virtual_users, duration, ramp_up
$array['stages'] = array_map(fn (Stage $s) => $s->toArray(), $this->stages);
} else {
// Constant mode
$array['virtual_users'] = $this->virtualUsers;
if (trim($this->rampUp) !== '') {
$array['ramp_up'] = $this->rampUp;
}
if (trim($this->duration) !== '') {
$array['duration'] = $this->duration;
}
}
if (trim($this->duration) !== '') {
$array['duration'] = $this->duration;

if (trim($this->httpTimeout) !== '') {
$array['http_timeout'] = $this->httpTimeout;
}

return $array;
Expand Down Expand Up @@ -92,10 +108,40 @@ public function setTarget(string $idleTimeout = '30s'): self
return $this;
}

public function setHttpTimeout(string $httpTimeout): self
{
if (! preg_match('/^\d+[smh]$/', $httpTimeout)) {
throw new VoltTestException('Invalid HTTP timeout format. Use <number>[s|m|h]');
}
$this->httpTimeout = $httpTimeout;

return $this;
}

public function setHttpDebug(bool $httpDebug): self
{
$this->httpDebug = $httpDebug;

return $this;
}

/**
* @throws VoltTestException
*/
public function addStage(string $duration, int $target): self
{
$this->stages[] = new Stage($duration, $target);

return $this;
}

public function hasStages(): bool
{
return count($this->stages) > 0;
}

public function hasConstantLoad(): bool
{
return $this->virtualUsers > 1 || trim($this->duration) !== '' || trim($this->rampUp) !== '';
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/AuthenticationException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class AuthenticationException extends CloudException
{
}
7 changes: 7 additions & 0 deletions src/Exceptions/CloudConnectionException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class CloudConnectionException extends CloudException
{
}
7 changes: 7 additions & 0 deletions src/Exceptions/CloudException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class CloudException extends VoltTestException
{
}
7 changes: 7 additions & 0 deletions src/Exceptions/CloudTimeoutException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class CloudTimeoutException extends CloudException
{
}
7 changes: 7 additions & 0 deletions src/Exceptions/PlanLimitException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class PlanLimitException extends CloudException
{
}
7 changes: 7 additions & 0 deletions src/Exceptions/RunFailedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace VoltTest\Exceptions;

class RunFailedException extends CloudException
{
}
1 change: 0 additions & 1 deletion src/Extractors/HtmlExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

class HtmlExtractor implements Extractor
{

private string $variableName;

private string $selector;
Expand Down
4 changes: 3 additions & 1 deletion src/ProcessManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class ProcessManager
private string $binaryPath;

private $currentProcess = null;

private mixed $pipes;

public function __construct(string $binaryPath)
Expand Down Expand Up @@ -43,7 +44,7 @@ public function handleSignal(int $signal): void
$this->currentProcess = null;

// Print the final output
if (!empty($output)) {
if (! empty($output)) {
echo "\n$output\n";
}
}
Expand Down Expand Up @@ -157,6 +158,7 @@ private function handleProcess(array $pipes, bool $streamOutput): string
if (feof($pipe)) {
fclose($pipe);
unset($pipes[$type]);

continue;
}
}
Expand Down
45 changes: 45 additions & 0 deletions src/Stage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace VoltTest;

use VoltTest\Exceptions\VoltTestException;

class Stage
{
private string $duration;

private int $target;

/**
* @throws VoltTestException
*/
public function __construct(string $duration, int $target)
{
if (! preg_match('/^\d+[smh]$/', $duration)) {
throw new VoltTestException('Invalid stage duration format. Use <number>[s|m|h]');
}
if ($target < 0) {
throw new VoltTestException('Stage target must be non-negative');
}
$this->duration = $duration;
$this->target = $target;
}

public function getDuration(): string
{
return $this->duration;
}

public function getTarget(): int
{
return $this->target;
}

public function toArray(): array
{
return [
'duration' => $this->duration,
'target' => $this->target,
];
}
}
Loading
Loading