Pārlūkot izejas kodu

cli: Add Rust test template (#2805)

Aoi Kurokawa 1 gadu atpakaļ
vecāks
revīzija
8eee184938
3 mainītis faili ar 232 papildinājumiem un 59 dzēšanām
  1. 1 0
      CHANGELOG.md
  2. 47 56
      cli/src/lib.rs
  3. 184 3
      cli/src/rust_template.rs

+ 1 - 0
CHANGELOG.md

@@ -22,6 +22,7 @@ The minor version will be incremented upon a breaking change and the patch versi
 - ts: Add missing IDL PDA seed types ([#2752](https://github.com/coral-xyz/anchor/pull/2752)).
 - cli: `idl close` accepts optional `--idl-address` parameter ([#2760](https://github.com/coral-xyz/anchor/pull/2760)).
 - cli: Add support for simple wildcard patterns in Anchor.toml's `workspace.members` and `workspace.exclude`. ([#2785](https://github.com/coral-xyz/anchor/pull/2785)).
+- cli: Add new `test-template` option in `init` command  ([#2680](https://github.com/coral-xyz/anchor/issues/2680)).
 - cli: `anchor test` is able to run multiple commands ([#2799](https://github.com/coral-xyz/anchor/pull/2799)).
 - cli: Check `@coral-xyz/anchor` package and CLI version compatibility ([#2813](https://github.com/coral-xyz/anchor/pull/2813)).
 

+ 47 - 56
cli/src/lib.rs

@@ -22,7 +22,7 @@ use heck::{ToKebabCase, ToSnakeCase};
 use regex::{Regex, RegexBuilder};
 use reqwest::blocking::multipart::{Form, Part};
 use reqwest::blocking::Client;
-use rust_template::ProgramTemplate;
+use rust_template::{ProgramTemplate, TestTemplate};
 use semver::{Version, VersionReq};
 use serde::{Deserialize, Serialize};
 use serde_json::{json, Map, Value as JsonValue};
@@ -83,12 +83,12 @@ pub enum Command {
         /// Don't initialize git
         #[clap(long)]
         no_git: bool,
-        /// Use `jest` instead of `mocha` for tests
-        #[clap(long)]
-        jest: bool,
         /// Rust program template to use
         #[clap(value_enum, short, long, default_value = "single")]
         template: ProgramTemplate,
+        /// Test template to use
+        #[clap(value_enum, long, default_value = "mocha")]
+        test_template: TestTemplate,
         /// Initialize even if there are files
         #[clap(long, action)]
         force: bool,
@@ -651,8 +651,8 @@ fn process_command(opts: Opts) -> Result<()> {
             javascript,
             solidity,
             no_git,
-            jest,
             template,
+            test_template,
             force,
         } => init(
             &opts.cfg_override,
@@ -660,8 +660,8 @@ fn process_command(opts: Opts) -> Result<()> {
             javascript,
             solidity,
             no_git,
-            jest,
             template,
+            test_template,
             force,
         ),
         Command::New {
@@ -824,8 +824,8 @@ fn init(
     javascript: bool,
     solidity: bool,
     no_git: bool,
-    jest: bool,
     template: ProgramTemplate,
+    test_template: TestTemplate,
     force: bool,
 ) -> Result<()> {
     if !force && Config::discover(cfg_override)?.is_some() {
@@ -861,27 +861,9 @@ fn init(
     fs::create_dir_all("app")?;
 
     let mut cfg = Config::default();
-    if jest {
-        cfg.scripts.insert(
-            "test".to_owned(),
-            if javascript {
-                "yarn run jest"
-            } else {
-                "yarn run jest --preset ts-jest"
-            }
-            .to_owned(),
-        );
-    } else {
-        cfg.scripts.insert(
-            "test".to_owned(),
-            if javascript {
-                "yarn run mocha -t 1000000 tests/"
-            } else {
-                "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
-            }
-            .to_owned(),
-        );
-    }
+    let test_script = test_template.get_test_script(javascript);
+    cfg.scripts
+        .insert("test".to_owned(), test_script.to_owned());
 
     let mut localnet = BTreeMap::new();
     let program_id = rust_template::get_or_create_program_id(&rust_name);
@@ -919,32 +901,15 @@ fn init(
         rust_template::create_program(&project_name, template)?;
     }
 
-    // Build the test suite.
-    fs::create_dir_all("tests")?;
     // Build the migrations directory.
     fs::create_dir_all("migrations")?;
 
+    let jest = TestTemplate::Jest == test_template;
     if javascript {
         // Build javascript config
         let mut package_json = File::create("package.json")?;
         package_json.write_all(rust_template::package_json(jest).as_bytes())?;
 
-        if jest {
-            let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
-            if solidity {
-                test.write_all(solidity_template::jest(&project_name).as_bytes())?;
-            } else {
-                test.write_all(rust_template::jest(&project_name).as_bytes())?;
-            }
-        } else {
-            let mut test = File::create(format!("tests/{}.js", &project_name))?;
-            if solidity {
-                test.write_all(solidity_template::mocha(&project_name).as_bytes())?;
-            } else {
-                test.write_all(rust_template::mocha(&project_name).as_bytes())?;
-            }
-        }
-
         let mut deploy = File::create("migrations/deploy.js")?;
 
         deploy.write_all(rust_template::deploy_script().as_bytes())?;
@@ -958,15 +923,15 @@ fn init(
 
         let mut deploy = File::create("migrations/deploy.ts")?;
         deploy.write_all(rust_template::ts_deploy_script().as_bytes())?;
-
-        let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
-        if solidity {
-            mocha.write_all(solidity_template::ts_mocha(&project_name).as_bytes())?;
-        } else {
-            mocha.write_all(rust_template::ts_mocha(&project_name).as_bytes())?;
-        }
     }
 
+    test_template.create_test_files(
+        &project_name,
+        javascript,
+        solidity,
+        &program_id.to_string(),
+    )?;
+
     let yarn_result = install_node_modules("yarn")?;
     if !yarn_result.status.success() {
         println!("Failed yarn install will attempt to npm install");
@@ -1093,6 +1058,32 @@ pub fn create_files(files: &Files) -> Result<()> {
     Ok(())
 }
 
+/// Override or create files from the given (path, content) tuple array.
+///
+/// # Example
+///
+/// ```ignore
+/// override_or_create_files(vec![("programs/my_program/src/lib.rs".into(), "// Content".into())])?;
+/// ```
+pub fn override_or_create_files(files: &Files) -> Result<()> {
+    for (path, content) in files {
+        let path = Path::new(path);
+        if path.exists() {
+            let mut f = fs::OpenOptions::new()
+                .write(true)
+                .truncate(true)
+                .open(path)?;
+            f.write_all(content.as_bytes())?;
+            f.flush()?;
+        } else {
+            fs::create_dir_all(path.parent().unwrap())?;
+            fs::write(path, content)?;
+        }
+    }
+
+    Ok(())
+}
+
 pub fn expand(
     cfg_override: &ConfigOverride,
     program_name: Option<String>,
@@ -4541,8 +4532,8 @@ mod tests {
             true,
             false,
             false,
-            false,
             ProgramTemplate::default(),
+            TestTemplate::default(),
             false,
         )
         .unwrap();
@@ -4560,8 +4551,8 @@ mod tests {
             true,
             false,
             false,
-            false,
             ProgramTemplate::default(),
+            TestTemplate::default(),
             false,
         )
         .unwrap();
@@ -4579,8 +4570,8 @@ mod tests {
             true,
             false,
             false,
-            false,
             ProgramTemplate::default(),
+            TestTemplate::default(),
             false,
         )
         .unwrap();

+ 184 - 3
cli/src/rust_template.rs

@@ -1,5 +1,7 @@
-use crate::VERSION;
-use crate::{config::ProgramWorkspace, create_files, Files};
+use crate::{
+    config::ProgramWorkspace, create_files, override_or_create_files, solidity_template, Files,
+    VERSION,
+};
 use anchor_syn::idl::types::Idl;
 use anyhow::Result;
 use clap::{Parser, ValueEnum};
@@ -9,7 +11,13 @@ use solana_sdk::{
     signature::{read_keypair_file, write_keypair_file, Keypair},
     signer::Signer,
 };
-use std::{fmt::Write, path::Path};
+use std::{
+    fmt::Write as _,
+    fs::{self, File},
+    io::Write as _,
+    path::Path,
+    process::Stdio,
+};
 
 /// Program initialization template
 #[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
@@ -606,3 +614,176 @@ anchor.workspace.{} = new anchor.Program({}, new PublicKey("{}"), provider);
 
     Ok(eval_string)
 }
+
+/// Test initialization template
+#[derive(Clone, Debug, Default, Eq, PartialEq, Parser, ValueEnum)]
+pub enum TestTemplate {
+    /// Generate template for Mocha unit-test
+    #[default]
+    Mocha,
+    /// Generate template for Jest unit-test    
+    Jest,
+    /// Generate template for Rust unit-test
+    Rust,
+}
+
+impl TestTemplate {
+    pub fn get_test_script(&self, js: bool) -> &str {
+        match &self {
+            Self::Mocha => {
+                if js {
+                    "yarn run mocha -t 1000000 tests/"
+                } else {
+                    "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"
+                }
+            }
+            Self::Jest => {
+                if js {
+                    "yarn run jest"
+                } else {
+                    "yarn run jest --preset ts-jest"
+                }
+            }
+            Self::Rust => "cargo test",
+        }
+    }
+
+    pub fn create_test_files(
+        &self,
+        project_name: &str,
+        js: bool,
+        solidity: bool,
+        program_id: &str,
+    ) -> Result<()> {
+        match self {
+            Self::Mocha => {
+                // Build the test suite.
+                fs::create_dir_all("tests")?;
+
+                if js {
+                    let mut test = File::create(format!("tests/{}.js", &project_name))?;
+                    if solidity {
+                        test.write_all(solidity_template::mocha(project_name).as_bytes())?;
+                    } else {
+                        test.write_all(mocha(project_name).as_bytes())?;
+                    }
+                } else {
+                    let mut mocha = File::create(format!("tests/{}.ts", &project_name))?;
+                    if solidity {
+                        mocha.write_all(solidity_template::ts_mocha(project_name).as_bytes())?;
+                    } else {
+                        mocha.write_all(ts_mocha(project_name).as_bytes())?;
+                    }
+                }
+            }
+            Self::Jest => {
+                // Build the test suite.
+                fs::create_dir_all("tests")?;
+
+                let mut test = File::create(format!("tests/{}.test.js", &project_name))?;
+                if solidity {
+                    test.write_all(solidity_template::jest(project_name).as_bytes())?;
+                } else {
+                    test.write_all(jest(project_name).as_bytes())?;
+                }
+            }
+            Self::Rust => {
+                // Do not initilize git repo
+                let exit = std::process::Command::new("cargo")
+                    .arg("new")
+                    .arg("--vcs")
+                    .arg("none")
+                    .arg("--lib")
+                    .arg("tests")
+                    .stderr(Stdio::inherit())
+                    .output()
+                    .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
+                if !exit.status.success() {
+                    eprintln!("'cargo new --lib tests' failed");
+                    std::process::exit(exit.status.code().unwrap_or(1));
+                }
+
+                let mut files = Vec::new();
+                let tests_path = Path::new("tests");
+                files.extend(vec![(
+                    tests_path.join("Cargo.toml"),
+                    tests_cargo_toml(project_name),
+                )]);
+                files.extend(create_program_template_rust_test(
+                    project_name,
+                    tests_path,
+                    program_id,
+                ));
+                override_or_create_files(&files)?;
+            }
+        }
+
+        Ok(())
+    }
+}
+
+pub fn tests_cargo_toml(name: &str) -> String {
+    format!(
+        r#"[package]
+name = "tests"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2021"
+
+[dependencies]
+anchor-client = "{0}"
+{1} = {{ version = "0.1.0", path = "../programs/{1}" }}
+"#,
+        VERSION, name,
+    )
+}
+
+/// Generate template for Rust unit-test
+fn create_program_template_rust_test(name: &str, tests_path: &Path, program_id: &str) -> Files {
+    let src_path = tests_path.join("src");
+    vec![
+        (
+            src_path.join("lib.rs"),
+            r#"#[cfg(test)]
+mod test_initialize;
+"#
+            .into(),
+        ),
+        (
+            src_path.join("test_initialize.rs"),
+            format!(
+                r#"use std::str::FromStr;
+
+use anchor_client::{{
+    solana_sdk::{{
+        commitment_config::CommitmentConfig, pubkey::Pubkey, signature::read_keypair_file,
+    }},
+    Client, Cluster,
+}};
+
+#[test]
+fn test_initialize() {{
+    let program_id = "{0}";
+    let anchor_wallet = std::env::var("ANCHOR_WALLET").unwrap();
+    let payer = read_keypair_file(&anchor_wallet).unwrap();
+
+    let client = Client::new_with_options(Cluster::Localnet, &payer, CommitmentConfig::confirmed());
+    let program_id = Pubkey::from_str(program_id).unwrap();
+    let program = client.program(program_id).unwrap();
+
+    let tx = program
+        .request()
+        .accounts({1}::accounts::Initialize {{}})
+        .args({1}::instruction::Initialize {{}})
+        .send()
+        .expect("");
+
+    println!("Your transaction signature {{}}", tx);
+}}
+"#,
+                program_id,
+                name.to_snake_case(),
+            ),
+        ),
+    ]
+}