From c7b8813c4715afd0a6fdf0872545a516bd8e1924 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Thu, 11 Nov 2021 21:26:08 +0100 Subject: [PATCH] Add `trybuild` tests for axum-debug (#501) The things are also covered by the doc tests but this also gives us assertions on the errors produced, which is the whole point of this crate :) --- .github/workflows/CI.yml | 12 +- axum-debug/Cargo.toml | 3 + axum-debug/src/lib.rs | 154 ++++++++++-------- axum-debug/tests/fail/.gitkeep | 0 .../tests/fail/argument_not_extractor.rs | 6 + .../tests/fail/argument_not_extractor.stderr | 11 ++ axum-debug/tests/fail/not_a_function.rs | 6 + axum-debug/tests/fail/not_a_function.stderr | 5 + axum-debug/tests/fail/not_async.rs | 6 + axum-debug/tests/fail/not_async.stderr | 5 + axum-debug/tests/fail/not_send.rs | 9 + axum-debug/tests/fail/not_send.stderr | 21 +++ axum-debug/tests/fail/self_receiver.rs | 20 +++ axum-debug/tests/fail/self_receiver.stderr | 5 + axum-debug/tests/fail/too_many_extractors.rs | 24 +++ .../tests/fail/too_many_extractors.stderr | 12 ++ axum-debug/tests/fail/wrong_return_type.rs | 8 + .../tests/fail/wrong_return_type.stderr | 11 ++ 18 files changed, 246 insertions(+), 72 deletions(-) create mode 100644 axum-debug/tests/fail/.gitkeep create mode 100644 axum-debug/tests/fail/argument_not_extractor.rs create mode 100644 axum-debug/tests/fail/argument_not_extractor.stderr create mode 100644 axum-debug/tests/fail/not_a_function.rs create mode 100644 axum-debug/tests/fail/not_a_function.stderr create mode 100644 axum-debug/tests/fail/not_async.rs create mode 100644 axum-debug/tests/fail/not_async.stderr create mode 100644 axum-debug/tests/fail/not_send.rs create mode 100644 axum-debug/tests/fail/not_send.stderr create mode 100644 axum-debug/tests/fail/self_receiver.rs create mode 100644 axum-debug/tests/fail/self_receiver.stderr create mode 100644 axum-debug/tests/fail/too_many_extractors.rs create mode 100644 axum-debug/tests/fail/too_many_extractors.stderr create mode 100644 axum-debug/tests/fail/wrong_return_type.rs create mode 100644 axum-debug/tests/fail/wrong_return_type.stderr diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c0eec713..a98305be 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -100,9 +100,19 @@ jobs: command: test args: > -p axum - -p axum-debug -p axum-handle-error-extract --all-features --all-targets + # the compiler errors are different on 1.54 which makes + # the trybuild tests in axum-debug fail, so just run the doc + # tests + - name: Run axum-debug doc tests + uses: actions-rs/cargo@v1 + with: + command: test + args: > + -p axum-debug + --doc + --all-features test-docs: needs: check diff --git a/axum-debug/Cargo.toml b/axum-debug/Cargo.toml index 78d5ff5c..e3b65921 100644 --- a/axum-debug/Cargo.toml +++ b/axum-debug/Cargo.toml @@ -19,3 +19,6 @@ proc-macro2 = "1.0" quote = "1.0" syn = { version = "1.0", features = ["full"] } axum = { path = "../axum", version = "0.3" } + +[dev-dependencies] +trybuild = "1.0" diff --git a/axum-debug/src/lib.rs b/axum-debug/src/lib.rs index 5c7bf467..c1321d0f 100644 --- a/axum-debug/src/lib.rs +++ b/axum-debug/src/lib.rs @@ -129,8 +129,8 @@ use proc_macro::TokenStream; /// /// Function is not async: /// -/// ```rust,ignore -/// #[debug_handler] +/// ```rust,compile_fail +/// #[axum_debug::debug_handler] /// fn handler() -> &'static str { /// "Hello, world" /// } @@ -146,8 +146,8 @@ use proc_macro::TokenStream; /// /// Wrong return type: /// -/// ```rust,ignore -/// #[debug_handler] +/// ```rust,compile_fail +/// #[axum_debug::debug_handler] /// async fn handler() -> bool { /// false /// } @@ -165,8 +165,8 @@ use proc_macro::TokenStream; /// /// Wrong extractor: /// -/// ```rust,ignore -/// #[debug_handler] +/// ```rust,compile_fail +/// #[axum_debug::debug_handler] /// async fn handler(a: bool) -> String { /// format!("Can I extract a bool? {}", a) /// } @@ -184,8 +184,8 @@ use proc_macro::TokenStream; /// /// Too many extractors: /// -/// ```rust,ignore -/// #[debug_handler] +/// ```rust,compile_fail +/// #[axum_debug::debug_handler] /// async fn handler( /// a: String, /// b: String, @@ -224,12 +224,12 @@ use proc_macro::TokenStream; /// /// Future is not [`Send`]: /// -/// ```rust,ignore -/// #[debug_handler] +/// ```rust,compile_fail +/// #[axum_debug::debug_handler] /// async fn handler() { /// let not_send = std::rc::Rc::new(()); /// -/// async{}.await; +/// async {}.await; /// } /// ``` /// @@ -253,12 +253,11 @@ pub fn debug_handler(_attr: TokenStream, input: TokenStream) -> TokenStream { #[cfg(debug_assertions)] mod debug { - use proc_macro::TokenStream; - use proc_macro2::Span; - use quote::quote_spanned; - use syn::{parse_macro_input, FnArg, Ident, ItemFn, ReturnType, Signature}; + use proc_macro2::TokenStream; + use quote::{format_ident, quote_spanned}; + use syn::{parse_macro_input, spanned::Spanned, FnArg, Ident, ItemFn, ReturnType, Signature}; - pub(crate) fn apply_debug_handler(input: TokenStream) -> TokenStream { + pub(crate) fn apply_debug_handler(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let function = parse_macro_input!(input as ItemFn); let vis = &function.vis; @@ -276,17 +275,20 @@ mod debug { }); let block = &function.block; - if let Err(error) = async_check(sig) { - return error; + if let Err(err) = async_check(sig) { + return err.into_compile_error().into(); } - if let Err(error) = param_limit_check(sig) { - return error; + if let Err(err) = param_limit_check(sig) { + return err.into_compile_error().into(); } let check_trait = check_trait_code(sig, &generics); let check_return = check_return_code(sig, &generics); - let check_params = check_params_code(sig, &generics); + let check_params = match check_params_code(sig, &generics) { + Ok(tokens) => tokens, + Err(err) => return err.into_compile_error().into(), + }; let expanded = quote_spanned! {span=> #vis #sig { @@ -304,38 +306,34 @@ mod debug { } fn create_generics(len: usize) -> Vec { - let mut vec = Vec::new(); - for i in 1..=len { - vec.push(Ident::new(&format!("T{}", i), Span::call_site())); - } - vec + (1..=len).map(|i| format_ident!("T{}", i)).collect() } - fn async_check(sig: &Signature) -> Result<(), TokenStream> { + fn async_check(sig: &Signature) -> Result<(), syn::Error> { if sig.asyncness.is_none() { - let error = syn::Error::new_spanned(sig.fn_token, "handlers must be async functions") - .to_compile_error() - .into(); - - return Err(error); + Err(syn::Error::new_spanned( + sig.fn_token, + "handlers must be async functions", + )) + } else { + Ok(()) } - - Ok(()) } - fn param_limit_check(sig: &Signature) -> Result<(), TokenStream> { - if sig.inputs.len() > 16 { - let msg = "too many extractors. 16 extractors are allowed\n\ - note: you can nest extractors like \"a: (Extractor, Extractor), b: (Extractor, Extractor)\""; + fn param_limit_check(sig: &Signature) -> Result<(), syn::Error> { + let max_extractors = 16; - let error = syn::Error::new_spanned(&sig.inputs, msg) - .to_compile_error() - .into(); + if sig.inputs.len() > max_extractors { + let msg = format!( + "too many extractors. {} extractors are allowed\n\ + note: you can nest extractors like \"a: (Extractor, Extractor), b: (Extractor, Extractor)\"", + max_extractors + ); - return Err(error); + Err(syn::Error::new_spanned(&sig.inputs, msg)) + } else { + Ok(()) } - - Ok(()) } fn check_trait_code(sig: &Signature, generics: &[Ident]) -> proc_macro2::TokenStream { @@ -357,8 +355,8 @@ mod debug { fn check_return_code(sig: &Signature, generics: &[Ident]) -> proc_macro2::TokenStream { let span = match &sig.output { - ReturnType::Default => syn::Error::new_spanned(&sig.output, "").span(), - ReturnType::Type(_, t) => syn::Error::new_spanned(t, "").span(), + ReturnType::Default => sig.output.span(), + ReturnType::Type(_, ty) => ty.span(), }; let ident = &sig.ident; @@ -376,34 +374,48 @@ mod debug { } } - fn check_params_code(sig: &Signature, generics: &[Ident]) -> Vec { - let mut vec = Vec::new(); - + fn check_params_code( + sig: &Signature, + generics: &[Ident], + ) -> Result, syn::Error> { let ident = &sig.ident; + generics + .iter() + .enumerate() + .map(|(i, generic)| { + let span = match &sig.inputs[i] { + FnArg::Typed(pat_type) => pat_type.ty.span(), + FnArg::Receiver(receiver) => { + // TODO: look into whether its possible to make this work + return Err(syn::Error::new_spanned( + receiver, + "`#[debug_handler]` is not supported on methods", + )); + } + }; - for (i, generic) in generics.iter().enumerate() { - let span = if let FnArg::Typed(pat_type) = &sig.inputs[i] { - syn::Error::new_spanned(&pat_type.ty, "").span() - } else { - panic!("not a handler") - }; + let tokens = quote_spanned! {span=> + { + debug_handler(#ident); - let token_stream = quote_spanned! {span=> - { - debug_handler(#ident); + fn debug_handler(_f: F) + where + F: ::std::ops::FnOnce(#(#generics),*) -> Fut, + Fut: ::std::future::Future, + #generic: ::axum::extract::FromRequest + Send, + {} + } + }; - fn debug_handler(_f: F) - where - F: ::std::ops::FnOnce(#(#generics),*) -> Fut, - Fut: ::std::future::Future, - #generic: ::axum::extract::FromRequest + Send, - {} - } - }; - - vec.push(token_stream); - } - - vec + Ok(tokens) + }) + .collect() } } + +#[test] +fn ui() { + let t = trybuild::TestCases::new(); + t.pass("tests/pass/*.rs"); + t.compile_fail("tests/fail/*.rs"); +} diff --git a/axum-debug/tests/fail/.gitkeep b/axum-debug/tests/fail/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/axum-debug/tests/fail/argument_not_extractor.rs b/axum-debug/tests/fail/argument_not_extractor.rs new file mode 100644 index 00000000..0629cde7 --- /dev/null +++ b/axum-debug/tests/fail/argument_not_extractor.rs @@ -0,0 +1,6 @@ +use axum_debug::debug_handler; + +#[debug_handler] +async fn handler(foo: bool) {} + +fn main() {} diff --git a/axum-debug/tests/fail/argument_not_extractor.stderr b/axum-debug/tests/fail/argument_not_extractor.stderr new file mode 100644 index 00000000..1ef2e6c8 --- /dev/null +++ b/axum-debug/tests/fail/argument_not_extractor.stderr @@ -0,0 +1,11 @@ +error[E0277]: the trait bound `bool: FromRequest` is not satisfied + --> tests/fail/argument_not_extractor.rs:4:23 + | +4 | async fn handler(foo: bool) {} + | ^^^^ the trait `FromRequest` is not implemented for `bool` + | +note: required by a bound in `handler::{closure#0}::debug_handler` + --> tests/fail/argument_not_extractor.rs:4:23 + | +4 | async fn handler(foo: bool) {} + | ^^^^ required by this bound in `handler::{closure#0}::debug_handler` diff --git a/axum-debug/tests/fail/not_a_function.rs b/axum-debug/tests/fail/not_a_function.rs new file mode 100644 index 00000000..d3d75162 --- /dev/null +++ b/axum-debug/tests/fail/not_a_function.rs @@ -0,0 +1,6 @@ +use axum_debug::debug_handler; + +#[debug_handler] +struct A; + +fn main() {} diff --git a/axum-debug/tests/fail/not_a_function.stderr b/axum-debug/tests/fail/not_a_function.stderr new file mode 100644 index 00000000..c15f8796 --- /dev/null +++ b/axum-debug/tests/fail/not_a_function.stderr @@ -0,0 +1,5 @@ +error: expected `fn` + --> tests/fail/not_a_function.rs:4:1 + | +4 | struct A; + | ^^^^^^ diff --git a/axum-debug/tests/fail/not_async.rs b/axum-debug/tests/fail/not_async.rs new file mode 100644 index 00000000..eced8ec0 --- /dev/null +++ b/axum-debug/tests/fail/not_async.rs @@ -0,0 +1,6 @@ +use axum_debug::debug_handler; + +#[debug_handler] +fn handler() {} + +fn main() {} diff --git a/axum-debug/tests/fail/not_async.stderr b/axum-debug/tests/fail/not_async.stderr new file mode 100644 index 00000000..e36eb148 --- /dev/null +++ b/axum-debug/tests/fail/not_async.stderr @@ -0,0 +1,5 @@ +error: handlers must be async functions + --> tests/fail/not_async.rs:4:1 + | +4 | fn handler() {} + | ^^ diff --git a/axum-debug/tests/fail/not_send.rs b/axum-debug/tests/fail/not_send.rs new file mode 100644 index 00000000..4013db61 --- /dev/null +++ b/axum-debug/tests/fail/not_send.rs @@ -0,0 +1,9 @@ +use axum_debug::debug_handler; + +#[debug_handler] +async fn handler() { + let rc = std::rc::Rc::new(()); + async {}.await; +} + +fn main() {} diff --git a/axum-debug/tests/fail/not_send.stderr b/axum-debug/tests/fail/not_send.stderr new file mode 100644 index 00000000..00403d79 --- /dev/null +++ b/axum-debug/tests/fail/not_send.stderr @@ -0,0 +1,21 @@ +error: future cannot be sent between threads safely + --> tests/fail/not_send.rs:4:10 + | +4 | async fn handler() { + | ^^^^^^^ future returned by `handler` is not `Send` + | + = help: within `impl Future`, the trait `Send` is not implemented for `Rc<()>` +note: future is not `Send` as this value is used across an await + --> tests/fail/not_send.rs:6:5 + | +5 | let rc = std::rc::Rc::new(()); + | -- has type `Rc<()>` which is not `Send` +6 | async {}.await; + | ^^^^^^^^^^^^^^ await occurs here, with `rc` maybe used later +7 | } + | - `rc` is later dropped here +note: required by a bound in `handler::{closure#0}::debug_handler` + --> tests/fail/not_send.rs:4:10 + | +4 | async fn handler() { + | ^^^^^^^ required by this bound in `handler::{closure#0}::debug_handler` diff --git a/axum-debug/tests/fail/self_receiver.rs b/axum-debug/tests/fail/self_receiver.rs new file mode 100644 index 00000000..006ceb3e --- /dev/null +++ b/axum-debug/tests/fail/self_receiver.rs @@ -0,0 +1,20 @@ +use axum_debug::debug_handler; +use axum::{async_trait, extract::{FromRequest, RequestParts}}; + +struct A; + +#[async_trait] +impl FromRequest for A { + type Rejection = (); + + async fn from_request(_req: &mut RequestParts) -> Result { + unimplemented!() + } +} + +impl A { + #[debug_handler] + async fn handler(self) {} +} + +fn main() {} diff --git a/axum-debug/tests/fail/self_receiver.stderr b/axum-debug/tests/fail/self_receiver.stderr new file mode 100644 index 00000000..d9456dd1 --- /dev/null +++ b/axum-debug/tests/fail/self_receiver.stderr @@ -0,0 +1,5 @@ +error: `#[debug_handler]` is not supported on methods + --> tests/fail/self_receiver.rs:17:22 + | +17 | async fn handler(self) {} + | ^^^^ diff --git a/axum-debug/tests/fail/too_many_extractors.rs b/axum-debug/tests/fail/too_many_extractors.rs new file mode 100644 index 00000000..9e151faa --- /dev/null +++ b/axum-debug/tests/fail/too_many_extractors.rs @@ -0,0 +1,24 @@ +use axum_debug::debug_handler; + +#[debug_handler] +async fn handler( + e1: String, + e2: String, + e3: String, + e4: String, + e5: String, + e6: String, + e7: String, + e8: String, + e9: String, + e10: String, + e11: String, + e12: String, + e13: String, + e14: String, + e15: String, + e16: String, + e17: String, +) {} + +fn main() {} diff --git a/axum-debug/tests/fail/too_many_extractors.stderr b/axum-debug/tests/fail/too_many_extractors.stderr new file mode 100644 index 00000000..aa25bcbb --- /dev/null +++ b/axum-debug/tests/fail/too_many_extractors.stderr @@ -0,0 +1,12 @@ +error: too many extractors. 16 extractors are allowed +note: you can nest extractors like "a: (Extractor, Extractor), b: (Extractor, Extractor)" + --> tests/fail/too_many_extractors.rs:5:5 + | +5 | / e1: String, +6 | | e2: String, +7 | | e3: String, +8 | | e4: String, +... | +20 | | e16: String, +21 | | e17: String, + | |________________^ diff --git a/axum-debug/tests/fail/wrong_return_type.rs b/axum-debug/tests/fail/wrong_return_type.rs new file mode 100644 index 00000000..4ddf636d --- /dev/null +++ b/axum-debug/tests/fail/wrong_return_type.rs @@ -0,0 +1,8 @@ +use axum_debug::debug_handler; + +#[debug_handler] +async fn handler() -> bool { + false +} + +fn main() {} diff --git a/axum-debug/tests/fail/wrong_return_type.stderr b/axum-debug/tests/fail/wrong_return_type.stderr new file mode 100644 index 00000000..aabdbceb --- /dev/null +++ b/axum-debug/tests/fail/wrong_return_type.stderr @@ -0,0 +1,11 @@ +error[E0277]: the trait bound `bool: IntoResponse` is not satisfied + --> tests/fail/wrong_return_type.rs:4:23 + | +4 | async fn handler() -> bool { + | ^^^^ the trait `IntoResponse` is not implemented for `bool` + | +note: required by a bound in `handler::{closure#0}::debug_handler` + --> tests/fail/wrong_return_type.rs:4:23 + | +4 | async fn handler() -> bool { + | ^^^^ required by this bound in `handler::{closure#0}::debug_handler`