#![forbid(unsafe_code)]
#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
use darling::FromMeta;
use darling::ast::NestedMeta;
use proc_macro_crate::{FoundCrate, crate_name};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::ext::IdentExt;
use syn::{Expr, Ident, ItemFn, Lit, parse_quote, parse2};
#[derive(Debug, FromMeta)]
struct Args {
#[darling(default)]
monitor: Option<MonitorSetting>,
#[darling(default)]
panic: Option<PanicSetting>,
#[darling(default, with = "darling::util::parse_expr::preserve_str_literal", map = "Some")]
workers: Option<Expr>,
}
#[derive(Clone, Copy, Debug)]
enum MonitorSetting {
Enabled,
Disabled,
}
#[derive(Clone, Copy, Debug)]
enum PanicSetting {
Shutdown,
Ignore,
}
impl FromMeta for MonitorSetting {
fn from_expr(expr: &Expr) -> darling::Result<Self> {
let Expr::Path(path) = expr else {
return Err(invalid_monitor_setting(expr));
};
let Some(ident) = path.path.get_ident() else {
return Err(invalid_monitor_setting(expr));
};
match ident.to_string().as_str() {
"enabled" => Ok(Self::Enabled),
"disabled" => Ok(Self::Disabled),
_ => Err(invalid_monitor_setting(expr)),
}
}
}
fn invalid_monitor_setting(expr: &Expr) -> darling::Error {
darling::Error::custom("`monitor` must be the bare identifier `enabled` or `disabled`").with_span(expr)
}
impl FromMeta for PanicSetting {
fn from_expr(expr: &Expr) -> darling::Result<Self> {
let Expr::Path(path) = expr else {
return Err(invalid_panic_setting(expr));
};
let Some(ident) = path.path.get_ident() else {
return Err(invalid_panic_setting(expr));
};
match ident.to_string().as_str() {
"shutdown" => Ok(Self::Shutdown),
"ignore" => Ok(Self::Ignore),
_ => Err(invalid_panic_setting(expr)),
}
}
}
fn invalid_panic_setting(expr: &Expr) -> darling::Error {
darling::Error::custom("`panic` must be the bare identifier `shutdown` or `ignore`").with_span(expr)
}
impl Args {
fn runtime(&self, runtime_path: &TokenStream, clock: Option<&Ident>, test: bool) -> syn::Result<TokenStream> {
let monitor = self.monitor.or_else(|| test.then_some(MonitorSetting::Disabled));
if self.workers.is_none() && clock.is_none() && monitor.is_none() && self.panic.is_none() {
return Ok(quote!(#runtime_path::runtime::Runtime::new()));
}
let workers = self.workers.as_ref().map(|value| worker_policy(value, runtime_path)).transpose()?;
let workers = workers.map(|policy| quote!(.workers(#policy)));
let monitor = monitor.map(|monitor| match monitor {
MonitorSetting::Enabled => quote!(.monitor(#runtime_path::runtime::MonitorPolicy::enabled())),
MonitorSetting::Disabled => quote!(.monitor(#runtime_path::runtime::MonitorPolicy::disabled())),
});
let panic = self.panic.map(|panic| match panic {
PanicSetting::Shutdown => {
quote!(.panic(#runtime_path::runtime::PanicPolicy::shutdown()))
}
PanicSetting::Ignore => quote!(.panic(#runtime_path::runtime::PanicPolicy::ignore())),
});
let clock = clock.map(|binding| quote!(.clock(::core::clone::Clone::clone(&#binding))));
Ok(quote!(#runtime_path::runtime::Runtime::builder() #workers #monitor #panic #clock .build()))
}
}
fn arty_path() -> TokenStream {
arty_path_for(crate_name("arty").ok())
}
fn arty_path_for(found: Option<FoundCrate>) -> TokenStream {
match found {
Some(FoundCrate::Name(name)) => {
let name = name.replace('-', "_");
let name = syn::parse_str::<Ident>(&name)
.or_else(|_| syn::parse_str::<Ident>(&format!("r#{name}")))
.expect("Cargo dependency aliases are valid Rust identifiers, possibly requiring raw syntax");
quote!(::#name)
}
Some(FoundCrate::Itself) | None => quote!(::arty),
}
}
fn worker_policy(mut value: &Expr, runtime_path: &TokenStream) -> syn::Result<TokenStream> {
while let Expr::Group(group) = value {
value = &group.expr;
}
if let Expr::Lit(syn::ExprLit { lit: Lit::Int(count), .. }) = value
&& !count.base10_digits().starts_with('-')
&& matches!(count.suffix(), "" | "usize")
{
return Ok(quote!(#runtime_path::runtime::WorkersPolicy::exactly(#count)));
}
if let Expr::Path(syn::ExprPath { qself: None, path, .. }) = value
&& path.is_ident("all")
{
return Ok(quote!(#runtime_path::runtime::WorkersPolicy::all()));
}
Err(syn::Error::new_spanned(
value,
"`workers` must be a nonnegative integer literal, optionally suffixed with `usize`, or `all`; use explicit runtime construction for expressions or other processor policies",
))
}
#[must_use]
pub fn main(args: TokenStream, item: TokenStream) -> TokenStream {
entrypoint(args, item, false)
}
#[must_use]
pub fn test(args: TokenStream, item: TokenStream) -> TokenStream {
entrypoint(args, item, true)
}
fn entrypoint(args: TokenStream, item: TokenStream, test: bool) -> TokenStream {
let mut input: ItemFn = match parse2(item.clone()) {
Ok(input) => input,
Err(error) => return error.to_compile_error(),
};
let args = match NestedMeta::parse_meta_list(args) {
Ok(args) => args,
Err(error) => return darling::Error::from(error).write_errors(),
};
let args = match Args::from_list(&args) {
Ok(args) => args,
Err(error) => return error.write_errors(),
};
let runtime_path = arty_path();
let sig = &mut input.sig;
let mut inputs = sig.inputs.iter();
let fail = move |error: syn::Error| {
let error = error.to_compile_error();
quote! { #item #error }
};
if sig.asyncness.is_none() {
return fail(syn::Error::new_spanned(sig.fn_token, "function must be async to use the attribute"));
}
let Some(syn::FnArg::Typed(state)) = inputs.next() else {
return fail(syn::Error::new_spanned(
sig.fn_token,
"function must take an Arty task context argument, such as cx: arty::task::Context",
));
};
let syn::Pat::Ident(state_ident) = state.pat.as_ref() else {
return fail(syn::Error::new_spanned(&state.pat, "argument must have an identifier"));
};
let syn::Type::Path(state_type) = state.ty.as_ref() else {
return fail(syn::Error::new_spanned(
&state.ty,
"the first parameter must take arty::task::Context by value, such as cx: arty::task::Context",
));
};
let state_ident = state_ident.clone();
let state_type = state_type.clone();
let clock = match inputs.next() {
Some(syn::FnArg::Typed(clock)) if test && matches!(clock.ty.as_ref(), syn::Type::Path(_)) => {
let syn::Pat::Ident(ident) = clock.pat.as_ref() else {
return fail(syn::Error::new_spanned(&clock.pat, "argument must have an identifier"));
};
if ident.ident.unraw() == state_ident.ident.unraw() {
return fail(syn::Error::new_spanned(ident, "arguments must have distinct identifiers"));
}
Some((ident.clone(), clock.ty.clone()))
}
Some(extra) => return fail(syn::Error::new_spanned(extra, "unexpected arguments")),
None => None,
};
if let Some(extra) = inputs.next() {
return fail(syn::Error::new_spanned(extra, "unexpected arguments"));
}
let clock_binding = Ident::new("__arty_clock_control", Span::mixed_site());
let runtime_binding = Ident::new("__arty_runtime", Span::mixed_site());
let result_binding = Ident::new("__arty_result", Span::mixed_site());
let shutdown_binding = Ident::new("__arty_shutdown", Span::mixed_site());
let value_binding = Ident::new("__arty_value", Span::mixed_site());
let runtime = match args.runtime(&runtime_path, clock.as_ref().map(|_| &clock_binding), test) {
Ok(runtime) => runtime,
Err(error) => return fail(error),
};
sig.asyncness = None;
sig.inputs.clear();
let mut attrs = input.attrs;
if test {
attrs.push(parse_quote!(#[::core::prelude::v1::test]));
}
let visibility = input.vis;
let body = input.block;
let (setup, body) = if let Some((ident, ty)) = clock {
(
quote!(let #clock_binding = #runtime_path::runtime::__private::ClockControl::new();),
quote!({
let #ident: #ty = #clock_binding;
#body
}),
)
} else {
(TokenStream::new(), quote!(#body))
};
quote! {
#(#attrs)*
#visibility #sig {
#setup
let #runtime_binding = #runtime
.expect("failed to create the runtime for the entry point");
let #result_binding = #runtime_binding
.scheduler()
.block_on(async move |#state_ident: #state_type| #body);
let #shutdown_binding = #runtime_binding.stop();
#result_binding
.and_then(|#value_binding| #shutdown_binding.map(|()| #value_binding))
.unwrap_or_else(|error| #runtime_path::runtime::__private::resume_error(error))
}
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] mod tests {
use insta::assert_snapshot;
use testing_aids::render_expansion;
use super::*;
#[test]
fn arty_path_uses_a_renamed_dependency() {
assert_eq!(
arty_path_for(Some(FoundCrate::Name("renamed-arty".to_owned()))).to_string(),
":: renamed_arty"
);
assert_eq!(arty_path_for(Some(FoundCrate::Itself)).to_string(), ":: arty");
assert_eq!(arty_path_for(None).to_string(), ":: arty");
}
#[test]
fn arty_path_supports_a_keyword_dependency_alias() {
assert_eq!(arty_path_for(Some(FoundCrate::Name("type".to_owned()))).to_string(), ":: r#type");
}
#[test]
fn main_preserves_visibility_and_return_type() {
let expansion = main(
TokenStream::new(),
quote! {
pub async fn main(cx: arty::task::Context) -> Result<(), Error> {
run(cx).await
}
},
);
assert_snapshot!(render_expansion(&expansion), @r#"
pub fn main() -> Result<(), Error> {
let __arty_runtime = ::arty::runtime::Runtime::new()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: arty::task::Context| { run(cx).await });
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
"#);
}
#[test]
fn test_preserves_attributes() {
let expansion = test(
TokenStream::new(),
quote! {
#[should_panic(expected = "original payload")]
async fn fails(mut cx: arty::task::Context) {
fail(&mut cx).await;
}
},
);
assert_snapshot!(render_expansion(&expansion), @r#"
#[should_panic(expected = "original payload")]
#[::core::prelude::v1::test]
fn fails() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.monitor(::arty::runtime::MonitorPolicy::disabled())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |mut cx: arty::task::Context| {
fail(&mut cx).await;
});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
"#);
}
#[test]
fn workers_selects_exactly_the_literal_count() {
let expansion = main(
quote!(workers = 4usize, monitor = disabled),
quote! {
pub async fn run(cx: <App as Types>::Context) -> AppResult {
run(cx).await
}
},
);
let expected = quote! {
pub fn run() -> AppResult {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.workers(::arty::runtime::WorkersPolicy::exactly(4usize))
.monitor(::arty::runtime::MonitorPolicy::disabled())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: <App as Types>::Context| { run(cx).await });
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string());
}
#[test]
fn all_workers_selects_all_available_processors() {
let input = quote! {
async fn run(cx: Context) {}
};
let main_expansion = main(quote!(workers = all), input.clone());
let expected_main = quote! {
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.workers(::arty::runtime::WorkersPolicy::all())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(main_expansion.to_string(), expected_main.to_string());
let test_expansion = test(quote!(workers = all), input);
let expected_test = quote! {
#[::core::prelude::v1::test]
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.workers(::arty::runtime::WorkersPolicy::all())
.monitor(::arty::runtime::MonitorPolicy::disabled())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(test_expansion.to_string(), expected_test.to_string());
}
#[test]
fn controlled_test_can_select_all_workers() {
let expansion = test(
quote!(workers = all),
quote! {
async fn run(cx: Context, time: ClockControl) {}
},
);
let expected = quote! {
#[::core::prelude::v1::test]
fn run() {
let __arty_clock_control = ::arty::runtime::__private::ClockControl::new();
let __arty_runtime = ::arty::runtime::Runtime::builder()
.workers(::arty::runtime::WorkersPolicy::all())
.monitor(::arty::runtime::MonitorPolicy::disabled())
.clock(::core::clone::Clone::clone(&__arty_clock_control))
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {
let time: ClockControl = __arty_clock_control;
{}
});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string());
}
#[test]
fn controlled_test_shares_one_clock_and_preserves_qualified_types() {
let expansion = test(
quote!(monitor = enabled, workers = 1),
quote! {
#[ignore = "manual test"]
#[should_panic(expected = "original payload")]
async fn run(cx: <App as Types>::Context, mut time: <App as Types>::Control) {
run(cx, &mut time).await;
}
},
);
let expected = quote! {
#[ignore = "manual test"]
#[should_panic(expected = "original payload")]
#[::core::prelude::v1::test]
fn run() {
let __arty_clock_control = ::arty::runtime::__private::ClockControl::new();
let __arty_runtime = ::arty::runtime::Runtime::builder()
.workers(::arty::runtime::WorkersPolicy::exactly(1))
.monitor(::arty::runtime::MonitorPolicy::enabled())
.clock(::core::clone::Clone::clone(&__arty_clock_control))
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: <App as Types>::Context| {
let mut time: <App as Types>::Control = __arty_clock_control;
{
run(cx, &mut time).await;
}
});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string());
}
#[test]
fn controlled_test_without_workers_uses_the_runtime_default() {
let expansion = test(
TokenStream::new(),
quote! {
async fn run(cx: Context, time: ClockControl) {
run(cx, time).await;
}
},
);
let expected = quote! {
#[::core::prelude::v1::test]
fn run() {
let __arty_clock_control = ::arty::runtime::__private::ClockControl::new();
let __arty_runtime = ::arty::runtime::Runtime::builder()
.monitor(::arty::runtime::MonitorPolicy::disabled())
.clock(::core::clone::Clone::clone(&__arty_clock_control))
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {
let time: ClockControl = __arty_clock_control;
{
run(cx, time).await;
}
});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string());
}
#[test]
fn main_monitor_settings_are_forwarded_exactly() {
for (setting, policy) in [(quote!(enabled), quote!(enabled)), (quote!(disabled), quote!(disabled))] {
let expansion = main(
quote!(monitor = #setting),
quote! {
async fn run(cx: Context) {}
},
);
let expected = quote! {
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.monitor(::arty::runtime::MonitorPolicy::#policy())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string(), "monitor = {setting}");
}
}
#[test]
fn test_monitor_settings_are_forwarded_exactly() {
for (setting, policy) in [(quote!(enabled), quote!(enabled)), (quote!(disabled), quote!(disabled))] {
let expansion = test(
quote!(monitor = #setting),
quote! {
async fn run(cx: Context) {}
},
);
let expected = quote! {
#[::core::prelude::v1::test]
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.monitor(::arty::runtime::MonitorPolicy::#policy())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string(), "monitor = {setting}");
}
}
#[test]
fn main_panic_settings_are_forwarded_exactly() {
for policy in [quote!(shutdown), quote!(ignore)] {
let expansion = main(
quote!(panic = #policy),
quote! {
async fn run(cx: Context) {}
},
);
let expected = quote! {
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.panic(::arty::runtime::PanicPolicy::#policy())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string(), "panic = {policy}");
}
}
#[test]
fn test_panic_settings_are_forwarded_exactly() {
for policy in [quote!(shutdown), quote!(ignore)] {
let expansion = test(
quote!(panic = #policy),
quote! {
async fn run(cx: Context) {}
},
);
let expected = quote! {
#[::core::prelude::v1::test]
fn run() {
let __arty_runtime = ::arty::runtime::Runtime::builder()
.monitor(::arty::runtime::MonitorPolicy::disabled())
.panic(::arty::runtime::PanicPolicy::#policy())
.build()
.expect("failed to create the runtime for the entry point");
let __arty_result = __arty_runtime
.scheduler()
.block_on(async move |cx: Context| {});
let __arty_shutdown = __arty_runtime.stop();
__arty_result
.and_then(|__arty_value| __arty_shutdown.map(|()| __arty_value))
.unwrap_or_else(|error| ::arty::runtime::__private::resume_error(error))
}
};
assert_eq!(expansion.to_string(), expected.to_string(), "panic = {policy}");
}
}
#[test]
fn monitor_composes_independently_of_argument_order() {
for expand in [main, test] {
let input = quote! {
async fn run(cx: Context) {}
};
let workers_first = expand(quote!(workers = 2, monitor = disabled), input.clone());
let monitor_first = expand(quote!(monitor = disabled, workers = 2), input);
assert_eq!(workers_first.to_string(), monitor_first.to_string());
}
}
#[test]
fn panic_composes_with_other_settings_independently_of_argument_order() {
for expand in [main, test] {
let input = quote! {
async fn run(cx: Context) {}
};
let panic_last = expand(quote!(workers = 2, monitor = disabled, panic = ignore), input.clone());
let panic_first = expand(quote!(panic = ignore, monitor = disabled, workers = 2), input);
assert_eq!(panic_last.to_string(), panic_first.to_string());
}
}
#[test]
fn worker_literals_are_not_limited_to_the_macro_hosts_pointer_width() {
let forwarded = proc_macro2::TokenTree::Group(proc_macro2::Group::new(proc_macro2::Delimiter::None, quote!(2)));
for workers in [
quote!(0),
quote!(0x0usize),
quote!(1),
quote!(0x10usize),
quote!(340282366920938463463374607431768211456),
TokenStream::from(forwarded),
] {
let expansion = main(
quote!(workers = #workers),
quote! {
async fn run(cx: Context) {}
},
);
assert!(!expansion.to_string().contains("compile_error"));
}
}
#[test]
fn worker_policy_unwraps_nested_expression_groups() {
for (mut value, expected) in [
(parse_quote!(4usize), quote!(::arty::runtime::WorkersPolicy::exactly(4usize))),
(parse_quote!(all), quote!(::arty::runtime::WorkersPolicy::all())),
] {
for _ in 0..2 {
value = Expr::Group(syn::ExprGroup {
attrs: Vec::new(),
group_token: syn::token::Group::default(),
expr: Box::new(value),
});
}
let policy = worker_policy(&value, "e!(::arty)).unwrap();
assert_eq!(policy.to_string(), expected.to_string());
}
}
#[test]
fn invalid_worker_counts_report_errors() {
for workers in [
quote!(-0),
quote!(-1),
quote!(-1usize),
quote!(-0x10),
quote!(1u32),
quote!(1.5),
quote!("2"),
quote!("ALL"),
quote!(""),
quote!("all"),
quote!(ALL),
quote!(module::all),
quote!(::all),
quote!(all::<usize>),
quote!(<T>::all),
quote!(true),
quote!(COUNT),
quote!(1 + 1),
quote!(WorkersPolicy::all()),
] {
for expand in [main, test] {
let expansion = expand(
quote!(workers = #workers),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert!(expansion.contains("compile_error"), "workers = {workers}: {expansion}");
assert!(expansion.contains("nonnegative integer literal"), "{expansion}");
}
}
}
#[test]
fn invalid_monitor_settings_report_errors() {
let expected = quote!(::core::compile_error! { "`monitor` must be the bare identifier `enabled` or `disabled`" }).to_string();
for monitor in [
quote!(true),
quote!(false),
quote!("enabled"),
quote!("disabled"),
quote!(automatic),
quote!(MonitorPolicy::enabled()),
quote!(::enabled),
] {
for expand in [main, test] {
let expansion = expand(
quote!(monitor = #monitor),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert_eq!(expansion, expected, "monitor = {monitor}");
}
}
}
#[test]
fn duplicate_monitor_settings_report_errors() {
let expected = quote!(::core::compile_error! { "Duplicate field `monitor`" }).to_string();
for expand in [main, test] {
let expansion = expand(
quote!(monitor = enabled, monitor = disabled),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert_eq!(expansion, expected);
}
}
#[test]
fn invalid_panic_settings_report_errors() {
let expected = quote!(::core::compile_error! { "`panic` must be the bare identifier `shutdown` or `ignore`" }).to_string();
for panic in [
quote!(true),
quote!(false),
quote!("shutdown"),
quote!("ignore"),
quote!(continue),
quote!(abort),
quote!(PanicPolicy::shutdown()),
quote!(::shutdown),
] {
for expand in [main, test] {
let expansion = expand(
quote!(panic = #panic),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert_eq!(expansion, expected, "panic = {panic}");
}
}
}
#[test]
fn duplicate_panic_settings_report_errors() {
let expected = quote!(::core::compile_error! { "Duplicate field `panic`" }).to_string();
for expand in [main, test] {
let expansion = expand(
quote!(panic = shutdown, panic = ignore),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert_eq!(expansion, expected);
}
}
#[test]
fn removed_configuration_options_are_rejected() {
for args in [quote!(builder = configure()), quote!(runtime_path = ::renamed)] {
for expand in [main, test] {
let expansion = expand(
args.clone(),
quote! {
async fn run(cx: Context) {}
},
)
.to_string();
assert!(expansion.contains("compile_error"), "{expansion}");
assert!(expansion.contains("Unknown field"), "{expansion}");
}
}
}
#[test]
fn injected_clock_requires_a_named_owned_second_test_parameter() {
for input in [
quote!(
async fn run(cx: Context, _: ClockControl) {}
),
quote!(
async fn run(cx: Context, control: &ClockControl) {}
),
quote!(
async fn run(cx: Context, control: ClockControl, extra: ClockControl) {}
),
quote!(
async fn run(cx: Context, cx: ClockControl) {}
),
quote!(
async fn run(cx: Context, r#cx: ClockControl) {}
),
] {
for expand in [main, test] {
let expansion = expand(TokenStream::new(), input.clone()).to_string();
assert!(expansion.starts_with(&input.to_string()));
assert!(expansion.contains("compile_error"), "{expansion}");
}
}
let expansion = main(
TokenStream::new(),
quote!(
async fn run(cx: Context, control: ClockControl) {}
),
)
.to_string();
assert!(expansion.contains("unexpected arguments"));
}
#[test]
fn invalid_signatures_preserve_input_and_report_errors() {
let cases = [
(
quote!(
fn run(cx: Context) {}
),
"function must be async",
),
(
quote!(
async fn run() {}
),
"function must take an Arty task context argument",
),
(
quote!(
async fn run(_: Context) {}
),
"argument must have an identifier",
),
(
quote!(
async fn run(cx: &Context) {}
),
"arty::task::Context by value",
),
(
quote!(
async fn run(cx: Context, extra: ()) {}
),
"unexpected arguments",
),
];
for (input, message) in cases {
for expand in [main, test] {
let expansion = expand(TokenStream::new(), input.clone()).to_string();
assert!(expansion.starts_with(&input.to_string()));
assert!(expansion.contains(message), "{expansion}");
assert!(expansion.contains("compile_error"));
}
}
}
#[test]
fn malformed_items_and_arguments_report_errors() {
for (args, input) in [
(TokenStream::new(), quote!(not a function)),
(
quote!(@),
quote!(
async fn run(cx: Context) {}
),
),
(
quote!(unknown = 1),
quote!(
async fn run(cx: Context) {}
),
),
(
quote!(workers = 1, workers = 2),
quote!(
async fn run(cx: Context) {}
),
),
] {
assert!(main(args, input).to_string().contains("compile_error"));
}
}
}