use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::Duration;
use cloud::{
LocalStaticOptions, MesofactStaticReconciler, MirrorConfig, MirrorProviderSlot, MirrorShape,
Provider, ProviderScope, ReconcileCtx, Reconciler, ServiceComponent, ServiceConfig,
};
fn workspace_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.ancestors()
.nth(3)
.expect("workspace root from manifest dir")
.to_path_buf()
}
fn pick_port() -> u16 {
let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let p = l.local_addr().unwrap().port();
drop(l);
p
}
#[tokio::test]
async fn local_static_reconciler_brings_up_app_yah_web() {
let Ok(bin) = std::env::var("YAH_RECONCILER_E2E_BIN") else {
eprintln!("skipping: set YAH_RECONCILER_E2E_BIN=<path/to/mesofact-dev> to run");
return;
};
let bin = PathBuf::from(bin);
assert!(
bin.exists(),
"YAH_RECONCILER_E2E_BIN points at missing file: {}",
bin.display(),
);
let workspace = workspace_root();
let workload_path = workspace.join("app/yah/web/marketing");
assert!(
workload_path.join("workload.toml").exists(),
"expected app/yah/web/marketing/workload.toml under {}",
workspace.display(),
);
let port = pick_port();
let mut fields = BTreeMap::new();
fields.insert("port".to_string(), toml::Value::Integer(port as i64));
let static_slot = MirrorProviderSlot::Inline {
kind: Provider::LocalStatic,
fields,
};
let mut providers = BTreeMap::new();
providers.insert("static".to_string(), static_slot);
let mirror = MirrorConfig {
schema_version: 1,
shape: MirrorShape::Local,
providers,
ingress: Default::default(),
ingress_machines: Vec::new(),
drivers: Default::default(),
asset_aliases: Default::default(),
};
let service = ServiceConfig {
schema_version: 1,
name: "dev-yah".to_string(),
domain: "yah.dev".to_string(),
components: vec![],
db: cloud::DbCatalog::default(),
};
let component = ServiceComponent {
mount: None,
id: "site".to_string(),
kind: "mesofact-static".to_string(),
path: "app/yah/web/marketing".to_string(),
role: "static".to_string(),
publishes: None,
git: None,
wave: 0,
deploy: Default::default(),
};
let reconciler = MesofactStaticReconciler::new().with_local_static(LocalStaticOptions {
binary: Some(bin),
extra_args: vec![],
ready_timeout: Some(Duration::from_secs(30)),
..LocalStaticOptions::default()
});
let ctx = ReconcileCtx {
workspace_root: &workspace,
service: &service,
component: &component,
mirror: &mirror,
env: "local",
scope: ProviderScope::singleton(),
};
let running = reconciler
.up(ctx)
.await
.expect("reconciler.up succeeds against real workload");
let dev_url = running
.dev_url
.clone()
.expect("local-static path sets dev_url");
eprintln!("reconciler reported dev_url={dev_url}");
assert!(dev_url.starts_with(&format!("http://127.0.0.1:{port}")));
assert_eq!(running.kind, "mesofact-static");
assert_eq!(running.slot, "static");
let client = reqwest::Client::new();
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
let body = loop {
let response = client
.get(&dev_url)
.send()
.await
.expect("GET / against reconciled dev_url");
if response.status() == 200 {
break response.text().await.expect("read body");
}
if tokio::time::Instant::now() >= deadline {
panic!(
"GET / never returned 200 (last status {}); initial build may have failed",
response.status(),
);
}
tokio::time::sleep(Duration::from_millis(200)).await;
};
assert!(
body.contains("<!doctype html>") || body.contains("<!DOCTYPE html>"),
"expected the served body to be HTML, got: {}",
&body[..body.len().min(120)],
);
running.shutdown().await.expect("shutdown succeeds");
tokio::time::sleep(Duration::from_millis(250)).await;
let connect = tokio::net::TcpStream::connect(format!("127.0.0.1:{port}")).await;
assert!(
connect.is_err(),
"port {port} still accepting connections after shutdown"
);
}