mod common;
use std::time::Duration;
const METRICS_YAML: &str = r#"
version: 2
defaults:
rate: 10
duration: 30s
encoder:
type: prometheus_text
sink:
type: stdout
scenarios:
- id: test_metric
signal_type: metrics
name: test_metric
generator:
type: constant
value: 42.0
"#;
const LOGS_YAML: &str = r#"
version: 2
defaults:
rate: 10
duration: 30s
encoder:
type: json_lines
sink:
type: stdout
scenarios:
- id: test_log
signal_type: logs
name: test_log
log_generator:
type: template
templates:
- message: "integration test log line"
"#;
#[test]
fn full_lifecycle_metrics_and_logs() {
let (port, _guard) = common::start_server();
let base = format!("http://127.0.0.1:{port}");
let client = common::http_client();
let resp = client
.post(format!("{base}/scenarios"))
.header("Content-Type", "text/yaml")
.body(METRICS_YAML)
.send()
.expect("POST metrics scenario must succeed");
assert_eq!(
resp.status().as_u16(),
201,
"POST metrics scenario must return 201 Created"
);
let metrics_body: serde_json::Value = resp.json().expect("response must be valid JSON");
let metrics_id = metrics_body["id"]
.as_str()
.expect("response must have an id field")
.to_string();
assert_eq!(
metrics_body["name"].as_str(),
Some("test_metric"),
"metrics scenario name must match"
);
let metrics_state = metrics_body["state"].as_str().unwrap_or("");
assert!(
matches!(metrics_state, "pending" | "running"),
"metrics scenario state must be pending or running, got {metrics_state:?}"
);
let resp = client
.post(format!("{base}/scenarios"))
.header("Content-Type", "text/yaml")
.body(LOGS_YAML)
.send()
.expect("POST logs scenario must succeed");
assert_eq!(
resp.status().as_u16(),
201,
"POST logs scenario must return 201 Created"
);
let logs_body: serde_json::Value = resp.json().expect("response must be valid JSON");
let logs_id = logs_body["id"]
.as_str()
.expect("response must have an id field")
.to_string();
assert_eq!(
logs_body["name"].as_str(),
Some("test_log"),
"logs scenario name must match"
);
let logs_state = logs_body["state"].as_str().unwrap_or("");
assert!(
matches!(logs_state, "pending" | "running"),
"logs scenario state must be pending or running, got {logs_state:?}"
);
let resp = client
.get(format!("{base}/scenarios"))
.send()
.expect("GET /scenarios must succeed");
assert_eq!(resp.status().as_u16(), 200);
let list: serde_json::Value = resp.json().expect("response must be valid JSON");
let scenarios = list["scenarios"]
.as_array()
.expect("response must have a scenarios array");
assert!(
scenarios.len() >= 2,
"GET /scenarios must list at least 2 scenarios, got {}",
scenarios.len()
);
let ids_in_list: Vec<&str> = scenarios.iter().filter_map(|s| s["id"].as_str()).collect();
assert!(
ids_in_list.contains(&metrics_id.as_str()),
"metrics scenario must be in list"
);
assert!(
ids_in_list.contains(&logs_id.as_str()),
"logs scenario must be in list"
);
for s in scenarios {
if s["id"].as_str() == Some(metrics_id.as_str())
|| s["id"].as_str() == Some(logs_id.as_str())
{
let state = s["state"].as_str().unwrap_or("");
assert!(
matches!(state, "pending" | "running"),
"scenario {} must be pending or running, got {:?}",
s["id"],
state
);
}
}
std::thread::sleep(Duration::from_secs(3));
for (label, id) in [("metrics", &metrics_id), ("logs", &logs_id)] {
let resp = client
.get(format!("{base}/scenarios/{id}/stats"))
.send()
.unwrap_or_else(|_| panic!("GET /scenarios/{id}/stats must succeed"));
assert_eq!(
resp.status().as_u16(),
200,
"GET /scenarios/{id}/stats must return 200"
);
let stats: serde_json::Value = resp.json().expect("stats response must be valid JSON");
let total_events = stats["total_events"]
.as_u64()
.expect("stats must have total_events");
assert!(
total_events > 0,
"{label} scenario must have emitted events after 3 seconds, got total_events={total_events}"
);
}
for (label, id) in [("metrics", &metrics_id), ("logs", &logs_id)] {
let resp = client
.delete(format!("{base}/scenarios/{id}"))
.send()
.unwrap_or_else(|_| panic!("DELETE /scenarios/{id} must succeed"));
assert_eq!(
resp.status().as_u16(),
200,
"DELETE {label} scenario must return 200"
);
let del_body: serde_json::Value = resp.json().expect("delete response must be valid JSON");
assert_eq!(
del_body["id"].as_str(),
Some(id.as_str()),
"delete response must echo the scenario id"
);
assert!(
del_body["status"].as_str() == Some("stopped")
|| del_body["status"].as_str() == Some("force_stopped"),
"{label} scenario status must be stopped or force_stopped, got {:?}",
del_body["status"]
);
assert!(
del_body["total_events"].as_u64().unwrap_or(0) > 0,
"{label} scenario must report non-zero total_events after deletion"
);
}
let resp = client
.get(format!("{base}/scenarios"))
.send()
.expect("GET /scenarios must succeed after deletions");
assert_eq!(resp.status().as_u16(), 200);
let list: serde_json::Value = resp.json().expect("response must be valid JSON");
let scenarios = list["scenarios"]
.as_array()
.expect("response must have a scenarios array");
assert!(
scenarios.is_empty(),
"GET /scenarios must return empty list after all scenarios are deleted, got {} entries",
scenarios.len()
);
}
const NON_GATED_METRIC_YAML: &str = r#"
version: 2
defaults:
rate: 5
duration: 2s
encoder:
type: prometheus_text
sink:
type: stdout
scenarios:
- id: state_lifecycle
signal_type: metrics
name: state_lifecycle
generator:
type: constant
value: 1.0
"#;
#[test]
fn non_gated_scenario_state_transitions_running_to_finished() {
let (port, _guard) = common::start_server();
let base = format!("http://127.0.0.1:{port}");
let client = common::http_client();
let resp = client
.post(format!("{base}/scenarios"))
.header("Content-Type", "text/yaml")
.body(NON_GATED_METRIC_YAML)
.send()
.expect("POST scenario must succeed");
assert_eq!(resp.status().as_u16(), 201, "POST must return 201");
let body: serde_json::Value = resp.json().expect("response must be valid JSON");
let id = body["id"]
.as_str()
.expect("response must have id")
.to_string();
let mut saw_running = false;
let deadline = std::time::Instant::now() + Duration::from_millis(1500);
while std::time::Instant::now() < deadline {
let resp = client
.get(format!("{base}/scenarios/{id}/stats"))
.send()
.expect("GET stats must succeed");
assert_eq!(resp.status().as_u16(), 200);
let stats: serde_json::Value = resp.json().expect("stats JSON");
if stats["state"].as_str() == Some("running") {
saw_running = true;
break;
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(
saw_running,
"non-gated scenario must transition through 'running' state on /stats"
);
let finish_deadline = std::time::Instant::now() + Duration::from_secs(5);
let mut last_state = String::new();
while std::time::Instant::now() < finish_deadline {
let resp = client
.get(format!("{base}/scenarios/{id}/stats"))
.send()
.expect("GET stats must succeed");
if resp.status().as_u16() != 200 {
break;
}
let stats: serde_json::Value = resp.json().expect("stats JSON");
last_state = stats["state"].as_str().unwrap_or("").to_string();
if last_state == "finished" {
break;
}
std::thread::sleep(Duration::from_millis(100));
}
assert_eq!(
last_state, "finished",
"non-gated scenario must terminate in 'finished' state after duration"
);
}