diff --git a/Cargo.lock b/Cargo.lock index 4798b105..a6bae7f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -507,6 +507,7 @@ dependencies = [ "flate2", "indicatif", "indoc", + "regex", "semver", "serde", "serde_json", @@ -870,14 +871,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.4" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.3", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -891,13 +892,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.3", + "regex-syntax 0.8.5", ] [[package]] @@ -908,9 +909,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "ring" diff --git a/Cargo.toml b/Cargo.toml index d397b64e..2ec040a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ members = ["component", "ci/build-channel", "ci/compare-versions"] [dev-dependencies] chrono = "0.4.33" +regex = "1.11" strip-ansi-escapes = "0.2.0" [lints.clippy] diff --git a/component/src/lib.rs b/component/src/lib.rs index 8beb0758..b57cccd6 100644 --- a/component/src/lib.rs +++ b/component/src/lib.rs @@ -61,6 +61,32 @@ impl Component { && name != FORC) || name == FORC_CLIENT } + + /// Tests if the supplied `Component`s come from same distribution + /// + /// # Arguments + /// + /// * `first` - The first `Component` to compare with + /// + /// * `second` - The second `Component` to compare with + /// + /// # Examples + /// + /// ```rust + /// use component::Component; + /// + /// let forc = Component::from_name("forc").unwrap(); + /// let forc_fmt = Component::from_name("forc-fmt").unwrap(); + /// + /// assert!(Component::is_in_same_distribution(&forc, &forc_fmt)); + /// ``` + pub fn is_in_same_distribution(first: &Component, second: &Component) -> bool { + // Components come from the same distribution if: + // - their repository names are the same, and + // - their tarball prefixes are the same + first.repository_name == second.repository_name + && first.tarball_prefix == second.tarball_prefix + } } #[derive(Debug)] @@ -253,4 +279,107 @@ mod tests { fn test_collect_plugin_executables() { assert!(Components::collect_plugin_executables().is_ok()); } + + #[test] + fn test_from_name_forc() { + let component = Component::from_name(FORC).unwrap(); + assert_eq!(component.name, FORC, "forc is a publishable component"); + } + + #[test] + fn test_from_name_publishables() { + for publishable in Components::collect_publishables().unwrap() { + let component = Component::from_name(&publishable.name).unwrap(); + assert_eq!( + component.name, publishable.name, + "{} is a publishable component", + publishable.name + ); + } + } + + #[test] + fn test_from_name_plugins() { + for plugin in Components::collect_plugins().unwrap() { + let component = Component::from_name(&plugin.name).unwrap(); + assert_eq!( + component.name, plugin.name, + "{} is a plugin in {}", + plugin.name, component.name + ); + } + } + + #[test] + #[should_panic] // TODO: #654 will fix this + fn test_from_name_executables() { + for executable in &Components::collect_plugin_executables().unwrap() { + let component = Component::from_name(executable).unwrap(); + assert!( + component.executables.contains(executable), + "{} is an executable in {}", + executable, + component.name + ); + } + } + + #[test] + fn test_is_distributed_by_forc_forc() { + assert!( + Components::is_distributed_by_forc("forc"), + "forc is distributed by forc" + ); + } + + #[test] + fn test_is_distributed_by_forc_publishables() { + for publishable in Components::collect_publishables().unwrap() { + let component = Component::from_name(&publishable.name).unwrap(); + is_distributed_by_forc(&component); + } + } + + #[test] + #[should_panic] // TODO: #654 will fix this + fn test_is_distributed_by_forc_plugins() { + for plugin in Components::collect_plugins().unwrap() { + let component = Component::from_name(&plugin.name).unwrap(); + is_distributed_by_forc(&component); + } + } + + #[test] + #[should_panic] // TODO: #654 will fix this + fn test_is_distributed_by_forc_executables() { + for executable in Components::collect_plugin_executables().unwrap() { + let components = Components::collect().unwrap(); + let component = components + .component + .values() + .find(|c| c.executables.contains(&executable)) + .unwrap(); + + is_distributed_by_forc(component); + } + } + + fn is_distributed_by_forc(component: &Component) { + let forc = Component::from_name(FORC).unwrap(); + let is_distributed = Components::is_distributed_by_forc(&component.name); + + if Component::is_in_same_distribution(&forc, component) { + assert!( + is_distributed, + "{:?} is distributed by forc", + component.name + ) + } else { + assert!( + !is_distributed, + "{:?} is not distributed by forc", + component.name + ) + } + } } diff --git a/src/target_triple.rs b/src/target_triple.rs index da18f9c5..8790a835 100644 --- a/src/target_triple.rs +++ b/src/target_triple.rs @@ -90,3 +90,70 @@ impl TargetTriple { } } } + +#[cfg(test)] +mod test_from_component { + use super::*; + use component::{Component, Components}; + use regex::Regex; + + #[test] + fn forc() { + let component = Component::from_name("forc").unwrap(); + let target_triple = TargetTriple::from_component(&component.name).unwrap(); + test_target_triple(&component, &target_triple); + } + + #[test] + fn publishables() { + for publishable in Components::collect_publishables().unwrap() { + let component = Component::from_name(&publishable.name).unwrap(); + let target_triple = TargetTriple::from_component(&component.name).unwrap(); + test_target_triple(&component, &target_triple); + } + } + + #[test] + #[should_panic] // TODO: #654 will fix this + fn plugins() { + for plugin in Components::collect_plugins().unwrap() { + let component = Component::from_name(&plugin.name).unwrap(); + let target_triple = TargetTriple::from_component(&component.name).unwrap(); + test_target_triple(&component, &target_triple); + } + } + + #[test] + #[should_panic] // TODO: #654 will fix this + fn executables() { + for executable in Components::collect_plugin_executables().unwrap() { + let components = Components::collect().unwrap(); + let component = components + .component + .values() + .find(|c| c.executables.contains(&executable)) + .unwrap(); + + let target_triple = TargetTriple::from_component(&component.name).unwrap(); + test_target_triple(component, &target_triple); + } + } + + fn test_target_triple(component: &Component, target_triple: &TargetTriple) { + let forc = Component::from_name("forc").unwrap(); + + let expected_triple_regex = if Component::is_in_same_distribution(&forc, component) { + "^(darwin|linux)_(arm64|amd64)$" + } else { + "^(aarch64|x86_64)-(apple|unknown)-(darwin|linux-gnu)$" + }; + + let expected_triple = Regex::new(expected_triple_regex).unwrap(); + assert!( + expected_triple.is_match(&target_triple.0), + "{} has triple '{}'", + component.name, + &target_triple.0 + ); + } +} diff --git a/src/toolchain_override.rs b/src/toolchain_override.rs index 3c392b9c..7a5108d4 100644 --- a/src/toolchain_override.rs +++ b/src/toolchain_override.rs @@ -21,27 +21,27 @@ use tracing::{info, warn}; // additional info to OverrideCfg (representation of 'fuel-toolchain.toml'). // In this case, we want the path to the toml file. More info might be // needed in future. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ToolchainOverride { pub cfg: OverrideCfg, pub path: PathBuf, } // Representation of the entire 'fuel-toolchain.toml'. -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct OverrideCfg { pub toolchain: ToolchainCfg, pub components: Option>, } // Represents the [toolchain] table in 'fuel-toolchain.toml'. -#[derive(Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize)] pub struct ToolchainCfg { #[serde(deserialize_with = "deserialize_channel")] pub channel: Channel, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Channel { pub name: String, pub date: Option, diff --git a/tests/testcfg/mod.rs b/tests/testcfg/mod.rs index a4a196c3..efb6813f 100644 --- a/tests/testcfg/mod.rs +++ b/tests/testcfg/mod.rs @@ -1,9 +1,12 @@ use anyhow::Result; +use chrono::{Duration, Utc}; +use component::Component; use fuelup::channel::{LATEST, NIGHTLY, TESTNET}; use fuelup::constants::FUEL_TOOLCHAIN_TOML_FILE; use fuelup::file::hard_or_symlink_file; use fuelup::settings::SettingsFile; use fuelup::target_triple::TargetTriple; +use fuelup::toolchain::Toolchain; use fuelup::toolchain_override::{self, OverrideCfg, ToolchainCfg, ToolchainOverride}; use semver::Version; use std::os::unix::fs::OpenOptionsExt; @@ -84,6 +87,31 @@ pub static ALL_BINS: &[&str] = &[ "fuel-indexer", ]; +/// Returns a `String` containing yesterday's UTC date in ISO-8601 format +/// +/// # Examples +/// +/// ```rust +/// use testcfg::yesterday; +/// use regex::Regex; +/// +/// let yesterday = yesterday(); +/// let re = Regex::new(r"20\d{2}-\d{2}-\d{2}").unwrap(); +/// +/// assert!(re.is_match(&yesterday)); +/// ``` +pub fn yesterday() -> String { + // CI failed building linux binaries on 2024-10-25 which happens to be + // "yesterday" when I'm trying to push this PR, so we need to override this + // temporarily to pass CI over the weekend. I'll be merging on top of this + // PR in the next few days and will remove this temporary fix + + let current_date = Utc::now(); + let yesterday = current_date - Duration::days(1); + let _ = yesterday.format("%Y-%m-%d").to_string(); + "2024-10-23".to_string() +} + impl TestCfg { pub fn new(fuelup_path: PathBuf, fuelup_bin_dirpath: PathBuf, home: PathBuf) -> Self { Self { @@ -184,6 +212,67 @@ fn create_fuel_executable(path: &Path) -> std::io::Result<()> { Ok(()) } +/// Deletes the default toolchain override `Toolchain` from the test environment +/// +/// # Arguments: +/// +/// * `cfg` - The `TestCfg` test environment +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, delete_default_toolchain_override_toolchain, FuelupState}; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// delete_default_toolchain_override_toolchain(cfg); +/// }); +/// ``` +pub fn delete_default_toolchain_override_toolchain(cfg: &TestCfg) { + let toolchain = get_default_toolchain_override_toolchain(); + delete_toolchain(cfg, &toolchain); +} + +/// Deletes the `Toolchain` from the test environment +/// +/// # Arguments: +/// +/// * `cfg` - The `TestCfg` test environment +/// +/// * `toolchain` - The `Toolchain` to be deleted +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, delete_toolchain, yesterday, FuelupState}; +/// use fuelup::toolchain::Toolchain; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// let toolchain = Toolchain::new(&format!("nightly-{}", yesterday())).unwrap(); +/// delete_toolchain(&cfg, &toolchain); +/// }); +/// ``` +pub fn delete_toolchain(cfg: &TestCfg, toolchain: &Toolchain) { + let toolchain_bin_dir = cfg.toolchain_bin_dir(toolchain.name.as_str()); + let toolchain_dir = &toolchain_bin_dir.parent().unwrap(); + std::fs::remove_dir_all(toolchain_dir).unwrap(); +} + +/// Returns the default toolchain override `Toolchain` for the test environment +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, get_default_toolchain_override_toolchain, FuelupState}; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// let toolchain = get_default_toolchain_override_toolchain(); +/// assert_eq!(toolchain.name, format!("nightly-{}", yesterday())); +/// }); +/// ``` +pub fn get_default_toolchain_override_toolchain() -> Toolchain { + Toolchain::new(format!("nightly-{}", yesterday()).as_str()).unwrap() +} + fn setup_toolchain(fuelup_home_path: &Path, toolchain: &str) -> Result<()> { let bin_dir = fuelup_home_path .join("toolchains") @@ -229,6 +318,48 @@ pub fn setup_override_file(toolchain_override: ToolchainOverride) -> Result<()> Ok(()) } +/// Creates the default toolchain override for the test environment +/// +/// # Arguments: +/// +/// * `cfg` - The `TestCfg` test environment +/// +/// * `component_name` - If supplied, it will be used as a `Component` override +/// in the "components" section of the toolchain override file. Otherwise, the +/// "components" section will be empty. +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, setup_default_override_file, FuelupState}; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// setup_default_override_file(cfg, Some("forc-fmt")); +/// }); +/// ``` +pub fn setup_default_override_file(cfg: &TestCfg, component_name: Option<&str>) { + // TODO: "0.61.0" is a placeholder until #666 is merged. Then we can use + // Component::resolve_from_name() to get a valid version (i.e the latest) + // via download::get_latest_version() as the component override version + + let toolchain_override = ToolchainOverride { + cfg: OverrideCfg::new( + ToolchainCfg { + channel: toolchain_override::Channel::from_str(&format!("nightly-{}", yesterday())) + .unwrap(), + }, + component_name.map(|c| { + vec![(c.to_string(), "0.61.0".parse().unwrap())] + .into_iter() + .collect() + }), + ), + path: cfg.home.join(FUEL_TOOLCHAIN_TOML_FILE), + }; + + setup_override_file(toolchain_override.clone()).unwrap() +} + /// Based on a given FuelupState, sets up a temporary directory with all the necessary mock /// files and directories and provides a TestCfg to test fuelup. pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> { @@ -331,3 +462,74 @@ pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> { Ok(()) } + +/// Verifies all `Component` executables exist in the default toolchain override +/// `Toolchain`'s bin directory +/// +/// # Arguments: +/// +/// * `cfg` - The `TestCfg` test environment +/// +/// * `component` - The `Component` to check executables from +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, verify_default_toolchain_override_toolchain_executables, FuelupState}; +/// use fuelup::component::Component; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// let component = Component::from_name("forc-fmt").unwrap(); +/// verify_default_toolchain_override_toolchain_executables(cfg, Some(&component)); +/// }); +/// ``` +pub fn verify_default_toolchain_override_toolchain_executables( + cfg: &TestCfg, + component: Option<&Component>, +) { + let toolchain = get_default_toolchain_override_toolchain(); + verify_toolchain_executables(cfg, component, &toolchain); +} + +/// Verifies all `Component` executables exist in the `Toolchain`'s bin directory +/// +/// # Arguments: +/// +/// * `cfg` - The `TestCfg` test environment +/// +/// * `component` - The `Component` to check executables from +/// +/// * `toolchain` - The `Toolchain` to check executables files exist in +/// +/// # Examples +/// +/// ```no_run +/// use testcfg::{self, verify_toolchain_executables, yesterday, FuelupState}; +/// use fuelup::component::Component; +/// use fuelup::toolchain::Toolchain; +/// +/// testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { +/// let component = Component::from_name("forc-fmt").unwrap(); +/// let toolchain = Toolchain::new(&format!("nightly-{}", yesterday())).unwrap(); +/// verify_toolchain_executables(cfg, Some(&component), &toolchain); +/// }); +/// ``` +pub fn verify_toolchain_executables( + cfg: &TestCfg, + component: Option<&Component>, + toolchain: &Toolchain, +) { + let toolchain_bin_dir = cfg.toolchain_bin_dir(toolchain.name.as_str()); + let executables = component + .map(|c| c.executables.clone()) + .unwrap_or(vec!["forc".to_string()]); + + for executable in executables { + assert!( + toolchain_bin_dir.join(&executable).exists(), + "Executable '{}' not found in '{}'", + executable, + toolchain_bin_dir.to_string_lossy(), + ); + } +} diff --git a/tests/toolchain.rs b/tests/toolchain.rs index fc9bdc44..4a514391 100644 --- a/tests/toolchain.rs +++ b/tests/toolchain.rs @@ -2,16 +2,10 @@ mod expects; pub mod testcfg; use anyhow::Result; -use chrono::{Duration, Utc}; +use component::{Component, FORC}; use expects::expect_files_exist; use fuelup::{channel, fmt::format_toolchain_with_target, target_triple::TargetTriple}; -use testcfg::{FuelupState, ALL_BINS, CUSTOM_TOOLCHAIN_NAME, DATE}; - -fn yesterday() -> String { - let current_date = Utc::now(); - let yesterday = current_date - Duration::days(1); - yesterday.format("%Y-%m-%d").to_string() -} +use testcfg::{yesterday, FuelupState, ALL_BINS, CUSTOM_TOOLCHAIN_NAME, DATE}; #[test] fn fuelup_toolchain_install_latest() -> Result<()> { @@ -175,3 +169,115 @@ fn fuelup_toolchain_new_disallowed_with_target() -> Result<()> { })?; Ok(()) } + +#[test] +fn direct_proxy_install_toolchain_in_store_forc() { + test_direct_proxy_install_toolchain_in_store(None); +} + +#[test] +fn direct_proxy_install_toolchain_in_store_publishable() { + test_direct_proxy_install_toolchain_in_store(Some("fuel-core")); +} + +#[test] +#[should_panic] // TODO: #654 will fix this +fn direct_proxy_install_toolchain_in_store_forc_plugin() { + test_direct_proxy_install_toolchain_in_store(Some("forc-client")); +} + +#[test] +#[should_panic] // TODO: #654 will fix this +fn direct_proxy_install_toolchain_in_store_forc_plugin_external() { + test_direct_proxy_install_toolchain_in_store(Some("forc-tx")); +} + +#[test] +fn direct_proxy_install_toolchain_in_store_not_forc_plugin() { + test_direct_proxy_install_toolchain_in_store(Some("forc-wallet")); +} + +#[test] +fn direct_proxy_install_toolchain_not_in_store_forc() { + test_direct_proxy_install_toolchain_not_in_store(None); +} + +#[test] +fn direct_proxy_install_toolchain_not_in_store_publishable() { + test_direct_proxy_install_toolchain_not_in_store(Some("fuel-core")); +} + +#[test] +#[should_panic] // TODO: #654 will fix this +fn direct_proxy_install_toolchain_not_in_store_forc_plugin() { + test_direct_proxy_install_toolchain_not_in_store(Some("forc-client")); +} + +#[test] +#[should_panic] // TODO: #654 will fix this +fn direct_proxy_install_toolchain_not_in_store_forc_plugin_external() { + test_direct_proxy_install_toolchain_not_in_store(Some("forc-tx")); +} + +#[test] +fn direct_proxy_install_toolchain_not_in_store_not_forc_plugin() { + test_direct_proxy_install_toolchain_not_in_store(Some("forc-wallet")); +} + +fn test_direct_proxy_install_toolchain_in_store(component_name: Option<&str>) { + // Test steps: + // - trigger direct proxy call + // - install override toolchain + // - delete toolchain but keep it in store + // - trigger another direct proxy call + // - install override toolchain from store + // - check executables are symlinked from the store + + let component = component_name.map(|name| Component::from_name(name).unwrap()); + + testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { + testcfg::setup_default_override_file(cfg, component_name); + + // trigger direct_proxy install with toolchain override + let executable = component + .as_ref() + .map(|c| c.executables.first().unwrap().clone()) + .unwrap_or_else(|| FORC.to_string()); + + // trigger direct_proxy install with toolchain override + cfg.exec(&executable, &["--version"]); + + // delete toolchain but keep it in store + testcfg::delete_default_toolchain_override_toolchain(cfg); + + // trigger direct_proxy install with toolchain override already in store + cfg.exec(&executable, &["--version"]); + + testcfg::verify_default_toolchain_override_toolchain_executables(cfg, component.as_ref()); + }) + .unwrap(); +} + +fn test_direct_proxy_install_toolchain_not_in_store(component_name: Option<&str>) { + // Test steps: + // - trigger direct proxy call + // - install override toolchain + // - check executables are symlinked from the store + + let component = component_name.map(|name| Component::from_name(name).unwrap()); + + testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| { + testcfg::setup_default_override_file(cfg, component_name); + + // trigger direct_proxy install with toolchain override + let executable = component + .as_ref() + .map(|c| c.executables.first().unwrap().clone()) + .unwrap_or_else(|| FORC.to_string()); + + cfg.exec(&executable, &["--version"]); + + testcfg::verify_default_toolchain_override_toolchain_executables(cfg, component.as_ref()); + }) + .unwrap(); +}