"storyteller root" directory in development

This commit is contained in:
Brandon Thomas
2023-10-17 21:04:54 -04:00
parent bfd325900f
commit 3fc0f45942
14 changed files with 805 additions and 11 deletions
Generated
+51 -3
View File
@@ -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"
+1
View File
@@ -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",
+1 -2
View File
@@ -55,5 +55,4 @@ pub use types::string::get_env_string_required;
mod error;
mod init;
mod logging;
mod types;
pub (crate) mod types;
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -56,7 +56,7 @@ pub fn get_env_pathbuf_or_default<P: AsRef<Path>>(env_name: &str, default_value:
})
}
fn get_env_pathbuf_internal(env_name: &str) -> Result<Option<PathBuf>, EnvError> {
pub (crate) fn get_env_pathbuf_internal(env_name: &str) -> Result<Option<PathBuf>, EnvError> {
match env::var(env_name).as_ref() {
Err(err) => match err {
// TODO: EnvError enum variant for equals sign in env var name
+26
View File
@@ -0,0 +1,26 @@
[package]
name = "storyteller_root"
edition = "2018"
version = "0.0.1"
authors = [
"Brandon Thomas <bt@brand.io>",
"Brandon Thomas <echelon@gmail.com>",
]
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"
+37
View File
@@ -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;
@@ -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<Option<PathBuf>, 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);
}
}
@@ -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;
@@ -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);
}
}
@@ -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);
}
}
@@ -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<PathBuf> {
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<PathBuf> {
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);
}
}
@@ -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);
}
}
@@ -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);
}
}