Repository: bearcove/loona (language: rust)

Issue:
Add httpwg macros to generate (or return?) Catalog
httpwg-cli currently hardcodes 3 tests, but httpwg-gen already has everything we'd want to to generate a macro that generates code that generates a Catalog that can then be filtered at runtime — the hard work is already done.


Discussion / hints:

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index a70a87c7..81f87881 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -756,6 +756,7 @@ dependencies = [
  "color-eyre",
  "eyre",
  "httpwg",
+ "httpwg-macros",
  "lexopt",
  "libc",
  "tokio",
diff --git a/Justfile b/Justfile
index 7f9749ca..1b70fc2d 100644
--- a/Justfile
+++ b/Justfile
@@ -1,5 +1,7 @@
 # just manual: https://github.com/casey/just#readme
 
+set positional-arguments
+
 _default:
 	just --list
 
@@ -56,17 +58,14 @@ tls-sample:
 httpwg-gen:
     cargo run --release --package httpwg-gen
 
-httpwg-over-tcp *args:
+httpwg-over-tcp *args='':
     #!/usr/bin/env -S bash -eux
     cargo build --release \
         --package httpwg-loona \
         --package httpwg-cli
     export TEST_PROTO=h2
     export PORT=8001
-    ./target/release/httpwg \
-        --address localhost:8001 \
-        {{args}} \
-        -- ./target/release/httpwg-loona
+    ./target/release/httpwg --address localhost:8001 "$@" -- ./target/release/httpwg-loona
 
 instruments:
     #!/usr/bin/env -S bash -eux
diff --git a/crates/httpwg-cli/Cargo.toml b/crates/httpwg-cli/Cargo.toml
index d5042f90..eac42d48 100644
--- a/crates/httpwg-cli/Cargo.toml
+++ b/crates/httpwg-cli/Cargo.toml
@@ -24,3 +24,4 @@ libc = "0.2.155"
 tokio = { version = "1.39.2", features = ["time"] }
 tracing = "0.1.40"
 tracing-subscriber = { version = "0.3.18" }
+httpwg-macros = { version = "0.2.0", path = "../httpwg-macros" }
diff --git a/crates/httpwg-cli/src/main.rs b/crates/httpwg-cli/src/main.rs
index 29dbf3bd..4d3e75af 100644
--- a/crates/httpwg-cli/src/main.rs
+++ b/crates/httpwg-cli/src/main.rs
@@ -1,10 +1,9 @@
 use std::{
-    collections::HashMap, ffi::OsString, future::Future, net::SocketAddr, pin::Pin, rc::Rc,
-    time::Duration,
+    cell::RefCell, collections::HashMap, ffi::OsString, net::SocketAddr, rc::Rc, time::Duration,
 };
 
 use buffet::{net::TcpStream, IntoHalves};
-use httpwg::{rfc9113, Config, Conn};
+use httpwg::{Config, Conn};
 use tracing::Level;
 use tracing_subscriber::{filter::Targets, layer::SubscriberExt, util::SubscriberInitExt};
 
@@ -21,6 +20,9 @@ struct Args {
 
     /// which tests to run
     filter: Option<String>,
+
+    /// whether to print verbose output
+    verbose: bool,
 }
 
 pub trait IntoStringResult {
@@ -70,6 +72,9 @@ fn parse_args() -> eyre::Result<Args> {
             lexopt::Arg::Long("filter") | lexopt::Arg::Short('f') => {
                 args.filter = Some(parser.value()?.into_string_result()?);
             }
+            lexopt::Arg::Long("verbose") | lexopt::Arg::Short('v') => {
+                args.verbose = true;
+            }
             lexopt::Arg::Value(value) => {
                 args.server_binary.push(value.into_string_result()?);
             }
@@ -87,6 +92,7 @@ Options:
     -a, --address <ADDRESS>    The address/port the server will listen on
     -t, --connect-timeout <MS> The timeout for connections in milliseconds
     -f, --filter <FILTER>      Which tests to run
+    -v, --verbose              Print verbose output
 
 Arguments:
     SERVER                     The server to run tests against
@@ -116,7 +122,7 @@ fn main() -> eyre::Result<()> {
     Ok(())
 }
 
-async fn async_main(args: Args) -> eyre::Result<()> {
+async fn async_main(mut args: Args) -> eyre::Result<()> {
     let cat = catalog::<buffet::net::TcpStream>();
 
     let addr = match args.server_address {
@@ -137,8 +143,16 @@ async fn async_main(args: Args) -> eyre::Result<()> {
     });
 
     eprintln!("Will run tests against {addr} with a connect timeout of {connect_timeout:?}");
+
+    // this works around an oddity of Just when forwarding positional arguments
+    args.server_binary.retain(|s| !s.is_empty());
+
+    let mut server_name = format!("a server listening on {addr}");
+
     if !args.server_binary.is_empty() {
         let binary_and_args = args.server_binary;
+        let binary_name = &binary_and_args[0];
+        server_name = format!("{binary_name} listening on {addr}");
 
         eprintln!(
             "Launching ({}) now and waiting until it listens on {addr}",
@@ -185,7 +199,16 @@ async fn async_main(args: Args) -> eyre::Result<()> {
         panic!("Server did not start listening within 3 seconds");
     }
 
-    let local_set = tokio::task::LocalSet::new();
+    let mut local_set = tokio::task::LocalSet::new();
+
+    let sequential = std::env::var("SEQUENTIAL")
+        .map(|v| v == "1")
+        .unwrap_or(false);
+
+    let mut num_tests = 0;
+    let num_passed: Rc<RefCell<usize>> = Rc::new(RefCell::new(0));
+
+    let start_time = std::time::Instant::now();
 
     for (rfc, sections) in cat {
         for (section, tests) in sections {
@@ -193,32 +216,61 @@ async fn async_main(args: Args) -> eyre::Result<()> {
                 let test_name = format!("{rfc} :: {section} :: {test}");
                 if let Some(filter) = &args.filter {
                     if !test_name.contains(filter) {
-                        println!("Skipping test: {}", test_name);
                         continue;
                     }
                 }
 
+                num_tests += 1;
                 let stream =
                     tokio::time::timeout(Duration::from_millis(250), TcpStream::connect(addr))
                         .await
                         .unwrap()
                         .unwrap();
                 let conn = Conn::new(conf.clone(), stream);
+                let num_passed = num_passed.clone();
                 let test = async move {
-                    println!("🔷 Running test: {}", test_name);
-                    boxed_test(conn).await.unwrap();
-                    println!("✅ Test passed: {}", test_name);
+                    if args.verbose {
+                        eprintln!("🔷 Running test: {}", test_name);
+                    }
+                    match boxed_test(conn).await {
+                        Ok(()) => {
+                            eprintln!("✅ Test passed: {}", test_name);
+                            {
+                                *num_passed.borrow_mut() += 1;
+                            }
+                        }
+                        Err(e) => {
+                            eprintln!("❌ Test failed: {}\n{e:?}", test_name);
+                        }
+                    }
                 };
                 local_set.spawn_local(async move {
                     {
                         test.await;
                     }
                 });
+                if sequential {
+                    (&mut local_set).await;
+                }
             }
         }
     }
 
     local_set.await;
+    let num_passed = *num_passed.borrow();
+
+    eprintln!(
+        "🚄 Passed \x1b[1;32m{}/{}\x1b[0m tests in \x1b[1;33m{:.2}\x1b[0m seconds against \x1b[1;36m{}\x1b[0m",
+        num_passed,
+        num_tests,
+        start_time.elapsed().as_secs_f32(),
+        server_name,
+    );
+
+    if num_passed != num_tests {
+        eprintln!("❌ Some tests failed");
+        std::process::exit(1);
+    }
 
     Ok(())
 }
@@ -263,39 +315,5 @@ fn setup_tracing_and_error_reporting() {
         .with(fmt_layer)
         .init();
 }
-type BoxedTest<IO> = Box<dyn Fn(Conn<IO>) -> Pin<Box<dyn Future<Output = eyre::Result<()>>>>>;
-
-pub fn catalog<IO: IntoHalves>(
-) -> HashMap<&'static str, HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>> {
-    let mut rfcs: HashMap<
-        &'static str,
-        HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>,
-    > = Default::default();
-
-    {
-        let mut sections: HashMap<&'static str, _> = Default::default();
-
-        {
-            use rfc9113::_8_expressing_http_semantics_in_http2 as s;
-            let mut section8: HashMap<&'static str, BoxedTest<IO>> = Default::default();
-            section8.insert(
-                "client sends push promise frame",
-                Box::new(|conn: Conn<IO>| Box::pin(s::client_sends_push_promise_frame(conn))),
-            );
-            section8.insert(
-                "sends connect with scheme",
-                Box::new(|conn: Conn<IO>| Box::pin(s::sends_connect_with_scheme(conn))),
-            );
-            section8.insert(
-                "sends connect with path",
-                Box::new(|conn: Conn<IO>| Box::pin(s::sends_connect_with_path(conn))),
-            );
-
-            sections.insert("Section 8: Expressing HTTP Semantics in HTTP/2", section8);
-        }
 
-        rfcs.insert("RFC 9113", sections);
-    }
-
-    rfcs
-}
+httpwg_macros::gen_catalog!(catalog);
diff --git a/crates/httpwg-gen/src/main.rs b/crates/httpwg-gen/src/main.rs
index 34091c1d..796e65c5 100644
--- a/crates/httpwg-gen/src/main.rs
+++ b/crates/httpwg-gen/src/main.rs
@@ -290,6 +290,59 @@ fn main() {
         }
         w!("}}");
 
+        w!("");
+        w!("/// This generates a function that returns a Catalog of type");
+        w!("#[macro_export]");
+        w!("macro_rules! gen_catalog {{");
+        w!("  ($catalog_fn_name:ident) => {{");
+        w!("    use ::httpwg::BoxedTest;");
+        w!("    pub fn $catalog_fn_name<IO: IntoHalves>() -> HashMap<&'static str, HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>> {{");
+        w!("        let mut rfcs: HashMap<&'static str, HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>> = Default::default();");
+        w!("");
+        for suite in &suites {
+            {
+                let suite_name = &suite.name;
+                let pretty_suite_name = suite_name.to_uppercase().replace("RFC", "RFC ");
+                w!("        {{");
+                w!("            let mut sections: HashMap<&'static str, _> = Default::default();");
+                w!("");
+                for group in &suite.groups {
+                    {
+                        let group_name = &group.name;
+                        let pretty_group_name = group_name.strip_prefix('_').unwrap_or(group_name);
+                        let pretty_group_name = pretty_group_name.replace('_', " ");
+                        let pretty_group_name = pretty_group_name.replacen(' ', ". ", 1);
+                        let pretty_group_name = pretty_group_name.trim();
+                        w!("            {{");
+                        w!("                use ::httpwg::{suite_name}::{group_name} as s;");
+                        w!("                let mut {group_name}: HashMap<&'static str, BoxedTest<IO>> = Default::default();");
+                        w!("");
+                        for test in &group.tests {
+                            {
+                                let test_name = &test.name;
+                                let pretty_test_name = test_name.replace('_', " ");
+                                w!("                {group_name}.insert(");
+                                w!("                    \"{pretty_test_name}\",");
+                                w!("                    Box::new(|conn: Conn<IO>| Box::pin(s::{test_name}(conn))),");
+                                w!("                );");
+                            }
+                        }
+                        w!("");
+                        w!("                sections.insert(\"{pretty_group_name}\", {group_name});");
+                        w!("            }}");
+                    }
+                }
+                w!("");
+                w!("            rfcs.insert(\"{pretty_suite_name}\", sections);");
+                w!("        }}");
+            }
+        }
+        w!("");
+        w!("        rfcs");
+        w!("    }}");
+        w!("  }}");
+        w!("}}");
+
         out.flush().unwrap();
     }
 
diff --git a/crates/httpwg-loona/Cargo.toml b/crates/httpwg-loona/Cargo.toml
index 43556e93..844f6240 100644
--- a/crates/httpwg-loona/Cargo.toml
+++ b/crates/httpwg-loona/Cargo.toml
@@ -12,5 +12,5 @@ A reference HTTP 1+2 server for httpwg, powered by loona
 color-eyre = "0.6.3"
 loona = { version = "0.2.0", path = "../loona" }
 buffet = { version = "0.2.0", path = "../buffet" }
-tracing = { version = "0.1.40", features = ["release_max_level_info"] }
+tracing = { version = "0.1.40", features = ["release_max_level_debug"] }
 tracing-subscriber = "0.3.18"
diff --git a/crates/httpwg-loona/src/main.rs b/crates/httpwg-loona/src/main.rs
index b01f3090..7b1dbb95 100644
--- a/crates/httpwg-loona/src/main.rs
+++ b/crates/httpwg-loona/src/main.rs
@@ -37,7 +37,7 @@ fn main() {
 
         loop {
             let (stream, addr) = ln.accept().await.unwrap();
-            tracing::info!(?addr, "Accepted connection");
+            tracing::debug!(?addr, "Accepted connection");
 
             buffet::spawn(async move {
                 let client_buf = RollMut::alloc().unwrap();
@@ -50,9 +50,10 @@ fn main() {
                             ..Default::default()
                         });
 
-                        loona::h1::serve(io, server_conf, client_buf, driver)
-                            .await
-                            .unwrap();
+                        if let Err(e) = loona::h1::serve(io, server_conf, client_buf, driver).await
+                        {
+                            tracing::warn!("http/1 server error: {e:?}");
+                        }
                         tracing::debug!("http/1 server done");
                     }
                     Proto::H2 => {
@@ -61,9 +62,10 @@ fn main() {
                             ..Default::default()
                         });
 
-                        loona::h2::serve(io, server_conf, client_buf, driver)
-                            .await
-                            .unwrap();
+                        if let Err(e) = loona::h2::serve(io, server_conf, client_buf, driver).await
+                        {
+                            tracing::warn!("http/2 server error: {e:?}");
+                        }
                         tracing::debug!("http/2 server done");
                     }
                 }
diff --git a/crates/httpwg-macros/src/lib.rs b/crates/httpwg-macros/src/lib.rs
index e8cd3a2d..9b015769 100644
--- a/crates/httpwg-macros/src/lib.rs
+++ b/crates/httpwg-macros/src/lib.rs
@@ -1160,3 +1160,493 @@ $body
 }
 }
 }
+
+/// This generates a function that returns a Catalog of type
+#[macro_export]
+macro_rules! gen_catalog {
+  ($catalog_fn_name:ident) => {
+    use ::httpwg::BoxedTest;
+    pub fn $catalog_fn_name<IO: IntoHalves>() -> HashMap<&'static str, HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>> {
+        let mut rfcs: HashMap<&'static str, HashMap<&'static str, HashMap<&'static str, BoxedTest<IO>>>> = Default::default();
+
+        {
+            let mut sections: HashMap<&'static str, _> = Default::default();
+
+            {
+                use ::httpwg::rfc9113::_3_starting_http2 as s;
+                let mut _3_starting_http2: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _3_starting_http2.insert(
+                    "sends client connection preface",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_client_connection_preface(conn))),
+                );
+                _3_starting_http2.insert(
+                    "sends invalid connection preface",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_invalid_connection_preface(conn))),
+                );
+
+                sections.insert("3. starting http2", _3_starting_http2);
+            }
+            {
+                use ::httpwg::rfc9113::_4_http_frames as s;
+                let mut _4_http_frames: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _4_http_frames.insert(
+                    "sends frame with unknown type",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_frame_with_unknown_type(conn))),
+                );
+                _4_http_frames.insert(
+                    "sends frame with unused flags",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_frame_with_unused_flags(conn))),
+                );
+                _4_http_frames.insert(
+                    "sends frame with reserved bit set",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_frame_with_reserved_bit_set(conn))),
+                );
+                _4_http_frames.insert(
+                    "data frame with max length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::data_frame_with_max_length(conn))),
+                );
+                _4_http_frames.insert(
+                    "frame exceeding max size",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::frame_exceeding_max_size(conn))),
+                );
+                _4_http_frames.insert(
+                    "large headers frame exceeding max size",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::large_headers_frame_exceeding_max_size(conn))),
+                );
+                _4_http_frames.insert(
+                    "invalid header block fragment",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::invalid_header_block_fragment(conn))),
+                );
+                _4_http_frames.insert(
+                    "priority frame while sending headers",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::priority_frame_while_sending_headers(conn))),
+                );
+                _4_http_frames.insert(
+                    "headers frame to another stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::headers_frame_to_another_stream(conn))),
+                );
+
+                sections.insert("4. http frames", _4_http_frames);
+            }
+            {
+                use ::httpwg::rfc9113::_5_streams_and_multiplexing as s;
+                let mut _5_streams_and_multiplexing: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _5_streams_and_multiplexing.insert(
+                    "idle sends data frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::idle_sends_data_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "idle sends rst stream frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::idle_sends_rst_stream_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "idle sends window update frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::idle_sends_window_update_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "idle sends continuation frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::idle_sends_continuation_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "half closed remote sends data frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::half_closed_remote_sends_data_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "half closed remote sends headers frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::half_closed_remote_sends_headers_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "half closed remote sends continuation frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::half_closed_remote_sends_continuation_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends data frame after rst stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_data_frame_after_rst_stream(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends headers frame after rst stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_headers_frame_after_rst_stream(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends continuation frame after rst stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_continuation_frame_after_rst_stream(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends data frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_data_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends headers frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_headers_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "closed sends continuation frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::closed_sends_continuation_frame(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "sends even numbered stream identifier",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_even_numbered_stream_identifier(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "sends smaller stream identifier",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_smaller_stream_identifier(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "exceeds concurrent stream limit",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::exceeds_concurrent_stream_limit(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "invalid ping frame for connection close",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::invalid_ping_frame_for_connection_close(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "test invalid ping frame for goaway",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::test_invalid_ping_frame_for_goaway(conn))),
+                );
+                _5_streams_and_multiplexing.insert(
+                    "unknown extension frame in header block",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::unknown_extension_frame_in_header_block(conn))),
+                );
+
+                sections.insert("5. streams and multiplexing", _5_streams_and_multiplexing);
+            }
+            {
+                use ::httpwg::rfc9113::_6_frame_definitions as s;
+                let mut _6_frame_definitions: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _6_frame_definitions.insert(
+                    "sends data frame with zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_data_frame_with_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends data frame on invalid stream state",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_data_frame_on_invalid_stream_state(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends data frame with invalid pad length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_data_frame_with_invalid_pad_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends headers frame with zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends headers frame with invalid pad length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_invalid_pad_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends priority frame with zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_priority_frame_with_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends priority frame with invalid length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_priority_frame_with_invalid_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends rst stream frame with zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_rst_stream_frame_with_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends rst stream frame on idle stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_rst_stream_frame_on_idle_stream(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends rst stream frame with invalid length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_rst_stream_frame_with_invalid_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame with ack and payload",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_with_ack_and_payload(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame with non zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_with_non_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame with invalid length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_with_invalid_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings enable push with invalid value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_enable_push_with_invalid_value(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings initial window size with invalid value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_initial_window_size_with_invalid_value(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings max frame size with invalid value below initial",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_max_frame_size_with_invalid_value_below_initial(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings max frame size with invalid value above max",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_max_frame_size_with_invalid_value_above_max(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame with unknown identifier",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_with_unknown_identifier(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends multiple values of settings initial window size",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_multiple_values_of_settings_initial_window_size(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame without ack flag",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_without_ack_flag(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends ping frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_ping_frame(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends ping frame with ack",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_ping_frame_with_ack(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends ping frame with non zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_ping_frame_with_non_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends ping frame with invalid length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_ping_frame_with_invalid_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends goaway frame with non zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_goaway_frame_with_non_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends window update frame with zero increment",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_window_update_frame_with_zero_increment(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends window update frame with zero increment on stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_window_update_frame_with_zero_increment_on_stream(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends window update frame with invalid length",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_window_update_frame_with_invalid_length(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame to set initial window size to 1 and sends headers frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_to_set_initial_window_size_to_1_and_sends_headers_frame(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends multiple window update frames increasing flow control window above max",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_multiple_window_update_frames_increasing_flow_control_window_above_max(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends multiple window update frames increasing flow control window above max on stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_multiple_window_update_frames_increasing_flow_control_window_above_max_on_stream(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "changes settings initial window size after sending headers frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::changes_settings_initial_window_size_after_sending_headers_frame(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings frame for window size to be negative",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_frame_for_window_size_to_be_negative(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends settings initial window size with exceeded max window size value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_settings_initial_window_size_with_exceeded_max_window_size_value(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends multiple continuation frames preceded by headers frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_multiple_continuation_frames_preceded_by_headers_frame(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends continuation frame followed by non continuation frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_continuation_frame_followed_by_non_continuation_frame(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends continuation frame with zero stream id",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_continuation_frame_with_zero_stream_id(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends continuation frame preceded by headers frame with end headers flag",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_continuation_frame_preceded_by_headers_frame_with_end_headers_flag(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends continuation frame preceded by continuation frame with end headers flag",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_continuation_frame_preceded_by_continuation_frame_with_end_headers_flag(conn))),
+                );
+                _6_frame_definitions.insert(
+                    "sends continuation frame preceded by data frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_continuation_frame_preceded_by_data_frame(conn))),
+                );
+
+                sections.insert("6. frame definitions", _6_frame_definitions);
+            }
+            {
+                use ::httpwg::rfc9113::_7_error_codes as s;
+                let mut _7_error_codes: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _7_error_codes.insert(
+                    "sends goaway frame with unknown error code",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_goaway_frame_with_unknown_error_code(conn))),
+                );
+                _7_error_codes.insert(
+                    "sends rst stream frame with unknown error code",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_rst_stream_frame_with_unknown_error_code(conn))),
+                );
+
+                sections.insert("7. error codes", _7_error_codes);
+            }
+            {
+                use ::httpwg::rfc9113::_8_expressing_http_semantics_in_http2 as s;
+                let mut _8_expressing_http_semantics_in_http2: HashMap<&'static str, BoxedTest<IO>> = Default::default();
+
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends second headers frame without end stream",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_second_headers_frame_without_end_stream(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with incorrect content length single data frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_incorrect_content_length_single_data_frame(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with incorrect content length multiple data frames",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_incorrect_content_length_multiple_data_frames(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with uppercase field name",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_uppercase_field_name(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with space in field name",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_space_in_field_name(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with non visible ascii",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_non_visible_ascii(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with del character",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_del_character(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with non ascii character",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_non_ascii_character(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with colon in field name",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_colon_in_field_name(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with lf in field value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_lf_in_field_value(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with cr in field value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_cr_in_field_value(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with nul in field value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_nul_in_field_value(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with leading space in field value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_leading_space_in_field_value(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with trailing tab in field value",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_trailing_tab_in_field_value(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with connection header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_connection_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with proxy connection header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_proxy_connection_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with keep alive header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_keep_alive_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with transfer encoding header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_transfer_encoding_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with upgrade header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_upgrade_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with te trailers",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_te_trailers(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with te not trailers",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_te_not_trailers(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with response pseudo header",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_response_pseudo_header(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with pseudo header in trailer",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_pseudo_header_in_trailer(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with duplicate pseudo headers",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_duplicate_pseudo_headers(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with mismatched host authority",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_mismatched_host_authority(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with empty path component",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_empty_path_component(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame without method",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_without_method(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame without scheme",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_without_scheme(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame without path",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_without_path(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame without status",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_without_status(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "client sends push promise frame",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::client_sends_push_promise_frame(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends connect with scheme",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_connect_with_scheme(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends connect with path",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_connect_with_path(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends connect without authority",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_connect_without_authority(conn))),
+                );
+                _8_expressing_http_semantics_in_http2.insert(
+                    "sends headers frame with pseudo headers after regular headers",
+                    Box::new(|conn: Conn<IO>| Box::pin(s::sends_headers_frame_with_pseudo_headers_after_regular_headers(conn))),
+                );
+
+                sections.insert("8. expressing http semantics in http2", _8_expressing_http_semantics_in_http2);
+            }
+
+            rfcs.insert("RFC 9113", sections);
+        }
+
+        rfcs
+    }
+  }
+}
diff --git a/crates/httpwg/src/lib.rs b/crates/httpwg/src/lib.rs
index b34cc2c9..88f9c1f3 100644
--- a/crates/httpwg/src/lib.rs
+++ b/crates/httpwg/src/lib.rs
@@ -1,6 +1,6 @@
 use eyre::eyre;
 use rfc9113::DEFAULT_FRAME_SIZE;
-use std::{collections::VecDeque, rc::Rc, time::Duration};
+use std::{collections::VecDeque, future::Future, pin::Pin, rc::Rc, time::Duration};
 
 use buffet::{IntoHalves, Piece, PieceList, Roll, RollMut, WriteOwned};
 use enumflags2::{bitflags, BitFlags};
@@ -18,6 +18,8 @@ use crate::rfc9113::default_settings;
 
 pub mod rfc9113;
 
+pub type BoxedTest<IO> = Box<dyn Fn(Conn<IO>) -> Pin<Box<dyn Future<Output = eyre::Result<()>>>>>;
+
 #[derive(Default)]
 pub struct Headers {
     values: VecDeque<(Piece, Piece)>,
@@ -243,10 +245,12 @@ impl<IO: IntoHalves> Conn<IO> {
         let (ev_tx, ev_rx) = tokio::sync::mpsc::channel::<Ev>(1);
         let mut eof = false;
 
+        let ev_tx_unwrap = ev_tx.clone();
+        let mut res_buf = RollMut::alloc().unwrap();
+
         let recv_fut = {
             let config = config.clone();
             async move {
-                let mut res_buf = RollMut::alloc()?;
                 'read: loop {
                     trace!("'read loop");
 
@@ -258,7 +262,7 @@ impl<IO: IntoHalves> Conn<IO> {
                             // read frame payload
                             let frame_len = frame.len as usize;
                             trace!(?frame_len, "reserving memory");
-                            res_buf.reserve_at_least(frame_len)?;
+                            res_buf.reserve_at_least(frame_len).unwrap();
 
                             let deadline = Instant::now() + config.timeout;
                             trace!(?frame_len, ?deadline, "reading");
@@ -319,7 +323,7 @@ impl<IO: IntoHalves> Conn<IO> {
                             }
 
                             trace!("reserving");
-                            res_buf.reserve()?;
+                            res_buf.reserve().unwrap();
                             let res;
                             trace!("re-filling buffer");
                             let deadline = Instant::now() + config.timeout;
@@ -352,7 +356,7 @@ impl<IO: IntoHalves> Conn<IO> {
                     }
                 }
 
-                Ok::<_, eyre::Report>(())
+                Ok::<_, std::io::Error>(())
             }
         };
 
@@ -368,7 +372,11 @@ impl<IO: IntoHalves> Conn<IO> {
                     tracing::trace!("httpwg receive loop cancelled!");
                 },
                 result = recv_fut => {
-                    result.unwrap();
+                    if let Err(e) = result {
+                        if ev_tx_unwrap.send(Ev::IoError { error: e }).await.is_err() {
+                            // well the test already hung up I guess.
+                        }
+                    }
                 }
             }
         });
diff --git a/crates/loona/src/h2/server.rs b/crates/loona/src/h2/server.rs
index f536cd66..d1bf4e9e 100644
--- a/crates/loona/src/h2/server.rs
+++ b/crates/loona/src/h2/server.rs
@@ -584,7 +584,6 @@ impl<D: ServerDriver + 'static, W: WriteOwned> ServerContext<D, W> {
                         // tell the sender to stop sending chunks, which is not
                         // possible if they all share the same ev_tx
                         // TODO: make it possible to propagate errors to the sender
-                        tracing::warn!(stream_id = %ev.stream_id, "ignoring event for stream, since we have no state for it");
 
                         return Ok(());
                     }


Test patch: