From 3fc0f45942ed51bed549cf06de068ed3a9bf20ba Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 17 Oct 2023 19:00:11 -0400 Subject: [PATCH] "storyteller root" directory in development --- Cargo.lock | 54 ++++++- Cargo.toml | 1 + crates/lib/easyenv/src/lib.rs | 3 +- crates/lib/easyenv/src/types/mod.rs | 10 +- crates/lib/easyenv/src/types/pathbuf.rs | 2 +- crates/lib/storyteller_root/Cargo.toml | 26 +++ crates/lib/storyteller_root/src/lib.rs | 37 +++++ .../src/paths/env_get_path.rs | 80 ++++++++++ crates/lib/storyteller_root/src/paths/mod.rs | 5 + .../src/paths/storyteller_frontend_root.rs | 108 +++++++++++++ .../src/paths/storyteller_ml_root.rs | 108 +++++++++++++ .../src/paths/storyteller_root.rs | 151 ++++++++++++++++++ .../src/paths/storyteller_rust_root.rs | 108 +++++++++++++ .../storyteller_root/src/substituted_path.rs | 123 ++++++++++++++ 14 files changed, 805 insertions(+), 11 deletions(-) create mode 100644 crates/lib/storyteller_root/Cargo.toml create mode 100644 crates/lib/storyteller_root/src/lib.rs create mode 100644 crates/lib/storyteller_root/src/paths/env_get_path.rs create mode 100644 crates/lib/storyteller_root/src/paths/mod.rs create mode 100644 crates/lib/storyteller_root/src/paths/storyteller_frontend_root.rs create mode 100644 crates/lib/storyteller_root/src/paths/storyteller_ml_root.rs create mode 100644 crates/lib/storyteller_root/src/paths/storyteller_root.rs create mode 100644 crates/lib/storyteller_root/src/paths/storyteller_rust_root.rs create mode 100644 crates/lib/storyteller_root/src/substituted_path.rs diff --git a/Cargo.lock b/Cargo.lock index 590cfc6a2b..30d12bfd21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1676,6 +1676,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "dashmap" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8858831f7781322e539ea39e72449c46b059638250c14344fec8d0aa6e539c" +dependencies = [ + "cfg-if 1.0.0", + "num_cpus", + "parking_lot 0.12.1", +] + [[package]] name = "data-encoding" version = "2.3.3" @@ -3809,11 +3820,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.18", + "hermit-abi 0.3.1", "libc", ] @@ -5104,6 +5115,31 @@ dependencies = [ "serde", ] +[[package]] +name = "serial_test" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" +dependencies = [ + "dashmap", + "futures 0.3.26", + "lazy_static", + "log", + "parking_lot 0.12.1", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.26", +] + [[package]] name = "server_environment" version = "0.0.1" @@ -5586,6 +5622,18 @@ dependencies = [ "uuid 0.8.2", ] +[[package]] +name = "storyteller_root" +version = "0.0.1" +dependencies = [ + "dotenv", + "env_logger 0.10.0", + "errors", + "log", + "serial_test", + "tempdir", +] + [[package]] name = "string_cache" version = "0.8.4" diff --git a/Cargo.toml b/Cargo.toml index fd93afe243..6781c87208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "crates/lib/redis_common", "crates/lib/server_environment", "crates/lib/sqlx_mysql_helpers", + "crates/lib/storyteller_root", "crates/lib/subprocess_common", "crates/lib/testing", "crates/lib/tts_common", diff --git a/crates/lib/easyenv/src/lib.rs b/crates/lib/easyenv/src/lib.rs index fbd5e3f69b..03ac3ef4e6 100644 --- a/crates/lib/easyenv/src/lib.rs +++ b/crates/lib/easyenv/src/lib.rs @@ -55,5 +55,4 @@ pub use types::string::get_env_string_required; mod error; mod init; mod logging; -mod types; - +pub (crate) mod types; diff --git a/crates/lib/easyenv/src/types/mod.rs b/crates/lib/easyenv/src/types/mod.rs index 7f5f836e7f..5a3813d4f5 100644 --- a/crates/lib/easyenv/src/types/mod.rs +++ b/crates/lib/easyenv/src/types/mod.rs @@ -1,5 +1,5 @@ -pub mod boolean; -pub mod duration; -pub mod pathbuf; -pub mod string; -pub mod num; +pub (crate) mod boolean; +pub (crate) mod duration; +pub (crate) mod pathbuf; +pub (crate) mod string; +pub (crate) mod num; diff --git a/crates/lib/easyenv/src/types/pathbuf.rs b/crates/lib/easyenv/src/types/pathbuf.rs index 94f374bed5..7b1e345549 100644 --- a/crates/lib/easyenv/src/types/pathbuf.rs +++ b/crates/lib/easyenv/src/types/pathbuf.rs @@ -56,7 +56,7 @@ pub fn get_env_pathbuf_or_default>(env_name: &str, default_value: }) } -fn get_env_pathbuf_internal(env_name: &str) -> Result, EnvError> { +pub (crate) fn get_env_pathbuf_internal(env_name: &str) -> Result, EnvError> { match env::var(env_name).as_ref() { Err(err) => match err { // TODO: EnvError enum variant for equals sign in env var name diff --git a/crates/lib/storyteller_root/Cargo.toml b/crates/lib/storyteller_root/Cargo.toml new file mode 100644 index 0000000000..243fb161ae --- /dev/null +++ b/crates/lib/storyteller_root/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "storyteller_root" +edition = "2018" +version = "0.0.1" +authors = [ + "Brandon Thomas ", + "Brandon Thomas ", +] +publish = false + +[lib] +name = "storyteller_root" +path = "src/lib.rs" + +[dependencies] +# Internal +errors = { path = "../errors" } + +# External +dotenv = "0.15.0" +env_logger = "0.10.0" +log = "0.4.14" + +[dev-dependencies] +serial_test = "2.0.0" +tempdir = "0.3.7" diff --git a/crates/lib/storyteller_root/src/lib.rs b/crates/lib/storyteller_root/src/lib.rs new file mode 100644 index 0000000000..b720725cc4 --- /dev/null +++ b/crates/lib/storyteller_root/src/lib.rs @@ -0,0 +1,37 @@ +//! storyteller_root +//! +//! This crate defines functions that locate the development root for Storyteller projects, +//! as well as our major monorepos. This can be used to build relative paths that should work +//! across developer machines and environments. +//! +//! These functions should not be relied upon in production. +//! +//! The following environment variables are read: +//! +//! - STORYTELLER_ROOT +//! - STORYTELLER_FRONTEND_ROOT +//! - STORYTELLER_ML_ROOT +//! - STORYTELLER_RUST_ROOT +//! + +// Never allow these +#![forbid(private_in_public)] +#![forbid(unused_must_use)] // NB: It's unsafe to not close/check some things + +// Okay to toggle +#![forbid(unreachable_patterns)] +#![forbid(unused_imports)] +#![forbid(unused_mut)] +#![forbid(unused_variables)] + +// Always allow +#![allow(dead_code)] +#![allow(non_snake_case)] + +mod paths; +mod substituted_path; + +pub use paths::storyteller_frontend_root::get_storyteller_frontend_root; +pub use paths::storyteller_ml_root::get_storyteller_ml_root; +pub use paths::storyteller_root::get_storyteller_root; +pub use paths::storyteller_rust_root::get_storyteller_rust_root; diff --git a/crates/lib/storyteller_root/src/paths/env_get_path.rs b/crates/lib/storyteller_root/src/paths/env_get_path.rs new file mode 100644 index 0000000000..54020ba02f --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/env_get_path.rs @@ -0,0 +1,80 @@ +use std::env::VarError; +use std::error::Error; +use std::fmt::{Display, Formatter}; +use std::path::PathBuf; +use std::str::FromStr; + +/// Errors with reading and parsing env variables. +#[derive(Debug, Eq, PartialEq)] +pub enum EnvError { + /// The environment variable value is not unicode. + NotUnicode, + /// Problem parsing the env variable as the desired type. + ParseError { + /// Explanation of the parsing failure. + reason: String + }, + /// The required environment variable wasn't present. + RequiredNotPresent, +} + +impl Display for EnvError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let reason = match self { + EnvError::NotUnicode => "EnvError::NotUnicode", + EnvError::ParseError { .. } => "EnvError::ParseError", + EnvError::RequiredNotPresent => "EnvError::RequiredNotPresent", + }; + write!(f, "{:?}", reason) + } +} + +impl Error for EnvError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + None + } +} + +pub (crate) fn env_get_path(env_name: &str) -> Result, EnvError> { + match std::env::var(env_name).as_ref() { + Err(err) => match err { + // TODO: EnvError enum variant for equals sign in env var name + VarError::NotPresent => Ok(None), + VarError::NotUnicode(_) => Err(EnvError::NotUnicode), + } + Ok(val) => { + match PathBuf::from_str(val) { + Ok(path) => Ok(Some(path)), + Err(_err) => Err(EnvError::ParseError { + reason: "error parsing PathBuf from value".to_string() + }), + } + } + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use super::env_get_path; + + #[test] + fn test_read_unset_env() { + assert_eq!(env_get_path("ASDFGH_FOO"), Ok(None)); + } + + #[test] + fn test_read_set_env() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + const ENV_VAR_NAME : &str = "ENV_TEST_VAR_DO_NOT_REUSE"; + + std::env::set_var(ENV_VAR_NAME, "/foo/bar"); + + assert_eq!(env_get_path(ENV_VAR_NAME), Ok(Some(PathBuf::from("/foo/bar")))); + + std::env::remove_var(ENV_VAR_NAME); + } +} diff --git a/crates/lib/storyteller_root/src/paths/mod.rs b/crates/lib/storyteller_root/src/paths/mod.rs new file mode 100644 index 0000000000..2d2a0798cc --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/mod.rs @@ -0,0 +1,5 @@ +pub (crate) mod env_get_path; +pub (crate) mod storyteller_frontend_root; +pub (crate) mod storyteller_ml_root; +pub (crate) mod storyteller_root; +pub (crate) mod storyteller_rust_root; diff --git a/crates/lib/storyteller_root/src/paths/storyteller_frontend_root.rs b/crates/lib/storyteller_root/src/paths/storyteller_frontend_root.rs new file mode 100644 index 0000000000..5a48af8f51 --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/storyteller_frontend_root.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +use crate::get_storyteller_root; +use crate::paths::env_get_path::env_get_path; + +/// The env var that declares where the storyteller frontend monorepo lives. +pub const STORYTELLER_FRONTEND_ROOT : &str = "STORYTELLER_FRONTEND_ROOT"; + +// DO NOT LEAK THIS. THIS IS FOR TESTING ONLY. +pub const TEST_STORYTELLER_FRONTEND_ROOT : &str = "ENV_TEST_STORYTELLER_FRONTEND_ROOT_DO_NOT_LEAK_"; + +/// Get the root of the Rust monorepo +pub fn get_storyteller_frontend_root() -> PathBuf { + // 1) Try "STORYTELLER_FRONTEND_ROOT" env var override. Do not check for path existence. + let root = if cfg!(test) { TEST_STORYTELLER_FRONTEND_ROOT } else { STORYTELLER_FRONTEND_ROOT }; + + if let Ok(Some(path)) = env_get_path(root) { + return path; + } + + // 2) Relative to "storyteller root" + let mut dir = get_storyteller_root(); + dir.push("storyteller-frontend"); + dir +} + +#[cfg(test)] +mod tests { + use std::env::{current_dir, temp_dir}; + use std::fs::{create_dir, remove_dir}; + use std::path::PathBuf; + + use serial_test::serial; + + use crate::paths::storyteller_root::TEST_HOME; + use crate::paths::storyteller_frontend_root::{get_storyteller_frontend_root, TEST_STORYTELLER_FRONTEND_ROOT}; + + #[test] + #[serial] + fn test_storyteller_frontend_home_env_var() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_FRONTEND_ROOT, "/testing/storyteller/root/custom-storyteller-frontend"); + assert_eq!(get_storyteller_frontend_root(), PathBuf::from("/testing/storyteller/root/custom-storyteller-frontend")); + std::env::remove_var(TEST_STORYTELLER_FRONTEND_ROOT); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_no_directories_exist() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, "/testing/fake_home"); + assert_eq!(get_storyteller_frontend_root(), PathBuf::from("/testing/fake_home/code/storyteller/storyteller-frontend")); + std::env::remove_var(TEST_HOME); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_with_existing_directory() { + let mut fake_home = temp_dir(); + fake_home.push("temp_dir_for_testing"); + + let mut fake_dev = fake_home.clone(); + fake_dev.push("dev"); + + // NB: Final path should be /tmp/temp_dir_for_testing/dev/storyteller + let mut fake_storyteller = fake_dev.clone(); + fake_storyteller.push("storyteller"); + + // NB: Ordered for repeatability + let _r = remove_dir(&fake_home).ok(); + let _r = create_dir(&fake_home).ok(); + let _r = create_dir(&fake_dev).ok(); + let _r = create_dir(&fake_storyteller).ok(); + + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, fake_home.to_str().expect("should be string")); + + let mut expected = fake_storyteller.clone(); + expected.push("storyteller-frontend"); + + assert_eq!(get_storyteller_frontend_root(), expected); + + std::env::remove_var(TEST_HOME); + + // NB: Must remove in reverse order, or we leave dangling pointers + let _r = remove_dir(&fake_storyteller).ok(); + let _r = remove_dir(&fake_dev).ok(); + let _r = remove_dir(&fake_home).ok(); + } + + #[test] + #[serial] + fn test_relative_to_current_working_directory() { + // NB: In this test we have no "TEST_HOME" directory. + let current_working_dir = current_dir().expect("Should have working dir."); + + let mut expected_dir = current_working_dir.clone(); + expected_dir.push("code/storyteller/storyteller-frontend"); + + assert_eq!(get_storyteller_frontend_root(), expected_dir); + } +} diff --git a/crates/lib/storyteller_root/src/paths/storyteller_ml_root.rs b/crates/lib/storyteller_root/src/paths/storyteller_ml_root.rs new file mode 100644 index 0000000000..c9233b062f --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/storyteller_ml_root.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +use crate::get_storyteller_root; +use crate::paths::env_get_path::env_get_path; + +/// The env var that declares where the storyteller ml monorepo lives. +pub const STORYTELLER_ML_ROOT : &str = "STORYTELLER_ML_ROOT"; + +// DO NOT LEAK THIS. THIS IS FOR TESTING ONLY. +pub const TEST_STORYTELLER_ML_ROOT : &str = "ENV_TEST_STORYTELLER_ML_ROOT_DO_NOT_LEAK_"; + +/// Get the root of the Rust monorepo +pub fn get_storyteller_ml_root() -> PathBuf { + // 1) Try "STORYTELLER_ML_ROOT" env var override. Do not check for path existence. + let root = if cfg!(test) { TEST_STORYTELLER_ML_ROOT } else { STORYTELLER_ML_ROOT }; + + if let Ok(Some(path)) = env_get_path(root) { + return path; + } + + // 2) Relative to "storyteller root" + let mut dir = get_storyteller_root(); + dir.push("storyteller-ml"); + dir +} + +#[cfg(test)] +mod tests { + use std::env::{current_dir, temp_dir}; + use std::fs::{create_dir, remove_dir}; + use std::path::PathBuf; + + use serial_test::serial; + + use crate::paths::storyteller_ml_root::{get_storyteller_ml_root, TEST_STORYTELLER_ML_ROOT}; + use crate::paths::storyteller_root::TEST_HOME; + + #[test] + #[serial] + fn test_storyteller_ml_home_env_var() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ML_ROOT, "/testing/storyteller/root/custom-storyteller-ml"); + assert_eq!(get_storyteller_ml_root(), PathBuf::from("/testing/storyteller/root/custom-storyteller-ml")); + std::env::remove_var(TEST_STORYTELLER_ML_ROOT); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_no_directories_exist() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, "/testing/fake_home"); + assert_eq!(get_storyteller_ml_root(), PathBuf::from("/testing/fake_home/code/storyteller/storyteller-ml")); + std::env::remove_var(TEST_HOME); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_with_existing_directory() { + let mut fake_home = temp_dir(); + fake_home.push("temp_dir_for_testing"); + + let mut fake_dev = fake_home.clone(); + fake_dev.push("dev"); + + // NB: Final path should be /tmp/temp_dir_for_testing/dev/storyteller + let mut fake_storyteller = fake_dev.clone(); + fake_storyteller.push("storyteller"); + + // NB: Ordered for repeatability + let _r = remove_dir(&fake_home).ok(); + let _r = create_dir(&fake_home).ok(); + let _r = create_dir(&fake_dev).ok(); + let _r = create_dir(&fake_storyteller).ok(); + + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, fake_home.to_str().expect("should be string")); + + let mut expected = fake_storyteller.clone(); + expected.push("storyteller-ml"); + + assert_eq!(get_storyteller_ml_root(), expected); + + std::env::remove_var(TEST_HOME); + + // NB: Must remove in reverse order, or we leave dangling pointers + let _r = remove_dir(&fake_storyteller).ok(); + let _r = remove_dir(&fake_dev).ok(); + let _r = remove_dir(&fake_home).ok(); + } + + #[test] + #[serial] + fn test_relative_to_current_working_directory() { + // NB: In this test we have no "TEST_HOME" directory. + let current_working_dir = current_dir().expect("Should have working dir."); + + let mut expected_dir = current_working_dir.clone(); + expected_dir.push("code/storyteller/storyteller-ml"); + + assert_eq!(get_storyteller_ml_root(), expected_dir); + } +} diff --git a/crates/lib/storyteller_root/src/paths/storyteller_root.rs b/crates/lib/storyteller_root/src/paths/storyteller_root.rs new file mode 100644 index 0000000000..53c96beefa --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/storyteller_root.rs @@ -0,0 +1,151 @@ +use std::path::{Path, PathBuf}; + +use crate::paths::env_get_path::env_get_path; + +/// The env var that declares where the storyteller root directory lives. +pub const STORYTELLER_ROOT : &str = "STORYTELLER_ROOT"; + +/// Default OS home directory. +pub const HOME : &str = "HOME"; + +// DO NOT LEAK THIS. THIS IS FOR TESTING ONLY. +pub (crate) const TEST_STORYTELLER_ROOT : &str = "ENV_TEST_STORYTELLER_ROOT_DO_NOT_LEAK_"; +pub (crate) const TEST_HOME : &str = "ENV_TEST_HOME_DO_NOT_LEAK_"; + +// TODO: Test on Mac, Windows, and WSL. +/// Use several heuristics to find "storyteller root", where all of our monorepos are located. +pub fn get_storyteller_root() -> PathBuf { + // 1) Try "STORYTELLER_ROOT" env var override. Do not check for path existence. + let root = if cfg!(test) { TEST_STORYTELLER_ROOT } else { STORYTELLER_ROOT }; + + if let Ok(Some(path)) = env_get_path(root) { + return path; + } + + // TODO: Consider `dirs` or `directories` crates for cross-platform support: + // https://crates.io/crates/dirs + // https://crates.io/crates/directories + + // 2) Try to construct paths relative to the "HOME" directory. Prefer existing directories. + let home = if cfg!(test) { TEST_HOME } else { HOME }; + + if let Ok(Some(home_path)) = env_get_path(home) { + if let Some(path) = try_subdirectories(home_path) { + return path; + } + } + + // 3) Try to construct paths relative to the current working directory. + if let Ok(current_working_dir) = std::env::current_dir() { + if let Some(path) = try_subdirectories(current_working_dir) { + return path; + } + } + + // 4) Rooted directory (since this function is infallible) + PathBuf::from("/storyteller") +} + +fn try_subdirectories(root_path: PathBuf) -> Option { + None + // First try to see if the directories exist (in order) + .or_else(|| try_subdirectory(&root_path, "code/storyteller", true)) + .or_else(|| try_subdirectory(&root_path, "dev/storyteller", true)) + // Then just use whatever parses. + .or_else(|| try_subdirectory(&root_path, "code/storyteller", false)) +} + +fn try_subdirectory<'a>(dir: &Path, sub_path: &str, check_if_exists: bool) -> Option { + let mut home = dir.to_path_buf(); + home.push(sub_path); + + if let Ok(canonical) = home.canonicalize() { + home = canonical; // NB: Ignore errors + } + + if check_if_exists && !(home.exists() && home.is_dir()) { + return None; + } + + Some(home) +} + +#[cfg(test)] +mod tests { + use std::env::{current_dir, temp_dir}; + use std::fs::{create_dir, remove_dir}; + use std::path::PathBuf; + + use serial_test::serial; + + use crate::get_storyteller_root; + use crate::paths::storyteller_root::{TEST_HOME, TEST_STORYTELLER_ROOT}; + + #[test] + #[serial] + fn test_storyteller_home_env_var() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ROOT, "/testing/storyteller/root"); + assert_eq!(get_storyteller_root(), PathBuf::from("/testing/storyteller/root")); + std::env::remove_var(TEST_STORYTELLER_ROOT); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_no_directories_exist() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, "/testing/fake_home"); + assert_eq!(get_storyteller_root(), PathBuf::from("/testing/fake_home/code/storyteller")); + std::env::remove_var(TEST_HOME); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_with_existing_directory() { + let mut fake_home = temp_dir(); + fake_home.push("temp_dir_for_testing"); + + let mut fake_dev = fake_home.clone(); + fake_dev.push("dev"); + + // NB: Final path should be /tmp/temp_dir_for_testing/dev/storyteller + let mut fake_storyteller = fake_dev.clone(); + fake_storyteller.push("storyteller"); + + // NB: Ordered for repeatability + let _r = remove_dir(&fake_home).ok(); + let _r = create_dir(&fake_home).ok(); + let _r = create_dir(&fake_dev).ok(); + let _r = create_dir(&fake_storyteller).ok(); + + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, fake_home.to_str().expect("should be string")); + + assert_eq!(get_storyteller_root(), fake_storyteller); + + std::env::remove_var(TEST_HOME); + + // NB: Must remove in reverse order, or we leave dangling pointers + let _r = remove_dir(&fake_storyteller).ok(); + let _r = remove_dir(&fake_dev).ok(); + let _r = remove_dir(&fake_home).ok(); + } + + #[test] + #[serial] + fn test_relative_to_current_working_directory() { + // NB: In this test we have no "TEST_HOME" directory. + let current_working_dir = current_dir().expect("Should have working dir."); + + let mut storyteller_dir = current_working_dir.clone(); + storyteller_dir.push("code/storyteller"); + + assert_eq!(get_storyteller_root(), storyteller_dir); + } +} diff --git a/crates/lib/storyteller_root/src/paths/storyteller_rust_root.rs b/crates/lib/storyteller_root/src/paths/storyteller_rust_root.rs new file mode 100644 index 0000000000..fd339e73c7 --- /dev/null +++ b/crates/lib/storyteller_root/src/paths/storyteller_rust_root.rs @@ -0,0 +1,108 @@ +use std::path::PathBuf; + +use crate::get_storyteller_root; +use crate::paths::env_get_path::env_get_path; + +/// The env var that declares where the storyteller rust monorepo lives. +pub const STORYTELLER_RUST_ROOT : &str = "STORYTELLER_RUST_ROOT"; + +// DO NOT LEAK THIS. THIS IS FOR TESTING ONLY. +pub (crate) const TEST_STORYTELLER_RUST_ROOT : &str = "ENV_TEST_STORYTELLER_RUST_ROOT_DO_NOT_LEAK_"; + +/// Get the root of the Rust monorepo +pub fn get_storyteller_rust_root() -> PathBuf { + // 1) Try "STORYTELLER_RUST_ROOT" env var override. Do not check for path existence. + let root = if cfg!(test) { TEST_STORYTELLER_RUST_ROOT } else { STORYTELLER_RUST_ROOT }; + + if let Ok(Some(path)) = env_get_path(root) { + return path; + } + + // 2) Relative to "storyteller root" + let mut dir = get_storyteller_root(); + dir.push("storyteller-rust"); + dir +} + +#[cfg(test)] +mod tests { + use std::env::{current_dir, temp_dir}; + use std::fs::{create_dir, remove_dir}; + use std::path::PathBuf; + + use serial_test::serial; + + use crate::paths::storyteller_root::TEST_HOME; + use crate::paths::storyteller_rust_root::{get_storyteller_rust_root, TEST_STORYTELLER_RUST_ROOT}; + + #[test] + #[serial] + fn test_storyteller_rust_home_env_var() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_RUST_ROOT, "/testing/storyteller/root/custom-storyteller-rust"); + assert_eq!(get_storyteller_rust_root(), PathBuf::from("/testing/storyteller/root/custom-storyteller-rust")); + std::env::remove_var(TEST_STORYTELLER_RUST_ROOT); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_no_directories_exist() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, "/testing/fake_home"); + assert_eq!(get_storyteller_rust_root(), PathBuf::from("/testing/fake_home/code/storyteller/storyteller-rust")); + std::env::remove_var(TEST_HOME); + } + + #[test] + #[serial] + fn test_relative_to_home_directory_with_existing_directory() { + let mut fake_home = temp_dir(); + fake_home.push("temp_dir_for_testing"); + + let mut fake_dev = fake_home.clone(); + fake_dev.push("dev"); + + // NB: Final path should be /tmp/temp_dir_for_testing/dev/storyteller + let mut fake_storyteller = fake_dev.clone(); + fake_storyteller.push("storyteller"); + + // NB: Ordered for repeatability + let _r = remove_dir(&fake_home).ok(); + let _r = create_dir(&fake_home).ok(); + let _r = create_dir(&fake_dev).ok(); + let _r = create_dir(&fake_storyteller).ok(); + + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_HOME, fake_home.to_str().expect("should be string")); + + let mut expected = fake_storyteller.clone(); + expected.push("storyteller-rust"); + + assert_eq!(get_storyteller_rust_root(), expected); + + std::env::remove_var(TEST_HOME); + + // NB: Must remove in reverse order, or we leave dangling pointers + let _r = remove_dir(&fake_storyteller).ok(); + let _r = remove_dir(&fake_dev).ok(); + let _r = remove_dir(&fake_home).ok(); + } + + #[test] + #[serial] + fn test_relative_to_current_working_directory() { + // NB: In this test we have no "TEST_HOME" directory. + let current_working_dir = current_dir().expect("Should have working dir."); + + let mut expected_dir = current_working_dir.clone(); + expected_dir.push("code/storyteller/storyteller-rust"); + + assert_eq!(get_storyteller_rust_root(), expected_dir); + } +} diff --git a/crates/lib/storyteller_root/src/substituted_path.rs b/crates/lib/storyteller_root/src/substituted_path.rs new file mode 100644 index 0000000000..15bb9fb0de --- /dev/null +++ b/crates/lib/storyteller_root/src/substituted_path.rs @@ -0,0 +1,123 @@ +use std::path::PathBuf; + +use crate::{get_storyteller_frontend_root, get_storyteller_ml_root, get_storyteller_root, get_storyteller_rust_root}; + +const STORYTELLER_ROOT_REPLACEMENT : &str = "%storyteller_root%"; + +const STORYTELLER_FRONTEND_REPLACEMENT : &str = "%storyteller_frontend%"; +const STORYTELLER_ML_REPLACEMENT : &str = "%storyteller_ml%"; +const STORYTELLER_RUST_REPLACEMENT : &str = "%storyteller_rust%"; + +/// Transform a given string path, substituting "magic" variables for the various storyteller +/// project directories. +/// +/// Substitution variables: +/// +/// - %storyteller_root% +/// - %storyteller_rust% +/// - %storyteller_ml% +/// - %storyteller_frontend% +/// +pub fn get_substituted_path(path: &str) -> PathBuf { + // TODO(bt,2023-10-17): Should this be infallible? If not, where in the stack of transformations + // should the failure manifest? ie, if infallible, should we make the final paths "closer" to + // the de-substituted string, or the environment variable? Which makes it easier to detect the + // error? + // + // TODO(bt,2023-10-17): I'm not happy with all of the internal type juggling and allocations, + // but I shouldn't spend time on refactoring this now. + // + if path.contains(STORYTELLER_ROOT_REPLACEMENT) { + let replacement = type_juggle_path(get_storyteller_root()); + let path = path.replacen(STORYTELLER_ROOT_REPLACEMENT, &replacement, 1); + return PathBuf::from(path); + } else if path.contains(STORYTELLER_RUST_REPLACEMENT) { + let replacement = type_juggle_path(get_storyteller_rust_root()); + let path = path.replacen(STORYTELLER_RUST_REPLACEMENT, &replacement, 1); + return PathBuf::from(path); + } else if path.contains(STORYTELLER_ML_REPLACEMENT) { + let replacement = type_juggle_path(get_storyteller_ml_root()); + let path = path.replacen(STORYTELLER_ML_REPLACEMENT, &replacement, 1); + return PathBuf::from(path); + } else if path.contains(STORYTELLER_FRONTEND_REPLACEMENT) { + let replacement = type_juggle_path(get_storyteller_frontend_root()); + let path = path.replacen(STORYTELLER_FRONTEND_REPLACEMENT, &replacement, 1); + return PathBuf::from(path); + } + + PathBuf::from(path) +} + +fn type_juggle_path(path: PathBuf) -> String { + // NB: The Path/PathBuf library does some silly stuff regarding non-UTF-8 paths. + path.to_str() + .map(|s| s.to_string()) + .unwrap_or_else(|| path.to_string_lossy().to_string()) +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + + use serial_test::serial; + + use crate::paths::storyteller_root::TEST_STORYTELLER_ROOT; + use crate::substituted_path::get_substituted_path; + + #[test] + fn test_no_substitutions() { + assert_eq!(get_substituted_path("/foo/bar"), PathBuf::from("/foo/bar")); + } + + #[test] + #[serial] + fn test_storyteller_root_substitution() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ROOT, "/testing/storyteller/root"); + + assert_eq!(get_substituted_path("%storyteller_root%/foo/bar"), PathBuf::from("/testing/storyteller/root/foo/bar")); + + std::env::remove_var(TEST_STORYTELLER_ROOT); + } + + #[test] + #[serial] + fn test_storyteller_rust_root_substitution() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ROOT, "/testing/storyteller/root"); + + assert_eq!(get_substituted_path("%storyteller_rust%/foo/bar"), PathBuf::from("/testing/storyteller/root/storyteller-rust/foo/bar")); + + std::env::remove_var(TEST_STORYTELLER_ROOT); + } + + #[test] + #[serial] + fn test_storyteller_ml_root_substitution() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ROOT, "/testing/storyteller/root"); + + assert_eq!(get_substituted_path("%storyteller_ml%/foo/bar"), PathBuf::from("/testing/storyteller/root/storyteller-ml/foo/bar")); + + std::env::remove_var(TEST_STORYTELLER_ROOT); + } + + #[test] + #[serial] + fn test_storyteller_frontend_root_substitution() { + // NB: Careful with `set_var` as it escapes into *all* other tests! To make matters + // more complicated, tests run in parallel and in any order. We're using a special + // test variable so we never infect other tests. + std::env::set_var(TEST_STORYTELLER_ROOT, "/testing/storyteller/root"); + + assert_eq!(get_substituted_path("%storyteller_frontend%/foo/bar"), PathBuf::from("/testing/storyteller/root/storyteller-frontend/foo/bar")); + + std::env::remove_var(TEST_STORYTELLER_ROOT); + } +}