diff --git a/packages/fuels-code-gen/src/program_bindings.rs b/packages/fuels-code-gen/src/program_bindings.rs index 6e7ac5d34..240013408 100644 --- a/packages/fuels-code-gen/src/program_bindings.rs +++ b/packages/fuels-code-gen/src/program_bindings.rs @@ -4,4 +4,4 @@ mod generated_code; mod resolved_type; mod utils; -pub use abigen::{Abigen, AbigenTarget, ProgramType}; +pub use abigen::{Abi, Abigen, AbigenTarget, ProgramType}; diff --git a/packages/fuels-code-gen/src/program_bindings/abigen.rs b/packages/fuels-code-gen/src/program_bindings/abigen.rs index 16fe42941..6d3269749 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen.rs @@ -1,6 +1,6 @@ use std::{collections::HashSet, path::PathBuf}; -pub use abigen_target::{AbigenTarget, ProgramType}; +pub use abigen_target::{Abi, AbigenTarget, ProgramType}; use fuel_abi_types::abi::full_program::FullTypeDeclaration; use inflector::Inflector; use itertools::Itertools; @@ -11,8 +11,7 @@ use regex::Regex; use crate::{ error::Result, program_bindings::{ - abigen::{abigen_target::ParsedAbigenTarget, bindings::generate_bindings}, - custom_types::generate_types, + abigen::bindings::generate_bindings, custom_types::generate_types, generated_code::GeneratedCode, }, utils::ident, @@ -35,9 +34,7 @@ impl Abigen { /// for, and of what nature (Contract, Script or Predicate). /// * `no_std`: don't use the Rust std library. pub fn generate(targets: Vec, no_std: bool) -> Result { - let parsed_targets = Self::parse_targets(targets)?; - - let generated_code = Self::generate_code(no_std, parsed_targets)?; + let generated_code = Self::generate_code(no_std, targets)?; let use_statements = generated_code.use_statements_for_uniquely_named_types(); @@ -68,10 +65,7 @@ impl Abigen { .expect("Wasm hotfix failed!") } - fn generate_code( - no_std: bool, - parsed_targets: Vec, - ) -> Result { + fn generate_code(no_std: bool, parsed_targets: Vec) -> Result { let custom_types = Self::filter_custom_types(&parsed_targets); let shared_types = Self::filter_shared_types(custom_types); @@ -83,11 +77,11 @@ impl Abigen { } fn generate_all_bindings( - parsed_targets: Vec, + targets: Vec, no_std: bool, shared_types: &HashSet, ) -> Result { - parsed_targets + targets .into_iter() .map(|target| Self::generate_binding(target, no_std, shared_types)) .fold_ok(GeneratedCode::default(), |acc, generated_code| { @@ -96,7 +90,7 @@ impl Abigen { } fn generate_binding( - target: ParsedAbigenTarget, + target: AbigenTarget, no_std: bool, shared_types: &HashSet, ) -> Result { @@ -128,13 +122,6 @@ impl Abigen { GeneratedCode::new(code, Default::default(), no_std) } - fn parse_targets(targets: Vec) -> Result> { - targets - .into_iter() - .map(|target| target.try_into()) - .collect() - } - fn generate_shared_types( shared_types: HashSet, no_std: bool, @@ -150,7 +137,7 @@ impl Abigen { } fn filter_custom_types( - all_types: &[ParsedAbigenTarget], + all_types: &[AbigenTarget], ) -> impl Iterator { all_types .iter() diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/abigen_target.rs b/packages/fuels-code-gen/src/program_bindings/abigen/abigen_target.rs index 1fd8649a4..fbd5e15eb 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/abigen_target.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/abigen_target.rs @@ -1,51 +1,121 @@ -use std::{convert::TryFrom, path::PathBuf, str::FromStr}; +use std::{ + convert::TryFrom, + env, fs, + path::{Path, PathBuf}, + str::FromStr, +}; use fuel_abi_types::abi::full_program::FullProgramABI; use proc_macro2::Ident; -use crate::{ - error, - error::{Error, Result}, - utils::Source, -}; +use crate::error::{error, Error, Result}; #[derive(Debug, Clone)] pub struct AbigenTarget { - pub name: String, - pub abi: String, - pub program_type: ProgramType, + pub(crate) name: String, + pub(crate) source: Abi, + pub(crate) program_type: ProgramType, +} + +impl AbigenTarget { + pub fn new(name: String, source: Abi, program_type: ProgramType) -> Self { + Self { + name, + source, + program_type, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + pub fn source(&self) -> &Abi { + &self.source + } + + pub fn program_type(&self) -> ProgramType { + self.program_type + } } -pub(crate) struct Abi { +#[derive(Debug, Clone)] +pub struct Abi { pub(crate) path: Option, pub(crate) abi: FullProgramABI, } -pub(crate) struct ParsedAbigenTarget { - pub name: String, - pub source: Abi, - pub program_type: ProgramType, -} +impl Abi { + pub fn load_from(path: impl AsRef) -> Result { + let path = Self::canonicalize_path(path.as_ref())?; -impl TryFrom for ParsedAbigenTarget { - type Error = Error; + let json_abi = fs::read_to_string(&path).map_err(|e| { + error!( + "failed to read `abi` file with path {}: {}", + path.display(), + e + ) + })?; + let abi = Self::parse_from_json(&json_abi)?; - fn try_from(value: AbigenTarget) -> Result { - Ok(Self { - name: value.name, - source: parse_program_abi(&value.abi)?, - program_type: value.program_type, + Ok(Abi { + path: Some(path), + abi, }) } + + fn canonicalize_path(path: &Path) -> Result { + let current_dir = env::current_dir() + .map_err(|e| error!("unable to get current directory: ").combine(e))?; + + let root = current_dir.canonicalize().map_err(|e| { + error!( + "unable to canonicalize current directory {}: ", + current_dir.display() + ) + .combine(e) + })?; + + let path = root.join(path); + + if path.is_relative() { + path.canonicalize().map_err(|e| { + error!( + "unable to canonicalize file from working dir {} with path {}: {}", + env::current_dir() + .map(|cwd| cwd.display().to_string()) + .unwrap_or_else(|err| format!("??? ({err})")), + path.display(), + e + ) + }) + } else { + Ok(path) + } + } + + fn parse_from_json(json_abi: &str) -> Result { + FullProgramABI::from_json_abi(json_abi) + .map_err(|e| error!("malformed `abi`. Did you use `forc` to create it?: ").combine(e)) + } + + pub fn path(&self) -> Option<&PathBuf> { + self.path.as_ref() + } + + pub fn abi(&self) -> &FullProgramABI { + &self.abi + } } -fn parse_program_abi(abi_source: &str) -> Result { - let source = Source::parse(abi_source).expect("failed to parse JSON ABI"); +impl FromStr for Abi { + type Err = Error; + + fn from_str(json_abi: &str) -> Result { + let abi = Abi::parse_from_json(json_abi)?; - let json_abi_str = source.get().expect("failed to parse JSON ABI from string"); - let abi = FullProgramABI::from_json_abi(&json_abi_str)?; - let path = source.path(); - Ok(Abi { path, abi }) + Ok(Abi { path: None, abi }) + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings.rs index 68a85d697..77db3666f 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings.rs @@ -2,7 +2,7 @@ use crate::{ error::Result, program_bindings::{ abigen::{ - abigen_target::ParsedAbigenTarget, + abigen_target::AbigenTarget, bindings::{ contract::contract_bindings, predicate::predicate_bindings, script::script_bindings, }, @@ -19,7 +19,7 @@ mod predicate; mod script; mod utils; -pub(crate) fn generate_bindings(target: ParsedAbigenTarget, no_std: bool) -> Result { +pub(crate) fn generate_bindings(target: AbigenTarget, no_std: bool) -> Result { let bindings_generator = match target.program_type { ProgramType::Script => script_bindings, ProgramType::Contract => contract_bindings, diff --git a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/utils.rs b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/utils.rs index 29f35e580..55dfc734a 100644 --- a/packages/fuels-code-gen/src/program_bindings/abigen/bindings/utils.rs +++ b/packages/fuels-code-gen/src/program_bindings/abigen/bindings/utils.rs @@ -16,7 +16,7 @@ pub(crate) fn extract_main_fn(abi: &[FullABIFunction]) -> Result<&FullABIFunctio .map(|candidate| candidate.name()) .collect::>(); Err(error!( - "ABI must have one and only one function with the name 'main'. Got: {fn_names:?}" + "`abi` must have only one function with the name 'main'. Got: {fn_names:?}" )) } } @@ -45,7 +45,7 @@ mod tests { assert_eq!( err.to_string(), - r#"ABI must have one and only one function with the name 'main'. Got: ["main", "another", "main"]"# + r#"`abi` must have only one function with the name 'main'. Got: ["main", "another", "main"]"# ); } diff --git a/packages/fuels-code-gen/src/utils.rs b/packages/fuels-code-gen/src/utils.rs index 7a405c360..d0e1259ff 100644 --- a/packages/fuels-code-gen/src/utils.rs +++ b/packages/fuels-code-gen/src/utils.rs @@ -1,4 +1 @@ pub use fuel_abi_types::utils::{ident, safe_ident, TypePath}; -pub use source::Source; - -mod source; diff --git a/packages/fuels-code-gen/src/utils/source.rs b/packages/fuels-code-gen/src/utils/source.rs deleted file mode 100644 index 63d479ef2..000000000 --- a/packages/fuels-code-gen/src/utils/source.rs +++ /dev/null @@ -1,116 +0,0 @@ -use std::{ - borrow::Cow, - env, fs, - path::{Path, PathBuf}, - str::FromStr, -}; - -use crate::error::{error, Error, Result}; - -/// A source of a Truffle artifact JSON. -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Source { - /// A raw ABI string - String(String), - - /// An ABI located on the local file system. - Local(PathBuf), -} - -impl Source { - /// Parses an ABI from a source - /// - /// Contract ABIs can be retrieved from the local filesystem or it can - /// be provided in-line. It accepts: - /// - /// - raw ABI JSON - /// - /// - `relative/path/to/Contract.json`: a relative path to an ABI JSON file. - /// This relative path is rooted in the current working directory. - /// To specify the root for relative paths, use `Source::with_root`. - /// - /// - `/absolute/path/to/Contract.json to an ABI JSON file. - pub fn parse(source: S) -> Result - where - S: AsRef, - { - let source = source.as_ref().trim(); - - if source.starts_with('{') || source.starts_with('[') || source.starts_with('\n') { - return Ok(Source::String(source.to_owned())); - } - let root = env::current_dir()?.canonicalize()?; - Ok(Source::with_root(root, source)) - } - - /// Parses an artifact source from a string and a specified root directory - /// for resolving relative paths. See `Source::with_root` for more details - /// on supported source strings. - fn with_root(root: P, source: S) -> Self - where - P: AsRef, - S: AsRef, - { - Source::local(root.as_ref().join(source.as_ref())) - } - - /// Creates a local filesystem source from a path string. - fn local

(path: P) -> Self - where - P: AsRef, - { - Source::Local(path.as_ref().into()) - } - - /// Retrieves the source JSON of the artifact this will either read the JSON - /// from the file system or retrieve a contract ABI from the network - /// depending on the source type. - pub fn get(&self) -> Result { - match self { - Source::Local(path) => get_local_contract(path), - Source::String(abi) => Ok(abi.clone()), - } - } - - pub fn path(&self) -> Option { - match self { - Source::Local(path) => Some(path.clone()), - _ => None, - } - } -} - -fn get_local_contract(path: &Path) -> Result { - let path = if path.is_relative() { - let absolute_path = path.canonicalize().map_err(|e| { - error!( - "unable to canonicalize file from working dir {} with path {}", - env::current_dir() - .map(|cwd| cwd.display().to_string()) - .unwrap_or_else(|err| format!("??? ({err})")), - path.display() - ) - .combine(e) - })?; - Cow::Owned(absolute_path) - } else { - Cow::Borrowed(path) - }; - - let json = fs::read_to_string(&path).map_err(|e| { - error!( - "failed to read artifact JSON file with path {}", - path.display() - ) - .combine(e) - })?; - Ok(json) -} - -impl FromStr for Source { - type Err = Error; - - fn from_str(s: &str) -> Result { - Source::parse(s) - } -} diff --git a/packages/fuels-macros/src/abigen/parsing.rs b/packages/fuels-macros/src/abigen/parsing.rs index 991d14f46..b2e38331b 100644 --- a/packages/fuels-macros/src/abigen/parsing.rs +++ b/packages/fuels-macros/src/abigen/parsing.rs @@ -1,7 +1,7 @@ -use fuels_code_gen::{AbigenTarget, ProgramType}; +use fuels_code_gen::{Abi, AbigenTarget, ProgramType}; use syn::{ parse::{Parse, ParseStream}, - Result, + LitStr, Result, }; use crate::parse_utils::{Command, UniqueNameValues}; @@ -14,11 +14,11 @@ impl From for Vec { impl From for AbigenTarget { fn from(macro_target: MacroAbigenTarget) -> Self { - AbigenTarget { - name: macro_target.name, - abi: macro_target.abi, - program_type: macro_target.program_type, - } + AbigenTarget::new( + macro_target.name, + macro_target.source, + macro_target.program_type, + ) } } @@ -27,8 +27,8 @@ impl From for AbigenTarget { #[derive(Debug)] pub(crate) struct MacroAbigenTarget { pub(crate) name: String, - pub(crate) abi: String, - pub(crate) program_type: ProgramType, + pub(crate) source: Abi, + pub program_type: ProgramType, } pub(crate) struct MacroAbigenTargets { @@ -54,12 +54,25 @@ impl MacroAbigenTarget { name_values.validate_has_no_other_names(&["name", "abi"])?; let name = name_values.get_as_lit_str("name")?.value(); - let abi = name_values.get_as_lit_str("abi")?.value(); + let abi_lit_str = name_values.get_as_lit_str("abi")?; + let source = Self::parse_inline_or_load_abi(abi_lit_str)?; Ok(Self { name, - abi, + source, program_type, }) } + + fn parse_inline_or_load_abi(abi_lit_str: &LitStr) -> Result { + let abi_string = abi_lit_str.value(); + let abi_str = abi_string.trim(); + + if abi_str.starts_with('{') || abi_str.starts_with('[') || abi_str.starts_with('\n') { + abi_str.parse() + } else { + Abi::load_from(abi_str) + } + .map_err(|e| syn::Error::new(abi_lit_str.span(), e.to_string())) + } } diff --git a/packages/fuels-macros/src/lib.rs b/packages/fuels-macros/src/lib.rs index e9758eab7..1bd9c7032 100644 --- a/packages/fuels-macros/src/lib.rs +++ b/packages/fuels-macros/src/lib.rs @@ -36,14 +36,18 @@ mod setup_program_test; pub fn abigen(input: TokenStream) -> TokenStream { let targets = parse_macro_input!(input as MacroAbigenTargets); - Abigen::generate(targets.into(), false).unwrap().into() + Abigen::generate(targets.into(), false) + .expect("abigen generation failed") + .into() } #[proc_macro] pub fn wasm_abigen(input: TokenStream) -> TokenStream { let targets = parse_macro_input!(input as MacroAbigenTargets); - Abigen::generate(targets.into(), true).unwrap().into() + Abigen::generate(targets.into(), true) + .expect("abigen generation failed") + .into() } /// Used to reduce boilerplate in integration tests. diff --git a/packages/fuels-macros/src/setup_program_test/code_gen.rs b/packages/fuels-macros/src/setup_program_test/code_gen.rs index 1fc3391df..9f49a2a30 100644 --- a/packages/fuels-macros/src/setup_program_test/code_gen.rs +++ b/packages/fuels-macros/src/setup_program_test/code_gen.rs @@ -3,8 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -use fuels_code_gen::{utils::ident, Abigen, AbigenTarget, ProgramType}; -use proc_macro2::{Ident, TokenStream}; +use fuels_code_gen::{utils::ident, Abi, Abigen, AbigenTarget, ProgramType}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use syn::LitStr; @@ -24,7 +24,7 @@ pub(crate) fn generate_setup_program_test_code( } = commands; let project_lookup = generate_project_lookup(&generate_bindings)?; - let abigen_code = abigen_code(&project_lookup); + let abigen_code = abigen_code(&project_lookup)?; let wallet_code = wallet_initialization_code(initialize_wallets); let deploy_code = contract_deploying_code(&deploy_contract, &project_lookup); let script_code = script_loading_code(&load_scripts, &project_lookup); @@ -50,18 +50,26 @@ fn generate_project_lookup(commands: &AbigenCommand) -> syn::Result) -> TokenStream { - let targets = generate_abigen_targets(project_lookup); - Abigen::generate(targets, false).expect("Failed to generate abigen") +fn abigen_code(project_lookup: &HashMap) -> syn::Result { + let targets = parse_abigen_targets(project_lookup)?; + + Ok(Abigen::generate(targets, false).expect("abigen generation failed")) } -fn generate_abigen_targets(project_lookup: &HashMap) -> Vec { +fn parse_abigen_targets( + project_lookup: &HashMap, +) -> syn::Result> { project_lookup .iter() - .map(|(name, project)| AbigenTarget { - name: name.clone(), - abi: project.abi_path(), - program_type: project.program_type, + .map(|(name, project)| { + let source = Abi::load_from(project.abi_path()) + .map_err(|e| syn::Error::new(project.path_span, e.to_string()))?; + + Ok(AbigenTarget::new( + name.clone(), + source, + project.program_type, + )) }) .collect() } @@ -181,6 +189,7 @@ fn script_loading_code( struct Project { program_type: ProgramType, path: PathBuf, + path_span: Span, } impl Project { @@ -188,11 +197,15 @@ impl Project { let path = Path::new(&dir.value()).canonicalize().map_err(|_| { syn::Error::new_spanned( dir.clone(), - "Unable to canonicalize forc project path. Make sure the path is valid!", + "unable to canonicalize forc project path. Make sure the path is valid!", ) })?; - Ok(Self { program_type, path }) + Ok(Self { + program_type, + path, + path_span: dir.span(), + }) } fn compile_file_path(&self, suffix: &str, description: &str) -> String { diff --git a/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.rs b/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.rs new file mode 100644 index 000000000..d069c48eb --- /dev/null +++ b/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.rs @@ -0,0 +1,5 @@ +use fuels_macros::abigen; + +abigen!(Contract(name = "SomeName", abi = "some_abi.json")); + +fn main() {} diff --git a/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.stderr b/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.stderr new file mode 100644 index 000000000..dbf6fe6fd --- /dev/null +++ b/packages/fuels-macros/tests/ui/abigen/invalid_abi_path.stderr @@ -0,0 +1,5 @@ +error: failed to read `abi` file with path $WORKSPACE/target/tests/trybuild/fuels-macros/some_abi.json: No such file or directory (os error 2) + --> tests/ui/abigen/invalid_abi_path.rs:3:43 + | +3 | abigen!(Contract(name = "SomeName", abi = "some_abi.json")); + | ^^^^^^^^^^^^^^^ diff --git a/packages/fuels-macros/tests/ui/abigen/malformed_abi.rs b/packages/fuels-macros/tests/ui/abigen/malformed_abi.rs new file mode 100644 index 000000000..7397d6c57 --- /dev/null +++ b/packages/fuels-macros/tests/ui/abigen/malformed_abi.rs @@ -0,0 +1,5 @@ +use fuels_macros::abigen; + +abigen!(Contract(name = "SomeName", abi = r#"{}"#)); + +fn main() {} diff --git a/packages/fuels-macros/tests/ui/abigen/malformed_abi.stderr b/packages/fuels-macros/tests/ui/abigen/malformed_abi.stderr new file mode 100644 index 000000000..ea10a8867 --- /dev/null +++ b/packages/fuels-macros/tests/ui/abigen/malformed_abi.stderr @@ -0,0 +1,5 @@ +error: malformed `abi`. Did you use `forc` to create it?: missing field `types` at line 1 column 2 + --> tests/ui/abigen/malformed_abi.rs:3:43 + | +3 | abigen!(Contract(name = "SomeName", abi = r#"{}"#)); + | ^^^^^^^ diff --git a/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_command.rs b/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_command.rs index 3710fd646..4fa2f78c5 100644 --- a/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_command.rs +++ b/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_command.rs @@ -3,7 +3,7 @@ use fuels_macros::setup_program_test; setup_program_test!( Wallets("wallet1"), Wallets("wallet2"), - Abigen(Contract(name = "MyContract", project = "some_file.json")) + Abigen(Contract(name = "MyContract", project = "some_project")) ); fn main() {} diff --git a/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_names.rs b/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_names.rs index 346164f32..a39d621e0 100644 --- a/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_names.rs +++ b/packages/fuels-macros/tests/ui/setup_program_test/duplicate_wallet_names.rs @@ -2,7 +2,7 @@ use fuels_macros::setup_program_test; setup_program_test!( Wallets("wallet1", "wallet1"), - Abigen(Contract(name = "MyContract", project = "some_file.json")) + Abigen(Contract(name = "MyContract", project = "some_project")) ); fn main() {} diff --git a/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.rs b/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.rs new file mode 100644 index 000000000..f24a39f08 --- /dev/null +++ b/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.rs @@ -0,0 +1,8 @@ +use fuels_macros::setup_program_test; + +setup_program_test!(Abigen(Contract( + name = "MyContract", + project = "some_project" +))); + +fn main() {} diff --git a/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.stderr b/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.stderr new file mode 100644 index 000000000..53dec148f --- /dev/null +++ b/packages/fuels-macros/tests/ui/setup_program_test/invalid_path.stderr @@ -0,0 +1,5 @@ +error: unable to canonicalize forc project path. Make sure the path is valid! + --> tests/ui/setup_program_test/invalid_path.rs:5:15 + | +5 | project = "some_project" + | ^^^^^^^^^^^^^^ diff --git a/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.rs b/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.rs new file mode 100644 index 000000000..730515260 --- /dev/null +++ b/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.rs @@ -0,0 +1,5 @@ +use fuels_macros::setup_program_test; + +setup_program_test!(Abigen(Contract(name = "MyContract", project = "."))); + +fn main() {} diff --git a/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.stderr b/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.stderr new file mode 100644 index 000000000..b13c26030 --- /dev/null +++ b/packages/fuels-macros/tests/ui/setup_program_test/invalid_project_path.stderr @@ -0,0 +1,5 @@ +error: failed to read `abi` file with path $WORKSPACE/target/tests/trybuild/fuels-macros/out/release/fuels-macros-abi.json: No such file or directory (os error 2) + --> tests/ui/setup_program_test/invalid_project_path.rs:3:68 + | +3 | setup_program_test!(Abigen(Contract(name = "MyContract", project = "."))); + | ^^^