use std::collections::BTreeMap;
use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
pub struct Fixture {
pub url: String,
pub shutdown: Arc<AtomicBool>,
served: Arc<AtomicUsize>,
errors: Arc<Mutex<Vec<String>>>,
}
impl Fixture {
pub fn served(&self) -> usize {
self.served.load(Ordering::Relaxed)
}
pub fn transport_errors(&self) -> Vec<String> {
self.errors.lock().unwrap().clone()
}
pub fn stop(&self) {
self.shutdown.store(true, Ordering::Relaxed);
}
}
pub fn serve_html(html: String) -> Fixture {
serve(html, BTreeMap::new())
}
pub fn serve(html: String, headers: BTreeMap<String, String>) -> Fixture {
let listener = TcpListener::bind("127.0.0.1:0").expect("failed to bind");
let port = listener.local_addr().unwrap().port();
let url = format!("http://127.0.0.1:{port}");
let shutdown = Arc::new(AtomicBool::new(false));
let served = Arc::new(AtomicUsize::new(0));
let errors: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let shutdown_thread = shutdown.clone();
let served_thread = served.clone();
let errors_thread = errors.clone();
thread::spawn(move || {
listener
.set_nonblocking(true)
.expect("cannot set non-blocking");
let mut header_lines = String::new();
for (key, value) in &headers {
header_lines.push_str(&format!("{key}: {value}\r\n"));
}
let response = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nContent-Length: {}\r\n{}\r\n{}",
html.len(),
header_lines,
html
);
let response = Arc::new(response);
loop {
if shutdown_thread.load(Ordering::Relaxed) {
break;
}
match listener.accept() {
Ok((stream, _)) => {
let response = response.clone();
let served = served_thread.clone();
let errors = errors_thread.clone();
thread::spawn(move || {
serve_one(stream, &response, &served, &errors);
});
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
Err(e) => {
errors_thread
.lock()
.unwrap()
.push(format!("accept failed: {e}"));
break;
}
}
}
});
Fixture {
url,
shutdown,
served,
errors,
}
}
fn serve_one(
mut stream: std::net::TcpStream,
response: &str,
served: &Arc<AtomicUsize>,
errors: &Arc<Mutex<Vec<String>>>,
) {
let note = |msg: String| errors.lock().unwrap().push(msg);
if let Err(e) = stream.set_nonblocking(false) {
note(format!("set_nonblocking(false) failed: {e}"));
return;
}
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
let _ = stream.set_write_timeout(Some(Duration::from_secs(5)));
let mut request = Vec::new();
let mut buf = [0u8; 1024];
loop {
match stream.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
request.extend_from_slice(&buf[..n]);
if request.windows(4).any(|w| w == b"\r\n\r\n") {
break;
}
}
Err(e) => {
note(format!("read failed: {e}"));
break;
}
}
}
match stream
.write_all(response.as_bytes())
.and_then(|()| stream.flush())
{
Ok(()) => {
served.fetch_add(1, Ordering::Relaxed);
}
Err(e) => note(format!("write failed: {e}")),
}
}