1#![doc(
7 html_logo_url = "https://commonware.xyz/imgs/rustdoc_logo.svg",
8 html_favicon_url = "https://commonware.xyz/favicon.ico"
9)]
10
11use crate::nextest::configured_test_groups;
12use proc_macro::TokenStream;
13use proc_macro2::Span;
14use proc_macro_crate::{crate_name, FoundCrate};
15use quote::{format_ident, quote};
16use syn::{
17 braced,
18 parse::{Parse, ParseStream, Result},
19 parse_macro_input, Error, Expr, Ident, ItemFn, LitInt, LitStr, Pat, Token, Visibility,
20};
21
22mod nextest;
23
24struct StabilityLevel {
27 value: u8,
28}
29
30impl Parse for StabilityLevel {
31 fn parse(input: ParseStream<'_>) -> Result<Self> {
32 let lookahead = input.lookahead1();
33 if lookahead.peek(LitInt) {
34 let lit: LitInt = input.parse()?;
35 let value: u8 = lit
36 .base10_parse()
37 .map_err(|_| Error::new(lit.span(), "stability level must be 0, 1, 2, 3, or 4"))?;
38 if value > 4 {
39 return Err(Error::new(
40 lit.span(),
41 "stability level must be 0, 1, 2, 3, or 4",
42 ));
43 }
44 Ok(Self { value })
45 } else if lookahead.peek(Ident) {
46 let ident: Ident = input.parse()?;
47 let value = match ident.to_string().as_str() {
48 "ALPHA" => 0,
49 "BETA" => 1,
50 "GAMMA" => 2,
51 "DELTA" => 3,
52 "EPSILON" => 4,
53 _ => {
54 return Err(Error::new(
55 ident.span(),
56 "expected stability level: ALPHA, BETA, GAMMA, DELTA, EPSILON, or 0-4",
57 ));
58 }
59 };
60 Ok(Self { value })
61 } else {
62 Err(lookahead.error())
63 }
64 }
65}
66
67fn level_name(level: u8) -> &'static str {
68 match level {
69 0 => "ALPHA",
70 1 => "BETA",
71 2 => "GAMMA",
72 3 => "DELTA",
73 4 => "EPSILON",
74 _ => unreachable!(),
75 }
76}
77
78fn exclusion_cfg_names(level: u8) -> Vec<proc_macro2::Ident> {
99 let mut names: Vec<_> = ((level + 1)..=4)
100 .map(|l| format_ident!("commonware_stability_{}", level_name(l)))
101 .collect();
102
103 names.push(format_ident!("commonware_stability_RESERVED"));
104 names
105}
106
107#[proc_macro_attribute]
108pub fn stability(attr: TokenStream, item: TokenStream) -> TokenStream {
109 let level = parse_macro_input!(attr as StabilityLevel);
110 let exclude_names = exclusion_cfg_names(level.value);
111
112 let item2: proc_macro2::TokenStream = item.into();
113 let expanded = quote! {
114 #[cfg(not(any(#(#exclude_names),*)))]
115 #item2
116 };
117
118 TokenStream::from(expanded)
119}
120
121struct StabilityModInput {
123 level: StabilityLevel,
124 visibility: Visibility,
125 name: Ident,
126}
127
128impl Parse for StabilityModInput {
129 fn parse(input: ParseStream<'_>) -> Result<Self> {
130 let level: StabilityLevel = input.parse()?;
131 input.parse::<Token![,]>()?;
132 let visibility: Visibility = input.parse()?;
133 input.parse::<Token![mod]>()?;
134 let name: Ident = input.parse()?;
135 Ok(Self {
136 level,
137 visibility,
138 name,
139 })
140 }
141}
142
143#[proc_macro]
144pub fn stability_mod(input: TokenStream) -> TokenStream {
145 let StabilityModInput {
146 level,
147 visibility,
148 name,
149 } = parse_macro_input!(input as StabilityModInput);
150
151 let exclude_names = exclusion_cfg_names(level.value);
152
153 let expanded = quote! {
154 #[cfg(not(any(#(#exclude_names),*)))]
155 #visibility mod #name;
156 };
157
158 TokenStream::from(expanded)
159}
160
161struct StabilityScopeInput {
163 level: StabilityLevel,
164 predicate: Option<syn::Meta>,
165 items: Vec<syn::Item>,
166}
167
168impl Parse for StabilityScopeInput {
169 fn parse(input: ParseStream<'_>) -> Result<Self> {
170 let level: StabilityLevel = input.parse()?;
171
172 let predicate = if input.peek(Token![,]) {
174 input.parse::<Token![,]>()?;
175
176 let cfg_ident: Ident = input.parse()?;
178 if cfg_ident != "cfg" {
179 return Err(Error::new(cfg_ident.span(), "expected `cfg`"));
180 }
181 let cfg_content;
182 syn::parenthesized!(cfg_content in input);
183 Some(cfg_content.parse()?)
184 } else {
185 None
186 };
187
188 let content;
189 braced!(content in input);
190
191 let mut items = Vec::new();
192 while !content.is_empty() {
193 items.push(content.parse()?);
194 }
195
196 Ok(Self {
197 level,
198 predicate,
199 items,
200 })
201 }
202}
203
204#[proc_macro]
205pub fn stability_scope(input: TokenStream) -> TokenStream {
206 let StabilityScopeInput {
207 level,
208 predicate,
209 items,
210 } = parse_macro_input!(input as StabilityScopeInput);
211
212 let exclude_names = exclusion_cfg_names(level.value);
213
214 let cfg_attr = predicate.map_or_else(
215 || quote! { #[cfg(not(any(#(#exclude_names),*)))] },
216 |pred| quote! { #[cfg(all(#pred, not(any(#(#exclude_names),*))))] },
217 );
218
219 let expanded_items: Vec<_> = items
220 .into_iter()
221 .map(|item| {
222 quote! {
223 #cfg_attr
224 #item
225 }
226 })
227 .collect();
228
229 let expanded = quote! {
230 #(#expanded_items)*
231 };
232
233 TokenStream::from(expanded)
234}
235
236#[proc_macro_attribute]
237pub fn boxed(_: TokenStream, item: TokenStream) -> TokenStream {
238 let mut input = parse_macro_input!(item as ItemFn);
239 if input.sig.asyncness.is_none() {
240 return Error::new_spanned(&input.sig, "#[boxed] can only be used with async functions")
241 .to_compile_error()
242 .into();
243 }
244
245 let block = input.block;
246 input.block = syn::parse_quote!({ ::std::boxed::Box::pin(async move #block).await });
247
248 quote!(#input).into()
249}
250
251#[proc_macro_attribute]
252pub fn test_async(_: TokenStream, item: TokenStream) -> TokenStream {
253 let input = parse_macro_input!(item as ItemFn);
255
256 let attrs = input.attrs;
258 let vis = input.vis;
259 let mut sig = input.sig;
260 let block = input.block;
261
262 sig.asyncness
265 .take()
266 .expect("test_async macro can only be used with async functions");
267
268 let expanded = quote! {
270 #[test]
271 #(#attrs)*
272 #vis #sig {
273 futures::executor::block_on(async #block);
274 }
275 };
276 TokenStream::from(expanded)
277}
278
279#[proc_macro_attribute]
280pub fn test_traced(attr: TokenStream, item: TokenStream) -> TokenStream {
281 let input = parse_macro_input!(item as ItemFn);
283
284 let default_level = if attr.is_empty() {
286 "debug".to_string()
287 } else {
288 let level_str = parse_macro_input!(attr as LitStr);
289 let level_ident = level_str.value().to_lowercase();
290 match level_ident.as_str() {
291 "trace" | "debug" | "info" | "warn" | "error" => level_ident,
292 _ => {
293 return Error::new_spanned(
294 level_str,
295 "Invalid log level. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR.",
296 )
297 .to_compile_error()
298 .into();
299 }
300 }
301 };
302
303 let attrs = input.attrs;
305 let vis = input.vis;
306 let sig = input.sig;
307 let block = input.block;
308
309 let expanded = quote! {
311 #[test]
312 #(#attrs)*
313 #vis #sig {
314 use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
315
316 let filter = EnvFilter::try_from_default_env()
318 .unwrap_or_else(|_| EnvFilter::new(#default_level));
319 let subscriber = tracing_subscriber::Registry::default()
320 .with(
321 tracing_subscriber::fmt::layer()
322 .with_test_writer()
323 .with_line_number(true)
324 .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE)
325 )
326 .with(filter);
327 let dispatcher = tracing::Dispatch::new(subscriber);
328
329 tracing::dispatcher::with_default(&dispatcher, || {
331 #block
332 });
333 }
334 };
335 TokenStream::from(expanded)
336}
337
338#[proc_macro_attribute]
339pub fn test_group(attr: TokenStream, item: TokenStream) -> TokenStream {
340 if attr.is_empty() {
341 return Error::new(
342 Span::call_site(),
343 "test_group requires a string literal filter group name",
344 )
345 .to_compile_error()
346 .into();
347 }
348
349 let mut input = parse_macro_input!(item as ItemFn);
350 let group_literal = parse_macro_input!(attr as LitStr);
351
352 let group = match nextest::sanitize_group_literal(&group_literal) {
353 Ok(group) => group,
354 Err(err) => return err.to_compile_error().into(),
355 };
356 let groups = match configured_test_groups() {
357 Ok(groups) => groups,
358 Err(_) => {
359 return TokenStream::from(quote!(#input));
361 }
362 };
363
364 if let Err(err) = nextest::ensure_group_known(groups, &group, group_literal.span()) {
365 return err.to_compile_error().into();
366 }
367
368 let original_name = input.sig.ident.to_string();
369 let new_ident = Ident::new(&format!("{original_name}_{group}_"), input.sig.ident.span());
370
371 input.sig.ident = new_ident;
372
373 TokenStream::from(quote!(#input))
374}
375
376#[proc_macro_attribute]
377pub fn test_collect_traces(attr: TokenStream, item: TokenStream) -> TokenStream {
378 let input = parse_macro_input!(item as ItemFn);
379
380 let log_level = if attr.is_empty() {
382 quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG }
384 } else {
385 let level_str = parse_macro_input!(attr as LitStr);
387 let level_ident = level_str.value().to_uppercase();
388 match level_ident.as_str() {
389 "TRACE" => quote! { ::tracing_subscriber::filter::LevelFilter::TRACE },
390 "DEBUG" => quote! { ::tracing_subscriber::filter::LevelFilter::DEBUG },
391 "INFO" => quote! { ::tracing_subscriber::filter::LevelFilter::INFO },
392 "WARN" => quote! { ::tracing_subscriber::filter::LevelFilter::WARN },
393 "ERROR" => quote! { ::tracing_subscriber::filter::LevelFilter::ERROR },
394 _ => {
395 return Error::new_spanned(
397 level_str,
398 "Invalid log level. Expected one of: TRACE, DEBUG, INFO, WARN, ERROR.",
399 )
400 .to_compile_error()
401 .into();
402 }
403 }
404 };
405
406 let attrs = input.attrs;
407 let vis = input.vis;
408 let sig = input.sig;
409 let block = input.block;
410
411 let inner_ident = format_ident!("__{}_inner_traced", sig.ident);
413 let mut inner_sig = sig.clone();
414 inner_sig.ident = inner_ident.clone();
415
416 let mut outer_sig = sig;
418 outer_sig.inputs.clear();
419
420 let rt_path = match crate_name("commonware-runtime") {
424 Ok(FoundCrate::Itself) => quote!(crate),
425 Ok(FoundCrate::Name(name)) => {
426 let ident = syn::Ident::new(&name, Span::call_site());
427 quote!(#ident)
428 }
429 Err(_) => quote!(::commonware_runtime), };
431
432 let expanded = quote! {
433 #(#attrs)*
436 #vis #inner_sig #block
437
438 #[test]
439 #vis #outer_sig {
440 use ::tracing_subscriber::{Layer, fmt, Registry, layer::SubscriberExt, util::SubscriberInitExt};
441 use ::tracing::{Dispatch, dispatcher};
442 use #rt_path::telemetry::traces::collector::{CollectingLayer, TraceStorage};
443
444 let trace_store = TraceStorage::default();
445 let collecting_layer = CollectingLayer::new(trace_store.clone());
446
447 let fmt_layer = fmt::layer()
448 .with_test_writer()
449 .with_line_number(true)
450 .with_span_events(fmt::format::FmtSpan::CLOSE)
451 .with_filter(#log_level);
452
453 let subscriber = Registry::default().with(collecting_layer).with(fmt_layer);
454 let dispatcher = Dispatch::new(subscriber);
455 dispatcher::with_default(&dispatcher, || {
456 #inner_ident(trace_store);
457 });
458 }
459 };
460
461 TokenStream::from(expanded)
462}
463
464struct SelectInput {
465 branches: Vec<Branch>,
466}
467
468struct Branch {
469 pattern: Pat,
470 future: Expr,
471 body: Expr,
472}
473
474struct SelectLoopBranch {
476 pattern: Pat,
477 future: Expr,
478 else_body: Option<Expr>,
479 body: Expr,
480}
481
482impl Parse for SelectInput {
483 fn parse(input: ParseStream<'_>) -> Result<Self> {
484 let mut branches = Vec::new();
485
486 while !input.is_empty() {
487 let pattern = Pat::parse_single(input)?;
488 input.parse::<Token![=]>()?;
489 let future: Expr = input.parse()?;
490 input.parse::<Token![=>]>()?;
491 let body: Expr = input.parse()?;
492
493 branches.push(Branch {
494 pattern,
495 future,
496 body,
497 });
498
499 if input.peek(Token![,]) {
500 input.parse::<Token![,]>()?;
501 } else {
502 break;
503 }
504 }
505
506 Ok(Self { branches })
507 }
508}
509
510#[proc_macro]
511pub fn select(input: TokenStream) -> TokenStream {
512 let SelectInput { branches } = parse_macro_input!(input as SelectInput);
514
515 let mut select_branches = Vec::new();
517 for Branch {
518 pattern,
519 future,
520 body,
521 } in branches.into_iter()
522 {
523 let branch_code = quote! {
525 #pattern = #future => #body,
526 };
527 select_branches.push(branch_code);
528 }
529
530 quote! {
532 {
533 ::commonware_macros::__reexport::tokio::select! {
534 biased;
535 #(#select_branches)*
536 }
537 }
538 }
539 .into()
540}
541
542struct SelectLoopInput {
546 context: Expr,
547 start_expr: Option<Expr>,
548 shutdown_expr: Expr,
549 branches: Vec<SelectLoopBranch>,
550 end_expr: Option<Expr>,
551}
552
553impl Parse for SelectLoopInput {
554 fn parse(input: ParseStream<'_>) -> Result<Self> {
555 let context: Expr = input.parse()?;
557 input.parse::<Token![,]>()?;
558
559 let start_expr = if input.peek(Ident) {
561 let ident: Ident = input.fork().parse()?;
562 if ident == "on_start" {
563 input.parse::<Ident>()?; input.parse::<Token![=>]>()?;
565 let expr: Expr = input.parse()?;
566 input.parse::<Token![,]>()?;
567 Some(expr)
568 } else {
569 None
570 }
571 } else {
572 None
573 };
574
575 let on_stopped_ident: Ident = input.parse()?;
577 if on_stopped_ident != "on_stopped" {
578 return Err(Error::new(
579 on_stopped_ident.span(),
580 "expected `on_stopped` keyword",
581 ));
582 }
583 input.parse::<Token![=>]>()?;
584
585 let shutdown_expr: Expr = input.parse()?;
587
588 input.parse::<Token![,]>()?;
590
591 let mut branches = Vec::new();
594 while !input.is_empty() {
595 if input.peek(Ident) {
597 let ident: Ident = input.fork().parse()?;
598 if ident == "on_end" {
599 break;
600 }
601 }
602
603 let pattern = Pat::parse_single(input)?;
604 input.parse::<Token![=]>()?;
605 let future: Expr = input.parse()?;
606
607 let else_body = if input.peek(Token![else]) {
609 input.parse::<Token![else]>()?;
610 Some(input.parse::<Expr>()?)
611 } else {
612 None
613 };
614
615 input.parse::<Token![=>]>()?;
616 let body: Expr = input.parse()?;
617
618 branches.push(SelectLoopBranch {
619 pattern,
620 future,
621 else_body,
622 body,
623 });
624
625 if input.peek(Token![,]) {
626 input.parse::<Token![,]>()?;
627 } else {
628 break;
629 }
630 }
631
632 let end_expr = if !input.is_empty() && input.peek(Ident) {
634 let ident: Ident = input.parse()?;
635 if ident == "on_end" {
636 input.parse::<Token![=>]>()?;
637 let expr: Expr = input.parse()?;
638 if input.peek(Token![,]) {
639 input.parse::<Token![,]>()?;
640 }
641 Some(expr)
642 } else {
643 return Err(Error::new(ident.span(), "expected `on_end` keyword"));
644 }
645 } else {
646 None
647 };
648
649 Ok(Self {
650 context,
651 start_expr,
652 shutdown_expr,
653 branches,
654 end_expr,
655 })
656 }
657}
658
659#[proc_macro]
660pub fn select_loop(input: TokenStream) -> TokenStream {
661 let SelectLoopInput {
662 context,
663 start_expr,
664 shutdown_expr,
665 branches,
666 end_expr,
667 } = parse_macro_input!(input as SelectLoopInput);
668
669 fn is_irrefutable(pat: &Pat) -> bool {
670 match pat {
671 Pat::Wild(_) | Pat::Rest(_) => true,
672 Pat::Ident(i) => i.subpat.as_ref().is_none_or(|(_, p)| is_irrefutable(p)),
673 Pat::Type(t) => is_irrefutable(&t.pat),
674 Pat::Tuple(t) => t.elems.iter().all(is_irrefutable),
675 Pat::Reference(r) => is_irrefutable(&r.pat),
676 Pat::Paren(p) => is_irrefutable(&p.pat),
677 _ => false,
678 }
679 }
680
681 for b in &branches {
682 if b.else_body.is_none() && !is_irrefutable(&b.pattern) {
683 return Error::new_spanned(
684 &b.pattern,
685 "refutable patterns require an else clause: \
686 `Some(msg) = future else break => { ... }`",
687 )
688 .to_compile_error()
689 .into();
690 }
691 }
692
693 let branch_tokens: Vec<_> = branches
695 .iter()
696 .map(|b| {
697 let pattern = &b.pattern;
698 let future = &b.future;
699 let body = &b.body;
700
701 b.else_body.as_ref().map_or_else(
703 || quote! { #pattern = #future => #body, },
705 |else_expr| {
707 quote! {
708 __select_result = #future => {
709 let #pattern = __select_result else { #else_expr };
710 #body
711 },
712 }
713 },
714 )
715 })
716 .collect();
717
718 fn expr_to_tokens(expr: &Expr) -> proc_macro2::TokenStream {
721 match expr {
722 Expr::Block(block) => {
723 let stmts = &block.block.stmts;
724 quote! { #(#stmts)* }
725 }
726 other => quote! { #other; },
727 }
728 }
729
730 let on_start_tokens = start_expr.as_ref().map(expr_to_tokens);
732 let on_end_tokens = end_expr.as_ref().map(expr_to_tokens);
733 let shutdown_tokens = expr_to_tokens(&shutdown_expr);
734
735 quote! {
736 {
737 let mut shutdown = #context.stopped();
738 loop {
739 #on_start_tokens
740
741 commonware_macros::select! {
742 _ = &mut shutdown => {
743 #shutdown_tokens
744
745 #[allow(unreachable_code)]
748 break;
749 },
750 #(#branch_tokens)*
751 }
752
753 #on_end_tokens
754 }
755 }
756 }
757 .into()
758}