Repository: FuelLabs/fuelup (rust)

Issue:
support installing `nightly` from a specific date
#167 will allow downloads of the latest nightly but not with specific dates. We should be able to do `fuelup toolchain install nightly-<date>`.

Pull request:
nightly specify date
Closes #169 
Follow up to #167 

Allows you to install a specific date of nightly. Specifying the `TargetTriple` is still disallowed (automatically sourced from the running system's OS).

Commands that should work:
- [x]  `fuelup toolchain install nightly-2022-08-31 `
- [x]  `fuelup default nightly-2022-08-31`
- [x] `fuelup toolchain uninstall nightly-2022-08-31`

In addition to the above,
`fuelup check` should not check updates for date-specific versions
You should still not be able to add/remove components from date specific nightlies

This is not ready for review just yet - would like to look through the code again tomorrow and add more tests.

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index fef97da1..56e93db8 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -770,6 +770,7 @@ version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b"
 dependencies = [
+ "itoa",
  "libc",
  "num_threads",
  "time-macros",
diff --git a/Cargo.toml b/Cargo.toml
index 07d337f0..8d65b31f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,7 +30,7 @@ sha2 = "0.10"
 tar = "0.4"
 tempfile = "3"
 termcolor = "1"
-time = { version = "0.3", features = ["macros", "parsing"] }
+time = { version = "0.3", features = ["macros", "parsing", "formatting"] }
 toml_edit = { version = "0.13", features = ["serde", "easy"] }
 tracing = "0.1"
 tracing-subscriber = { version = "0.3", features = ["ansi", "env-filter", "json"] }
diff --git a/src/channel.rs b/src/channel.rs
index 224fa6b7..74c29448 100644
--- a/src/channel.rs
+++ b/src/channel.rs
@@ -1,5 +1,8 @@
 use crate::{
-    constants::{CHANNEL_LATEST_FILE_NAME, CHANNEL_NIGHTLY_FILE_NAME, FUELUP_GH_PAGES},
+    constants::{
+        CHANNEL_LATEST_FILE_NAME, CHANNEL_NIGHTLY_FILE_NAME, DATE_FORMAT_URL_FRIENDLY,
+        FUELUP_GH_PAGES,
+    },
     download::{download, DownloadCfg},
     toolchain::{DistToolchainName, OfficialToolchainDescription},
 };
@@ -40,7 +43,15 @@ impl Channel {
             DistToolchainName::Latest => CHANNEL_LATEST_FILE_NAME,
             DistToolchainName::Nightly => CHANNEL_NIGHTLY_FILE_NAME,
         };
-        let channel_url = FUELUP_GH_PAGES.to_owned() + channel_file_name;
+
+        let mut channel_url = FUELUP_GH_PAGES.to_owned();
+        if desc.name == DistToolchainName::Nightly && desc.date.is_some() {
+            channel_url.push_str("channels/nightly/");
+            channel_url.push_str(&desc.date.unwrap().format(DATE_FORMAT_URL_FRIENDLY)?);
+            channel_url.push('/');
+        }
+
+        channel_url.push_str(channel_file_name);
         let mut hasher = Sha256::new();
         let toml = match download(&channel_url, &mut hasher) {
             Ok(t) => String::from_utf8(t)?,
diff --git a/src/commands/default.rs b/src/commands/default.rs
index a3f0bee0..78589d09 100644
--- a/src/commands/default.rs
+++ b/src/commands/default.rs
@@ -26,10 +26,10 @@ pub fn exec(command: DefaultCommand) -> Result<()> {
         }
     };
 
-    let mut new_default = Toolchain::from(&toolchain)?;
+    let mut new_default = Toolchain::from_path(&toolchain)?;
 
     if let Ok(description) = OfficialToolchainDescription::from_str(&toolchain) {
-        new_default = Toolchain::from(&description.to_string())?;
+        new_default = Toolchain::from_path(&description.to_string())?;
     }
 
     if !new_default.exists() {
diff --git a/src/config.rs b/src/config.rs
index 49f6fd4c..b85b399e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -5,6 +5,7 @@ use anyhow::Result;
 use std::io;
 
 use crate::path::toolchains_dir;
+use crate::target_triple::TargetTriple;
 use crate::toolchain::RESERVED_TOOLCHAIN_NAMES;
 
 pub struct Config {
@@ -20,11 +21,13 @@ impl Config {
 
     pub(crate) fn list_toolchains(&self) -> Result<Vec<String>> {
         if self.toolchains_dir.is_dir() {
-            let toolchains: Vec<String> = fs::read_dir(&self.toolchains_dir)?
+            let mut toolchains: Vec<String> = fs::read_dir(&self.toolchains_dir)?
                 .filter_map(io::Result::ok)
                 .filter(|e| e.file_type().map(|f| f.is_dir()).unwrap_or(false))
                 .map(|e| e.file_name().into_string().ok().unwrap_or_default())
                 .collect();
+            toolchains.sort();
+
             Ok(toolchains)
         } else {
             Ok(Vec::new())
@@ -37,9 +40,14 @@ impl Config {
                 .filter_map(io::Result::ok)
                 .filter(|e| {
                     e.file_type().map(|f| f.is_dir()).unwrap_or(false)
-                        && RESERVED_TOOLCHAIN_NAMES
-                            .iter()
-                            .any(|t| e.file_name().to_string_lossy().starts_with(t))
+                        && RESERVED_TOOLCHAIN_NAMES.iter().any(|t| {
+                            e.file_name().to_string_lossy()
+                                == format!(
+                                    "{}-{}",
+                                    t,
+                                    &TargetTriple::from_host().unwrap_or_default().to_string()
+                                )
+                        })
                 })
                 .map(|e| e.file_name().into_string().ok().unwrap_or_default())
                 .collect();
diff --git a/src/constants.rs b/src/constants.rs
index ba3774c1..97bfd5a5 100644
--- a/src/constants.rs
+++ b/src/constants.rs
@@ -8,3 +8,4 @@ pub const CHANNEL_LATEST_FILE_NAME: &str = "channel-fuel-latest.toml";
 pub const CHANNEL_NIGHTLY_FILE_NAME: &str = "channel-fuel-nightly.toml";
 
 pub const DATE_FORMAT: &[FormatItem] = format_description!("[year]-[month]-[day]");
+pub const DATE_FORMAT_URL_FRIENDLY: &[FormatItem] = format_description!("[year]/[month]/[day]");
diff --git a/src/ops/fuelup_toolchain/install.rs b/src/ops/fuelup_toolchain/install.rs
index 41fc872b..aaa364ff 100644
--- a/src/ops/fuelup_toolchain/install.rs
+++ b/src/ops/fuelup_toolchain/install.rs
@@ -11,14 +11,16 @@ use tracing::{error, info};
 pub fn install(command: InstallCommand) -> Result<()> {
     let InstallCommand { name } = command;
 
-    let toolchain = Toolchain::new(&name)?;
     let description = OfficialToolchainDescription::from_str(&name)?;
 
-    let settings = SettingsFile::new(settings_file());
-    settings.with_mut(|s| {
-        s.default_toolchain = Some(toolchain.name.clone());
-        Ok(())
-    })?;
+    let settings_file = settings_file();
+    if !settings_file.exists() {
+        let settings = SettingsFile::new(settings_file);
+        settings.with_mut(|s| {
+            s.default_toolchain = Some(description.to_string());
+            Ok(())
+        })?;
+    }
 
     let mut errored_bins = String::new();
     let mut installed_bins = String::new();
@@ -35,6 +37,8 @@ pub fn install(command: InstallCommand) -> Result<()> {
             .map(|c| c.name.clone() + " ")
             .collect::<String>()
     );
+
+    let toolchain = Toolchain::from_path(&description.to_string())?;
     for cfg in cfgs {
         match toolchain.add_component(cfg) {
             Ok(cfg) => writeln!(installed_bins, "- {} {}", cfg.name, cfg.version)?,
diff --git a/src/ops/fuelup_toolchain/uninstall.rs b/src/ops/fuelup_toolchain/uninstall.rs
index 4b9c30b7..f7c7abd3 100644
--- a/src/ops/fuelup_toolchain/uninstall.rs
+++ b/src/ops/fuelup_toolchain/uninstall.rs
@@ -11,11 +11,11 @@ use crate::{
 pub fn uninstall(command: UninstallCommand) -> Result<()> {
     let UninstallCommand { name } = command;
 
-    let mut toolchain = Toolchain::from(&name)?;
+    let mut toolchain = Toolchain::from_path(&name)?;
 
     if toolchain.is_official() {
         let description = OfficialToolchainDescription::from_str(&name)?;
-        toolchain = Toolchain::from(&description.to_string())?;
+        toolchain = Toolchain::from_path(&description.to_string())?;
     }
 
     if !toolchain.exists() {
diff --git a/src/target_triple.rs b/src/target_triple.rs
index d84c8b5d..e924fbb4 100644
--- a/src/target_triple.rs
+++ b/src/target_triple.rs
@@ -13,13 +13,19 @@ impl fmt::Display for TargetTriple {
 
 impl TargetTriple {
     pub fn new(s: &str) -> Result<Self> {
-        let (architecture, rest) = s.split_once('-').unwrap_or(("", ""));
+        let (architecture, rest) = match s.split_once('-') {
+            Some((architecture, rest)) => (architecture, rest),
+            None => bail!("missing vendor-os specifier"),
+        };
 
         if !["aarch64", "x86_64"].contains(&architecture) {
             bail!("Unsupported architecture: '{}'", architecture);
         }
 
-        let (vendor, os) = rest.split_once('-').unwrap_or(("", ""));
+        let (vendor, os) = match rest.split_once('-') {
+            Some((vendor, os)) => (vendor, os),
+            None => bail!("missing os specifier"),
+        };
 
         if !["apple", "unknown"].contains(&vendor) {
             bail!("Unsupported vendor: '{}'", vendor);
diff --git a/src/toolchain.rs b/src/toolchain.rs
index a0f9236f..93b5833e 100644
--- a/src/toolchain.rs
+++ b/src/toolchain.rs
@@ -60,13 +60,24 @@ pub struct OfficialToolchainDescription {
 }
 
 fn parse_metadata(metadata: String) -> Result<(Option<Date>, Option<TargetTriple>)> {
+    if metadata.is_empty() {
+        return Ok((None, None));
+    }
+
     let (first, second) = metadata.split_at(std::cmp::min(10, metadata.len()));
 
     match Date::parse(first, DATE_FORMAT) {
-        Ok(d) => match TargetTriple::new(second.trim_start_matches('-')) {
-            Ok(t) => Ok((Some(d), Some(t))),
-            Err(_) => Ok((Some(d), None)),
-        },
+        Ok(d) => {
+            if second.is_empty() {
+                Ok((Some(d), None))
+            } else {
+                let target = second.trim_start_matches('-');
+                bail!(
+                    "You specified target '{}': specifying a target is not supported yet.",
+                    target
+                );
+            }
+        }
         Err(_) => match TargetTriple::new(&metadata) {
             Ok(t) => Ok((None, Some(t))),
             Err(_) => bail!("Failed to parse date or target"),
@@ -99,20 +110,15 @@ impl FromStr for OfficialToolchainDescription {
                 date: None,
                 target: None,
             })
-        } else if let Ok((_, _)) = parse_metadata(metadata.to_string()) {
-            bail!(
-                "You may not specify a date or target for official toolchain name '{}' yet.",
-                name
-            );
-
-            // TODO: uncomment once specifying date and target is supported
-            // Ok(Self {
-            //     name: DistToolchainName::from_str(name)?,
-            //     date,
-            //     target,
-            // })
         } else {
-            bail!("Invalid official toolchain name '{}'", s);
+            match parse_metadata(metadata.to_string()) {
+                Ok((date, target)) => Ok(Self {
+                    name: DistToolchainName::from_str(name)?,
+                    date,
+                    target,
+                }),
+                Err(e) => bail!("Invalid toolchain metadata within input '{}' - {}", s, e),
+            }
         }
     }
 }
@@ -135,7 +141,7 @@ impl Toolchain {
         })
     }
 
-    pub fn from(toolchain: &str) -> Result<Self> {
+    pub fn from_path(toolchain: &str) -> Result<Self> {
         Ok(Self {
             name: toolchain.to_string(),
             path: toolchain_dir(toolchain),
@@ -299,8 +305,6 @@ mod tests {
     const TARGET_X86_LINUX: &str = "x86_64-unknown-linux-gnu";
     const TARGET_ARM_LINUX: &str = "aarch64-unknown-linux-gnu";
 
-    const NIGHTLY_DATE: &str = "nightly-2022-08-29";
-
     #[test]
     fn test_parse_name() -> Result<()> {
         for name in [channel::LATEST, channel::NIGHTLY] {
@@ -325,12 +329,12 @@ mod tests {
 
     #[test]
     fn test_parse_nightly_date() -> Result<()> {
-        assert!(OfficialToolchainDescription::from_str(NIGHTLY_DATE).is_err());
+        let toolchain = format!("{}-{}", channel::NIGHTLY.to_owned(), DATE);
+        let desc = OfficialToolchainDescription::from_str(&toolchain).unwrap();
 
-        // TODO: uncomment once specifying date and target is supporting
-        //assert_eq!(desc.name, DistToolchainName::from_str("nightly").unwrap());
-        //assert_eq!(desc.date.unwrap().to_string(), DATE);
-        //assert_eq!(desc.target, None);
+        assert_eq!(desc.name, DistToolchainName::from_str("nightly").unwrap());
+        assert_eq!(desc.date.unwrap().to_string(), DATE);
+        assert_eq!(desc.target, None);
 
         Ok(())
     }
@@ -343,15 +347,16 @@ mod tests {
             TARGET_X86_APPLE,
             TARGET_X86_LINUX,
         ] {
-            let input = channel::NIGHTLY.to_owned() + "-" + DATE + "-" + target;
-            assert!(OfficialToolchainDescription::from_str(&input).is_err());
-            // TODO: uncomment once specifying date and target is supporting
-            //   assert_eq!(
-            //       desc.name,
-            //       DistToolchainName::from_str(channel::NIGHTLY).unwrap()
-            //   );
-            //   assert_eq!(desc.date.unwrap().to_string(), DATE);
-            //   assert_eq!(desc.target.unwrap().to_string(), target);
+            let toolchain = format!("{}-{}-{}", channel::NIGHTLY.to_owned(), DATE, target);
+            assert!(OfficialToolchainDescription::from_str(&toolchain).is_err());
+            // TODO: Uncomment once target specification is supported
+            // see issue #237: https://github.com/FuelLabs/fuelup/issues/237
+            // assert_eq!(
+            //     desc.name,
+            //     DistToolchainName::from_str(channel::NIGHTLY).unwrap()
+            // );
+            // assert_eq!(desc.date.unwrap().to_string(), DATE);
+            // assert_eq!(desc.target.unwrap().to_string(), target);
         }
 
         Ok(())
@@ -367,12 +372,11 @@ mod tests {
         ] {
             for name in [channel::LATEST, channel::NIGHTLY] {
                 let toolchain = name.to_owned() + "-" + target;
-                assert!(OfficialToolchainDescription::from_str(&toolchain).is_err());
+                let desc = OfficialToolchainDescription::from_str(&toolchain).unwrap();
 
-                // TODO: uncomment once specifying date and target is supporting
-                // assert_eq!(desc.name, DistToolchainName::from_str(name).unwrap());
-                // assert!(desc.date.is_none());
-                // assert_eq!(desc.target.unwrap().to_string(), target);
+                assert_eq!(desc.name, DistToolchainName::from_str(name).unwrap());
+                assert!(desc.date.is_none());
+                assert_eq!(desc.target.unwrap().to_string(), target);
             }
         }
 
@@ -388,9 +392,11 @@ mod tests {
 
     #[test]
     fn test_parse_metadata_date_target() -> Result<()> {
-        let (date, target) = parse_metadata(DATE_TARGET_APPLE.to_string())?;
-        assert_eq!(DATE, date.unwrap().to_string());
-        assert_eq!(TARGET_X86_APPLE, target.unwrap().to_string());
+        assert!(parse_metadata(DATE_TARGET_APPLE.to_string()).is_err());
+        // TODO: Uncomment once target specification is supported
+        // see issue #237: https://github.com/FuelLabs/fuelup/issues/237
+        //assert_eq!(DATE, date.unwrap().to_string());
+        //assert_eq!(TARGET_X86_APPLE, target.unwrap().to_string());
         Ok(())
     }
 


Test patch:
diff --git a/tests/commands.rs b/tests/commands.rs
index 0eebe8fe..c3aa0d38 100644
--- a/tests/commands.rs
+++ b/tests/commands.rs
@@ -4,7 +4,7 @@ use std::{env, path::Path};
 
 mod testcfg;
 
-use testcfg::{FuelupState, ALL_BINS};
+use testcfg::{FuelupState, ALL_BINS, DATE};
 
 fn expect_files_exist(dir: &Path, expected: &[&str]) {
     let mut actual: Vec<String> = dir
@@ -73,6 +73,88 @@ fn fuelup_toolchain_install_nightly() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn fuelup_toolchain_install_nightly_date() -> Result<()> {
+    testcfg::setup(FuelupState::Empty, &|cfg| {
+        cfg.fuelup(&["toolchain", "install", "nightly-2022-08-31"]);
+
+        for entry in cfg.toolchains_dir().read_dir().expect("Could not read dir") {
+            let toolchain_dir = entry.unwrap();
+            let expected_toolchain_name =
+                "nightly-2022-08-31-".to_owned() + &TargetTriple::from_host().unwrap().to_string();
+            assert_eq!(
+                expected_toolchain_name,
+                toolchain_dir.file_name().to_str().unwrap()
+            );
+            assert!(toolchain_dir.file_type().unwrap().is_dir());
+
+            expect_files_exist(&toolchain_dir.path().join("bin"), ALL_BINS);
+        }
+    })?;
+
+    Ok(())
+}
+
+#[test]
+fn fuelup_toolchain_install_malformed_date() -> Result<()> {
+    testcfg::setup(FuelupState::Empty, &|cfg| {
+        let toolchain = "nightly-2022-08-31-";
+        let output = cfg.fuelup(&["toolchain", "install", toolchain]);
+
+        let expected_stdout = format!("Invalid toolchain metadata within input '{toolchain}' - You specified target '': specifying a target is not supported yet.\n");
+
+        assert!(output.status.success());
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
+#[test]
+fn fuelup_toolchain_install_date_target_disallowed() -> Result<()> {
+    testcfg::setup(FuelupState::Empty, &|cfg| {
+        let toolchain = "nightly-2022-08-31-x86_64-apple-darwin";
+        let output = cfg.fuelup(&["toolchain", "install", toolchain]);
+
+        let expected_stdout =
+            format!("Invalid toolchain metadata within input '{toolchain}' - You specified target 'x86_64-apple-darwin': specifying a target is not supported yet.\n");
+
+        assert!(output.status.success());
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
+#[test]
+fn fuelup_toolchain_uninstall() -> Result<()> {
+    testcfg::setup(FuelupState::Empty, &|cfg| {
+        let toolchains = ["latest", "nightly", &format!("nightly-{}", DATE)];
+        for toolchain in toolchains {
+            let toolchain_with_target =
+                format!("{}-{}", toolchain, TargetTriple::from_host().unwrap());
+            let output = cfg.fuelup(&["toolchain", "uninstall", toolchain]);
+            let expected_stdout = format!("toolchain '{}' does not exist\n", toolchain_with_target);
+
+            assert_eq!(output.stdout, expected_stdout);
+        }
+    })?;
+
+    testcfg::setup(FuelupState::AllInstalled, &|cfg| {
+        let toolchains = ["latest", "nightly", &format!("nightly-{}", DATE)];
+        for toolchain in toolchains {
+            let toolchain_with_target =
+                format!("{}-{}", toolchain, TargetTriple::from_host().unwrap());
+            let output = cfg.fuelup(&["toolchain", "uninstall", toolchain]);
+            let expected_stdout = format!("toolchain '{}' uninstalled\n", toolchain_with_target);
+
+            assert_eq!(output.stdout, expected_stdout);
+        }
+    })?;
+
+    Ok(())
+}
+
 #[test]
 fn fuelup_check() -> Result<()> {
     testcfg::setup(FuelupState::Empty, &|cfg| {
@@ -85,8 +167,9 @@ fn fuelup_check() -> Result<()> {
 
 #[test]
 fn fuelup_show() -> Result<()> {
-    testcfg::setup(FuelupState::Empty, &|cfg| {
+    testcfg::setup(FuelupState::AllInstalled, &|cfg| {
         cfg.fuelup(&["toolchain", "new", "my_toolchain"]);
+        cfg.fuelup(&["default", "my_toolchain"]);
         let stdout = cfg.fuelup(&["show"]).stdout;
 
         let mut lines = stdout.lines();
@@ -96,9 +179,15 @@ fn fuelup_show() -> Result<()> {
         );
         assert!(lines.next().unwrap().contains("fuelup home: "));
 
-        let expected_stdout = r#"installed toolchains
+        let target = TargetTriple::from_host().unwrap();
+        let expected_stdout = &format!(
+            r#"
+installed toolchains
 --------------------
+latest-{target}
 my_toolchain (default)
+nightly-2022-08-30-{target}
+nightly-{target}
 
 active toolchain
 ----------------
@@ -112,7 +201,8 @@ my_toolchain (default)
     - forc-lsp - not found
     - forc-wallet - not found
   fuel-core - not found
-"#;
+"#
+        );
         assert!(stdout.contains(expected_stdout));
     })?;
     Ok(())
@@ -145,9 +235,30 @@ fn fuelup_default_empty() -> Result<()> {
 
 #[test]
 fn fuelup_default() -> Result<()> {
+    let latest = format!("latest-{}", TargetTriple::from_host().unwrap());
     testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| {
         let output = cfg.fuelup(&["default"]);
-        let expected_stdout = "latest-x86_64-apple-darwin (default)\n";
+        let expected_stdout = format!("{} (default)\n", latest);
+
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
+#[test]
+fn fuelup_default_latest_and_custom() -> Result<()> {
+    testcfg::setup(FuelupState::LatestAndCustomInstalled, &|cfg| {
+        let output = cfg.fuelup(&["default", "latest"]);
+        let expected_stdout = format!(
+            "default toolchain set to 'latest-{}'\n",
+            TargetTriple::from_host().unwrap()
+        );
+
+        assert_eq!(output.stdout, expected_stdout);
+
+        let output = cfg.fuelup(&["default", "my-toolchain"]);
+        let expected_stdout = "default toolchain set to 'my-toolchain'\n";
 
         assert_eq!(output.stdout, expected_stdout);
     })?;
@@ -170,6 +281,43 @@ fn fuelup_default_uninstalled_toolchain() -> Result<()> {
     Ok(())
 }
 
+#[test]
+fn fuelup_default_nightly() -> Result<()> {
+    testcfg::setup(FuelupState::LatestAndNightlyInstalled, &|cfg| {
+        let output = cfg.fuelup(&["default", "nightly"]);
+        let expected_stdout = format!(
+            "default toolchain set to 'nightly-{}'\n",
+            TargetTriple::from_host().unwrap()
+        );
+
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
+#[test]
+fn fuelup_default_nightly_and_nightly_date() -> Result<()> {
+    testcfg::setup(FuelupState::NightlyAndNightlyDateInstalled, &|cfg| {
+        let output = cfg.fuelup(&["default", "nightly"]);
+        let expected_stdout = format!(
+            "default toolchain set to 'nightly-{}'\n",
+            TargetTriple::from_host().unwrap()
+        );
+        assert_eq!(output.stdout, expected_stdout);
+
+        let output = cfg.fuelup(&["default", &format!("nightly-{}", DATE)]);
+        let expected_stdout = format!(
+            "default toolchain set to 'nightly-{}-{}'\n",
+            DATE,
+            TargetTriple::from_host().unwrap()
+        );
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    Ok(())
+}
+
 #[test]
 fn fuelup_toolchain_new() -> Result<()> {
     testcfg::setup(FuelupState::Empty, &|cfg| {
@@ -241,34 +389,98 @@ fn fuelup_component_add_with_version() -> Result<()> {
 
 #[test]
 fn fuelup_component_add_disallowed() -> Result<()> {
+    let latest = format!("latest-{}", TargetTriple::from_host().unwrap());
+    let nightly = format!("nightly-{}", TargetTriple::from_host().unwrap());
+    let nightly_date = format!("nightly-{}-{}", DATE, TargetTriple::from_host().unwrap());
+
     testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| {
         let output = cfg.fuelup(&["component", "add", "forc@0.19.1"]);
-        let expected_stdout = r#"Installing specific components is reserved for custom toolchains.
-You are currently using 'latest-x86_64-apple-darwin'.
+        let expected_stdout = format!(
+            r#"Installing specific components is reserved for custom toolchains.
+You are currently using '{}'.
 
 You may create a custom toolchain using 'fuelup toolchain new <toolchain>'.
-"#;
+"#,
+            latest
+        );
+        assert_eq!(output.stdout, expected_stdout);
+
+        let output = cfg.fuelup(&["component", "add", "fuel-core"]);
         assert_eq!(output.stdout, expected_stdout);
     })?;
 
+    testcfg::setup(FuelupState::NightlyInstalled, &|cfg| {
+        let output = cfg.fuelup(&["component", "add", "forc@.19.1"]);
+        let expected_stdout = format!(
+            r#"Installing specific components is reserved for custom toolchains.
+You are currently using '{}'.
+
+You may create a custom toolchain using 'fuelup toolchain new <toolchain>'.
+"#,
+            nightly
+        );
+        assert_eq!(output.stdout, expected_stdout);
+
+        let output = cfg.fuelup(&["component", "add", "fuel-core"]);
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
+
+    testcfg::setup(FuelupState::NightlyDateInstalled, &|cfg| {
+        let output = cfg.fuelup(&["component", "add", "forc@.19.1"]);
+        let expected_stdout = format!(
+            r#"Installing specific components is reserved for custom toolchains.
+You are currently using '{}'.
+
+You may create a custom toolchain using 'fuelup toolchain new <toolchain>'.
+"#,
+            nightly_date
+        );
+        assert_eq!(output.stdout, expected_stdout);
+
+        let output = cfg.fuelup(&["component", "add", "fuel-core"]);
+        assert_eq!(output.stdout, expected_stdout);
+    })?;
     Ok(())
 }
+
 #[test]
 fn fuelup_component_remove_disallowed() -> Result<()> {
+    let latest = format!("latest-{}", TargetTriple::from_host().unwrap());
+    let nightly_date = format!("nightly-{}-{}", DATE, TargetTriple::from_host().unwrap());
+
     testcfg::setup(FuelupState::LatestToolchainInstalled, &|cfg| {
-        let latest_toolchain_bin_dir = cfg.toolchain_bin_dir("latest-x86_64-apple-darwin");
+        let latest_toolchain_bin_dir = cfg.toolchain_bin_dir(&latest);
 
         expect_files_exist(&latest_toolchain_bin_dir, ALL_BINS);
         let output = cfg.fuelup(&["component", "remove", "forc"]);
 
-        let expected_stdout = r#"Removing specific components is reserved for custom toolchains.
-You are currently using 'latest-x86_64-apple-darwin'.
+        let expected_stdout = format!(
+            r#"Removing specific components is reserved for custom toolchains.
+You are currently using '{latest}'.
 
 You may create a custom toolchain using 'fuelup toolchain new <toolchain>'.
-"#;
+"#
+        );
         assert_eq!(output.stdout, expected_stdout);
         expect_files_exist(&latest_toolchain_bin_dir, ALL_BINS);
     })?;
 
+    testcfg::setup(FuelupState::NightlyDateInstalled, &|cfg| {
+        let latest_toolchain_bin_dir = cfg.toolchain_bin_dir(&nightly_date);
+
+        expect_files_exist(&latest_toolchain_bin_dir, ALL_BINS);
+        let output = cfg.fuelup(&["component", "remove", "forc"]);
+
+        let expected_stdout = format!(
+            r#"Removing specific components is reserved for custom toolchains.
+You are currently using '{}'.
+
+You may create a custom toolchain using 'fuelup toolchain new <toolchain>'.
+"#,
+            nightly_date
+        );
+        assert_eq!(output.stdout, expected_stdout);
+        expect_files_exist(&latest_toolchain_bin_dir, ALL_BINS);
+    })?;
     Ok(())
 }
diff --git a/tests/testcfg.rs b/tests/testcfg.rs
index 661f2f67..403085f6 100644
--- a/tests/testcfg.rs
+++ b/tests/testcfg.rs
@@ -1,15 +1,22 @@
 use anyhow::Result;
 use fuelup::settings::SettingsFile;
+use fuelup::target_triple::TargetTriple;
 use std::{
     env, fs,
-    path::PathBuf,
+    path::{Path, PathBuf},
     process::{Command, ExitStatus},
 };
 use tempfile::tempdir_in;
 
 pub enum FuelupState {
+    AllInstalled,
     Empty,
     LatestToolchainInstalled,
+    NightlyInstalled,
+    NightlyDateInstalled,
+    LatestAndCustomInstalled,
+    LatestAndNightlyInstalled,
+    NightlyAndNightlyDateInstalled,
 }
 
 pub struct TestCfg {
@@ -25,6 +32,8 @@ pub struct TestOutput {
     pub status: ExitStatus,
 }
 
+pub const DATE: &str = "2022-08-30";
+
 pub static ALL_BINS: &[&str] = &[
     "forc",
     "forc-deploy",
@@ -84,6 +93,30 @@ impl TestCfg {
     }
 }
 
+fn setup_toolchain(fuelup_home_path: &Path, toolchain: &str) -> Result<()> {
+    let bin_dir = fuelup_home_path
+        .join("toolchains")
+        .join(toolchain)
+        .join("bin");
+    fs::create_dir_all(&bin_dir).expect("Failed to create temporary latest toolchain bin dir");
+
+    for bin in ALL_BINS {
+        fs::File::create(&bin_dir.join(bin))?;
+    }
+
+    Ok(())
+}
+
+fn setup_settings_file(settings_dir: &Path, default_toolchain: &str) -> Result<()> {
+    let settings_path = settings_dir.join("settings.toml");
+    fs::write(
+        settings_path,
+        format!("default_toolchain = \"{}\"", default_toolchain),
+    )
+    .expect("Failed to copy settings");
+    Ok(())
+}
+
 pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> {
     let root = env::current_exe()
         .unwrap()
@@ -104,27 +137,56 @@ pub fn setup(state: FuelupState, f: &dyn Fn(&mut TestCfg)) -> Result<()> {
         &tmp_fuelup_bin_dir_path.join("fuelup"),
     )?;
 
+    let target = TargetTriple::from_host().unwrap();
+    let latest = format!("latest-{}", target);
+    let nightly = format!("nightly-{}", target);
+
     match state {
         FuelupState::Empty => {}
+        FuelupState::AllInstalled => {
+            setup_toolchain(&tmp_fuelup_root_path, &latest)?;
+            setup_toolchain(&tmp_fuelup_root_path, &nightly)?;
+            setup_toolchain(
+                &tmp_fuelup_root_path,
+                &format!("nightly-{}-{}", DATE, target),
+            )?;
+            setup_settings_file(&tmp_fuelup_root_path, &latest)?;
+        }
         FuelupState::LatestToolchainInstalled => {
-            let bin_dir = tmp_fuelup_root_path
-                .join("toolchains")
-                .join("latest-x86_64-apple-darwin")
-                .join("bin");
-            fs::create_dir_all(&bin_dir)
-                .expect("Failed to create temporary latest toolchain bin dir");
-
-            for bin in ALL_BINS {
-                fs::File::create(&bin_dir.join(bin))?;
-            }
-
-            fs::copy(
-                &env::current_dir()
-                    .unwrap()
-                    .join("tests/settings-example.toml"),
-                &tmp_fuelup_root_path.join("settings.toml"),
-            )
-            .expect("Failed to copy settings");
+            setup_toolchain(&tmp_fuelup_root_path, &latest)?;
+            setup_settings_file(&tmp_fuelup_root_path, &latest)?;
+        }
+        FuelupState::NightlyInstalled => {
+            setup_toolchain(&tmp_fuelup_root_path, &nightly)?;
+            setup_settings_file(&tmp_fuelup_root_path, &nightly)?;
+        }
+        FuelupState::NightlyDateInstalled => {
+            setup_toolchain(
+                &tmp_fuelup_root_path,
+                &format!("nightly-{}-{}", DATE, target),
+            )?;
+            setup_settings_file(
+                &tmp_fuelup_root_path,
+                &format!("nightly-{}-{}", DATE, target),
+            )?;
+        }
+        FuelupState::LatestAndCustomInstalled => {
+            setup_toolchain(&tmp_fuelup_root_path, &latest)?;
+            setup_toolchain(&tmp_fuelup_root_path, "my-toolchain")?;
+            setup_settings_file(&tmp_fuelup_root_path, &latest)?;
+        }
+        FuelupState::LatestAndNightlyInstalled => {
+            setup_toolchain(&tmp_fuelup_root_path, &latest)?;
+            setup_toolchain(&tmp_fuelup_root_path, &nightly)?;
+            setup_settings_file(&tmp_fuelup_root_path, &latest)?;
+        }
+        FuelupState::NightlyAndNightlyDateInstalled => {
+            setup_toolchain(&tmp_fuelup_root_path, &nightly)?;
+            setup_toolchain(
+                &tmp_fuelup_root_path,
+                &format!("nightly-{}-{}", DATE, target),
+            )?;
+            setup_settings_file(&tmp_fuelup_root_path, &nightly)?;
         }
     }