diff --git a/Cargo.lock b/Cargo.lock index ed66717cbe..ce4063f100 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,9 +184,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abf770dad29577cd3580f3dd09005799224a912b8cdfdd6dc04d030d42b3df4e" +checksum = "805f7a974de5804f5c053edc6ca43b20883bdd3a733b3691200ae3a4b454a2db" dependencies = [ "num_enum", "strum 0.26.3", @@ -1685,12 +1685,12 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "8f43644eed690f5374f1af436ecd6aea01cd201f6fbdf0178adaf6907afb2cec" dependencies = [ "async-trait", - "axum-core 0.4.3", + "axum-core 0.4.4", "bytes", "futures-util", "http 1.1.0", @@ -1711,7 +1711,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower 0.4.13", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -1736,9 +1736,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "5e6b8ba012a258d63c9adfa28b9ddcf66149da6f986c5b5452e629d5ee64bf00" dependencies = [ "async-trait", "bytes", @@ -1749,7 +1749,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -1903,7 +1903,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.12.1", "lazy_static", "lazycell", "log", @@ -3356,9 +3356,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -3376,9 +3376,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -3388,18 +3388,18 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3" +checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" dependencies = [ "clap", ] [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -6795,7 +6795,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -7816,9 +7816,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -8177,7 +8177,7 @@ dependencies = [ "assert_fs", "chrono", "katana-node-bindings", - "runner-macro", + "katana-runner-macro", "serde", "serde_json", "starknet 0.12.0", @@ -8185,6 +8185,16 @@ dependencies = [ "url", ] +[[package]] +name = "katana-runner-macro" +version = "1.0.0-alpha.12" +dependencies = [ + "proc-macro2", + "quote", + "starknet 0.12.0", + "syn 2.0.77", +] + [[package]] name = "katana-slot-controller" version = "1.0.0-alpha.12" @@ -8365,7 +8375,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -10464,9 +10474,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" [[package]] name = "postcard" @@ -10824,8 +10834,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" dependencies = [ "bytes", - "heck 0.4.1", - "itertools 0.11.0", + "heck 0.5.0", + "itertools 0.12.1", "log", "multimap 0.10.0", "once_cell", @@ -10858,7 +10868,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", - "itertools 0.11.0", + "itertools 0.12.1", "proc-macro2", "quote", "syn 2.0.77", @@ -11745,14 +11755,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" -[[package]] -name = "runner-macro" -version = "1.0.0-alpha.12" -dependencies = [ - "quote", - "syn 2.0.77", -] - [[package]] name = "rust_decimal" version = "1.36.0" @@ -12440,9 +12442,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -12906,7 +12908,7 @@ source = "git+https://github.com/cartridge-gg/slot?rev=630ed37#630ed377d55662847 dependencies = [ "account_sdk", "anyhow", - "axum 0.7.5", + "axum 0.7.6", "base64 0.22.1", "dirs 5.0.1", "graphql_client", @@ -15056,8 +15058,10 @@ dependencies = [ "futures-util", "pin-project-lite", "sync_wrapper 0.1.2", + "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -15444,9 +15448,9 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -16169,7 +16173,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 1cbf31f5d2..4b4fdf930a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ members = [ "crates/katana/rpc/rpc-types", "crates/katana/rpc/rpc-types-builder", "crates/katana/runner", - "crates/katana/runner/runner-macro", + "crates/katana/runner/macro", "crates/katana/storage/codecs", "crates/katana/storage/codecs/derive", "crates/katana/storage/db", @@ -86,6 +86,7 @@ katana-core = { path = "crates/katana/core", default-features = false } katana-db = { path = "crates/katana/storage/db" } katana-executor = { path = "crates/katana/executor" } katana-node = { path = "crates/katana/node", default-features = false } +katana-node-bindings = { path = "crates/katana/node-bindings" } katana-pool = { path = "crates/katana/pool" } katana-primitives = { path = "crates/katana/primitives" } katana-provider = { path = "crates/katana/storage/provider" } diff --git a/bin/sozo/src/commands/options/account/mod.rs b/bin/sozo/src/commands/options/account/mod.rs index 9f73d74d8a..89231243b5 100644 --- a/bin/sozo/src/commands/options/account/mod.rs +++ b/bin/sozo/src/commands/options/account/mod.rs @@ -151,6 +151,7 @@ impl AccountOptions { #[cfg(test)] mod tests { use clap::Parser; + use katana_runner::RunnerCtx; use starknet::accounts::ExecutionEncoder; use starknet::core::types::Call; use starknet_crypto::Felt; @@ -211,8 +212,9 @@ mod tests { assert!(cmd.account.account_address(None).is_err()); } - #[katana_runner::katana_test(2, true)] - async fn legacy_flag_works_as_expected() { + #[tokio::test] + #[katana_runner::test(accounts = 2, fee = false)] + async fn legacy_flag_works_as_expected(runner: &RunnerCtx) { let cmd = Command::parse_from([ "sozo", "--legacy", @@ -235,8 +237,9 @@ mod tests { assert!(*result.get(3).unwrap() == Felt::from_hex("0x0").unwrap()); } - #[katana_runner::katana_test(2, true)] - async fn without_legacy_flag_works_as_expected() { + #[tokio::test] + #[katana_runner::test(accounts = 2, fee = false)] + async fn without_legacy_flag_works_as_expected(runner: &RunnerCtx) { let cmd = Command::parse_from(["sozo", "--account-address", "0x0", "--private-key", "0x1"]); let dummy_call = vec![Call { to: Felt::from_hex("0x0").unwrap(), diff --git a/bin/sozo/tests/test_account.rs b/bin/sozo/tests/test_account.rs index 0d42870286..1ed467e9b6 100644 --- a/bin/sozo/tests/test_account.rs +++ b/bin/sozo/tests/test_account.rs @@ -3,6 +3,7 @@ mod utils; use std::fs; use assert_fs::fixture::PathChild; +use katana_runner::RunnerCtx; use sozo_ops::account; use starknet::accounts::Account; use utils::snapbox::get_snapbox; @@ -29,8 +30,9 @@ fn test_account_new() { assert!(pt.child("account.json").exists()); } -#[katana_runner::katana_test(1, true)] -async fn test_account_fetch() { +#[tokio::test] +#[katana_runner::test(accounts = 2, fee = false)] +async fn test_account_fetch(runner: &RunnerCtx) { let pt = assert_fs::TempDir::new().unwrap(); account::fetch( diff --git a/crates/katana/runner/Cargo.toml b/crates/katana/runner/Cargo.toml index 284da3e854..9bd5323b50 100644 --- a/crates/katana/runner/Cargo.toml +++ b/crates/katana/runner/Cargo.toml @@ -6,11 +6,12 @@ version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +katana-node-bindings.workspace = true +katana-runner-macro = { path = "macro" } + anyhow.workspace = true assert_fs.workspace = true chrono.workspace = true -katana-node-bindings = { path = "../node-bindings" } -runner-macro = { path = "./runner-macro" } serde.workspace = true serde_json.workspace = true starknet.workspace = true diff --git a/crates/katana/runner/macro/Cargo.toml b/crates/katana/runner/macro/Cargo.toml new file mode 100644 index 0000000000..45c0ce5064 --- /dev/null +++ b/crates/katana/runner/macro/Cargo.toml @@ -0,0 +1,16 @@ +[package] +edition.workspace = true +name = "katana-runner-macro" +version.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.86" +quote = "1.0" +syn = { version = "2.0", features = [ "fold", "full" ] } + +[dev-dependencies] +starknet.workspace = true diff --git a/crates/katana/runner/macro/src/config.rs b/crates/katana/runner/macro/src/config.rs new file mode 100644 index 0000000000..c8a2fc484a --- /dev/null +++ b/crates/katana/runner/macro/src/config.rs @@ -0,0 +1,205 @@ +#![allow(unused)] + +use std::str::FromStr; + +use syn::spanned::Spanned; + +use crate::utils::{parse_bool, parse_int, parse_path, parse_string}; + +pub const DEFAULT_ERROR_CONFIG: Configuration = Configuration::new(false); + +/// Partial configuration for extracting the individual configuration values from the attribute +/// arguments. +pub struct Configuration { + pub crate_name: Option, + pub dev: bool, + pub is_test: bool, + pub accounts: Option, + pub fee: Option, + pub validation: Option, + pub db_dir: Option, + pub block_time: Option, + pub log_path: Option, +} + +impl Configuration { + const fn new(is_test: bool) -> Self { + Self { + is_test, + fee: None, + db_dir: None, + accounts: None, + dev: is_test, + log_path: None, + validation: None, + block_time: None, + crate_name: None, + } + } + + fn set_crate_name( + &mut self, + name: syn::Lit, + span: proc_macro2::Span, + ) -> Result<(), syn::Error> { + if self.crate_name.is_some() { + return Err(syn::Error::new(span, "`crate` set multiple times.")); + } + + let name_path = parse_path(name, span, "crate")?; + self.crate_name = Some(name_path); + + Ok(()) + } + + fn set_db_dir(&mut self, db_dir: syn::Lit, span: proc_macro2::Span) -> Result<(), syn::Error> { + if self.db_dir.is_some() { + return Err(syn::Error::new(span, "`db_dir` set multiple times.")); + } + + let db_dir = parse_string(db_dir, span, "db_dir")?; + self.db_dir = Some(db_dir); + + Ok(()) + } + + fn set_fee(&mut self, fee: syn::Lit, span: proc_macro2::Span) -> Result<(), syn::Error> { + if self.fee.is_some() { + return Err(syn::Error::new(span, "`fee` set multiple times.")); + } + + let fee = parse_bool(fee, span, "fee")?; + self.fee = Some(fee); + + Ok(()) + } + + fn set_validation( + &mut self, + validation: syn::Lit, + span: proc_macro2::Span, + ) -> Result<(), syn::Error> { + if self.validation.is_some() { + return Err(syn::Error::new(span, "`validation` set multiple times.")); + } + + let validation = parse_bool(validation, span, "validation")?; + self.validation = Some(validation); + + Ok(()) + } + + fn set_block_time( + &mut self, + block_time: syn::Lit, + span: proc_macro2::Span, + ) -> Result<(), syn::Error> { + if self.block_time.is_some() { + return Err(syn::Error::new(span, "`block_time` set multiple times.")); + } + + let block_time = parse_int(block_time, span, "block_time")? as u64; + self.block_time = Some(block_time); + + Ok(()) + } + + fn set_accounts( + &mut self, + accounts: syn::Lit, + span: proc_macro2::Span, + ) -> Result<(), syn::Error> { + if self.accounts.is_some() { + return Err(syn::Error::new(span, "`accounts` set multiple times.")); + } + + let int = parse_int(accounts, span, "accounts")?; + let accounts = u16::try_from(int) + .map_err(|_| syn::Error::new(span, "`accounts` must be a valid u16."))?; + + self.accounts = Some(accounts); + + Ok(()) + } +} + +enum RunnerArg { + BlockTime, + Fee, + Validation, + Accounts, + DbDir, +} + +impl std::str::FromStr for RunnerArg { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "block_time" => Ok(RunnerArg::BlockTime), + "fee" => Ok(RunnerArg::Fee), + "validation" => Ok(RunnerArg::Validation), + "accounts" => Ok(RunnerArg::Accounts), + "db_dir" => Ok(RunnerArg::DbDir), + _ => Err(format!( + "Unknown attribute {s} is specified; expected one of: `fee`, `validation`, \ + `accounts`, `db_dir`, `block_time`", + )), + } + } +} + +pub fn build_config( + _input: &crate::item::ItemFn, + args: crate::entry::AttributeArgs, + is_test: bool, +) -> Result { + let mut config = Configuration::new(is_test); + + for arg in args { + match arg { + syn::Meta::NameValue(namevalue) => { + let ident = namevalue + .path + .get_ident() + .ok_or_else(|| { + syn::Error::new_spanned(&namevalue, "Must have specified ident") + })? + .to_string() + .to_lowercase(); + + // the value of the attribute + let lit = match &namevalue.value { + syn::Expr::Lit(syn::ExprLit { lit, .. }) => lit, + expr => return Err(syn::Error::new_spanned(expr, "Must be a literal")), + }; + + // the ident of the attribute + let ident = ident.as_str(); + let arg = RunnerArg::from_str(ident) + .map_err(|err| syn::Error::new_spanned(&namevalue, err))?; + + match arg { + RunnerArg::BlockTime => { + config.set_block_time(lit.clone(), Spanned::span(lit))? + } + RunnerArg::Validation => { + config.set_validation(lit.clone(), Spanned::span(lit))? + } + RunnerArg::Accounts => { + config.set_accounts(lit.clone(), Spanned::span(lit))?; + } + + RunnerArg::Fee => config.set_fee(lit.clone(), Spanned::span(lit))?, + RunnerArg::DbDir => config.set_db_dir(lit.clone(), Spanned::span(lit))?, + } + } + + other => { + return Err(syn::Error::new_spanned(other, "Unknown attribute inside the macro")); + } + } + } + + Ok(config) +} diff --git a/crates/katana/runner/macro/src/entry.rs b/crates/katana/runner/macro/src/entry.rs new file mode 100644 index 0000000000..e233ffb1da --- /dev/null +++ b/crates/katana/runner/macro/src/entry.rs @@ -0,0 +1,119 @@ +use proc_macro2::{Span, TokenStream}; +use quote::{quote, quote_spanned, ToTokens}; +use syn::parse::Parser; +use syn::{parse_quote, Ident}; + +use crate::config::{build_config, Configuration, DEFAULT_ERROR_CONFIG}; +use crate::item::ItemFn; +use crate::utils::attr_ends_with; + +// Because syn::AttributeArgs does not implement syn::Parse +pub type AttributeArgs = syn::punctuated::Punctuated; + +pub(crate) fn test(args: TokenStream, item: TokenStream) -> TokenStream { + // If any of the steps for this macro fail, we still want to expand to an item that is as close + // to the expected output as possible. This helps out IDEs such that completions and other + // related features keep working. + + let input: ItemFn = match syn::parse2(item.clone()) { + Ok(it) => it, + Err(e) => return token_stream_with_error(item, e), + }; + + // parse the attribute arguments + let config = AttributeArgs::parse_terminated + .parse2(args) + .and_then(|args| build_config(&input, args, true)); + + match config { + Ok(config) => parse_knobs(input, true, config), + Err(e) => token_stream_with_error(parse_knobs(input, false, DEFAULT_ERROR_CONFIG), e), + } +} + +fn token_stream_with_error(mut tokens: TokenStream, error: syn::Error) -> TokenStream { + tokens.extend(error.into_compile_error()); + tokens +} + +pub fn parse_knobs(input: ItemFn, is_test: bool, config: Configuration) -> TokenStream { + // If type mismatch occurs, the current rustc points to the last statement. + let (last_stmt_start_span, last_stmt_end_span) = { + let mut last_stmt = input.stmts.last().cloned().unwrap_or_default().into_iter(); + + // `Span` on stable Rust has a limitation that only points to the first + // token, not the whole tokens. We can work around this limitation by + // using the first/last span of the tokens like + // `syn::Error::new_spanned` does. + let start = last_stmt.next().map_or_else(Span::call_site, |t| t.span()); + let end = last_stmt.last().map_or(start, |t| t.span()); + (start, end) + }; + + let crate_path = config + .crate_name + .map(ToTokens::into_token_stream) + .unwrap_or_else(|| Ident::new("katana_runner", last_stmt_start_span).into_token_stream()); + + let mut cfg: TokenStream = quote! {}; + + if let Some(value) = config.block_time { + cfg = quote_spanned! (last_stmt_start_span=> #cfg block_time: Some(#value), ); + } + + if let Some(value) = config.fee { + cfg = quote_spanned! (last_stmt_start_span=> #cfg disable_fee: #value, ); + } + + if let Some(value) = config.db_dir { + cfg = quote_spanned! (last_stmt_start_span=> #cfg db_dir: Some(std::path::PathBuf::from(#value)), ); + } + + if let Some(value) = config.accounts { + cfg = quote_spanned! (last_stmt_start_span=> #cfg n_accounts: #value, ); + } + + if let Some(value) = config.log_path { + cfg = quote_spanned! (last_stmt_start_span=> #cfg, log_path: Some(#value), ); + } + + if config.dev { + cfg = quote_spanned! (last_stmt_start_span=> #cfg dev: true, ); + } + + cfg = quote_spanned! {last_stmt_start_span=> + #crate_path::KatanaRunnerConfig { #cfg ..Default::default() } + }; + + let generated_attrs = if is_test { + // Don't include the #[test] attribute if it already exists. + // Otherwise, if we use a proc macro that applies the #[test] attribute (eg. + // #[tokio::test]), the test would be executed twice. + if input.attrs().any(|a| attr_ends_with(a, &parse_quote! {test})) { + quote! {} + } else { + quote! { + #[::core::prelude::v1::test] + } + } + } else { + quote! {} + }; + + let mut inner = input.clone(); + inner.sig.ident = Ident::new(&format!("___{}", inner.sig.ident), inner.sig.ident.span()); + inner.outer_attrs.clear(); + let inner_name = &inner.sig.ident; + + let last_block = quote_spanned! {last_stmt_end_span=> + { + let runner = #crate_path::KatanaRunner::new_with_config(#cfg).expect("Failed to start runner."); + let ctx = #crate_path::RunnerCtx::new(runner); + #[allow(clippy::needless_return)] + return #inner_name(&ctx); + } + }; + let inner = quote! { #inner }; + + input.into_tokens(generated_attrs, inner, last_block) +} diff --git a/crates/katana/runner/macro/src/item.rs b/crates/katana/runner/macro/src/item.rs new file mode 100644 index 0000000000..77d4d4b1c2 --- /dev/null +++ b/crates/katana/runner/macro/src/item.rs @@ -0,0 +1,140 @@ +#![allow(unused)] + +use proc_macro2::{Span, TokenStream, TokenTree}; +use quote::{quote, quote_spanned, ToTokens, TokenStreamExt}; +use syn::parse::{Parse, ParseStream}; +use syn::{braced, parse_quote, Attribute, Ident, Signature, Visibility}; + +use crate::config::Configuration; +use crate::utils::{attr_ends_with, parse_bool, parse_int, parse_string}; + +#[derive(Clone)] +pub struct ItemFn { + pub outer_attrs: Vec, + pub vis: Visibility, + pub sig: Signature, + pub brace_token: syn::token::Brace, + pub inner_attrs: Vec, + pub stmts: Vec, +} + +impl ItemFn { + /// Access all attributes of the function item. + pub fn attrs(&self) -> impl Iterator { + self.outer_attrs.iter().chain(self.inner_attrs.iter()) + } + + /// Get the body of the function item in a manner so that it can be + /// conveniently used with the `quote!` macro. + pub fn body(&self) -> Body<'_> { + Body { brace_token: self.brace_token, stmts: &self.stmts } + } + + /// Convert our local function item into a token stream. + pub fn into_tokens( + mut self, + generated_attrs: proc_macro2::TokenStream, + inner: proc_macro2::TokenStream, + last_block: proc_macro2::TokenStream, + ) -> TokenStream { + self.sig.asyncness = None; + // empty out the arguments + self.sig.inputs.clear(); + + let mut tokens = proc_macro2::TokenStream::new(); + // Outer attributes are simply streamed as-is. + for attr in self.outer_attrs { + attr.to_tokens(&mut tokens); + } + + // Inner attributes require extra care, since they're not supported on + // blocks (which is what we're expanded into) we instead lift them + // outside of the function. This matches the behavior of `syn`. + for mut attr in self.inner_attrs { + attr.style = syn::AttrStyle::Outer; + attr.to_tokens(&mut tokens); + } + + // Add generated macros at the end, so macros processed later are aware of them. + generated_attrs.to_tokens(&mut tokens); + + self.vis.to_tokens(&mut tokens); + self.sig.to_tokens(&mut tokens); + + self.brace_token.surround(&mut tokens, |tokens| { + inner.to_tokens(tokens); + last_block.to_tokens(tokens); + }); + + tokens + } +} + +impl ToTokens for ItemFn { + fn to_tokens(&self, tokens: &mut TokenStream) { + tokens.append_all(self.outer_attrs.iter()); + self.vis.to_tokens(tokens); + self.sig.to_tokens(tokens); + self.brace_token.surround(tokens, |tokens| { + tokens.append_all(self.inner_attrs.iter()); + tokens.append_all(&self.stmts); + }); + } +} + +impl Parse for ItemFn { + #[inline] + fn parse(input: ParseStream<'_>) -> syn::Result { + // This parse implementation has been largely lifted from `syn`, with + // the exception of: + // * We don't have access to the plumbing necessary to parse inner attributes in-place. + // * We do our own statements parsing to avoid recursively parsing entire statements and + // only look for the parts we're interested in. + + let outer_attrs = input.call(Attribute::parse_outer)?; + let vis: Visibility = input.parse()?; + let sig: Signature = input.parse()?; + + let content; + let brace_token = braced!(content in input); + let inner_attrs = Attribute::parse_inner(&content)?; + + let mut buf = proc_macro2::TokenStream::new(); + let mut stmts = Vec::new(); + + while !content.is_empty() { + if let Some(semi) = content.parse::>()? { + semi.to_tokens(&mut buf); + stmts.push(buf); + buf = proc_macro2::TokenStream::new(); + continue; + } + + // Parse a single token tree and extend our current buffer with it. + // This avoids parsing the entire content of the sub-tree. + buf.extend([content.parse::()?]); + } + + if !buf.is_empty() { + stmts.push(buf); + } + + Ok(Self { outer_attrs, vis, sig, brace_token, inner_attrs, stmts }) + } +} + +pub struct Body<'a> { + brace_token: syn::token::Brace, + // Statements, with terminating `;`. + stmts: &'a [TokenStream], +} + +impl ToTokens for Body<'_> { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.brace_token.surround(tokens, |tokens| { + for stmt in self.stmts { + stmt.to_tokens(tokens); + } + }); + } +} diff --git a/crates/katana/runner/macro/src/lib.rs b/crates/katana/runner/macro/src/lib.rs new file mode 100644 index 0000000000..3ee2c73b37 --- /dev/null +++ b/crates/katana/runner/macro/src/lib.rs @@ -0,0 +1,16 @@ +#![cfg_attr(not(test), warn(unused_crate_dependencies))] + +//! Implementation of the proc macro in this module is highly adapted from `tokio-macros` crate. +//! `tokio-macro`: https://docs.rs/tokio-macros/2.4.0/tokio_macros/ + +pub(crate) mod config; +mod entry; +pub(crate) mod item; +pub(crate) mod utils; + +use proc_macro::TokenStream; + +#[proc_macro_attribute] +pub fn test(args: TokenStream, input: TokenStream) -> TokenStream { + entry::test(args.into(), input.into()).into() +} diff --git a/crates/katana/runner/macro/src/utils.rs b/crates/katana/runner/macro/src/utils.rs new file mode 100644 index 0000000000..ed2e1f211a --- /dev/null +++ b/crates/katana/runner/macro/src/utils.rs @@ -0,0 +1,46 @@ +use proc_macro2::Span; +use syn::{Attribute, Error, Lit, PathSegment}; + +pub fn parse_string(int: Lit, span: Span, field: &str) -> Result { + match int { + Lit::Str(s) => Ok(s.value()), + Lit::Verbatim(s) => Ok(s.to_string()), + _ => Err(Error::new(span, format!("Failed to parse value of `{field}` as string."))), + } +} + +pub fn parse_path(lit: Lit, span: Span, field: &str) -> Result { + match lit { + Lit::Str(s) => { + let err = Error::new( + span, + format!("Failed to parse value of `{field}` as path: \"{}\"", s.value()), + ); + s.parse::().map_err(|_| err.clone()) + } + _ => Err(Error::new(span, format!("Failed to parse value of `{}` as path.", field))), + } +} + +pub fn parse_bool(bool: Lit, span: proc_macro2::Span, field: &str) -> Result { + match bool { + Lit::Bool(b) => Ok(b.value), + _ => Err(Error::new(span, format!("Failed to parse value of `{field}` as bool."))), + } +} + +pub fn parse_int(int: Lit, span: proc_macro2::Span, field: &str) -> Result { + match int { + Lit::Int(lit) => match lit.base10_parse::() { + Ok(value) => Ok(value), + Err(e) => { + Err(Error::new(span, format!("Failed to parse value of `{field}` as integer: {e}"))) + } + }, + _ => Err(Error::new(span, format!("Failed to parse value of `{field}` as integer."))), + } +} + +pub fn attr_ends_with(attr: &Attribute, segment: &PathSegment) -> bool { + attr.path().segments.iter().last() == Some(segment) +} diff --git a/crates/katana/runner/runner-macro/Cargo.toml b/crates/katana/runner/runner-macro/Cargo.toml deleted file mode 100644 index bdc2b2250a..0000000000 --- a/crates/katana/runner/runner-macro/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -edition = "2021" -name = "runner-macro" -version = "1.0.0-alpha.12" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -proc-macro = true - -[dependencies] -quote = "1.0.35" -syn = { version = "2.0.48", features = [ "fold", "full" ] } diff --git a/crates/katana/runner/runner-macro/src/lib.rs b/crates/katana/runner/runner-macro/src/lib.rs deleted file mode 100644 index 77867a284f..0000000000 --- a/crates/katana/runner/runner-macro/src/lib.rs +++ /dev/null @@ -1,124 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, parse_quote, Stmt}; - -/// Default runner block interval -const DEFAULT_BLOCK_TIME: u64 = 3000; // 3 seconds - -/// Parses the metadata string into the number of accounts and the block time. -/// -/// # Arguments -/// -/// * `metadata` - The metadata string to parse. The string is expected to be in the format of -/// `n_accounts,executable,block_time` where `block_time` is either a number (time block is ms) or -/// a boolean with `false` to use instand mining, and `true` to use the default block time. -/// -/// # Returns -/// -/// A tuple containing the number of accounts, the path to the katana program and the block time. -fn parse_metadata(metadata: String) -> (u16, Option, Option) { - if metadata.is_empty() { - return (2, None, None); - } - - let args = metadata.split(',').collect::>(); - let n_accounts = if !args.is_empty() { args[0].parse::().unwrap() } else { 1 }; - - // Block time can be `false` to be `None`, or a number to be `Some(block_time_ms)`. - // if set to `true`, we use a default block time. - let block_time = if args.len() >= 2 { - if let Ok(b) = args[1].trim().parse::() { - if !b { None } else { Some(DEFAULT_BLOCK_TIME) } - } else if let Ok(block_time_ms) = args[1].trim().parse::() { - Some(block_time_ms) - } else { - None - } - } else { - None - }; - - let executable = if args.len() >= 3 { - args[2].trim() - } else { - return (2, None, None); - }; - - let executable = executable.replace('"', ""); - - // plus one as the first account is used for deployment - (n_accounts + 1, Some(executable), block_time) -} - -#[proc_macro_attribute] -pub fn katana_test(metadata: TokenStream, input: TokenStream) -> TokenStream { - let mut test_function = parse_macro_input!(input as syn::ItemFn); - let function_name = test_function.sig.ident.to_string(); - - let (n_accounts, executable, block_time) = parse_metadata(metadata.to_string()); - - let block_time = block_time.map(|b| quote!(Some(#b))).unwrap_or(quote!(None)); - - let program_name = executable.map(|b| quote!(Some(String::from(#b)))).unwrap_or(quote!(None)); - - let header: Stmt = parse_quote! { - let runner = - katana_runner::KatanaRunner::new_with_config( - katana_runner::KatanaRunnerConfig { - program_name: #program_name, - run_name: Some(String::from(#function_name)), - block_time: #block_time, - n_accounts: #n_accounts, - ..Default::default() - } - ) - .expect("failed to start katana"); - }; - - test_function.block.stmts.insert(0, header); - - if test_function.sig.asyncness.is_none() { - TokenStream::from(quote! { - #[test] - #test_function - }) - } else { - TokenStream::from(quote! { - #[tokio::test] - #test_function - }) - } -} - -#[proc_macro] // Needed because the main macro doesn't work with proptest -pub fn runner(metadata: TokenStream) -> TokenStream { - let metadata = metadata.to_string(); - let mut args = metadata.split(',').collect::>(); - let function_name = args.remove(0); - - let (n_accounts, executable, block_time) = parse_metadata(args.join(",")); - - let block_time = block_time.map(|b| quote!(Some(#b))).unwrap_or(quote!(None)); - - let program_name = executable.map(|b| quote!(Some(String::from(#b)))).unwrap_or(quote!(None)); - - TokenStream::from(quote! { - lazy_static::lazy_static! { - pub static ref RUNNER: std::sync::Arc = std::sync::Arc::new( - katana_runner::KatanaRunner::new_with_config( - katana_runner::KatanaRunnerConfig { - program_name: #program_name, - run_name: Some(String::from(#function_name)), - block_time: #block_time, - n_accounts: #n_accounts, - ..Default::default() - } - ) - .expect("failed to start katana") - ); - - } - - let runner = &RUNNER; - }) -} diff --git a/crates/katana/runner/src/lib.rs b/crates/katana/runner/src/lib.rs index 45c3d38785..fc063e01cc 100644 --- a/crates/katana/runner/src/lib.rs +++ b/crates/katana/runner/src/lib.rs @@ -1,7 +1,6 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] mod logs; -mod prefunded; mod utils; use std::path::PathBuf; @@ -10,14 +9,33 @@ use std::thread; use anyhow::{Context, Result}; use assert_fs::TempDir; use katana_node_bindings::{Katana, KatanaInstance}; -pub use runner_macro::{katana_test, runner}; -use starknet::core::types::Felt; +pub use katana_runner_macro::test; +use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; +use starknet::core::types::{BlockId, BlockTag, Felt}; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; +use starknet::signers::LocalWallet; use tokio::sync::Mutex; use url::Url; use utils::find_free_port; +#[allow(dead_code)] +#[derive(Debug)] +pub struct RunnerCtx(KatanaRunner); + +impl RunnerCtx { + pub fn new(runner: KatanaRunner) -> Self { + Self(runner) + } +} + +impl core::ops::Deref for RunnerCtx { + type Target = KatanaRunner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + #[derive(Debug)] pub struct KatanaRunner { instance: KatanaInstance, @@ -119,14 +137,15 @@ impl KatanaRunner { builder = builder.dev(config.dev); - let mut katana = builder.spawn(); + // start the katana instance + let mut instance = builder.spawn(); let stdout = - katana.child_mut().stdout.take().context("failed to take subprocess stdout")?; + instance.child_mut().stdout.take().context("failed to take subprocess stdout")?; let log_filename = PathBuf::from(format!( "katana-{}.log", - config.run_name.clone().unwrap_or_else(|| port.to_string()) + config.run_name.unwrap_or_else(|| port.to_string()) )); let log_file_path = if let Some(log_path) = config.log_path { @@ -141,10 +160,10 @@ impl KatanaRunner { utils::listen_to_stdout(&log_file_path_sent, stdout); }); - let provider = JsonRpcClient::new(HttpTransport::new(katana.endpoint_url())); + let provider = JsonRpcClient::new(HttpTransport::new(instance.endpoint_url())); let contract = Mutex::new(Option::None); - Ok(KatanaRunner { instance: katana, provider, log_file_path, contract }) + Ok(KatanaRunner { instance, provider, log_file_path, contract }) } pub fn log_file_path(&self) -> &PathBuf { @@ -178,6 +197,51 @@ impl KatanaRunner { pub async fn contract(&self) -> Option { *self.contract.lock().await } + + pub fn accounts_data(&self) -> &[katana_node_bindings::Account] { + self.instance.accounts() + } + + pub fn accounts(&self) -> Vec, LocalWallet>> { + self.accounts_data().iter().map(|account| self.account_to_single_owned(account)).collect() + } + + pub fn account_data(&self, index: usize) -> &katana_node_bindings::Account { + &self.accounts_data()[index] + } + + pub fn account( + &self, + index: usize, + ) -> SingleOwnerAccount, LocalWallet> { + self.account_to_single_owned(&self.accounts_data()[index]) + } + + fn account_to_single_owned( + &self, + account: &katana_node_bindings::Account, + ) -> SingleOwnerAccount, LocalWallet> { + let signer = if let Some(private_key) = &account.private_key { + LocalWallet::from(private_key.clone()) + } else { + panic!("Account does not have a private key") + }; + + let chain_id = self.instance.chain_id(); + let provider = self.owned_provider(); + + let mut account = SingleOwnerAccount::new( + provider, + signer, + account.address, + chain_id, + ExecutionEncoding::New, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + account + } } /// Determines the default program path for the katana runner based on the KATANA_RUNNER_BIN @@ -188,7 +252,7 @@ fn determine_default_program_path() -> String { #[cfg(test)] mod tests { - use super::*; + use crate::determine_default_program_path; #[test] fn test_determine_default_program_path() { diff --git a/crates/katana/runner/src/prefunded.rs b/crates/katana/runner/src/prefunded.rs deleted file mode 100644 index 033ae50fc6..0000000000 --- a/crates/katana/runner/src/prefunded.rs +++ /dev/null @@ -1,54 +0,0 @@ -use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; -use starknet::core::types::{BlockId, BlockTag}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; -use starknet::signers::LocalWallet; - -use crate::KatanaRunner; - -impl KatanaRunner { - pub fn accounts_data(&self) -> &[katana_node_bindings::Account] { - self.instance.accounts() - } - - pub fn accounts(&self) -> Vec, LocalWallet>> { - self.accounts_data().iter().map(|account| self.account_to_single_owned(account)).collect() - } - - pub fn account_data(&self, index: usize) -> &katana_node_bindings::Account { - &self.accounts_data()[index] - } - - pub fn account( - &self, - index: usize, - ) -> SingleOwnerAccount, LocalWallet> { - self.account_to_single_owned(&self.accounts_data()[index]) - } - - fn account_to_single_owned( - &self, - account: &katana_node_bindings::Account, - ) -> SingleOwnerAccount, LocalWallet> { - let signer = if let Some(private_key) = &account.private_key { - LocalWallet::from(private_key.clone()) - } else { - panic!("Account does not have a private key") - }; - - let chain_id = self.instance.chain_id(); - let provider = self.owned_provider(); - - let mut account = SingleOwnerAccount::new( - provider, - signer, - account.address, - chain_id, - ExecutionEncoding::New, - ); - - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - account - } -} diff --git a/crates/katana/runner/tests/runner.rs b/crates/katana/runner/tests/runner.rs index 684230dbe2..55586399fe 100644 --- a/crates/katana/runner/tests/runner.rs +++ b/crates/katana/runner/tests/runner.rs @@ -1,23 +1,20 @@ -use katana_runner::*; +use katana_runner::RunnerCtx; use starknet::providers::Provider; -#[katana_test(2, false)] -async fn test_run() { - for i in 0..10 { - let logname = format!("katana-test_run-{}", i); - let runner = KatanaRunner::new_with_config(KatanaRunnerConfig { - run_name: Some(logname), - ..Default::default() - }) - .expect("failed to start another katana"); +#[katana_runner::test(fee = false, accounts = 7)] +fn simple(runner: &RunnerCtx) { + assert_eq!(runner.accounts().len(), 7); +} - let _block_number = runner.provider().block_number().await.unwrap(); - // created by the macro at the beginning of the test - let _other_block_number = runner.provider().block_number().await.unwrap(); - } +#[katana_runner::test] +fn with_return(_: &RunnerCtx) -> Result<(), Box> { + Ok(()) } -#[katana_test(2, true)] -async fn basic_macro_usage() { - let _block_number = runner.provider().block_number().await.unwrap(); +#[tokio::test] +#[katana_runner::test] +async fn with_async(ctx: &RunnerCtx) -> Result<(), Box> { + let provider = ctx.provider(); + let _ = provider.chain_id().await?; + Ok(()) }