Tasks: general system for recognizing and executing service work (#1343)

`polkadot-sdk` version of original tasks PR located here:
https://github.com/paritytech/substrate/pull/14329

Fixes #206

## Status
- [x] Generic `Task` trait
- [x] `RuntimeTask` aggregated enum, compatible with
`construct_runtime!`
- [x] Casting between `Task` and `RuntimeTask` without needing `dyn` or
`Box`
- [x] Tasks Example pallet
- [x] Runtime tests for Tasks example pallet
- [x] Parsing for task-related macros
- [x] Retrofit parsing to make macros optional
- [x] Expansion for task-related macros
- [x] Adds support for args in tasks
- [x] Retrofit tasks example pallet to use macros instead of manual
syntax
- [x] Weights
- [x] Cleanup
- [x] UI tests
- [x] Docs

## Target Syntax
Adapted from
https://github.com/paritytech/polkadot-sdk/issues/206#issue-1865172283

```rust
// NOTE: this enum is optional and is auto-generated by the other macros if not present
#[pallet::task]
pub enum Task<T: Config> {
    AddNumberIntoTotal {
        i: u32,
    }
}

/// Some running total.
#[pallet::storage]
pub(super) type Total<T: Config<I>, I: 'static = ()> =
StorageValue<_, (u32, u32), ValueQuery>;

/// Numbers to be added into the total.
#[pallet::storage]
pub(super) type Numbers<T: Config<I>, I: 'static = ()> =
StorageMap<_, Twox64Concat, u32, u32, OptionQuery>;

#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
	/// Add a pair of numbers into the totals and remove them.
	#[pallet::task_list(Numbers::<T, I>::iter_keys())]
	#[pallet::task_condition(|i| Numbers::<T, I>::contains_key(i))]
	#[pallet::task_index(0)]
	pub fn add_number_into_total(i: u32) -> DispatchResult {
		let v = Numbers::<T, I>::take(i).ok_or(Error::<T, I>::NotFound)?;
		Total::<T, I>::mutate(|(total_keys, total_values)| {
			*total_keys += i;
			*total_values += v;
		});
		Ok(())
	}
}
```

---------

Co-authored-by: Nikhil Gupta <17176722+gupnik@users.noreply.github.com>
Co-authored-by: kianenigma <kian@parity.io>
Co-authored-by: Nikhil Gupta <>
Co-authored-by: Gavin Wood <gavin@parity.io>
Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io>
Co-authored-by: gupnik <nikhilgupta.iitk@gmail.com>
This commit is contained in:
Sam Johnson
2023-12-08 00:40:26 -05:00
committed by GitHub
parent 34c991e2cf
commit ac3f14d23b
75 changed files with 3516 additions and 24 deletions
@@ -26,6 +26,7 @@ mod metadata;
mod origin;
mod outer_enums;
mod slash_reason;
mod task;
mod unsigned;
pub use call::expand_outer_dispatch;
@@ -38,4 +39,5 @@ pub use metadata::expand_runtime_metadata;
pub use origin::expand_outer_origin;
pub use outer_enums::{expand_outer_enum, OuterEnumType};
pub use slash_reason::expand_outer_slash_reason;
pub use task::expand_outer_task;
pub use unsigned::expand_outer_validate_unsigned;
@@ -0,0 +1,131 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License
use crate::construct_runtime::Pallet;
use proc_macro2::{Ident, TokenStream as TokenStream2};
use quote::quote;
/// Expands aggregate `RuntimeTask` enum.
pub fn expand_outer_task(
runtime_name: &Ident,
pallet_decls: &[Pallet],
scrate: &TokenStream2,
) -> TokenStream2 {
let mut from_impls = Vec::new();
let mut task_variants = Vec::new();
let mut variant_names = Vec::new();
let mut task_paths = Vec::new();
for decl in pallet_decls {
if decl.find_part("Task").is_none() {
continue;
}
let variant_name = &decl.name;
let path = &decl.path;
let index = decl.index;
from_impls.push(quote! {
impl From<#path::Task<#runtime_name>> for RuntimeTask {
fn from(hr: #path::Task<#runtime_name>) -> Self {
RuntimeTask::#variant_name(hr)
}
}
impl TryInto<#path::Task<#runtime_name>> for RuntimeTask {
type Error = ();
fn try_into(self) -> Result<#path::Task<#runtime_name>, Self::Error> {
match self {
RuntimeTask::#variant_name(hr) => Ok(hr),
_ => Err(()),
}
}
}
});
task_variants.push(quote! {
#[codec(index = #index)]
#variant_name(#path::Task<#runtime_name>),
});
variant_names.push(quote!(#variant_name));
task_paths.push(quote!(#path::Task));
}
let prelude = quote!(#scrate::traits::tasks::__private);
const INCOMPLETE_MATCH_QED: &'static str =
"cannot have an instantiated RuntimeTask without some Task variant in the runtime. QED";
let output = quote! {
/// An aggregation of all `Task` enums across all pallets included in the current runtime.
#[derive(
Clone, Eq, PartialEq,
#scrate::__private::codec::Encode,
#scrate::__private::codec::Decode,
#scrate::__private::scale_info::TypeInfo,
#scrate::__private::RuntimeDebug,
)]
pub enum RuntimeTask {
#( #task_variants )*
}
#[automatically_derived]
impl #scrate::traits::Task for RuntimeTask {
type Enumeration = #prelude::IntoIter<RuntimeTask>;
fn is_valid(&self) -> bool {
match self {
#(RuntimeTask::#variant_names(val) => val.is_valid(),)*
_ => unreachable!(#INCOMPLETE_MATCH_QED),
}
}
fn run(&self) -> Result<(), #scrate::traits::tasks::__private::DispatchError> {
match self {
#(RuntimeTask::#variant_names(val) => val.run(),)*
_ => unreachable!(#INCOMPLETE_MATCH_QED),
}
}
fn weight(&self) -> #scrate::pallet_prelude::Weight {
match self {
#(RuntimeTask::#variant_names(val) => val.weight(),)*
_ => unreachable!(#INCOMPLETE_MATCH_QED),
}
}
fn task_index(&self) -> u32 {
match self {
#(RuntimeTask::#variant_names(val) => val.task_index(),)*
_ => unreachable!(#INCOMPLETE_MATCH_QED),
}
}
fn iter() -> Self::Enumeration {
let mut all_tasks = Vec::new();
#(all_tasks.extend(#task_paths::iter().map(RuntimeTask::from).collect::<Vec<_>>());)*
all_tasks.into_iter()
}
}
#( #from_impls )*
};
output
}
@@ -386,6 +386,7 @@ fn construct_runtime_final_expansion(
let pallet_to_index = decl_pallet_runtime_setup(&name, &pallets, &scrate);
let dispatch = expand::expand_outer_dispatch(&name, system_pallet, &pallets, &scrate);
let tasks = expand::expand_outer_task(&name, &pallets, &scrate);
let metadata = expand::expand_runtime_metadata(
&name,
&pallets,
@@ -475,6 +476,8 @@ fn construct_runtime_final_expansion(
#dispatch
#tasks
#metadata
#outer_config
@@ -42,6 +42,7 @@ mod keyword {
syn::custom_keyword!(ValidateUnsigned);
syn::custom_keyword!(FreezeReason);
syn::custom_keyword!(HoldReason);
syn::custom_keyword!(Task);
syn::custom_keyword!(LockId);
syn::custom_keyword!(SlashReason);
syn::custom_keyword!(exclude_parts);
@@ -404,6 +405,7 @@ pub enum PalletPartKeyword {
ValidateUnsigned(keyword::ValidateUnsigned),
FreezeReason(keyword::FreezeReason),
HoldReason(keyword::HoldReason),
Task(keyword::Task),
LockId(keyword::LockId),
SlashReason(keyword::SlashReason),
}
@@ -434,6 +436,8 @@ impl Parse for PalletPartKeyword {
Ok(Self::FreezeReason(input.parse()?))
} else if lookahead.peek(keyword::HoldReason) {
Ok(Self::HoldReason(input.parse()?))
} else if lookahead.peek(keyword::Task) {
Ok(Self::Task(input.parse()?))
} else if lookahead.peek(keyword::LockId) {
Ok(Self::LockId(input.parse()?))
} else if lookahead.peek(keyword::SlashReason) {
@@ -459,6 +463,7 @@ impl PalletPartKeyword {
Self::ValidateUnsigned(_) => "ValidateUnsigned",
Self::FreezeReason(_) => "FreezeReason",
Self::HoldReason(_) => "HoldReason",
Self::Task(_) => "Task",
Self::LockId(_) => "LockId",
Self::SlashReason(_) => "SlashReason",
}
@@ -471,7 +476,7 @@ impl PalletPartKeyword {
/// Returns the names of all pallet parts that allow to have a generic argument.
fn all_generic_arg() -> &'static [&'static str] {
&["Event", "Error", "Origin", "Config"]
&["Event", "Error", "Origin", "Config", "Task"]
}
}
@@ -489,6 +494,7 @@ impl ToTokens for PalletPartKeyword {
Self::ValidateUnsigned(inner) => inner.to_tokens(tokens),
Self::FreezeReason(inner) => inner.to_tokens(tokens),
Self::HoldReason(inner) => inner.to_tokens(tokens),
Self::Task(inner) => inner.to_tokens(tokens),
Self::LockId(inner) => inner.to_tokens(tokens),
Self::SlashReason(inner) => inner.to_tokens(tokens),
}
+54 -4
View File
@@ -646,7 +646,6 @@ pub fn storage_alias(attributes: TokenStream, input: TokenStream) -> TokenStream
/// ```
///
/// where `TestDefaultConfig` was defined and registered as follows:
///
/// ```ignore
/// pub struct TestDefaultConfig;
///
@@ -673,7 +672,6 @@ pub fn storage_alias(attributes: TokenStream, input: TokenStream) -> TokenStream
/// ```
///
/// The above call to `derive_impl` would expand to roughly the following:
///
/// ```ignore
/// impl frame_system::Config for Test {
/// use frame_system::config_preludes::TestDefaultConfig;
@@ -881,6 +879,7 @@ pub fn inject_runtime_type(_: TokenStream, tokens: TokenStream) -> TokenStream {
let item = syn::parse_macro_input!(item as TraitItemType);
if item.ident != "RuntimeCall" &&
item.ident != "RuntimeEvent" &&
item.ident != "RuntimeTask" &&
item.ident != "RuntimeOrigin" &&
item.ident != "RuntimeHoldReason" &&
item.ident != "RuntimeFreezeReason" &&
@@ -888,10 +887,11 @@ pub fn inject_runtime_type(_: TokenStream, tokens: TokenStream) -> TokenStream {
{
return syn::Error::new_spanned(
item,
"`#[inject_runtime_type]` can only be attached to `RuntimeCall`, `RuntimeEvent`, `RuntimeOrigin` or `PalletInfo`",
"`#[inject_runtime_type]` can only be attached to `RuntimeCall`, `RuntimeEvent`, \
`RuntimeTask`, `RuntimeOrigin` or `PalletInfo`",
)
.to_compile_error()
.into();
.into()
}
tokens
}
@@ -1518,6 +1518,56 @@ pub fn composite_enum(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
///
/// ---
///
/// **Rust-Analyzer users**: See the documentation of the Rust item in
/// `frame_support::pallet_macros::tasks_experimental`.
#[proc_macro_attribute]
pub fn tasks_experimental(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
///
/// ---
///
/// **Rust-Analyzer users**: See the documentation of the Rust item in
/// `frame_support::pallet_macros::task_list`.
#[proc_macro_attribute]
pub fn task_list(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
///
/// ---
///
/// **Rust-Analyzer users**: See the documentation of the Rust item in
/// `frame_support::pallet_macros::task_condition`.
#[proc_macro_attribute]
pub fn task_condition(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
///
/// ---
///
/// **Rust-Analyzer users**: See the documentation of the Rust item in
/// `frame_support::pallet_macros::task_weight`.
#[proc_macro_attribute]
pub fn task_weight(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
///
/// ---
///
/// **Rust-Analyzer users**: See the documentation of the Rust item in
/// `frame_support::pallet_macros::task_index`.
#[proc_macro_attribute]
pub fn task_index(_: TokenStream, _: TokenStream) -> TokenStream {
pallet_macro_stub()
}
/// Can be attached to a module. Doing so will declare that module as importable into a pallet
/// via [`#[import_section]`](`macro@import_section`).
///
@@ -31,6 +31,7 @@ mod origin;
mod pallet_struct;
mod storage;
mod store_trait;
mod tasks;
mod tt_default_parts;
mod type_value;
mod validate_unsigned;
@@ -60,6 +61,7 @@ pub fn expand(mut def: Def) -> proc_macro2::TokenStream {
let pallet_struct = pallet_struct::expand_pallet_struct(&mut def);
let config = config::expand_config(&mut def);
let call = call::expand_call(&mut def);
let tasks = tasks::expand_tasks(&mut def);
let error = error::expand_error(&mut def);
let event = event::expand_event(&mut def);
let storages = storage::expand_storages(&mut def);
@@ -100,6 +102,7 @@ storage item. Otherwise, all storage items are listed among [*Type Definitions*]
#pallet_struct
#config
#call
#tasks
#error
#event
#storages
@@ -0,0 +1,267 @@
//! Contains logic for expanding task-related items.
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Home of the expansion code for the Tasks API
use crate::pallet::{parse::tasks::*, Def};
use derive_syn_parse::Parse;
use inflector::Inflector;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote, ToTokens};
use syn::{parse_quote, spanned::Spanned, ItemEnum, ItemImpl};
impl TaskEnumDef {
/// Since we optionally allow users to manually specify a `#[pallet::task_enum]`, in the
/// event they _don't_ specify one (which is actually the most common behavior) we have to
/// generate one based on the existing [`TasksDef`]. This method performs that generation.
pub fn generate(
tasks: &TasksDef,
type_decl_bounded_generics: TokenStream2,
type_use_generics: TokenStream2,
) -> Self {
let variants = if tasks.tasks_attr.is_some() {
tasks
.tasks
.iter()
.map(|task| {
let ident = &task.item.sig.ident;
let ident =
format_ident!("{}", ident.to_string().to_class_case(), span = ident.span());
let args = task.item.sig.inputs.iter().collect::<Vec<_>>();
if args.is_empty() {
quote!(#ident)
} else {
quote!(#ident {
#(#args),*
})
}
})
.collect::<Vec<_>>()
} else {
Vec::new()
};
let mut task_enum_def: TaskEnumDef = parse_quote! {
/// Auto-generated enum that encapsulates all tasks defined by this pallet.
///
/// Conceptually similar to the [`Call`] enum, but for tasks. This is only
/// generated if there are tasks present in this pallet.
#[pallet::task_enum]
pub enum Task<#type_decl_bounded_generics> {
#(
#variants,
)*
}
};
task_enum_def.type_use_generics = type_use_generics;
task_enum_def
}
}
impl ToTokens for TaskEnumDef {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let item_enum = &self.item_enum;
let ident = &item_enum.ident;
let vis = &item_enum.vis;
let attrs = &item_enum.attrs;
let generics = &item_enum.generics;
let variants = &item_enum.variants;
let scrate = &self.scrate;
let type_use_generics = &self.type_use_generics;
if self.attr.is_some() {
// `item_enum` is short-hand / generated enum
tokens.extend(quote! {
#(#attrs)*
#[derive(
#scrate::CloneNoBound,
#scrate::EqNoBound,
#scrate::PartialEqNoBound,
#scrate::pallet_prelude::Encode,
#scrate::pallet_prelude::Decode,
#scrate::pallet_prelude::TypeInfo,
)]
#[codec(encode_bound())]
#[codec(decode_bound())]
#[scale_info(skip_type_params(#type_use_generics))]
#vis enum #ident #generics {
#variants
#[doc(hidden)]
#[codec(skip)]
__Ignore(core::marker::PhantomData<T>, #scrate::Never),
}
impl<T: Config> core::fmt::Debug for #ident<#type_use_generics> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct(stringify!(#ident)).field("value", self).finish()
}
}
});
} else {
// `item_enum` is a manually specified enum (no attribute)
tokens.extend(item_enum.to_token_stream());
}
}
}
/// Represents an already-expanded [`TasksDef`].
#[derive(Parse)]
pub struct ExpandedTasksDef {
pub task_item_impl: ItemImpl,
pub task_trait_impl: ItemImpl,
}
impl ToTokens for TasksDef {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let scrate = &self.scrate;
let enum_ident = syn::Ident::new("Task", self.enum_ident.span());
let enum_arguments = &self.enum_arguments;
let enum_use = quote!(#enum_ident #enum_arguments);
let task_fn_idents = self
.tasks
.iter()
.map(|task| {
format_ident!(
"{}",
&task.item.sig.ident.to_string().to_class_case(),
span = task.item.sig.ident.span()
)
})
.collect::<Vec<_>>();
let task_indices = self.tasks.iter().map(|task| &task.index_attr.meta.index);
let task_conditions = self.tasks.iter().map(|task| &task.condition_attr.meta.expr);
let task_weights = self.tasks.iter().map(|task| &task.weight_attr.meta.expr);
let task_iters = self.tasks.iter().map(|task| &task.list_attr.meta.expr);
let task_fn_impls = self.tasks.iter().map(|task| {
let mut task_fn_impl = task.item.clone();
task_fn_impl.attrs = vec![];
task_fn_impl
});
let task_fn_names = self.tasks.iter().map(|task| &task.item.sig.ident);
let task_arg_names = self.tasks.iter().map(|task| &task.arg_names).collect::<Vec<_>>();
let sp_std = quote!(#scrate::__private::sp_std);
let impl_generics = &self.item_impl.generics;
tokens.extend(quote! {
impl #impl_generics #enum_use
{
#(#task_fn_impls)*
}
impl #impl_generics #scrate::traits::Task for #enum_use
{
type Enumeration = #sp_std::vec::IntoIter<#enum_use>;
fn iter() -> Self::Enumeration {
let mut all_tasks = #sp_std::vec![];
#(all_tasks
.extend(#task_iters.map(|(#(#task_arg_names),*)| #enum_ident::#task_fn_idents { #(#task_arg_names: #task_arg_names.clone()),* })
.collect::<#sp_std::vec::Vec<_>>());
)*
all_tasks.into_iter()
}
fn task_index(&self) -> u32 {
match self.clone() {
#(#enum_ident::#task_fn_idents { .. } => #task_indices,)*
Task::__Ignore(_, _) => unreachable!(),
}
}
fn is_valid(&self) -> bool {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => (#task_conditions)(#(#task_arg_names),* ),)*
Task::__Ignore(_, _) => unreachable!(),
}
}
fn run(&self) -> Result<(), #scrate::pallet_prelude::DispatchError> {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => {
<#enum_use>::#task_fn_names(#( #task_arg_names, )* )
},)*
Task::__Ignore(_, _) => unreachable!(),
}
}
#[allow(unused_variables)]
fn weight(&self) -> #scrate::pallet_prelude::Weight {
match self.clone() {
#(#enum_ident::#task_fn_idents { #(#task_arg_names),* } => #task_weights,)*
Task::__Ignore(_, _) => unreachable!(),
}
}
}
});
}
}
/// Expands the [`TasksDef`] in the enclosing [`Def`], if present, and returns its tokens.
///
/// This modifies the underlying [`Def`] in addition to returning any tokens that were added.
pub fn expand_tasks_impl(def: &mut Def) -> TokenStream2 {
let Some(tasks) = &mut def.tasks else { return quote!() };
let ExpandedTasksDef { task_item_impl, task_trait_impl } = parse_quote!(#tasks);
quote! {
#task_item_impl
#task_trait_impl
}
}
/// Represents a fully-expanded [`TaskEnumDef`].
#[derive(Parse)]
pub struct ExpandedTaskEnum {
pub item_enum: ItemEnum,
pub debug_impl: ItemImpl,
}
/// Modifies a [`Def`] to expand the underlying [`TaskEnumDef`] if present, and also returns
/// its tokens. A blank [`TokenStream2`] is returned if no [`TaskEnumDef`] has been generated
/// or defined.
pub fn expand_task_enum(def: &mut Def) -> TokenStream2 {
let Some(task_enum) = &mut def.task_enum else { return quote!() };
let ExpandedTaskEnum { item_enum, debug_impl } = parse_quote!(#task_enum);
quote! {
#item_enum
#debug_impl
}
}
/// Modifies a [`Def`] to expand the underlying [`TasksDef`] and also generate a
/// [`TaskEnumDef`] if applicable. The tokens for these items are returned if they are created.
pub fn expand_tasks(def: &mut Def) -> TokenStream2 {
if let Some(tasks_def) = &def.tasks {
if def.task_enum.is_none() {
def.task_enum = Some(TaskEnumDef::generate(
&tasks_def,
def.type_decl_bounded_generics(tasks_def.item_impl.span()),
def.type_use_generics(tasks_def.item_impl.span()),
));
}
}
let tasks_extra_output = expand_tasks_impl(def);
let task_enum_extra_output = expand_task_enum(def);
quote! {
#tasks_extra_output
#task_enum_extra_output
}
}
@@ -31,6 +31,8 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream {
let call_part = def.call.as_ref().map(|_| quote::quote!(Call,));
let task_part = def.task_enum.as_ref().map(|_| quote::quote!(Task,));
let storage_part = (!def.storages.is_empty()).then(|| quote::quote!(Storage,));
let event_part = def.event.as_ref().map(|event| {
@@ -99,7 +101,7 @@ pub fn expand_tt_default_parts(def: &mut Def) -> proc_macro2::TokenStream {
tokens = [{
expanded::{
Pallet, #call_part #storage_part #event_part #error_part #origin_part #config_part
#inherent_part #validate_unsigned_part #freeze_reason_part
#inherent_part #validate_unsigned_part #freeze_reason_part #task_part
#hold_reason_part #lock_id_part #slash_reason_part
}
}]
@@ -286,8 +286,7 @@ impl CallDef {
if weight_attrs.is_empty() && dev_mode {
// inject a default O(1) weight when dev mode is enabled and no weight has
// been specified on the call
let empty_weight: syn::Expr = syn::parse(quote::quote!(0).into())
.expect("we are parsing a quoted string; qed");
let empty_weight: syn::Expr = syn::parse_quote!(0);
weight_attrs.push(FunctionAttr::Weight(empty_weight));
}
@@ -26,11 +26,14 @@ pub mod keyword {
syn::custom_keyword!(HoldReason);
syn::custom_keyword!(LockId);
syn::custom_keyword!(SlashReason);
syn::custom_keyword!(Task);
pub enum CompositeKeyword {
FreezeReason(FreezeReason),
HoldReason(HoldReason),
LockId(LockId),
SlashReason(SlashReason),
Task(Task),
}
impl ToTokens for CompositeKeyword {
@@ -41,6 +44,7 @@ pub mod keyword {
HoldReason(inner) => inner.to_tokens(tokens),
LockId(inner) => inner.to_tokens(tokens),
SlashReason(inner) => inner.to_tokens(tokens),
Task(inner) => inner.to_tokens(tokens),
}
}
}
@@ -56,6 +60,8 @@ pub mod keyword {
Ok(Self::LockId(input.parse()?))
} else if lookahead.peek(SlashReason) {
Ok(Self::SlashReason(input.parse()?))
} else if lookahead.peek(Task) {
Ok(Self::Task(input.parse()?))
} else {
Err(lookahead.error())
}
@@ -71,6 +77,7 @@ pub mod keyword {
match self {
FreezeReason(_) => "FreezeReason",
HoldReason(_) => "HoldReason",
Task(_) => "Task",
LockId(_) => "LockId",
SlashReason(_) => "SlashReason",
}
@@ -80,7 +87,7 @@ pub mod keyword {
}
pub struct CompositeDef {
/// The index of the HoldReason item in the pallet module.
/// The index of the CompositeDef item in the pallet module.
pub index: usize,
/// The composite keyword used (contains span).
pub composite_keyword: keyword::CompositeKeyword,
@@ -33,11 +33,16 @@ pub mod inherent;
pub mod origin;
pub mod pallet_struct;
pub mod storage;
pub mod tasks;
pub mod type_value;
pub mod validate_unsigned;
#[cfg(test)]
pub mod tests;
use composite::{keyword::CompositeKeyword, CompositeDef};
use frame_support_procedural_tools::generate_access_from_frame_or_crate;
use quote::ToTokens;
use syn::spanned::Spanned;
/// Parsed definition of a pallet.
@@ -49,6 +54,8 @@ pub struct Def {
pub pallet_struct: pallet_struct::PalletStructDef,
pub hooks: Option<hooks::HooksDef>,
pub call: Option<call::CallDef>,
pub tasks: Option<tasks::TasksDef>,
pub task_enum: Option<tasks::TaskEnumDef>,
pub storages: Vec<storage::StorageDef>,
pub error: Option<error::ErrorDef>,
pub event: Option<event::EventDef>,
@@ -84,6 +91,8 @@ impl Def {
let mut pallet_struct = None;
let mut hooks = None;
let mut call = None;
let mut tasks = None;
let mut task_enum = None;
let mut error = None;
let mut event = None;
let mut origin = None;
@@ -118,6 +127,32 @@ impl Def {
},
Some(PalletAttr::RuntimeCall(cw, span)) if call.is_none() =>
call = Some(call::CallDef::try_from(span, index, item, dev_mode, cw)?),
Some(PalletAttr::Tasks(_)) if tasks.is_none() => {
let item_tokens = item.to_token_stream();
// `TasksDef::parse` needs to know if attr was provided so we artificially
// re-insert it here
tasks = Some(syn::parse2::<tasks::TasksDef>(quote::quote! {
#[pallet::tasks_experimental]
#item_tokens
})?);
// replace item with a no-op because it will be handled by the expansion of tasks
*item = syn::Item::Verbatim(quote::quote!());
}
Some(PalletAttr::TaskCondition(span)) => return Err(syn::Error::new(
span,
"`#[pallet::task_condition]` can only be used on items within an `impl` statement."
)),
Some(PalletAttr::TaskIndex(span)) => return Err(syn::Error::new(
span,
"`#[pallet::task_index]` can only be used on items within an `impl` statement."
)),
Some(PalletAttr::TaskList(span)) => return Err(syn::Error::new(
span,
"`#[pallet::task_list]` can only be used on items within an `impl` statement."
)),
Some(PalletAttr::RuntimeTask(_)) if task_enum.is_none() =>
task_enum = Some(syn::parse2::<tasks::TaskEnumDef>(item.to_token_stream())?),
Some(PalletAttr::Error(span)) if error.is_none() =>
error = Some(error::ErrorDef::try_from(span, index, item)?),
Some(PalletAttr::RuntimeEvent(span)) if event.is_none() =>
@@ -190,6 +225,8 @@ impl Def {
return Err(syn::Error::new(item_span, msg))
}
Self::resolve_tasks(&item_span, &mut tasks, &mut task_enum, items)?;
let def = Def {
item,
config: config
@@ -198,6 +235,8 @@ impl Def {
.ok_or_else(|| syn::Error::new(item_span, "Missing `#[pallet::pallet]`"))?,
hooks,
call,
tasks,
task_enum,
extra_constants,
genesis_config,
genesis_build,
@@ -220,6 +259,99 @@ impl Def {
Ok(def)
}
/// Performs extra logic checks necessary for the `#[pallet::tasks_experimental]` feature.
fn resolve_tasks(
item_span: &proc_macro2::Span,
tasks: &mut Option<tasks::TasksDef>,
task_enum: &mut Option<tasks::TaskEnumDef>,
items: &mut Vec<syn::Item>,
) -> syn::Result<()> {
// fallback for manual (without macros) definition of tasks impl
Self::resolve_manual_tasks_impl(tasks, task_enum, items)?;
// fallback for manual (without macros) definition of task enum
Self::resolve_manual_task_enum(tasks, task_enum, items)?;
// ensure that if `task_enum` is specified, `tasks` is also specified
match (&task_enum, &tasks) {
(Some(_), None) =>
return Err(syn::Error::new(
*item_span,
"Missing `#[pallet::tasks_experimental]` impl",
)),
(None, Some(tasks)) =>
if tasks.tasks_attr.is_none() {
return Err(syn::Error::new(
tasks.item_impl.impl_token.span(),
"A `#[pallet::tasks_experimental]` attribute must be attached to your `Task` impl if the \
task enum has been omitted",
))
} else {
},
_ => (),
}
Ok(())
}
/// Tries to locate task enum based on the tasks impl target if attribute is not specified
/// but impl is present. If one is found, `task_enum` is set appropriately.
fn resolve_manual_task_enum(
tasks: &Option<tasks::TasksDef>,
task_enum: &mut Option<tasks::TaskEnumDef>,
items: &mut Vec<syn::Item>,
) -> syn::Result<()> {
let (None, Some(tasks)) = (&task_enum, &tasks) else { return Ok(()) };
let syn::Type::Path(type_path) = &*tasks.item_impl.self_ty else { return Ok(()) };
let type_path = type_path.path.segments.iter().collect::<Vec<_>>();
let (Some(seg), None) = (type_path.get(0), type_path.get(1)) else { return Ok(()) };
let mut result = None;
for item in items {
let syn::Item::Enum(item_enum) = item else { continue };
if item_enum.ident == seg.ident {
result = Some(syn::parse2::<tasks::TaskEnumDef>(item_enum.to_token_stream())?);
// replace item with a no-op because it will be handled by the expansion of
// `task_enum`. We use a no-op instead of simply removing it from the vec
// so that any indices collected by `Def::try_from` remain accurate
*item = syn::Item::Verbatim(quote::quote!());
break
}
}
*task_enum = result;
Ok(())
}
/// Tries to locate a manual tasks impl (an impl impling a trait whose last path segment is
/// `Task`) in the event that one has not been found already via the attribute macro
pub fn resolve_manual_tasks_impl(
tasks: &mut Option<tasks::TasksDef>,
task_enum: &Option<tasks::TaskEnumDef>,
items: &Vec<syn::Item>,
) -> syn::Result<()> {
let None = tasks else { return Ok(()) };
let mut result = None;
for item in items {
let syn::Item::Impl(item_impl) = item else { continue };
let Some((_, path, _)) = &item_impl.trait_ else { continue };
let Some(trait_last_seg) = path.segments.last() else { continue };
let syn::Type::Path(target_path) = &*item_impl.self_ty else { continue };
let target_path = target_path.path.segments.iter().collect::<Vec<_>>();
let (Some(target_ident), None) = (target_path.get(0), target_path.get(1)) else {
continue
};
let matches_task_enum = match task_enum {
Some(task_enum) => task_enum.item_enum.ident == target_ident.ident,
None => true,
};
if trait_last_seg.ident == "Task" && matches_task_enum {
result = Some(syn::parse2::<tasks::TasksDef>(item_impl.to_token_stream())?);
break
}
}
*tasks = result;
Ok(())
}
/// Check that usage of trait `Event` is consistent with the definition, i.e. it is declared
/// and trait defines type RuntimeEvent, or not declared and no trait associated type.
fn check_event_usage(&self) -> syn::Result<()> {
@@ -408,6 +540,11 @@ impl GenericKind {
mod keyword {
syn::custom_keyword!(origin);
syn::custom_keyword!(call);
syn::custom_keyword!(tasks_experimental);
syn::custom_keyword!(task_enum);
syn::custom_keyword!(task_list);
syn::custom_keyword!(task_condition);
syn::custom_keyword!(task_index);
syn::custom_keyword!(weight);
syn::custom_keyword!(event);
syn::custom_keyword!(config);
@@ -472,6 +609,11 @@ enum PalletAttr {
/// instead of the zero weight. So to say: it works together with `dev_mode`.
RuntimeCall(Option<InheritedCallWeightAttr>, proc_macro2::Span),
Error(proc_macro2::Span),
Tasks(proc_macro2::Span),
TaskList(proc_macro2::Span),
TaskCondition(proc_macro2::Span),
TaskIndex(proc_macro2::Span),
RuntimeTask(proc_macro2::Span),
RuntimeEvent(proc_macro2::Span),
RuntimeOrigin(proc_macro2::Span),
Inherent(proc_macro2::Span),
@@ -490,8 +632,13 @@ impl PalletAttr {
Self::Config(span, _) => *span,
Self::Pallet(span) => *span,
Self::Hooks(span) => *span,
Self::RuntimeCall(_, span) => *span,
Self::Tasks(span) => *span,
Self::TaskCondition(span) => *span,
Self::TaskIndex(span) => *span,
Self::TaskList(span) => *span,
Self::Error(span) => *span,
Self::RuntimeTask(span) => *span,
Self::RuntimeCall(_, span) => *span,
Self::RuntimeEvent(span) => *span,
Self::RuntimeOrigin(span) => *span,
Self::Inherent(span) => *span,
@@ -535,6 +682,16 @@ impl syn::parse::Parse for PalletAttr {
false => Some(InheritedCallWeightAttr::parse(&content)?),
};
Ok(PalletAttr::RuntimeCall(attr, span))
} else if lookahead.peek(keyword::tasks_experimental) {
Ok(PalletAttr::Tasks(content.parse::<keyword::tasks_experimental>()?.span()))
} else if lookahead.peek(keyword::task_enum) {
Ok(PalletAttr::RuntimeTask(content.parse::<keyword::task_enum>()?.span()))
} else if lookahead.peek(keyword::task_condition) {
Ok(PalletAttr::TaskCondition(content.parse::<keyword::task_condition>()?.span()))
} else if lookahead.peek(keyword::task_index) {
Ok(PalletAttr::TaskIndex(content.parse::<keyword::task_index>()?.span()))
} else if lookahead.peek(keyword::task_list) {
Ok(PalletAttr::TaskList(content.parse::<keyword::task_list>()?.span()))
} else if lookahead.peek(keyword::error) {
Ok(PalletAttr::Error(content.parse::<keyword::error>()?.span()))
} else if lookahead.peek(keyword::event) {
@@ -0,0 +1,968 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Home of the parsing code for the Tasks API
use std::collections::HashSet;
#[cfg(test)]
use crate::assert_parse_error_matches;
#[cfg(test)]
use crate::pallet::parse::tests::simulate_manifest_dir;
use derive_syn_parse::Parse;
use frame_support_procedural_tools::generate_access_from_frame_or_crate;
use proc_macro2::TokenStream as TokenStream2;
use quote::{quote, ToTokens};
use syn::{
parse::ParseStream,
parse2,
spanned::Spanned,
token::{Bracket, Paren, PathSep, Pound},
Attribute, Error, Expr, Ident, ImplItem, ImplItemFn, ItemEnum, ItemImpl, LitInt, Path,
PathArguments, Result, TypePath,
};
pub mod keywords {
use syn::custom_keyword;
custom_keyword!(tasks_experimental);
custom_keyword!(task_enum);
custom_keyword!(task_list);
custom_keyword!(task_condition);
custom_keyword!(task_index);
custom_keyword!(task_weight);
custom_keyword!(pallet);
}
/// Represents the `#[pallet::tasks_experimental]` attribute and its attached item. Also includes
/// metadata about the linked [`TaskEnumDef`] if applicable.
#[derive(Clone, Debug)]
pub struct TasksDef {
pub tasks_attr: Option<PalletTasksAttr>,
pub tasks: Vec<TaskDef>,
pub item_impl: ItemImpl,
/// Path to `frame_support`
pub scrate: Path,
pub enum_ident: Ident,
pub enum_arguments: PathArguments,
}
impl syn::parse::Parse for TasksDef {
fn parse(input: ParseStream) -> Result<Self> {
let item_impl: ItemImpl = input.parse()?;
let (tasks_attrs, normal_attrs) = partition_tasks_attrs(&item_impl);
let tasks_attr = match tasks_attrs.first() {
Some(attr) => Some(parse2::<PalletTasksAttr>(attr.to_token_stream())?),
None => None,
};
if let Some(extra_tasks_attr) = tasks_attrs.get(1) {
return Err(Error::new(
extra_tasks_attr.span(),
"unexpected extra `#[pallet::tasks_experimental]` attribute",
))
}
let tasks: Vec<TaskDef> = if tasks_attr.is_some() {
item_impl
.items
.clone()
.into_iter()
.filter(|impl_item| matches!(impl_item, ImplItem::Fn(_)))
.map(|item| parse2::<TaskDef>(item.to_token_stream()))
.collect::<Result<_>>()?
} else {
Vec::new()
};
let mut task_indices = HashSet::<LitInt>::new();
for task in tasks.iter() {
let task_index = &task.index_attr.meta.index;
if !task_indices.insert(task_index.clone()) {
return Err(Error::new(
task_index.span(),
format!("duplicate task index `{}`", task_index),
))
}
}
let mut item_impl = item_impl;
item_impl.attrs = normal_attrs;
// we require the path on the impl to be a TypePath
let enum_path = parse2::<TypePath>(item_impl.self_ty.to_token_stream())?;
let segments = enum_path.path.segments.iter().collect::<Vec<_>>();
let (Some(last_seg), None) = (segments.get(0), segments.get(1)) else {
return Err(Error::new(
enum_path.span(),
"if specified manually, the task enum must be defined locally in this \
pallet and cannot be a re-export",
))
};
let enum_ident = last_seg.ident.clone();
let enum_arguments = last_seg.arguments.clone();
// We do this here because it would be improper to do something fallible like this at
// the expansion phase. Fallible stuff should happen during parsing.
let scrate = generate_access_from_frame_or_crate("frame-support")?;
Ok(TasksDef { tasks_attr, item_impl, tasks, scrate, enum_ident, enum_arguments })
}
}
/// Parsing for a `#[pallet::tasks_experimental]` attr.
pub type PalletTasksAttr = PalletTaskAttr<keywords::tasks_experimental>;
/// Parsing for any of the attributes that can be used within a `#[pallet::tasks_experimental]`
/// [`ItemImpl`].
pub type TaskAttr = PalletTaskAttr<TaskAttrMeta>;
/// Parsing for a `#[pallet::task_index]` attr.
pub type TaskIndexAttr = PalletTaskAttr<TaskIndexAttrMeta>;
/// Parsing for a `#[pallet::task_condition]` attr.
pub type TaskConditionAttr = PalletTaskAttr<TaskConditionAttrMeta>;
/// Parsing for a `#[pallet::task_list]` attr.
pub type TaskListAttr = PalletTaskAttr<TaskListAttrMeta>;
/// Parsing for a `#[pallet::task_weight]` attr.
pub type TaskWeightAttr = PalletTaskAttr<TaskWeightAttrMeta>;
/// Parsing for a `#[pallet:task_enum]` attr.
pub type PalletTaskEnumAttr = PalletTaskAttr<keywords::task_enum>;
/// Parsing for a manually-specified (or auto-generated) task enum, optionally including the
/// attached `#[pallet::task_enum]` attribute.
#[derive(Clone, Debug)]
pub struct TaskEnumDef {
pub attr: Option<PalletTaskEnumAttr>,
pub item_enum: ItemEnum,
pub scrate: Path,
pub type_use_generics: TokenStream2,
}
impl syn::parse::Parse for TaskEnumDef {
fn parse(input: ParseStream) -> Result<Self> {
let mut item_enum = input.parse::<ItemEnum>()?;
let attr = extract_pallet_attr(&mut item_enum)?;
let attr = match attr {
Some(attr) => Some(parse2(attr)?),
None => None,
};
// We do this here because it would be improper to do something fallible like this at
// the expansion phase. Fallible stuff should happen during parsing.
let scrate = generate_access_from_frame_or_crate("frame-support")?;
let type_use_generics = quote!(T);
Ok(TaskEnumDef { attr, item_enum, scrate, type_use_generics })
}
}
/// Represents an individual tasks within a [`TasksDef`].
#[derive(Debug, Clone)]
pub struct TaskDef {
pub index_attr: TaskIndexAttr,
pub condition_attr: TaskConditionAttr,
pub list_attr: TaskListAttr,
pub weight_attr: TaskWeightAttr,
pub normal_attrs: Vec<Attribute>,
pub item: ImplItemFn,
pub arg_names: Vec<Ident>,
}
impl syn::parse::Parse for TaskDef {
fn parse(input: ParseStream) -> Result<Self> {
let item = input.parse::<ImplItemFn>()?;
// we only want to activate TaskAttrType parsing errors for tasks-related attributes,
// so we filter them here
let (task_attrs, normal_attrs) = partition_task_attrs(&item);
let task_attrs: Vec<TaskAttr> = task_attrs
.into_iter()
.map(|attr| parse2(attr.to_token_stream()))
.collect::<Result<_>>()?;
let Some(index_attr) = task_attrs
.iter()
.find(|attr| matches!(attr.meta, TaskAttrMeta::TaskIndex(_)))
.cloned()
else {
return Err(Error::new(
item.sig.ident.span(),
"missing `#[pallet::task_index(..)]` attribute",
))
};
let Some(condition_attr) = task_attrs
.iter()
.find(|attr| matches!(attr.meta, TaskAttrMeta::TaskCondition(_)))
.cloned()
else {
return Err(Error::new(
item.sig.ident.span(),
"missing `#[pallet::task_condition(..)]` attribute",
))
};
let Some(list_attr) = task_attrs
.iter()
.find(|attr| matches!(attr.meta, TaskAttrMeta::TaskList(_)))
.cloned()
else {
return Err(Error::new(
item.sig.ident.span(),
"missing `#[pallet::task_list(..)]` attribute",
))
};
let Some(weight_attr) = task_attrs
.iter()
.find(|attr| matches!(attr.meta, TaskAttrMeta::TaskWeight(_)))
.cloned()
else {
return Err(Error::new(
item.sig.ident.span(),
"missing `#[pallet::task_weight(..)]` attribute",
))
};
if let Some(duplicate) = task_attrs
.iter()
.filter(|attr| matches!(attr.meta, TaskAttrMeta::TaskCondition(_)))
.collect::<Vec<_>>()
.get(1)
{
return Err(Error::new(
duplicate.span(),
"unexpected extra `#[pallet::task_condition(..)]` attribute",
))
}
if let Some(duplicate) = task_attrs
.iter()
.filter(|attr| matches!(attr.meta, TaskAttrMeta::TaskList(_)))
.collect::<Vec<_>>()
.get(1)
{
return Err(Error::new(
duplicate.span(),
"unexpected extra `#[pallet::task_list(..)]` attribute",
))
}
if let Some(duplicate) = task_attrs
.iter()
.filter(|attr| matches!(attr.meta, TaskAttrMeta::TaskIndex(_)))
.collect::<Vec<_>>()
.get(1)
{
return Err(Error::new(
duplicate.span(),
"unexpected extra `#[pallet::task_index(..)]` attribute",
))
}
let mut arg_names = vec![];
for input in item.sig.inputs.iter() {
match input {
syn::FnArg::Typed(pat_type) => match &*pat_type.pat {
syn::Pat::Ident(ident) => arg_names.push(ident.ident.clone()),
_ => return Err(Error::new(input.span(), "unexpected pattern type")),
},
_ => return Err(Error::new(input.span(), "unexpected function argument type")),
}
}
let index_attr = index_attr.try_into().expect("we check the type above; QED");
let condition_attr = condition_attr.try_into().expect("we check the type above; QED");
let list_attr = list_attr.try_into().expect("we check the type above; QED");
let weight_attr = weight_attr.try_into().expect("we check the type above; QED");
Ok(TaskDef {
index_attr,
condition_attr,
list_attr,
weight_attr,
normal_attrs,
item,
arg_names,
})
}
}
/// The contents of a [`TasksDef`]-related attribute.
#[derive(Parse, Debug, Clone)]
pub enum TaskAttrMeta {
#[peek(keywords::task_list, name = "#[pallet::task_list(..)]")]
TaskList(TaskListAttrMeta),
#[peek(keywords::task_index, name = "#[pallet::task_index(..)")]
TaskIndex(TaskIndexAttrMeta),
#[peek(keywords::task_condition, name = "#[pallet::task_condition(..)")]
TaskCondition(TaskConditionAttrMeta),
#[peek(keywords::task_weight, name = "#[pallet::task_weight(..)")]
TaskWeight(TaskWeightAttrMeta),
}
/// The contents of a `#[pallet::task_list]` attribute.
#[derive(Parse, Debug, Clone)]
pub struct TaskListAttrMeta {
pub task_list: keywords::task_list,
#[paren]
_paren: Paren,
#[inside(_paren)]
pub expr: Expr,
}
/// The contents of a `#[pallet::task_index]` attribute.
#[derive(Parse, Debug, Clone)]
pub struct TaskIndexAttrMeta {
pub task_index: keywords::task_index,
#[paren]
_paren: Paren,
#[inside(_paren)]
pub index: LitInt,
}
/// The contents of a `#[pallet::task_condition]` attribute.
#[derive(Parse, Debug, Clone)]
pub struct TaskConditionAttrMeta {
pub task_condition: keywords::task_condition,
#[paren]
_paren: Paren,
#[inside(_paren)]
pub expr: Expr,
}
/// The contents of a `#[pallet::task_weight]` attribute.
#[derive(Parse, Debug, Clone)]
pub struct TaskWeightAttrMeta {
pub task_weight: keywords::task_weight,
#[paren]
_paren: Paren,
#[inside(_paren)]
pub expr: Expr,
}
/// The contents of a `#[pallet::task]` attribute.
#[derive(Parse, Debug, Clone)]
pub struct PalletTaskAttr<T: syn::parse::Parse + core::fmt::Debug + ToTokens> {
pub pound: Pound,
#[bracket]
_bracket: Bracket,
#[inside(_bracket)]
pub pallet: keywords::pallet,
#[inside(_bracket)]
pub colons: PathSep,
#[inside(_bracket)]
pub meta: T,
}
impl ToTokens for TaskListAttrMeta {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let task_list = self.task_list;
let expr = &self.expr;
tokens.extend(quote!(#task_list(#expr)));
}
}
impl ToTokens for TaskConditionAttrMeta {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let task_condition = self.task_condition;
let expr = &self.expr;
tokens.extend(quote!(#task_condition(#expr)));
}
}
impl ToTokens for TaskWeightAttrMeta {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let task_weight = self.task_weight;
let expr = &self.expr;
tokens.extend(quote!(#task_weight(#expr)));
}
}
impl ToTokens for TaskIndexAttrMeta {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let task_index = self.task_index;
let index = &self.index;
tokens.extend(quote!(#task_index(#index)))
}
}
impl ToTokens for TaskAttrMeta {
fn to_tokens(&self, tokens: &mut TokenStream2) {
match self {
TaskAttrMeta::TaskList(list) => tokens.extend(list.to_token_stream()),
TaskAttrMeta::TaskIndex(index) => tokens.extend(index.to_token_stream()),
TaskAttrMeta::TaskCondition(condition) => tokens.extend(condition.to_token_stream()),
TaskAttrMeta::TaskWeight(weight) => tokens.extend(weight.to_token_stream()),
}
}
}
impl<T: syn::parse::Parse + core::fmt::Debug + ToTokens> ToTokens for PalletTaskAttr<T> {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let pound = self.pound;
let pallet = self.pallet;
let colons = self.colons;
let meta = &self.meta;
tokens.extend(quote!(#pound[#pallet #colons #meta]));
}
}
impl TryFrom<PalletTaskAttr<TaskAttrMeta>> for TaskIndexAttr {
type Error = syn::Error;
fn try_from(value: PalletTaskAttr<TaskAttrMeta>) -> Result<Self> {
let pound = value.pound;
let pallet = value.pallet;
let colons = value.colons;
match value.meta {
TaskAttrMeta::TaskIndex(meta) => parse2(quote!(#pound[#pallet #colons #meta])),
_ =>
return Err(Error::new(
value.span(),
format!("`{:?}` cannot be converted to a `TaskIndexAttr`", value.meta),
)),
}
}
}
impl TryFrom<PalletTaskAttr<TaskAttrMeta>> for TaskConditionAttr {
type Error = syn::Error;
fn try_from(value: PalletTaskAttr<TaskAttrMeta>) -> Result<Self> {
let pound = value.pound;
let pallet = value.pallet;
let colons = value.colons;
match value.meta {
TaskAttrMeta::TaskCondition(meta) => parse2(quote!(#pound[#pallet #colons #meta])),
_ =>
return Err(Error::new(
value.span(),
format!("`{:?}` cannot be converted to a `TaskConditionAttr`", value.meta),
)),
}
}
}
impl TryFrom<PalletTaskAttr<TaskAttrMeta>> for TaskWeightAttr {
type Error = syn::Error;
fn try_from(value: PalletTaskAttr<TaskAttrMeta>) -> Result<Self> {
let pound = value.pound;
let pallet = value.pallet;
let colons = value.colons;
match value.meta {
TaskAttrMeta::TaskWeight(meta) => parse2(quote!(#pound[#pallet #colons #meta])),
_ =>
return Err(Error::new(
value.span(),
format!("`{:?}` cannot be converted to a `TaskWeightAttr`", value.meta),
)),
}
}
}
impl TryFrom<PalletTaskAttr<TaskAttrMeta>> for TaskListAttr {
type Error = syn::Error;
fn try_from(value: PalletTaskAttr<TaskAttrMeta>) -> Result<Self> {
let pound = value.pound;
let pallet = value.pallet;
let colons = value.colons;
match value.meta {
TaskAttrMeta::TaskList(meta) => parse2(quote!(#pound[#pallet #colons #meta])),
_ =>
return Err(Error::new(
value.span(),
format!("`{:?}` cannot be converted to a `TaskListAttr`", value.meta),
)),
}
}
}
fn extract_pallet_attr(item_enum: &mut ItemEnum) -> Result<Option<TokenStream2>> {
let mut duplicate = None;
let mut attr = None;
item_enum.attrs = item_enum
.attrs
.iter()
.filter(|found_attr| {
let segs = found_attr
.path()
.segments
.iter()
.map(|seg| seg.ident.clone())
.collect::<Vec<_>>();
let (Some(seg1), Some(_), None) = (segs.get(0), segs.get(1), segs.get(2)) else {
return true
};
if seg1 != "pallet" {
return true
}
if attr.is_some() {
duplicate = Some(found_attr.span());
}
attr = Some(found_attr.to_token_stream());
false
})
.cloned()
.collect();
if let Some(span) = duplicate {
return Err(Error::new(span, "only one `#[pallet::_]` attribute is supported on this item"))
}
Ok(attr)
}
fn partition_tasks_attrs(item_impl: &ItemImpl) -> (Vec<syn::Attribute>, Vec<syn::Attribute>) {
item_impl.attrs.clone().into_iter().partition(|attr| {
let mut path_segs = attr.path().segments.iter();
let (Some(prefix), Some(suffix), None) =
(path_segs.next(), path_segs.next(), path_segs.next())
else {
return false
};
prefix.ident == "pallet" && suffix.ident == "tasks_experimental"
})
}
fn partition_task_attrs(item: &ImplItemFn) -> (Vec<syn::Attribute>, Vec<syn::Attribute>) {
item.attrs.clone().into_iter().partition(|attr| {
let mut path_segs = attr.path().segments.iter();
let (Some(prefix), Some(suffix)) = (path_segs.next(), path_segs.next()) else {
return false
};
// N.B: the `PartialEq` impl between `Ident` and `&str` is more efficient than
// parsing and makes no stack or heap allocations
prefix.ident == "pallet" &&
(suffix.ident == "tasks_experimental" ||
suffix.ident == "task_list" ||
suffix.ident == "task_condition" ||
suffix.ident == "task_weight" ||
suffix.ident == "task_index")
})
}
#[test]
fn test_parse_task_list_() {
parse2::<TaskAttr>(quote!(#[pallet::task_list(Something::iter())])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_list(Numbers::<T, I>::iter_keys())])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_list(iter())])).unwrap();
assert_parse_error_matches!(
parse2::<TaskAttr>(quote!(#[pallet::task_list()])),
"expected an expression"
);
assert_parse_error_matches!(
parse2::<TaskAttr>(quote!(#[pallet::task_list])),
"expected parentheses"
);
}
#[test]
fn test_parse_task_index() {
parse2::<TaskAttr>(quote!(#[pallet::task_index(3)])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_index(0)])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_index(17)])).unwrap();
assert_parse_error_matches!(
parse2::<TaskAttr>(quote!(#[pallet::task_index])),
"expected parentheses"
);
assert_parse_error_matches!(
parse2::<TaskAttr>(quote!(#[pallet::task_index("hey")])),
"expected integer literal"
);
assert_parse_error_matches!(
parse2::<TaskAttr>(quote!(#[pallet::task_index(0.3)])),
"expected integer literal"
);
}
#[test]
fn test_parse_task_condition() {
parse2::<TaskAttr>(quote!(#[pallet::task_condition(|x| x.is_some())])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_condition(|_x| some_expr())])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_condition(|| some_expr())])).unwrap();
parse2::<TaskAttr>(quote!(#[pallet::task_condition(some_expr())])).unwrap();
}
#[test]
fn test_parse_tasks_attr() {
parse2::<PalletTasksAttr>(quote!(#[pallet::tasks_experimental])).unwrap();
assert_parse_error_matches!(
parse2::<PalletTasksAttr>(quote!(#[pallet::taskss])),
"expected `tasks_experimental`"
);
assert_parse_error_matches!(
parse2::<PalletTasksAttr>(quote!(#[pallet::tasks_])),
"expected `tasks_experimental`"
);
assert_parse_error_matches!(
parse2::<PalletTasksAttr>(quote!(#[pal::tasks])),
"expected `pallet`"
);
assert_parse_error_matches!(
parse2::<PalletTasksAttr>(quote!(#[pallet::tasks_experimental()])),
"unexpected token"
);
}
#[test]
fn test_parse_tasks_def_basic() {
simulate_manifest_dir("../../examples/basic", || {
let parsed = parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Add a pair of numbers into the totals and remove them.
#[pallet::task_list(Numbers::<T, I>::iter_keys())]
#[pallet::task_condition(|i| Numbers::<T, I>::contains_key(i))]
#[pallet::task_index(0)]
#[pallet::task_weight(0)]
pub fn add_number_into_total(i: u32) -> DispatchResult {
let v = Numbers::<T, I>::take(i).ok_or(Error::<T, I>::NotFound)?;
Total::<T, I>::mutate(|(total_keys, total_values)| {
*total_keys += i;
*total_values += v;
});
Ok(())
}
}
})
.unwrap();
assert_eq!(parsed.tasks.len(), 1);
});
}
#[test]
fn test_parse_tasks_def_basic_increment_decrement() {
simulate_manifest_dir("../../examples/basic", || {
let parsed = parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// Get the value and check if it can be incremented
#[pallet::task_index(0)]
#[pallet::task_condition(|| {
let value = Value::<T>::get().unwrap();
value < 255
})]
#[pallet::task_list(Vec::<Task<T>>::new())]
#[pallet::task_weight(0)]
fn increment() -> DispatchResult {
let value = Value::<T>::get().unwrap_or_default();
if value >= 255 {
Err(Error::<T>::ValueOverflow.into())
} else {
let new_val = value.checked_add(1).ok_or(Error::<T>::ValueOverflow)?;
Value::<T>::put(new_val);
Pallet::<T>::deposit_event(Event::Incremented { new_val });
Ok(())
}
}
// Get the value and check if it can be decremented
#[pallet::task_index(1)]
#[pallet::task_condition(|| {
let value = Value::<T>::get().unwrap();
value > 0
})]
#[pallet::task_list(Vec::<Task<T>>::new())]
#[pallet::task_weight(0)]
fn decrement() -> DispatchResult {
let value = Value::<T>::get().unwrap_or_default();
if value == 0 {
Err(Error::<T>::ValueUnderflow.into())
} else {
let new_val = value.checked_sub(1).ok_or(Error::<T>::ValueUnderflow)?;
Value::<T>::put(new_val);
Pallet::<T>::deposit_event(Event::Decremented { new_val });
Ok(())
}
}
}
})
.unwrap();
assert_eq!(parsed.tasks.len(), 2);
});
}
#[test]
fn test_parse_tasks_def_duplicate_index() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_list(Something::iter())]
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_index(0)]
#[pallet::task_weight(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
#[pallet::task_list(Numbers::<T, I>::iter_keys())]
#[pallet::task_condition(|i| Numbers::<T, I>::contains_key(i))]
#[pallet::task_index(0)]
#[pallet::task_weight(0)]
pub fn bar(i: u32) -> DispatchResult {
Ok(())
}
}
}),
"duplicate task index `0`"
);
});
}
#[test]
fn test_parse_tasks_def_missing_task_list() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_index(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"missing `#\[pallet::task_list\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_missing_task_condition() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_list(Something::iter())]
#[pallet::task_index(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"missing `#\[pallet::task_condition\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_missing_task_index() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_list(Something::iter())]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"missing `#\[pallet::task_index\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_missing_task_weight() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_list(Something::iter())]
#[pallet::task_index(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"missing `#\[pallet::task_weight\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_unexpected_extra_task_list_attr() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_index(0)]
#[pallet::task_weight(0)]
#[pallet::task_list(Something::iter())]
#[pallet::task_list(SomethingElse::iter())]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"unexpected extra `#\[pallet::task_list\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_unexpected_extra_task_condition_attr() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_condition(|i| i % 4 == 0)]
#[pallet::task_index(0)]
#[pallet::task_list(Something::iter())]
#[pallet::task_weight(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"unexpected extra `#\[pallet::task_condition\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_unexpected_extra_task_index_attr() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_condition(|i| i % 2 == 0)]
#[pallet::task_index(0)]
#[pallet::task_index(0)]
#[pallet::task_list(Something::iter())]
#[pallet::task_weight(0)]
pub fn foo(i: u32) -> DispatchResult {
Ok(())
}
}
}),
r"unexpected extra `#\[pallet::task_index\(\.\.\)\]`"
);
});
}
#[test]
fn test_parse_tasks_def_extra_tasks_attribute() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TasksDef>(quote! {
#[pallet::tasks_experimental]
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {}
}),
r"unexpected extra `#\[pallet::tasks_experimental\]` attribute"
);
});
}
#[test]
fn test_parse_task_enum_def_basic() {
simulate_manifest_dir("../../examples/basic", || {
parse2::<TaskEnumDef>(quote! {
#[pallet::task_enum]
pub enum Task<T: Config> {
Increment,
Decrement,
}
})
.unwrap();
});
}
#[test]
fn test_parse_task_enum_def_non_task_name() {
simulate_manifest_dir("../../examples/basic", || {
parse2::<TaskEnumDef>(quote! {
#[pallet::task_enum]
pub enum Something {
Foo
}
})
.unwrap();
});
}
#[test]
fn test_parse_task_enum_def_missing_attr_allowed() {
simulate_manifest_dir("../../examples/basic", || {
parse2::<TaskEnumDef>(quote! {
pub enum Task<T: Config> {
Increment,
Decrement,
}
})
.unwrap();
});
}
#[test]
fn test_parse_task_enum_def_missing_attr_alternate_name_allowed() {
simulate_manifest_dir("../../examples/basic", || {
parse2::<TaskEnumDef>(quote! {
pub enum Foo {
Red,
}
})
.unwrap();
});
}
#[test]
fn test_parse_task_enum_def_wrong_attr() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TaskEnumDef>(quote! {
#[pallet::something]
pub enum Task<T: Config> {
Increment,
Decrement,
}
}),
"expected `task_enum`"
);
});
}
#[test]
fn test_parse_task_enum_def_wrong_item() {
simulate_manifest_dir("../../examples/basic", || {
assert_parse_error_matches!(
parse2::<TaskEnumDef>(quote! {
#[pallet::task_enum]
pub struct Something;
}),
"expected `enum`"
);
});
}
@@ -0,0 +1,264 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{panic, sync::Mutex};
use syn::parse_quote;
#[doc(hidden)]
pub mod __private {
pub use regex;
}
/// Allows you to assert that the input expression resolves to an error whose string
/// representation matches the specified regex literal.
///
/// ## Example:
///
/// ```
/// use super::tasks::*;
///
/// assert_parse_error_matches!(
/// parse2::<TaskEnumDef>(quote! {
/// #[pallet::task_enum]
/// pub struct Something;
/// }),
/// "expected `enum`"
/// );
/// ```
///
/// More complex regular expressions are also possible (anything that could pass as a regex for
/// use with the [`regex`] crate.):
///
/// ```ignore
/// assert_parse_error_matches!(
/// parse2::<TasksDef>(quote! {
/// #[pallet::tasks_experimental]
/// impl<T: Config<I>, I: 'static> Pallet<T, I> {
/// #[pallet::task_condition(|i| i % 2 == 0)]
/// #[pallet::task_index(0)]
/// pub fn foo(i: u32) -> DispatchResult {
/// Ok(())
/// }
/// }
/// }),
/// r"missing `#\[pallet::task_list\(\.\.\)\]`"
/// );
/// ```
///
/// Although this is primarily intended to be used with parsing errors, this macro is general
/// enough that it will work with any error with a reasonable [`core::fmt::Display`] impl.
#[macro_export]
macro_rules! assert_parse_error_matches {
($expr:expr, $reg:literal) => {
match $expr {
Ok(_) => panic!("Expected an `Error(..)`, but got Ok(..)"),
Err(e) => {
let error_message = e.to_string();
let re = $crate::pallet::parse::tests::__private::regex::Regex::new($reg)
.expect("Invalid regex pattern");
assert!(
re.is_match(&error_message),
"Error message \"{}\" does not match the pattern \"{}\"",
error_message,
$reg
);
},
}
};
}
/// Allows you to assert that an entire pallet parses successfully. A custom syntax is used for
/// specifying arguments so please pay attention to the docs below.
///
/// The general syntax is:
///
/// ```ignore
/// assert_pallet_parses! {
/// #[manifest_dir("../../examples/basic")]
/// #[frame_support::pallet]
/// pub mod pallet {
/// #[pallet::config]
/// pub trait Config: frame_system::Config {}
///
/// #[pallet::pallet]
/// pub struct Pallet<T>(_);
/// }
/// };
/// ```
///
/// The `#[manifest_dir(..)]` attribute _must_ be specified as the _first_ attribute on the
/// pallet module, and should reference the relative (to your current directory) path of a
/// directory containing containing the `Cargo.toml` of a valid pallet. Typically you will only
/// ever need to use the `examples/basic` pallet, but sometimes it might be advantageous to
/// specify a different one that has additional dependencies.
///
/// The reason this must be specified is that our underlying parsing of pallets depends on
/// reaching out into the file system to look for particular `Cargo.toml` dependencies via the
/// [`generate_access_from_frame_or_crate`] method, so to simulate this properly in a proc
/// macro crate, we need to temporarily convince this function that we are running from the
/// directory of a valid pallet.
#[macro_export]
macro_rules! assert_pallet_parses {
(
#[manifest_dir($manifest_dir:literal)]
$($tokens:tt)*
) => {
{
let mut pallet: Option<$crate::pallet::parse::Def> = None;
$crate::pallet::parse::tests::simulate_manifest_dir($manifest_dir, core::panic::AssertUnwindSafe(|| {
pallet = Some($crate::pallet::parse::Def::try_from(syn::parse_quote! {
$($tokens)*
}, false).unwrap());
}));
pallet.unwrap()
}
}
}
/// Similar to [`assert_pallet_parses`], except this instead expects the pallet not to parse,
/// and allows you to specify a regex matching the expected parse error.
///
/// This is identical syntactically to [`assert_pallet_parses`] in every way except there is a
/// second attribute that must be specified immediately after `#[manifest_dir(..)]` which is
/// `#[error_regex(..)]` which should contain a string/regex literal designed to match what you
/// consider to be the correct parsing error we should see when we try to parse this particular
/// pallet.
///
/// ## Example:
///
/// ```
/// assert_pallet_parse_error! {
/// #[manifest_dir("../../examples/basic")]
/// #[error_regex("Missing `\\#\\[pallet::pallet\\]`")]
/// #[frame_support::pallet]
/// pub mod pallet {
/// #[pallet::config]
/// pub trait Config: frame_system::Config {}
/// }
/// }
/// ```
#[macro_export]
macro_rules! assert_pallet_parse_error {
(
#[manifest_dir($manifest_dir:literal)]
#[error_regex($reg:literal)]
$($tokens:tt)*
) => {
$crate::pallet::parse::tests::simulate_manifest_dir($manifest_dir, || {
$crate::assert_parse_error_matches!(
$crate::pallet::parse::Def::try_from(
parse_quote! {
$($tokens)*
},
false
),
$reg
);
});
}
}
/// Safely runs the specified `closure` while simulating an alternative `CARGO_MANIFEST_DIR`,
/// restoring `CARGO_MANIFEST_DIR` to its original value upon completion regardless of whether
/// the closure panics.
///
/// This is useful in tests of `Def::try_from` and other pallet-related methods that internally
/// make use of [`generate_access_from_frame_or_crate`], which is sensitive to entries in the
/// "current" `Cargo.toml` files.
///
/// This function uses a [`Mutex`] to avoid a race condition created when multiple tests try to
/// modify and then restore the `CARGO_MANIFEST_DIR` ENV var in an overlapping way.
pub fn simulate_manifest_dir<P: AsRef<std::path::Path>, F: FnOnce() + std::panic::UnwindSafe>(
path: P,
closure: F,
) {
use std::{env::*, path::*};
/// Ensures that only one thread can modify/restore the `CARGO_MANIFEST_DIR` ENV var at a time,
/// avoiding a race condition because `cargo test` runs tests in parallel.
///
/// Although this forces all tests that use [`simulate_manifest_dir`] to run sequentially with
/// respect to each other, this is still several orders of magnitude faster than using UI
/// tests, even if they are run in parallel.
static MANIFEST_DIR_LOCK: Mutex<()> = Mutex::new(());
// avoid race condition when swapping out `CARGO_MANIFEST_DIR`
let guard = MANIFEST_DIR_LOCK.lock().unwrap();
// obtain the current/original `CARGO_MANIFEST_DIR`
let orig = PathBuf::from(
var("CARGO_MANIFEST_DIR").expect("failed to read ENV var `CARGO_MANIFEST_DIR`"),
);
// set `CARGO_MANIFEST_DIR` to the provided path, relative to current working dir
set_var("CARGO_MANIFEST_DIR", orig.join(path.as_ref()));
// safely run closure catching any panics
let result = panic::catch_unwind(closure);
// restore original `CARGO_MANIFEST_DIR` before unwinding
set_var("CARGO_MANIFEST_DIR", &orig);
// unlock the mutex so we don't poison it if there is a panic
drop(guard);
// unwind any panics originally encountered when running closure
result.unwrap();
}
mod tasks;
#[test]
fn test_parse_minimal_pallet() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}
#[test]
fn test_parse_pallet_missing_pallet() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex("Missing `\\#\\[pallet::pallet\\]`")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::config]
pub trait Config: frame_system::Config {}
}
}
}
#[test]
fn test_parse_pallet_missing_config() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex("Missing `\\#\\[pallet::config\\]`")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::pallet]
pub struct Pallet<T>(_);
}
}
}
@@ -0,0 +1,240 @@
// This file is part of Substrate.
// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use syn::parse_quote;
#[test]
fn test_parse_pallet_with_task_enum_missing_impl() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex("Missing `\\#\\[pallet::tasks_experimental\\]` impl")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::task_enum]
pub enum Task<T: Config> {
Something,
}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
}
}
#[test]
fn test_parse_pallet_with_task_enum_wrong_attribute() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex("expected one of")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::wrong_attribute]
pub enum Task<T: Config> {
Something,
}
#[pallet::task_list]
impl<T: Config> frame_support::traits::Task for Task<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
}
}
#[test]
fn test_parse_pallet_missing_task_enum() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::tasks_experimental]
#[cfg(test)] // aha, this means it's being eaten
impl<T: Config> frame_support::traits::Task for Task<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}
#[test]
fn test_parse_pallet_task_list_in_wrong_place() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex("can only be used on items within an `impl` statement.")]
#[frame_support::pallet]
pub mod pallet {
pub enum MyCustomTaskEnum<T: Config> {
Something,
}
#[pallet::task_list]
pub fn something() {
println!("hey");
}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
}
}
#[test]
fn test_parse_pallet_manual_tasks_impl_without_manual_tasks_enum() {
assert_pallet_parse_error! {
#[manifest_dir("../../examples/basic")]
#[error_regex(".*attribute must be attached to your.*")]
#[frame_support::pallet]
pub mod pallet {
impl<T: Config> frame_support::traits::Task for Task<T>
where
T: TypeInfo,
{
type Enumeration = sp_std::vec::IntoIter<Task<T>>;
fn iter() -> Self::Enumeration {
sp_std::vec![Task::increment, Task::decrement].into_iter()
}
}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
}
}
#[test]
fn test_parse_pallet_manual_task_enum_non_manual_impl() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
pub enum MyCustomTaskEnum<T: Config> {
Something,
}
#[pallet::tasks_experimental]
impl<T: Config> frame_support::traits::Task for MyCustomTaskEnum<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}
#[test]
fn test_parse_pallet_non_manual_task_enum_manual_impl() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
#[pallet::task_enum]
pub enum MyCustomTaskEnum<T: Config> {
Something,
}
impl<T: Config> frame_support::traits::Task for MyCustomTaskEnum<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}
#[test]
fn test_parse_pallet_manual_task_enum_manual_impl() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
pub enum MyCustomTaskEnum<T: Config> {
Something,
}
impl<T: Config> frame_support::traits::Task for MyCustomTaskEnum<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}
#[test]
fn test_parse_pallet_manual_task_enum_mismatch_ident() {
assert_pallet_parses! {
#[manifest_dir("../../examples/basic")]
#[frame_support::pallet]
pub mod pallet {
pub enum WrongIdent<T: Config> {
Something,
}
#[pallet::tasks_experimental]
impl<T: Config> frame_support::traits::Task for MyCustomTaskEnum<T>
where
T: TypeInfo,
{}
#[pallet::config]
pub trait Config: frame_system::Config {}
#[pallet::pallet]
pub struct Pallet<T>(_);
}
};
}