previa-runner 1.0.0-alpha.30

API for remote execution of integration and load tests via HTTP streaming (SSE).
Documentation
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;

use previa_runner::Pipeline;
use previa_runner::RuntimeEnvGroup;
use previa_runner::RuntimeSpec;
use previa_runner::StepExecutionResult;

#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct E2eTestRequest {
    pub pipeline: Pipeline,
    pub selected_base_url_key: Option<String>,
    pub selected_env_group_slug: Option<String>,
    #[serde(default)]
    pub specs: Vec<RuntimeSpec>,
    #[serde(default)]
    pub env_groups: Vec<RuntimeEnvGroup>,
}

#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct E2eRerunFromStepRequest {
    pub pipeline: Pipeline,
    pub start_step_id: String,
    #[serde(default)]
    pub prior_results: std::collections::HashMap<String, StepExecutionResult>,
    pub selected_env_group_slug: Option<String>,
    #[serde(default)]
    pub specs: Vec<RuntimeSpec>,
    #[serde(default)]
    pub env_groups: Vec<RuntimeEnvGroup>,
}

#[derive(Debug, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadTestRequest {
    pub pipeline: Pipeline,
    #[serde(default)]
    pub config: Option<LoadTestConfig>,
    #[serde(default)]
    pub load: Option<LoadProfile>,
    pub selected_base_url_key: Option<String>,
    pub selected_env_group_slug: Option<String>,
    #[serde(default)]
    pub specs: Vec<RuntimeSpec>,
    #[serde(default)]
    pub env_groups: Vec<RuntimeEnvGroup>,
}

#[derive(Debug, Clone, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadTestConfig {
    pub total_requests: usize,
    pub concurrency: usize,
    pub ramp_up_seconds: f64,
}

#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LoadProfile {
    pub points: Vec<LoadPoint>,
    #[serde(default)]
    pub interpolation: LoadInterpolation,
    pub runner_max_rps: f64,
    #[serde(default = "default_load_grace_period_ms")]
    pub grace_period_ms: u64,
}

#[derive(Debug, Clone, Deserialize, Serialize, ToSchema, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct LoadPoint {
    pub at_ms: u64,
    pub intensity: f64,
}

#[derive(Debug, Clone, Copy, Deserialize, Serialize, ToSchema, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum LoadInterpolation {
    Smooth,
    Linear,
    Step,
}

impl Default for LoadInterpolation {
    fn default() -> Self {
        Self::Smooth
    }
}

fn default_load_grace_period_ms() -> u64 {
    30_000
}

#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct E2eSummary {
    pub total_steps: usize,
    pub passed: usize,
    pub failed: usize,
    pub total_duration: u128,
}

#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadLatencyBucket {
    pub duration_ms: u64,
    pub count: usize,
}

#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadDispatchBucket {
    pub elapsed_ms: u64,
    pub count: usize,
}

#[derive(Debug, Serialize, Clone, Copy, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "camelCase")]
pub enum LoadMetricsSnapshotMode {
    Live,
    Final,
}

#[derive(Debug, Serialize, Clone, Default, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadLifecycleBucket {
    pub elapsed_ms: u64,
    #[serde(skip_serializing_if = "is_zero")]
    pub planned: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub slot_enqueued: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub request_prepared: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub request_enqueued: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub send_task_spawned: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub send_started: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub http_started: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub http_send_returned: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub response_body_completed: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub dispatcher_lagged: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub runtime_lagged: usize,
    #[serde(skip_serializing_if = "is_zero")]
    pub sender_lagged: usize,
    #[serde(skip_serializing_if = "is_zero_u64")]
    pub sender_start_lag_ms_max: u64,
    #[serde(skip_serializing_if = "is_zero_u64")]
    pub http_send_duration_ms_max: u64,
    #[serde(skip_serializing_if = "is_zero_u64")]
    pub response_observation_duration_ms_max: u64,
}

fn is_zero(value: &usize) -> bool {
    *value == 0
}

fn is_zero_u64(value: &u64) -> bool {
    *value == 0
}

#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadErrorSample {
    pub step_id: String,
    pub http_status: Option<u16>,
    pub error: String,
    pub count: usize,
}

#[derive(Debug, Serialize, Clone, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct LoadTestMetrics {
    #[serde(skip_serializing_if = "Option::is_none")]
    pub snapshot_mode: Option<LoadMetricsSnapshotMode>,
    pub total_started: usize,
    pub total_sent: usize,
    pub total_success: usize,
    pub total_error: usize,
    pub http_started: usize,
    pub http_completed: usize,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dispatch_submitted: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dispatch_started: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_send_returned: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_body_completed: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dependency_limited_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub dispatcher_lagged_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runtime_lagged_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scheduler_lag_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scheduler_lagged_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub slot_enqueued: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_prepared: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub request_enqueued: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send_task_spawned: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub send_started: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_lagged_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_queue_depth: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_start_lag_avg_ms: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_start_lag_p95_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_start_lag_p99_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub sender_start_lag_max_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_send_duration_avg_ms: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_send_duration_p95_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub http_send_duration_p99_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_observation_duration_avg_ms: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_observation_duration_p95_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub response_observation_duration_p99_ms: Option<u64>,
    pub rps: f64,
    pub start_time: u64,
    pub elapsed_ms: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_intensity: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target_rps_limit: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub in_flight: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runner_max_rps: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub tick_ms: Option<u64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub scheduled_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub missed_starts: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub ready_requests: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub active_pipelines: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub outstanding_requests: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub curve_adherence: Option<f64>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub latency_buckets: Vec<LoadLatencyBucket>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub dispatch_buckets: Vec<LoadDispatchBucket>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub lifecycle_buckets: Vec<LoadLifecycleBucket>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub latency_sample_count: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub latency_total_duration_ms: Option<u64>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub error_samples: Vec<LoadErrorSample>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub runtime: Option<RunnerInfoResponse>,
}

impl Default for LoadTestMetrics {
    fn default() -> Self {
        Self {
            snapshot_mode: None,
            total_started: 0,
            total_sent: 0,
            total_success: 0,
            total_error: 0,
            http_started: 0,
            http_completed: 0,
            dispatch_submitted: None,
            dispatch_started: None,
            http_send_returned: None,
            response_body_completed: None,
            dependency_limited_starts: None,
            dispatcher_lagged_starts: None,
            runtime_lagged_starts: None,
            scheduler_lag_ms: None,
            scheduler_lagged_starts: None,
            slot_enqueued: None,
            request_prepared: None,
            request_enqueued: None,
            send_task_spawned: None,
            send_started: None,
            sender_lagged_starts: None,
            sender_queue_depth: None,
            sender_start_lag_avg_ms: None,
            sender_start_lag_p95_ms: None,
            sender_start_lag_p99_ms: None,
            sender_start_lag_max_ms: None,
            http_send_duration_avg_ms: None,
            http_send_duration_p95_ms: None,
            http_send_duration_p99_ms: None,
            response_observation_duration_avg_ms: None,
            response_observation_duration_p95_ms: None,
            response_observation_duration_p99_ms: None,
            rps: 0.0,
            start_time: crate::server::utils::now_ms(),
            elapsed_ms: 0,
            target_intensity: None,
            target_rps_limit: None,
            in_flight: None,
            runner_max_rps: None,
            tick_ms: None,
            scheduled_starts: None,
            missed_starts: None,
            ready_requests: None,
            active_pipelines: None,
            outstanding_requests: None,
            curve_adherence: None,
            duration_ms: None,
            latency_buckets: Vec::new(),
            dispatch_buckets: Vec::new(),
            lifecycle_buckets: Vec::new(),
            latency_sample_count: None,
            latency_total_duration_ms: None,
            error_samples: Vec::new(),
            runtime: None,
        }
    }
}

#[derive(Debug, Serialize, ToSchema)]
pub struct ErrorResponse {
    pub error: String,
    pub message: String,
}

#[derive(Debug, Clone, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct RunnerInfoResponse {
    pub pid: u32,
    pub memory_bytes: u64,
    pub virtual_memory_bytes: u64,
    pub cpu_usage_percent: f32,
    pub network_tx_bytes: u64,
    pub network_rx_bytes: u64,
    pub network_total_bytes: u64,
    pub busy: bool,
    pub started_execution_count: u64,
    pub last_started_at: Option<String>,
    pub last_finished_at: Option<String>,
}

#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionInitEvent {
    pub execution_id: String,
}

#[derive(Debug, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
pub struct StepStartEvent {
    pub step_id: String,
}