diff --git a/crates/forge/assets/CounterTemplate.s.sol b/crates/forge/assets/solidity/CounterTemplate.s.sol similarity index 100% rename from crates/forge/assets/CounterTemplate.s.sol rename to crates/forge/assets/solidity/CounterTemplate.s.sol diff --git a/crates/forge/assets/CounterTemplate.sol b/crates/forge/assets/solidity/CounterTemplate.sol similarity index 100% rename from crates/forge/assets/CounterTemplate.sol rename to crates/forge/assets/solidity/CounterTemplate.sol diff --git a/crates/forge/assets/CounterTemplate.t.sol b/crates/forge/assets/solidity/CounterTemplate.t.sol similarity index 100% rename from crates/forge/assets/CounterTemplate.t.sol rename to crates/forge/assets/solidity/CounterTemplate.t.sol diff --git a/crates/forge/assets/workflowTemplate.yml b/crates/forge/assets/solidity/workflowTemplate.yml similarity index 61% rename from crates/forge/assets/workflowTemplate.yml rename to crates/forge/assets/solidity/workflowTemplate.yml index 4481ec6a8b2de..57bdb22cdfc13 100644 --- a/crates/forge/assets/workflowTemplate.yml +++ b/crates/forge/assets/solidity/workflowTemplate.yml @@ -13,7 +13,7 @@ jobs: name: Foundry project runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: submodules: recursive @@ -21,20 +21,13 @@ jobs: uses: foundry-rs/foundry-toolchain@v1 - name: Show Forge version - run: | - forge --version + run: forge --version - name: Run Forge fmt - run: | - forge fmt --check - id: fmt + run: forge fmt --check - name: Run Forge build - run: | - forge build --sizes - id: build + run: forge build --sizes - name: Run Forge tests - run: | - forge test -vvv - id: test + run: forge test -vvv diff --git a/crates/forge/assets/vyper/CounterTemplate.s.sol b/crates/forge/assets/vyper/CounterTemplate.s.sol new file mode 100644 index 0000000000000..aab0faa29c80c --- /dev/null +++ b/crates/forge/assets/vyper/CounterTemplate.s.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script} from "forge-std/Script.sol"; +import {ICounter} from "../src/ICounter.sol"; + +contract CounterScript is Script { + ICounter public counter; + + function setUp() public {} + + function run() public { + vm.startBroadcast(); + + counter = ICounter(deployCode("src/Counter.vy")); + + vm.stopBroadcast(); + } +} diff --git a/crates/forge/assets/vyper/CounterTemplate.t.sol b/crates/forge/assets/vyper/CounterTemplate.t.sol new file mode 100644 index 0000000000000..b492089b190ea --- /dev/null +++ b/crates/forge/assets/vyper/CounterTemplate.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; +import {ICounter} from "../src/ICounter.sol"; + +contract CounterTest is Test { + ICounter public counter; + + function setUp() public { + counter = ICounter(deployCode("src/Counter.vy")); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/crates/forge/assets/vyper/CounterTemplate.vy b/crates/forge/assets/vyper/CounterTemplate.vy new file mode 100644 index 0000000000000..f0a00db3d6209 --- /dev/null +++ b/crates/forge/assets/vyper/CounterTemplate.vy @@ -0,0 +1,11 @@ +# pragma version ~=0.4.3 + +number: public(uint256) + +@external +def setNumber(newNumber: uint256): + self.number = newNumber + +@external +def increment(): + self.number += 1 \ No newline at end of file diff --git a/crates/forge/assets/vyper/ICounterTemplate.sol b/crates/forge/assets/vyper/ICounterTemplate.sol new file mode 100644 index 0000000000000..a3219a951776f --- /dev/null +++ b/crates/forge/assets/vyper/ICounterTemplate.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +interface ICounter { + function number() external view returns (uint256); + function setNumber(uint256 newNumber) external; + function increment() external; +} diff --git a/crates/forge/assets/vyper/workflowTemplate.yml b/crates/forge/assets/vyper/workflowTemplate.yml new file mode 100644 index 0000000000000..72fb01522a2b3 --- /dev/null +++ b/crates/forge/assets/vyper/workflowTemplate.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + + - name: Show Forge version + run: forge --version + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.x" + + - name: Install Vyper + run: pip install git+https://github.com/vyperlang/vyper.git@v0.4.3 + + - name: Show the Vyper version + run: vyper --version + + - name: Run Forge fmt + run: forge fmt --check + + - name: Run Forge build + run: forge build --sizes + + - name: Run Forge tests + run: forge test -vvv diff --git a/crates/forge/src/cmd/init.rs b/crates/forge/src/cmd/init.rs index 52b0b6a6a3b78..f46840e6fdb53 100644 --- a/crates/forge/src/cmd/init.rs +++ b/crates/forge/src/cmd/init.rs @@ -37,6 +37,10 @@ pub struct InitArgs { #[arg(long, conflicts_with = "template")] pub vscode: bool, + /// Initialize a Vyper project template. + #[arg(long, conflicts_with = "template")] + pub vyper: bool, + /// Use the parent git repository instead of initializing a new one. /// Only valid if the target is in a git repository. #[arg(long, conflicts_with = "template")] @@ -48,7 +52,8 @@ pub struct InitArgs { impl InitArgs { pub fn run(self) -> Result<()> { - let Self { root, template, branch, install, offline, force, vscode, use_parent_git } = self; + let Self { root, template, branch, install, offline, force, vscode, use_parent_git, vyper } = + self; let DependencyInstallOpts { shallow, no_git, commit } = install; // create the root dir if it does not exist @@ -123,15 +128,43 @@ impl InitArgs { let script = root.join("script"); fs::create_dir_all(&script)?; - // write the contract file - let contract_path = src.join("Counter.sol"); - fs::write(contract_path, include_str!("../../assets/CounterTemplate.sol"))?; - // write the tests - let contract_path = test.join("Counter.t.sol"); - fs::write(contract_path, include_str!("../../assets/CounterTemplate.t.sol"))?; - // write the script - let contract_path = script.join("Counter.s.sol"); - fs::write(contract_path, include_str!("../../assets/CounterTemplate.s.sol"))?; + if vyper { + // write the contract file + let contract_path = src.join("Counter.vy"); + fs::write(contract_path, include_str!("../../assets/vyper/CounterTemplate.vy"))?; + let interface_path = src.join("ICounter.sol"); + fs::write(interface_path, include_str!("../../assets/vyper/ICounterTemplate.sol"))?; + + // write the tests + let contract_path = test.join("Counter.t.sol"); + fs::write(contract_path, include_str!("../../assets/vyper/CounterTemplate.t.sol"))?; + + // write the script + let contract_path = script.join("Counter.s.sol"); + fs::write(contract_path, include_str!("../../assets/vyper/CounterTemplate.s.sol"))?; + } else { + // write the contract file + let contract_path = src.join("Counter.sol"); + fs::write( + contract_path, + include_str!("../../assets/solidity/CounterTemplate.sol"), + )?; + + // write the tests + let contract_path = test.join("Counter.t.sol"); + fs::write( + contract_path, + include_str!("../../assets/solidity/CounterTemplate.t.sol"), + )?; + + // write the script + let contract_path = script.join("Counter.s.sol"); + fs::write( + contract_path, + include_str!("../../assets/solidity/CounterTemplate.s.sol"), + )?; + } + // Write the default README file let readme_path = root.join("README.md"); fs::write(readme_path, include_str!("../../assets/README.md"))?; @@ -146,7 +179,7 @@ impl InitArgs { // set up the repo if !no_git { - init_git_repo(git, commit, use_parent_git)?; + init_git_repo(git, commit, use_parent_git, vyper)?; } // install forge-std @@ -177,7 +210,7 @@ impl InitArgs { /// Creates `.gitignore` and `.github/workflows/test.yml`, if they don't exist already. /// /// Commits everything in `root` if `commit` is true. -fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool) -> Result<()> { +fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool, vyper: bool) -> Result<()> { // `git init` if !git.is_in_repo()? || (!use_parent_git && !git.is_repo_root()?) { git.init()?; @@ -193,7 +226,12 @@ fn init_git_repo(git: Git<'_>, commit: bool, use_parent_git: bool) -> Result<()> let workflow = git.root.join(".github/workflows/test.yml"); if !workflow.exists() { fs::create_dir_all(workflow.parent().unwrap())?; - fs::write(workflow, include_str!("../../assets/workflowTemplate.yml"))?; + + if vyper { + fs::write(workflow, include_str!("../../assets/vyper/workflowTemplate.yml"))?; + } else { + fs::write(workflow, include_str!("../../assets/solidity/workflowTemplate.yml"))?; + } } // commit everything diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index bc004c45fa6c1..ac24818377768 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -775,6 +775,63 @@ forgetest!(can_clone_keep_directory_structure, |prj, cmd| { let _config: BasicConfig = parse_with_profile(&s).unwrap().unwrap().1; }); +// checks that `forge init` works. +forgetest!(can_init_project, |prj, cmd| { + prj.wipe(); + + cmd.args(["init"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std[..] + Initialized forge project + +"#]]); + + assert!(prj.root().join("foundry.toml").exists()); + assert!(prj.root().join("lib/forge-std").exists()); + + assert!(prj.root().join("src").exists()); + assert!(prj.root().join("src").join("Counter.sol").exists()); + + assert!(prj.root().join("test").exists()); + assert!(prj.root().join("test").join("Counter.t.sol").exists()); + + assert!(prj.root().join("script").exists()); + assert!(prj.root().join("script").join("Counter.s.sol").exists()); + + assert!(prj.root().join(".github").join("workflows").exists()); + assert!(prj.root().join(".github").join("workflows").join("test.yml").exists()); +}); + +// checks that `forge init --vyper` works. +forgetest!(can_init_vyper_project, |prj, cmd| { + prj.wipe(); + + cmd.args(["init", "--vyper"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std[..] + Initialized forge project + +"#]]); + + assert!(prj.root().join("foundry.toml").exists()); + assert!(prj.root().join("lib/forge-std").exists()); + + assert!(prj.root().join("src").exists()); + assert!(prj.root().join("src").join("Counter.vy").exists()); + assert!(prj.root().join("src").join("ICounter.sol").exists()); + + assert!(prj.root().join("test").exists()); + assert!(prj.root().join("test").join("Counter.t.sol").exists()); + + assert!(prj.root().join("script").exists()); + assert!(prj.root().join("script").join("Counter.s.sol").exists()); + + assert!(prj.root().join(".github").join("workflows").exists()); + assert!(prj.root().join(".github").join("workflows").join("test.yml").exists()); +}); + // checks that clone works with raw src containing `node_modules` // forgetest!(can_clone_with_node_modules, |prj, cmd| {