Skip to content
Snippets Groups Projects
Select Git revision
  • master default protected
  • network/gdev-800 protected
  • cgeek/issue-297-cpu
  • gdev-800-tests
  • update-docker-compose-rpc-squid-names
  • fix-252
  • 1000i100-test
  • hugo/tmp-0.9.1
  • network/gdev-803 protected
  • hugo/endpoint-gossip
  • network/gdev-802 protected
  • hugo/distance-precompute
  • network/gdev-900 protected
  • tuxmain/anonymous-tx
  • debug/podman
  • hugo/195-doc
  • hugo/195-graphql-schema
  • hugo-tmp-dockerfile-cache
  • release/client-800.2 protected
  • release/runtime-800 protected
  • gdev-900-0.10.1 protected
  • gdev-900-0.10.0 protected
  • gdev-900-0.9.2 protected
  • gdev-800-0.8.0 protected
  • gdev-900-0.9.1 protected
  • gdev-900-0.9.0 protected
  • gdev-803 protected
  • gdev-802 protected
  • runtime-801 protected
  • gdev-800 protected
  • runtime-800-bis protected
  • runtime-800 protected
  • runtime-800-backup protected
  • runtime-701 protected
  • runtime-700 protected
  • runtime-600 protected
  • runtime-500 protected
  • v0.4.1 protected
  • runtime-401 protected
  • v0.4.0 protected
40 results

cucumber_tests.rs

Blame
  • cucumber_tests.rs 16.55 KiB
    // Copyright 2021 Axiom-Team
    //
    // This file is part of Duniter-v2S.
    //
    // Duniter-v2S is free software: you can redistribute it and/or modify
    // it under the terms of the GNU Affero General Public License as published by
    // the Free Software Foundation, version 3 of the License.
    //
    // Duniter-v2S is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    // GNU Affero General Public License for more details.
    //
    // You should have received a copy of the GNU Affero General Public License
    // along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>.
    
    mod common;
    
    use async_trait::async_trait;
    use common::*;
    use cucumber::{given, then, when, World, WorldInit};
    use sp_keyring::AccountKeyring;
    use std::convert::Infallible;
    use std::path::PathBuf;
    use std::str::FromStr;
    use std::sync::{
        atomic::{AtomicBool, Ordering},
        Arc,
    };
    
    // ===== world =====
    
    #[derive(WorldInit)]
    pub struct DuniterWorld {
        ignore_errors: bool,
        inner: Option<DuniterWorldInner>,
    }
    
    impl DuniterWorld {
        // Write methods
        async fn init(&mut self, maybe_genesis_conf_file: Option<PathBuf>) {
            if let Some(ref mut inner) = self.inner {
                inner.kill();
            }
            self.inner = Some(DuniterWorldInner::new(maybe_genesis_conf_file).await);
        }
        fn kill(&mut self) {
            if let Some(ref mut inner) = self.inner {
                inner.kill();
            }
        }
        fn set_ignore_errors(&mut self, ignore_errors: bool) {
            self.ignore_errors = ignore_errors;
        }
        // Read methods
        fn client(&self) -> &Client {
            if let Some(ref inner) = self.inner {
                &inner.client
            } else {
                panic!("uninit")
            }
        }
        fn ignore_errors(&self) -> bool {
            self.ignore_errors
        }
        // Read storage entry on last block
        fn read<'a, Address>(
            &self,
            address: &'a Address,
        ) -> impl std::future::Future<
            Output = std::result::Result<
                Option<<Address::Target as subxt::metadata::DecodeWithMetadata>::Target>,
                subxt::error::Error,
            >,
        > + 'a
        where
            Address: subxt::storage::StorageAddress<IsFetchable = subxt::storage::address::Yes> + 'a,
        {
            self.client().storage().fetch(address, None)
        }
        // Read storage entry with default value (on last block)
        fn read_or_default<'a, Address>(
            &self,
            address: &'a Address,
        ) -> impl std::future::Future<
            Output = std::result::Result<
                <Address::Target as subxt::metadata::DecodeWithMetadata>::Target,
                subxt::error::Error,
            >,
        > + 'a
        where
            Address: subxt::storage::StorageAddress<
                    IsFetchable = subxt::storage::address::Yes,
                    IsDefaultable = subxt::storage::address::Yes,
                > + 'a,
        {
            self.client().storage().fetch_or_default(address, None)
        }
    }
    
    impl std::fmt::Debug for DuniterWorld {
        fn fmt(&self, _: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
            Ok(())
        }
    }
    
    #[async_trait(?Send)]
    impl World for DuniterWorld {
        // We do require some error type.
        type Error = Infallible;
    
        async fn new() -> std::result::Result<Self, Infallible> {
            Ok(Self {
                ignore_errors: false,
                inner: None,
            })
        }
    }
    
    struct DuniterWorldInner {
        client: Client,
        process: Process,
    }
    
    impl DuniterWorldInner {
        async fn new(maybe_genesis_conf_file: Option<PathBuf>) -> Self {
            let (client, process) = spawn_node(maybe_genesis_conf_file).await;
            DuniterWorldInner { client, process }
        }
        fn kill(&mut self) {
            self.process.kill();
        }
    }
    
    fn parse_amount(amount: u64, unit: &str) -> (u64, bool) {
        match unit {
            "ĞD" => (amount * 100, false),
            "cĞD" => (amount, false),
            "UD" => (amount * 1_000, true),
            "mUD" => (amount, true),
            _ => unreachable!(),
        }
    }
    
    // ===== given =====
    
    #[given(regex = r"([a-zA-Z]+) ha(?:ve|s) (\d+) (ĞD|cĞD|UD|mUD)")]
    async fn who_have(world: &mut DuniterWorld, who: String, amount: u64, unit: String) -> Result<()> {
        // Parse inputs
        let who = AccountKeyring::from_str(&who).expect("unknown to");
        let (mut amount, is_ud) = parse_amount(amount, &unit);
    
        if is_ud {
            let current_ud_amount = world
                .read(&gdev::storage().universal_dividend().current_ud())
                .await?
                .unwrap_or_default();
            amount = (amount * current_ud_amount) / 1_000;
        }
    
        // Create {amount} ĞD for {who}
        common::balances::set_balance(world.client(), who, amount).await?;
    
        Ok(())
    }
    
    // ===== when =====
    
    #[when(regex = r"(\d+) blocks? later")]
    async fn n_blocks_later(world: &mut DuniterWorld, n: usize) -> Result<()> {
        for _ in 0..n {
            common::create_empty_block(world.client()).await?;
        }
        Ok(())
    }
    
    #[when(regex = r"([a-zA-Z]+) sends? (\d+) (ĞD|cĞD|UD|mUD) to ([a-zA-Z]+)$")]
    async fn transfer(
        world: &mut DuniterWorld,
        from: String,
        amount: u64,
        unit: String,
        to: String,
    ) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
        let (amount, is_ud) = parse_amount(amount, &unit);
    
        let res = if is_ud {
            common::balances::transfer_ud(world.client(), from, amount, to).await
        } else {
            common::balances::transfer(world.client(), from, amount, to).await
        };
    
        if world.ignore_errors() {
            Ok(())
        } else {
            res
        }
    }
    
    #[when(regex = r"([a-zA-Z]+) sends? (\d+) (ĞD|cĞD) to oneshot ([a-zA-Z]+)")]
    async fn create_oneshot_account(
        world: &mut DuniterWorld,
        from: String,
        amount: u64,
        unit: String,
        to: String,
    ) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
        let (amount, is_ud) = parse_amount(amount, &unit);
    
        assert!(!is_ud);
    
        common::oneshot::create_oneshot_account(world.client(), from, amount, to).await
    }
    
    #[when(regex = r"oneshot ([a-zA-Z]+) consumes? into (oneshot|account) ([a-zA-Z]+)")]
    async fn consume_oneshot_account(
        world: &mut DuniterWorld,
        from: String,
        is_dest_oneshot: String,
        to: String,
    ) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
        let to = match is_dest_oneshot.as_str() {
            "oneshot" => common::oneshot::Account::Oneshot(to),
            "account" => common::oneshot::Account::Normal(to),
            _ => unreachable!(),
        };
    
        common::oneshot::consume_oneshot_account(world.client(), from, to).await
    }
    
    #[when(
        regex = r"oneshot ([a-zA-Z]+) consumes? (\d+) (ĞD|cĞD) into (oneshot|account) ([a-zA-Z]+) and the rest into (oneshot|account) ([a-zA-Z]+)"
    )]
    #[allow(clippy::too_many_arguments)]
    async fn consume_oneshot_account_with_remaining(
        world: &mut DuniterWorld,
        from: String,
        amount: u64,
        unit: String,
        is_dest_oneshot: String,
        to: String,
        is_remaining_to_oneshot: String,
        remaining_to: String,
    ) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
        let remaining_to = AccountKeyring::from_str(&remaining_to).expect("unknown remaining_to");
        let to = match is_dest_oneshot.as_str() {
            "oneshot" => common::oneshot::Account::Oneshot(to),
            "account" => common::oneshot::Account::Normal(to),
            _ => unreachable!(),
        };
        let remaining_to = match is_remaining_to_oneshot.as_str() {
            "oneshot" => common::oneshot::Account::Oneshot(remaining_to),
            "account" => common::oneshot::Account::Normal(remaining_to),
            _ => unreachable!(),
        };
        let (amount, is_ud) = parse_amount(amount, &unit);
    
        assert!(!is_ud);
    
        common::oneshot::consume_oneshot_account_with_remaining(
            world.client(),
            from,
            amount,
            to,
            remaining_to,
        )
        .await
    }
    
    #[when(regex = r"([a-zA-Z]+) sends? all (?:his|her) (?:ĞDs?|DUs?|UDs?) to ([a-zA-Z]+)")]
    async fn send_all_to(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
    
        common::balances::transfer_all(world.client(), from, to).await
    }
    
    #[when(regex = r"([a-zA-Z]+) certifies ([a-zA-Z]+)")]
    async fn certifies(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
    
        common::cert::certify(world.client(), from, to).await
    }
    
    #[when(regex = r"([a-zA-Z]+) creates identity for ([a-zA-Z]+)")]
    async fn creates_identity(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
        // Parse inputs
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to = AccountKeyring::from_str(&to).expect("unknown to");
    
        common::identity::create_identity(world.client(), from, to).await
    }
    
    #[when(regex = r#"([a-zA-Z]+) confirms (?:his|her) identity with pseudo "([a-zA-Z]+)""#)]
    async fn confirm_identity(world: &mut DuniterWorld, from: String, pseudo: String) -> Result<()> {
        let from = AccountKeyring::from_str(&from).expect("unknown from");
    
        common::identity::confirm_identity(world.client(), from, pseudo).await
    }
    
    #[when(regex = r#"([a-zA-Z]+) validates ([a-zA-Z]+) identity"#)]
    async fn validate_identity(world: &mut DuniterWorld, from: String, to: String) -> Result<()> {
        // input names to keyrings
        let from = AccountKeyring::from_str(&from).expect("unknown from");
        let to: u32 = common::identity::get_identity_index(world, to).await?;
    
        common::identity::validate_identity(world.client(), from, to).await
    }
    
    // ===== then ====
    
    #[then(regex = r"([a-zA-Z]+) should have (\d+) (ĞD|cĞD)")]
    async fn should_have(
        world: &mut DuniterWorld,
        who: String,
        amount: u64,
        unit: String,
    ) -> Result<()> {
        // Parse inputs
        let who = AccountKeyring::from_str(&who)
            .expect("unknown to")
            .to_account_id();
        let (amount, _is_ud) = parse_amount(amount, &unit);
    
        let who_account = world
            .read_or_default(&gdev::storage().system().account(&who))
            .await?;
        assert_eq!(who_account.data.free, amount);
        Ok(())
    }
    
    #[then(regex = r"([a-zA-Z]+) should have oneshot (\d+) (ĞD|cĞD)")]
    async fn should_have_oneshot(
        world: &mut DuniterWorld,
        who: String,
        amount: u64,
        unit: String,
    ) -> Result<()> {
        // Parse inputs
        let who = AccountKeyring::from_str(&who)
            .expect("unknown to")
            .to_account_id();
        let (amount, _is_ud) = parse_amount(amount, &unit);
    
        let oneshot_amount = world
            .read(&gdev::storage().oneshot_account().oneshot_accounts(&who))
            .await?;
        assert_eq!(oneshot_amount.unwrap_or(0), amount);
        Ok(())
    }
    
    #[then(regex = r"Current UD amount should be (\d+).(\d+)")]
    async fn current_ud_amount_should_be(
        world: &mut DuniterWorld,
        amount: u64,
        cents: u64,
    ) -> Result<()> {
        let expected = (amount * 100) + cents;
        let actual = world
            .read_or_default(&gdev::storage().universal_dividend().current_ud())
            .await?;
        assert_eq!(actual, expected);
        Ok(())
    }
    
    #[then(regex = r"Monetary mass should be (\d+).(\d+)")]
    async fn monetary_mass_should_be(world: &mut DuniterWorld, amount: u64, cents: u64) -> Result<()> {
        let expected = (amount * 100) + cents;
        let actual = world
            .read_or_default(&gdev::storage().universal_dividend().monetary_mass())
            .await?;
        assert_eq!(actual, expected);
        Ok(())
    }
    
    #[then(regex = r"([a-zA-Z]+) should be certified by ([a-zA-Z]+)")]
    async fn should_be_certified_by(
        world: &mut DuniterWorld,
        receiver: String,
        issuer: String,
    ) -> Result<()> {
        // Parse inputs
        let receiver_account = AccountKeyring::from_str(&receiver)
            .expect("unknown to")
            .to_account_id();
        let issuer_account = AccountKeyring::from_str(&issuer)
            .expect("unknown to")
            .to_account_id();
    
        // get corresponding identities index
        let issuer_index = world
            .read(
                &gdev::storage()
                    .identity()
                    .identity_index_of(&issuer_account),
            )
            .await?
            .unwrap();
        let receiver_index = world
            .read(
                &gdev::storage()
                    .identity()
                    .identity_index_of(&receiver_account),
            )
            .await?
            .unwrap();
    
        let issuers = world
            .read_or_default(&gdev::storage().cert().certs_by_receiver(receiver_index))
            .await?;
    
        // look for certification by issuer/receiver pair
        match issuers.binary_search_by(|(issuer_, _)| issuer_index.cmp(issuer_)) {
            Ok(_) => Ok(()),
            Err(_) => Err(anyhow::anyhow!(
                "no certification found from {} to {}: {:?}",
                issuer,
                receiver,
                issuers
            )
            .into()),
        }
    }
    
    use gdev::runtime_types::pallet_identity::types::IdtyStatus;
    
    // status from string
    impl FromStr for IdtyStatus {
        type Err = String;
    
        fn from_str(input: &str) -> std::result::Result<IdtyStatus, String> {
            match input {
                "created" => Ok(IdtyStatus::Created),
                "confirmed" => Ok(IdtyStatus::ConfirmedByOwner),
                "validated" => Ok(IdtyStatus::Validated),
                _ => Err(format!("'{input}' does not match a status")),
            }
        }
    }
    
    #[then(regex = r"([a-zA-Z]+) identity should be ([a-zA-Z ]+)")]
    async fn identity_status_should_be(
        world: &mut DuniterWorld,
        name: String,
        status: String,
    ) -> Result<()> {
        let identity_value = common::identity::get_identity_value(world, name).await?;
        let expected_status = IdtyStatus::from_str(&status)?;
        assert_eq!(identity_value.status, expected_status);
        Ok(())
    }
    
    // ============================================================
    
    #[derive(clap::Args)]
    struct CustomOpts {
        /// Keep running
        #[clap(short, long)]
        keep_running: bool,
    }
    
    const DOCKER_FEATURES_PATH: &str = "/var/lib/duniter/cucumber-features";
    const LOCAL_FEATURES_PATH: &str = "cucumber-features";
    
    #[tokio::main(flavor = "current_thread")]
    async fn main() {
        //env_logger::init();
    
        let features_path = if std::path::Path::new(DOCKER_FEATURES_PATH).exists() {
            DOCKER_FEATURES_PATH
        } else if std::path::Path::new(LOCAL_FEATURES_PATH).exists() {
            LOCAL_FEATURES_PATH
        } else {
            panic!("cucumber-features folder not found");
        };
    
        let opts = cucumber::cli::Opts::<_, _, _, CustomOpts>::parsed();
        let keep_running = opts.custom.keep_running;
    
        // Handle crtl+C
        let running = Arc::new(AtomicBool::new(true));
        let running_clone = running.clone();
        ctrlc::set_handler(move || {
            running_clone.store(false, Ordering::SeqCst);
        })
        .expect("Error setting Ctrl-C handler");
    
        DuniterWorld::cucumber()
            //.fail_on_skipped()
            .max_concurrent_scenarios(4)
            .before(|feature, _rule, scenario, world| {
                let mut genesis_conf_file_path = PathBuf::new();
                genesis_conf_file_path.push("cucumber-genesis");
                genesis_conf_file_path.push(&format!(
                    "{}.json",
                    genesis_conf_name(&feature.tags, &scenario.tags)
                ));
                world.set_ignore_errors(ignore_errors(&scenario.tags));
                Box::pin(world.init(Some(genesis_conf_file_path)))
            })
            .after(move |_feature, _rule, _scenario, maybe_world| {
                if keep_running {
                    while running.load(Ordering::SeqCst) {}
                }
    
                if let Some(world) = maybe_world {
                    world.kill();
                }
                Box::pin(std::future::ready(()))
            })
            .with_cli(opts)
            .run_and_exit(features_path)
            .await;
    }
    
    fn genesis_conf_name(feature_tags: &[String], scenario_tags: &[String]) -> String {
        for tag in scenario_tags {
            if let Some(("genesis", conf_name)) = tag.split_once('.') {
                return conf_name.to_owned();
            }
        }
        for tag in feature_tags {
            if let Some(("genesis", conf_name)) = tag.split_once('.') {
                return conf_name.to_owned();
            }
        }
        "default".to_owned()
    }
    
    fn ignore_errors(scenario_tags: &[String]) -> bool {
        for tag in scenario_tags {
            if tag == "ignoreErrors" {
                return true;
            }
        }
        false
    }