Jelajahi Sumber

lang: Add support for multiple test suites with separate local validators (#1681)

Paul 3 tahun lalu
induk
melakukan
470e902f48
31 mengubah file dengan 976 tambahan dan 137 penghapusan
  1. 2 0
      .github/workflows/tests.yaml
  2. 2 0
      .gitignore
  3. 1 0
      CHANGELOG.md
  4. 1 1
      cli/Cargo.toml
  5. 395 25
      cli/src/config.rs
  6. 171 110
      cli/src/lib.rs
  7. 18 0
      tests/multiple-suites/Anchor.toml
  8. 4 0
      tests/multiple-suites/Cargo.toml
  9. 12 0
      tests/multiple-suites/migrations/deploy.ts
  10. 19 0
      tests/multiple-suites/package.json
  11. 19 0
      tests/multiple-suites/programs/multiple-suites/Cargo.toml
  12. 2 0
      tests/multiple-suites/programs/multiple-suites/Xargo.toml
  13. 16 0
      tests/multiple-suites/programs/multiple-suites/src/lib.rs
  14. 5 0
      tests/multiple-suites/tests/Test.base.toml
  15. 12 0
      tests/multiple-suites/tests/Test.root.base.toml
  16. 13 0
      tests/multiple-suites/tests/accounts/ANOTHER_ACC.json
  17. 13 0
      tests/multiple-suites/tests/accounts/SOME_ACCOUNT.json
  18. 13 0
      tests/multiple-suites/tests/accounts/SOME_TOKEN.json
  19. 4 0
      tests/multiple-suites/tests/another-suite/Test.toml
  20. 44 0
      tests/multiple-suites/tests/another-suite/another-suite.ts
  21. 4 0
      tests/multiple-suites/tests/fourth-and-fifth-suite/fifth-suite/Test.toml
  22. 43 0
      tests/multiple-suites/tests/fourth-and-fifth-suite/fifth-suite/fifthSuite.ts
  23. 4 0
      tests/multiple-suites/tests/fourth-and-fifth-suite/forth-suite/Test.toml
  24. 43 0
      tests/multiple-suites/tests/fourth-and-fifth-suite/forth-suite/forth-suite.ts
  25. 4 0
      tests/multiple-suites/tests/multiple-suites/Test.toml
  26. 32 0
      tests/multiple-suites/tests/multiple-suites/multiple-suites.ts
  27. 4 0
      tests/multiple-suites/tests/third-suite/Test.toml
  28. 32 0
      tests/multiple-suites/tests/third-suite/sub-dir-one/subDirOne.ts
  29. 32 0
      tests/multiple-suites/tests/third-suite/sub-dir-two/subDirTwo.ts
  30. 10 0
      tests/multiple-suites/tsconfig.json
  31. 2 1
      tests/package.json

+ 2 - 0
.github/workflows/tests.yaml

@@ -381,6 +381,8 @@ jobs:
             path: tests/validator-clone
           - cmd: cd tests/cpi-returns && anchor test --skip-lint
             path: tests/cpi-returns
+          - cmd: cd tests/multiple-suites && anchor test --skip-lint
+            path: tests/multiple-suites
     steps:
       - uses: actions/checkout@v2
       - uses: ./.github/actions/setup/

+ 2 - 0
.gitignore

@@ -12,6 +12,8 @@ examples/*/Cargo.lock
 examples/**/Cargo.lock
 tests/*/Cargo.lock
 tests/**/Cargo.lock
+tests/*/yarn.lock
+tests/**/yarn.lock
 .DS_Store
 docs/yarn.lock
 ts/docs/

+ 1 - 0
CHANGELOG.md

@@ -12,6 +12,7 @@ The minor version will be incremented upon a breaking change and the patch versi
 
 ### Features
 
+* lang: Add support for multiple test suites with separate local validators ([#1681](https://github.com/project-serum/anchor/pull/1681)).
 * lang: Add return values to CPI client. ([#1598](https://github.com/project-serum/anchor/pull/1598)).
 * avm: New `avm update` command to update the Anchor CLI to the latest version ([#1670](https://github.com/project-serum/anchor/pull/1670)).
 

+ 1 - 1
cli/Cargo.toml

@@ -39,6 +39,6 @@ reqwest = { version = "0.11.4", features = ["multipart", "blocking"] }
 tokio = "1.0"
 pathdiff = "0.2.0"
 cargo_toml = "0.9.2"
-walkdir = "2"
+walkdir = "2.3.2"
 chrono = "0.4.19"
 portpicker = "0.1.1"

+ 395 - 25
cli/src/config.rs

@@ -1,3 +1,4 @@
+use crate::is_hidden;
 use anchor_client::Cluster;
 use anchor_syn::idl::Idl;
 use anyhow::{anyhow, Context, Error, Result};
@@ -7,7 +8,7 @@ use serde::{Deserialize, Serialize};
 use solana_cli_config::{Config as SolanaConfig, CONFIG_FILE};
 use solana_sdk::pubkey::Pubkey;
 use solana_sdk::signature::{Keypair, Signer};
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 use std::convert::TryFrom;
 use std::fs::{self, File};
 use std::io;
@@ -16,6 +17,11 @@ use std::ops::Deref;
 use std::path::Path;
 use std::path::PathBuf;
 use std::str::FromStr;
+use walkdir::WalkDir;
+
+pub trait Merge: Sized {
+    fn merge(&mut self, _other: Self) {}
+}
 
 #[derive(Default, Debug, Parser)]
 pub struct ConfigOverride {
@@ -27,6 +33,7 @@ pub struct ConfigOverride {
     pub wallet: Option<WalletPath>,
 }
 
+#[derive(Debug)]
 pub struct WithPath<T> {
     inner: T,
     path: PathBuf,
@@ -267,7 +274,11 @@ pub struct Config {
     pub programs: ProgramsConfig,
     pub scripts: ScriptsConfig,
     pub workspace: WorkspaceConfig,
-    pub test: Option<Test>,
+    // Separate entry next to test_config because
+    // "anchor localnet" only has access to the Anchor.toml,
+    // not the Test.toml files
+    pub test_validator: Option<TestValidator>,
+    pub test_config: Option<TestConfig>,
 }
 
 #[derive(Default, Clone, Debug, Serialize, Deserialize)]
@@ -324,6 +335,11 @@ pub struct BuildConfig {
 }
 
 impl Config {
+    fn with_test_config(mut self, p: impl AsRef<Path>) -> Result<Self> {
+        self.test_config = TestConfig::discover(p)?;
+        Ok(self)
+    }
+
     pub fn docker(&self) -> String {
         let ver = self
             .anchor_version
@@ -377,7 +393,8 @@ impl Config {
     fn from_path(p: impl AsRef<Path>) -> Result<Self> {
         fs::read_to_string(&p)
             .with_context(|| format!("Error reading the file with path: {}", p.as_ref().display()))?
-            .parse()
+            .parse::<Self>()?
+            .with_test_config(p.as_ref().parent().unwrap())
     }
 
     pub fn wallet_kp(&self) -> Result<Keypair> {
@@ -396,7 +413,7 @@ struct _Config {
     provider: Provider,
     workspace: Option<WorkspaceConfig>,
     scripts: Option<ScriptsConfig>,
-    test: Option<Test>,
+    test: Option<_TestValidator>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -424,7 +441,7 @@ impl ToString for Config {
                 cluster: format!("{}", self.provider.cluster),
                 wallet: self.provider.wallet.to_string(),
             },
-            test: self.test.clone(),
+            test: self.test_validator.clone().map(Into::into),
             scripts: match self.scripts.is_empty() {
                 true => None,
                 false => Some(self.scripts.clone()),
@@ -454,7 +471,8 @@ impl FromStr for Config {
                 wallet: shellexpand::tilde(&cfg.provider.wallet).parse()?,
             },
             scripts: cfg.scripts.unwrap_or_default(),
-            test: cfg.test,
+            test_validator: cfg.test.map(Into::into),
+            test_config: None,
             programs: cfg.programs.map_or(Ok(BTreeMap::new()), deser_programs)?,
             workspace: cfg.workspace.unwrap_or_default(),
         })
@@ -531,11 +549,235 @@ fn deser_programs(
         .collect::<Result<BTreeMap<Cluster, BTreeMap<String, ProgramDeployment>>>>()
 }
 
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct Test {
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct TestValidator {
     pub genesis: Option<Vec<GenesisEntry>>,
     pub validator: Option<Validator>,
+    pub startup_wait: i32,
+    pub shutdown_wait: i32,
+}
+
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
+pub struct _TestValidator {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub genesis: Option<Vec<GenesisEntry>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub validator: Option<_Validator>,
+    #[serde(skip_serializing_if = "Option::is_none")]
     pub startup_wait: Option<i32>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub shutdown_wait: Option<i32>,
+}
+
+pub const STARTUP_WAIT: i32 = 5000;
+pub const SHUTDOWN_WAIT: i32 = 2000;
+
+impl From<_TestValidator> for TestValidator {
+    fn from(_test_validator: _TestValidator) -> Self {
+        Self {
+            shutdown_wait: _test_validator.shutdown_wait.unwrap_or(SHUTDOWN_WAIT),
+            startup_wait: _test_validator.startup_wait.unwrap_or(STARTUP_WAIT),
+            genesis: _test_validator.genesis,
+            validator: _test_validator.validator.map(Into::into),
+        }
+    }
+}
+
+impl From<TestValidator> for _TestValidator {
+    fn from(test_validator: TestValidator) -> Self {
+        Self {
+            shutdown_wait: Some(test_validator.shutdown_wait),
+            startup_wait: Some(test_validator.startup_wait),
+            genesis: test_validator.genesis,
+            validator: test_validator.validator.map(Into::into),
+        }
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct TestConfig {
+    pub test_suite_configs: HashMap<PathBuf, TestToml>,
+}
+
+impl Deref for TestConfig {
+    type Target = HashMap<PathBuf, TestToml>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.test_suite_configs
+    }
+}
+
+impl TestConfig {
+    pub fn discover(root: impl AsRef<Path>) -> Result<Option<Self>> {
+        let walker = WalkDir::new(root).into_iter();
+        let mut test_suite_configs = HashMap::new();
+        for entry in walker.filter_entry(|e| !is_hidden(e)) {
+            let entry = entry?;
+            if entry.file_name() == "Test.toml" {
+                let test_toml = TestToml::from_path(entry.path())?;
+                test_suite_configs.insert(entry.path().into(), test_toml);
+            }
+        }
+
+        Ok(match test_suite_configs.is_empty() {
+            true => None,
+            false => Some(Self { test_suite_configs }),
+        })
+    }
+}
+
+// This file needs to have the same (sub)structure as Anchor.toml
+// so it can be parsed as a base test file from an Anchor.toml
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct _TestToml {
+    pub extends: Option<Vec<String>>,
+    pub test: Option<_TestValidator>,
+    pub scripts: Option<ScriptsConfig>,
+}
+
+impl _TestToml {
+    fn from_path(path: impl AsRef<Path>) -> Result<Self, Error> {
+        let s = fs::read_to_string(&path)?;
+        let parsed_toml: Self = toml::from_str(&s)?;
+        let mut current_toml = _TestToml {
+            extends: None,
+            test: None,
+            scripts: None,
+        };
+        if let Some(bases) = &parsed_toml.extends {
+            for base in bases {
+                let mut canonical_base = base.clone();
+                canonical_base = canonicalize_filepath_from_origin(&canonical_base, &path)?;
+                current_toml.merge(_TestToml::from_path(&canonical_base)?);
+            }
+        }
+        current_toml.merge(parsed_toml);
+
+        if let Some(test) = &mut current_toml.test {
+            if let Some(genesis_programs) = &mut test.genesis {
+                for entry in genesis_programs {
+                    entry.program = canonicalize_filepath_from_origin(&entry.program, &path)?;
+                }
+            }
+            if let Some(validator) = &mut test.validator {
+                if let Some(ledger_dir) = &mut validator.ledger {
+                    *ledger_dir = canonicalize_filepath_from_origin(&ledger_dir, &path)?;
+                }
+                if let Some(accounts) = &mut validator.account {
+                    for entry in accounts {
+                        entry.filename = canonicalize_filepath_from_origin(&entry.filename, &path)?;
+                    }
+                }
+            }
+        }
+        Ok(current_toml)
+    }
+}
+
+/// canonicalizes the `file_path` arg.
+/// uses the `path` arg as the current dir
+/// from which to turn the relative path
+/// into a canonical one
+fn canonicalize_filepath_from_origin(
+    file_path: impl AsRef<Path>,
+    path: impl AsRef<Path>,
+) -> Result<String> {
+    let previous_dir = std::env::current_dir()?;
+    std::env::set_current_dir(path.as_ref().parent().unwrap())?;
+    let result = fs::canonicalize(file_path)?.display().to_string();
+    std::env::set_current_dir(previous_dir)?;
+    Ok(result)
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TestToml {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub test: Option<TestValidator>,
+    pub scripts: ScriptsConfig,
+}
+
+impl TestToml {
+    pub fn from_path(p: impl AsRef<Path>) -> Result<Self> {
+        WithPath::new(_TestToml::from_path(&p)?, p.as_ref().into()).try_into()
+    }
+}
+
+impl Merge for _TestToml {
+    fn merge(&mut self, other: Self) {
+        let mut my_scripts = self.scripts.take();
+        match &mut my_scripts {
+            None => my_scripts = other.scripts,
+            Some(my_scripts) => {
+                if let Some(other_scripts) = other.scripts {
+                    for (name, script) in other_scripts {
+                        my_scripts.insert(name, script);
+                    }
+                }
+            }
+        }
+
+        let mut my_test = self.test.take();
+        match &mut my_test {
+            Some(my_test) => {
+                if let Some(other_test) = other.test {
+                    if let Some(startup_wait) = other_test.startup_wait {
+                        my_test.startup_wait = Some(startup_wait);
+                    }
+                    if let Some(other_genesis) = other_test.genesis {
+                        match &mut my_test.genesis {
+                            Some(my_genesis) => {
+                                for other_entry in other_genesis {
+                                    match my_genesis
+                                        .iter()
+                                        .position(|g| *g.address == other_entry.address)
+                                    {
+                                        None => my_genesis.push(other_entry),
+                                        Some(i) => my_genesis[i] = other_entry,
+                                    }
+                                }
+                            }
+                            None => my_test.genesis = Some(other_genesis),
+                        }
+                    }
+                    let mut my_validator = my_test.validator.take();
+                    match &mut my_validator {
+                        None => my_validator = other_test.validator,
+                        Some(my_validator) => {
+                            if let Some(other_validator) = other_test.validator {
+                                my_validator.merge(other_validator)
+                            }
+                        }
+                    }
+
+                    my_test.validator = my_validator;
+                }
+            }
+            None => my_test = other.test,
+        };
+
+        // Instantiating a new Self object here ensures that
+        // this function will fail to compile if new fields get added
+        // to Self. This is useful as a reminder if they also require merging
+        *self = Self {
+            test: my_test,
+            scripts: my_scripts,
+            extends: self.extends.take(),
+        };
+    }
+}
+
+impl TryFrom<WithPath<_TestToml>> for TestToml {
+    type Error = Error;
+
+    fn try_from(mut value: WithPath<_TestToml>) -> Result<Self, Self::Error> {
+        Ok(Self {
+            test: value.test.take().map(Into::into),
+            scripts: value
+                .scripts
+                .take()
+                .ok_or_else(|| anyhow!("Missing 'scripts' section in Test.toml file."))?,
+        })
+    }
 }
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -561,13 +803,13 @@ pub struct AccountEntry {
 }
 
 #[derive(Debug, Default, Clone, Serialize, Deserialize)]
-pub struct Validator {
+pub struct _Validator {
     // Load an account from the provided JSON file
     #[serde(skip_serializing_if = "Option::is_none")]
     pub account: Option<Vec<AccountEntry>>,
     // IP address to bind the validator ports. [default: 0.0.0.0]
-    #[serde(default = "default_bind_address")]
-    pub bind_address: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub bind_address: Option<String>,
     // Copy an account from the cluster referenced by the url argument.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub clone: Option<Vec<CloneEntry>>,
@@ -575,8 +817,8 @@ pub struct Validator {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub dynamic_port_range: Option<String>,
     // Enable the faucet on this port [default: 9900].
-    #[serde(default = "default_faucet_port")]
-    pub faucet_port: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub faucet_port: Option<u16>,
     // Give the faucet address this much SOL in genesis. [default: 1000000]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub faucet_sol: Option<String>,
@@ -590,14 +832,14 @@ pub struct Validator {
     #[serde(skip_serializing_if = "Option::is_none")]
     pub url: Option<String>,
     // Use DIR as ledger location
-    #[serde(default = "default_ledger_path")]
-    pub ledger: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub ledger: Option<String>,
     // Keep this amount of shreds in root slots. [default: 10000]
     #[serde(skip_serializing_if = "Option::is_none")]
     pub limit_ledger_size: Option<String>,
     // Enable JSON RPC on this port, and the next port for the RPC websocket. [default: 8899]
-    #[serde(default = "default_rpc_port")]
-    pub rpc_port: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub rpc_port: Option<u16>,
     // Override the number of slots in an epoch.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub slots_per_epoch: Option<String>,
@@ -606,20 +848,148 @@ pub struct Validator {
     pub warp_slot: Option<String>,
 }
 
-fn default_ledger_path() -> String {
-    ".anchor/test-ledger".to_string()
+#[derive(Debug, Default, Clone, Serialize, Deserialize)]
+pub struct Validator {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub account: Option<Vec<AccountEntry>>,
+    pub bind_address: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub clone: Option<Vec<CloneEntry>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub dynamic_port_range: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub faucet_port: Option<u16>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub faucet_sol: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gossip_host: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub gossip_port: Option<u16>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub url: Option<String>,
+    pub ledger: String,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub limit_ledger_size: Option<String>,
+    pub rpc_port: u16,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub slots_per_epoch: Option<String>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub warp_slot: Option<String>,
 }
 
-fn default_bind_address() -> String {
-    "0.0.0.0".to_string()
+impl From<_Validator> for Validator {
+    fn from(_validator: _Validator) -> Self {
+        Self {
+            account: _validator.account,
+            bind_address: _validator
+                .bind_address
+                .unwrap_or_else(|| DEFAULT_BIND_ADDRESS.to_string()),
+            clone: _validator.clone,
+            dynamic_port_range: _validator.dynamic_port_range,
+            faucet_port: _validator.faucet_port,
+            faucet_sol: _validator.faucet_sol,
+            gossip_host: _validator.gossip_host,
+            gossip_port: _validator.gossip_port,
+            url: _validator.url,
+            ledger: _validator
+                .ledger
+                .unwrap_or_else(|| DEFAULT_LEDGER_PATH.to_string()),
+            limit_ledger_size: _validator.limit_ledger_size,
+            rpc_port: _validator
+                .rpc_port
+                .unwrap_or(solana_sdk::rpc_port::DEFAULT_RPC_PORT),
+            slots_per_epoch: _validator.slots_per_epoch,
+            warp_slot: _validator.warp_slot,
+        }
+    }
 }
 
-pub fn default_rpc_port() -> u16 {
-    solana_sdk::rpc_port::DEFAULT_RPC_PORT
+impl From<Validator> for _Validator {
+    fn from(validator: Validator) -> Self {
+        Self {
+            account: validator.account,
+            bind_address: Some(validator.bind_address),
+            clone: validator.clone,
+            dynamic_port_range: validator.dynamic_port_range,
+            faucet_port: validator.faucet_port,
+            faucet_sol: validator.faucet_sol,
+            gossip_host: validator.gossip_host,
+            gossip_port: validator.gossip_port,
+            url: validator.url,
+            ledger: Some(validator.ledger),
+            limit_ledger_size: validator.limit_ledger_size,
+            rpc_port: Some(validator.rpc_port),
+            slots_per_epoch: validator.slots_per_epoch,
+            warp_slot: validator.warp_slot,
+        }
+    }
 }
 
-pub fn default_faucet_port() -> u16 {
-    solana_faucet::faucet::FAUCET_PORT
+const DEFAULT_LEDGER_PATH: &str = ".anchor/test-ledger";
+const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0";
+
+impl Merge for _Validator {
+    fn merge(&mut self, other: Self) {
+        // Instantiating a new Self object here ensures that
+        // this function will fail to compile if new fields get added
+        // to Self. This is useful as a reminder if they also require merging
+        *self = Self {
+            account: match self.account.take() {
+                None => other.account,
+                Some(mut entries) => match other.account {
+                    None => Some(entries),
+                    Some(other_entries) => {
+                        for other_entry in other_entries {
+                            match entries
+                                .iter()
+                                .position(|my_entry| *my_entry.address == other_entry.address)
+                            {
+                                None => entries.push(other_entry),
+                                Some(i) => entries[i] = other_entry,
+                            };
+                        }
+                        Some(entries)
+                    }
+                },
+            },
+            bind_address: other.bind_address.or_else(|| self.bind_address.take()),
+            clone: match self.clone.take() {
+                None => other.clone,
+                Some(mut entries) => match other.clone {
+                    None => Some(entries),
+                    Some(other_entries) => {
+                        for other_entry in other_entries {
+                            match entries
+                                .iter()
+                                .position(|my_entry| *my_entry.address == other_entry.address)
+                            {
+                                None => entries.push(other_entry),
+                                Some(i) => entries[i] = other_entry,
+                            };
+                        }
+                        Some(entries)
+                    }
+                },
+            },
+            dynamic_port_range: other
+                .dynamic_port_range
+                .or_else(|| self.dynamic_port_range.take()),
+            faucet_port: other.faucet_port.or_else(|| self.faucet_port.take()),
+            faucet_sol: other.faucet_sol.or_else(|| self.faucet_sol.take()),
+            gossip_host: other.gossip_host.or_else(|| self.gossip_host.take()),
+            gossip_port: other.gossip_port.or_else(|| self.gossip_port.take()),
+            url: other.url.or_else(|| self.url.take()),
+            ledger: other.ledger.or_else(|| self.ledger.take()),
+            limit_ledger_size: other
+                .limit_ledger_size
+                .or_else(|| self.limit_ledger_size.take()),
+            rpc_port: other.rpc_port.or_else(|| self.rpc_port.take()),
+            slots_per_epoch: other
+                .slots_per_epoch
+                .or_else(|| self.slots_per_epoch.take()),
+            warp_slot: other.warp_slot.or_else(|| self.warp_slot.take()),
+        };
+    }
 }
 
 #[derive(Debug, Clone)]

+ 171 - 110
cli/src/lib.rs

@@ -1,6 +1,6 @@
 use crate::config::{
     AnchorPackage, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest, ProgramDeployment,
-    ProgramWorkspace, Test, WithPath,
+    ProgramWorkspace, ScriptsConfig, TestValidator, WithPath, SHUTDOWN_WAIT, STARTUP_WAIT,
 };
 use anchor_client::Cluster;
 use anchor_lang::idl::{IdlAccount, IdlInstruction};
@@ -8,7 +8,6 @@ use anchor_lang::{AccountDeserialize, AnchorDeserialize, AnchorSerialize};
 use anchor_syn::idl::Idl;
 use anyhow::{anyhow, Context, Result};
 use clap::Parser;
-use config::{default_faucet_port, default_rpc_port};
 use flate2::read::GzDecoder;
 use flate2::read::ZlibDecoder;
 use flate2::write::{GzEncoder, ZlibEncoder};
@@ -1278,7 +1277,7 @@ fn verify(
         .join("target/verifiable/")
         .join(format!("{}.so", binary_name));
 
-    let url = cluster_url(&cfg);
+    let url = cluster_url(&cfg, &cfg.test_validator);
     let bin_ver = verify_bin(program_id, &bin_path, &url)?;
     if !bin_ver.is_verified {
         println!("Error: Binaries don't match");
@@ -1424,7 +1423,7 @@ pub enum BinVerificationState {
 // Fetches an IDL for the given program_id.
 fn fetch_idl(cfg_override: &ConfigOverride, idl_addr: Pubkey) -> Result<Idl> {
     let url = match Config::discover(cfg_override)? {
-        Some(cfg) => cluster_url(&cfg),
+        Some(cfg) => cluster_url(&cfg, &cfg.test_validator),
         None => {
             // If the command is not run inside a workspace,
             // cluster_url will be used from default solana config
@@ -1538,7 +1537,7 @@ fn idl_set_buffer(cfg_override: &ConfigOverride, program_id: Pubkey, buffer: Pub
     with_workspace(cfg_override, |cfg| {
         let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string())
             .map_err(|_| anyhow!("Unable to read keypair file"))?;
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let client = RpcClient::new(url);
 
         // Instruction to set the buffer onto the IdlAccount.
@@ -1591,7 +1590,7 @@ fn idl_upgrade(
 
 fn idl_authority(cfg_override: &ConfigOverride, program_id: Pubkey) -> Result<()> {
     with_workspace(cfg_override, |cfg| {
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let client = RpcClient::new(url);
         let idl_address = {
             let account = client
@@ -1629,7 +1628,7 @@ fn idl_set_authority(
         };
         let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string())
             .map_err(|_| anyhow!("Unable to read keypair file"))?;
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let client = RpcClient::new(url);
 
         // Instruction data.
@@ -1700,7 +1699,7 @@ fn idl_write(cfg: &Config, program_id: &Pubkey, idl: &Idl, idl_address: Pubkey)
     // Misc.
     let keypair = solana_sdk::signature::read_keypair_file(&cfg.provider.wallet.to_string())
         .map_err(|_| anyhow!("Unable to read keypair file"))?;
-    let url = cluster_url(cfg);
+    let url = cluster_url(cfg, &cfg.test_validator);
     let client = RpcClient::new(url);
 
     // Serialize and compress the idl.
@@ -1841,95 +1840,156 @@ fn test(
         if (!is_localnet || skip_local_validator) && !skip_deploy {
             deploy(cfg_override, None)?;
         }
-        // Start local test validator, if needed.
-        let mut validator_handle = None;
-        if is_localnet && (!skip_local_validator) {
-            let flags = match skip_deploy {
-                true => None,
-                false => Some(validator_flags(cfg)?),
-            };
-            validator_handle = Some(start_test_validator(cfg, flags, true)?);
+        let mut is_first_suite = true;
+        if cfg.scripts.get("test").is_some() {
+            is_first_suite = false;
+            println!("\nFound a 'test' script in the Anchor.toml. Running it as a test suite!");
+            run_test_suite(
+                cfg.path(),
+                cfg,
+                is_localnet,
+                skip_local_validator,
+                skip_deploy,
+                detach,
+                &cfg.test_validator,
+                &cfg.scripts,
+                &extra_args,
+            )?;
         }
+        if let Some(test_config) = &cfg.test_config {
+            for test_suite in test_config.iter() {
+                if !is_first_suite {
+                    std::thread::sleep(std::time::Duration::from_millis(
+                        test_suite
+                            .1
+                            .test
+                            .as_ref()
+                            .map(|val| val.shutdown_wait)
+                            .unwrap_or(SHUTDOWN_WAIT) as u64,
+                    ));
+                } else {
+                    is_first_suite = false;
+                }
 
-        let url = cluster_url(cfg);
+                run_test_suite(
+                    test_suite.0,
+                    cfg,
+                    is_localnet,
+                    skip_local_validator,
+                    skip_deploy,
+                    detach,
+                    &test_suite.1.test,
+                    &test_suite.1.scripts,
+                    &extra_args,
+                )?;
+            }
+        }
+        Ok(())
+    })
+}
 
-        let node_options = format!(
-            "{} {}",
-            match std::env::var_os("NODE_OPTIONS") {
-                Some(value) => value
-                    .into_string()
-                    .map_err(std::env::VarError::NotUnicode)?,
-                None => "".to_owned(),
-            },
-            get_node_dns_option()?,
-        );
+#[allow(clippy::too_many_arguments)]
+fn run_test_suite(
+    test_suite_path: impl AsRef<Path>,
+    cfg: &WithPath<Config>,
+    is_localnet: bool,
+    skip_local_validator: bool,
+    skip_deploy: bool,
+    detach: bool,
+    test_validator: &Option<TestValidator>,
+    scripts: &ScriptsConfig,
+    extra_args: &[String],
+) -> Result<()> {
+    println!("\nRunning test suite: {:#?}\n", test_suite_path.as_ref());
+    // Start local test validator, if needed.
+    let mut validator_handle = None;
+    if is_localnet && (!skip_local_validator) {
+        let flags = match skip_deploy {
+            true => None,
+            false => Some(validator_flags(cfg, test_validator)?),
+        };
+        validator_handle = Some(start_test_validator(cfg, test_validator, flags, true)?);
+    }
 
-        // Setup log reader.
-        let log_streams = stream_logs(cfg, &url);
+    let url = cluster_url(cfg, test_validator);
 
-        // Run the tests.
-        let test_result: Result<_> = {
-            let cmd = cfg
-                .scripts
-                .get("test")
-                .expect("Not able to find command for `test`")
-                .clone();
-            let mut args: Vec<&str> = cmd
-                .split(' ')
-                .chain(extra_args.iter().map(|arg| arg.as_str()))
-                .collect();
-            let program = args.remove(0);
+    let node_options = format!(
+        "{} {}",
+        match std::env::var_os("NODE_OPTIONS") {
+            Some(value) => value
+                .into_string()
+                .map_err(std::env::VarError::NotUnicode)?,
+            None => "".to_owned(),
+        },
+        get_node_dns_option()?,
+    );
 
-            std::process::Command::new(program)
-                .args(args)
-                .env("ANCHOR_PROVIDER_URL", url)
-                .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
-                .env("NODE_OPTIONS", node_options)
-                .stdout(Stdio::inherit())
-                .stderr(Stdio::inherit())
-                .output()
-                .map_err(anyhow::Error::from)
-                .context(cmd)
-        };
+    // Setup log reader.
+    let log_streams = stream_logs(cfg, &url);
+
+    // Run the tests.
+    let test_result: Result<_> = {
+        let cmd = scripts
+            .get("test")
+            .expect("Not able to find script for `test`")
+            .clone();
+        let mut args: Vec<&str> = cmd
+            .split(' ')
+            .chain(extra_args.iter().map(|arg| arg.as_str()))
+            .collect();
+        let program = args.remove(0);
+
+        std::process::Command::new(program)
+            .args(args)
+            .env("ANCHOR_PROVIDER_URL", url)
+            .env("ANCHOR_WALLET", cfg.provider.wallet.to_string())
+            .env("NODE_OPTIONS", node_options)
+            .stdout(Stdio::inherit())
+            .stderr(Stdio::inherit())
+            .output()
+            .map_err(anyhow::Error::from)
+            .context(cmd)
+    };
 
-        // Keep validator running if needed.
-        if test_result.is_ok() && detach {
-            println!("Local validator still running. Press Ctrl + C quit.");
-            std::io::stdin().lock().lines().next().unwrap().unwrap();
-        }
+    // Keep validator running if needed.
+    if test_result.is_ok() && detach {
+        println!("Local validator still running. Press Ctrl + C quit.");
+        std::io::stdin().lock().lines().next().unwrap().unwrap();
+    }
 
-        // Check all errors and shut down.
-        if let Some(mut child) = validator_handle {
-            if let Err(err) = child.kill() {
-                println!("Failed to kill subprocess {}: {}", child.id(), err);
-            }
+    // Check all errors and shut down.
+    if let Some(mut child) = validator_handle {
+        if let Err(err) = child.kill() {
+            println!("Failed to kill subprocess {}: {}", child.id(), err);
         }
-        for mut child in log_streams? {
-            if let Err(err) = child.kill() {
-                println!("Failed to kill subprocess {}: {}", child.id(), err);
-            }
+    }
+    for mut child in log_streams? {
+        if let Err(err) = child.kill() {
+            println!("Failed to kill subprocess {}: {}", child.id(), err);
         }
+    }
 
-        // Must exist *after* shutting down the validator and log streams.
-        match test_result {
-            Ok(exit) => {
-                if !exit.status.success() {
-                    std::process::exit(exit.status.code().unwrap());
-                }
-            }
-            Err(err) => {
-                println!("Failed to run test: {:#}", err)
+    // Must exist *after* shutting down the validator and log streams.
+    match test_result {
+        Ok(exit) => {
+            if !exit.status.success() {
+                std::process::exit(exit.status.code().unwrap());
             }
         }
+        Err(err) => {
+            println!("Failed to run test: {:#}", err)
+        }
+    }
 
-        Ok(())
-    })
+    Ok(())
 }
-
 // Returns the solana-test-validator flags. This will embed the workspace
 // programs in the genesis block so we don't have to deploy every time. It also
 // allows control of other solana-test-validator features.
-fn validator_flags(cfg: &WithPath<Config>) -> Result<Vec<String>> {
+fn validator_flags(
+    cfg: &WithPath<Config>,
+    test_validator: &Option<TestValidator>,
+) -> Result<Vec<String>> {
     let programs = cfg.programs.get(&Cluster::Localnet);
 
     let mut flags = Vec::new();
@@ -1959,7 +2019,7 @@ fn validator_flags(cfg: &WithPath<Config>) -> Result<Vec<String>> {
         }
     }
 
-    if let Some(test) = cfg.test.as_ref() {
+    if let Some(test) = test_validator.as_ref() {
         if let Some(genesis) = &test.genesis {
             for entry in genesis {
                 let program_path = Path::new(&entry.program);
@@ -2090,7 +2150,7 @@ fn stream_logs(config: &WithPath<Config>, rpc_url: &str) -> Result<Vec<std::proc
             .spawn()?;
         handles.push(child);
     }
-    if let Some(test) = config.test.as_ref() {
+    if let Some(test) = config.test_validator.as_ref() {
         if let Some(genesis) = &test.genesis {
             for entry in genesis {
                 let log_file = File::create(format!("{}/{}.log", program_logs_dir, entry.address))?;
@@ -2117,11 +2177,13 @@ pub struct IdlTestMetadata {
 
 fn start_test_validator(
     cfg: &Config,
+    test_validator: &Option<TestValidator>,
     flags: Option<Vec<String>>,
     test_log_stdout: bool,
 ) -> Result<Child> {
     //
-    let (test_ledger_directory, test_ledger_log_filename) = test_validator_file_paths(cfg);
+    let (test_ledger_directory, test_ledger_log_filename) =
+        test_validator_file_paths(test_validator);
 
     // Start a validator for testing.
     let (test_validator_stdout, test_validator_stderr) = match test_log_stdout {
@@ -2136,23 +2198,23 @@ fn start_test_validator(
         false => (Stdio::inherit(), Stdio::inherit()),
     };
 
-    let rpc_url = test_validator_rpc_url(cfg);
+    let rpc_url = test_validator_rpc_url(test_validator);
 
     let rpc_port = cfg
-        .test
+        .test_validator
         .as_ref()
         .and_then(|test| test.validator.as_ref().map(|v| v.rpc_port))
-        .unwrap_or_else(default_rpc_port);
+        .unwrap_or(solana_sdk::rpc_port::DEFAULT_RPC_PORT);
     if !portpicker::is_free(rpc_port) {
         return Err(anyhow!(
             "Your configured rpc port: {rpc_port} is already in use"
         ));
     }
     let faucet_port = cfg
-        .test
+        .test_validator
         .as_ref()
-        .and_then(|test| test.validator.as_ref().map(|v| v.faucet_port))
-        .unwrap_or_else(default_faucet_port);
+        .and_then(|test| test.validator.as_ref().and_then(|v| v.faucet_port))
+        .unwrap_or(solana_faucet::faucet::FAUCET_PORT);
     if !portpicker::is_free(faucet_port) {
         return Err(anyhow!(
             "Your configured faucet port: {faucet_port} is already in use"
@@ -2173,11 +2235,10 @@ fn start_test_validator(
     // Wait for the validator to be ready.
     let client = RpcClient::new(rpc_url);
     let mut count = 0;
-    let ms_wait = cfg
-        .test
+    let ms_wait = test_validator
         .as_ref()
-        .and_then(|test| test.startup_wait)
-        .unwrap_or(5_000);
+        .map(|test| test.startup_wait)
+        .unwrap_or(STARTUP_WAIT);
     while count < ms_wait {
         let r = client.get_latest_blockhash();
         if r.is_ok() {
@@ -2199,9 +2260,9 @@ fn start_test_validator(
 
 // Return the URL that solana-test-validator should be running on given the
 // configuration
-fn test_validator_rpc_url(cfg: &Config) -> String {
-    match &cfg.test.as_ref() {
-        Some(Test {
+fn test_validator_rpc_url(test_validator: &Option<TestValidator>) -> String {
+    match test_validator {
+        Some(TestValidator {
             validator: Some(validator),
             ..
         }) => format!("http://{}:{}", validator.bind_address, validator.rpc_port),
@@ -2211,9 +2272,9 @@ fn test_validator_rpc_url(cfg: &Config) -> String {
 
 // Setup and return paths to the solana-test-validator ledger directory and log
 // files given the configuration
-fn test_validator_file_paths(cfg: &Config) -> (String, String) {
-    let ledger_directory = match &cfg.test.as_ref() {
-        Some(Test {
+fn test_validator_file_paths(test_validator: &Option<TestValidator>) -> (String, String) {
+    let ledger_directory = match test_validator {
+        Some(TestValidator {
             validator: Some(validator),
             ..
         }) => &validator.ledger,
@@ -2238,12 +2299,12 @@ fn test_validator_file_paths(cfg: &Config) -> (String, String) {
     )
 }
 
-fn cluster_url(cfg: &Config) -> String {
+fn cluster_url(cfg: &Config, test_validator: &Option<TestValidator>) -> String {
     let is_localnet = cfg.provider.cluster == Cluster::Localnet;
     match is_localnet {
         // Cluster is Localnet, assume the intent is to use the configuration
         // for solana-test-validator
-        true => test_validator_rpc_url(cfg),
+        true => test_validator_rpc_url(test_validator),
         false => cfg.provider.cluster.url().to_string(),
     }
 }
@@ -2278,7 +2339,7 @@ fn clean(cfg_override: &ConfigOverride) -> Result<()> {
 
 fn deploy(cfg_override: &ConfigOverride, program_str: Option<String>) -> Result<()> {
     with_workspace(cfg_override, |cfg| {
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let keypair = cfg.provider.wallet.to_string();
 
         // Deploy the programs.
@@ -2352,7 +2413,7 @@ fn upgrade(
     let program_filepath = path.canonicalize()?.display().to_string();
 
     with_workspace(cfg_override, |cfg| {
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let exit = std::process::Command::new("solana")
             .arg("program")
             .arg("deploy")
@@ -2385,7 +2446,7 @@ fn create_idl_account(
     let idl_address = IdlAccount::address(program_id);
     let keypair = solana_sdk::signature::read_keypair_file(keypair_path)
         .map_err(|_| anyhow!("Unable to read keypair file"))?;
-    let url = cluster_url(cfg);
+    let url = cluster_url(cfg, &cfg.test_validator);
     let client = RpcClient::new(url);
     let idl_data = serialize_idl(idl)?;
 
@@ -2439,7 +2500,7 @@ fn create_idl_buffer(
 ) -> Result<Pubkey> {
     let keypair = solana_sdk::signature::read_keypair_file(keypair_path)
         .map_err(|_| anyhow!("Unable to read keypair file"))?;
-    let url = cluster_url(cfg);
+    let url = cluster_url(cfg, &cfg.test_validator);
     let client = RpcClient::new(url);
 
     let buffer = Keypair::generate(&mut OsRng);
@@ -2513,7 +2574,7 @@ fn migrate(cfg_override: &ConfigOverride) -> Result<()> {
     with_workspace(cfg_override, |cfg| {
         println!("Running migration deploy script");
 
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let cur_dir = std::env::current_dir()?;
 
         let use_ts =
@@ -2669,7 +2730,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
                     .collect::<Vec<ProgramWorkspace>>(),
             }
         };
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let js_code = template::node_shell(&url, &cfg.provider.wallet.to_string(), programs)?;
         let mut child = std::process::Command::new("node")
             .args(&["-e", &js_code, "-i", "--experimental-repl-await"])
@@ -2688,7 +2749,7 @@ fn shell(cfg_override: &ConfigOverride) -> Result<()> {
 
 fn run(cfg_override: &ConfigOverride, script: String) -> Result<()> {
     with_workspace(cfg_override, |cfg| {
-        let url = cluster_url(cfg);
+        let url = cluster_url(cfg, &cfg.test_validator);
         let script = cfg
             .scripts
             .get(&script)
@@ -2955,13 +3016,13 @@ fn localnet(
 
         let flags = match skip_deploy {
             true => None,
-            false => Some(validator_flags(cfg)?),
+            false => Some(validator_flags(cfg, &cfg.test_validator)?),
         };
 
-        let validator_handle = &mut start_test_validator(cfg, flags, false)?;
+        let validator_handle = &mut start_test_validator(cfg, &cfg.test_validator, flags, false)?;
 
         // Setup log reader.
-        let url = test_validator_rpc_url(cfg);
+        let url = test_validator_rpc_url(&cfg.test_validator);
         let log_streams = stream_logs(cfg, &url);
 
         std::io::stdin().lock().lines().next().unwrap().unwrap();

+ 18 - 0
tests/multiple-suites/Anchor.toml

@@ -0,0 +1,18 @@
+[features]
+seeds = false
+[programs.localnet]
+multiple_suites = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
+
+[registry]
+url = "https://anchor.projectserum.com"
+
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"
+
+[test]
+startup_wait = 20000
+
+[[test.validator.account]]
+address = "C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw"
+filename = "./tests/accounts/SOME_ACCOUNT.json"

+ 4 - 0
tests/multiple-suites/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 12 - 0
tests/multiple-suites/migrations/deploy.ts

@@ -0,0 +1,12 @@
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+};

+ 19 - 0
tests/multiple-suites/package.json

@@ -0,0 +1,19 @@
+{
+    "name": "multiple-suites",
+    "version": "0.23.0",
+    "license": "(MIT OR Apache-2.0)",
+    "homepage": "https://github.com/project-serum/anchor#readme",
+    "bugs": {
+      "url": "https://github.com/project-serum/anchor/issues"
+    },
+    "repository": {
+      "type": "git",
+      "url": "https://github.com/project-serum/anchor.git"
+    },
+    "engines": {
+      "node": ">=11"
+    },
+    "scripts": {
+      "test": "anchor test"
+    }
+  }

+ 19 - 0
tests/multiple-suites/programs/multiple-suites/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "multiple-suites"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "multiple_suites"
+
+[features]
+no-entrypoint = []
+no-idl = []
+no-log-ix-name = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = "0.23.0"

+ 2 - 0
tests/multiple-suites/programs/multiple-suites/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 16 - 0
tests/multiple-suites/programs/multiple-suites/src/lib.rs

@@ -0,0 +1,16 @@
+use anchor_lang::prelude::*;
+
+declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS");
+
+#[program]
+pub mod multiple_suites {
+    use super::*;
+
+    // _val to ensure tx are different so they don't get rejected.
+    pub fn initialize(_ctx: Context<Initialize>, _val: u64) -> Result<()> {
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct Initialize {}

+ 5 - 0
tests/multiple-suites/tests/Test.base.toml

@@ -0,0 +1,5 @@
+extends = ["./Test.root.base.toml"]
+
+[[test.validator.account]]
+address = "C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw"
+filename = "./accounts/SOME_TOKEN.json"

+ 12 - 0
tests/multiple-suites/tests/Test.root.base.toml

@@ -0,0 +1,12 @@
+[test]
+startup_wait = 20000
+
+[test.validator]
+url = "https://api.mainnet-beta.solana.com"
+
+[[test.validator.clone]]
+address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
+
+[[test.validator.account]]
+address = "JC7Vcye5upE6tMLAjAem76MCGuPNidTtg2cuYm71UukH"
+filename = "./accounts/ANOTHER_ACC.json"

+ 13 - 0
tests/multiple-suites/tests/accounts/ANOTHER_ACC.json

@@ -0,0 +1,13 @@
+{
+  "pubkey": "JC7Vcye5upE6tMLAjAem76MCGuPNidTtg2cuYm71UukH",
+  "account": {
+    "lamports": 1141440,
+    "data": [
+      "AgAAALd5VermN3GAImByma8xRkcfPsbJkljqjdkZwDg8igZy",
+      "base64"
+    ],
+    "owner": "BPFLoaderUpgradeab1e11111111111111111111111",
+    "executable": true,
+    "rentEpoch": 211
+  }
+}

+ 13 - 0
tests/multiple-suites/tests/accounts/SOME_ACCOUNT.json

@@ -0,0 +1,13 @@
+{
+  "pubkey": "3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj",
+  "account": {
+    "lamports": 7906560,
+    "data": [
+      "oZzT/fpANfoAAAacjYHHz87iGrP2++EgvfsQjuGvj1dqAHoCKU9FPDLg9bWjnUCkIZgQQSmJpClN5jbVq6JbdM4Ec4ZpEb2ViTzO//sSNXmZhK5OA5kiKWvPWoUsXExQpNgiaEeW8MWANStiNrxKs/p6yWv9cqbJBd6AygqKx0Y7WysO04FMaSFa/6o5Lt/NyPH8jkM12IYJcvGzIQIbaXRo9eDbJOcPskyugDoJAAAAAAAAAAAAAAAAAAAAAAAAAAAAFQMAAAAAAAB4AQAAAAAAAHIDAAAAAAAAcgMAAAAAAAABAAAAAAAAAAjqplnVZa4ZML82vxalcFuNRlmUiSpI+ZGRVoMP+eF1hDOHYqnPMZXS8rUwGGcq0p98MYBGv4V+9NsD84s31F0BRTMAAAAAAAAAAACAOgkAAAAAAPySPwEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8Cd4FAAAAAL4E7wIAAAAA6pstAQAAAAAAAR0AAAAAALHFQ2IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAabiFf+q4GE+2h/Y0YYwDXaxDncGus7VZig8AAAAAABb8MVpWsIyg7EhPN8Vhh8GnNx3+zvD0Kz0wqIECi79vwBAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
+      "base64"
+    ],
+    "owner": "7NryUmAmy8hMbawRNUs9UdkgAGEiHFxghSfve31dC7xh",
+    "executable": false,
+    "rentEpoch": 292
+  }
+}

+ 13 - 0
tests/multiple-suites/tests/accounts/SOME_TOKEN.json

@@ -0,0 +1,13 @@
+{
+  "pubkey": "C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw",
+  "account": {
+    "lamports": 839123197950,
+    "data": [
+      "AAAAAFnkpzMo+KIHXFu0C7POimfWZAwz81Y+ImohwO+lC39o2tFLxYmAIwAGAQAAAABZ5KczKPiiB1xbtAuzzopn1mQMM/NWPiJqIcDvpQt/aA==",
+      "base64"
+    ],
+    "owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
+    "executable": false,
+    "rentEpoch": 292
+  }
+}

+ 4 - 0
tests/multiple-suites/tests/another-suite/Test.toml

@@ -0,0 +1,4 @@
+extends = ["../Test.base.toml"]
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/another-suite/**/*.ts"

+ 44 - 0
tests/multiple-suites/tests/another-suite/another-suite.ts

@@ -0,0 +1,44 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { assert } from "chai";
+import { MultipleSuites } from "../../target/types/multiple_suites";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(100000), {});
+
+    // SOME_TOKEN.json should exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should NOT exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    // ANOTHER_ACC.json should exist.
+    const ANOTHER_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("JC7Vcye5upE6tMLAjAem76MCGuPNidTtg2cuYm71UukH")
+    );
+
+    // CLONED ACC should exist.
+    const CLONED_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")
+    );
+
+    assert.isNotNull(SOME_TOKEN);
+    assert.isNull(SOME_ACCOUNT);
+    assert.isNotNull(ANOTHER_ACC);
+    assert.isNotNull(CLONED_ACC);
+
+    console.log("Your transaction signature", tx);
+  });
+});

+ 4 - 0
tests/multiple-suites/tests/fourth-and-fifth-suite/fifth-suite/Test.toml

@@ -0,0 +1,4 @@
+extends = ["../../Test.base.toml"]
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/fourth-and-fifth-suite/fifth-suite/**/*.ts"

+ 43 - 0
tests/multiple-suites/tests/fourth-and-fifth-suite/fifth-suite/fifthSuite.ts

@@ -0,0 +1,43 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { assert } from "chai";
+import { MultipleSuites } from "../../../target/types/multiple_suites";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(4389242), {});
+
+    // SOME_TOKEN.json should exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should NOT exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    // ANOTHER_ACC.json should exist.
+    const ANOTHER_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("JC7Vcye5upE6tMLAjAem76MCGuPNidTtg2cuYm71UukH")
+    );
+
+    // CLONED ACC should exist.
+    const CLONED_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")
+    );
+
+    assert.isNotNull(SOME_TOKEN);
+    assert.isNull(SOME_ACCOUNT);
+    assert.isNotNull(ANOTHER_ACC);
+    assert.isNotNull(CLONED_ACC);
+    console.log("Your transaction signature", tx);
+  });
+});

+ 4 - 0
tests/multiple-suites/tests/fourth-and-fifth-suite/forth-suite/Test.toml

@@ -0,0 +1,4 @@
+extends = ["../../Test.base.toml"]
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/fourth-and-fifth-suite/forth-suite/**/*.ts"

+ 43 - 0
tests/multiple-suites/tests/fourth-and-fifth-suite/forth-suite/forth-suite.ts

@@ -0,0 +1,43 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { assert } from "chai";
+import { MultipleSuites } from "../../../target/types/multiple_suites";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(34823), {});
+
+    // SOME_TOKEN.json should exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should NOT exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    // ANOTHER_ACC.json should exist.
+    const ANOTHER_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("JC7Vcye5upE6tMLAjAem76MCGuPNidTtg2cuYm71UukH")
+    );
+
+    // CLONED ACC should exist.
+    const CLONED_ACC = await program.provider.connection.getAccountInfo(
+      new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s")
+    );
+
+    assert.isNotNull(SOME_TOKEN);
+    assert.isNull(SOME_ACCOUNT);
+    assert.isNotNull(ANOTHER_ACC);
+    assert.isNotNull(CLONED_ACC);
+    console.log("Your transaction signature", tx);
+  });
+});

+ 4 - 0
tests/multiple-suites/tests/multiple-suites/Test.toml

@@ -0,0 +1,4 @@
+extends = ["../../Anchor.toml"]
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/multiple-suites/**/*.ts"

+ 32 - 0
tests/multiple-suites/tests/multiple-suites/multiple-suites.ts

@@ -0,0 +1,32 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { assert } from "chai";
+import { MultipleSuites } from "../../target/types/multiple_suites";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(2394832), {});
+
+    // SOME_TOKEN.json should NOT exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    assert.isNull(SOME_TOKEN);
+    assert.isNotNull(SOME_ACCOUNT);
+
+    console.log("Your transaction signature", tx);
+  });
+});

+ 4 - 0
tests/multiple-suites/tests/third-suite/Test.toml

@@ -0,0 +1,4 @@
+extends = ["../Test.base.toml"]
+
+[scripts]
+test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/third-suite/**/*.ts"

+ 32 - 0
tests/multiple-suites/tests/third-suite/sub-dir-one/subDirOne.ts

@@ -0,0 +1,32 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { MultipleSuites } from "../../../target/types/multiple_suites";
+import { assert } from "chai";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(347234), {});
+
+    // SOME_TOKEN.json should exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should NOT exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    assert.isNotNull(SOME_TOKEN);
+    assert.isNull(SOME_ACCOUNT);
+
+    console.log("Your transaction signature", tx);
+  });
+});

+ 32 - 0
tests/multiple-suites/tests/third-suite/sub-dir-two/subDirTwo.ts

@@ -0,0 +1,32 @@
+import * as anchor from "@project-serum/anchor";
+import { Program } from "@project-serum/anchor";
+import { PublicKey } from "@solana/web3.js";
+import { assert } from "chai";
+import { MultipleSuites } from "../../../target/types/multiple_suites";
+
+describe("multiple-suites", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.MultipleSuites as Program<MultipleSuites>;
+
+  it("Is initialized!", async () => {
+    // Add your test here.
+    const tx = await program.rpc.initialize(new anchor.BN(9348239), {});
+
+    // SOME_TOKEN.json should exist.
+    const SOME_TOKEN = await program.provider.connection.getAccountInfo(
+      new PublicKey("C4XeBpzX4tDjGV1gkLsj7jJh6XHunVqAykANWCfTLszw")
+    );
+
+    // SOME_ACCOUNT.json should NOT exist.
+    const SOME_ACCOUNT = await program.provider.connection.getAccountInfo(
+      new PublicKey("3vMPj13emX9JmifYcWc77ekEzV1F37ga36E1YeSr6Mdj")
+    );
+
+    assert.isNotNull(SOME_TOKEN);
+    assert.isNull(SOME_ACCOUNT);
+
+    console.log("Your transaction signature", tx);
+  });
+});

+ 10 - 0
tests/multiple-suites/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2015"],
+    "module": "commonjs",
+    "target": "es6",
+    "esModuleInterop": true
+  }
+}

+ 2 - 1
tests/package.json

@@ -32,7 +32,8 @@
     "validator-clone",
     "zero-copy",
     "declare-id",
-    "cpi-returns"
+    "cpi-returns",
+    "multiple-suites"
   ],
   "dependencies": {
     "@project-serum/anchor": "^0.23.0",