mod support; use std::ffi::OsString; use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; use std::process::Command; use std::time::{Duration, Instant}; use support::{checked, rejected, sources, stdout}; const ROUTE: &str = r#" route(request: dever.http.Request) (response: dever.http.Response) recover("route policy") { response = dever.http.Response { status = 201 headers = [dever.http.header("Set-Cookie", "a=1"), dever.http.header("Set-Cookie", "b=2")] body = request.body } } "#; const LOCAL_SERVER: &str = r#" exchange(result: dever.net.PortResult.Bound(port), listener: dever.system.Listener) (answer: Bool) recover("test reports network failures") { server = run(dever.http.serve(route, listener, dever.http.default_limits())) request = dever.http.Request { method = "POST" target = "/echo" headers = [dever.http.header("Host", "test")] body = dever.bytes.from_text("hello") } answer = received(result(dever.http.send("127.0.0.1", port, request, dever.http.default_limits()))) stop(server) dever.net.close_listener(listener) } exchange(result: dever.net.PortResult.Failed(message), listener: dever.system.Listener) (answer: Bool) recover("test reports network failures") { answer = false } listening(result: dever.net.ListenResult.Listening(listener)) (answer: Bool) recover("test reports network failures") { answer = exchange(result(dever.net.port(listener)), listener) } listening(result: dever.net.ListenResult.Failed(message)) (answer: Bool) recover("test reports network failures") { answer = false } received(result: dever.http.ResponseResult.Read(response)) (answer: Bool) recover("test reports network failures") { answer = response.status == 201 and response.body == dever.bytes.from_text("hello") and length(response.headers) >= 2 and cookie(dever.http.header_value(response.headers, "SET-COOKIE")) } received(result: dever.http.ResponseResult.Failed(message)) (answer: Bool) recover("test reports network failures") { answer = false } cookie(value: null) (answer: Bool) { answer = false } cookie(value: Bytes) (answer: Bool) { answer = value == dever.bytes.from_text("a=1") } main() (answer: Bool) { answer = listening(result(dever.net.listen("127.0.0.1", 0))) } "#; fn local_source(http2: bool) -> String { let protocol = if http2 { "dever.http.default_http2_limits()" } else { "null" }; let server = LOCAL_SERVER.replace("dever.http.default_limits()", "http_limits()"); format!( r#"{ROUTE} {server} http_limits() (limits: dever.http.Limits) {{ limits = dever.http.default_limits() limits.http2 = {protocol} }} "# ) } #[test] fn native_http_uses_static_handlers_and_runs_under_structured_tasks() { let source = local_source(false); let map = sources(&source); let program = checked(&map); let generated = dever_core::native::emit(&program, &map, "main.main").unwrap(); assert!(generated.contains("dever_runtime::http::serve(")); assert!(generated.contains("dever_runtime::http::send(")); assert!(!generated.contains("Box dever_core::native::NativeProgram { let sources = sources(source); let rustc = std::env::var_os("RUSTC").unwrap_or_else(|| OsString::from("rustc")); dever_core::native::compile(&checked(&sources), &sources, "main.main", &rustc).unwrap() } fn accept_before(listener: &TcpListener) -> TcpStream { let deadline = Instant::now() + Duration::from_secs(5); loop { match listener.accept() { Ok((socket, _)) => return socket, Err(error) if error.kind() == std::io::ErrorKind::WouldBlock && Instant::now() < deadline => { std::thread::sleep(Duration::from_millis(5)) } Err(error) => panic!("test peer accept failed: {error}"), } } } #[test] fn native_client_matches_independent_wire_and_retains_repeated_response_headers() { let listener = TcpListener::bind(("127.0.0.1", 0)).unwrap(); listener.set_nonblocking(true).unwrap(); let port = listener.local_addr().unwrap().port(); let native = compile(&format!( r#"received(result: dever.http.ResponseResult.Read(response)) (answer: Bool) recover("test reports wire failure") {{ answer = response.status == 201 and response.body == dever.bytes.from_text("abc") and length(response.headers) == 3 }} received(result: dever.http.ResponseResult.Failed(message)) (answer: Bool) recover("test reports wire failure") {{ answer = false }} public main() (answer: Bool) {{ request = dever.http.Request {{ method = "POST" target = "/echo" headers = [dever.http.header("Host", "example.test")] body = dever.bytes.from_text("x") }} answer = received(result(dever.http.send("127.0.0.1", {port}, request, dever.http.default_limits()))) }} "# )); let peer = std::thread::spawn(move || { let mut socket = accept_before(&listener); socket .set_read_timeout(Some(Duration::from_secs(2))) .unwrap(); socket .set_write_timeout(Some(Duration::from_secs(2))) .unwrap(); let expected = b"POST /echo HTTP/1.1\r\nhost: example.test\r\ncontent-length: 1\r\n\r\nx"; let mut wire = vec![0; expected.len()]; socket.read_exact(&mut wire).unwrap(); assert_eq!(wire, expected); for piece in [ b"HTTP/1.1 201 Created\r\nTransfer-Encoding: chunked\r\nSet-Cookie: a=1\r\n".as_slice(), b"Set-Cookie: b=2\r\n\r\n1\r\na\r\n", b"2\r\nbc\r\n0\r\n\r\n", ] { socket.write_all(piece).unwrap(); std::thread::sleep(Duration::from_millis(5)); } }); let output = Command::new(native.executable()).output().unwrap(); peer.join().unwrap(); assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); assert_eq!(String::from_utf8(output.stdout).unwrap(), "answer = true\n"); } #[test] fn http_and_server_examples_pass_checking_and_format_roundtrips() { for (path, check_source) in [ ("library/dever/http.dever", false), ("examples/old/dever/http/module/main.dever", true), ] { let disk_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) .join("../..") .join(path); let source = std::fs::read_to_string(disk_path).unwrap(); let mut map = dever_core::source::SourceMap::default(); let id = map.add(path, source); let first = dever_core::format::format(map.get(id)).unwrap(); let mut formatted = dever_core::source::SourceMap::default(); let id = formatted.add("main.dever", first.clone()); assert_eq!( dever_core::format::format(formatted.get(id)).unwrap(), first ); if check_source { checked(&formatted); } } }