blackopsrepl's picture
test(app): add deliveries validation suites
c5a0215
use std::sync::Arc;
use std::time::{Duration, Instant};
use axum::body::Body;
use axum::http::{Request, StatusCode};
use http_body_util::BodyExt;
use solverforge_deliveries::api;
use solverforge_deliveries::data::{generate, DemoData};
use solverforge_deliveries::domain::{Plan, RoutingMode};
use tower::ServiceExt;
use tower_http::services::ServeDir;
pub fn test_app() -> axum::Router {
let state = Arc::new(api::AppState::new());
api::router(state)
.merge(solverforge_ui::routes())
.fallback_service(ServeDir::new("static"))
}
pub fn small_plan() -> serde_json::Value {
let mut plan = generate(DemoData::Hartford);
plan.deliveries.truncate(8);
plan.vehicles.truncate(3);
plan.normalize();
plan.routing_mode = RoutingMode::StraightLine;
serde_json::to_value(plan.refreshed_for_transport()).expect("plan should serialize")
}
pub fn bigger_plan() -> serde_json::Value {
let mut plan = generate(DemoData::Philadelphia);
plan.routing_mode = RoutingMode::StraightLine;
serde_json::to_value(plan.refreshed_for_transport()).expect("plan should serialize")
}
pub fn completion_plan() -> serde_json::Value {
let mut plan = generate(DemoData::Hartford);
plan.deliveries.truncate(6);
plan.vehicles.truncate(2);
plan.normalize();
plan.routing_mode = RoutingMode::StraightLine;
serde_json::to_value(plan.refreshed_for_transport()).expect("plan should serialize")
}
pub fn empty_road_network_plan() -> serde_json::Value {
let mut plan = Plan::new("Empty Road Network", Vec::new(), Vec::new());
plan.routing_mode = RoutingMode::RoadNetwork;
serde_json::to_value(plan.refreshed_for_transport()).expect("plan should serialize")
}
pub fn small_road_network_plan() -> serde_json::Value {
let mut plan = generate(DemoData::Hartford);
plan.deliveries.truncate(4);
plan.vehicles.truncate(2);
plan.normalize();
plan.routing_mode = RoutingMode::RoadNetwork;
serde_json::to_value(plan.refreshed_for_transport()).expect("plan should serialize")
}
pub async fn read_json(response: axum::response::Response) -> serde_json::Value {
let body = read_body_text(response).await;
serde_json::from_str(&body).expect("body should be valid JSON")
}
pub async fn read_body_text(response: axum::response::Response) -> String {
let body = response
.into_body()
.collect()
.await
.expect("body should collect")
.to_bytes();
String::from_utf8(body.to_vec()).expect("body should be valid UTF-8")
}
pub fn assert_score_value<'a>(value: &'a serde_json::Value, label: &str) -> &'a str {
let score = value
.as_str()
.unwrap_or_else(|| panic!("{label} should be a display string, got {value:?}"));
assert_score_text(score, label);
score
}
pub fn assert_score_text(score: &str, label: &str) {
assert!(
score.contains("hard/") && score.ends_with("soft") && !score.trim_start().starts_with('{'),
"{label} should use SolverForge display-score format, got {score:?}"
);
}
pub async fn poll_job_state(app: &axum::Router, job_id: &str, wanted: &str) -> serde_json::Value {
let start = Instant::now();
loop {
let response = app
.clone()
.oneshot(
Request::get(format!("/jobs/{job_id}"))
.body(Body::empty())
.expect("request should build"),
)
.await
.expect("request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let json = read_json(response).await;
if json["lifecycleState"] == wanted {
return json;
}
if start.elapsed() > Duration::from_secs(6) {
panic!("job {job_id} did not reach {wanted} in time; last state={json:?}");
}
tokio::time::sleep(Duration::from_millis(80)).await;
}
}