@@ -818,3 +818,167 @@ pub async fn spam<D: DbOps + Clone + Send + Sync + 'static>(
818818 let mut test_scenario = args. init_scenario ( db) . await ?;
819819 spam_inner ( db, & mut test_scenario, args, run_context) . await
820820}
821+
822+ #[ cfg( test) ]
823+ mod tests {
824+ use crate :: commands:: common:: AuthCliArgs ;
825+ use crate :: commands:: SetupCommandArgs ;
826+
827+ use super :: * ;
828+ use alloy:: node_bindings:: { Anvil , AnvilInstance , WEI_IN_ETHER } ;
829+ use contender_sqlite:: SqliteDb ;
830+ use std:: {
831+ collections:: HashMap ,
832+ path:: { Path , PathBuf } ,
833+ } ;
834+
835+ fn create_send_args ( sf : & Path , anvil : & AnvilInstance ) -> ScenarioSendTxsCliArgs {
836+ // map scenario files to custom tx types if needed
837+ // this might be replaced with a more robust solution in the future
838+ // e.g. mapping the entire ScenarioSendTxsCliArgs structure instead of just tx types
839+ let custom_tx_types = HashMap :: < & str , TxTypeCli > :: from_iter ( [
840+ ( "blobs.toml" , TxTypeCli :: Eip4844 ) ,
841+ ( "setCode.toml" , TxTypeCli :: Eip7702 ) ,
842+ ] ) ;
843+
844+ // use last components of the path after "scenarios/" as a scenario ID
845+ // this supports nested directories under "scenarios/"
846+ let relative_path = sf
847+ . components ( )
848+ . rev ( )
849+ . take_while ( |component| component. as_os_str ( ) != "scenarios" )
850+ . collect :: < Vec < _ > > ( )
851+ . into_iter ( )
852+ . rev ( )
853+ . collect :: < PathBuf > ( ) ;
854+
855+ ScenarioSendTxsCliArgs {
856+ testfile : Some ( sf. to_str ( ) . unwrap ( ) . to_owned ( ) ) ,
857+ rpc_args : SendTxsCliArgsInner {
858+ rpc_url : anvil. endpoint_url ( ) ,
859+ seed : None ,
860+ private_keys : None ,
861+ min_balance : WEI_IN_ETHER * U256 :: from ( 10 ) ,
862+ tx_type : custom_tx_types
863+ . get ( relative_path. as_os_str ( ) . to_str ( ) . unwrap ( ) )
864+ . cloned ( )
865+ . unwrap_or ( TxTypeCli :: Eip1559 ) ,
866+ bundle_type : crate :: commands:: common:: BundleTypeCli :: L1 ,
867+ auth_args : AuthCliArgs :: default ( ) ,
868+ call_forkchoice : false ,
869+ env : None ,
870+ override_senders : false ,
871+ gas_price : None ,
872+ accounts_per_agent : None ,
873+ } ,
874+ }
875+ }
876+
877+ async fn run_scenario (
878+ sf : & Path ,
879+ anvil : & AnvilInstance ,
880+ db : & SqliteDb ,
881+ rand_seed : & RandSeed ,
882+ ) -> Result < ( ) > {
883+ // special case: skip bundle scenario (anvil doesn't support it)
884+ if sf. ends_with ( "bundles.toml" ) {
885+ println ! ( "Skipping bundle scenario (anvil doesn't support bundles)" ) ;
886+ return Ok ( ( ) ) ;
887+ }
888+
889+ // initialize a logger
890+ let _ = tracing_subscriber:: fmt ( )
891+ . with_env_filter ( "contender_core=debug,info" )
892+ . with_test_writer ( ) // captures output properly in tests
893+ . try_init ( ) ; // try_init() won't panic if already initialized
894+
895+ // initialize scenario
896+ let scenario = SpamScenario :: Testfile ( sf. to_str ( ) . unwrap ( ) . to_owned ( ) ) ;
897+ let send_args = create_send_args ( sf, anvil) ;
898+
899+ // run setup
900+ crate :: commands:: setup (
901+ db,
902+ SetupCommandArgs {
903+ scenario : scenario. clone ( ) ,
904+ eth_json_rpc_args : send_args. rpc_args . clone ( ) ,
905+ seed : rand_seed. clone ( ) ,
906+ } ,
907+ )
908+ . await ?;
909+
910+ // do a quick spam run
911+ let res = spam (
912+ db,
913+ & SpamCommandArgs {
914+ scenario,
915+ spam_args : SpamCliArgs {
916+ eth_json_rpc_args : send_args,
917+ spam_args : SendSpamCliArgs {
918+ builder_url : None ,
919+ txs_per_second : Some ( 50 ) ,
920+ txs_per_block : None ,
921+ duration : 4 ,
922+ pending_timeout : 10 ,
923+ run_forever : false ,
924+ } ,
925+ ignore_receipts : false ,
926+ optimistic_nonces : true ,
927+ gen_report : false ,
928+ redeploy : true ,
929+ skip_setup : false ,
930+ rpc_batch_size : 0 ,
931+ spam_timeout : Duration :: from_secs ( 5 ) ,
932+ } ,
933+ seed : rand_seed. clone ( ) ,
934+ } ,
935+ SpamCampaignContext {
936+ campaign_id : None ,
937+ campaign_name : None ,
938+ stage_name : None ,
939+ scenario_name : None ,
940+ } ,
941+ )
942+ . await ?;
943+
944+ println ! ( "spam run successful. run id: {:?}" , res) ;
945+ Ok ( ( ) )
946+ }
947+
948+ /// Spin up a fresh anvil instance, DB, & seed, then run the scenario file given at `path`.
949+ async fn run_scenario_file ( path : & Path ) -> Result < ( ) > {
950+ let anvil = Anvil :: new ( ) . block_time ( 1 ) . spawn ( ) ;
951+ let db = SqliteDb :: new_memory ( ) ;
952+ db. create_tables ( ) ?;
953+ let rand_seed = RandSeed :: new ( ) ;
954+
955+ run_scenario ( path, & anvil, & db, & rand_seed) . await
956+ }
957+
958+ /// Generates individual spam test for each scenario given in the macro input.
959+ /// NOTE: paths are relative to the project root. See `build.rs` for usage.
960+ macro_rules! scenario_tests {
961+ ( $( $name: ident => $relative_path: expr) ,* $( , ) ?) => {
962+ $(
963+ #[ tokio:: test]
964+ async fn $name( ) -> std:: result:: Result <( ) , CliError > {
965+ let project_root = Path :: new( env!( "CARGO_MANIFEST_DIR" ) )
966+ . parent( )
967+ . unwrap( )
968+ . parent( )
969+ . unwrap( ) ;
970+ let path: PathBuf = project_root. join( $relative_path) ;
971+ run_scenario_file( & path) . await ?;
972+ Ok ( ( ) )
973+ }
974+ ) *
975+ } ;
976+ }
977+
978+ #[ allow( non_snake_case) ]
979+ mod generated_scenario_tests {
980+ use super :: * ;
981+ // Generate tests for all scenario files identified by build.rs
982+ include ! ( concat!( env!( "OUT_DIR" ) , "/generated_scenario_tests.rs" ) ) ;
983+ }
984+ }
0 commit comments