diff --git a/Cargo.lock b/Cargo.lock index 9278cf751c4a2..fa1f2aee63c86 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4054,11 +4054,9 @@ dependencies = [ "rand", "regex", "serde_json", - "similar-asserts", "snapbox", "tracing", "tracing-subscriber", - "walkdir", ] [[package]] diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 6ef092cb1e717..8ece9ef65dedf 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -1,10 +1,10 @@ //! Contains various tests for checking cast commands use alloy_chains::NamedChain; -use alloy_primitives::{address, b256, Address, B256}; +use alloy_primitives::{b256, B256}; use anvil::{Hardfork, NodeConfig}; use foundry_test_utils::{ - casttest, + casttest, file, rpc::{next_http_rpc_endpoint, next_rpc_endpoint, next_ws_rpc_endpoint}, str, util::OutputExt, @@ -13,8 +13,21 @@ use std::{fs, io::Write, path::Path, str::FromStr}; // tests `--help` is printed to std out casttest!(print_help, |_prj, cmd| { - cmd.arg("--help"); - cmd.assert_non_empty_stdout(); + cmd.arg("--help").assert_success().stdout_eq(str![[r#" +Perform Ethereum RPC calls from the comfort of your command line + +Usage: cast[..] + +Commands: +... + +Options: + -h, --help Print help + -V, --version Print version + +Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html + +"#]]); }); // tests that the `cast block` command works correctly @@ -23,14 +36,41 @@ casttest!(latest_block, |_prj, cmd| { // Call `cast find-block` cmd.args(["block", "latest", "--rpc-url", eth_rpc_url.as_str()]); - let output = cmd.stdout_lossy(); - assert!(output.contains("transactions:")); - assert!(output.contains("gasUsed")); + cmd.assert_success().stdout_eq(str![[r#" + + +baseFeePerGas [..] +difficulty [..] +extraData [..] +gasLimit [..] +gasUsed [..] +hash [..] +logsBloom [..] +miner [..] +mixHash [..] +nonce [..] +number [..] +parentHash [..] +transactionsRoot [..] +receiptsRoot [..] +sha3Uncles [..] +size [..] +stateRoot [..] +timestamp [..] +withdrawalsRoot [..] +totalDifficulty [..] +transactions: [ +... +] + +"#]]); // cmd.cast_fuse().args(["block", "15007840", "-f", "hash", "--rpc-url", eth_rpc_url.as_str()]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim(), "0x950091817a57e22b6c1f3b951a15f52d41ac89b299cc8f9c89bb6d185f80c415") + cmd.assert_success().stdout_eq(str![[r#" +0x950091817a57e22b6c1f3b951a15f52d41ac89b299cc8f9c89bb6d185f80c415 + +"#]]); }); // tests that the `cast find-block` command works correctly @@ -40,23 +80,24 @@ casttest!(finds_block, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); // Call `cast find-block` - cmd.args(["find-block", "--rpc-url", eth_rpc_url.as_str(), ×tamp]); - let output = cmd.stdout_lossy(); - println!("{output}"); - - // Expect successful block query - // Query: 1647843609, Mar 21 2022 06:20:09 UTC - // Output block: https://etherscan.io/block/14428082 - // Output block time: Mar 21 2022 06:20:09 UTC - assert!(output.contains("14428082"), "{}", output); + // + cmd.args(["find-block", "--rpc-url", eth_rpc_url.as_str(), ×tamp]) + .assert_success() + .stdout_eq(str![[r#" +14428082 + +"#]]); }); // tests that we can create a new wallet with keystore casttest!(new_wallet_keystore_with_password, |_prj, cmd| { - cmd.args(["wallet", "new", ".", "--unsafe-password", "test"]); - let out = cmd.stdout_lossy(); - assert!(out.contains("Created new encrypted keystore file")); - assert!(out.contains("Address")); + cmd.args(["wallet", "new", ".", "--unsafe-password", "test"]).assert_success().stdout_eq(str![ + [r#" +Created new encrypted keystore file: [..] +[ADDRESS] + +"#] + ]); }); // tests that we can get the address of a keystore file @@ -73,9 +114,12 @@ casttest!(wallet_address_keystore_with_password_file, |_prj, cmd| { .unwrap(), "--password-file", keystore_dir.join("password-ec554").to_str().unwrap(), - ]); - let out = cmd.stdout_lossy(); - assert!(out.contains("0xeC554aeAFE75601AaAb43Bd4621A22284dB566C2")); + ]) + .assert_success() + .stdout_eq(str![[r#" +0xeC554aeAFE75601AaAb43Bd4621A22284dB566C2 + +"#]]); }); // tests that `cast wallet sign message` outputs the expected signature @@ -85,17 +129,29 @@ casttest!(wallet_sign_message_utf8_data, |_prj, cmd| { let msg = "test"; let expected = "0xfe28833983d6faa0715c7e8c3873c725ddab6fa5bf84d40e780676e463e6bea20fc6aea97dc273a98eb26b0914e224c8dd5c615ceaab69ddddcf9b0ae3de0e371c"; - cmd.args(["wallet", "sign", "--private-key", pk, msg]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim(), expected); + cmd.args(["wallet", "sign", "--private-key", pk, msg]).assert_success().stdout_eq(str![[r#" +0xfe28833983d6faa0715c7e8c3873c725ddab6fa5bf84d40e780676e463e6bea20fc6aea97dc273a98eb26b0914e224c8dd5c615ceaab69ddddcf9b0ae3de0e371c + +"#]]); // Success. cmd.cast_fuse() .args(["wallet", "verify", "-a", address, msg, expected]) - .assert_non_empty_stdout(); + .assert_success() + .stdout_eq(str![[r#" +Validation succeeded. Address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf signed this message. + +"#]]); // Fail. - cmd.cast_fuse().args(["wallet", "verify", "-a", address, "other msg", expected]).assert_err(); + cmd.cast_fuse() + .args(["wallet", "verify", "-a", address, "other msg", expected]) + .assert_failure() + .stderr_eq(str![[r#" +Error: +Validation failed. Address 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf did not sign this message. + +"#]]); }); // tests that `cast wallet sign message` outputs the expected signature, given a 0x-prefixed data @@ -172,25 +228,74 @@ casttest!(wallet_list_local_accounts, |prj, cmd| { cmd.set_current_dir(prj.root()); // empty results - cmd.cast_fuse().args(["wallet", "list", "--dir", "keystore"]); - let list_output = cmd.stdout_lossy(); - assert!(list_output.is_empty()); + cmd.cast_fuse() + .args(["wallet", "list", "--dir", "keystore"]) + .assert_success() + .stdout_eq(str![""]); // create 10 wallets - cmd.cast_fuse().args(["wallet", "new", "keystore", "-n", "10", "--unsafe-password", "test"]); - cmd.stdout_lossy(); + cmd.cast_fuse() + .args(["wallet", "new", "keystore", "-n", "10", "--unsafe-password", "test"]) + .assert_success() + .stdout_eq(str![[r#" +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] +Created new encrypted keystore file: [..] +[ADDRESS] + +"#]]); // test list new wallet - cmd.cast_fuse().args(["wallet", "list", "--dir", "keystore"]); - let list_output = cmd.stdout_lossy(); - assert_eq!(list_output.matches('\n').count(), 10); + cmd.cast_fuse().args(["wallet", "list", "--dir", "keystore"]).assert_success().stdout_eq(str![ + [r#" +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) +[..] (Local) + +"#] + ]); }); // tests that `cast wallet new-mnemonic --entropy` outputs the expected mnemonic casttest!(wallet_mnemonic_from_entropy, |_prj, cmd| { - cmd.args(["wallet", "new-mnemonic", "--entropy", "0xdf9bf37e6fcdf9bf37e6fcdf9bf37e3c"]); - let output = cmd.stdout_lossy(); - assert!(output.contains("test test test test test test test test test test test junk")); + cmd.args(["wallet", "new-mnemonic", "--entropy", "0xdf9bf37e6fcdf9bf37e6fcdf9bf37e3c"]) + .assert_success() + .stdout_eq(str![[r#" +Generating mnemonic from provided entropy... +Successfully generated a new mnemonic. +Phrase: +test test test test test test test test test test test junk + +Accounts: +- Account 0: +Address: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +Private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 + + +"#]]); }); // tests that `cast wallet private-key` with arguments outputs the private key @@ -217,9 +322,12 @@ casttest!(wallet_private_key_from_mnemonic_option, |_prj, cmd| { "test test test test test test test test test test test junk", "--mnemonic-index", "1", - ]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim(), "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"); + ]) + .assert_success() + .stdout_eq(str![[r#" +0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + +"#]]); }); // tests that `cast wallet private-key` with derivation path outputs the private key @@ -231,9 +339,12 @@ casttest!(wallet_private_key_with_derivation_path, |_prj, cmd| { "test test test test test test test test test test test junk", "--mnemonic-derivation-path", "m/44'/60'/0'/0/1", - ]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim(), "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d"); + ]) + .assert_success() + .stdout_eq(str![[r#" +0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d + +"#]]); }); // tests that `cast wallet import` creates a keystore for a private key and that `cast wallet @@ -250,19 +361,23 @@ casttest!(wallet_import_and_decrypt, |prj, cmd| { b256!("ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"); // import private key - cmd.cast_fuse().args([ - "wallet", - "import", - account_name, - "--private-key", - &test_private_key.to_string(), - "-k", - "keystore", - "--unsafe-password", - "test", - ]); + cmd.cast_fuse() + .args([ + "wallet", + "import", + account_name, + "--private-key", + &test_private_key.to_string(), + "-k", + "keystore", + "--unsafe-password", + "test", + ]) + .assert_success() + .stdout_eq(str![[r#" +`testAccount` keystore was saved successfully. [ADDRESS] - cmd.assert_non_empty_stdout(); +"#]]); // check that the keystore file was created let keystore_file = keystore_path.join(account_name); @@ -281,7 +396,7 @@ casttest!(wallet_import_and_decrypt, |prj, cmd| { ]); // get the PK out of the output (last word in the output) - let decrypt_output = decrypt_output.stdout_lossy(); + let decrypt_output = decrypt_output.assert_success().get_output().stdout_lossy(); let private_key_string = decrypt_output.split_whitespace().last().unwrap(); // check that the decrypted private key matches the imported private key let decrypted_private_key = B256::from_str(private_key_string).unwrap(); @@ -292,18 +407,25 @@ casttest!(wallet_import_and_decrypt, |prj, cmd| { // tests that `cast estimate` is working correctly. casttest!(estimate_function_gas, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - cmd.args([ - "estimate", - "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // vitalik.eth - "--value", - "100", - "deposit()", - "--rpc-url", - eth_rpc_url.as_str(), - ]); - let out: u32 = cmd.stdout_lossy().trim().parse().unwrap(); + // ensure we get a positive non-error value for gas estimate - assert!(out.ge(&0)); + let output: u32 = cmd + .args([ + "estimate", + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", // vitalik.eth + "--value", + "100", + "deposit()", + "--rpc-url", + eth_rpc_url.as_str(), + ]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .parse() + .unwrap(); + assert!(output.ge(&0)); }); // tests that `cast estimate --create` is working correctly. @@ -311,41 +433,45 @@ casttest!(estimate_contract_deploy_gas, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); // sample contract code bytecode. Wouldn't run but is valid bytecode that the estimate method // accepts and could be deployed. - cmd.args([ - "estimate", - "--rpc-url", - eth_rpc_url.as_str(), - "--create", - "0000", - "ERC20(uint256,string,string)", - "100", - "Test", - "TST", - ]); + let output = cmd + .args([ + "estimate", + "--rpc-url", + eth_rpc_url.as_str(), + "--create", + "0000", + "ERC20(uint256,string,string)", + "100", + "Test", + "TST", + ]) + .assert_success() + .get_output() + .stdout_lossy(); - let gas: u32 = cmd.stdout_lossy().trim().parse().unwrap(); // ensure we get a positive non-error value for gas estimate - assert!(gas > 0); + let output: u32 = output.trim().parse().unwrap(); + assert!(output > 0); }); // tests that the `cast upload-signatures` command works correctly casttest!(upload_signatures, |_prj, cmd| { // test no prefix is accepted as function - cmd.args(["upload-signature", "transfer(address,uint256)"]); - let output = cmd.stdout_lossy(); - + let output = cmd + .args(["upload-signature", "transfer(address,uint256)"]) + .assert_success() + .get_output() + .stdout_lossy(); assert!(output.contains("Function transfer(address,uint256): 0xa9059cbb"), "{}", output); // test event prefix cmd.args(["upload-signature", "event Transfer(address,uint256)"]); - let output = cmd.stdout_lossy(); - + let output = cmd.assert_success().get_output().stdout_lossy(); assert!(output.contains("Event Transfer(address,uint256): 0x69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2"), "{}", output); // test error prefix cmd.args(["upload-signature", "error ERC20InsufficientBalance(address,uint256,uint256)"]); - let output = cmd.stdout_lossy(); - + let output = cmd.assert_success().get_output().stdout_lossy(); assert!( output.contains("Function ERC20InsufficientBalance(address,uint256,uint256): 0xe450d38c"), "{}", @@ -359,8 +485,7 @@ casttest!(upload_signatures, |_prj, cmd| { "transfer(address,uint256)", "approve(address,uint256)", ]); - let output = cmd.stdout_lossy(); - + let output = cmd.assert_success().get_output().stdout_lossy(); assert!(output.contains("Event Transfer(address,uint256): 0x69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2"), "{}", output); assert!(output.contains("Function transfer(address,uint256): 0xa9059cbb"), "{}", output); assert!(output.contains("Function approve(address,uint256): 0x095ea7b3"), "{}", output); @@ -378,8 +503,7 @@ casttest!(upload_signatures, |_prj, cmd| { .unwrap() .as_str(), ]); - let output = cmd.stdout_lossy(); - + let output = cmd.assert_success().get_output().stdout_lossy(); assert!(output.contains("Event Transfer(address,uint256): 0x69ca02dd4edd7bf0a4abb9ed3b7af3f14778db5d61921c7dc7cd545266326de2"), "{}", output); assert!(output.contains("Function transfer(address,uint256): 0xa9059cbb"), "{}", output); assert!(output.contains("Function approve(address,uint256): 0x095ea7b3"), "{}", output); @@ -394,14 +518,18 @@ casttest!(upload_signatures, |_prj, cmd| { // tests that the `cast to-rlp` and `cast from-rlp` commands work correctly casttest!(rlp, |_prj, cmd| { - cmd.args(["--to-rlp", "[\"0xaa\", [[\"bb\"]], \"0xcc\"]"]); - let out = cmd.stdout_lossy(); - assert!(out.contains("0xc881aac3c281bb81cc"), "{}", out); + cmd.args(["--to-rlp", "[\"0xaa\", [[\"bb\"]], \"0xcc\"]"]).assert_success().stdout_eq(str![[ + r#" +0xc881aac3c281bb81cc + +"# + ]]); cmd.cast_fuse(); - cmd.args(["--from-rlp", "0xcbc58455556666c0c0c2c1c0"]); - let out = cmd.stdout_lossy(); - assert!(out.contains("[[\"0x55556666\"],[],[],[[[]]]]"), "{}", out); + cmd.args(["--from-rlp", "0xcbc58455556666c0c0c2c1c0"]).assert_success().stdout_eq(str![[r#" +[["0x55556666"],[],[],[[[]]]] + +"#]]); }); // test for cast_rpc without arguments @@ -409,9 +537,12 @@ casttest!(rpc_no_args, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); // Call `cast rpc eth_chainId` - cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim_end(), r#""0x1""#); + cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]).assert_success().stdout_eq( + str![[r#" +"0x1" + +"#]], + ); }); // test for cast_rpc without arguments using websocket @@ -419,9 +550,12 @@ casttest!(ws_rpc_no_args, |_prj, cmd| { let eth_rpc_url = next_ws_rpc_endpoint(); // Call `cast rpc eth_chainId` - cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim_end(), r#""0x1""#); + cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_chainId"]).assert_success().stdout_eq( + str![[r#" +"0x1" + +"#]], + ); }); // test for cast_rpc with arguments @@ -429,9 +563,12 @@ casttest!(rpc_with_args, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); // Call `cast rpc eth_getBlockByNumber 0x123 false` - cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_getBlockByNumber", "0x123", "false"]); - let output = cmd.stdout_lossy(); - assert!(output.contains(r#""number":"0x123""#), "{}", output); + cmd.args(["rpc", "--rpc-url", eth_rpc_url.as_str(), "eth_getBlockByNumber", "0x123", "false"]) + .assert_success() + .stdout_eq(str![[r#" +{"number":"0x123","hash":"0xc5dab4e189004a1312e9db43a40abb2de91ad7dd25e75880bf36016d8e9df524","transactions":[],"totalDifficulty":"0x4dea420908b","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","extraData":"0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32","nonce":"0x29d6547c196e00e0","miner":"0xbb7b8287f3f0a933474a79eae42cbca977791171","difficulty":"0x494433b31","gasLimit":"0x1388","gasUsed":"0x0","uncles":[],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x220","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0x3fe6bd17aa85376c7d566df97d9f2e536f37f7a87abb3a6f9e2891cf9442f2e4","mixHash":"0x943056aa305aa6d22a3c06110942980342d1f4d4b11c17711961436a0f963ea0","parentHash":"0x7abfd11e862ccde76d6ea8ee20978aac26f4bcb55de1188cc0335be13e817017","timestamp":"0x55ba4564"} + +"#]]); }); // test for cast_rpc with raw params @@ -446,9 +583,12 @@ casttest!(rpc_raw_params, |_prj, cmd| { "eth_getBlockByNumber", "--raw", r#"["0x123", false]"#, - ]); - let output = cmd.stdout_lossy(); - assert!(output.contains(r#""number":"0x123""#), "{}", output); + ]) + .assert_success() + .stdout_eq(str![[r#" +{"number":"0x123","hash":"0xc5dab4e189004a1312e9db43a40abb2de91ad7dd25e75880bf36016d8e9df524","transactions":[],"totalDifficulty":"0x4dea420908b","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","extraData":"0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32","nonce":"0x29d6547c196e00e0","miner":"0xbb7b8287f3f0a933474a79eae42cbca977791171","difficulty":"0x494433b31","gasLimit":"0x1388","gasUsed":"0x0","uncles":[],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x220","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0x3fe6bd17aa85376c7d566df97d9f2e536f37f7a87abb3a6f9e2891cf9442f2e4","mixHash":"0x943056aa305aa6d22a3c06110942980342d1f4d4b11c17711961436a0f963ea0","parentHash":"0x7abfd11e862ccde76d6ea8ee20978aac26f4bcb55de1188cc0335be13e817017","timestamp":"0x55ba4564"} + +"#]]); }); // test for cast_rpc with direct params @@ -460,17 +600,20 @@ casttest!(rpc_raw_params_stdin, |_prj, cmd| { |mut stdin| { stdin.write_all(b"\n[\n\"0x123\",\nfalse\n]\n").unwrap(); }, - ); - let output = cmd.stdout_lossy(); - assert!(output.contains(r#""number":"0x123""#), "{}", output); + ) + .assert_success() + .stdout_eq(str![[r#" +{"number":"0x123","hash":"0xc5dab4e189004a1312e9db43a40abb2de91ad7dd25e75880bf36016d8e9df524","transactions":[],"totalDifficulty":"0x4dea420908b","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","extraData":"0x476574682f4c5649562f76312e302e302f6c696e75782f676f312e342e32","nonce":"0x29d6547c196e00e0","miner":"0xbb7b8287f3f0a933474a79eae42cbca977791171","difficulty":"0x494433b31","gasLimit":"0x1388","gasUsed":"0x0","uncles":[],"sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x220","transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","stateRoot":"0x3fe6bd17aa85376c7d566df97d9f2e536f37f7a87abb3a6f9e2891cf9442f2e4","mixHash":"0x943056aa305aa6d22a3c06110942980342d1f4d4b11c17711961436a0f963ea0","parentHash":"0x7abfd11e862ccde76d6ea8ee20978aac26f4bcb55de1188cc0335be13e817017","timestamp":"0x55ba4564"} + +"#]]); }); // checks `cast calldata` can handle arrays casttest!(calldata_array, |_prj, cmd| { - cmd.args(["calldata", "propose(string[])", "[\"\"]"]); - let out = cmd.stdout_lossy(); - assert_eq!(out.trim(),"0xcde2baba0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000" - ); + cmd.args(["calldata", "propose(string[])", "[\"\"]"]).assert_success().stdout_eq(str![[r#" +0xcde2baba0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000 + +"#]]); }); // @@ -483,10 +626,14 @@ casttest!(run_succeeds, |_prj, cmd| { "--quick", "--rpc-url", rpc.as_str(), - ]); - let output = cmd.stdout_lossy(); - assert!(output.contains("Transaction successfully executed")); - assert!(!output.contains("Revert")); + ]) + .assert_success() + .stdout_eq(str![[r#" +... +Transaction successfully executed. +[GAS] + +"#]]); }); // tests that `cast --to-base` commands are working correctly. @@ -507,11 +654,11 @@ casttest!(to_base, |_prj, cmd| { if subcmd == "--to-base" { for base in ["bin", "oct", "dec", "hex"] { cmd.cast_fuse().args([subcmd, value, base]); - assert!(!cmd.stdout_lossy().trim().is_empty()); + assert!(!cmd.assert_success().get_output().stdout_lossy().trim().is_empty()); } } else { cmd.cast_fuse().args([subcmd, value]); - assert!(!cmd.stdout_lossy().trim().is_empty()); + assert!(!cmd.assert_success().get_output().stdout_lossy().trim().is_empty()); } } } @@ -522,25 +669,68 @@ casttest!(receipt_revert_reason, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); // - cmd.cast_fuse().args([ + cmd.args([ "receipt", "0x44f2aaa351460c074f2cb1e5a9e28cbc7d83f33e425101d2de14331c7b7ec31e", "--rpc-url", rpc.as_str(), - ]); - let output = cmd.stdout_lossy(); - assert!(!output.contains("revertReason")); + ]) + .assert_success() + .stdout_eq(str![[r#" + +blockHash 0x2cfe65be49863676b6dbc04d58176a14f39b123f1e2f4fea0383a2d82c2c50d0 +blockNumber 16239315 +contractAddress +cumulativeGasUsed 10743428 +effectiveGasPrice 10539984136 +from 0x199D5ED7F45F4eE35960cF22EAde2076e95B253F +gasUsed 21000 +logs [] +logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +root +status 1 (success) +transactionHash 0x44f2aaa351460c074f2cb1e5a9e28cbc7d83f33e425101d2de14331c7b7ec31e +transactionIndex 116 +type 0 +blobGasPrice +blobGasUsed +authorizationList +to 0x91da5bf3F8Eb72724E6f50Ec6C3D199C6355c59c + +"#]]); // - cmd.cast_fuse().args([ - "receipt", - "0x0e07d8b53ed3d91314c80e53cf25bcde02084939395845cbb625b029d568135c", - "--rpc-url", - rpc.as_str(), - ]); - let output = cmd.stdout_lossy(); - assert!(output.contains("revertReason")); - assert!(output.contains("Transaction too old")); + cmd.cast_fuse() + .args([ + "receipt", + "0x0e07d8b53ed3d91314c80e53cf25bcde02084939395845cbb625b029d568135c", + "--rpc-url", + rpc.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" + +blockHash 0x883f974b17ca7b28cb970798d1c80f4d4bb427473dc6d39b2a7fe24edc02902d +blockNumber 14839405 +contractAddress +cumulativeGasUsed 20273649 +effectiveGasPrice 21491736378 +from 0x3cF412d970474804623bb4e3a42dE13F9bCa5436 +gasUsed 24952 +logs [] +logsBloom 0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 +root +status 0 (failed) +transactionHash 0x0e07d8b53ed3d91314c80e53cf25bcde02084939395845cbb625b029d568135c +transactionIndex 173 +type 2 +blobGasPrice +blobGasUsed +authorizationList +to 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 +revertReason Transaction too old, data: "0x08c379a0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000135472616e73616374696f6e20746f6f206f6c6400000000000000000000000000" + +"#]]); }); // tests that `cast --parse-bytes32-address` command is working correctly. @@ -548,9 +738,12 @@ casttest!(parse_bytes32_address, |_prj, cmd| { cmd.args([ "--parse-bytes32-address", "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045", - ]); - let output = cmd.stdout_lossy(); - assert_eq!(output.trim(), "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045") + ]) + .assert_success() + .stdout_eq(str![[r#" +0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045 + +"#]]); }); casttest!(access_list, |_prj, cmd| { @@ -564,12 +757,22 @@ casttest!(access_list, |_prj, cmd| { rpc.as_str(), "--gas-limit", // need to set this for alchemy.io to avoid "intrinsic gas too low" error "100000", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[GAS] +access list: +- address: [..] + keys: +... +- address: [..] + keys: +... +- address: [..] + keys: +... - let output = cmd.stdout_lossy(); - assert!(output.contains("address: 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599")); - assert!(output.contains("0x0d2a19d3ac39dc6cc6fd07423195495e18679bd8c7dd610aa1db7cd784a683a8")); - assert!(output.contains("0x7fba2702a7d6e85ac783a88eacdc48e51310443458071f6db9ac66f8ca7068b8")); +"#]]); }); casttest!(logs_topics, |_prj, cmd| { @@ -584,11 +787,9 @@ casttest!(logs_topics, |_prj, cmd| { "12421182", "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x000000000000000000000000ab5801a7d398351b8be11c439e05c5b3259aec9b", - ]); - - cmd.unchecked_output().stdout_matches_path( - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cast_logs.stdout"), - ); + ]) + .assert_success() + .stdout_eq(file!["../fixtures/cast_logs.stdout"]); }); casttest!(logs_topic_2, |_prj, cmd| { @@ -605,11 +806,9 @@ casttest!(logs_topic_2, |_prj, cmd| { "", "0x00000000000000000000000068a99f89e475a078645f4bac491360afe255dff1", /* Filter on the * `to` address */ - ]); - - cmd.unchecked_output().stdout_matches_path( - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cast_logs.stdout"), - ); + ]) + .assert_success() + .stdout_eq(file!["../fixtures/cast_logs.stdout"]); }); casttest!(logs_sig, |_prj, cmd| { @@ -624,11 +823,9 @@ casttest!(logs_sig, |_prj, cmd| { "12421182", "Transfer(address indexed from, address indexed to, uint256 value)", "0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B", - ]); - - cmd.unchecked_output().stdout_matches_path( - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cast_logs.stdout"), - ); + ]) + .assert_success() + .stdout_eq(file!["../fixtures/cast_logs.stdout"]); }); casttest!(logs_sig_2, |_prj, cmd| { @@ -644,11 +841,9 @@ casttest!(logs_sig_2, |_prj, cmd| { "Transfer(address indexed from, address indexed to, uint256 value)", "", "0x68A99f89E475a078645f4BAC491360aFe255Dff1", - ]); - - cmd.unchecked_output().stdout_matches_path( - Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/cast_logs.stdout"), - ); + ]) + .assert_success() + .stdout_eq(file!["../fixtures/cast_logs.stdout"]); }); casttest!(mktx, |_prj, cmd| { @@ -669,12 +864,10 @@ casttest!(mktx, |_prj, cmd| { "--priority-gas-price", "1000000000", "0x0000000000000000000000000000000000000001", - ]); - let output = cmd.stdout_lossy(); - assert_eq!( - output.trim(), - "0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000016480c001a070d55e79ed3ac9fc8f51e78eb91fd054720d943d66633f2eb1bc960f0126b0eca052eda05a792680de3181e49bab4093541f75b49d1ecbe443077b3660c836016a" - ); + ]).assert_success().stdout_eq(str![[r#" +0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000016480c001a070d55e79ed3ac9fc8f51e78eb91fd054720d943d66633f2eb1bc960f0126b0eca052eda05a792680de3181e49bab4093541f75b49d1ecbe443077b3660c836016a + +"#]]); }); // ensure recipient or code is required @@ -684,11 +877,11 @@ casttest!(mktx_requires_to, |_prj, cmd| { "--private-key", "0x0000000000000000000000000000000000000000000000000000000000000001", ]); - let output = cmd.stderr_lossy(); - assert_eq!( - output.trim(), - "Error: \nMust specify a recipient address or contract code to deploy" - ); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +Must specify a recipient address or contract code to deploy + +"#]]); }); casttest!(mktx_signer_from_mismatch, |_prj, cmd| { @@ -702,10 +895,14 @@ casttest!(mktx_signer_from_mismatch, |_prj, cmd| { "1", "0x0000000000000000000000000000000000000001", ]); - let output = cmd.stderr_lossy(); - assert!( - output.contains("The specified sender via CLI/env vars does not match the sender configured via\nthe hardware wallet's HD Path.") - ); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +The specified sender via CLI/env vars does not match the sender configured via +the hardware wallet's HD Path. +Please use the `--hd-path ` parameter to specify the BIP32 Path which +corresponds to the sender, or let foundry automatically detect it by not specifying any sender address. + +"#]]); }); casttest!(mktx_signer_from_match, |_prj, cmd| { @@ -726,43 +923,39 @@ casttest!(mktx_signer_from_match, |_prj, cmd| { "--priority-gas-price", "1000000000", "0x0000000000000000000000000000000000000001", - ]); - let output = cmd.stdout_lossy(); - assert_eq!( - output.trim(), - "0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000018080c001a0cce9a61187b5d18a89ecd27ec675e3b3f10d37f165627ef89a15a7fe76395ce8a07537f5bffb358ffbef22cda84b1c92f7211723f9e09ae037e81686805d3e5505" - ); + ]).assert_success().stdout_eq(str![[r#" +0x02f86b0180843b9aca008502540be4008252089400000000000000000000000000000000000000018080c001a0cce9a61187b5d18a89ecd27ec675e3b3f10d37f165627ef89a15a7fe76395ce8a07537f5bffb358ffbef22cda84b1c92f7211723f9e09ae037e81686805d3e5505 + +"#]]); }); // tests that the raw encoded transaction is returned casttest!(tx_raw, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); - // - cmd.cast_fuse().args([ + // + cmd.args([ "tx", "0x44f2aaa351460c074f2cb1e5a9e28cbc7d83f33e425101d2de14331c7b7ec31e", "raw", "--rpc-url", rpc.as_str(), - ]); - let output = cmd.stdout_lossy(); + ]).assert_success().stdout_eq(str![[r#" +0xf86d824c548502743b65088275309491da5bf3f8eb72724e6f50ec6c3d199c6355c59c87a0a73f33e9e4cc8025a0428518b1748a08bbeb2392ea055b418538944d30adfc2accbbfa8362a401d3a4a07d6093ab2580efd17c11b277de7664fce56e6953cae8e925bec3313399860470 - // - assert_eq!( - output.trim(), - "0xf86d824c548502743b65088275309491da5bf3f8eb72724e6f50ec6c3d199c6355c59c87a0a73f33e9e4cc8025a0428518b1748a08bbeb2392ea055b418538944d30adfc2accbbfa8362a401d3a4a07d6093ab2580efd17c11b277de7664fce56e6953cae8e925bec3313399860470" - ); +"#]]); + // cmd.cast_fuse().args([ "tx", "0x44f2aaa351460c074f2cb1e5a9e28cbc7d83f33e425101d2de14331c7b7ec31e", "--raw", "--rpc-url", rpc.as_str(), - ]); - let output2 = cmd.stdout_lossy(); - assert_eq!(output, output2); + ]).assert_success().stdout_eq(str![[r#" +0xf86d824c548502743b65088275309491da5bf3f8eb72724e6f50ec6c3d199c6355c59c87a0a73f33e9e4cc8025a0428518b1748a08bbeb2392ea055b418538944d30adfc2accbbfa8362a401d3a4a07d6093ab2580efd17c11b277de7664fce56e6953cae8e925bec3313399860470 + +"#]]); }); // ensure receipt or code is required @@ -772,62 +965,66 @@ casttest!(send_requires_to, |_prj, cmd| { "--private-key", "0x0000000000000000000000000000000000000000000000000000000000000001", ]); - let output = cmd.stderr_lossy(); - assert_eq!( - output.trim(), - "Error: \nMust specify a recipient address or contract code to deploy" - ); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +Must specify a recipient address or contract code to deploy + +"#]]); }); casttest!(storage, |_prj, cmd| { - let empty = "0x0000000000000000000000000000000000000000000000000000000000000000"; - let rpc = next_http_rpc_endpoint(); - cmd.cast_fuse().args(["storage", "vitalik.eth", "1", "--rpc-url", &rpc]); - assert_eq!(cmd.stdout_lossy().trim(), empty); + cmd.args(["storage", "vitalik.eth", "1", "--rpc-url", &rpc]).assert_success().stdout_eq(str![ + [r#" +0x0000000000000000000000000000000000000000000000000000000000000000 + +"#] + ]); let rpc = next_http_rpc_endpoint(); - cmd.cast_fuse().args(["storage", "vitalik.eth", "0x01", "--rpc-url", &rpc]); - assert_eq!(cmd.stdout_lossy().trim(), empty); + cmd.cast_fuse() + .args(["storage", "vitalik.eth", "0x01", "--rpc-url", &rpc]) + .assert_success() + .stdout_eq(str![[r#" +0x0000000000000000000000000000000000000000000000000000000000000000 + +"#]]); let rpc = next_http_rpc_endpoint(); let usdt = "0xdac17f958d2ee523a2206206994597c13d831ec7"; let decimals_slot = "0x09"; - let six = "0x0000000000000000000000000000000000000000000000000000000000000006"; - cmd.cast_fuse().args(["storage", usdt, decimals_slot, "--rpc-url", &rpc]); - assert_eq!(cmd.stdout_lossy().trim(), six); + cmd.cast_fuse() + .args(["storage", usdt, decimals_slot, "--rpc-url", &rpc]) + .assert_success() + .stdout_eq(str![[r#" +0x0000000000000000000000000000000000000000000000000000000000000006 + +"#]]); let rpc = next_http_rpc_endpoint(); let total_supply_slot = "0x01"; - let issued = "0x000000000000000000000000000000000000000000000000000000174876e800"; let block_before = "4634747"; let block_after = "4634748"; - cmd.cast_fuse().args([ - "storage", - usdt, - total_supply_slot, - "--rpc-url", - &rpc, - "--block", - block_before, - ]); - assert_eq!(cmd.stdout_lossy().trim(), empty); - cmd.cast_fuse().args([ - "storage", - usdt, - total_supply_slot, - "--rpc-url", - &rpc, - "--block", - block_after, - ]); - assert_eq!(cmd.stdout_lossy().trim(), issued); + cmd.cast_fuse() + .args(["storage", usdt, total_supply_slot, "--rpc-url", &rpc, "--block", block_before]) + .assert_success() + .stdout_eq(str![[r#" +0x0000000000000000000000000000000000000000000000000000000000000000 + +"#]]); + + cmd.cast_fuse() + .args(["storage", usdt, total_supply_slot, "--rpc-url", &rpc, "--block", block_after]) + .assert_success() + .stdout_eq(str![[r#" +0x000000000000000000000000000000000000000000000000000000174876e800 + +"#]]); }); // casttest!(storage_layout, |_prj, cmd| { - cmd.cast_fuse() - .args([ + cmd.args([ "storage", "--rpc-url", next_rpc_endpoint(NamedChain::Optimism).as_str(), @@ -874,27 +1071,39 @@ casttest!(storage_layout, |_prj, cmd| { casttest!(balance, |_prj, cmd| { let rpc = next_http_rpc_endpoint(); let usdt = "0xdac17f958d2ee523a2206206994597c13d831ec7"; - cmd.cast_fuse().args([ - "balance", - "0x0000000000000000000000000000000000000000", - "--erc20", - usdt, - "--rpc-url", - &rpc, - ]); - cmd.cast_fuse().args([ - "balance", - "0x0000000000000000000000000000000000000000", - "--erc721", - usdt, - "--rpc-url", - &rpc, - ]); - let usdt_result = cmd.stdout_lossy(); - let alias_result = cmd.stdout_lossy(); + let usdt_result = cmd + .args([ + "balance", + "0x0000000000000000000000000000000000000000", + "--erc20", + usdt, + "--rpc-url", + &rpc, + ]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); - assert_ne!(usdt_result, "0x0000000000000000000000000000000000000000000000000000000000000000"); + let alias_result = cmd + .cast_fuse() + .args([ + "balance", + "0x0000000000000000000000000000000000000000", + "--erc721", + usdt, + "--rpc-url", + &rpc, + ]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); + + assert_ne!(usdt_result, "0"); assert_eq!(alias_result, usdt_result); }); @@ -906,10 +1115,8 @@ casttest!(interface_no_constructor, |prj, cmd| { let path = prj.root().join("interface.json"); fs::write(&path, interface).unwrap(); // Call `cast find-block` - cmd.args(["interface"]).arg(&path); - let output = cmd.stdout_lossy(); - - let s = r#"// SPDX-License-Identifier: UNLICENSED + cmd.args(["interface"]).arg(&path).assert_success().stdout_eq(str![[ + r#"// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.4; interface Interface { @@ -928,8 +1135,10 @@ interface Interface { uint256[] memory minIncomingAssetAmounts_ ); function redeem(address _vaultProxy, bytes memory, bytes memory _assetData) external; -}"#; - assert_eq!(output.trim(), s); +} + +"# + ]]); }); // tests that fetches WETH interface from etherscan @@ -937,10 +1146,9 @@ interface Interface { casttest!(fetch_weth_interface_from_etherscan, |_prj, cmd| { let weth_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"; let api_key = "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591"; - cmd.args(["interface", "--etherscan-api-key", api_key, weth_address]); - let output = cmd.stdout_lossy(); - - let weth_interface = r#"// SPDX-License-Identifier: UNLICENSED + cmd.args(["interface", "--etherscan-api-key", api_key, weth_address]) + .assert_success() + .stdout_eq(str![[r#"// SPDX-License-Identifier: UNLICENSED pragma solidity ^0.8.4; interface WETH9 { @@ -962,65 +1170,89 @@ interface WETH9 { function transfer(address dst, uint256 wad) external returns (bool); function transferFrom(address src, address dst, uint256 wad) external returns (bool); function withdraw(uint256 wad) external; -}"#; - assert_eq!(output.trim(), weth_interface); -}); +} -const ENS_NAME: &str = "emo.eth"; -const ENS_NAMEHASH: B256 = - b256!("0a21aaf2f6414aa664deb341d1114351fdb023cad07bf53b28e57c26db681910"); -const ENS_ADDRESS: Address = address!("28679A1a632125fbBf7A68d850E50623194A709E"); +"#]]); +}); casttest!(ens_namehash, |_prj, cmd| { - cmd.args(["namehash", ENS_NAME]); - let out = cmd.stdout_lossy().trim().parse::(); - assert_eq!(out, Ok(ENS_NAMEHASH)); + cmd.args(["namehash", "emo.eth"]).assert_success().stdout_eq(str![[r#" +0x0a21aaf2f6414aa664deb341d1114351fdb023cad07bf53b28e57c26db681910 + +"#]]); }); casttest!(ens_lookup, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - cmd.args(["lookup-address", &ENS_ADDRESS.to_string(), "--rpc-url", ð_rpc_url, "--verify"]); - let out = cmd.stdout_lossy(); - assert_eq!(out.trim(), ENS_NAME); + cmd.args([ + "lookup-address", + "0x28679A1a632125fbBf7A68d850E50623194A709E", + "--rpc-url", + ð_rpc_url, + "--verify", + ]) + .assert_success() + .stdout_eq(str![[r#" +emo.eth + +"#]]); }); casttest!(ens_resolve, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - cmd.args(["resolve-name", ENS_NAME, "--rpc-url", ð_rpc_url, "--verify"]); - let out = cmd.stdout_lossy().trim().parse::
(); - assert_eq!(out, Ok(ENS_ADDRESS)); + cmd.args(["resolve-name", "emo.eth", "--rpc-url", ð_rpc_url, "--verify"]) + .assert_success() + .stdout_eq(str![[r#" +0x28679A1a632125fbBf7A68d850E50623194A709E + +"#]]); }); casttest!(ens_resolve_no_dot_eth, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - let name = ENS_NAME.strip_suffix(".eth").unwrap(); - cmd.args(["resolve-name", name, "--rpc-url", ð_rpc_url, "--verify"]); - let (_out, err) = cmd.unchecked_output_lossy(); - assert!(err.contains("not found"), "{err:?}"); + cmd.args(["resolve-name", "emo", "--rpc-url", ð_rpc_url, "--verify"]) + .assert_failure() + .stderr_eq(str![[r#" +Error: +ENS resolver not found for name "emo" + +"#]]); }); casttest!(index7201, |_prj, cmd| { - let tests = - [("example.main", "0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500")]; - for (id, expected) in tests { - cmd.cast_fuse(); - assert_eq!(cmd.args(["index-erc7201", id]).stdout_lossy().trim(), expected); - } + cmd.args(["index-erc7201", "example.main"]).assert_success().stdout_eq(str![[r#" +0x183a6125c38840424c4a85fa12bab2ab606c4b6d0e7cc73c0c06ba5300eab500 + +"#]]); }); casttest!(index7201_unknown_formula_id, |_prj, cmd| { - cmd.args(["index-7201", "test", "--formula-id", "unknown"]).assert_err(); + cmd.args(["index-erc7201", "test", "--formula-id", "unknown"]).assert_failure().stderr_eq( + str![[r#" +Error: +unsupported formula ID: unknown + +"#]], + ); }); casttest!(block_number, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - let s = cmd.args(["block-number", "--rpc-url", eth_rpc_url.as_str()]).stdout_lossy(); + let s = cmd + .args(["block-number", "--rpc-url", eth_rpc_url.as_str()]) + .assert_success() + .get_output() + .stdout_lossy(); assert!(s.trim().parse::().unwrap() > 0, "{s}") }); casttest!(block_number_latest, |_prj, cmd| { let eth_rpc_url = next_http_rpc_endpoint(); - let s = cmd.args(["block-number", "--rpc-url", eth_rpc_url.as_str(), "latest"]).stdout_lossy(); + let s = cmd + .args(["block-number", "--rpc-url", eth_rpc_url.as_str(), "latest"]) + .assert_success() + .get_output() + .stdout_lossy(); assert!(s.trim().parse::().unwrap() > 0, "{s}") }); @@ -1033,6 +1265,8 @@ casttest!(block_number_hash, |_prj, cmd| { eth_rpc_url.as_str(), "0x88e96d4537bea4d9c05d12549907b32561d3bf31f45aae734cdc119f13406cb6", ]) + .assert_success() + .get_output() .stdout_lossy(); assert_eq!(s.trim().parse::().unwrap(), 1, "{s}") }); @@ -1041,6 +1275,7 @@ casttest!(send_eip7702, async |_prj, cmd| { let (_api, handle) = anvil::spawn(NodeConfig::test().with_hardfork(Some(Hardfork::PragueEOF))).await; let endpoint = handle.http_endpoint(); + cmd.args([ "send", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", @@ -1063,12 +1298,13 @@ casttest!(send_eip7702, async |_prj, cmd| { }); casttest!(hash_message, |_prj, cmd| { - let tests = [ - ("hello", "0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750"), - ("0x68656c6c6f", "0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750"), - ]; - for (message, expected) in tests { - cmd.cast_fuse(); - assert_eq!(cmd.args(["hash-message", message]).stdout_lossy().trim(), expected); - } + cmd.args(["hash-message", "hello"]).assert_success().stdout_eq(str![[r#" +0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750 + +"#]]); + + cmd.cast_fuse().args(["hash-message", "0x68656c6c6f"]).assert_success().stdout_eq(str![[r#" +0x50b2c43fd39106bafbba0da34fc430e1f91e3c96ea2acee2bc34119f92b37750 + +"#]]); }); diff --git a/crates/forge/benches/test.rs b/crates/forge/benches/test.rs index 7646a3c214a33..593bce3d320d5 100644 --- a/crates/forge/benches/test.rs +++ b/crates/forge/benches/test.rs @@ -1,5 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use foundry_test_utils::{util::setup_forge_remote, TestCommand, TestProject}; +use foundry_test_utils::{ + util::{lossy_string, setup_forge_remote}, + TestCommand, TestProject, +}; /// Returns a cloned and `forge built` `solmate` project fn built_solmate() -> (TestProject, TestCommand) { @@ -15,7 +18,9 @@ fn forge_test_benchmark(c: &mut Criterion) { let mut cmd = prj.forge_command(); cmd.arg("test"); b.iter(|| { - cmd.print_output(); + let output = cmd.execute(); + println!("stdout:\n{}", lossy_string(&output.stdout)); + println!("\nstderr:\n{}", lossy_string(&output.stderr)); }); }); } diff --git a/crates/forge/tests/cli/build.rs b/crates/forge/tests/cli/build.rs index f5848173c921e..d9861f19e392d 100644 --- a/crates/forge/tests/cli/build.rs +++ b/crates/forge/tests/cli/build.rs @@ -45,7 +45,7 @@ contract Dummy { // tests build output is as expected forgetest_init!(exact_build_output, |prj, cmd| { cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" -Compiling 27 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -54,11 +54,15 @@ Compiler run successful! // tests build output is as expected forgetest_init!(build_sizes_no_forge_std, |prj, cmd| { - cmd.args(["build", "--sizes"]); - let stdout = cmd.stdout_lossy(); - assert!(!stdout.contains("console"), "\n{stdout}"); - assert!(!stdout.contains("std"), "\n{stdout}"); - assert!(stdout.contains("Counter"), "\n{stdout}"); + cmd.args(["build", "--sizes"]).assert_success().stdout_eq(str![ + r#" +... +| Contract | Size (B) | Margin (B) | +|----------|----------|------------| +| Counter | 247 | 24,329 | +... +"# + ]); }); // tests that skip key in config can be used to skip non-compilable contract diff --git a/crates/forge/tests/cli/cmd.rs b/crates/forge/tests/cli/cmd.rs index 9160973e9b024..1a8d2cb7e08c9 100644 --- a/crates/forge/tests/cli/cmd.rs +++ b/crates/forge/tests/cli/cmd.rs @@ -1,7 +1,6 @@ //! Contains various tests for checking forge's commands use crate::constants::*; -use alloy_primitives::hex; use foundry_compilers::artifacts::{remappings::Remapping, ConfigurableContractArtifact, Metadata}; use foundry_config::{ parse_with_profile, BasicConfig, Chain, Config, FuzzConfig, InvariantConfig, SolidityErrorCode, @@ -21,8 +20,21 @@ use std::{ // tests `--help` is printed to std out forgetest!(print_help, |_prj, cmd| { - cmd.arg("--help"); - cmd.assert_non_empty_stdout(); + cmd.arg("--help").assert_success().stdout_eq(str![[r#" +Build, test, fuzz, debug and deploy Solidity contracts + +Usage: forge[..] + +Commands: +... + +Options: + -h, --help Print help + -V, --version Print version + +Find more information in the book: http://book.getfoundry.sh/reference/forge/forge.html + +"#]]); }); // checks that `clean` can be invoked even if out and cache don't exist @@ -52,10 +64,9 @@ forgetest!( fs::write(block2_file, "{}").unwrap(); fs::create_dir_all(etherscan_cache_dir).unwrap(); - cmd.args(["cache", "ls"]); - let output_string = String::from_utf8_lossy(&cmd.output().stdout).to_string(); - let output_lines = output_string.split('\n').collect::>(); - println!("{output_string}"); + let output = cmd.args(["cache", "ls"]).assert_success().get_output().stdout_lossy(); + let output_lines = output.split('\n').collect::>(); + println!("{output}"); assert_eq!(output_lines.len(), 6); assert!(output_lines[0].starts_with("-️ mainnet (")); @@ -213,8 +224,14 @@ forgetest!(can_init_repo_with_config, |prj, cmd| { let foundry_toml = prj.root().join(Config::FILE_NAME); assert!(!foundry_toml.exists()); - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let s = read_string(&foundry_toml); let _config: BasicConfig = parse_with_profile(&s).unwrap().unwrap().1; @@ -256,8 +273,13 @@ https://github.com/foundry-rs/foundry/issues/new/choose forgetest!(can_init_no_git, |prj, cmd| { prj.wipe(); - cmd.arg("init").arg(prj.root()).arg("--no-git"); - cmd.assert_non_empty_stdout(); + cmd.arg("init").arg(prj.root()).arg("--no-git").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 + +"#]]); prj.assert_config_exists(); assert!(!prj.root().join(".git").exists()); @@ -269,8 +291,7 @@ forgetest!(can_init_no_git, |prj, cmd| { forgetest!(can_init_quiet, |prj, cmd| { prj.wipe(); - cmd.arg("init").arg(prj.root()).arg("-q"); - let _ = cmd.output(); + cmd.arg("init").arg(prj.root()).arg("-q").assert_empty_stdout(); }); // `forge init foobar` works with dir argument @@ -284,10 +305,14 @@ forgetest!(can_init_with_dir, |prj, cmd| { // `forge init foobar --template [template]` works with dir argument forgetest!(can_init_with_dir_and_template, |prj, cmd| { - cmd.args(["init", "foobar", "--template", "foundry-rs/forge-template"]); + cmd.args(["init", "foobar", "--template", "foundry-rs/forge-template"]) + .assert_success() + .stdout_eq(str![[r#" +Initializing [..] from https://github.com/foundry-rs/forge-template... + Initialized forge project + +"#]]); - cmd.assert_success(); - cmd.assert_non_empty_stdout(); assert!(prj.root().join("foobar/.git").exists()); assert!(prj.root().join("foobar/foundry.toml").exists()); assert!(prj.root().join("foobar/lib/forge-std").exists()); @@ -306,10 +331,14 @@ forgetest!(can_init_with_dir_and_template_and_branch, |prj, cmd| { "foundry-rs/forge-template", "--branch", "test/deployments", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +Initializing [..] from https://github.com/foundry-rs/forge-template... + Initialized forge project + +"#]]); - cmd.assert_success(); - cmd.assert_non_empty_stdout(); assert!(prj.root().join("foobar/.dapprc").exists()); assert!(prj.root().join("foobar/lib/ds-test").exists()); // assert that gitmodules were correctly initialized @@ -321,11 +350,22 @@ forgetest!(can_init_with_dir_and_template_and_branch, |prj, cmd| { // `forge init --force` works on non-empty dirs forgetest!(can_init_non_empty, |prj, cmd| { prj.create_file("README.md", "non-empty dir"); - cmd.arg("init").arg(prj.root()); - cmd.assert_err(); + cmd.arg("init").arg(prj.root()).assert_failure().stderr_eq(str![[r#" +Error: +Cannot run `init` on a non-empty directory. +Run with the `--force` flag to initialize regardless. + +"#]]); + + cmd.arg("--force").assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); - cmd.arg("--force"); - cmd.assert_non_empty_stdout(); assert!(prj.root().join(".git").exists()); assert!(prj.root().join("lib/forge-std").exists()); }); @@ -345,11 +385,21 @@ forgetest!(can_init_in_empty_repo, |prj, cmd| { assert!(status.success()); assert!(root.join(".git").exists()); - cmd.arg("init").arg(root); - cmd.assert_err(); + cmd.arg("init").arg(root).assert_failure().stderr_eq(str![[r#" +Error: +Cannot run `init` on a non-empty directory. +Run with the `--force` flag to initialize regardless. + +"#]]); + + cmd.arg("--force").assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project - cmd.arg("--force"); - cmd.assert_non_empty_stdout(); +"#]]); assert!(root.join("lib/forge-std").exists()); }); @@ -371,11 +421,21 @@ forgetest!(can_init_in_non_empty_repo, |prj, cmd| { prj.create_file("README.md", "non-empty dir"); prj.create_file(".gitignore", "not foundry .gitignore"); - cmd.arg("init").arg(root); - cmd.assert_err(); + cmd.arg("init").arg(root).assert_failure().stderr_eq(str![[r#" +Error: +Cannot run `init` on a non-empty directory. +Run with the `--force` flag to initialize regardless. - cmd.arg("--force"); - cmd.assert_non_empty_stdout(); +"#]]); + + cmd.arg("--force").assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); assert!(root.join("lib/forge-std").exists()); // not overwritten @@ -388,8 +448,13 @@ forgetest!(can_init_in_non_empty_repo, |prj, cmd| { forgetest!(can_init_vscode, |prj, cmd| { prj.wipe(); - cmd.arg("init").arg(prj.root()).arg("--vscode"); - cmd.assert_non_empty_stdout(); + cmd.arg("init").arg(prj.root()).arg("--vscode").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 + +"#]]); let settings = prj.root().join(".vscode/settings.json"); assert!(settings.is_file()); @@ -411,8 +476,16 @@ forgetest!(can_init_vscode, |prj, cmd| { // checks that forge can init with template forgetest!(can_init_template, |prj, cmd| { prj.wipe(); - cmd.args(["init", "--template", "foundry-rs/forge-template"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); + + cmd.args(["init", "--template", "foundry-rs/forge-template"]) + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" +Initializing [..] from https://github.com/foundry-rs/forge-template... + Initialized forge project + +"#]]); + assert!(prj.root().join(".git").exists()); assert!(prj.root().join("foundry.toml").exists()); assert!(prj.root().join("lib/forge-std").exists()); @@ -426,8 +499,14 @@ forgetest!(can_init_template, |prj, cmd| { forgetest!(can_init_template_with_branch, |prj, cmd| { prj.wipe(); cmd.args(["init", "--template", "foundry-rs/forge-template", "--branch", "test/deployments"]) - .arg(prj.root()); - cmd.assert_non_empty_stdout(); + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" +Initializing [..] from https://github.com/foundry-rs/forge-template... + Initialized forge project + +"#]]); + assert!(prj.root().join(".git").exists()); assert!(prj.root().join(".dapprc").exists()); assert!(prj.root().join("lib/ds-test").exists()); @@ -440,8 +519,13 @@ forgetest!(can_init_template_with_branch, |prj, cmd| { // checks that init fails when the provided template doesn't exist forgetest!(fail_init_nonexistent_template, |prj, cmd| { prj.wipe(); - cmd.args(["init", "--template", "a"]).arg(prj.root()); - cmd.assert_non_empty_stderr(); + cmd.args(["init", "--template", "a"]).arg(prj.root()).assert_failure().stderr_eq(str![[r#" +remote: Not Found +fatal: repository 'https://github.com/a/' not found +Error: +git fetch exited with code 128 + +"#]]); }); // checks that clone works @@ -457,8 +541,20 @@ forgetest!(can_clone, |prj, cmd| { next_etherscan_api_key().as_str(), "0x044b75f554b886A065b9567891e45c79542d7357", ]) - .arg(prj.root()); - cmd.assert_non_empty_stdout(); + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" +Downloading the source code of 0x044b75f554b886A065b9567891e45c79542d7357 from Etherscan... +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project +Collecting the creation information of 0x044b75f554b886A065b9567891e45c79542d7357 from Etherscan... +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let s = read_string(&foundry_toml); let _config: BasicConfig = parse_with_profile(&s).unwrap().unwrap().1; @@ -475,8 +571,8 @@ forgetest!(can_clone_quiet, |prj, cmd| { "--quiet", "0xDb53f47aC61FE54F456A4eb3E09832D08Dd7BEec", ]) - .arg(prj.root()); - cmd.assert_empty_stdout(); + .arg(prj.root()) + .assert_empty_stdout(); }); // checks that clone works with --no-remappings-txt @@ -493,8 +589,20 @@ forgetest!(can_clone_no_remappings_txt, |prj, cmd| { "--no-remappings-txt", "0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf", ]) - .arg(prj.root()); - cmd.assert_non_empty_stdout(); + .arg(prj.root()) + .assert_success() + .stdout_eq(str![[r#" +Downloading the source code of 0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf from Etherscan... +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project +Collecting the creation information of 0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf from Etherscan... +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let s = read_string(&foundry_toml); let _config: BasicConfig = parse_with_profile(&s).unwrap().unwrap().1; @@ -507,16 +615,21 @@ forgetest!(can_clone_keep_directory_structure, |prj, cmd| { let foundry_toml = prj.root().join(Config::FILE_NAME); assert!(!foundry_toml.exists()); - cmd.args([ - "clone", - "--etherscan-api-key", - next_etherscan_api_key().as_str(), - "--keep-directory-structure", - "0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf", - ]) - .arg(prj.root()); - let out = cmd.unchecked_output(); - if out.stdout_lossy().contains("502 Bad Gateway") { + let output = cmd + .forge_fuse() + .args([ + "clone", + "--etherscan-api-key", + next_etherscan_api_key().as_str(), + "--keep-directory-structure", + "0x33e690aEa97E4Ef25F0d140F1bf044d663091DAf", + ]) + .arg(prj.root()) + .assert_success() + .get_output() + .stdout_lossy(); + + if output.contains("502 Bad Gateway") { // etherscan nginx proxy issue, skip this test: // // stdout: @@ -526,10 +639,9 @@ forgetest!(can_clone_keep_directory_structure, |prj, cmd| { // Gateway\r\n\r\n

502 Bad // Gateway

\r\n
nginx
\r\n\r\n\r\n" - eprintln!("Skipping test due to 502 Bad Gateway: {}", cmd.make_error_message(&out, false)); - return + eprintln!("Skipping test due to 502 Bad Gateway"); + return; } - cmd.ensure_success(&out).unwrap(); let s = read_string(&foundry_toml); let _config: BasicConfig = parse_with_profile(&s).unwrap().unwrap().1; @@ -557,15 +669,18 @@ forgetest!(can_clean_hardhat, PathStyle::HardHat, |prj, cmd| { forgetest_init!(can_clean_config, |prj, cmd| { let config = Config { out: "custom-out".into(), ..Default::default() }; prj.write_config(config); - cmd.arg("build"); - cmd.assert_non_empty_stdout(); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); // default test contract is written in custom out directory let artifact = prj.root().join(format!("custom-out/{TEMPLATE_TEST_CONTRACT_ARTIFACT_JSON}")); assert!(artifact.exists()); - cmd.forge_fuse().arg("clean"); - cmd.output(); + cmd.forge_fuse().arg("clean").assert_empty_stdout(); assert!(!artifact.exists()); }); @@ -586,24 +701,37 @@ forgetest_init!(can_clean_test_cache, |prj, cmd| { assert!(fuzz_cache_dir.exists()); assert!(invariant_cache_dir.exists()); - cmd.forge_fuse().arg("clean"); - cmd.output(); + cmd.forge_fuse().arg("clean").assert_empty_stdout(); assert!(!fuzz_cache_dir.exists()); assert!(!invariant_cache_dir.exists()); }); // checks that extra output works forgetest_init!(can_emit_extra_output, |prj, cmd| { - cmd.args(["build", "--extra-output", "metadata"]); - cmd.assert_non_empty_stdout(); + prj.clear(); + + cmd.args(["build", "--extra-output", "metadata"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let artifact_path = prj.paths().artifacts.join(TEMPLATE_CONTRACT_ARTIFACT_JSON); let artifact: ConfigurableContractArtifact = foundry_compilers::utils::read_json_file(&artifact_path).unwrap(); assert!(artifact.metadata.is_some()); - cmd.forge_fuse().args(["build", "--extra-output-files", "metadata", "--force"]).root_arg(); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse() + .args(["build", "--extra-output-files", "metadata", "--force"]) + .root_arg() + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let metadata_path = prj.paths().artifacts.join(format!("{TEMPLATE_CONTRACT_ARTIFACT_BASE}.metadata.json")); @@ -612,8 +740,14 @@ forgetest_init!(can_emit_extra_output, |prj, cmd| { // checks that extra output works forgetest_init!(can_emit_multiple_extra_output, |prj, cmd| { - cmd.args(["build", "--extra-output", "metadata", "ir-optimized", "--extra-output", "ir"]); - cmd.assert_non_empty_stdout(); + cmd.args(["build", "--extra-output", "metadata", "ir-optimized", "--extra-output", "ir"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let artifact_path = prj.paths().artifacts.join(TEMPLATE_CONTRACT_ARTIFACT_JSON); let artifact: ConfigurableContractArtifact = @@ -631,8 +765,14 @@ forgetest_init!(can_emit_multiple_extra_output, |prj, cmd| { "evm.bytecode.sourceMap", "--force", ]) - .root_arg(); - cmd.assert_non_empty_stdout(); + .root_arg() + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let metadata_path = prj.paths().artifacts.join(format!("{TEMPLATE_CONTRACT_ARTIFACT_BASE}.metadata.json")); @@ -659,10 +799,30 @@ contract Greeter { ) .unwrap(); - cmd.arg("build"); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (5667): Unused function parameter. Remove or comment out the variable name to silence this warning. + [FILE]:5:18: + | +5 | function foo(uint256 a) public { + | ^^^^^^^^^ + +Warning (2072): Unused local variable. + [FILE]:6:9: + | +6 | uint256 x = 1; + | ^^^^^^^^^ + +Warning (2018): Function state mutability can be restricted to pure + [FILE]:5:5: + | +5 | function foo(uint256 a) public { + | ^ (Relevant source part starts here and spans across multiple lines). + - let output = cmd.stdout_lossy(); - assert!(output.contains("Warning"), "{output}"); +"#]]); }); // Tests that direct import paths are handled correctly @@ -700,13 +860,12 @@ library FooLib { ) .unwrap(); - cmd.arg("build"); - - assert!(cmd.stdout_lossy().ends_with( - " + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] Compiler run successful! -" - )); + +"#]]); }); // tests that the `inspect` command works correctly @@ -726,17 +885,18 @@ contract Foo { ) .unwrap(); - let check_output = |output: String| { - let output = output.trim(); - assert!(output.starts_with("0x") && hex::decode(output).is_ok(), "{output}"); - }; + cmd.arg("inspect").arg(contract_name).arg("bytecode").assert_success().stdout_eq(str![[r#" +0x60806040[..] - cmd.arg("inspect").arg(contract_name).arg("bytecode"); - check_output(cmd.stdout_lossy()); +"#]]); let info = format!("src/{}:{}", path.file_name().unwrap().to_string_lossy(), contract_name); - cmd.forge_fuse().arg("inspect").arg(info).arg("bytecode"); - check_output(cmd.stdout_lossy()); + cmd.forge_fuse().arg("inspect").arg(info).arg("bytecode").assert_success().stdout_eq(str![[ + r#" +0x60806040[..] + +"# + ]]); }); // test that `forge snapshot` commands work @@ -757,7 +917,7 @@ contract ATest is DSTest { .unwrap(); cmd.args(["snapshot"]).assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -769,8 +929,16 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); - cmd.arg("--check"); - let _ = cmd.output(); + cmd.arg("--check").assert_success().stdout_eq(str![[r#" +No files changed, compilation skipped + +Ran 1 test for src/ATest.t.sol:ATest +[PASS] testExample() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // test that `forge build` does not print `(with warnings)` if file path is ignored @@ -793,20 +961,27 @@ contract A { ) .unwrap(); - cmd.args(["build", "--force"]); - let out = cmd.stdout_lossy(); - // expect no warning as path is ignored - assert!(out.contains("Compiler run successful!")); - assert!(!out.contains("Compiler run successful with warnings:")); + cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); // Reconfigure without ignored paths or error codes and check for warnings // need to reset empty error codes as default would set some error codes prj.write_config(Config { ignored_error_codes: vec![], ..Default::default() }); - let out = cmd.stdout_lossy(); - // expect warnings as path is not ignored - assert!(out.contains("Compiler run successful with warnings:"), "{out}"); - assert!(out.contains("Warning") && out.contains("SPDX-License-Identifier"), "{out}"); + cmd.forge_fuse().args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +[FILE] + + +"#]]); }); // test that `forge build` does not print `(with warnings)` if there arent any @@ -827,19 +1002,27 @@ contract A { ) .unwrap(); - cmd.args(["build", "--force"]); - let out = cmd.stdout_lossy(); - // no warnings - assert!(out.contains("Compiler run successful!")); - assert!(!out.contains("Compiler run successful with warnings:")); + cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); // don't ignore errors let config = Config { ignored_error_codes: vec![], ..Default::default() }; prj.write_config(config); - let out = cmd.stdout_lossy(); - assert!(out.contains("Compiler run successful with warnings:"), "{out}"); - assert!(out.contains("Warning") && out.contains("SPDX-License-Identifier"), "{out}"); + cmd.forge_fuse().args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +[FILE] + + +"#]]); }); // test that `forge build` compiles when severity set to error, fails when set to warning, and @@ -859,14 +1042,30 @@ contract A { .unwrap(); // there are no errors - cmd.args(["build", "--force"]); - let out = cmd.stdout_lossy(); - assert!(out.contains("Compiler run successful with warnings:"), "{out}"); + cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +[FILE] + + +"#]]); // warning fails to compile let config = Config { ignored_error_codes: vec![], deny_warnings: true, ..Default::default() }; prj.write_config(config); - cmd.assert_err(); + + cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" +Error: +Compiler run failed: +Warning (1878): SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +Warning: SPDX license identifier not provided in source file. Before publishing, consider adding a comment containing "SPDX-License-Identifier: " to each source file. Use "SPDX-License-Identifier: UNLICENSED" for non-open-source code. Please see https://spdx.org for more information. +[FILE] + + +"#]]); // ignores error code and compiles let config = Config { @@ -875,10 +1074,13 @@ contract A { ..Default::default() }; prj.write_config(config); - let out = cmd.stdout_lossy(); - assert!(out.contains("Compiler run successful!")); - assert!(!out.contains("Compiler run successful with warnings:")); + cmd.forge_fuse().args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); }); // test that a failing `forge build` does not impact followup builds @@ -910,8 +1112,14 @@ contract BTest is DSTest { ) .unwrap(); - cmd.arg("build"); - cmd.assert_non_empty_stdout(); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +... +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); + prj.assert_cache_exists(); prj.assert_artifacts_dir_exists(); @@ -928,16 +1136,34 @@ contract CTest is DSTest { prj.add_source("CTest.t.sol", syntax_err).unwrap(); // `forge build --force` which should fail - cmd.arg("--force"); - cmd.assert_err(); + cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" +Error: +Compiler run failed: +Error (2314): Expected ';' but got identifier + [FILE]:7:19: + | +7 | THIS WILL CAUSE AN ERROR + | ^^^^^ + + +"#]]); // but ensure this cleaned cache and artifacts assert!(!prj.paths().artifacts.exists()); assert!(!prj.cache().exists()); // still errors - cmd.forge_fuse().arg("build"); - cmd.assert_err(); + cmd.forge_fuse().args(["build", "--force"]).assert_failure().stderr_eq(str![[r#" +Error: +Compiler run failed: +Error (2314): Expected ';' but got identifier + [FILE]:7:19: + | +7 | THIS WILL CAUSE AN ERROR + | ^^^^^ + + +"#]]); // resolve the error by replacing the file prj.add_source( @@ -953,7 +1179,13 @@ contract CTest is DSTest { ) .unwrap(); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse().args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); + prj.assert_cache_exists(); prj.assert_artifacts_dir_exists(); @@ -962,7 +1194,17 @@ contract CTest is DSTest { // introduce the error again but building without force prj.add_source("CTest.t.sol", syntax_err).unwrap(); - cmd.assert_err(); + cmd.forge_fuse().arg("build").assert_failure().stderr_eq(str![[r#" +Error: +Compiler run failed: +Error (2314): Expected ';' but got identifier + [FILE]:7:19: + | +7 | THIS WILL CAUSE AN ERROR + | ^^^^^ + + +"#]]); // ensure unchanged cache file let cache_after = fs::read_to_string(prj.cache()).unwrap(); @@ -981,8 +1223,15 @@ forgetest!(can_install_and_remove, |prj, cmd| { let forge_std_mod = git_mod.join("forge-std"); let install = |cmd: &mut TestCommand| { - cmd.forge_fuse().args(["install", "foundry-rs/forge-std", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse() + .args(["install", "foundry-rs/forge-std", "--no-commit"]) + .assert_success() + .stdout_eq(str![[r#" +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + +"#]]); + assert!(forge_std.exists()); assert!(forge_std_mod.exists()); @@ -991,8 +1240,14 @@ forgetest!(can_install_and_remove, |prj, cmd| { }; let remove = |cmd: &mut TestCommand, target: &str| { - cmd.forge_fuse().args(["remove", "--force", target]); - cmd.assert_non_empty_stdout(); + // TODO: flaky behavior with URL, sometimes it is None, sometimes it is Some("https://github.com/lib/forge-std") + cmd.forge_fuse().args(["remove", "--force", target]).assert_success().stdout_eq(str![[ + r#" +Removing 'forge-std' in [..], (url: [..], tag: None) + +"# + ]]); + assert!(!forge_std.exists()); assert!(!forge_std_mod.exists()); let submods = read_string(&git_mod_file); @@ -1017,8 +1272,8 @@ forgetest!(can_install_empty, |prj, cmd| { // create initial commit fs::write(prj.root().join("README.md"), "Initial commit").unwrap(); - cmd.git_add().unwrap(); - cmd.git_commit("Initial commit").unwrap(); + cmd.git_add(); + cmd.git_commit("Initial commit"); cmd.forge_fuse().args(["install"]); cmd.assert_empty_stdout(); @@ -1036,8 +1291,15 @@ forgetest!(can_reinstall_after_manual_remove, |prj, cmd| { let forge_std_mod = git_mod.join("forge-std"); let install = |cmd: &mut TestCommand| { - cmd.forge_fuse().args(["install", "foundry-rs/forge-std", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse() + .args(["install", "foundry-rs/forge-std", "--no-commit"]) + .assert_success() + .stdout_eq(str![[r#" +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + +"#]]); + assert!(forge_std.exists()); assert!(forge_std_mod.exists()); @@ -1098,8 +1360,14 @@ forgetest!( let package_mod = git_mod.join("forge-5980-test"); // install main dependency - cmd.forge_fuse().args(["install", "evalir/forge-5980-test", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse() + .args(["install", "evalir/forge-5980-test", "--no-commit"]) + .assert_success() + .stdout_eq(str![[r#" +Installing forge-5980-test in [..] (url: Some("https://github.com/evalir/forge-5980-test"), tag: None) + Installed forge-5980-test + +"#]]); // assert paths exist assert!(package.exists()); @@ -1111,8 +1379,7 @@ forgetest!( // try to update the top-level dependency; there should be no update for this dependency, // but its sub-dependency has upstream (breaking) changes; forge should not attempt to // update the sub-dependency - cmd.forge_fuse().args(["update", "lib/forge-5980-test"]); - cmd.stdout_lossy(); + cmd.forge_fuse().args(["update", "lib/forge-5980-test"]).assert_empty_stdout(); // add explicit remappings for test file let config = Config { @@ -1142,9 +1409,12 @@ contract CounterCopy is Counter { .unwrap(); // build and check output - cmd.forge_fuse().arg("build"); - let output = cmd.stdout_lossy(); - assert!(output.contains("Compiler run successful",)); + cmd.forge_fuse().arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); } ); @@ -1245,23 +1515,36 @@ contract ContractThreeTest is DSTest { gas_reports_ignore: (vec![]), ..Default::default() }); - cmd.forge_fuse(); - let first_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + + let first_out = cmd + .forge_fuse() + .arg("test") + .arg("--gas-report") + .assert_success() + .get_output() + .stdout_lossy(); assert!(first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); - cmd.forge_fuse(); prj.write_config(Config { gas_reports: (vec![]), ..Default::default() }); - cmd.forge_fuse(); - let second_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let second_out = cmd + .forge_fuse() + .arg("test") + .arg("--gas-report") + .assert_success() + .get_output() + .stdout_lossy(); assert!(second_out.contains("foo") && second_out.contains("bar") && second_out.contains("baz")); - cmd.forge_fuse(); prj.write_config(Config { gas_reports: (vec!["*".to_string()]), ..Default::default() }); - cmd.forge_fuse(); - let third_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let third_out = cmd + .forge_fuse() + .arg("test") + .arg("--gas-report") + .assert_success() + .get_output() + .stdout_lossy(); assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); - cmd.forge_fuse(); prj.write_config(Config { gas_reports: (vec![ "ContractOne".to_string(), @@ -1270,8 +1553,13 @@ contract ContractThreeTest is DSTest { ]), ..Default::default() }); - cmd.forge_fuse(); - let fourth_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let fourth_out = cmd + .forge_fuse() + .arg("test") + .arg("--gas-report") + .assert_success() + .get_output() + .stdout_lossy(); assert!(fourth_out.contains("foo") && fourth_out.contains("bar") && fourth_out.contains("baz")); }); @@ -1369,7 +1657,8 @@ contract ContractThreeTest is DSTest { // report for One prj.write_config(Config { gas_reports: vec!["ContractOne".to_string()], ..Default::default() }); cmd.forge_fuse(); - let first_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let first_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!( first_out.contains("foo") && !first_out.contains("bar") && !first_out.contains("baz"), "foo:\n{first_out}" @@ -1378,7 +1667,8 @@ contract ContractThreeTest is DSTest { // report for Two prj.write_config(Config { gas_reports: vec!["ContractTwo".to_string()], ..Default::default() }); cmd.forge_fuse(); - let second_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let second_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!( !second_out.contains("foo") && second_out.contains("bar") && !second_out.contains("baz"), "bar:\n{second_out}" @@ -1390,7 +1680,8 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let third_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let third_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!( !third_out.contains("foo") && !third_out.contains("bar") && third_out.contains("baz"), "baz:\n{third_out}" @@ -1495,7 +1786,8 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let first_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let first_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!(!first_out.contains("foo") && first_out.contains("bar") && first_out.contains("baz")); // ignore ContractTwo @@ -1506,7 +1798,8 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let second_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let second_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!( second_out.contains("foo") && !second_out.contains("bar") && second_out.contains("baz") ); @@ -1523,7 +1816,8 @@ contract ContractThreeTest is DSTest { ..Default::default() }); cmd.forge_fuse(); - let third_out = cmd.arg("test").arg("--gas-report").stdout_lossy(); + let third_out = + cmd.arg("test").arg("--gas-report").assert_success().get_output().stdout_lossy(); assert!(third_out.contains("foo") && third_out.contains("bar") && third_out.contains("baz")); }); @@ -1566,9 +1860,12 @@ forgetest_init!(can_use_absolute_imports, |prj, cmd| { ) .unwrap(); - cmd.arg("build"); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("Compiler run successful")); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); }); // @@ -1609,9 +1906,12 @@ contract MyTest is IMyTest {} ) .unwrap(); - cmd.arg("build"); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("Compiler run successful"), "{stdout}"); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); }); // checks `forge inspect irOptimized works @@ -1621,61 +1921,90 @@ forgetest_init!(can_inspect_ir_optimized, |_prj, cmd| { }); // checks forge bind works correctly on the default project -forgetest_init!(can_bind, |_prj, cmd| { - cmd.arg("bind"); - cmd.assert_non_empty_stdout(); +forgetest_init!(can_bind, |prj, cmd| { + prj.clear(); + + cmd.arg("bind").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Generating bindings for [..] contracts +Bindings have been generated to [..] + +"#]]); }); // checks missing dependencies are auto installed forgetest_init!(can_install_missing_deps_test, |prj, cmd| { + prj.clear(); + // wipe forge-std let forge_std_dir = prj.root().join("lib/forge-std"); pretty_err(&forge_std_dir, fs::remove_dir_all(&forge_std_dir)); - cmd.arg("test"); + cmd.arg("test").assert_success().stdout_eq(str![[r#" +Missing dependencies found. Installing now... + +[UPDATING_DEPENDENCIES] +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for test/Counter.t.sol:CounterTest +[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS]) +[PASS] test_Increment() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] - let output = cmd.stdout_lossy(); - assert!(output.contains("Missing dependencies found. Installing now"), "{}", output); - assert!(output.contains("[PASS]"), "{}", output); +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); }); // checks missing dependencies are auto installed forgetest_init!(can_install_missing_deps_build, |prj, cmd| { + prj.clear(); + // wipe forge-std let forge_std_dir = prj.root().join("lib/forge-std"); pretty_err(&forge_std_dir, fs::remove_dir_all(&forge_std_dir)); - cmd.arg("build"); + // Build the project + cmd.arg("build").assert_success().stdout_eq(str![[r#" +Missing dependencies found. Installing now... + +[UPDATING_DEPENDENCIES] +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); - let output = cmd.stdout_lossy(); - assert!(output.contains("Missing dependencies found. Installing now"), "{output}"); + // Expect compilation to be skipped as no files have changed + cmd.arg("build").assert_success().stdout_eq(str![[r#" +No files changed, compilation skipped - // re-run - let output = cmd.stdout_lossy(); - assert!(!output.contains("Missing dependencies found. Installing now"), "{output}"); - assert!(output.contains("No files changed, compilation skipped"), "{output}"); +"#]]); }); // checks that extra output works forgetest_init!(can_build_skip_contracts, |prj, cmd| { prj.clear(); - // only builds the single template contract `src/*` + // Only builds the single template contract `src/*` cmd.args(["build", "--skip", "tests", "--skip", "scripts"]).assert_success().stdout_eq(str![[ r#" -... -Compiling 1 files [..] -[..] +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] Compiler run successful! -... + "# ]]); - // re-run command - let out = cmd.stdout_lossy(); + // Expect compilation to be skipped as no files have changed + cmd.arg("build").assert_success().stdout_eq(str![[r#" +No files changed, compilation skipped - // unchanged - assert!(out.contains("No files changed, compilation skipped"), "{}", out); +"#]]); }); forgetest_init!(can_build_skip_glob, |prj, cmd| { @@ -1693,7 +2022,7 @@ function test_run() external {} cmd.args(["build", "--skip", "*/test/**", "--skip", "*/script/**", "--force"]) .assert_success() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1703,7 +2032,7 @@ Compiler run successful! .args(["build", "--skip", "./test/**", "--skip", "./script/**", "--force"]) .assert_success() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1740,7 +2069,7 @@ function test_bar() external {} // Build 2 files within test dir prj.clear(); cmd.args(["build", "test", "--force"]).assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1750,7 +2079,7 @@ Compiler run successful! prj.clear(); cmd.forge_fuse(); cmd.args(["build", "src", "--force"]).assert_success().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1760,7 +2089,7 @@ Compiler run successful! prj.clear(); cmd.forge_fuse(); cmd.args(["build", "src", "test", "--force"]).assert_success().stdout_eq(str![[r#" -Compiling 3 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1770,7 +2099,7 @@ Compiler run successful! prj.clear(); cmd.forge_fuse(); cmd.args(["build", "test/Bar.sol", "--force"]).assert_success().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1781,45 +2110,44 @@ Compiler run successful! forgetest_init!(can_build_sizes_repeatedly, |prj, cmd| { prj.clear_cache(); - cmd.args(["build", "--sizes"]); - let out = cmd.stdout_lossy(); - - // contains: Counter ┆ 0.247 ┆ 24.329 - assert!(out.contains(TEMPLATE_CONTRACT)); + cmd.args(["build", "--sizes"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +| Contract | Size (B) | Margin (B) | +|----------|----------|------------| +| Counter | 247 | 24,329 | - // get the entire table - let table = out.split("Compiler run successful!").nth(1).unwrap().trim(); - let unchanged = cmd.stdout_lossy(); - assert!(unchanged.contains(table), "{}", table); +"#]]); }); // checks that build --names includes all contracts even if unchanged forgetest_init!(can_build_names_repeatedly, |prj, cmd| { prj.clear_cache(); - cmd.args(["build", "--names"]); - let out = cmd.stdout_lossy(); - - assert!(out.contains(TEMPLATE_CONTRACT)); - - // get the entire list - let list = out.split("Compiler run successful!").nth(1).unwrap().trim(); + cmd.args(["build", "--names"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + compiler version: [..] + - [..] +... - let unchanged = cmd.stdout_lossy(); - assert!(unchanged.contains(list), "{}", list); +"#]]); }); // forgetest_init!(can_inspect_counter_pretty, |prj, cmd| { - cmd.args(["inspect", "src/Counter.sol:Counter", "abi", "--pretty"]); - let output = cmd.stdout_lossy(); - assert_eq!( - output.trim(), - "interface Counter { + cmd.args(["inspect", "src/Counter.sol:Counter", "abi", "--pretty"]).assert_success().stdout_eq( + str![[r#" +interface Counter { function increment() external; function number() external view returns (uint256); function setNumber(uint256 newNumber) external; -}" +} + + +"#]], ); }); diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 88ac186b20cb7..2cb11e72f1861 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -14,7 +14,7 @@ use foundry_config::{ use foundry_evm::opts::EvmOpts; use foundry_test_utils::{ foundry_compilers::artifacts::{remappings::Remapping, EvmVersion}, - util::{pretty_err, TestCommand, OTHER_SOLC_VERSION}, + util::{pretty_err, OutputExt, TestCommand, OTHER_SOLC_VERSION}, }; use path_slash::PathBufExt; use similar_asserts::assert_eq; @@ -160,10 +160,10 @@ forgetest!(can_extract_config_values, |prj, cmd| { // tests config gets printed to std out forgetest!(can_show_config, |prj, cmd| { - cmd.arg("config"); let expected = Config::load_with_root(prj.root()).to_string_pretty().unwrap().trim().to_string(); - assert_eq!(expected, cmd.stdout_lossy().trim().to_string()); + let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string(); + assert_eq!(expected, output); }); // checks that config works @@ -188,9 +188,9 @@ forgetest_init!(can_override_config, |prj, cmd| { Remapping::from(profile.remappings[0].clone()).to_string() ); - cmd.arg("config"); - let expected = profile.to_string_pretty().unwrap(); - assert_eq!(expected.trim().to_string(), cmd.stdout_lossy().trim().to_string()); + let expected = profile.to_string_pretty().unwrap().trim().to_string(); + let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string(); + assert_eq!(expected, output); // remappings work let remappings_txt = @@ -236,9 +236,16 @@ forgetest_init!(can_override_config, |prj, cmd| { std::env::remove_var("DAPP_REMAPPINGS"); pretty_err(&remappings_txt, fs::remove_file(&remappings_txt)); - cmd.set_cmd(prj.forge_bin()).args(["config", "--basic"]); - let expected = profile.into_basic().to_string_pretty().unwrap(); - assert_eq!(expected.trim().to_string(), cmd.stdout_lossy().trim().to_string()); + let expected = profile.into_basic().to_string_pretty().unwrap().trim().to_string(); + let output = cmd + .forge_fuse() + .args(["config", "--basic"]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); + assert_eq!(expected, output); }); forgetest_init!(can_parse_remappings_correctly, |prj, cmd| { @@ -255,13 +262,18 @@ forgetest_init!(can_parse_remappings_correctly, |prj, cmd| { // the loaded config has resolved, absolute paths assert_eq!("forge-std/=lib/forge-std/src/", Remapping::from(r.clone()).to_string()); - cmd.arg("config"); - let expected = profile.to_string_pretty().unwrap(); - assert_eq!(expected.trim().to_string(), cmd.stdout_lossy().trim().to_string()); + let expected = profile.to_string_pretty().unwrap().trim().to_string(); + let output = cmd.arg("config").assert_success().get_output().stdout_lossy().trim().to_string(); + assert_eq!(expected, output); let install = |cmd: &mut TestCommand, dep: &str| { - cmd.forge_fuse().args(["install", dep, "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse().args(["install", dep, "--no-commit"]).assert_success().stdout_eq(str![[ + r#" +Installing solmate in [..] (url: Some("https://github.com/transmissions11/solmate"), tag: None) + Installed solmate + +"# + ]]); }; install(&mut cmd, "transmissions11/solmate"); @@ -288,9 +300,16 @@ forgetest_init!(can_parse_remappings_correctly, |prj, cmd| { ); pretty_err(&remappings_txt, fs::remove_file(&remappings_txt)); - cmd.set_cmd(prj.forge_bin()).args(["config", "--basic"]); - let expected = profile.into_basic().to_string_pretty().unwrap(); - assert_eq!(expected.trim().to_string(), cmd.stdout_lossy().trim().to_string()); + let expected = profile.into_basic().to_string_pretty().unwrap().trim().to_string(); + let output = cmd + .forge_fuse() + .args(["config", "--basic"]) + .assert_success() + .get_output() + .stdout_lossy() + .trim() + .to_string(); + assert_eq!(expected, output); }); forgetest_init!(can_detect_config_vals, |prj, _cmd| { @@ -348,9 +367,12 @@ contract Greeter {} let config = Config { solc: Some(OTHER_SOLC_VERSION.into()), ..Default::default() }; prj.write_config(config); - cmd.arg("build"); + cmd.arg("build").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! - assert!(cmd.stdout_lossy().contains("Compiler run successful!")); +"#]]); }); // tests that `--use ` works @@ -364,25 +386,45 @@ contract Foo {} ) .unwrap(); - cmd.args(["build", "--use", OTHER_SOLC_VERSION]); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("Compiler run successful")); + cmd.args(["build", "--use", OTHER_SOLC_VERSION]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); cmd.forge_fuse() .args(["build", "--force", "--use", &format!("solc:{OTHER_SOLC_VERSION}")]) - .root_arg(); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("Compiler run successful")); + .root_arg() + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); // fails to use solc that does not exist cmd.forge_fuse().args(["build", "--use", "this/solc/does/not/exist"]); - assert!(cmd.stderr_lossy().contains("`solc` this/solc/does/not/exist does not exist")); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +`solc` this/solc/does/not/exist does not exist + +"#]]); // `OTHER_SOLC_VERSION` was installed in previous step, so we can use the path to this directly let local_solc = Solc::find_or_install(&OTHER_SOLC_VERSION.parse().unwrap()).unwrap(); - cmd.forge_fuse().args(["build", "--force", "--use"]).arg(local_solc.solc).root_arg(); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("Compiler run successful")); + cmd.forge_fuse() + .args(["build", "--force", "--use"]) + .arg(local_solc.solc) + .root_arg() + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); }); // test to ensure yul optimizer can be set as intended @@ -571,8 +613,13 @@ forgetest!(can_update_libs_section, |prj, cmd| { let init = Config { libs: vec!["node_modules".into()], ..Default::default() }; prj.write_config(init); - cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]).assert_success().stdout_eq(str![ + [r#" +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + +"#] + ]); let config = cmd.forge_fuse().config(); // `lib` was added automatically @@ -580,8 +627,14 @@ forgetest!(can_update_libs_section, |prj, cmd| { assert_eq!(config.libs, expected); // additional install don't edit `libs` - cmd.forge_fuse().args(["install", "dapphub/ds-test", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.forge_fuse() + .args(["install", "dapphub/ds-test", "--no-commit"]) + .assert_success() + .stdout_eq(str![[r#" +Installing ds-test in [..] (url: Some("https://github.com/dapphub/ds-test"), tag: None) + Installed ds-test + +"#]]); let config = cmd.forge_fuse().config(); assert_eq!(config.libs, expected); @@ -592,8 +645,13 @@ forgetest!(can_update_libs_section, |prj, cmd| { forgetest!(config_emit_warnings, |prj, cmd| { cmd.git_init(); - cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]); - cmd.assert_non_empty_stdout(); + cmd.args(["install", "foundry-rs/forge-std", "--no-commit"]).assert_success().stdout_eq(str![ + [r#" +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + +"#] + ]); let faulty_toml = r"[default] src = 'src' @@ -603,16 +661,12 @@ forgetest!(config_emit_warnings, |prj, cmd| { fs::write(prj.root().join("foundry.toml"), faulty_toml).unwrap(); fs::write(prj.root().join("lib").join("forge-std").join("foundry.toml"), faulty_toml).unwrap(); - cmd.forge_fuse().args(["config"]); - let output = cmd.execute(); - assert!(output.status.success()); - assert_eq!( - String::from_utf8_lossy(&output.stderr) - .lines() - .filter(|line| line.contains("unknown config section") && line.contains("[default]")) - .count(), - 1, - ); + cmd.forge_fuse().args(["config"]).assert_success().stderr_eq(str![[r#" +warning: Found unknown config section in foundry.toml: [default] +This notation for profiles has been deprecated and may result in the profile not being registered in future versions. +Please use [profile.default] instead or run `forge config --fix`. + +"#]]); }); forgetest_init!(can_skip_remappings_auto_detection, |prj, cmd| { @@ -705,8 +759,11 @@ forgetest_init!(can_resolve_symlink_fs_permissions, |prj, cmd| { // tests if evm version is normalized for config output forgetest!(normalize_config_evm_version, |_prj, cmd| { - cmd.args(["config", "--use", "0.8.0", "--json"]); - let output = cmd.stdout_lossy(); + let output = cmd + .args(["config", "--use", "0.8.0", "--json"]) + .assert_success() + .get_output() + .stdout_lossy(); let config: Config = serde_json::from_str(&output).unwrap(); assert_eq!(config.evm_version, EvmVersion::Istanbul); }); diff --git a/crates/forge/tests/cli/create.rs b/crates/forge/tests/cli/create.rs index c7071dcf586fa..ebf8c81dbcc1e 100644 --- a/crates/forge/tests/cli/create.rs +++ b/crates/forge/tests/cli/create.rs @@ -10,7 +10,7 @@ use foundry_compilers::artifacts::{remappings::Remapping, BytecodeHash}; use foundry_config::Config; use foundry_test_utils::{ forgetest, forgetest_async, str, - util::{TestCommand, TestProject}, + util::{OutputExt, TestCommand, TestProject}, }; use std::str::FromStr; @@ -104,12 +104,16 @@ where { if let Some(info) = info { let contract_path = f(&prj); - cmd.arg("create"); - cmd.args(info.create_args()).arg(contract_path); - let out = cmd.stdout_lossy(); - let _address = utils::parse_deployed_address(out.as_str()) - .unwrap_or_else(|| panic!("Failed to parse deployer {out}")); + let output = cmd + .arg("create") + .args(info.create_args()) + .arg(contract_path) + .assert_success() + .get_output() + .stdout_lossy(); + let _address = utils::parse_deployed_address(output.as_str()) + .unwrap_or_else(|| panic!("Failed to parse deployer {output}")); } } @@ -151,7 +155,7 @@ forgetest_async!(can_create_template_contract, |prj, cmd| { ]); cmd.assert().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 @@ -192,7 +196,7 @@ forgetest_async!(can_create_using_unlocked, |prj, cmd| { ]); cmd.assert().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 @@ -249,7 +253,7 @@ contract ConstructorContract { ]) .assert_success() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 @@ -286,7 +290,7 @@ contract TupleArrayConstructorContract { ]) .assert() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 @@ -323,15 +327,29 @@ contract UniswapV2Swap { ) .unwrap(); - cmd.forge_fuse().args([ - "create", - "./src/UniswapV2Swap.sol:UniswapV2Swap", - "--rpc-url", - rpc.as_str(), - "--private-key", - pk.as_str(), - ]); + cmd.forge_fuse() + .args([ + "create", + "./src/UniswapV2Swap.sol:UniswapV2Swap", + "--rpc-url", + rpc.as_str(), + "--private-key", + pk.as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (2018): Function state mutability can be restricted to pure + [FILE]:6:5: + | +6 | function pairInfo() public view returns (uint reserveA, uint reserveB, uint totalSupply) { + | ^ (Relevant source part starts here and spans across multiple lines). - let (stdout, _) = cmd.output_lossy(); - assert!(stdout.contains("Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3")); +Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 +Deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 +[TX_HASH] + +"#]]); }); diff --git a/crates/forge/tests/cli/debug.rs b/crates/forge/tests/cli/debug.rs index 3e3d08c7e2ad7..8ee1eb3f27753 100644 --- a/crates/forge/tests/cli/debug.rs +++ b/crates/forge/tests/cli/debug.rs @@ -7,8 +7,14 @@ forgetest_async!( #[ignore = "ran manually"] manual_debug_setup, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()).assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); prj.add_source("Counter2.sol", r#" contract A { @@ -74,8 +80,7 @@ contract Script0 is Script, Test { ) .unwrap(); - cmd.args(["build"]).assert_success(); - cmd.forge_fuse(); + cmd.forge_fuse().args(["build"]).assert_success(); cmd.args([ "script", diff --git a/crates/forge/tests/cli/script.rs b/crates/forge/tests/cli/script.rs index 2b92559f2da91..6ef99d9d11bde 100644 --- a/crates/forge/tests/cli/script.rs +++ b/crates/forge/tests/cli/script.rs @@ -54,7 +54,7 @@ contract Demo { .unwrap(); cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Script ran successfully. @@ -113,7 +113,7 @@ contract Demo { cmd.arg("script").arg(script).arg("--sig").arg("myFunction()").assert_success().stdout_eq( str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Script ran successfully. @@ -145,10 +145,11 @@ forgetest_async!(assert_exit_code_error_on_failure_script, |prj, cmd| { cmd.arg("script").arg(script); // run command and assert error exit code - cmd.assert_err(); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +script failed: revert: failed - let output = cmd.stderr_lossy(); - assert!(output.contains("script failed: revert: failed")); +"#]]); }); // Tests that execution throws upon encountering a revert in the script with --json option. @@ -161,10 +162,11 @@ forgetest_async!(assert_exit_code_error_on_failure_script_with_json, |prj, cmd| cmd.arg("script").arg(script).arg("--json"); // run command and assert error exit code - cmd.assert_err(); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +script failed: revert: failed - let output = cmd.stderr_lossy(); - assert!(output.contains("script failed: revert: failed")); +"#]]); }); // Tests that the manually specified gas limit is used when using the --unlocked option @@ -182,7 +184,7 @@ contract GasWaster { } } contract DeployScript is Script { - function run() external returns (uint256 result, uint8) { + function run() external { vm.startBroadcast(); GasWaster gasWaster = new GasWaster(); gasWaster.wasteGas{gas: 500000}(200000); @@ -213,11 +215,66 @@ contract DeployScript is Script { "--slow", "--broadcast", "--unlocked", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (2018): Function state mutability can be restricted to view + [FILE]:7:5: + | +7 | function wasteGas(uint256 minGas) public { + | ^ (Relevant source part starts here and spans across multiple lines). + +Traces: + [81040] DeployScript::run() + ├─ [0] VM::startBroadcast() + │ └─ ← [Return] + ├─ [45299] → new GasWaster@[..] + │ └─ ← [Return] 226 bytes of code + ├─ [226] GasWaster::wasteGas(200000 [2e5]) + │ └─ ← [Stop] + └─ ← [Stop] + + +Script ran successfully. + +## Setting up 1 EVM. +========================== +Simulated On-chain Traces: + +Gas limit was set in script to 500000 + [45299] → new GasWaster@[..] + └─ ← [Return] 226 bytes of code + + [226] GasWaster::wasteGas(200000 [2e5]) + └─ ← [Stop] + + +========================== + +Chain 1 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] - let output = cmd.stdout_lossy(); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); - assert!(output.contains("Gas limit was set in script to 500000")); +========================== + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); }); // Tests that the manually specified gas limit is used. @@ -235,7 +292,7 @@ contract GasWaster { } } contract DeployScript is Script { - function run() external returns (uint256 result, uint8) { + function run() external { vm.startBroadcast(); GasWaster gasWaster = new GasWaster(); gasWaster.wasteGas{gas: 500000}(200000); @@ -266,11 +323,66 @@ contract DeployScript is Script { "--broadcast", "--private-key", &private_key, - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (2018): Function state mutability can be restricted to view + [FILE]:7:5: + | +7 | function wasteGas(uint256 minGas) public { + | ^ (Relevant source part starts here and spans across multiple lines). + +Traces: + [81040] DeployScript::run() + ├─ [0] VM::startBroadcast() + │ └─ ← [Return] + ├─ [45299] → new GasWaster@[..] + │ └─ ← [Return] 226 bytes of code + ├─ [226] GasWaster::wasteGas(200000 [2e5]) + │ └─ ← [Stop] + └─ ← [Stop] + + +Script ran successfully. + +## Setting up 1 EVM. +========================== +Simulated On-chain Traces: + +Gas limit was set in script to 500000 + [45299] → new GasWaster@[..] + └─ ← [Return] 226 bytes of code + + [226] GasWaster::wasteGas(200000 [2e5]) + └─ ← [Stop] - let output = cmd.stdout_lossy(); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); - assert!(output.contains("Gas limit was set in script to 500000")); + +========================== + +Chain 1 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); }); // Tests that the run command can run functions with arguments @@ -300,7 +412,7 @@ contract Demo { .arg("2") .assert_success() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Script ran successfully. @@ -331,7 +443,7 @@ contract Demo { .unwrap(); cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Script ran successfully. @@ -358,20 +470,24 @@ import "forge-std/Script.sol"; contract HashChecker { bytes32 public lastHash; + function update() public { bytes32 newHash = blockhash(block.number - 1); require(newHash != lastHash, "Hash didn't change"); lastHash = newHash; } - function checkLastHash() public { + function checkLastHash() public view { require(lastHash != bytes32(0), "Hash shouldn't be zero"); } } + contract DeployScript is Script { - function run() external returns (uint256 result, uint8) { + HashChecker public hashChecker; + + function run() external { vm.startBroadcast(); - HashChecker hashChecker = new HashChecker(); + hashChecker = new HashChecker(); } }"#, ) @@ -399,12 +515,36 @@ contract DeployScript is Script { "--skip-simulation", "--private-key", &private_key, - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Traces: + [116040] DeployScript::run() + ├─ [0] VM::startBroadcast() + │ └─ ← [Return] + ├─ [75723] → new HashChecker@[..] + │ └─ ← [Return] 378 bytes of code + └─ ← [Stop] + + +Script ran successfully. + +SKIPPING ON CHAIN SIMULATION. + - let output = cmd.stdout_lossy(); +========================== - assert!(output.contains("SKIPPING ON CHAIN SIMULATION")); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); let run_log = std::fs::read_to_string("broadcast/DeployScript.sol/1/run-latest.json").unwrap(); let run_object: Value = serde_json::from_str(&run_log).unwrap(); @@ -420,9 +560,11 @@ import "forge-std/Script.sol"; import { HashChecker } from "./DeployScript.sol"; contract RunScript is Script { - function run() external returns (uint256 result, uint8) { + HashChecker public hashChecker; + + function run() external { vm.startBroadcast(); - HashChecker hashChecker = HashChecker(CONTRACT_ADDRESS); + hashChecker = HashChecker(CONTRACT_ADDRESS); uint numUpdates = 8; vm.roll(block.number - numUpdates); for(uint i = 0; i < numUpdates; i++) { @@ -437,28 +579,100 @@ contract RunScript is Script { let run_script = prj.add_source("RunScript", &run_code).unwrap(); let run_contract = run_script.display().to_string() + ":RunScript"; - cmd.forge_fuse(); - cmd.set_current_dir(prj.root()); - cmd.args([ - "script", - &run_contract, - "--root", - prj.root().to_str().unwrap(), - "--fork-url", - &handle.http_endpoint(), - "-vvvvv", - "--broadcast", - "--slow", - "--skip-simulation", - "--gas-estimate-multiplier", - "200", - "--private-key", - &private_key, - ]); + cmd.forge_fuse() + .args([ + "script", + &run_contract, + "--root", + prj.root().to_str().unwrap(), + "--fork-url", + &handle.http_endpoint(), + "-vvvvv", + "--broadcast", + "--slow", + "--skip-simulation", + "--gas-estimate-multiplier", + "200", + "--private-key", + &private_key, + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Traces: + [51327] RunScript::run() + ├─ [0] VM::startBroadcast() + │ └─ ← [Return] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [22394] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + ├─ [0] VM::roll([..]) + │ └─ ← [Return] + ├─ [494] [..]::update() + │ └─ ← [Stop] + ├─ [239] [..]::checkLastHash() [staticcall] + │ └─ ← [Stop] + └─ ← [Stop] - let output = cmd.stdout_lossy(); - assert!(output.contains("SKIPPING ON CHAIN SIMULATION")); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); + +Script ran successfully. + +SKIPPING ON CHAIN SIMULATION. + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); }); forgetest_async!(can_deploy_script_without_lib, |prj, cmd| { @@ -801,12 +1015,17 @@ struct Transaction { // test we output arguments forgetest_async!(can_execute_script_with_arguments, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let (_api, handle) = spawn(NodeConfig::test()).await; - let script = prj .add_script( + let script = prj.add_script( "Counter.s.sol", r#" import "forge-std/Script.sol"; @@ -822,6 +1041,9 @@ contract A { int c; bytes32 d; bool e; + bytes f; + Point g; + string h; constructor(address _a, uint _b, int _c, bytes32 _d, bool _e, bytes memory _f, Point memory _g, string memory _h) { a = _a; @@ -829,6 +1051,9 @@ contract A { c = _c; d = _d; e = _e; + f = _f; + g = _g; + h = _h; } } @@ -843,16 +1068,48 @@ contract Script0 is Script { ) .unwrap(); - cmd.arg("script").arg(script).args([ - "--tc", - "Script0", - "--sender", - "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", - "--rpc-url", - handle.http_endpoint().as_str(), - ]); + cmd + .forge_fuse() + .arg("script") + .arg(script) + .args([ + "--tc", + "Script0", + "--sender", + "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + "--rpc-url", + handle.http_endpoint().as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +... +Script ran successfully. + +## Setting up 1 EVM. + +========================== - assert!(cmd.stdout_lossy().contains("SIMULATION COMPLETE")); +Chain 31337 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + +SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); let run_latest = foundry_common::fs::json_files(&prj.root().join("broadcast")) .find(|path| path.ends_with("run-latest.json")) @@ -880,9 +1137,14 @@ contract Script0 is Script { // test we output arguments forgetest_async!(can_execute_script_with_arguments_nested_deploy, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let (_api, handle) = spawn(NodeConfig::test()).await; let script = prj @@ -927,16 +1189,48 @@ contract Script0 is Script { ) .unwrap(); - cmd.arg("script").arg(script).args([ - "--tc", - "Script0", - "--sender", - "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", - "--rpc-url", - handle.http_endpoint().as_str(), - ]); + cmd + .forge_fuse() + .arg("script") + .arg(script) + .args([ + "--tc", + "Script0", + "--sender", + "0x00a329c0648769A73afAc7F9381E08FB43dBEA72", + "--rpc-url", + handle.http_endpoint().as_str(), + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +... +Script ran successfully. + +## Setting up 1 EVM. + +========================== + +Chain 31337 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] - assert!(cmd.stdout_lossy().contains("SIMULATION COMPLETE")); +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + +SIMULATION COMPLETE. To broadcast these transactions, add --broadcast and wallet configuration(s) to the previous command. See forge script --help for more. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); let run_latest = foundry_common::fs::json_files(&prj.root().join("broadcast")) .find(|file| file.ends_with("run-latest.json")) @@ -981,7 +1275,7 @@ contract Demo { .args(["--skip", "tests", "--skip", TEMPLATE_CONTRACT]) .assert_success() .stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! Script ran successfully. @@ -1010,9 +1304,14 @@ forgetest_async!(does_script_override_correctly, |prj, cmd| { }); forgetest_async!(assert_tx_origin_is_not_overritten, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let script = prj .add_script( @@ -1068,14 +1367,32 @@ contract ContractC { ) .unwrap(); - cmd.arg("script").arg(script).args(["--tc", "ScriptTxOrigin"]); - assert!(cmd.stdout_lossy().contains("Script ran successfully.")); + cmd.forge_fuse() + .arg("script") + .arg(script) + .args(["--tc", "ScriptTxOrigin"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +If you wish to simulate on-chain transactions pass a RPC URL. + +"#]]); }); forgetest_async!(assert_can_create_multiple_contracts_with_correct_nonce, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let script = prj .add_script( @@ -1088,18 +1405,20 @@ contract Contract { console.log(tx.origin); } } + contract SubContract { constructor() { console.log(tx.origin); } } + contract BadContract { constructor() { - // new SubContract(); + new SubContract(); console.log(tx.origin); } } -contract NestedCreateFail is Script { +contract NestedCreate is Script { function run() public { address sender = address(uint160(uint(keccak256("woops")))); @@ -1114,8 +1433,26 @@ contract NestedCreateFail is Script { ) .unwrap(); - cmd.arg("script").arg(script).args(["--tc", "NestedCreateFail"]); - assert!(cmd.stdout_lossy().contains("Script ran successfully.")); + cmd.forge_fuse() + .arg("script") + .arg(script) + .args(["--tc", "NestedCreate"]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +== Logs == + 0x159E2f2F1C094625A2c6c8bF59526d91454c2F3c + 0x159E2f2F1C094625A2c6c8bF59526d91454c2F3c + 0x159E2f2F1C094625A2c6c8bF59526d91454c2F3c + +If you wish to simulate on-chain transactions pass a RPC URL. + +"#]]); }); forgetest_async!(assert_can_detect_target_contract_with_interfaces, |prj, cmd| { @@ -1132,8 +1469,14 @@ interface Interface {} ) .unwrap(); - cmd.arg("script").arg(script); - assert!(cmd.stdout_lossy().contains("Script ran successfully.")); + cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +"#]]); }); forgetest_async!(assert_can_detect_unlinked_target_with_libraries, |prj, cmd| { @@ -1154,8 +1497,16 @@ contract Script { ) .unwrap(); - cmd.arg("script").arg(script); - assert!(cmd.stdout_lossy().contains("Script ran successfully.")); + cmd.arg("script").arg(script).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +Script ran successfully. +[GAS] + +If you wish to simulate on-chain transactions pass a RPC URL. + +"#]]); }); forgetest_async!(assert_can_resume_with_additional_contracts, |prj, cmd| { @@ -1239,7 +1590,7 @@ forgetest_async!(can_sign_with_script_wallet_multiple, |prj, cmd| { forgetest_async!(fails_with_function_name_and_overloads, |prj, cmd| { let script = prj .add_script( - "Sctipt.s.sol", + "Script.s.sol", r#" contract Script { function run() external {} @@ -1251,13 +1602,22 @@ contract Script { .unwrap(); cmd.arg("script").args([&script.to_string_lossy(), "--sig", "run"]); - assert!(cmd.stderr_lossy().contains("Multiple functions with the same name")); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +Multiple functions with the same name `run` found in the ABI + +"#]]); }); forgetest_async!(can_decode_custom_errors, |prj, cmd| { - cmd.args(["init", "--force"]).arg(prj.root()); - cmd.assert_non_empty_stdout(); - cmd.forge_fuse(); + cmd.args(["init", "--force"]).arg(prj.root()).assert_success().stdout_eq(str![[r#" +Target directory is not empty, but `--force` was specified +Initializing [..]... +Installing forge-std in [..] (url: Some("https://github.com/foundry-rs/forge-std"), tag: None) + Installed forge-std [..] + Initialized forge project + +"#]]); let script = prj .add_script( @@ -1284,8 +1644,12 @@ contract CustomErrorScript is Script { ) .unwrap(); - cmd.arg("script").arg(script).args(["--tc", "CustomErrorScript"]); - assert!(cmd.stderr_lossy().contains("script failed: CustomError()")); + cmd.forge_fuse().arg("script").arg(script).args(["--tc", "CustomErrorScript"]); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +script failed: CustomError() + +"#]]); }); // https://github.com/foundry-rs/foundry/issues/7620 @@ -1297,9 +1661,9 @@ forgetest_async!(can_run_zero_base_fee, |prj, cmd| { import "forge-std/Script.sol"; contract SimpleScript is Script { - function run() external { + function run() external returns (bool success) { vm.startBroadcast(); - address(0).call(""); + (success, ) = address(0).call(""); } } "#, @@ -1325,25 +1689,90 @@ contract SimpleScript is Script { "2000000", "--priority-gas-price", "100000", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +... +Script ran successfully. + +== Return == +success: bool true + +## Setting up 1 EVM. + +========================== + +Chain 31337 - let output = cmd.stdout_lossy(); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); // Ensure that we can correctly estimate gas when base fee is zero but priority fee is not. - cmd.forge_fuse().args([ - "script", - "SimpleScript", - "--fork-url", - &handle.http_endpoint(), - "--sender", - format!("{dev:?}").as_str(), - "--broadcast", - "--unlocked", - ]); + cmd.forge_fuse() + .args([ + "script", + "SimpleScript", + "--fork-url", + &handle.http_endpoint(), + "--sender", + format!("{dev:?}").as_str(), + "--broadcast", + "--unlocked", + ]) + .assert_success() + .stdout_eq(str![[r#" +No files changed, compilation skipped +... +Script ran successfully. + +== Return == +success: bool true + +## Setting up 1 EVM. + +========================== + +Chain 31337 + +[ESTIMATED_GAS_PRICE] + +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + + +========================== - let output = cmd.stdout_lossy(); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); }); // https://github.com/foundry-rs/foundry/pull/7742 @@ -1355,9 +1784,9 @@ forgetest_async!(unlocked_no_sender, |prj, cmd| { import "forge-std/Script.sol"; contract SimpleScript is Script { - function run() external { + function run() external returns (bool success) { vm.startBroadcast(0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266); - address(0).call(""); + (success, ) = address(0).call(""); } } "#, @@ -1373,10 +1802,43 @@ contract SimpleScript is Script { &handle.http_endpoint(), "--broadcast", "--unlocked", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +... +Script ran successfully. + +== Return == +success: bool true + +## Setting up 1 EVM. + +========================== + +Chain 31337 + +[ESTIMATED_GAS_PRICE] - let output = cmd.stdout_lossy(); - assert!(output.contains("ONCHAIN EXECUTION COMPLETE & SUCCESSFUL")); +[ESTIMATED_TOTAL_GAS_USED] + +[ESTIMATED_AMOUNT_REQUIRED] + +========================== + + +========================== + +ONCHAIN EXECUTION COMPLETE & SUCCESSFUL. + +[SAVED_TRANSACTIONS] + +[SAVED_SENSITIVE_VALUES] + + +"#]]); }); // https://github.com/foundry-rs/foundry/issues/7833 @@ -1411,8 +1873,11 @@ contract SimpleScript is Script { "--unlocked", ]); - let output = cmd.stderr_lossy(); - assert!(output.contains("missing CREATE2 deployer")); + cmd.assert_failure().stderr_eq(str![[r#" +Error: +script failed: missing CREATE2 deployer + +"#]]); }); forgetest_async!(can_switch_forks_in_setup, |prj, cmd| { @@ -1450,9 +1915,21 @@ contract SimpleScript is Script { &url, "--sender", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", - ]); + ]) + .assert_success() + .stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful with warnings: +Warning (2018): Function state mutability can be restricted to view + [FILE]:13:5: + | +13 | function run() external { + | ^ (Relevant source part starts here and spans across multiple lines). + +Script ran successfully. - cmd.stdout_lossy(); +"#]]); }); // Asserts that running the same script twice only deploys library once. diff --git a/crates/forge/tests/cli/soldeer.rs b/crates/forge/tests/cli/soldeer.rs index bacf47230ba69..21666edbb493b 100644 --- a/crates/forge/tests/cli/soldeer.rs +++ b/crates/forge/tests/cli/soldeer.rs @@ -7,14 +7,17 @@ use std::{ use foundry_test_utils::forgesoldeer; use std::io::Write; + forgesoldeer!(install_dependency, |prj, cmd| { let command = "install"; let dependency = "forge-std~1.8.1"; let foundry_file = prj.root().join("foundry.toml"); - cmd.arg("soldeer").args([command, dependency]); - cmd.execute(); + cmd.arg("soldeer").args([command, dependency]).assert_success().stdout_eq(str![[r#" +🦌 Running [..]oldeer install 🦌 +... +"#]]); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -53,8 +56,10 @@ forgesoldeer!(install_dependency_git, |prj, cmd| { let foundry_file = prj.root().join("foundry.toml"); - cmd.arg("soldeer").args([command, dependency, git]); - cmd.execute(); + cmd.arg("soldeer").args([command, dependency, git]).assert_success().stdout_eq(str![[r#" +🦌 Running [..]oldeer install 🦌 +... +"#]]); // Making sure the path was created to the dependency and that README.md exists // meaning that the dependencies were installed correctly @@ -93,8 +98,13 @@ forgesoldeer!(install_dependency_git_commit, |prj, cmd| { let foundry_file = prj.root().join("foundry.toml"); - cmd.arg("soldeer").args([command, dependency, git, rev_flag, commit]); - cmd.execute(); + cmd.arg("soldeer") + .args([command, dependency, git, rev_flag, commit]) + .assert_success() + .stdout_eq(str![[r#" +🦌 Running [..]oldeer install 🦌 +... +"#]]); // Making sure the path was created to the dependency and that README.md exists // meaning that the dependencies were installed correctly @@ -147,8 +157,11 @@ mario-custom-branch = { version = "1.0", git = "https://gitlab.com/mario4582928/ eprintln!("Couldn't write to file: {e}"); } - cmd.arg("soldeer").arg(command); - cmd.execute(); + cmd.arg("soldeer").arg(command).assert_success().stdout_eq(str![[r#" +🦌 Running [..]oldeer update 🦌 +... + +"#]]); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -216,8 +229,11 @@ forge-std = "1.8.1" eprintln!("Couldn't write to file: {e}"); } - cmd.arg("soldeer").arg(command); - cmd.execute(); + cmd.arg("soldeer").arg(command).assert_success().stdout_eq(str![[r#" +🦌 Running [..]oldeer update 🦌 +... + +"#]]); // Making sure the path was created to the dependency and that foundry.toml exists // meaning that the dependencies were installed correctly @@ -252,12 +268,20 @@ forge-std = "1.8.1" forgesoldeer!(login, |prj, cmd| { let command = "login"; - cmd.arg("soldeer").arg(command); - let output = cmd.unchecked_output(); - - // On login, we can only check if the prompt is displayed in the stdout - let stdout = String::from_utf8(output.stdout).expect("Could not parse the output"); - assert!(stdout.contains("Please enter your email")); + cmd.arg("soldeer") + .arg(command) + .assert_failure() + .stderr_eq(str![[r#" +Error: +Failed to run [..] + +"#]]) + .stdout_eq(str![[r#" +🦌 Running [..]oldeer login 🦌 +... +ℹ️ If you do not have an account, please go to soldeer.xyz to create one. +📧 Please enter your email: +"#]]); }); forgesoldeer!(install_dependency_with_remappings_config, |prj, cmd| { diff --git a/crates/forge/tests/cli/svm.rs b/crates/forge/tests/cli/svm.rs index 8403053d84d6a..ab8db41f8a8b0 100644 --- a/crates/forge/tests/cli/svm.rs +++ b/crates/forge/tests/cli/svm.rs @@ -57,5 +57,18 @@ contract CounterTest is Test {{ "# ); prj.add_test("Counter", &src).unwrap(); - cmd.arg("test").stdout_lossy().contains("[PASS]"); + cmd.arg("test").assert_success().stdout_eq(str![[r#" +... +Ran 1 test for test/Counter.sol:CounterTest +[PASS] testAssert() ([GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] +... +Ran 2 tests for test/Counter.t.sol:CounterTest +[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS]) +[PASS] test_Increment() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 2 test suites [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) + +"#]]); }); diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index c2cf46f8fac62..1804a46138a48 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -4,7 +4,7 @@ use alloy_primitives::U256; use foundry_config::{Config, FuzzConfig}; use foundry_test_utils::{ rpc, str, - util::{OTHER_SOLC_VERSION, SOLC_VERSION}, + util::{OutputExt, OTHER_SOLC_VERSION, SOLC_VERSION}, }; use similar_asserts::assert_eq; use std::{path::PathBuf, str::FromStr}; @@ -128,7 +128,7 @@ forgetest!(can_fuzz_array_params, |prj, cmd| { r#" import "./test.sol"; contract ATest is DSTest { - function testArray(uint64[2] calldata values) external { + function testArray(uint64[2] calldata) external { assertTrue(true); } } @@ -136,8 +136,18 @@ contract ATest is DSTest { ) .unwrap(); - cmd.arg("test"); - cmd.stdout_lossy().contains("[PASS]"); + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for src/ATest.t.sol:ATest +[PASS] testArray(uint64[2]) (runs: 256, [AVG_GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that `bytecode_hash` will be sanitized @@ -151,7 +161,7 @@ forgetest!(can_test_pre_bytecode_hash, |prj, cmd| { pragma solidity 0.5.17; import "./test.sol"; contract ATest is DSTest { - function testArray(uint64[2] calldata values) external { + function testArray(uint64[2] calldata) external { assertTrue(true); } } @@ -159,8 +169,18 @@ contract ATest is DSTest { ) .unwrap(); - cmd.arg("test"); - cmd.stdout_lossy().contains("[PASS]"); + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for src/ATest.t.sol:ATest +[PASS] testArray(uint64[2]) (runs: 256, [AVG_GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that using the --match-path option only runs files matching the path @@ -194,7 +214,7 @@ contract FailTest is DSTest { .unwrap(); cmd.args(["test", "--match-path", "*src/ATest.t.sol"]).assert_success().stdout_eq(str![[r#" -Compiling 3 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -241,7 +261,7 @@ contract FailTest is DSTest { let test_path = test_path.to_string_lossy(); cmd.args(["test", "--match-path", test_path.as_ref()]).assert_success().stdout_eq(str![[r#" -Compiling 3 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -278,7 +298,7 @@ contract MyTest is DSTest { .unwrap(); cmd.arg("test").assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -293,9 +313,22 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) // checks that forge test repeatedly produces the same output #[cfg(not(feature = "isolate-by-default"))] -forgetest_init!(can_test_repeatedly, |_prj, cmd| { - cmd.arg("test"); - cmd.assert_non_empty_stdout(); +forgetest_init!(can_test_repeatedly, |prj, cmd| { + prj.clear(); + + cmd.arg("test").assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for test/Counter.t.sol:CounterTest +[PASS] testFuzz_SetNumber(uint256) (runs: 256, [AVG_GAS]) +[PASS] test_Increment() ([GAS]) +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); for _ in 0..5 { cmd.assert_success().stdout_eq(str![[r#" @@ -339,7 +372,7 @@ contract ContractTest is DSTest { prj.write_config(config); cmd.arg("test").assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -356,7 +389,7 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) prj.write_config(config); cmd.forge_fuse().arg("test").assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -417,7 +450,7 @@ contract ContractTest is Test { .unwrap(); cmd.arg("test").assert_success().stdout_eq(str![[r#" -Compiling 2 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -448,7 +481,7 @@ forgetest_init!(exit_code_error_on_fail_fast, |prj, cmd| { cmd.args(["test", "--fail-fast"]); // run command and assert error exit code - cmd.assert_err(); + cmd.assert_empty_stderr(); }); forgetest_init!(exit_code_error_on_fail_fast_with_json, |prj, cmd| { @@ -459,7 +492,7 @@ forgetest_init!(exit_code_error_on_fail_fast_with_json, |prj, cmd| { cmd.args(["test", "--fail-fast", "--json"]); // run command and assert error exit code - cmd.assert_err(); + cmd.assert_empty_stderr(); }); // https://github.com/foundry-rs/foundry/pull/6531 @@ -489,7 +522,7 @@ contract USDTCallingTest is Test { .unwrap(); cmd.args(["test", "-vvvv"]).assert_success().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -535,7 +568,7 @@ contract CustomTypesTest is Test { .unwrap(); cmd.args(["test", "-vvvv"]).assert_failure().stdout_eq(str![[r#" -Compiling 1 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -738,7 +771,7 @@ contract CounterTest is Test { // make sure there are only 61 runs (with proptest shrinking same test results in 298 runs) cmd.args(["test"]).assert_failure().stdout_eq(str![[r#" -Compiling 23 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -789,7 +822,7 @@ contract CounterTest is Test { // make sure invariant test exit early with 0 runs cmd.args(["test"]).assert_failure().stdout_eq(str![[r#" -Compiling 23 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -836,8 +869,29 @@ contract ReplayFailuresTest is Test { ) .unwrap(); - cmd.args(["test"]); - cmd.assert_err(); + cmd.args(["test"]).assert_failure().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 4 tests for test/ReplayFailures.t.sol:ReplayFailuresTest +[PASS] testA() ([GAS]) +[FAIL. Reason: revert: testB failed] testB() ([GAS]) +[PASS] testC() ([GAS]) +[FAIL. Reason: revert: testD failed] testD() ([GAS]) +Suite result: FAILED. 2 passed; 2 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 2 failed, 0 skipped (4 total tests) + +Failing tests: +Encountered 2 failing tests in test/ReplayFailures.t.sol:ReplayFailuresTest +[FAIL. Reason: revert: testB failed] testB() ([GAS]) +[FAIL. Reason: revert: testD failed] testD() ([GAS]) + +Encountered a total of 2 failing tests, 2 tests succeeded + +"#]]); + // Test failure filter should be persisted. assert!(prj.root().join("cache/test-failures").exists()); @@ -893,22 +947,52 @@ contract PrecompileLabelsTest is Test { ) .unwrap(); - let output = cmd.args(["test", "-vvvv"]).stdout_lossy(); - assert!(output.contains("VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D]")); - assert!(output.contains("console: [0x000000000000000000636F6e736F6c652e6c6f67]")); - assert!(output.contains("Create2Deployer: [0x4e59b44847b379578588920cA78FbF26c0B4956C]")); - assert!(output.contains("DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38]")); - assert!(output.contains("DefaultTestContract: [0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84]")); - assert!(output.contains("ECRecover: [0x0000000000000000000000000000000000000001]")); - assert!(output.contains("SHA-256: [0x0000000000000000000000000000000000000002]")); - assert!(output.contains("RIPEMD-160: [0x0000000000000000000000000000000000000003]")); - assert!(output.contains("Identity: [0x0000000000000000000000000000000000000004]")); - assert!(output.contains("ModExp: [0x0000000000000000000000000000000000000005]")); - assert!(output.contains("ECAdd: [0x0000000000000000000000000000000000000006]")); - assert!(output.contains("ECMul: [0x0000000000000000000000000000000000000007]")); - assert!(output.contains("ECPairing: [0x0000000000000000000000000000000000000008]")); - assert!(output.contains("Blake2F: [0x0000000000000000000000000000000000000009]")); - assert!(output.contains("PointEvaluation: [0x000000000000000000000000000000000000000A]")); + cmd.args(["test", "-vvvv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/Contract.t.sol:PrecompileLabelsTest +[PASS] testPrecompileLabels() ([GAS]) +Traces: + [9474] PrecompileLabelsTest::testPrecompileLabels() + ├─ [0] VM::deal(VM: [0x7109709ECfa91a80626fF3989D68f67F5b1DD12D], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(console: [0x000000000000000000636F6e736F6c652e6c6f67], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(Create2Deployer: [0x4e59b44847b379578588920cA78FbF26c0B4956C], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(DefaultSender: [0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(DefaultTestContract: [0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(ECRecover: [0x0000000000000000000000000000000000000001], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(SHA-256: [0x0000000000000000000000000000000000000002], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(RIPEMD-160: [0x0000000000000000000000000000000000000003], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(Identity: [0x0000000000000000000000000000000000000004], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(ModExp: [0x0000000000000000000000000000000000000005], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(ECAdd: [0x0000000000000000000000000000000000000006], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(ECMul: [0x0000000000000000000000000000000000000007], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(ECPairing: [0x0000000000000000000000000000000000000008], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(Blake2F: [0x0000000000000000000000000000000000000009], 1000000000000000000 [1e18]) + │ └─ ← [Return] + ├─ [0] VM::deal(PointEvaluation: [0x000000000000000000000000000000000000000A], 1000000000000000000 [1e18]) + │ └─ ← [Return] + └─ ← [Stop] + +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that `forge test` with config `show_logs: true` for fuzz tests will @@ -937,9 +1021,23 @@ forgetest_init!(should_show_logs_when_fuzz_test, |prj, cmd| { "#, ) .unwrap(); - cmd.args(["test", "-vv"]); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("inside fuzz test, x is:"), "\n{stdout}"); + cmd.args(["test", "-vv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/ContractFuzz.t.sol:ContractFuzz +[PASS] testFuzzConsoleLog(uint256) (runs: 3, [AVG_GAS]) +Logs: + inside fuzz test, x is: [..] + inside fuzz test, x is: [..] + inside fuzz test, x is: [..] + +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that `forge test` with inline config `show_logs = true` for fuzz tests will @@ -968,9 +1066,23 @@ forgetest_init!(should_show_logs_when_fuzz_test_inline_config, |prj, cmd| { "#, ) .unwrap(); - cmd.args(["test", "-vv"]); - let stdout = cmd.stdout_lossy(); - assert!(stdout.contains("inside fuzz test, x is:"), "\n{stdout}"); + cmd.args(["test", "-vv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/ContractFuzz.t.sol:ContractFuzz +[PASS] testFuzzConsoleLog(uint256) (runs: 3, [AVG_GAS]) +Logs: + inside fuzz test, x is: [..] + inside fuzz test, x is: [..] + inside fuzz test, x is: [..] + +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that `forge test` with config `show_logs: false` for fuzz tests will not display @@ -1000,9 +1112,18 @@ forgetest_init!(should_not_show_logs_when_fuzz_test, |prj, cmd| { "#, ) .unwrap(); - cmd.args(["test", "-vv"]); - let stdout = cmd.stdout_lossy(); - assert!(!stdout.contains("inside fuzz test, x is:"), "\n{stdout}"); + cmd.args(["test", "-vv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/ContractFuzz.t.sol:ContractFuzz +[PASS] testFuzzConsoleLog(uint256) (runs: 3, [AVG_GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests that `forge test` with inline config `show_logs = false` for fuzz tests will not @@ -1031,9 +1152,18 @@ forgetest_init!(should_not_show_logs_when_fuzz_test_inline_config, |prj, cmd| { "#, ) .unwrap(); - cmd.args(["test", "-vv"]); - let stdout = cmd.stdout_lossy(); - assert!(!stdout.contains("inside fuzz test, x is:"), "\n{stdout}"); + cmd.args(["test", "-vv"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 1 test for test/ContractFuzz.t.sol:ContractFuzz +[PASS] testFuzzConsoleLog(uint256) (runs: 3, [AVG_GAS]) +Suite result: ok. 1 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) + +"#]]); }); // tests internal functions trace @@ -1084,7 +1214,7 @@ contract SimpleContractTest is Test { ) .unwrap(); cmd.args(["test", "-vvvv", "--decode-internal"]).assert_success().stdout_eq(str![[r#" -Compiling 24 files with [SOLC_VERSION] +[COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] Compiler run successful! @@ -1211,29 +1341,37 @@ contract DeterministicRandomnessTest is Test { // Run the test twice with the same seed and verify the outputs are the same. let seed1 = "0xa1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; - cmd.args(["test", "--fuzz-seed", seed1, "-vv"]).assert_success(); - let out1 = cmd.stdout_lossy(); + let out1 = cmd + .args(["test", "--fuzz-seed", seed1, "-vv"]) + .assert_success() + .get_output() + .stdout_lossy(); let res1 = extract_test_result(&out1); - cmd.forge_fuse(); - cmd.args(["test", "--fuzz-seed", seed1, "-vv"]).assert_success(); - let out2 = cmd.stdout_lossy(); + let out2 = cmd + .forge_fuse() + .args(["test", "--fuzz-seed", seed1, "-vv"]) + .assert_success() + .get_output() + .stdout_lossy(); let res2 = extract_test_result(&out2); assert_eq!(res1, res2); // Run the test with another seed and verify the output differs. let seed2 = "0xb1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"; - cmd.forge_fuse(); - cmd.args(["test", "--fuzz-seed", seed2, "-vv"]).assert_success(); - let out3 = cmd.stdout_lossy(); + let out3 = cmd + .forge_fuse() + .args(["test", "--fuzz-seed", seed2, "-vv"]) + .assert_success() + .get_output() + .stdout_lossy(); let res3 = extract_test_result(&out3); assert_ne!(res3, res1); // Run the test without a seed and verify the outputs differs once again. cmd.forge_fuse(); - cmd.args(["test", "-vv"]).assert_success(); - let out4 = cmd.stdout_lossy(); + let out4 = cmd.args(["test", "-vv"]).assert_success().get_output().stdout_lossy(); let res4 = extract_test_result(&out4); assert_ne!(res4, res1); assert_ne!(res4, res3); diff --git a/crates/forge/tests/cli/verify.rs b/crates/forge/tests/cli/verify.rs index 2f1f1368d1907..c8ce2c25f1bfc 100644 --- a/crates/forge/tests/cli/verify.rs +++ b/crates/forge/tests/cli/verify.rs @@ -5,7 +5,7 @@ use crate::utils::{self, EnvExternalities}; use foundry_common::retry::Retry; use foundry_test_utils::{ forgetest, - util::{TestCommand, TestProject}, + util::{OutputExt, TestCommand, TestProject}, }; use std::time::Duration; @@ -77,7 +77,7 @@ fn parse_verification_result(cmd: &mut TestCommand, retries: u32) -> eyre::Resul // give etherscan some time to verify the contract let retry = Retry::new(retries, Some(Duration::from_secs(30))); retry.run(|| -> eyre::Result<()> { - let output = cmd.unchecked_output(); + let output = cmd.execute(); let out = String::from_utf8_lossy(&output.stdout); println!("{out}"); if out.contains("Contract successfully verified") { @@ -97,7 +97,7 @@ fn await_verification_response(info: EnvExternalities, mut cmd: TestCommand) { let retry = Retry::new(5, Some(Duration::from_secs(60))); retry .run(|| -> eyre::Result { - let output = cmd.unchecked_output(); + let output = cmd.execute(); let out = String::from_utf8_lossy(&output.stdout); utils::parse_verification_guid(&out).ok_or_else(|| { eyre::eyre!( @@ -132,11 +132,15 @@ fn verify_on_chain(info: Option, prj: TestProject, mut cmd: Te add_verify_target(&prj); let contract_path = "src/Verify.sol:Verify"; - cmd.arg("create").args(info.create_args()).arg(contract_path); - - let out = cmd.stdout_lossy(); - let address = utils::parse_deployed_address(out.as_str()) - .unwrap_or_else(|| panic!("Failed to parse deployer {out}")); + let output = cmd + .arg("create") + .args(info.create_args()) + .arg(contract_path) + .assert_success() + .get_output() + .stdout_lossy(); + let address = utils::parse_deployed_address(output.as_str()) + .unwrap_or_else(|| panic!("Failed to parse deployer {output}")); cmd.forge_fuse().arg("verify-contract").root_arg().args([ "--chain-id".to_string(), @@ -161,15 +165,21 @@ fn guess_constructor_args(info: Option, prj: TestProject, mut add_verify_target_with_constructor(&prj); let contract_path = "src/Verify.sol:Verify"; - cmd.arg("create").args(info.create_args()).arg(contract_path).args(vec![ - "--constructor-args", - "(239,SomeString)", - "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", - ]); - - let out = cmd.stdout_lossy(); - let address = utils::parse_deployed_address(out.as_str()) - .unwrap_or_else(|| panic!("Failed to parse deployer {out}")); + let output = cmd + .arg("create") + .args(info.create_args()) + .arg(contract_path) + .args(vec![ + "--constructor-args", + "(239,SomeString)", + "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", + ]) + .assert_success() + .get_output() + .stdout_lossy(); + + let address = utils::parse_deployed_address(output.as_str()) + .unwrap_or_else(|| panic!("Failed to parse deployer {output}")); cmd.forge_fuse().arg("verify-contract").root_arg().args([ "--rpc-url".to_string(), @@ -195,15 +205,15 @@ fn create_verify_on_chain(info: Option, prj: TestProject, mut add_single_verify_target_file(&prj); let contract_path = "src/Verify.sol:Verify"; - cmd.arg("create").args(info.create_args()).args([ - contract_path, - "--etherscan-api-key", - info.etherscan.as_str(), - "--verify", - ]); - - let out = cmd.stdout_lossy(); - assert!(out.contains("Contract successfully verified"), "{}", out); + let output = cmd + .arg("create") + .args(info.create_args()) + .args([contract_path, "--etherscan-api-key", info.etherscan.as_str(), "--verify"]) + .assert_success() + .get_output() + .stdout_lossy(); + + assert!(output.contains("Contract successfully verified"), "{}", output); } } diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index 960e22124f4ac..ca0f60d0d64c2 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -25,12 +25,10 @@ alloy-provider.workspace = true eyre.workspace = true fd-lock = "4.0.0" parking_lot.workspace = true -similar-asserts.workspace = true regex = "1" serde_json.workspace = true tracing.workspace = true tracing-subscriber = { workspace = true, features = ["env-filter"] } -walkdir.workspace = true rand.workspace = true snapbox = { version = "0.6.9", features = ["json", "regex"] } diff --git a/crates/test-utils/src/macros.rs b/crates/test-utils/src/macros.rs index 064a94ef2d6d0..cc92bb040607d 100644 --- a/crates/test-utils/src/macros.rs +++ b/crates/test-utils/src/macros.rs @@ -15,7 +15,7 @@ /// // adds `init` to forge's command arguments /// cmd.arg("init"); /// // executes forge and panics if the command failed or output is empty -/// cmd.assert_non_empty_stdout(); +/// cmd.assert_success().stdout_eq(str![[r#""#]]); /// }); /// ``` /// diff --git a/crates/test-utils/src/script.rs b/crates/test-utils/src/script.rs index 0bc4d399db527..9b018573261f0 100644 --- a/crates/test-utils/src/script.rs +++ b/crates/test-utils/src/script.rs @@ -1,4 +1,4 @@ -use crate::{init_tracing, TestCommand}; +use crate::{init_tracing, util::lossy_string, TestCommand}; use alloy_primitives::Address; use alloy_provider::Provider; use eyre::Result; @@ -221,7 +221,9 @@ impl ScriptTester { } pub fn run(&mut self, expected: ScriptOutcome) -> &mut Self { - let (stdout, stderr) = self.cmd.unchecked_output_lossy(); + let out = self.cmd.execute(); + let (stdout, stderr) = (lossy_string(&out.stdout), lossy_string(&out.stderr)); + trace!(target: "tests", "STDOUT\n{stdout}\n\nSTDERR\n{stderr}"); let output = if expected.is_err() { &stderr } else { &stdout }; diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index ac5674d9a3333..4b9f8f53ee2c4 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -11,7 +11,7 @@ use foundry_compilers::{ use foundry_config::Config; use parking_lot::Mutex; use regex::Regex; -use snapbox::cmd::OutputAssert; +use snapbox::{cmd::OutputAssert, str}; use std::{ env, ffi::OsStr, @@ -204,7 +204,7 @@ impl ExtTester { } test_cmd.env("FOUNDRY_INVARIANT_DEPTH", "15"); - test_cmd.assert_non_empty_stdout(); + test_cmd.assert_success(); } } @@ -382,8 +382,7 @@ pub fn try_setup_forge_remote( let prj = TestProject::with_project(tmp); if config.run_build { let mut cmd = prj.forge_command(); - cmd.arg("build"); - cmd.ensure_execute_success().wrap_err("`forge build` unsuccessful")?; + cmd.arg("build").assert_success(); } for addon in config.run_commands { debug_assert!(!addon.is_empty()); @@ -835,104 +834,83 @@ impl TestCommand { self } - /// Does not apply [`snapbox`] redactions to the command output. - pub fn with_no_redact(&mut self) -> &mut Self { - self.redact_output = false; - self - } - /// Returns the `Config` as spit out by `forge config` #[track_caller] pub fn config(&mut self) -> Config { self.cmd.args(["config", "--json"]); - let output = self.output(); - let c = lossy_string(&output.stdout); - let config = serde_json::from_str(c.as_ref()).unwrap(); + let output = self.assert().success().get_output().stdout_lossy(); + let config = serde_json::from_str(output.as_ref()).unwrap(); self.forge_fuse(); config } /// Runs `git init` inside the project's dir #[track_caller] - pub fn git_init(&self) -> Output { + pub fn git_init(&self) { let mut cmd = Command::new("git"); cmd.arg("init").current_dir(self.project.root()); - let output = cmd.output().unwrap(); - self.ensure_success(&output).unwrap(); - output - } - - /// Returns a new [Command] that is inside the current project dir - pub fn cmd_in_current_dir(&self, program: &str) -> Command { - let mut cmd = Command::new(program); - cmd.current_dir(self.project.root()); - cmd + let output = OutputAssert::new(cmd.output().unwrap()); + output.success(); } /// Runs `git add .` inside the project's dir #[track_caller] - pub fn git_add(&self) -> Result<()> { - let mut cmd = self.cmd_in_current_dir("git"); + pub fn git_add(&self) { + let mut cmd = Command::new("git"); + cmd.current_dir(self.project.root()); cmd.arg("add").arg("."); - let output = cmd.output()?; - self.ensure_success(&output) + let output = OutputAssert::new(cmd.output().unwrap()); + output.success(); } /// Runs `git commit .` inside the project's dir #[track_caller] - pub fn git_commit(&self, msg: &str) -> Result<()> { - let mut cmd = self.cmd_in_current_dir("git"); + pub fn git_commit(&self, msg: &str) { + let mut cmd = Command::new("git"); + cmd.current_dir(self.project.root()); cmd.arg("commit").arg("-m").arg(msg); - let output = cmd.output()?; - self.ensure_success(&output) + let output = OutputAssert::new(cmd.output().unwrap()); + output.success(); } - /// Executes the command and returns the `(stdout, stderr)` of the output as lossy `String`s. - /// - /// Expects the command to be successful. + /// Runs the command, returning a [`snapbox`] object to assert the command output. #[track_caller] - pub fn output_lossy(&mut self) -> (String, String) { - let output = self.output(); - (lossy_string(&output.stdout), lossy_string(&output.stderr)) + pub fn assert(&mut self) -> OutputAssert { + let assert = OutputAssert::new(self.execute()); + if self.redact_output { + return assert.with_assert(test_assert()); + }; + assert } - /// Executes the command and returns the `(stdout, stderr)` of the output as lossy `String`s. - /// - /// Does not expect the command to be successful. + /// Runs the command and asserts that it resulted in success. #[track_caller] - pub fn unchecked_output_lossy(&mut self) -> (String, String) { - let output = self.unchecked_output(); - (lossy_string(&output.stdout), lossy_string(&output.stderr)) + pub fn assert_success(&mut self) -> OutputAssert { + self.assert().success() } - /// Executes the command and returns the stderr as lossy `String`. - /// - /// **Note**: This function checks whether the command was successful. + /// Runs the command and asserts that it **failed** nothing was printed to stdout. #[track_caller] - pub fn stdout_lossy(&mut self) -> String { - lossy_string(&self.output().stdout) + pub fn assert_empty_stdout(&mut self) { + self.assert_success().stdout_eq(str![[r#""#]]); } - /// Executes the command and returns the stderr as lossy `String`. - /// - /// **Note**: This function does **not** check whether the command was successful. + /// Runs the command and asserts that it failed. #[track_caller] - pub fn stderr_lossy(&mut self) -> String { - lossy_string(&self.unchecked_output().stderr) + pub fn assert_failure(&mut self) -> OutputAssert { + self.assert().failure() } - /// Returns the output but does not expect that the command was successful + /// Runs the command and asserts that it **failed** nothing was printed to stderr. #[track_caller] - pub fn unchecked_output(&mut self) -> Output { - self.execute() + pub fn assert_empty_stderr(&mut self) { + self.assert_failure().stderr_eq(str![[r#""#]]); } - /// Gets the output of a command. If the command failed, then this panics. - #[track_caller] - pub fn output(&mut self) -> Output { - let output = self.execute(); - self.ensure_success(&output).unwrap(); - output + /// Does not apply [`snapbox`] redactions to the command output. + pub fn with_no_redact(&mut self) -> &mut Self { + self.redact_output = false; + self } /// Executes command, applies stdin function and returns output @@ -951,142 +929,6 @@ impl TestCommand { } child.wait_with_output() } - - /// Executes command and expects an successful result - #[track_caller] - pub fn ensure_execute_success(&mut self) -> Result { - let out = self.try_execute()?; - self.ensure_success(&out)?; - Ok(out) - } - - /// Runs the command and prints its output - /// You have to pass --nocapture to cargo test or the print won't be displayed. - /// The full command would be: cargo test -- --nocapture - #[track_caller] - pub fn print_output(&mut self) { - let output = self.execute(); - println!("stdout:\n{}", lossy_string(&output.stdout)); - println!("\nstderr:\n{}", lossy_string(&output.stderr)); - } - - /// Writes the content of the output to new fixture files - #[track_caller] - pub fn write_fixtures(&mut self, name: impl AsRef) { - let name = name.as_ref(); - if let Some(parent) = name.parent() { - fs::create_dir_all(parent).unwrap(); - } - let output = self.execute(); - fs::write(format!("{}.stdout", name.display()), &output.stdout).unwrap(); - fs::write(format!("{}.stderr", name.display()), &output.stderr).unwrap(); - } - - /// Runs the command and asserts that it **failed** (resulted in an error exit code). - #[track_caller] - pub fn assert_err(&mut self) { - let out = self.execute(); - if out.status.success() { - self.make_panic(&out, true); - } - } - - /// Runs the command and asserts that it **failed** and something was printed to stderr. - #[track_caller] - pub fn assert_non_empty_stderr(&mut self) { - let out = self.execute(); - if out.status.success() || out.stderr.is_empty() { - self.make_panic(&out, true); - } - } - - /// Runs the command and asserts that it **succeeded** and something was printed to stdout. - #[track_caller] - pub fn assert_non_empty_stdout(&mut self) { - let out = self.execute(); - if !out.status.success() || out.stdout.is_empty() { - self.make_panic(&out, false); - } - } - - /// Runs the command and asserts that it **failed** nothing was printed to stdout. - #[track_caller] - pub fn assert_empty_stdout(&mut self) { - let out = self.execute(); - if !out.status.success() || !out.stderr.is_empty() { - self.make_panic(&out, true); - } - } - - #[track_caller] - pub fn ensure_success(&self, out: &Output) -> Result<()> { - if out.status.success() { - Ok(()) - } else { - Err(self.make_error(out, false)) - } - } - - #[track_caller] - fn make_panic(&self, out: &Output, expected_fail: bool) -> ! { - panic!("{}", self.make_error_message(out, expected_fail)) - } - - #[track_caller] - fn make_error(&self, out: &Output, expected_fail: bool) -> eyre::Report { - eyre::eyre!("{}", self.make_error_message(out, expected_fail)) - } - - pub fn make_error_message(&self, out: &Output, expected_fail: bool) -> String { - let msg = if expected_fail { - "expected failure but command succeeded!" - } else { - "command failed but expected success!" - }; - format!( - "\ ---- {:?} --- -{msg} - -status: {} - -paths: -{} - -stdout: -{} - -stderr: -{}", - self.cmd, - out.status, - self.project.inner.paths(), - lossy_string(&out.stdout), - lossy_string(&out.stderr), - ) - } - - /// Runs the command, returning a [`snapbox`] object to assert the command output. - #[track_caller] - pub fn assert(&mut self) -> OutputAssert { - let assert = OutputAssert::new(self.execute()); - if self.redact_output { - return assert.with_assert(test_assert()); - }; - assert - } - - /// Runs the command and asserts that it resulted in success. - #[track_caller] - pub fn assert_success(&mut self) -> OutputAssert { - self.assert().success() - } - - /// Runs the command and asserts that it failed. - #[track_caller] - pub fn assert_failure(&mut self) -> OutputAssert { - self.assert().failure() - } } fn test_assert() -> snapbox::Assert { @@ -1105,7 +947,15 @@ fn test_redactions() -> snapbox::Redactions { ("[AVG_GAS]", r"μ: \d+, ~: \d+"), ("[FILE]", r"-->.*\.sol"), ("[FILE]", r"Location(.|\n)*\.rs(.|\n)*Backtrace"), + ("[COMPILING_FILES]", r"Compiling \d+ files?"), ("[TX_HASH]", r"Transaction hash: 0x[0-9A-Fa-f]{64}"), + ("[ADDRESS]", r"Address: 0x[0-9A-Fa-f]{40}"), + ("[UPDATING_DEPENDENCIES]", r"Updating dependencies in .*"), + ("[SAVED_TRANSACTIONS]", r"Transactions saved to: .*\.json"), + ("[SAVED_SENSITIVE_VALUES]", r"Sensitive values saved to: .*\.json"), + ("[ESTIMATED_GAS_PRICE]", r"Estimated gas price:\s*(\d+(\.\d+)?)\s*gwei"), + ("[ESTIMATED_TOTAL_GAS_USED]", r"Estimated total gas used for script: \d+"), + ("[ESTIMATED_AMOUNT_REQUIRED]", r"Estimated amount required:\s*(\d+(\.\d+)?)\s*ETH"), ]; for (placeholder, re) in redactions { r.insert(placeholder, Regex::new(re).expect(re)).expect(re); @@ -1116,135 +966,17 @@ fn test_redactions() -> snapbox::Redactions { } /// Extension trait for [`Output`]. -/// -/// These function will read the path's content and assert that the process' output matches the -/// fixture. Since `forge` commands may emit colorized output depending on whether the current -/// terminal is tty, the path argument can be wrapped in [tty_fixture_path()] pub trait OutputExt { - /// Ensure the command wrote the expected data to `stdout`. - fn stdout_matches_content(&self, expected: &str); - - /// Ensure the command wrote the expected data to `stdout`. - fn stdout_matches_path(&self, expected_path: impl AsRef); - - /// Ensure the command wrote the expected data to `stderr`. - fn stderr_matches_path(&self, expected_path: impl AsRef); - - /// Returns the stderr as lossy string - fn stderr_lossy(&self) -> String; - /// Returns the stdout as lossy string fn stdout_lossy(&self) -> String; } -/// Patterns to remove from fixtures before comparing output -/// -/// This should strip everything that can vary from run to run, like elapsed time, file paths -static IGNORE_IN_FIXTURES: LazyLock = LazyLock::new(|| { - let re = &[ - // solc version - r" ?Solc(?: version)? \d+.\d+.\d+", - r" with(?: Solc)? \d+.\d+.\d+", - // solc runs - r"runs: \d+, μ: \d+, ~: \d+", - // elapsed time - r"(?:finished)? ?in .*?s(?: \(.*?s CPU time\))?", - // file paths - r"-->.*\.sol", - r"Location(.|\n)*\.rs(.|\n)*Backtrace", - // other - r"Transaction hash: 0x[0-9A-Fa-f]{64}", - ]; - Regex::new(&format!("({})", re.join("|"))).unwrap() -}); - -pub fn normalize_output(s: &str) -> String { - let s = s.replace("\r\n", "\n").replace('\\', "/"); - IGNORE_IN_FIXTURES.replace_all(&s, "").into_owned() -} - impl OutputExt for Output { - #[track_caller] - fn stdout_matches_content(&self, expected: &str) { - let out = lossy_string(&self.stdout); - similar_asserts::assert_eq!(normalize_output(&out), normalize_output(expected)); - } - - #[track_caller] - fn stdout_matches_path(&self, expected_path: impl AsRef) { - let expected = fs::read_to_string(expected_path).unwrap(); - self.stdout_matches_content(&expected); - } - - #[track_caller] - fn stderr_matches_path(&self, expected_path: impl AsRef) { - let expected = fs::read_to_string(expected_path).unwrap(); - let err = lossy_string(&self.stderr); - similar_asserts::assert_eq!(normalize_output(&err), normalize_output(&expected)); - } - - fn stderr_lossy(&self) -> String { - lossy_string(&self.stderr) - } - fn stdout_lossy(&self) -> String { lossy_string(&self.stdout) } } -/// Returns the fixture path depending on whether the current terminal is tty -/// -/// This is useful in combination with [OutputExt] -pub fn tty_fixture_path(path: impl AsRef) -> PathBuf { - let path = path.as_ref(); - if *IS_TTY { - return if let Some(ext) = path.extension().and_then(|s| s.to_str()) { - path.with_extension(format!("tty.{ext}")) - } else { - path.with_extension("tty") - } - } - path.to_path_buf() -} - -/// Return a recursive listing of all files and directories in the given -/// directory. This is useful for debugging transient and odd failures in -/// integration tests. -pub fn dir_list>(dir: P) -> Vec { - walkdir::WalkDir::new(dir) - .follow_links(true) - .into_iter() - .map(|result| result.unwrap().path().to_string_lossy().into_owned()) - .collect() -} - -fn lossy_string(bytes: &[u8]) -> String { +pub fn lossy_string(bytes: &[u8]) -> String { String::from_utf8_lossy(bytes).replace("\r\n", "\n") } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tty_path_works() { - let path = "tests/fixture/test.stdout"; - if *IS_TTY { - assert_eq!(tty_fixture_path(path), PathBuf::from("tests/fixture/test.tty.stdout")); - } else { - assert_eq!(tty_fixture_path(path), PathBuf::from(path)); - } - } - - #[test] - fn fixture_regex_matches() { - assert!(IGNORE_IN_FIXTURES.is_match( - r" -Location: - cli/src/compile.rs:151 - -Backtrace omitted. - " - )); - } -} diff --git a/crates/verify/src/etherscan/mod.rs b/crates/verify/src/etherscan/mod.rs index 9136c200d7a29..0a1d39c1956a1 100644 --- a/crates/verify/src/etherscan/mod.rs +++ b/crates/verify/src/etherscan/mod.rs @@ -455,7 +455,7 @@ mod tests { use super::*; use clap::Parser; use foundry_common::fs; - use foundry_test_utils::forgetest_async; + use foundry_test_utils::{forgetest_async, str}; use tempfile::tempdir; #[test] @@ -566,7 +566,12 @@ mod tests { prj.add_source("Counter1", "contract Counter {}").unwrap(); prj.add_source("Counter2", "contract Counter {}").unwrap(); - cmd.args(["build", "--force"]).ensure_execute_success().unwrap(); + cmd.args(["build", "--force"]).assert_success().stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! + +"#]]); let args = VerifyArgs::parse_from([ "foundry-cli",