diff --git a/Cargo.lock b/Cargo.lock index 2a6f10dcc19..cf96770e9b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2883,6 +2883,7 @@ dependencies = [ "noirc_frontend", "semver", "serde", + "tempfile", "test-case", "thiserror", "toml 0.7.8", diff --git a/docs/docs/noir/modules_packages_crates/workspaces.md b/docs/docs/noir/modules_packages_crates/workspaces.md index 513497f12bf..2fbf10aec52 100644 --- a/docs/docs/noir/modules_packages_crates/workspaces.md +++ b/docs/docs/noir/modules_packages_crates/workspaces.md @@ -33,10 +33,14 @@ members = ["crates/a", "crates/b"] default-member = "crates/a" ``` -`members` indicates which packages are included in the workspace. As such, all member packages of a workspace will be processed when the `--workspace` flag is used with various commands or if a `default-member` is not specified. +`members` indicates which packages are included in the workspace. As such, all member packages of a workspace will be processed when the `--workspace` flag is used with various commands or if a `default-member` is not specified. The `--package` option can be used to limit +the scope of some commands to a specific member of the workspace; otherwise these commands run on the package nearest on the path to the +current directory where `nargo` was invoked. `default-member` indicates which package various commands process by default. Libraries can be defined in a workspace. Inside a workspace, these are consumed as `{ path = "../to_lib" }` dependencies in Nargo.toml. Inside a workspace, these are consumed as `{ path = "../to_lib" }` dependencies in Nargo.toml. + +Please note that nesting regular packages is not supported: certain commands work on the workspace level and will use the topmost Nargo.toml file they can find on the path; unless this is a workspace configuration with `members`, the command might run on some unintended package. diff --git a/tooling/nargo_cli/src/cli/check_cmd.rs b/tooling/nargo_cli/src/cli/check_cmd.rs index 9059f1dd8e8..c8695a8f626 100644 --- a/tooling/nargo_cli/src/cli/check_cmd.rs +++ b/tooling/nargo_cli/src/cli/check_cmd.rs @@ -4,33 +4,25 @@ use clap::Args; use fm::FileManager; use iter_extended::btree_map; use nargo::{ - errors::CompileError, - insert_all_files_for_workspace_into_file_manager, - ops::report_errors, - package::{CrateName, Package}, - parse_all, prepare_package, + errors::CompileError, insert_all_files_for_workspace_into_file_manager, ops::report_errors, + package::Package, parse_all, prepare_package, }; -use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml}; use noirc_abi::{AbiParameter, AbiType, MAIN_RETURN_NAME}; use noirc_driver::{ check_crate, compute_function_abi, CompileOptions, CrateId, NOIR_ARTIFACT_VERSION_STRING, }; use noirc_frontend::hir::{Context, ParsedFiles}; -use super::fs::write_to_file; use super::NargoConfig; +use super::{fs::write_to_file, PackageOptions}; /// Checks the constraint system for errors #[derive(Debug, Clone, Args)] #[clap(visible_alias = "c")] pub(crate) struct CheckCommand { - /// The name of the package to check - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Check all packages in the workspace - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, /// Force overwrite of existing files #[clap(long = "overwrite")] @@ -42,9 +34,7 @@ pub(crate) struct CheckCommand { pub(crate) fn run(args: CheckCommand, config: NargoConfig) -> Result<(), CliError> { let toml_path = get_package_manifest(&config.program_dir)?; - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); + let selection = args.package_options.package_selection(); let workspace = resolve_workspace_from_toml( &toml_path, selection, diff --git a/tooling/nargo_cli/src/cli/compile_cmd.rs b/tooling/nargo_cli/src/cli/compile_cmd.rs index f134374f89e..2ecf6959a94 100644 --- a/tooling/nargo_cli/src/cli/compile_cmd.rs +++ b/tooling/nargo_cli/src/cli/compile_cmd.rs @@ -5,7 +5,7 @@ use std::time::Duration; use acvm::acir::circuit::ExpressionWidth; use fm::FileManager; use nargo::ops::{collect_errors, compile_contract, compile_program, report_errors}; -use nargo::package::{CrateName, Package}; +use nargo::package::Package; use nargo::workspace::Workspace; use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all}; use nargo_toml::{ @@ -23,19 +23,14 @@ use notify_debouncer_full::new_debouncer; use crate::errors::CliError; use super::fs::program::{read_program_from_file, save_contract_to_file, save_program_to_file}; -use super::NargoConfig; +use super::{NargoConfig, PackageOptions}; use rayon::prelude::*; /// Compile the program and its secret execution trace into ACIR format #[derive(Debug, Clone, Args)] pub(crate) struct CompileCommand { - /// The name of the package to compile - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Compile all packages in the workspace. - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, #[clap(flatten)] compile_options: CompileOptions, @@ -46,10 +41,7 @@ pub(crate) struct CompileCommand { } pub(crate) fn run(args: CompileCommand, config: NargoConfig) -> Result<(), CliError> { - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); - + let selection = args.package_options.package_selection(); let workspace = read_workspace(&config.program_dir, selection)?; if args.watch { diff --git a/tooling/nargo_cli/src/cli/debug_cmd.rs b/tooling/nargo_cli/src/cli/debug_cmd.rs index e837f297475..f4dd607a53e 100644 --- a/tooling/nargo_cli/src/cli/debug_cmd.rs +++ b/tooling/nargo_cli/src/cli/debug_cmd.rs @@ -38,7 +38,7 @@ pub(crate) struct DebugCommand { /// The name of the package to execute #[clap(long)] - package: Option<CrateName>, + pub(super) package: Option<CrateName>, #[clap(flatten)] compile_options: CompileOptions, diff --git a/tooling/nargo_cli/src/cli/execute_cmd.rs b/tooling/nargo_cli/src/cli/execute_cmd.rs index fa95d3123c6..81aca16846b 100644 --- a/tooling/nargo_cli/src/cli/execute_cmd.rs +++ b/tooling/nargo_cli/src/cli/execute_cmd.rs @@ -8,8 +8,8 @@ use clap::Args; use nargo::constants::PROVER_INPUT_FILE; use nargo::errors::try_to_diagnose_runtime_error; use nargo::foreign_calls::DefaultForeignCallExecutor; -use nargo::package::{CrateName, Package}; -use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use nargo::package::Package; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml}; use noirc_abi::input_parser::{Format, InputValue}; use noirc_abi::InputMap; use noirc_artifacts::debug::DebugArtifact; @@ -17,7 +17,7 @@ use noirc_driver::{CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING use super::compile_cmd::compile_workspace_full; use super::fs::{inputs::read_inputs_from_file, witness::save_witness_to_dir}; -use super::NargoConfig; +use super::{NargoConfig, PackageOptions}; use crate::cli::fs::program::read_program_from_file; use crate::errors::CliError; @@ -34,13 +34,8 @@ pub(crate) struct ExecuteCommand { #[clap(long, short, default_value = PROVER_INPUT_FILE)] prover_name: String, - /// The name of the package to execute - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Execute all packages in the workspace - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, #[clap(flatten)] compile_options: CompileOptions, @@ -52,9 +47,7 @@ pub(crate) struct ExecuteCommand { pub(crate) fn run(args: ExecuteCommand, config: NargoConfig) -> Result<(), CliError> { let toml_path = get_package_manifest(&config.program_dir)?; - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); + let selection = args.package_options.package_selection(); let workspace = resolve_workspace_from_toml( &toml_path, selection, diff --git a/tooling/nargo_cli/src/cli/export_cmd.rs b/tooling/nargo_cli/src/cli/export_cmd.rs index c3752db7fbd..cb92b987c4e 100644 --- a/tooling/nargo_cli/src/cli/export_cmd.rs +++ b/tooling/nargo_cli/src/cli/export_cmd.rs @@ -10,13 +10,11 @@ use nargo::package::Package; use nargo::prepare_package; use nargo::workspace::Workspace; use nargo::{insert_all_files_for_workspace_into_file_manager, parse_all}; -use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml}; use noirc_driver::{ compile_no_check, CompileOptions, CompiledProgram, NOIR_ARTIFACT_VERSION_STRING, }; -use noirc_frontend::graph::CrateName; - use clap::Args; use crate::errors::CliError; @@ -24,18 +22,13 @@ use crate::errors::CliError; use super::check_cmd::check_crate_and_report_errors; use super::fs::program::save_program_to_file; -use super::NargoConfig; +use super::{NargoConfig, PackageOptions}; /// Exports functions marked with #[export] attribute #[derive(Debug, Clone, Args)] pub(crate) struct ExportCommand { - /// The name of the package to compile - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Compile all packages in the workspace - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, #[clap(flatten)] compile_options: CompileOptions, @@ -43,10 +36,7 @@ pub(crate) struct ExportCommand { pub(crate) fn run(args: ExportCommand, config: NargoConfig) -> Result<(), CliError> { let toml_path = get_package_manifest(&config.program_dir)?; - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); - + let selection = args.package_options.package_selection(); let workspace = resolve_workspace_from_toml( &toml_path, selection, diff --git a/tooling/nargo_cli/src/cli/fmt_cmd.rs b/tooling/nargo_cli/src/cli/fmt_cmd.rs index 66496db517c..7b29a90c5c0 100644 --- a/tooling/nargo_cli/src/cli/fmt_cmd.rs +++ b/tooling/nargo_cli/src/cli/fmt_cmd.rs @@ -9,7 +9,7 @@ use noirc_frontend::{hir::def_map::parse_file, parser::ParserError}; use crate::errors::CliError; -use super::NargoConfig; +use super::{NargoConfig, PackageOptions}; /// Format the Noir files in a workspace #[derive(Debug, Clone, Args)] @@ -17,15 +17,22 @@ pub(crate) struct FormatCommand { /// Run noirfmt in check mode #[arg(long)] check: bool, + + #[clap(flatten)] + pub(super) package_options: PackageOptions, } pub(crate) fn run(args: FormatCommand, config: NargoConfig) -> Result<(), CliError> { let check_mode = args.check; let toml_path = get_package_manifest(&config.program_dir)?; + let selection = match args.package_options.package_selection() { + PackageSelection::DefaultOrAll => PackageSelection::All, + other => other, + }; let workspace = resolve_workspace_from_toml( &toml_path, - PackageSelection::All, + selection, Some(NOIR_ARTIFACT_VERSION_STRING.to_string()), )?; diff --git a/tooling/nargo_cli/src/cli/info_cmd.rs b/tooling/nargo_cli/src/cli/info_cmd.rs index 769a1f79d81..4dab7da2ffb 100644 --- a/tooling/nargo_cli/src/cli/info_cmd.rs +++ b/tooling/nargo_cli/src/cli/info_cmd.rs @@ -3,11 +3,9 @@ use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; use iter_extended::vecmap; use nargo::{ - constants::PROVER_INPUT_FILE, - foreign_calls::DefaultForeignCallExecutor, - package::{CrateName, Package}, + constants::PROVER_INPUT_FILE, foreign_calls::DefaultForeignCallExecutor, package::Package, }; -use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml}; use noirc_abi::input_parser::Format; use noirc_artifacts::program::ProgramArtifact; use noirc_driver::{CompileOptions, NOIR_ARTIFACT_VERSION_STRING}; @@ -20,7 +18,7 @@ use crate::{cli::fs::inputs::read_inputs_from_file, errors::CliError}; use super::{ compile_cmd::{compile_workspace_full, get_target_width}, fs::program::read_program_from_file, - NargoConfig, + NargoConfig, PackageOptions, }; /// Provides detailed information on each of a program's function (represented by a single circuit) @@ -31,13 +29,8 @@ use super::{ #[derive(Debug, Clone, Args)] #[clap(visible_alias = "i")] pub(crate) struct InfoCommand { - /// The name of the package to detail - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Detail all packages in the workspace - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, /// Output a JSON formatted report. Changes to this format are not currently considered breaking. #[clap(long, hide = true)] @@ -56,9 +49,7 @@ pub(crate) struct InfoCommand { pub(crate) fn run(mut args: InfoCommand, config: NargoConfig) -> Result<(), CliError> { let toml_path = get_package_manifest(&config.program_dir)?; - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); + let selection = args.package_options.package_selection(); let workspace = resolve_workspace_from_toml( &toml_path, selection, diff --git a/tooling/nargo_cli/src/cli/mod.rs b/tooling/nargo_cli/src/cli/mod.rs index 284dd10cb88..cc72092daa1 100644 --- a/tooling/nargo_cli/src/cli/mod.rs +++ b/tooling/nargo_cli/src/cli/mod.rs @@ -1,8 +1,8 @@ use clap::{Args, Parser, Subcommand}; use const_format::formatcp; -use nargo_toml::find_package_root; -use noirc_driver::NOIR_ARTIFACT_VERSION_STRING; -use std::path::PathBuf; +use nargo_toml::{ManifestError, PackageSelection}; +use noirc_driver::{CrateName, NOIR_ARTIFACT_VERSION_STRING}; +use std::path::{Path, PathBuf}; use color_eyre::eyre; @@ -52,6 +52,48 @@ pub(crate) struct NargoConfig { program_dir: PathBuf, } +/// Options for commands that work on either workspace or package scope. +#[derive(Args, Clone, Debug, Default)] +pub(crate) struct PackageOptions { + /// The name of the package to run the command on. + /// By default run on the first one found moving up along the ancestors of the current directory. + #[clap(long, conflicts_with = "workspace")] + package: Option<CrateName>, + + /// Run on all packages in the workspace + #[clap(long, conflicts_with = "package")] + workspace: bool, +} + +impl PackageOptions { + /// Decide which package to run the command on: + /// * `package` if non-empty + /// * all packages if `workspace` is `true` + /// * otherwise the default package + pub(crate) fn package_selection(&self) -> PackageSelection { + let default_selection = + if self.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; + + self.package.clone().map_or(default_selection, PackageSelection::Selected) + } + + /// Whether we need to look for the package manifest at the workspace level. + /// If a package is specified, it might not be the current package. + fn scope(&self) -> CommandScope { + if self.workspace || self.package.is_some() { + CommandScope::Workspace + } else { + CommandScope::CurrentPackage + } + } +} + +enum CommandScope { + Workspace, + CurrentPackage, + Any, +} + #[non_exhaustive] #[derive(Subcommand, Clone, Debug)] enum NargoCommand { @@ -83,22 +125,8 @@ pub(crate) fn start_cli() -> eyre::Result<()> { } // Search through parent directories to find package root if necessary. - match &command { - NargoCommand::Check(..) - | NargoCommand::Fmt(..) - | NargoCommand::Compile(..) - | NargoCommand::Execute(..) - | NargoCommand::Export(..) - | NargoCommand::Debug(..) - | NargoCommand::Test(..) - | NargoCommand::Info(..) => { - config.program_dir = find_package_root(&config.program_dir)?; - } - NargoCommand::New(..) - | NargoCommand::Init(..) - | NargoCommand::Lsp(..) - | NargoCommand::Dap(..) - | NargoCommand::GenerateCompletionScript(..) => (), + if let Some(program_dir) = command_dir(&command, &config.program_dir)? { + config.program_dir = program_dir; } match command { @@ -127,6 +155,43 @@ pub(crate) fn start_cli() -> eyre::Result<()> { Ok(()) } +/// Some commands have package options, which we use here to decide whether to +/// alter `--program-dir` to point at a manifest, depending on whether we want +/// to work on a specific package or the entire workspace. +fn command_scope(cmd: &NargoCommand) -> CommandScope { + match &cmd { + NargoCommand::Check(cmd) => cmd.package_options.scope(), + NargoCommand::Compile(cmd) => cmd.package_options.scope(), + NargoCommand::Execute(cmd) => cmd.package_options.scope(), + NargoCommand::Export(cmd) => cmd.package_options.scope(), + NargoCommand::Test(cmd) => cmd.package_options.scope(), + NargoCommand::Info(cmd) => cmd.package_options.scope(), + NargoCommand::Fmt(cmd) => cmd.package_options.scope(), + NargoCommand::Debug(cmd) => { + if cmd.package.is_some() { + CommandScope::Workspace + } else { + CommandScope::CurrentPackage + } + } + NargoCommand::New(..) + | NargoCommand::Init(..) + | NargoCommand::Lsp(..) + | NargoCommand::Dap(..) + | NargoCommand::GenerateCompletionScript(..) => CommandScope::Any, + } +} + +/// A manifest directory we need to change into, if the command needs it. +fn command_dir(cmd: &NargoCommand, program_dir: &Path) -> Result<Option<PathBuf>, ManifestError> { + let workspace = match command_scope(cmd) { + CommandScope::Workspace => true, + CommandScope::CurrentPackage => false, + CommandScope::Any => return Ok(None), + }; + Ok(Some(nargo_toml::find_root(program_dir, workspace)?)) +} + #[cfg(test)] mod tests { use clap::Parser; diff --git a/tooling/nargo_cli/src/cli/test_cmd.rs b/tooling/nargo_cli/src/cli/test_cmd.rs index e8c0e16ff4a..92d3c91e713 100644 --- a/tooling/nargo_cli/src/cli/test_cmd.rs +++ b/tooling/nargo_cli/src/cli/test_cmd.rs @@ -5,12 +5,10 @@ use bn254_blackbox_solver::Bn254BlackBoxSolver; use clap::Args; use fm::FileManager; use nargo::{ - insert_all_files_for_workspace_into_file_manager, - ops::TestStatus, - package::{CrateName, Package}, - parse_all, prepare_package, + insert_all_files_for_workspace_into_file_manager, ops::TestStatus, package::Package, parse_all, + prepare_package, }; -use nargo_toml::{get_package_manifest, resolve_workspace_from_toml, PackageSelection}; +use nargo_toml::{get_package_manifest, resolve_workspace_from_toml}; use noirc_driver::{check_crate, CompileOptions, NOIR_ARTIFACT_VERSION_STRING}; use noirc_frontend::hir::{FunctionNameMatch, ParsedFiles}; use rayon::prelude::{IntoParallelIterator, ParallelBridge, ParallelIterator}; @@ -18,7 +16,7 @@ use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; use crate::{cli::check_cmd::check_crate_and_report_errors, errors::CliError}; -use super::NargoConfig; +use super::{NargoConfig, PackageOptions}; /// Run the tests for this program #[derive(Debug, Clone, Args)] @@ -35,13 +33,8 @@ pub(crate) struct TestCommand { #[clap(long)] exact: bool, - /// The name of the package to test - #[clap(long, conflicts_with = "workspace")] - package: Option<CrateName>, - - /// Test all packages in the workspace - #[clap(long, conflicts_with = "package")] - workspace: bool, + #[clap(flatten)] + pub(super) package_options: PackageOptions, #[clap(flatten)] compile_options: CompileOptions, @@ -53,9 +46,7 @@ pub(crate) struct TestCommand { pub(crate) fn run(args: TestCommand, config: NargoConfig) -> Result<(), CliError> { let toml_path = get_package_manifest(&config.program_dir)?; - let default_selection = - if args.workspace { PackageSelection::All } else { PackageSelection::DefaultOrAll }; - let selection = args.package.map_or(default_selection, PackageSelection::Selected); + let selection = args.package_options.package_selection(); let workspace = resolve_workspace_from_toml( &toml_path, selection, diff --git a/tooling/nargo_toml/Cargo.toml b/tooling/nargo_toml/Cargo.toml index 2bc24153836..f5f7d7cd595 100644 --- a/tooling/nargo_toml/Cargo.toml +++ b/tooling/nargo_toml/Cargo.toml @@ -25,4 +25,5 @@ noirc_driver.workspace = true semver = "1.0.20" [dev-dependencies] +tempfile.workspace = true test-case.workspace = true diff --git a/tooling/nargo_toml/src/lib.rs b/tooling/nargo_toml/src/lib.rs index c1990dab4a6..b5c45977618 100644 --- a/tooling/nargo_toml/src/lib.rs +++ b/tooling/nargo_toml/src/lib.rs @@ -47,6 +47,35 @@ pub fn find_file_manifest(current_path: &Path) -> Option<PathBuf> { } /// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path]. +/// When `workspace` is `true` it returns the topmost directory, when `false` the innermost one. +/// +/// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file. +pub fn find_root(current_path: &Path, workspace: bool) -> Result<PathBuf, ManifestError> { + if workspace { + find_package_root(current_path) + } else { + find_file_root(current_path) + } +} + +/// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path], +/// returning at the innermost directory found, i.e. the one corresponding to the package that contains the `current_path`. +/// +/// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file. +pub fn find_file_root(current_path: &Path) -> Result<PathBuf, ManifestError> { + match find_file_manifest(current_path) { + Some(manifest_path) => { + let package_root = manifest_path + .parent() + .expect("infallible: manifest file path can't be root directory"); + Ok(package_root.to_path_buf()) + } + None => Err(ManifestError::MissingFile(current_path.to_path_buf())), + } +} + +/// Returns the [PathBuf] of the directory containing the `Nargo.toml` by searching from `current_path` to the root of its [Path], +/// returning at the topmost directory found, i.e. the one corresponding to the entire workspace. /// /// Returns a [ManifestError] if no parent directories of `current_path` contain a manifest file. pub fn find_package_root(current_path: &Path) -> Result<PathBuf, ManifestError> { @@ -60,6 +89,11 @@ pub fn find_package_root(current_path: &Path) -> Result<PathBuf, ManifestError> } // TODO(#2323): We are probably going to need a "filepath utils" crate soon +/// Get the root of path, for example: +/// * `C:\foo\bar` -> `C:\foo` +/// * `//shared/foo/bar` -> `//shared/foo` +/// * `/foo` -> `/foo` +/// otherwise empty path. fn path_root(path: &Path) -> PathBuf { let mut components = path.components(); @@ -101,6 +135,7 @@ pub fn find_package_manifest( }) } } + /// Returns the [PathBuf] of the `Nargo.toml` file in the `current_path` directory. /// /// Returns a [ManifestError] if `current_path` does not contain a manifest file. @@ -490,9 +525,20 @@ pub fn resolve_workspace_from_toml( Ok(workspace) } -#[test] -fn parse_standard_toml() { - let src = r#" +#[cfg(test)] +mod tests { + use std::{ + path::{Path, PathBuf}, + str::FromStr, + }; + + use test_case::test_matrix; + + use crate::{find_root, Config, ManifestError}; + + #[test] + fn parse_standard_toml() { + let src = r#" [package] name = "test" @@ -505,49 +551,49 @@ fn parse_standard_toml() { hello = {path = "./noir_driver"} "#; - assert!(Config::try_from(String::from(src)).is_ok()); - assert!(Config::try_from(src).is_ok()); -} + assert!(Config::try_from(String::from(src)).is_ok()); + assert!(Config::try_from(src).is_ok()); + } -#[test] -fn parse_package_toml_no_deps() { - let src = r#" + #[test] + fn parse_package_toml_no_deps() { + let src = r#" [package] name = "test" authors = ["kev", "foo"] compiler_version = "*" "#; - assert!(Config::try_from(String::from(src)).is_ok()); - assert!(Config::try_from(src).is_ok()); -} + assert!(Config::try_from(String::from(src)).is_ok()); + assert!(Config::try_from(src).is_ok()); + } -#[test] -fn parse_workspace_toml() { - let src = r#" + #[test] + fn parse_workspace_toml() { + let src = r#" [workspace] members = ["a", "b"] "#; - assert!(Config::try_from(String::from(src)).is_ok()); - assert!(Config::try_from(src).is_ok()); -} + assert!(Config::try_from(String::from(src)).is_ok()); + assert!(Config::try_from(src).is_ok()); + } -#[test] -fn parse_workspace_default_member_toml() { - let src = r#" + #[test] + fn parse_workspace_default_member_toml() { + let src = r#" [workspace] members = ["a", "b"] default-member = "a" "#; - assert!(Config::try_from(String::from(src)).is_ok()); - assert!(Config::try_from(src).is_ok()); -} + assert!(Config::try_from(String::from(src)).is_ok()); + assert!(Config::try_from(src).is_ok()); + } -#[test] -fn parse_package_expression_width_toml() { - let src = r#" + #[test] + fn parse_package_expression_width_toml() { + let src = r#" [package] name = "test" version = "0.1.0" @@ -556,6 +602,124 @@ fn parse_package_expression_width_toml() { expression_width = "3" "#; - assert!(Config::try_from(String::from(src)).is_ok()); - assert!(Config::try_from(src).is_ok()); + assert!(Config::try_from(String::from(src)).is_ok()); + assert!(Config::try_from(src).is_ok()); + } + + /// Test that `find_root` handles all kinds of prefixes. + /// (It dispatches based on `workspace` to methods which handle paths differently). + #[test_matrix( + [true, false], + ["C:\\foo\\bar", "//shared/foo/bar", "/foo/bar", "bar/baz", ""] + )] + fn test_find_root_does_not_panic(workspace: bool, path: &str) { + let path = PathBuf::from_str(path).unwrap(); + let error = find_root(&path, workspace).expect_err("non-existing paths"); + assert!(matches!(error, ManifestError::MissingFile(_))); + } + + /// Test to demonstrate how `find_root` works. + #[test] + fn test_find_root_example() { + const INDENT_SIZE: usize = 4; + /// Create directories and files according to a YAML-like layout below + fn setup(layout: &str, root: &Path) { + fn is_dir(item: &str) -> bool { + !item.contains('.') + } + let mut current_dir = root.to_path_buf(); + let mut current_indent = 0; + let mut last_item: Option<String> = None; + + for line in layout.lines() { + if let Some((prefix, item)) = line.split_once('-') { + let item = item.replace(std::path::MAIN_SEPARATOR, "_").trim().to_string(); + + let indent = prefix.len() / INDENT_SIZE; + + if last_item.is_none() { + current_indent = indent; + } + + assert!( + indent <= current_indent + 1, + "cannot increase indent by more than {INDENT_SIZE}; item = {item}, current_dir={}", current_dir.display() + ); + + // Go into the last created directory + if indent > current_indent && last_item.is_some() { + let last_item = last_item.unwrap(); + assert!(is_dir(&last_item), "last item was not a dir: {last_item}"); + current_dir.push(last_item); + current_indent += 1; + } + // Go back into an ancestor directory + while indent < current_indent { + current_dir.pop(); + current_indent -= 1; + } + // Create a file or a directory + let item_path = current_dir.join(&item); + if is_dir(&item) { + std::fs::create_dir(&item_path).unwrap_or_else(|e| { + panic!("failed to create dir {}: {e}", item_path.display()) + }); + } else { + std::fs::write(&item_path, "").expect("failed to create file"); + } + + last_item = Some(item); + } + } + } + + // Temporary directory to hold the project. + let tmp = tempfile::tempdir().unwrap(); + // Join a string path to the tmp dir + let path = |p: &str| tmp.path().join(p); + // Check that an expected root is found + let assert_ok = |current_dir: &str, ws: bool, exp: &str| { + let root = find_root(&path(current_dir), ws).expect("should find a root"); + assert_eq!(root, path(exp)); + }; + // Check that a root is not found + let assert_err = |current_dir: &str| { + find_root(&path(current_dir), true).expect_err("shouldn't find a root"); + }; + + let layout = r" + - project + - docs + - workspace + - packages + - foo + - Nargo.toml + - Prover.toml + - src + - main.nr + - bar + - Nargo.toml + - src + - lib.nr + - Nargo.toml + - examples + - baz + - Nargo.toml + - src + - main.nr + "; + + // Set up the file system. + setup(layout, tmp.path()); + + assert_err("dummy"); + assert_err("project/docs"); + assert_err("project/examples"); + assert_ok("project/workspace", true, "project/workspace"); + assert_ok("project/workspace", false, "project/workspace"); + assert_ok("project/workspace/packages/foo", true, "project/workspace"); + assert_ok("project/workspace/packages/bar", false, "project/workspace/packages/bar"); + assert_ok("project/examples/baz/src", true, "project/examples/baz"); + assert_ok("project/examples/baz/src", false, "project/examples/baz"); + } }