From f6a418c9416c63fa29a652ab7daaa3533722b9b3 Mon Sep 17 00:00:00 2001 From: prxgr4mm3r Date: Fri, 8 Dec 2023 23:15:24 +0200 Subject: [PATCH 01/11] Add ink_e2e tests as default, make -moche flag for mocha tests, add `test generate` command to generate types for mocha tests, add test_helpers for ink_e2e tests, remove generate types from compile, add ink_e2e tests to tamplates --- .devcontainer/devcontainer.json | 2 +- src/commands/contract/compile.ts | 8 +- src/commands/contract/test.ts | 115 --------- .../contract/{typegen.ts => test/generate.ts} | 12 +- src/commands/contract/test/index.ts | 171 ++++++++++++++ src/commands/init/index.ts | 21 +- src/lib/tasks.ts | 10 + .../contracts/flipper/contract/Cargo.toml.hbs | 4 + .../contracts/flipper/contract/src/lib.rs.hbs | 85 ++++++- .../contracts/psp22/contract/Cargo.toml.hbs | 7 +- .../contracts/psp22/contract/src/lib.rs.hbs | 85 +++++++ .../contracts/psp22/test/index.test.ts.hbs | 45 +--- src/templates/test_helpers/Cargo.toml.hbs | 12 + src/templates/test_helpers/lib.rs.hbs | 218 ++++++++++++++++++ 14 files changed, 609 insertions(+), 186 deletions(-) delete mode 100644 src/commands/contract/test.ts rename src/commands/contract/{typegen.ts => test/generate.ts} (74%) create mode 100644 src/commands/contract/test/index.ts create mode 100644 src/templates/test_helpers/Cargo.toml.hbs create mode 100644 src/templates/test_helpers/lib.rs.hbs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c34e3ef1..9586e965 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "swanky-env", - "image": "ghcr.io/swankyhub/swanky-cli/swanky-base:swanky3.1.0-beta.0_v2.1.0", + "image": "ghcr.io/inkdevhub/swanky-cli/swanky-base:swanky3.1.0-beta.0_v2.1.1", // Mount the workspace volume "mounts": ["source=${localWorkspaceFolder},target=/workspaces,type=bind,consistency=cached"], diff --git a/src/commands/contract/compile.ts b/src/commands/contract/compile.ts index a42ccddf..4896094a 100644 --- a/src/commands/contract/compile.ts +++ b/src/commands/contract/compile.ts @@ -1,6 +1,6 @@ import { Args, Flags } from "@oclif/core"; import path from "node:path"; -import { storeArtifacts, Spinner, generateTypes } from "../../lib/index.js"; +import { storeArtifacts, Spinner } from "../../lib/index.js"; import { spawn } from "node:child_process"; import { pathExists } from "fs-extra/esm"; import { SwankyCommand } from "../../lib/swankyCommand.js"; @@ -108,12 +108,6 @@ export class CompileContract extends SwankyCommand { await spinner.runCommand(async () => { return storeArtifacts(artifactsPath, contractInfo.name, contractInfo.moduleName); }, "Moving artifacts"); - - await spinner.runCommand( - async () => await generateTypes(contractInfo.name), - `Generating ${contractName} contract ts types`, - `${contractName} contract's TS types Generated successfully` - ); } } } diff --git a/src/commands/contract/test.ts b/src/commands/contract/test.ts deleted file mode 100644 index 216eb6b0..00000000 --- a/src/commands/contract/test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import "ts-mocha"; -import { Flags, Args } from "@oclif/core"; -import path from "node:path"; -import { globby } from "globby"; -import Mocha from "mocha"; -import { emptyDir } from "fs-extra/esm"; -import shell from "shelljs"; -import { Contract } from "../../lib/contract.js"; -import { SwankyCommand } from "../../lib/swankyCommand.js"; -import { ConfigError, FileError, InputError, TestError } from "../../lib/errors.js"; - -declare global { - var contractTypesPath: string; // eslint-disable-line no-var -} - -export class TestContract extends SwankyCommand { - static description = "Run tests for a given contact"; - - static flags = { - all: Flags.boolean({ - default: false, - char: "a", - description: "Set all to true to compile all contracts", - }), - }; - - static args = { - contractName: Args.string({ - name: "contractName", - default: "", - description: "Name of the contract to test", - }), - }; - - async run(): Promise { - const { args, flags } = await this.parse(TestContract); - - if (args.contractName === undefined && !flags.all) { - throw new InputError("No contracts were selected to compile"); - } - - const contractNames = flags.all - ? Object.keys(this.swankyConfig.contracts) - : [args.contractName]; - - const testDir = path.resolve("tests"); - - for (const contractName of contractNames) { - const contractRecord = this.swankyConfig.contracts[contractName]; - if (!contractRecord) { - throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` - ); - } - - const contract = new Contract(contractRecord); - - if (!(await contract.pathExists())) { - throw new FileError( - `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` - ); - } - - const artifactsCheck = await contract.artifactsExist(); - - if (!artifactsCheck.result) { - throw new FileError( - `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` - ); - } - - console.log(`Testing contract: ${contractName}`); - - const reportDir = path.resolve(testDir, contract.name, "testReports"); - - await emptyDir(reportDir); - - const mocha = new Mocha({ - timeout: 200000, - reporter: "mochawesome", - reporterOptions: { - reportDir, - charts: true, - reportTitle: `${contractName} test report`, - quiet: true, - json: false, - }, - }); - - const tests = await globby(`${path.resolve(testDir, contractName)}/*.test.ts`); - - tests.forEach((test) => { - mocha.addFile(test); - }); - - global.contractTypesPath = path.resolve(testDir, contractName, "typedContract"); - - shell.cd(`${testDir}/${contractName}`); - try { - await new Promise((resolve, reject) => { - mocha.run((failures) => { - if (failures) { - reject(`At least one of the tests failed. Check report for details: ${reportDir}`); - } else { - this.log(`All tests passing. Check the report for details: ${reportDir}`); - resolve(); - } - }); - }); - } catch (cause) { - throw new TestError("Error in test", { cause }); - } - } - } -} diff --git a/src/commands/contract/typegen.ts b/src/commands/contract/test/generate.ts similarity index 74% rename from src/commands/contract/typegen.ts rename to src/commands/contract/test/generate.ts index 391e0a3c..e498b9fb 100644 --- a/src/commands/contract/typegen.ts +++ b/src/commands/contract/test/generate.ts @@ -1,10 +1,10 @@ import { Args } from "@oclif/core"; -import { generateTypes } from "../../lib/index.js"; -import { Contract } from "../../lib/contract.js"; -import { SwankyCommand } from "../../lib/swankyCommand.js"; -import { ConfigError, FileError } from "../../lib/errors.js"; +import { generateTypes } from "../../../lib/index.js"; +import { Contract } from "../../../lib/contract.js"; +import { SwankyCommand } from "../../../lib/swankyCommand.js"; +import { ConfigError, FileError } from "../../../lib/errors.js"; -export class TypegenCommand extends SwankyCommand { +export class ContractTestTypegen extends SwankyCommand { static description = "Generate types from compiled contract metadata"; static args = { @@ -16,7 +16,7 @@ export class TypegenCommand extends SwankyCommand { }; async run(): Promise { - const { args } = await this.parse(TypegenCommand); + const { args } = await this.parse(ContractTestTypegen); const contractRecord = this.swankyConfig.contracts[args.contractName]; if (!contractRecord) { diff --git a/src/commands/contract/test/index.ts b/src/commands/contract/test/index.ts new file mode 100644 index 00000000..2545e09e --- /dev/null +++ b/src/commands/contract/test/index.ts @@ -0,0 +1,171 @@ +import "ts-mocha"; +import { Flags, Args } from "@oclif/core"; +import path from "node:path"; +import { globby } from "globby"; +import Mocha from "mocha"; +import { emptyDir } from "fs-extra/esm"; +import shell from "shelljs"; +import { Contract } from "../../../lib/contract.js"; +import { SwankyCommand } from "../../../lib/swankyCommand.js"; +import { ConfigError, FileError, InputError, ProcessError, TestError } from "../../../lib/errors.js"; +import { spawn } from "node:child_process"; +import { Spinner } from "../../../lib/index.js"; + +declare global { + var contractTypesPath: string; // eslint-disable-line no-var +} + +export class TestContract extends SwankyCommand { + static description = "Run tests for a given contact"; + + static flags = { + all: Flags.boolean({ + default: false, + char: "a", + description: "Set all to true to compile all contracts", + }), + mocha: Flags.boolean({ + default: false, + description: "Run tests with mocha", + }), + }; + + static args = { + contractName: Args.string({ + name: "contractName", + default: "", + description: "Name of the contract to test", + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(TestContract); + + if (args.contractName === undefined && !flags.all) { + throw new InputError("No contracts were selected to compile"); + } + + const contractNames = flags.all + ? Object.keys(this.swankyConfig.contracts) + : [args.contractName]; + + const testDir = path.resolve("tests"); + + const spinner = new Spinner(); + + for (const contractName of contractNames) { + const contractRecord = this.swankyConfig.contracts[contractName]; + if (!contractRecord) { + throw new ConfigError( + `Cannot find a contract named ${args.contractName} in swanky.config.json` + ); + } + + const contract = new Contract(contractRecord); + + if (!(await contract.pathExists())) { + throw new FileError( + `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` + ); + } + + console.log(`Testing contract: ${contractName}`); + + if (!flags.ts) { + await spinner.runCommand( + async () => { + return new Promise((resolve, reject) => { + const compileArgs = [ + "test", + "--features", + "e2e-tests", + "--manifest-path", + `contracts/${contractName}/Cargo.toml`, + "--release" + ]; + + const compile = spawn("cargo", compileArgs); + this.logger.info(`Running e2e-tests command: [${JSON.stringify(compile.spawnargs)}]`); + let outputBuffer = ""; + let errorBuffer = ""; + + compile.stdout.on("data", (data) => { + outputBuffer += data.toString(); + spinner.ora.clear(); + }); + compile.stdout.pipe(process.stdout); + + compile.stderr.on("data", (data) => { + errorBuffer += data; + }); + + compile.on("exit", (code) => { + if (code === 0) { + const regex = /test result: (.*)/; + const match = outputBuffer.match(regex); + if (match) { + this.logger.info(`Contract ${contractName} e2e-testing done.`); + resolve(match[1]); + } + } else { + reject(new ProcessError(errorBuffer)); + } + }); + }); + }, + `Testing ${contractName} contract`, + `${contractName} testing finished successfully` + ); + } else { + + const artifactsCheck = await contract.artifactsExist(); + + if (!artifactsCheck.result) { + throw new FileError( + `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` + ); + } + + const reportDir = path.resolve(testDir, contract.name, "testReports"); + + await emptyDir(reportDir); + + const mocha = new Mocha({ + timeout: 200000, + reporter: "mochawesome", + reporterOptions: { + reportDir, + charts: true, + reportTitle: `${contractName} test report`, + quiet: true, + json: false, + }, + }); + + const tests = await globby(`${path.resolve(testDir, contractName)}/*.test.ts`); + + tests.forEach((test) => { + mocha.addFile(test); + }); + + global.contractTypesPath = path.resolve(testDir, contractName, "typedContract"); + + shell.cd(`${testDir}/${contractName}`); + try { + await new Promise((resolve, reject) => { + mocha.run((failures) => { + if (failures) { + reject(`At least one of the tests failed. Check report for details: ${reportDir}`); + } else { + this.log(`All tests passing. Check the report for details: ${reportDir}`); + resolve(); + } + }); + }); + } catch (cause) { + throw new TestError("Error in test", { cause }); + } + } + } + } +} diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 45177372..7a1589a9 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -16,7 +16,7 @@ import { ChainAccount, processTemplates, swankyNode, - getTemplates, + getTemplates, copyTestHelpers, } from "../../lib/index.js"; import { DEFAULT_ASTAR_NETWORK_URL, @@ -269,6 +269,15 @@ export class Init extends SwankyCommand { runningMessage: "Copying contract template files", }); + this.taskQueue.push({ + task: copyTestHelpers, + args: [ + path.resolve(templates.templatesPath), + this.projectPath, + ], + runningMessage: "Copying test helpers", + }); + this.taskQueue.push({ task: processTemplates, args: [ @@ -381,11 +390,11 @@ export class Init extends SwankyCommand { this.taskQueue.push({ task: async (tomlObject, projectPath) => { const tomlString = TOML.stringify(tomlObject); - const rootTomlPath = path.resolve(projectPath, "Cargo.toml"); + const rootTomlPath = path.resolve(projectPath, "Cargo.toml.hbs.hbs"); await outputFile(rootTomlPath, tomlString); }, args: [rootToml, this.projectPath], - runningMessage: "Writing Cargo.toml", + runningMessage: "Writing Cargo.toml.hbs.hbs", }); this.taskQueue.push({ @@ -433,9 +442,9 @@ async function detectModuleNames(copyList: CopyCandidates): Promise) { const templateFiles = await globby(projectPath, { expandDirectories: { extensions: ["hbs"] }, diff --git a/src/templates/contracts/flipper/contract/Cargo.toml.hbs b/src/templates/contracts/flipper/contract/Cargo.toml.hbs index de90dba7..2de5449d 100644 --- a/src/templates/contracts/flipper/contract/Cargo.toml.hbs +++ b/src/templates/contracts/flipper/contract/Cargo.toml.hbs @@ -10,6 +10,9 @@ ink = { version = "4.2.1", default-features = false } scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } +[dev-dependencies] +ink_e2e = "4.2.1" + [lib] name = "{{contract_name_snake}}" path = "src/lib.rs" @@ -22,3 +25,4 @@ std = [ "scale-info/std", ] ink-as-dependency = [] +e2e-tests = [] diff --git a/src/templates/contracts/flipper/contract/src/lib.rs.hbs b/src/templates/contracts/flipper/contract/src/lib.rs.hbs index a5137e64..a2329d0b 100644 --- a/src/templates/contracts/flipper/contract/src/lib.rs.hbs +++ b/src/templates/contracts/flipper/contract/src/lib.rs.hbs @@ -57,17 +57,90 @@ mod {{contract_name_snake}} { /// We test if the default constructor does its job. #[ink::test] fn default_works() { - let {{contract_name_snake}} = {{contract_name_pascal}}::default(); - assert_eq!({{contract_name_snake}}.get(), false); + let flipper = Flipper::default(); + assert_eq!(flipper.get(), false); } /// We test a simple use case of our contract. #[ink::test] fn it_works() { - let mut {{contract_name_snake}} = {{contract_name_pascal}}::new(false); - assert_eq!({{contract_name_snake}}.get(), false); - {{contract_name_snake}}.flip(); - assert_eq!({{contract_name_snake}}.get(), true); + let mut flipper = Flipper::new(false); + assert_eq!(flipper.get(), false); + flipper.flip(); + assert_eq!(flipper.get(), true); + } + } + + + /// This is how you'd write end-to-end (E2E) or integration tests for ink! contracts. + /// + /// When running these you need to make sure that you: + /// - Compile the tests with the `e2e-tests` feature flag enabled (`--features e2e-tests`) + /// - Are running a Substrate node which contains `pallet-contracts` in the background + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + /// Imports all the definitions from the outer scope so we can use them here. + use super::*; + + /// A helper function used for calling contract messages. + use ink_e2e::build_message; + + /// The End-to-End test `Result` type. + type E2EResult = std::result::Result>; + + /// We test that we can upload and instantiate the contract using its default constructor. + #[ink_e2e::test] + async fn default_works(mut client: ink_e2e::Client) -> E2EResult<()> { + // Given + let constructor = FlipperRef::default(); + + // When + let contract_account_id = client + .instantiate("flipper", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + // Then + let get = build_message::(contract_account_id.clone()) + .call(|flipper| flipper.get()); + let get_result = client.call_dry_run(&ink_e2e::alice(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), false)); + + Ok(()) + } + + /// We test that we can read and write a value from the on-chain contract contract. + #[ink_e2e::test] + async fn it_works(mut client: ink_e2e::Client) -> E2EResult<()> { + // Given + let constructor = FlipperRef::new(false); + let contract_account_id = client + .instantiate("flipper", &ink_e2e::bob(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let get = build_message::(contract_account_id.clone()) + .call(|flipper| flipper.get()); + let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), false)); + + // When + let flip = build_message::(contract_account_id.clone()) + .call(|flipper| flipper.flip()); + let _flip_result = client + .call(&ink_e2e::bob(), flip, 0, None) + .await + .expect("flip failed"); + + // Then + let get = build_message::(contract_account_id.clone()) + .call(|flipper| flipper.get()); + let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; + assert!(matches!(get_result.return_value(), true)); + + Ok(()) } } } diff --git a/src/templates/contracts/psp22/contract/Cargo.toml.hbs b/src/templates/contracts/psp22/contract/Cargo.toml.hbs index a2c880f3..0cf1e1d6 100644 --- a/src/templates/contracts/psp22/contract/Cargo.toml.hbs +++ b/src/templates/contracts/psp22/contract/Cargo.toml.hbs @@ -10,7 +10,11 @@ ink = { version = "4.2.1", default-features = false} scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } scale-info = { version = "2.6", default-features = false, features = ["derive"], optional = true } -openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", tag = "4.0.0-beta", default-features = false, features = ["psp22"] } +openbrush = { git = "https://github.com/Brushfam/openbrush-contracts", tag = "4.0.0", default-features = false, features = ["psp22"] } + +[dev-dependencies] +ink_e2e = "4.2.1" +test_helpers = { path = "../../tests/test_helpers", default-features = false } [lib] name = "{{contract_name_snake}}" @@ -25,6 +29,7 @@ std = [ "openbrush/std", ] ink-as-dependency = [] +e2e-tests = [] [profile.dev] codegen-units = 16 diff --git a/src/templates/contracts/psp22/contract/src/lib.rs.hbs b/src/templates/contracts/psp22/contract/src/lib.rs.hbs index 4b85fb96..88b50918 100644 --- a/src/templates/contracts/psp22/contract/src/lib.rs.hbs +++ b/src/templates/contracts/psp22/contract/src/lib.rs.hbs @@ -69,4 +69,89 @@ pub mod {{contract_name_snake}} { PSP22::total_supply(self) } } + + #[cfg(all(test, feature = "e2e-tests"))] + pub mod tests { + use super::*; + use ink_e2e::{ + build_message, + }; + use openbrush::contracts::psp22::psp22_external::PSP22; + use test_helpers::{ + address_of, + balance_of, + }; + + type E2EResult = Result>; + + #[ink_e2e::test] + async fn assigns_initial_balance(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = {{contract_name_pascal}}Ref::new(100); + let address = client + .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let result = { + let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) + .call(|contract| contract.balance_of(address_of!(Alice))); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + }; + + assert!(matches!(result.return_value(), 100)); + + Ok(()) + } + + #[ink_e2e::test] + async fn transfer_adds_amount_to_destination_account(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = {{contract_name_pascal}}Ref::new(100); + let address = client + .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let result = { + let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) + .call(|contract| contract.transfer(address_of!(Bob), 50, vec![])); + client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("transfer failed") + }; + + assert!(matches!(result.return_value(), Ok(()))); + + let balance_of_alice = balance_of!({{contract_name_pascal}}Ref, client, address, Alice); + + let balance_of_bob = balance_of!({{contract_name_pascal}}Ref, client, address, Bob); + + assert_eq!(balance_of_bob, 50, "Bob should have 50 tokens"); + assert_eq!(balance_of_alice, 50, "Alice should have 50 tokens"); + + Ok(()) + } + + #[ink_e2e::test] + async fn cannot_transfer_above_the_amount(mut client: ink_e2e::Client) -> E2EResult<()> { + let constructor = {{contract_name_pascal}}Ref::new(100); + let address = client + .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) + .await + .expect("instantiate failed") + .account_id; + + let result = { + let _msg = build_message::<{{contract_name_pascal}}Ref>(address.clone()) + .call(|contract| contract.transfer(address_of!(Bob), 101, vec![])); + client.call_dry_run(&ink_e2e::alice(), &_msg, 0, None).await + }; + + assert!(matches!(result.return_value(), Err(PSP22Error::InsufficientBalance))); + + Ok(()) + } + } } diff --git a/src/templates/contracts/psp22/test/index.test.ts.hbs b/src/templates/contracts/psp22/test/index.test.ts.hbs index 5e018a64..c17776a3 100644 --- a/src/templates/contracts/psp22/test/index.test.ts.hbs +++ b/src/templates/contracts/psp22/test/index.test.ts.hbs @@ -79,47 +79,4 @@ describe("{{contract_name}} test", () => { }) ).to.eventually.be.rejected; }); - - it("Can not transfer to hated account", async () => { - const hated_account = wallet2; - const transferredAmount = 10; - const { gasRequired } = await contract - .withSigner(deployer) - .query.transfer(wallet1.address, transferredAmount, []); - // Check that we can transfer money while account is not hated - await expect( - contract.tx.transfer(hated_account.address, 10, [], { - gasLimit: gasRequired, - }) - ).to.eventually.be.fulfilled; - - const result = await contract.query.balanceOf(hated_account.address); - expect(result.value.ok?.toNumber()).to.equal(transferredAmount); - - expect((await contract.query.getHatedAccount()).value.ok).to.equal( - EMPTY_ADDRESS - ); - - // Hate account - await expect( - contract.tx.setHatedAccount(hated_account.address, { - gasLimit: gasRequired, - }) - ).to.eventually.be.ok; - expect((await contract.query.getHatedAccount()).value.ok).to.equal( - hated_account.address - ); - - // Transfer must fail - expect( - contract.tx.transfer(hated_account.address, 10, [], { - gasLimit: gasRequired, - }) - ).to.eventually.be.rejected; - - // Amount of tokens must be the same - expect( - (await contract.query.balanceOf(hated_account.address)).value.ok?.toNumber() - ).to.equal(10); - }); -}); +}); \ No newline at end of file diff --git a/src/templates/test_helpers/Cargo.toml.hbs b/src/templates/test_helpers/Cargo.toml.hbs new file mode 100644 index 00000000..e0724a21 --- /dev/null +++ b/src/templates/test_helpers/Cargo.toml.hbs @@ -0,0 +1,12 @@ +[package] +name = "test_helpers" +version= "0.1.0" +authors = ["{{author_name}}"] +edition = "2021" + +[lib] +name = "test_helpers" +path = "lib.rs" + +[profile.dev] +codegen-units = 16 \ No newline at end of file diff --git a/src/templates/test_helpers/lib.rs.hbs b/src/templates/test_helpers/lib.rs.hbs new file mode 100644 index 00000000..8e1afe23 --- /dev/null +++ b/src/templates/test_helpers/lib.rs.hbs @@ -0,0 +1,218 @@ +#[macro_export] +macro_rules! address_of { + ($account:ident) => { + ink_e2e::account_id(ink_e2e::AccountKeyring::$account) + }; +} + +#[macro_export] +macro_rules! balance_of { + ($contract_ref:ident, $client:ident, $address:ident, $account:ident) => \{{ + let _msg = + build_message::<$contract_ref>($address.clone()).call(|contract| contract.balance_of(address_of!($account))); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! owner_of { + ($contract_ref:ident, $client:ident, $address:ident, $id:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.owner_of($id)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! balance_of_37 { + ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $token:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()) + .call(|contract| contract.balance_of(address_of!($account), $token)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! has_role { + ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()) + .call(|contract| contract.has_role($role, Some(address_of!($account)))); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! grant_role { + ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()) + .call(|contract| contract.grant_role($role, Some(address_of!($account)))); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("grant_role failed") + .return_value() + }}; +} + +#[macro_export] +macro_rules! revoke_role { + ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()) + .call(|contract| contract.revoke_role($role, Some(address_of!($account)))); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("revoke_role failed") + .return_value() + }}; +} + +#[macro_export] +macro_rules! mint_dry_run { + ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $id:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $account:ident, $id:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); + $client + .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! mint { + ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $id:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("mint failed") + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $account:ident, $id:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); + $client + .call(&ink_e2e::$signer(), _msg, 0, None) + .await + .expect("mint failed") + .return_value() + }}; +} + +#[macro_export] +macro_rules! get_role_member_count { + ($contract_ref:ident, $client:ident, $address:ident, $role:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.get_role_member_count($role)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! get_role_member { + ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $index:expr) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.get_role_member($role, $index)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! get_shares { + ($contract_ref:ident, $client:ident, $address:ident, $user:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.shares(address_of!($user))); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; +} + +#[macro_export] +macro_rules! method_call { + ($contract_ref:ident, $client:ident, $address:ident, $method:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); + $client + .call(&ink_e2e::$signer(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $method:ident($($args:expr),*)) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call(&ink_e2e::alice(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call(&ink_e2e::$signer(), _msg, 0, None) + .await + .expect("method_call failed") + .return_value() + }}; +} + +#[macro_export] +macro_rules! method_call_dry_run { + ($contract_ref:ident, $client:ident, $address:ident, $method:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $method:ident($($args:expr),*)) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .await + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); + $client + .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) + .await + .return_value() + }}; + ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => \{{ + let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); + $client + .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) + .await + .return_value() + }}; +} \ No newline at end of file From d5a9b65e4ad04d99d68bc3ef9545edd19d702df5 Mon Sep 17 00:00:00 2001 From: prxgr4mm3r Date: Fri, 8 Dec 2023 23:46:28 +0200 Subject: [PATCH 02/11] Fix checking mocha flag, add check if typedContracts folder exists --- src/commands/contract/test/index.ts | 15 +++++++++++++-- src/lib/contract.ts | 13 +++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/commands/contract/test/index.ts b/src/commands/contract/test/index.ts index 2545e09e..903cac94 100644 --- a/src/commands/contract/test/index.ts +++ b/src/commands/contract/test/index.ts @@ -27,7 +27,7 @@ export class TestContract extends SwankyCommand { mocha: Flags.boolean({ default: false, description: "Run tests with mocha", - }), + }), }; static args = { @@ -71,7 +71,7 @@ export class TestContract extends SwankyCommand { console.log(`Testing contract: ${contractName}`); - if (!flags.ts) { + if (!flags.mocha) { await spinner.runCommand( async () => { return new Promise((resolve, reject) => { @@ -126,6 +126,17 @@ export class TestContract extends SwankyCommand { ); } + const artifactPath = path.resolve("typedContracts", `${contractName}`); + const typedContractCheck = await contract.typedContractExists(contractName); + + this.log(`artifactPath: ${artifactPath}`); + + if (!typedContractCheck.result) { + throw new FileError( + `No typed contract found at path: ${typedContractCheck.missingPaths.toString()}` + ); + } + const reportDir = path.resolve(testDir, contract.name, "testReports"); await emptyDir(reportDir); diff --git a/src/lib/contract.ts b/src/lib/contract.ts index 482c0ae2..0546e468 100644 --- a/src/lib/contract.ts +++ b/src/lib/contract.ts @@ -41,6 +41,19 @@ export class Contract { return result; } + async typedContractExists(contractName: string) { + const result: { result: boolean; missingPaths: string[] } = { + result: true, + missingPaths: [], + }; + const artifactPath = path.resolve("typedContracts", `${contractName}`); + if(!(await pathExists(artifactPath))) { + result.result = false; + result.missingPaths.push(artifactPath); + } + return result; + } + async getABI(): Promise { const check = await this.artifactsExist(); if (!check.result && check.missingTypes.includes(".json")) { From dcb119aa9fd500d8f0a06ebd1af6647b5a7ae229 Mon Sep 17 00:00:00 2001 From: prxgr4mm3r Date: Mon, 12 Feb 2024 19:03:16 +0200 Subject: [PATCH 03/11] fixes --- src/commands/contract/test/index.ts | 12 ++ src/commands/init/index.ts | 18 +- .../contracts/flipper/contract/src/lib.rs.hbs | 20 +- src/templates/test_helpers/lib.rs.hbs | 200 ------------------ 4 files changed, 32 insertions(+), 218 deletions(-) diff --git a/src/commands/contract/test/index.ts b/src/commands/contract/test/index.ts index 903cac94..10479a2d 100644 --- a/src/commands/contract/test/index.ts +++ b/src/commands/contract/test/index.ts @@ -24,6 +24,10 @@ export class TestContract extends SwankyCommand { char: "a", description: "Set all to true to compile all contracts", }), + "local-node": Flags.boolean({ + default: false, + description: "Run tests on a local node", + }), mocha: Flags.boolean({ default: false, description: "Run tests with mocha", @@ -69,6 +73,14 @@ export class TestContract extends SwankyCommand { ); } + if (flags["local-node"]) { + if (!this.swankyConfig.node.localPath) + { + throw new ConfigError("Local node path not found in swanky.config.json"); + } + process.env.CONTRACTS_NODE = this.swankyConfig.node.localPath; + } + console.log(`Testing contract: ${contractName}`); if (!flags.mocha) { diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 7a1589a9..01050221 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -269,14 +269,16 @@ export class Init extends SwankyCommand { runningMessage: "Copying contract template files", }); - this.taskQueue.push({ - task: copyTestHelpers, - args: [ - path.resolve(templates.templatesPath), - this.projectPath, - ], - runningMessage: "Copying test helpers", - }); + if (contractTemplate === "psp22") { + this.taskQueue.push({ + task: copyTestHelpers, + args: [ + path.resolve(templates.templatesPath), + this.projectPath, + ], + runningMessage: "Copying test helpers", + }); + } this.taskQueue.push({ task: processTemplates, diff --git a/src/templates/contracts/flipper/contract/src/lib.rs.hbs b/src/templates/contracts/flipper/contract/src/lib.rs.hbs index a2329d0b..0681c362 100644 --- a/src/templates/contracts/flipper/contract/src/lib.rs.hbs +++ b/src/templates/contracts/flipper/contract/src/lib.rs.hbs @@ -57,14 +57,14 @@ mod {{contract_name_snake}} { /// We test if the default constructor does its job. #[ink::test] fn default_works() { - let flipper = Flipper::default(); + let flipper = {{contract_name_pascal}}::default(); assert_eq!(flipper.get(), false); } /// We test a simple use case of our contract. #[ink::test] fn it_works() { - let mut flipper = Flipper::new(false); + let mut flipper = {{contract_name_pascal}}::new(false); assert_eq!(flipper.get(), false); flipper.flip(); assert_eq!(flipper.get(), true); @@ -92,17 +92,17 @@ mod {{contract_name_snake}} { #[ink_e2e::test] async fn default_works(mut client: ink_e2e::Client) -> E2EResult<()> { // Given - let constructor = FlipperRef::default(); + let constructor = {{contract_name_pascal}}Ref::default(); // When let contract_account_id = client - .instantiate("flipper", &ink_e2e::alice(), constructor, 0, None) + .instantiate("{{contract_name_snake}}", &ink_e2e::alice(), constructor, 0, None) .await .expect("instantiate failed") .account_id; // Then - let get = build_message::(contract_account_id.clone()) + let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) .call(|flipper| flipper.get()); let get_result = client.call_dry_run(&ink_e2e::alice(), &get, 0, None).await; assert!(matches!(get_result.return_value(), false)); @@ -114,20 +114,20 @@ mod {{contract_name_snake}} { #[ink_e2e::test] async fn it_works(mut client: ink_e2e::Client) -> E2EResult<()> { // Given - let constructor = FlipperRef::new(false); + let constructor = {{contract_name_pascal}}Ref::new(false); let contract_account_id = client - .instantiate("flipper", &ink_e2e::bob(), constructor, 0, None) + .instantiate("{{contract_name_snake}}", &ink_e2e::bob(), constructor, 0, None) .await .expect("instantiate failed") .account_id; - let get = build_message::(contract_account_id.clone()) + let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) .call(|flipper| flipper.get()); let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; assert!(matches!(get_result.return_value(), false)); // When - let flip = build_message::(contract_account_id.clone()) + let flip = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) .call(|flipper| flipper.flip()); let _flip_result = client .call(&ink_e2e::bob(), flip, 0, None) @@ -135,7 +135,7 @@ mod {{contract_name_snake}} { .expect("flip failed"); // Then - let get = build_message::(contract_account_id.clone()) + let get = build_message::<{{contract_name_pascal}}Ref>(contract_account_id.clone()) .call(|flipper| flipper.get()); let get_result = client.call_dry_run(&ink_e2e::bob(), &get, 0, None).await; assert!(matches!(get_result.return_value(), true)); diff --git a/src/templates/test_helpers/lib.rs.hbs b/src/templates/test_helpers/lib.rs.hbs index 8e1afe23..a28f08ba 100644 --- a/src/templates/test_helpers/lib.rs.hbs +++ b/src/templates/test_helpers/lib.rs.hbs @@ -15,204 +15,4 @@ macro_rules! balance_of { .await .return_value() }}; -} - -#[macro_export] -macro_rules! owner_of { - ($contract_ref:ident, $client:ident, $address:ident, $id:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.owner_of($id)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! balance_of_37 { - ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $token:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()) - .call(|contract| contract.balance_of(address_of!($account), $token)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! has_role { - ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()) - .call(|contract| contract.has_role($role, Some(address_of!($account)))); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! grant_role { - ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()) - .call(|contract| contract.grant_role($role, Some(address_of!($account)))); - $client - .call(&ink_e2e::alice(), _msg, 0, None) - .await - .expect("grant_role failed") - .return_value() - }}; -} - -#[macro_export] -macro_rules! revoke_role { - ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $account:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()) - .call(|contract| contract.revoke_role($role, Some(address_of!($account)))); - $client - .call(&ink_e2e::alice(), _msg, 0, None) - .await - .expect("revoke_role failed") - .return_value() - }}; -} - -#[macro_export] -macro_rules! mint_dry_run { - ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $id:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $account:ident, $id:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); - $client - .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! mint { - ($contract_ref:ident, $client:ident, $address:ident, $account:ident, $id:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); - $client - .call(&ink_e2e::alice(), _msg, 0, None) - .await - .expect("mint failed") - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $account:ident, $id:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.mint(address_of!($account), $id)); - $client - .call(&ink_e2e::$signer(), _msg, 0, None) - .await - .expect("mint failed") - .return_value() - }}; -} - -#[macro_export] -macro_rules! get_role_member_count { - ($contract_ref:ident, $client:ident, $address:ident, $role:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.get_role_member_count($role)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! get_role_member { - ($contract_ref:ident, $client:ident, $address:ident, $role:expr, $index:expr) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.get_role_member($role, $index)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! get_shares { - ($contract_ref:ident, $client:ident, $address:ident, $user:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.shares(address_of!($user))); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; -} - -#[macro_export] -macro_rules! method_call { - ($contract_ref:ident, $client:ident, $address:ident, $method:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); - $client - .call(&ink_e2e::alice(), _msg, 0, None) - .await - .expect("method_call failed") - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); - $client - .call(&ink_e2e::$signer(), _msg, 0, None) - .await - .expect("method_call failed") - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $method:ident($($args:expr),*)) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); - $client - .call(&ink_e2e::alice(), _msg, 0, None) - .await - .expect("method_call failed") - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); - $client - .call(&ink_e2e::$signer(), _msg, 0, None) - .await - .expect("method_call failed") - .return_value() - }}; -} - -#[macro_export] -macro_rules! method_call_dry_run { - ($contract_ref:ident, $client:ident, $address:ident, $method:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $method:ident($($args:expr),*)) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); - $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) - .await - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method()); - $client - .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) - .await - .return_value() - }}; - ($contract_ref:ident, $client:ident, $address:ident, $signer:ident, $method:ident($($args:expr),*)) => \{{ - let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.$method($($args),*)); - $client - .call_dry_run(&ink_e2e::$signer(), &_msg, 0, None) - .await - .return_value() - }}; } \ No newline at end of file From 5e89608b4ade49a7b7075d3bc481a86b8b7eacdf Mon Sep 17 00:00:00 2001 From: prxgr4mm3r Date: Mon, 19 Feb 2024 19:57:52 +0200 Subject: [PATCH 04/11] fixes --- src/commands/contract/generate/tests.ts | 97 +++++++++++++++++++ .../{test/generate.ts => generate/types.ts} | 4 +- src/commands/contract/new.ts | 1 - .../contract/{test/index.ts => test.ts} | 18 ++-- src/commands/init/index.ts | 34 ++++--- src/lib/tasks.ts | 26 +++-- 6 files changed, 144 insertions(+), 36 deletions(-) create mode 100644 src/commands/contract/generate/tests.ts rename src/commands/contract/{test/generate.ts => generate/types.ts} (90%) rename src/commands/contract/{test/index.ts => test.ts} (93%) diff --git a/src/commands/contract/generate/tests.ts b/src/commands/contract/generate/tests.ts new file mode 100644 index 00000000..d97f8b8a --- /dev/null +++ b/src/commands/contract/generate/tests.ts @@ -0,0 +1,97 @@ +import { Args, Flags } from "@oclif/core"; +import { getTemplates, prepareTestFiles, processTemplates } from "../../../lib/index.js"; +import { Contract } from "../../../lib/contract.js"; +import { SwankyCommand } from "../../../lib/swankyCommand.js"; +import { ConfigError, FileError } from "../../../lib/errors.js"; +import path from "node:path"; +import { existsSync } from "node:fs"; +import inquirer from "inquirer"; +import { pascalCase } from "change-case"; + +export class GenerateTests extends SwankyCommand { + static description = "Generate types from compiled contract metadata"; + + static args = { + contractName: Args.string({ + name: "contractName", + required: true, + description: "Name of the contract", + }), + }; + + static flags = { + mocha: Flags.boolean({ + default: false, + description: "Generate tests with mocha", + }), + }; + + async run(): Promise { + const { args, flags } = await this.parse(GenerateTests); + + const contractRecord = this.swankyConfig.contracts[args.contractName]; + if (!contractRecord) { + throw new ConfigError( + `Cannot find a contract named ${args.contractName} in swanky.config.json` + ); + } + + const contract = new Contract(contractRecord); + + if (!(await contract.pathExists())) { + throw new FileError( + `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` + ); + } + + const artifactsCheck = await contract.artifactsExist(); + + if (!artifactsCheck.result) { + throw new FileError( + `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` + ); + } + + const templates = getTemplates(); + + const testsPath = path.resolve(process.cwd(), "tests"); + + let owerwrite = true; + if (!flags.mocha) + { + if (existsSync(path.resolve(testsPath, "test_helpers"))) { + owerwrite = (await inquirer.prompt({ + type: "confirm", + name: "overwrite", + message: "Test helpers already exist. Overwrite?", + default: false, + })).overwrite; + } + if (owerwrite) { + await this.spinner.runCommand(async () => { + await prepareTestFiles("e2e", templates.templatesPath, process.cwd(), args.contractName); + }, "Generating test helpers"); + } + } else { + if (existsSync(path.resolve(testsPath, args.contractName, "index.test.ts"))) { + owerwrite = (await inquirer.prompt({ + type: "confirm", + name: "overwrite", + message: `Mocha tests for ${args.contractName} are already exist. Overwrite?`, + default: false, + })).overwrite; + } + if (owerwrite) { + await this.spinner.runCommand(async () => { + await prepareTestFiles("mocha", templates.templatesPath, process.cwd(), args.contractName); + }, `Generating tests for ${args.contractName} with mocha`); + await this.spinner.runCommand(async () => { + await processTemplates(process.cwd(), { + contract_name: args.contractName, + contract_name_pascal: pascalCase(args.contractName), + }) + }, 'Processing templates') + } + } + } +} diff --git a/src/commands/contract/test/generate.ts b/src/commands/contract/generate/types.ts similarity index 90% rename from src/commands/contract/test/generate.ts rename to src/commands/contract/generate/types.ts index e498b9fb..506f0c9c 100644 --- a/src/commands/contract/test/generate.ts +++ b/src/commands/contract/generate/types.ts @@ -4,7 +4,7 @@ import { Contract } from "../../../lib/contract.js"; import { SwankyCommand } from "../../../lib/swankyCommand.js"; import { ConfigError, FileError } from "../../../lib/errors.js"; -export class ContractTestTypegen extends SwankyCommand { +export class GenerateTypes extends SwankyCommand { static description = "Generate types from compiled contract metadata"; static args = { @@ -16,7 +16,7 @@ export class ContractTestTypegen extends SwankyCommand { - const { args } = await this.parse(ContractTestTypegen); + const { args } = await this.parse(GenerateTypes); const contractRecord = this.swankyConfig.contracts[args.contractName]; if (!contractRecord) { diff --git a/src/commands/contract/new.ts b/src/commands/contract/new.ts index 0936663f..c455a801 100644 --- a/src/commands/contract/new.ts +++ b/src/commands/contract/new.ts @@ -93,7 +93,6 @@ export class NewContract extends SwankyCommand { ); await ensureDir(path.resolve(projectPath, "artifacts", args.contractName)); - await ensureDir(path.resolve(projectPath, "tests", args.contractName)); await this.spinner.runCommand(async () => { this.swankyConfig.contracts[args.contractName] = { diff --git a/src/commands/contract/test/index.ts b/src/commands/contract/test.ts similarity index 93% rename from src/commands/contract/test/index.ts rename to src/commands/contract/test.ts index 10479a2d..774f564e 100644 --- a/src/commands/contract/test/index.ts +++ b/src/commands/contract/test.ts @@ -3,13 +3,13 @@ import { Flags, Args } from "@oclif/core"; import path from "node:path"; import { globby } from "globby"; import Mocha from "mocha"; -import { emptyDir } from "fs-extra/esm"; +import { emptyDir, pathExists } from "fs-extra/esm"; import shell from "shelljs"; -import { Contract } from "../../../lib/contract.js"; -import { SwankyCommand } from "../../../lib/swankyCommand.js"; -import { ConfigError, FileError, InputError, ProcessError, TestError } from "../../../lib/errors.js"; +import { Contract } from "../../lib/contract.js"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { ConfigError, FileError, InputError, ProcessError, TestError } from "../../lib/errors.js"; import { spawn } from "node:child_process"; -import { Spinner } from "../../../lib/index.js"; +import { Spinner } from "../../lib/index.js"; declare global { var contractTypesPath: string; // eslint-disable-line no-var @@ -53,8 +53,6 @@ export class TestContract extends SwankyCommand { ? Object.keys(this.swankyConfig.contracts) : [args.contractName]; - const testDir = path.resolve("tests"); - const spinner = new Spinner(); for (const contractName of contractNames) { @@ -130,6 +128,12 @@ export class TestContract extends SwankyCommand { ); } else { + const testDir = path.resolve("tests"); + + if (!pathExists(testDir)) { + throw new FileError(`Tests folder does not exist: ${testDir}`); + } + const artifactsCheck = await contract.artifactsExist(); if (!artifactsCheck.result) { diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index 01050221..aefde1bb 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -1,22 +1,23 @@ import { Args, Flags } from "@oclif/core"; import path from "node:path"; -import { ensureDir, writeJSON, pathExists, copy, outputFile, readJSON, remove } from "fs-extra/esm"; -import { stat, readdir, readFile } from "fs/promises"; +import { copy, ensureDir, outputFile, pathExists, readJSON, remove, writeJSON } from "fs-extra/esm"; +import { readdir, readFile, stat } from "fs/promises"; import { execaCommand, execaCommandSync } from "execa"; import { paramCase, pascalCase, snakeCase } from "change-case"; import inquirer from "inquirer"; import TOML from "@iarna/toml"; import { choice, email, name, pickTemplate } from "../../lib/prompts.js"; import { + ChainAccount, checkCliDependencies, copyCommonTemplateFiles, copyContractTemplateFiles, downloadNode, + getTemplates, installDeps, - ChainAccount, + prepareTestFiles, processTemplates, swankyNode, - getTemplates, copyTestHelpers, } from "../../lib/index.js"; import { DEFAULT_ASTAR_NETWORK_URL, @@ -26,7 +27,7 @@ import { } from "../../lib/consts.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; import { InputError, UnknownError } from "../../lib/errors.js"; -import { GlobEntry, globby } from "globby"; +import { globby, GlobEntry } from "globby"; import { merge } from "lodash-es"; import inquirerFuzzyPath from "inquirer-fuzzy-path"; import { SwankyConfig } from "../../types/index.js"; @@ -91,6 +92,7 @@ export class Init extends SwankyCommand { super(argv, config); (this.constructor as typeof SwankyCommand).ENSURE_SWANKY_CONFIG = false; } + projectPath = ""; configBuilder: Partial = { @@ -188,7 +190,6 @@ export class Init extends SwankyCommand { Object.keys(this.configBuilder.contracts!).forEach(async (contractName) => { await ensureDir(path.resolve(this.projectPath, "artifacts", contractName)); - await ensureDir(path.resolve(this.projectPath, "tests", contractName)); }); this.taskQueue.push({ @@ -214,7 +215,7 @@ export class Init extends SwankyCommand { runningMessage, successMessage, failMessage, - shouldExitOnError + shouldExitOnError, ); if (result && callback) { callback(result as string); @@ -271,8 +272,9 @@ export class Init extends SwankyCommand { if (contractTemplate === "psp22") { this.taskQueue.push({ - task: copyTestHelpers, + task: prepareTestFiles, args: [ + "e2e", path.resolve(templates.templatesPath), this.projectPath, ], @@ -317,7 +319,7 @@ export class Init extends SwankyCommand { } catch (cause) { throw new InputError( `Error reading target directory [${chalk.yellowBright(pathToExistingProject)}]`, - { cause } + { cause }, ); } @@ -337,7 +339,7 @@ export class Init extends SwankyCommand { const candidatesList: CopyCandidates = await getCopyCandidatesList( pathToExistingProject, - copyGlobsList + copyGlobsList, ); const testDir = await detectTests(pathToExistingProject); @@ -505,10 +507,10 @@ async function confirmCopyList(candidatesList: CopyCandidates) { ( item: PathEntry & { group: "contracts" | "crates" | "tests"; - } + }, ) => { resultingList[item.group]?.push(item); - } + }, ); return resultingList; } @@ -535,7 +537,7 @@ async function detectTests(pathToExistingProject: string): Promise { const { selectedDirectory } = await inquirer.prompt([ { @@ -606,7 +608,7 @@ async function getCopyCandidatesList( pathsToCopy: { contractsDirectories: string[]; cratesDirectories: string[]; - } + }, ) { const detectedPaths = { contracts: await getDirsAndFiles(projectPath, pathsToCopy.contractsDirectories), @@ -627,7 +629,7 @@ async function getGlobPaths(projectPath: string, globList: string[], isDirOnly: onlyDirectories: isDirOnly, deep: 1, objectMode: true, - } + }, ); } diff --git a/src/lib/tasks.ts b/src/lib/tasks.ts index b43e7c2b..31c5c6dd 100644 --- a/src/lib/tasks.ts +++ b/src/lib/tasks.ts @@ -53,20 +53,26 @@ export async function copyContractTemplateFiles( path.resolve(contractTemplatePath, "contract"), path.resolve(projectPath, "contracts", contractName) ); - await copy( - path.resolve(contractTemplatePath, "test"), - path.resolve(projectPath, "tests", contractName) - ); } -export async function copyTestHelpers( +export async function prepareTestFiles( + testType: "e2e" | "mocha", templatePath: string, - projectPath: string + projectPath: string, + contractName: string ) { - await copy( - path.resolve(templatePath, "test_helpers"), - path.resolve(projectPath, "tests", "test_helpers") - ); + if (testType === "e2e") { + await copy( + path.resolve(templatePath, "test_helpers"), + path.resolve(projectPath, "tests", "test_helpers") + ); + } + else { + await copy( + path.resolve(templatePath, "contracts", contractName, "test"), + path.resolve(projectPath, "tests", contractName) + ); + } } export async function processTemplates(projectPath: string, templateData: Record) { From 227ff3e01c8964486b7abe5983376d9e52472095 Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Tue, 20 Feb 2024 10:26:16 +0100 Subject: [PATCH 05/11] fix: Exhaustive switch used for prepareTestFiles helper --- src/lib/tasks.ts | 45 +++++++++++++++++++++++++++++---------------- src/types/index.ts | 2 ++ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/lib/tasks.ts b/src/lib/tasks.ts index 31c5c6dd..6d147a47 100644 --- a/src/lib/tasks.ts +++ b/src/lib/tasks.ts @@ -1,5 +1,5 @@ import { execaCommand } from "execa"; -import { ensureDir, copy, remove } from "fs-extra/esm"; +import { ensureDir, copy, remove, pathExistsSync } from "fs-extra/esm"; import { rename, readFile, rm, writeFile } from "fs/promises"; import path from "node:path"; import { globby } from "globby"; @@ -9,8 +9,8 @@ import process from "node:process"; import { nodeInfo } from "./nodeInfo.js"; import decompress from "decompress"; import { Spinner } from "./spinner.js"; -import { SupportedPlatforms, SupportedArch } from "../types/index.js"; -import { ConfigError, NetworkError } from "./errors.js"; +import { SupportedPlatforms, SupportedArch, TestType } from "../types/index.js"; +import { ConfigError, NetworkError, ProcessError } from "./errors.js"; export async function checkCliDependencies(spinner: Spinner) { const dependencyList = [ @@ -56,22 +56,35 @@ export async function copyContractTemplateFiles( } export async function prepareTestFiles( - testType: "e2e" | "mocha", + testType: TestType, templatePath: string, projectPath: string, - contractName: string + contractName?: string ) { - if (testType === "e2e") { - await copy( - path.resolve(templatePath, "test_helpers"), - path.resolve(projectPath, "tests", "test_helpers") - ); - } - else { - await copy( - path.resolve(templatePath, "contracts", contractName, "test"), - path.resolve(projectPath, "tests", contractName) - ); + switch (testType) { + case "e2e": { + const e2ePath = path.resolve(projectPath, "tests", "test_helpers"); + if (pathExistsSync(e2ePath)) { + throw new ProcessError(`${e2ePath} already exists and will not be overwritten.`); + } + await copy(path.resolve(templatePath, "test_helpers"), e2ePath); + break; + } + case "mocha": { + if (!contractName) { + throw new ProcessError("contractName is required for mocha tests"); + } + const mochaPath = path.resolve(projectPath, "tests", contractName); + if (pathExistsSync(mochaPath)) { + throw new ProcessError(`${mochaPath} already exists and will not be overwritten.`); + } + await copy(path.resolve(templatePath, "contracts", contractName, "test"), mochaPath); + break; + } + default: { + // This case will make the switch exhaustive + throw new ProcessError("Unhandled test type"); + } } } diff --git a/src/types/index.ts b/src/types/index.ts index aa13a0df..609ea5fe 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -51,3 +51,5 @@ export interface SwankyConfig { export type SupportedPlatforms = "darwin" | "linux"; export type SupportedArch = "arm64" | "x64"; + +export type TestType = "e2e" | "mocha"; From cde7c06995f9fe0237238c4d4c960ee311a200ee Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Tue, 20 Feb 2024 11:00:38 +0100 Subject: [PATCH 06/11] fix: Cpy e2e test helpers on new command for psp22 template --- src/commands/contract/new.ts | 8 ++++++++ src/commands/init/index.ts | 6 +----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/commands/contract/new.ts b/src/commands/contract/new.ts index c455a801..573f5f18 100644 --- a/src/commands/contract/new.ts +++ b/src/commands/contract/new.ts @@ -6,6 +6,7 @@ import { copyContractTemplateFiles, processTemplates, getTemplates, + prepareTestFiles, } from "../../lib/index.js"; import { email, name, pickTemplate } from "../../lib/prompts.js"; import { paramCase, pascalCase, snakeCase } from "change-case"; @@ -78,6 +79,13 @@ export class NewContract extends SwankyCommand { "Copying contract template files" ); + if (contractTemplate === "psp22") { + await this.spinner.runCommand( + () => prepareTestFiles("e2e", path.resolve(templates.templatesPath), projectPath), + "Copying test helpers" + ); + } + await this.spinner.runCommand( () => processTemplates(projectPath, { diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index aefde1bb..a95ab067 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -273,11 +273,7 @@ export class Init extends SwankyCommand { if (contractTemplate === "psp22") { this.taskQueue.push({ task: prepareTestFiles, - args: [ - "e2e", - path.resolve(templates.templatesPath), - this.projectPath, - ], + args: ["e2e", path.resolve(templates.templatesPath), this.projectPath], runningMessage: "Copying test helpers", }); } From 5b2461d0a24438b1486243bdb15e2b8b4a6d399c Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Tue, 20 Feb 2024 12:26:30 +0100 Subject: [PATCH 07/11] chore: Remove local-node flag --- src/commands/contract/test.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/commands/contract/test.ts b/src/commands/contract/test.ts index 774f564e..3ef72bfe 100644 --- a/src/commands/contract/test.ts +++ b/src/commands/contract/test.ts @@ -3,7 +3,7 @@ import { Flags, Args } from "@oclif/core"; import path from "node:path"; import { globby } from "globby"; import Mocha from "mocha"; -import { emptyDir, pathExists } from "fs-extra/esm"; +import { emptyDir, pathExistsSync } from "fs-extra/esm"; import shell from "shelljs"; import { Contract } from "../../lib/contract.js"; import { SwankyCommand } from "../../lib/swankyCommand.js"; @@ -22,11 +22,7 @@ export class TestContract extends SwankyCommand { all: Flags.boolean({ default: false, char: "a", - description: "Set all to true to compile all contracts", - }), - "local-node": Flags.boolean({ - default: false, - description: "Run tests on a local node", + description: "Run tests for all contracts", }), mocha: Flags.boolean({ default: false, @@ -71,14 +67,6 @@ export class TestContract extends SwankyCommand { ); } - if (flags["local-node"]) { - if (!this.swankyConfig.node.localPath) - { - throw new ConfigError("Local node path not found in swanky.config.json"); - } - process.env.CONTRACTS_NODE = this.swankyConfig.node.localPath; - } - console.log(`Testing contract: ${contractName}`); if (!flags.mocha) { @@ -130,7 +118,7 @@ export class TestContract extends SwankyCommand { const testDir = path.resolve("tests"); - if (!pathExists(testDir)) { + if (!pathExistsSync(testDir)) { throw new FileError(`Tests folder does not exist: ${testDir}`); } From a692fef8e05f7a486743d73c7276303f12a45d6f Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Tue, 20 Feb 2024 12:27:52 +0100 Subject: [PATCH 08/11] refactor: More robust and concise code for the generate tests command --- src/commands/contract/generate/tests.ts | 131 ++++++++++++++++-------- src/commands/contract/new.ts | 15 ++- src/lib/tasks.ts | 26 ++--- 3 files changed, 115 insertions(+), 57 deletions(-) diff --git a/src/commands/contract/generate/tests.ts b/src/commands/contract/generate/tests.ts index d97f8b8a..4a19c085 100644 --- a/src/commands/contract/generate/tests.ts +++ b/src/commands/contract/generate/tests.ts @@ -6,10 +6,11 @@ import { ConfigError, FileError } from "../../../lib/errors.js"; import path from "node:path"; import { existsSync } from "node:fs"; import inquirer from "inquirer"; -import { pascalCase } from "change-case"; +import { paramCase, pascalCase } from "change-case"; +import { TestType } from "../../../index.js"; export class GenerateTests extends SwankyCommand { - static description = "Generate types from compiled contract metadata"; + static description = "Generate test files for the specified contract"; static args = { contractName: Args.string({ @@ -20,9 +21,12 @@ export class GenerateTests extends SwankyCommand { }; static flags = { + template: Flags.string({ + options: getTemplates().contractTemplatesList, + }), mocha: Flags.boolean({ default: false, - description: "Generate tests with mocha", + description: "Generate mocha test files", }), }; @@ -52,46 +56,93 @@ export class GenerateTests extends SwankyCommand { ); } + const testType: TestType = flags.mocha ? "mocha" : "e2e"; + const testsFolderPath = path.resolve("tests"); + const testPath = this.getTestPath(testType, testsFolderPath, args.contractName); + const templates = getTemplates(); + const templateName = await this.resolveTemplateName(flags, templates.contractTemplatesList); + + const overwrite = await this.checkOverwrite(testPath, args.contractName, testType); + if (!overwrite) return; + + await this.generateTests( + testType, + templates.templatesPath, + process.cwd(), + args.contractName, + templateName + ); + } - const testsPath = path.resolve(process.cwd(), "tests"); - - let owerwrite = true; - if (!flags.mocha) - { - if (existsSync(path.resolve(testsPath, "test_helpers"))) { - owerwrite = (await inquirer.prompt({ - type: "confirm", - name: "overwrite", - message: "Test helpers already exist. Overwrite?", - default: false, - })).overwrite; - } - if (owerwrite) { - await this.spinner.runCommand(async () => { - await prepareTestFiles("e2e", templates.templatesPath, process.cwd(), args.contractName); - }, "Generating test helpers"); - } + async checkOverwrite( + testPath: string, + contractName: string, + testType: TestType + ): Promise { + if (!existsSync(testPath)) return true; // No need to overwrite + const message = + testType === "e2e" + ? "Test helpers already exist. Overwrite?" + : `Mocha tests for ${contractName} already exist. Overwrite?`; + + const { overwrite } = await inquirer.prompt({ + type: "confirm", + name: "overwrite", + message, + default: false, + }); + + return overwrite; + } + + getTestPath(testType: TestType, testsPath: string, contractName: string): string { + return testType === "e2e" + ? path.resolve(testsPath, "test_helpers") + : path.resolve(testsPath, contractName, "index.test.ts"); + } + + async resolveTemplateName(flags: any, templates: any): Promise { + if (flags.mocha && !flags.template) { + if (!templates?.length) throw new ConfigError("Template list is empty!"); + const response = await inquirer.prompt([{ + type: "list", + name: "template", + message: "Choose a contract template:", + choices: templates, + }]); + return response.template; + } + return flags.template; + } + + async generateTests( + testType: TestType, + templatesPath: string, + projectPath: string, + contractName: string, + templateName?: string + ): Promise { + if (testType === "e2e") { + await this.spinner.runCommand( + () => prepareTestFiles("e2e", templatesPath, projectPath), + "Generating e2e test helpers" + ); } else { - if (existsSync(path.resolve(testsPath, args.contractName, "index.test.ts"))) { - owerwrite = (await inquirer.prompt({ - type: "confirm", - name: "overwrite", - message: `Mocha tests for ${args.contractName} are already exist. Overwrite?`, - default: false, - })).overwrite; - } - if (owerwrite) { - await this.spinner.runCommand(async () => { - await prepareTestFiles("mocha", templates.templatesPath, process.cwd(), args.contractName); - }, `Generating tests for ${args.contractName} with mocha`); - await this.spinner.runCommand(async () => { - await processTemplates(process.cwd(), { - contract_name: args.contractName, - contract_name_pascal: pascalCase(args.contractName), - }) - }, 'Processing templates') - } + await this.spinner.runCommand( + () => prepareTestFiles("mocha", templatesPath, projectPath, templateName, contractName), + `Generating tests for ${contractName} with mocha` + ); } + await this.spinner.runCommand( + () => + processTemplates(projectPath, { + project_name: paramCase(this.config.pjson.name), + swanky_version: this.config.pjson.version, + contract_name: contractName, + contract_name_pascal: pascalCase(contractName), + }), + "Processing templates" + ); } } diff --git a/src/commands/contract/new.ts b/src/commands/contract/new.ts index 573f5f18..c671886f 100644 --- a/src/commands/contract/new.ts +++ b/src/commands/contract/new.ts @@ -1,6 +1,6 @@ import { Args, Flags } from "@oclif/core"; import path from "node:path"; -import { ensureDir, pathExists, writeJSON } from "fs-extra/esm"; +import { ensureDir, pathExists, pathExistsSync, writeJSON } from "fs-extra/esm"; import { checkCliDependencies, copyContractTemplateFiles, @@ -80,10 +80,15 @@ export class NewContract extends SwankyCommand { ); if (contractTemplate === "psp22") { - await this.spinner.runCommand( - () => prepareTestFiles("e2e", path.resolve(templates.templatesPath), projectPath), - "Copying test helpers" - ); + const e2eTestHelpersPath = path.resolve(projectPath, "tests", "test_helpers"); + if (!pathExistsSync(e2eTestHelpersPath)) { + await this.spinner.runCommand( + () => prepareTestFiles("e2e", path.resolve(templates.templatesPath), projectPath), + "Copying e2e test helpers" + ); + } else { + console.log("e2e test helpers already exist. No files were copied."); + } } await this.spinner.runCommand( diff --git a/src/lib/tasks.ts b/src/lib/tasks.ts index 6d147a47..a85031f7 100644 --- a/src/lib/tasks.ts +++ b/src/lib/tasks.ts @@ -1,5 +1,5 @@ import { execaCommand } from "execa"; -import { ensureDir, copy, remove, pathExistsSync } from "fs-extra/esm"; +import { ensureDir, copy, remove } from "fs-extra/esm"; import { rename, readFile, rm, writeFile } from "fs/promises"; import path from "node:path"; import { globby } from "globby"; @@ -59,26 +59,28 @@ export async function prepareTestFiles( testType: TestType, templatePath: string, projectPath: string, + templateName?: string, contractName?: string ) { switch (testType) { case "e2e": { - const e2ePath = path.resolve(projectPath, "tests", "test_helpers"); - if (pathExistsSync(e2ePath)) { - throw new ProcessError(`${e2ePath} already exists and will not be overwritten.`); - } - await copy(path.resolve(templatePath, "test_helpers"), e2ePath); + await copy( + path.resolve(templatePath, "test_helpers"), + path.resolve(projectPath, "tests", "test_helpers") + ); break; } case "mocha": { - if (!contractName) { - throw new ProcessError("contractName is required for mocha tests"); + if (!templateName) { + throw new ProcessError("'templateName' argument is required for mocha tests"); } - const mochaPath = path.resolve(projectPath, "tests", contractName); - if (pathExistsSync(mochaPath)) { - throw new ProcessError(`${mochaPath} already exists and will not be overwritten.`); + if (!contractName) { + throw new ProcessError("'contractName' argument is required for mocha tests"); } - await copy(path.resolve(templatePath, "contracts", contractName, "test"), mochaPath); + await copy( + path.resolve(templatePath, "contracts", templateName, "test"), + path.resolve(projectPath, "tests", contractName) + ); break; } default: { From 8fd756597c7ac381bbf3e45989fe36228e9145ea Mon Sep 17 00:00:00 2001 From: Igor Papandinas Date: Tue, 20 Feb 2024 15:16:43 +0100 Subject: [PATCH 09/11] feat: Generate command implemented --- src/commands/{contract => }/generate/tests.ts | 98 +++++++++++-------- src/commands/{contract => }/generate/types.ts | 8 +- 2 files changed, 60 insertions(+), 46 deletions(-) rename src/commands/{contract => }/generate/tests.ts (59%) rename src/commands/{contract => }/generate/types.ts (83%) diff --git a/src/commands/contract/generate/tests.ts b/src/commands/generate/tests.ts similarity index 59% rename from src/commands/contract/generate/tests.ts rename to src/commands/generate/tests.ts index 4a19c085..09af4ead 100644 --- a/src/commands/contract/generate/tests.ts +++ b/src/commands/generate/tests.ts @@ -1,13 +1,13 @@ import { Args, Flags } from "@oclif/core"; -import { getTemplates, prepareTestFiles, processTemplates } from "../../../lib/index.js"; -import { Contract } from "../../../lib/contract.js"; -import { SwankyCommand } from "../../../lib/swankyCommand.js"; -import { ConfigError, FileError } from "../../../lib/errors.js"; +import { getTemplates, prepareTestFiles, processTemplates } from "../../lib/index.js"; +import { Contract } from "../../lib/contract.js"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { ConfigError, FileError, InputError } from "../../lib/errors.js"; import path from "node:path"; import { existsSync } from "node:fs"; import inquirer from "inquirer"; import { paramCase, pascalCase } from "change-case"; -import { TestType } from "../../../index.js"; +import { TestType } from "../../index.js"; export class GenerateTests extends SwankyCommand { static description = "Generate test files for the specified contract"; @@ -15,7 +15,7 @@ export class GenerateTests extends SwankyCommand { static args = { contractName: Args.string({ name: "contractName", - required: true, + required: false, description: "Name of the contract", }), }; @@ -33,27 +33,12 @@ export class GenerateTests extends SwankyCommand { async run(): Promise { const { args, flags } = await this.parse(GenerateTests); - const contractRecord = this.swankyConfig.contracts[args.contractName]; - if (!contractRecord) { - throw new ConfigError( - `Cannot find a contract named ${args.contractName} in swanky.config.json` - ); - } - - const contract = new Contract(contractRecord); - - if (!(await contract.pathExists())) { - throw new FileError( - `Path to contract ${args.contractName} does not exist: ${contract.contractPath}` - ); - } - - const artifactsCheck = await contract.artifactsExist(); + if (flags.mocha) { + if (!args.contractName) { + throw new InputError("The 'contractName' argument is required to generate mocha tests."); + } - if (!artifactsCheck.result) { - throw new FileError( - `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` - ); + await this.checkContract(args.contractName) } const testType: TestType = flags.mocha ? "mocha" : "e2e"; @@ -63,7 +48,7 @@ export class GenerateTests extends SwankyCommand { const templates = getTemplates(); const templateName = await this.resolveTemplateName(flags, templates.contractTemplatesList); - const overwrite = await this.checkOverwrite(testPath, args.contractName, testType); + const overwrite = await this.checkOverwrite(testPath, testType, args.contractName); if (!overwrite) return; await this.generateTests( @@ -75,10 +60,33 @@ export class GenerateTests extends SwankyCommand { ); } + async checkContract(name: string) { + const contractRecord = this.swankyConfig.contracts[name]; + if (!contractRecord) { + throw new ConfigError( + `Cannot find a contract named ${name} in swanky.config.json` + ); + } + + const contract = new Contract(contractRecord); + if (!(await contract.pathExists())) { + throw new FileError( + `Path to contract ${name} does not exist: ${contract.contractPath}` + ); + } + + const artifactsCheck = await contract.artifactsExist(); + if (!artifactsCheck.result) { + throw new FileError( + `No artifact file found at path: ${artifactsCheck.missingPaths.toString()}` + ); + } + } + async checkOverwrite( testPath: string, - contractName: string, - testType: TestType + testType: TestType, + contractName?: string ): Promise { if (!existsSync(testPath)) return true; // No need to overwrite const message = @@ -96,21 +104,27 @@ export class GenerateTests extends SwankyCommand { return overwrite; } - getTestPath(testType: TestType, testsPath: string, contractName: string): string { - return testType === "e2e" - ? path.resolve(testsPath, "test_helpers") - : path.resolve(testsPath, contractName, "index.test.ts"); + getTestPath(testType: TestType, testsPath: string, contractName?: string): string { + if (testType === "e2e") { + return path.resolve(testsPath, "test_helpers"); + } else if (testType === "mocha" && contractName) { + return path.resolve(testsPath, contractName, "index.test.ts"); + } else { + throw new InputError("The 'contractName' argument is required to generate mocha tests."); + } } async resolveTemplateName(flags: any, templates: any): Promise { if (flags.mocha && !flags.template) { if (!templates?.length) throw new ConfigError("Template list is empty!"); - const response = await inquirer.prompt([{ - type: "list", - name: "template", - message: "Choose a contract template:", - choices: templates, - }]); + const response = await inquirer.prompt([ + { + type: "list", + name: "template", + message: "Choose a contract template:", + choices: templates, + }, + ]); return response.template; } return flags.template; @@ -120,7 +134,7 @@ export class GenerateTests extends SwankyCommand { testType: TestType, templatesPath: string, projectPath: string, - contractName: string, + contractName?: string, templateName?: string ): Promise { if (testType === "e2e") { @@ -139,8 +153,8 @@ export class GenerateTests extends SwankyCommand { processTemplates(projectPath, { project_name: paramCase(this.config.pjson.name), swanky_version: this.config.pjson.version, - contract_name: contractName, - contract_name_pascal: pascalCase(contractName), + contract_name: contractName ?? "", + contract_name_pascal: contractName ? pascalCase(contractName) : "", }), "Processing templates" ); diff --git a/src/commands/contract/generate/types.ts b/src/commands/generate/types.ts similarity index 83% rename from src/commands/contract/generate/types.ts rename to src/commands/generate/types.ts index 506f0c9c..79cf6134 100644 --- a/src/commands/contract/generate/types.ts +++ b/src/commands/generate/types.ts @@ -1,8 +1,8 @@ import { Args } from "@oclif/core"; -import { generateTypes } from "../../../lib/index.js"; -import { Contract } from "../../../lib/contract.js"; -import { SwankyCommand } from "../../../lib/swankyCommand.js"; -import { ConfigError, FileError } from "../../../lib/errors.js"; +import { generateTypes } from "../../lib/index.js"; +import { Contract } from "../../lib/contract.js"; +import { SwankyCommand } from "../../lib/swankyCommand.js"; +import { ConfigError, FileError } from "../../lib/errors.js"; export class GenerateTypes extends SwankyCommand { static description = "Generate types from compiled contract metadata"; From 03314f777c3f9bb34d1fcfd2d4d1a1bf2eec4db0 Mon Sep 17 00:00:00 2001 From: prxgr4mm3r Date: Tue, 20 Feb 2024 18:12:40 +0200 Subject: [PATCH 10/11] fix: Remove .hbs.hbs extensions --- src/commands/init/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/init/index.ts b/src/commands/init/index.ts index a95ab067..7174c131 100644 --- a/src/commands/init/index.ts +++ b/src/commands/init/index.ts @@ -390,11 +390,11 @@ export class Init extends SwankyCommand { this.taskQueue.push({ task: async (tomlObject, projectPath) => { const tomlString = TOML.stringify(tomlObject); - const rootTomlPath = path.resolve(projectPath, "Cargo.toml.hbs.hbs"); + const rootTomlPath = path.resolve(projectPath, "Cargo.toml"); await outputFile(rootTomlPath, tomlString); }, args: [rootToml, this.projectPath], - runningMessage: "Writing Cargo.toml.hbs.hbs", + runningMessage: "Writing Cargo.toml", }); this.taskQueue.push({ @@ -442,9 +442,9 @@ async function detectModuleNames(copyList: CopyCandidates): Promise Date: Thu, 22 Feb 2024 17:15:57 +0200 Subject: [PATCH 11/11] fix: use absolute path in test helpers macros --- src/templates/test_helpers/lib.rs.hbs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/templates/test_helpers/lib.rs.hbs b/src/templates/test_helpers/lib.rs.hbs index a28f08ba..b45d2688 100644 --- a/src/templates/test_helpers/lib.rs.hbs +++ b/src/templates/test_helpers/lib.rs.hbs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! address_of { ($account:ident) => { - ink_e2e::account_id(ink_e2e::AccountKeyring::$account) + ::ink_e2e::account_id(::ink_e2e::AccountKeyring::$account) }; } @@ -11,7 +11,7 @@ macro_rules! balance_of { let _msg = build_message::<$contract_ref>($address.clone()).call(|contract| contract.balance_of(address_of!($account))); $client - .call_dry_run(&ink_e2e::alice(), &_msg, 0, None) + .call_dry_run(&::ink_e2e::alice(), &_msg, 0, None) .await .return_value() }};