blob: 51a5633002e625aafa342768e78fec15681bfac0 [file] [log] [blame]
// Copyright 2020 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
fidl_fuchsia_diagnostics::Severity,
proc_macro2::TokenStream,
quote::{quote, quote_spanned, TokenStreamExt},
syn::{
parse::{Parse, ParseStream},
Attribute, Block, Error, Ident, ItemFn, LitBool, LitInt, LitStr, Signature, Token,
Visibility,
},
};
#[derive(Clone, Copy)]
enum FunctionType {
Component,
Test,
}
// How should code be executed?
#[derive(Clone, Copy)]
enum Executor {
// Directly by calling it
None,
// fasync::run_singlethreaded
Singlethreaded,
// fasync::run
Multithreaded(usize),
// #[test]
Test,
// fasync::run_singlethreaded(test)
SinglethreadedTest,
// fasync::run(test)
MultithreadedTest(usize),
// fasync::run_until_stalled
UntilStalledTest,
}
impl Executor {
fn is_test(&self) -> bool {
match self {
Executor::Test
| Executor::SinglethreadedTest
| Executor::MultithreadedTest(_)
| Executor::UntilStalledTest => true,
Executor::None | Executor::Singlethreaded | Executor::Multithreaded(_) => false,
}
}
fn is_some(&self) -> bool {
!matches!(self, Executor::Test | Executor::None)
}
}
impl quote::ToTokens for Executor {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self {
Executor::None => quote! { ::fuchsia::main_not_async(func) },
Executor::Test => quote! { ::fuchsia::test_not_async(func) },
Executor::Singlethreaded => quote! { ::fuchsia::main_singlethreaded(func) },
Executor::Multithreaded(n) => quote! { ::fuchsia::main_multithreaded(func, #n) },
Executor::SinglethreadedTest => quote! { ::fuchsia::test_singlethreaded(func) },
Executor::MultithreadedTest(n) => quote! { ::fuchsia::test_multithreaded(func, #n) },
Executor::UntilStalledTest => quote! { ::fuchsia::test_until_stalled(func) },
})
}
}
// Helper trait for things that can generate the final token stream
trait Finish {
fn finish(self) -> TokenStream
where
Self: Sized;
}
pub struct Transformer {
executor: Executor,
attrs: Vec<Attribute>,
vis: Visibility,
sig: Signature,
block: Box<Block>,
logging: bool,
logging_blocking: bool,
logging_tags: LoggingTags,
panic_prefix: LitStr,
interest: Interest,
add_test_attr: bool,
}
struct Args {
threads: usize,
allow_stalls: Option<bool>,
logging: bool,
logging_blocking: bool,
logging_tags: LoggingTags,
interest: Interest,
panic_prefix: Option<LitStr>,
add_test_attr: bool,
}
#[derive(Default)]
struct LoggingTags {
tags: Vec<String>,
}
impl quote::ToTokens for LoggingTags {
fn to_tokens(&self, tokens: &mut TokenStream) {
for tag in &self.tags {
tag.as_str().to_tokens(tokens);
tokens.append(proc_macro2::Punct::new(',', proc_macro2::Spacing::Alone));
}
}
}
impl Parse for LoggingTags {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut tags = vec![];
while !input.is_empty() {
tags.push(input.parse::<LitStr>()?.value());
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok(Self { tags })
}
}
#[derive(Default)]
struct Interest {
min_severity: Option<Severity>,
}
impl Parse for Interest {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let str_token = input.parse::<LitStr>()?;
let min_severity = match str_token.value().to_lowercase().as_str() {
"trace" => Severity::Trace,
"debug" => Severity::Debug,
"info" => Severity::Info,
"warn" => Severity::Warn,
"error" => Severity::Error,
"fatal" => Severity::Fatal,
other => {
return Err(syn::Error::new(
str_token.span(),
format!("invalid severity: {}", other),
))
}
};
Ok(Interest { min_severity: Some(min_severity) })
}
}
impl quote::ToTokens for Interest {
fn to_tokens(&self, tokens: &mut TokenStream) {
tokens.extend(match self.min_severity {
None => quote! { ::fuchsia::Interest::default() },
Some(severity) => match severity {
Severity::Trace => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Trace),
..Default::default()
}
},
Severity::Debug => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Debug),
..Default::default()
}
},
Severity::Info => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Info),
..Default::default()
}
},
Severity::Warn => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Warn),
..Default::default()
}
},
Severity::Error => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Error),
..Default::default()
}
},
Severity::Fatal => quote! {
::fuchsia::Interest {
min_severity: Some(::fuchsia::Severity::Fatal),
..Default::default()
}
},
},
});
}
}
fn get_arg<T: Parse>(p: &ParseStream<'_>) -> syn::Result<T> {
p.parse::<Token![=]>()?;
p.parse()
}
fn get_base10_arg<T>(p: &ParseStream<'_>) -> syn::Result<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
get_arg::<LitInt>(p)?.base10_parse()
}
fn get_bool_arg(p: &ParseStream<'_>, if_present: bool) -> syn::Result<bool> {
if p.peek(Token![=]) {
Ok(get_arg::<LitBool>(p)?.value)
} else {
Ok(if_present)
}
}
fn get_logging_tags(p: &ParseStream<'_>) -> syn::Result<LoggingTags> {
p.parse::<Token![=]>()?;
let content;
syn::bracketed!(content in p);
let logging_tags = content.parse::<LoggingTags>()?;
Ok(logging_tags)
}
fn get_interest_arg(input: &ParseStream<'_>) -> syn::Result<Interest> {
input.parse::<Token![=]>()?;
input.parse::<Interest>()
}
impl Parse for Args {
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
let mut args = Self {
threads: 1,
allow_stalls: None,
logging: true,
logging_blocking: false,
logging_tags: LoggingTags::default(),
panic_prefix: None,
interest: Interest::default(),
add_test_attr: true,
};
loop {
if input.is_empty() {
break;
}
let ident: Ident = input.parse()?;
let err = |message| Err(Error::new(ident.span(), message));
match ident.to_string().as_ref() {
"threads" => args.threads = get_base10_arg(&input)?,
"allow_stalls" => args.allow_stalls = Some(get_bool_arg(&input, true)?),
"logging" => args.logging = get_bool_arg(&input, true)?,
"logging_blocking" => args.logging_blocking = get_bool_arg(&input, true)?,
"logging_tags" => args.logging_tags = get_logging_tags(&input)?,
"logging_minimum_severity" => args.interest = get_interest_arg(&input)?,
"logging_panic_prefix" => args.panic_prefix = Some(get_arg(&input)?),
"add_test_attr" => args.add_test_attr = get_bool_arg(&input, true)?,
x => return err(format!("unknown argument: {}", x)),
}
if input.is_empty() {
break;
}
input.parse::<Token![,]>()?;
}
Ok(args)
}
}
impl Transformer {
pub fn parse_main(args: TokenStream, input: TokenStream) -> Result<Self, Error> {
Self::parse(FunctionType::Component, args, input)
}
pub fn parse_test(args: TokenStream, input: TokenStream) -> Result<Self, Error> {
Self::parse(FunctionType::Test, args, input)
}
pub fn finish(self) -> TokenStream {
Finish::finish(self)
}
// Construct a new Transformer, verifying correctness.
fn parse(
function_type: FunctionType,
args: TokenStream,
input: TokenStream,
) -> Result<Transformer, Error> {
let args: Args = syn::parse2(args)?;
let ItemFn { attrs, vis, sig, block } = syn::parse2(input)?;
let is_async = sig.asyncness.is_some();
let err = |message| Err(Error::new(sig.ident.span(), message));
let executor = match (args.threads, args.allow_stalls, is_async, function_type) {
(0, _, _, _) => return err("need at least one thread"),
(_, Some(_), _, FunctionType::Component) => {
return err("allow_stalls only applies to tests")
}
(1, _, false, FunctionType::Component) => Executor::None,
(1, None, true, FunctionType::Component) => Executor::Singlethreaded,
(n, None, true, FunctionType::Component) => Executor::Multithreaded(n),
(1, Some(_), false, FunctionType::Test) => {
return err("allow_stalls only applies to async tests")
}
(1, None, false, FunctionType::Test) => Executor::Test,
(1, Some(true) | None, true, FunctionType::Test) => Executor::SinglethreadedTest,
(n, Some(true) | None, true, FunctionType::Test) => Executor::MultithreadedTest(n),
(1, Some(false), true, FunctionType::Test) => Executor::UntilStalledTest,
(_, Some(false), _, FunctionType::Test) => {
return err("allow_stalls=false tests must be single threaded")
}
(_, Some(true) | None, false, _) => return err("must be async to use >1 thread"),
};
let panic_prefix =
args.panic_prefix.unwrap_or_else(|| LitStr::new("PANIC", sig.ident.span()));
Ok(Transformer {
executor,
attrs,
vis,
sig,
block,
logging: args.logging,
logging_blocking: args.logging_blocking,
logging_tags: args.logging_tags,
panic_prefix,
interest: args.interest,
add_test_attr: args.add_test_attr,
})
}
}
impl Finish for Transformer {
// Build the transformed code, knowing that everything is ok because we proved that in parse.
fn finish(self) -> TokenStream {
let ident = self.sig.ident;
let span = ident.span();
let ret_type = self.sig.output;
let attrs = self.attrs;
let visibility = self.vis;
let asyncness = self.sig.asyncness;
let block = self.block;
let inputs = self.sig.inputs;
let logging_blocking = self.logging_blocking;
let mut logging_tags = self.logging_tags;
let panic_prefix = self.panic_prefix;
let interest = self.interest;
let mut func_attrs = Vec::new();
// Initialize logging
let init_logging = if !self.logging {
quote! { func }
} else if self.executor.is_test() {
logging_tags.tags.insert(0, format!("{ident}"));
let logging_options = quote! {
::fuchsia::LoggingOptions {
blocking: #logging_blocking,
interest: #interest,
tags: &[#logging_tags],
panic_prefix: #panic_prefix,
}
};
if self.executor.is_some() {
quote!(::fuchsia::init_logging_for_test_with_executor(func, #logging_options))
} else {
quote!(::fuchsia::init_logging_for_test_with_threads(func, #logging_options))
}
} else {
let logging_options = quote! {
::fuchsia::LoggingOptions {
blocking: #logging_blocking,
interest: #interest,
tags: &[#logging_tags],
panic_prefix: #panic_prefix,
}
};
if self.executor.is_some() {
quote!(::fuchsia::init_logging_for_component_with_executor(func, #logging_options))
} else {
quote!(::fuchsia::init_logging_for_component_with_threads(func, #logging_options))
}
};
if self.executor.is_test() && self.add_test_attr {
// Add test attribute to outer function.
func_attrs.push(quote!(#[test]));
}
let func = if self.executor.is_test() {
quote! { test_entry_point }
} else {
quote! { component_entry_point }
};
// Adapt the runner function based on whether it's a test and argument count
// by providing needed arguments.
let adapt_main = match (self.executor.is_test(), inputs.len()) {
// Main function, no arguments - no adaption needed.
(false, 0) => quote! { #func },
// Main function, one arguemnt - adapt by parsing command line arguments.
(false, 1) => quote! { ::fuchsia::adapt_to_parse_arguments(#func) },
// Test function, no arguments - adapt by taking the run number and discarding it.
(true, 0) => quote! { ::fuchsia::adapt_to_take_test_run_number(#func) },
// Test function, one argument - no adaption needed.
(true, 1) => quote! { #func },
// Anything with more than one argument: error.
(_, n) => panic!("Too many ({}) arguments to function", n),
};
let tokenized_executor = self.executor;
let is_nonempty_ret_type = !matches!(ret_type, syn::ReturnType::Default);
// Select executor
let (run_executor, modified_ret_type) =
match (self.executor.is_test(), self.logging, is_nonempty_ret_type) {
(_, true, false) | (_, false, false) => {
(quote!(#tokenized_executor), quote!(#ret_type))
}
(_, false, true) => (quote!(#tokenized_executor), quote!(#ret_type)),
(true, _, _) => (quote!(#tokenized_executor), quote!(#ret_type)),
(false, _, _) => (
quote! {
let result = #tokenized_executor;
match result {
std::result::Result::Ok(val) => {
use std::process::Termination;
val.report()
},
std::result::Result::Err(err) => {
::fuchsia::error!("{err:?}");
std::process::ExitCode::FAILURE
}
}
},
quote!(-> std::process::ExitCode),
),
};
// Finally build output.
let output = quote_spanned! {span =>
#(#attrs)* #(#func_attrs)*
#visibility fn #ident () #modified_ret_type {
// Note: `ItemFn::block` includes the function body braces. Do
// not add additional braces (will break source code coverage
// analysis).
// TODO(https://fxbug.dev/42157203): Try to improve the Rust compiler to
// ease this restriction.
#asyncness fn #func(#inputs) #ret_type #block
let func = #adapt_main;
let func = #init_logging;
#run_executor
}
};
output.into()
}
}
impl Finish for Error {
fn finish(self) -> TokenStream {
self.to_compile_error().into()
}
}
impl<R: Finish, E: Finish> Finish for Result<R, E> {
fn finish(self) -> TokenStream {
match self {
Ok(r) => r.finish(),
Err(e) => e.finish(),
}
}
}