use super::*;
#[test]
fn test_percentile_empty() {
assert_eq!(percentile(&[], 0.5), 0.0);
}
#[test]
fn test_percentile_single() {
assert_eq!(percentile(&[42.0], 0.5), 42.0);
assert_eq!(percentile(&[42.0], 0.99), 42.0);
}
#[test]
fn test_percentile_multiple() {
let data: Vec<f64> = (1..=100).map(|x| x as f64).collect();
assert!((percentile(&data, 0.50) - 50.5).abs() < 0.01);
assert!((percentile(&data, 0.95) - 95.05).abs() < 0.01);
assert!((percentile(&data, 0.99) - 99.01).abs() < 0.01);
}
#[test]
fn test_aggregate_empty() {
let result = aggregate_results(&[], 10.0, "test", 1, None, None, None, None);
assert_eq!(result.total_requests, 0);
assert_eq!(result.successful, 0);
assert_eq!(result.failed, 0);
assert_eq!(result.throughput_rps, 0.0);
assert_eq!(result.latency_p50_ms, 0.0);
assert_eq!(result.error_rate, 0.0);
assert_eq!(result.prompt_tokens_total, 0);
assert_eq!(result.completion_tokens_total, 0);
}
#[test]
fn test_aggregate_all_success() {
let records: Vec<RequestRecord> = (0..10)
.map(|i| RequestRecord {
latency: Duration::from_millis(100 + i * 10),
ttfb: Duration::from_millis(50 + i * 5),
tokens: 20,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
})
.collect();
let result = aggregate_results(&records, 10.0, "realizar", 2, None, None, None, None);
assert_eq!(result.total_requests, 10);
assert_eq!(result.successful, 10);
assert_eq!(result.failed, 0);
assert!((result.throughput_rps - 1.0).abs() < f64::EPSILON);
assert!(result.latency_p50_ms > 0.0);
assert!(result.tokens_per_sec > 0.0);
assert!((result.avg_tok_per_req - 20.0).abs() < f64::EPSILON);
assert!(result.itl_p50_ms > 0.0);
assert!(result.decode_tok_per_sec > 0.0);
assert_eq!(result.runtime_name, "realizar");
assert_eq!(result.concurrency, 2);
assert!(result.ttft_p90_ms > 0.0);
assert!(result.ttft_p95_ms > 0.0);
assert!(result.ttft_p99_ms > 0.0);
assert!(result.tpot_p50_ms > 0.0);
assert!(result.latency_min_ms > 0.0);
assert!(result.latency_max_ms >= result.latency_min_ms);
assert!(result.latency_stddev_ms >= 0.0);
assert!((result.error_rate).abs() < f64::EPSILON);
assert_eq!(result.prompt_tokens_total, 100);
assert_eq!(result.completion_tokens_total, 200);
}
#[test]
fn test_aggregate_mixed() {
let records = vec![
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
RequestRecord {
latency: Duration::from_millis(0),
ttfb: Duration::from_millis(0),
tokens: 0,
prompt_tokens: 0,
success: false,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
];
let result = aggregate_results(&records, 5.0, "ollama", 1, None, None, None, None);
assert_eq!(result.total_requests, 2);
assert_eq!(result.successful, 1);
assert_eq!(result.failed, 1);
assert!((result.error_rate - 0.5).abs() < f64::EPSILON);
}
#[test]
fn test_default_config() {
let config = LoadTestConfig::default();
assert_eq!(config.concurrency, 1);
assert_eq!(config.duration, Duration::from_secs(30));
assert_eq!(config.prompts.len(), 1);
assert_eq!(config.warmup_duration, Duration::ZERO);
}
#[test]
fn test_default_prompt() {
let p = default_prompt();
assert_eq!(p.messages.len(), 1);
assert_eq!(p.messages[0].role, Role::User);
assert_eq!(p.temperature, Some(0.0));
}
#[test]
fn test_load_test_result_serialization() {
let result = LoadTestResult {
total_requests: 100,
successful: 95,
failed: 5,
throughput_rps: 10.0,
latency_p50_ms: 150.0,
latency_p95_ms: 300.0,
latency_p99_ms: 500.0,
ttft_p50_ms: 80.0,
tokens_per_sec: 200.0,
avg_tok_per_req: 15.0,
itl_p50_ms: 5.0,
decode_tok_per_sec: 200.0,
prefill_tok_per_sec: 0.0,
timestamp: "2026-03-01T00:00:00Z".to_string(),
runtime_name: "realizar".to_string(),
elapsed_secs: 10.0,
concurrency: 4,
ttft_p90_ms: 90.0,
ttft_p95_ms: 95.0,
ttft_p99_ms: 99.0,
tpot_p50_ms: 6.0,
tpot_p90_ms: 8.0,
tpot_p95_ms: 9.0,
tpot_p99_ms: 12.0,
latency_min_ms: 50.0,
latency_max_ms: 800.0,
latency_stddev_ms: 120.0,
error_rate: 0.05,
prompt_tokens_total: 950,
completion_tokens_total: 1425,
truncated_pct: 0.0,
sse_batch_ratio: 0.0,
goodput_pct: 0.0,
decode_us_per_layer: None,
num_layers: None,
output_tokens_dist: None,
brick_trace_summary: None,
request_details: Vec::new(),
quality: None,
tail_analysis: None,
gpu_telemetry: None,
dataset_stats: None,
cold_start_ms: None,
};
let json = serde_json::to_string(&result).unwrap();
let back: LoadTestResult = serde_json::from_str(&json).unwrap();
assert_eq!(back.total_requests, 100);
assert_eq!(back.runtime_name, "realizar");
assert!((back.avg_tok_per_req - 15.0).abs() < f64::EPSILON);
assert!((back.itl_p50_ms - 5.0).abs() < f64::EPSILON);
assert!((back.decode_tok_per_sec - 200.0).abs() < f64::EPSILON);
assert!((back.tpot_p50_ms - 6.0).abs() < f64::EPSILON);
assert!((back.error_rate - 0.05).abs() < f64::EPSILON);
assert_eq!(back.prompt_tokens_total, 950);
assert_eq!(back.completion_tokens_total, 1425);
}
#[test]
fn test_load_test_result_backwards_compat() {
let json = r#"{
"total_requests": 50,
"successful": 50,
"failed": 0,
"throughput_rps": 5.0,
"latency_p50_ms": 100.0,
"latency_p95_ms": 200.0,
"latency_p99_ms": 300.0,
"ttft_p50_ms": 50.0,
"tokens_per_sec": 100.0,
"timestamp": "2026-01-01T00:00:00Z",
"runtime_name": "old",
"elapsed_secs": 10.0,
"concurrency": 1
}"#;
let result: LoadTestResult = serde_json::from_str(json).unwrap();
assert_eq!(result.total_requests, 50);
assert_eq!(result.tpot_p50_ms, 0.0);
assert_eq!(result.error_rate, 0.0);
assert_eq!(result.prompt_tokens_total, 0);
}
#[test]
fn test_percentile_boundary() {
let data = vec![1.0, 2.0, 3.0];
assert_eq!(percentile(&data, 0.0), 1.0);
assert_eq!(percentile(&data, 1.0), 3.0);
}
#[test]
fn test_itl_streaming() {
let records = vec![RequestRecord {
latency: Duration::from_millis(200),
ttfb: Duration::from_millis(50),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.itl_p50_ms - 10.0).abs() < 0.1);
assert!((result.decode_tok_per_sec - 100.0).abs() < 1.0);
assert!((result.avg_tok_per_req - 16.0).abs() < f64::EPSILON);
}
#[test]
fn test_itl_non_streaming() {
let records = vec![RequestRecord {
latency: Duration::from_millis(1600),
ttfb: Duration::from_millis(1599),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.itl_p50_ms - 100.0).abs() < 0.1);
assert!((result.decode_tok_per_sec - 10.0).abs() < 0.1);
}
#[test]
fn test_itl_single_token_excluded() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(100),
tokens: 1,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert_eq!(result.itl_p50_ms, 0.0);
assert_eq!(result.decode_tok_per_sec, 0.0);
assert!((result.avg_tok_per_req - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_aggregate_zero_elapsed() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 0.0, "test", 1, None, None, None, None);
assert_eq!(result.throughput_rps, 0.0);
assert_eq!(result.tokens_per_sec, 0.0);
}
#[test]
fn test_stddev() {
assert_eq!(stddev(&[]), 0.0);
assert_eq!(stddev(&[5.0]), 0.0);
let sd = stddev(&[10.0, 20.0, 30.0]);
assert!((sd - 10.0).abs() < 0.01);
}
#[test]
fn test_tpot_computation() {
let records = vec![RequestRecord {
latency: Duration::from_millis(200),
ttfb: Duration::from_millis(50),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.tpot_p50_ms - 10.0).abs() < 0.1);
}
#[test]
fn test_latency_min_max_stddev() {
let records = vec![
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
RequestRecord {
latency: Duration::from_millis(300),
ttfb: Duration::from_millis(100),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.latency_min_ms - 100.0).abs() < 0.1);
assert!((result.latency_max_ms - 300.0).abs() < 0.1);
assert!(result.latency_stddev_ms > 0.0);
}
#[test]
fn test_prompt_tokens_tracking() {
let records = vec![
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 20,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 15,
prompt_tokens: 25,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
},
];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert_eq!(result.prompt_tokens_total, 45);
assert_eq!(result.completion_tokens_total, 25);
}
#[test]
fn test_tpot_from_streaming_timestamps() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 5,
prompt_tokens: 10,
success: true,
token_timestamps: vec![
Duration::from_millis(50),
Duration::from_millis(60),
Duration::from_millis(70),
Duration::from_millis(80),
Duration::from_millis(90),
],
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.tpot_p50_ms - 10.0).abs() < 0.1);
assert!((result.itl_p50_ms - 10.0).abs() < 0.1);
assert!((result.decode_tok_per_sec - 100.0).abs() < 1.0);
}
#[test]
fn test_tpot_mixed_streaming_and_non_streaming() {
let records = vec![
RequestRecord {
latency: Duration::from_millis(200),
ttfb: Duration::from_millis(50),
tokens: 4,
prompt_tokens: 10,
success: true,
token_timestamps: vec![
Duration::from_millis(50),
Duration::from_millis(70),
Duration::from_millis(90),
Duration::from_millis(110),
],
brick_trace: None,
finish_reason: None,
response_content: None,
},
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 5,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(), brick_trace: None,
finish_reason: None,
response_content: None,
},
];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!((result.tpot_p50_ms - 20.0).abs() < 0.1);
}
#[test]
fn test_stream_config_default() {
let config = LoadTestConfig::default();
assert!(!config.stream);
}
#[test]
fn test_tpot_non_streaming_uses_latency_per_token() {
let records = vec![RequestRecord {
latency: Duration::from_millis(1600),
ttfb: Duration::from_millis(1599),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!(
(result.tpot_p50_ms - 100.0).abs() < 0.1,
"tpot={}",
result.tpot_p50_ms
);
assert!(
(result.itl_p50_ms - 100.0).abs() < 0.1,
"itl={}",
result.itl_p50_ms
);
}
#[test]
fn test_itl_robust_to_token_batching() {
let records = vec![RequestRecord {
latency: Duration::from_millis(350),
ttfb: Duration::from_millis(100),
tokens: 6,
prompt_tokens: 10,
success: true,
token_timestamps: vec![
Duration::from_millis(100), Duration::from_millis(100),
Duration::from_millis(100),
Duration::from_millis(300), Duration::from_millis(300),
Duration::from_millis(300),
],
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert!(
(result.itl_p50_ms - 40.0).abs() < 0.1,
"itl={}",
result.itl_p50_ms
);
assert!(
(result.tpot_p50_ms - 40.0).abs() < 0.1,
"tpot={}",
result.tpot_p50_ms
);
assert!(
(result.decode_tok_per_sec - 25.0).abs() < 0.5,
"decode={}",
result.decode_tok_per_sec
);
}
#[test]
fn test_request_details_populated() {
let records = vec![RequestRecord {
latency: Duration::from_millis(200),
ttfb: Duration::from_millis(50),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let result = aggregate_results(&records, 1.0, "test", 1, None, None, None, None);
assert_eq!(result.request_details.len(), 1);
let detail = &result.request_details[0];
assert!((detail.latency_ms - 200.0).abs() < 0.1);
assert!((detail.ttft_ms - 50.0).abs() < 0.1);
assert_eq!(detail.completion_tokens, 16);
assert_eq!(detail.prompt_tokens, 10);
assert!(detail.itl_ms > 0.0);
}
#[test]
fn test_quality_basic_all_pass() {
let records = vec![
RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
},
RequestRecord {
latency: Duration::from_millis(120),
ttfb: Duration::from_millis(60),
tokens: 8,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
},
];
let quality = compute_quality(&records, &ValidationMode::Basic);
assert_eq!(quality.total_validated, 2);
assert_eq!(quality.passed, 2);
assert_eq!(quality.failed, 0);
assert!((quality.pass_rate - 1.0).abs() < f64::EPSILON);
}
#[test]
fn test_quality_basic_zero_tokens() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(100),
tokens: 0,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
}];
let quality = compute_quality(&records, &ValidationMode::Basic);
assert_eq!(quality.failed, 1);
assert_eq!(quality.failures[0].reason, "zero_tokens");
}
#[test]
fn test_quality_basic_no_finish_reason() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let quality = compute_quality(&records, &ValidationMode::Basic);
assert_eq!(quality.failed, 1);
assert_eq!(quality.failures[0].reason, "no_finish_reason");
}
#[test]
fn test_quality_contains_match() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: Some("hello world".to_string()),
}];
let quality = compute_quality(&records, &ValidationMode::Contains("hello".to_string()));
assert_eq!(quality.passed, 1);
assert_eq!(quality.failed, 0);
}
#[test]
fn test_quality_contains_mismatch() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: Some("goodbye world".to_string()),
}];
let quality = compute_quality(&records, &ValidationMode::Contains("hello".to_string()));
assert_eq!(quality.failed, 1);
assert!(quality.failures[0].reason.starts_with("missing_substring:"));
}
#[test]
fn test_quality_none_skipped() {
let records = vec![RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 0,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: None,
response_content: None,
}];
let quality = compute_quality(&records, &ValidationMode::None);
assert_eq!(quality.validation_level, "none");
}
#[test]
fn test_quality_skips_failed_requests() {
let records = vec![
failed_record(), RequestRecord {
latency: Duration::from_millis(100),
ttfb: Duration::from_millis(50),
tokens: 10,
prompt_tokens: 5,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
},
];
let quality = compute_quality(&records, &ValidationMode::Basic);
assert_eq!(quality.total_validated, 1);
assert_eq!(quality.passed, 1);
}
#[test]
fn test_tail_analysis_basic() {
let records: Vec<RequestRecord> = (0..100)
.map(|i| RequestRecord {
latency: Duration::from_millis(100 + i),
ttfb: Duration::from_millis(50 + i / 2),
tokens: 20,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
})
.collect();
let tail = compute_tail_analysis(&records, 5.0);
assert!(tail.latency_p999_ms > 0.0);
assert!(tail.ttft_p999_ms > 0.0);
assert!(tail.tail_ratio_latency > 0.0);
}
#[test]
fn test_spike_detection() {
let mut records: Vec<RequestRecord> = (0..50)
.map(|_| RequestRecord {
latency: Duration::from_millis(200),
ttfb: Duration::from_millis(50),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
})
.collect();
records.push(RequestRecord {
latency: Duration::from_millis(2000),
ttfb: Duration::from_millis(50),
tokens: 16,
prompt_tokens: 10,
success: true,
token_timestamps: Vec::new(),
brick_trace: None,
finish_reason: Some("stop".to_string()),
response_content: None,
});
let tail = compute_tail_analysis(&records, 5.0);
assert!(tail.jitter.spike_threshold_ms > 0.0);
}
#[test]
fn test_linear_regression() {
let values: Vec<f64> = (0..10).map(|x| 2.0 * x as f64).collect();
let (slope, r2) = linear_regression(&values);
assert!((slope - 2.0).abs() < 0.01);
assert!((r2 - 1.0).abs() < 0.01);
}
#[test]
fn test_linear_regression_flat() {
let values = vec![5.0, 5.0, 5.0, 5.0, 5.0];
let (slope, _r2) = linear_regression(&values);
assert!(slope.abs() < 0.01);
}
#[test]
fn test_validation_mode_parse() {
assert!(matches!(
ValidationMode::parse("none"),
ValidationMode::None
));
assert!(matches!(
ValidationMode::parse("basic"),
ValidationMode::Basic
));
if let ValidationMode::Contains(s) = ValidationMode::parse("contains:hello") {
assert_eq!(s, "hello");
} else {
panic!("Expected Contains");
}
if let ValidationMode::Pattern(p) = ValidationMode::parse("pattern:\\d+") {
assert_eq!(p, "\\d+");
} else {
panic!("Expected Pattern");
}
}
#[test]
fn test_tail_analysis_empty() {
let records: Vec<RequestRecord> = Vec::new();
let tail = compute_tail_analysis(&records, 5.0);
assert_eq!(tail.itl_p999_ms, 0.0);
assert_eq!(tail.jitter.spike_count, 0);
assert!(!tail.drift.degradation_detected);
}