1//! One host under test, reached over HTTP: the same client for `whiskersd`, `wrangler dev` and `celld dev`. 2 3use std::time::{Duration, Instant}; 4 5/// The header that names a household in a host that was told to trust it (`.dev.vars`). 6pub const HOUSEHOLD_HEADER: &str = "x-whiskers-household"; 7 8/// What one request got back, as far as a device could tell. 9#[derive(Clone, Debug, PartialEq, Eq)] 10pub struct Reply { 11 pub status: u16, 12 pub content_type: Option<String>, 13 pub body: String, 14} 15 16/// A host's address and how to ask it things. 17#[derive(Clone)] 18pub struct Host { 19 base: String, 20 agent: ureq::Agent, 21} 22 23impl Host { 24 pub fn new(base: &str) -> Self { 25 let agent: ureq::Agent = ureq::Agent::config_builder() 26 .http_status_as_error(false) 27 .proxy(None) 28 .timeout_global(Some(Duration::from_secs(60))) 29 .build() 30 .into(); 31 Self { base: base.trim_end_matches('/').to_owned(), agent } 32 } 33 34 /// A request. `method` is any; `household` is sent as the household header. 35 pub fn send(&self, method: &str, path: &str, household: &str, body: &str) -> Result<Reply, String> { 36 let url = format!("{}{path}", self.base); 37 let request = ureq::http::Request::builder().method(method).uri(&url).header(HOUSEHOLD_HEADER, household); 38 let request = if method == "POST" { request.body(body.to_owned()) } else { request.body(String::new()) }.map_err(|e| e.to_string())?; 39 let mut response = self.agent.run(request).map_err(|e| format!("{method} {path}: {e}"))?; 40 let status = response.status().as_u16(); 41 let content_type = response.headers().get("content-type").and_then(|v| v.to_str().ok()).map(str::to_owned); 42 let body = response.body_mut().read_to_string().map_err(|e| format!("{method} {path}: reading the body: {e}"))?; 43 Ok(Reply { status, content_type, body }) 44 } 45 46 pub fn post(&self, path: &str, household: &str, body: &str) -> Result<Reply, String> { 47 self.send("POST", path, household, body) 48 } 49 50 /// Waits until the host answers a sync, for after a restart. 51 pub fn wait_ready(&self, within: Duration) -> Result<Duration, String> { 52 let started = Instant::now(); 53 loop { 54 let probe = self.post("/memory/sync", "readiness-probe", r#"{"facts":[],"forgotten":[]}"#); 55 if matches!(probe, Ok(Reply { status: 200, .. })) { 56 return Ok(started.elapsed()); 57 } 58 if started.elapsed() > within { 59 return Err(format!("the host was not ready within {within:?}: {probe:?}")); 60 } 61 std::thread::sleep(Duration::from_millis(200)); 62 } 63 } 64}