diff --git a/.gitignore b/.gitignore index f3f71df4..c8db9e06 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,7 @@ target/ .DS_Store # Testing directories -.venv/ +./.venv/ tmp/ temp/ docs/node_modules/ diff --git a/Cargo.lock b/Cargo.lock index 0a080d37..fde7bebd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -564,6 +564,7 @@ dependencies = [ name = "pet-python-utils" version = "0.1.0" dependencies = [ + "env_logger", "lazy_static", "log", "msvc_spectre_libs", diff --git a/crates/pet-conda/tests/ci_test.rs b/crates/pet-conda/tests/ci_test.rs index 3722bd1f..0bfd0a6a 100644 --- a/crates/pet-conda/tests/ci_test.rs +++ b/crates/pet-conda/tests/ci_test.rs @@ -310,10 +310,7 @@ fn detect_new_conda_env_created_with_p_flag_with_python() { use pet_core::{ os_environment::EnvironmentApi, python_environment::PythonEnvironmentKind, Locator, }; - use pet_reporter::{ - cache::{self, CacheReporter}, - collect, - }; + use pet_reporter::{cache::CacheReporter, collect}; setup(); let env_name = "env_with_python3"; diff --git a/crates/pet-homebrew/src/environments.rs b/crates/pet-homebrew/src/environments.rs index 2b8861e3..905b35a9 100644 --- a/crates/pet-homebrew/src/environments.rs +++ b/crates/pet-homebrew/src/environments.rs @@ -3,6 +3,7 @@ use crate::sym_links::get_known_symlinks; use lazy_static::lazy_static; +use log::trace; use pet_core::python_environment::{ PythonEnvironment, PythonEnvironmentBuilder, PythonEnvironmentKind, }; @@ -12,19 +13,21 @@ use std::path::{Path, PathBuf}; lazy_static! { static ref PYTHON_VERSION: Regex = - Regex::new(r"/(\d+\.\d+\.\d+)/").expect("error parsing Version regex for Homebrew"); + Regex::new(r"/(\d+\.\d+\.\d+)").expect("error parsing Version regex for Homebrew"); } pub fn get_python_info( python_exe_from_bin_dir: &Path, resolved_exe: &Path, ) -> Option { - // let user_friendly_exe = python_exe_from_bin_dir; - let python_version = resolved_exe.to_string_lossy().to_string(); - let version = match PYTHON_VERSION.captures(&python_version) { - Some(captures) => captures.get(1).map(|version| version.as_str().to_string()), - None => None, - }; + let version = get_version(resolved_exe); + + trace!( + "Getting homebrew python info for {:?} => {:?} with version {:?}", + python_exe_from_bin_dir, + resolved_exe, + version + ); let mut symlinks = vec![ python_exe_from_bin_dir.to_path_buf(), @@ -57,6 +60,14 @@ pub fn get_python_info( Some(env) } +fn get_version(resolved_exe: &Path) -> Option { + let python_version = resolved_exe.to_string_lossy().to_string(); + match PYTHON_VERSION.captures(&python_version) { + Some(captures) => captures.get(1).map(|version| version.as_str().to_string()), + None => None, + } +} + fn get_prefix(_resolved_file: &Path) -> Option { // NOTE: // While testing found that on Mac Intel @@ -135,3 +146,26 @@ fn get_prefix(_resolved_file: &Path) -> Option { // } None } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[cfg(unix)] + fn extract_version() { + assert_eq!( + get_version(&PathBuf::from( + "/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3" + )), + Some("3.12.4".to_string()) + ); + + assert_eq!( + get_version(&PathBuf::from( + "/home/linuxbrew/.linuxbrew/Cellar/python@3.11/3.11.9_1/bin/python3.11" + )), + Some("3.11.9".to_string()) + ); + } +} diff --git a/crates/pet-python-utils/Cargo.toml b/crates/pet-python-utils/Cargo.toml index a782b5a6..f99d6b15 100644 --- a/crates/pet-python-utils/Cargo.toml +++ b/crates/pet-python-utils/Cargo.toml @@ -15,3 +15,12 @@ serde = { version = "1.0.152", features = ["derive"] } log = "0.4.21" serde_json = "1.0.93" sha2 = "0.10.6" +env_logger = "0.10.2" + +[features] +ci = [] +ci-jupyter-container = [] +ci-homebrew-container = [] +ci-poetry-global = [] +ci-poetry-project = [] +ci-poetry-custom = [] diff --git a/crates/pet-python-utils/src/cache.rs b/crates/pet-python-utils/src/cache.rs index cbaf446c..00b64282 100644 --- a/crates/pet-python-utils/src/cache.rs +++ b/crates/pet-python-utils/src/cache.rs @@ -2,9 +2,10 @@ // Licensed under the MIT License. use lazy_static::lazy_static; -use log::trace; +use log::{trace, warn}; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, + io, path::PathBuf, sync::{Arc, Mutex}, time::SystemTime, @@ -12,7 +13,7 @@ use std::{ use crate::{ env::ResolvedPythonEnv, - fs_cache::{get_cache_from_file, store_cache_in_file}, + fs_cache::{delete_cache_file, get_cache_from_file, store_cache_in_file}, }; lazy_static! { @@ -25,6 +26,10 @@ pub trait CacheEntry: Send + Sync { fn track_symlinks(&self, symlinks: Vec); } +pub fn clear_cache() -> io::Result<()> { + CACHE.clear() +} + pub fn create_cache(executable: PathBuf) -> Arc>> { CACHE.create_cache(executable) } @@ -61,28 +66,39 @@ impl CacheImpl { /// Once a cache directory has been set, you cannot change it. /// No point supporting such a scenario. fn set_cache_directory(&self, cache_dir: PathBuf) { + if let Some(cache_dir) = self.cache_dir.lock().unwrap().clone() { + warn!( + "Cache directory has already been set to {:?}. Cannot change it now.", + cache_dir + ); + return; + } + trace!("Setting cache directory to {:?}", cache_dir); self.cache_dir.lock().unwrap().replace(cache_dir); } + fn clear(&self) -> io::Result<()> { + trace!("Clearing cache"); + self.locks.lock().unwrap().clear(); + if let Some(cache_directory) = self.cache_dir.lock().unwrap().clone() { + std::fs::remove_dir_all(cache_directory) + } else { + Ok(()) + } + } fn create_cache(&self, executable: PathBuf) -> LockableCacheEntry { - if let Some(cache_directory) = self.cache_dir.lock().unwrap().as_ref() { - match self.locks.lock().unwrap().entry(executable.clone()) { - Entry::Occupied(lock) => lock.get().clone(), - Entry::Vacant(lock) => { - let cache = - Box::new(CacheEntryImpl::create(cache_directory.clone(), executable)) - as Box<(dyn CacheEntry + 'static)>; - lock.insert(Arc::new(Mutex::new(cache))).clone() - } + let cache_directory = self.cache_dir.lock().unwrap().clone(); + match self.locks.lock().unwrap().entry(executable.clone()) { + Entry::Occupied(lock) => lock.get().clone(), + Entry::Vacant(lock) => { + let cache = Box::new(CacheEntryImpl::create(cache_directory.clone(), executable)) + as Box<(dyn CacheEntry + 'static)>; + lock.insert(Arc::new(Mutex::new(cache))).clone() } - } else { - Arc::new(Mutex::new(Box::new(CacheEntryImpl::empty( - executable.clone(), - )))) } } } -type FilePathWithMTimeCTime = (PathBuf, Option, Option); +type FilePathWithMTimeCTime = (PathBuf, SystemTime, SystemTime); struct CacheEntryImpl { cache_directory: Option, @@ -92,17 +108,9 @@ struct CacheEntryImpl { symlinks: Arc>>, } impl CacheEntryImpl { - pub fn create(cache_directory: PathBuf, executable: PathBuf) -> impl CacheEntry { + pub fn create(cache_directory: Option, executable: PathBuf) -> impl CacheEntry { CacheEntryImpl { - cache_directory: Some(cache_directory), - executable, - envoronment: Arc::new(Mutex::new(None)), - symlinks: Arc::new(Mutex::new(Vec::new())), - } - } - pub fn empty(executable: PathBuf) -> impl CacheEntry { - CacheEntryImpl { - cache_directory: None, + cache_directory, executable, envoronment: Arc::new(Mutex::new(None)), symlinks: Arc::new(Mutex::new(Vec::new())), @@ -112,10 +120,21 @@ impl CacheEntryImpl { // Check if any of the exes have changed since we last cached this. for symlink_info in self.symlinks.lock().unwrap().iter() { if let Ok(metadata) = symlink_info.0.metadata() { - if metadata.modified().ok() != symlink_info.1 - || metadata.created().ok() != symlink_info.2 + if metadata.modified().ok() != Some(symlink_info.1) + || metadata.created().ok() != Some(symlink_info.2) { + trace!( + "Symlink {:?} has changed since we last cached it. original mtime & ctime {:?}, {:?}, current mtime & ctime {:?}, {:?}", + symlink_info.0, + symlink_info.1, + symlink_info.2, + metadata.modified().ok(), + metadata.created().ok() + ); self.envoronment.lock().unwrap().take(); + if let Some(cache_directory) = &self.cache_directory { + delete_cache_file(cache_directory, &self.executable); + } } } } @@ -149,11 +168,12 @@ impl CacheEntry for CacheEntryImpl { let mut symlinks = vec![]; for symlink in environment.symlinks.clone().unwrap_or_default().iter() { if let Ok(metadata) = symlink.metadata() { - symlinks.push(( - symlink.clone(), - metadata.modified().ok(), - metadata.created().ok(), - )); + // We only care if we have the information + if let (Some(modified), Some(created)) = + (metadata.modified().ok(), metadata.created().ok()) + { + symlinks.push((symlink.clone(), modified, created)); + } } } @@ -167,8 +187,9 @@ impl CacheEntry for CacheEntryImpl { .unwrap() .replace(environment.clone()); + trace!("Caching interpreter info for {:?}", self.executable); + if let Some(ref cache_directory) = self.cache_directory { - trace!("Storing cache for {:?}", self.executable); store_cache_in_file(cache_directory, &self.executable, &environment, symlinks) } } diff --git a/crates/pet-python-utils/src/fs_cache.rs b/crates/pet-python-utils/src/fs_cache.rs index 2d359420..7de515d0 100644 --- a/crates/pet-python-utils/src/fs_cache.rs +++ b/crates/pet-python-utils/src/fs_cache.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use log::trace; +use log::{error, trace}; use pet_fs::path::norm_case; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; @@ -14,7 +14,7 @@ use std::{ use crate::env::ResolvedPythonEnv; -type FilePathWithMTimeCTime = (PathBuf, Option, Option); +type FilePathWithMTimeCTime = (PathBuf, SystemTime, SystemTime); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -23,8 +23,13 @@ struct CacheEntry { pub symlinks: Vec, } -fn generate_cache_file(cache_directory: &Path, executable: &PathBuf) -> PathBuf { - cache_directory.join(format!("{}.1.json", generate_hash(executable))) +pub fn generate_cache_file(cache_directory: &Path, executable: &PathBuf) -> PathBuf { + cache_directory.join(format!("{}.2.json", generate_hash(executable))) +} + +pub fn delete_cache_file(cache_directory: &Path, executable: &PathBuf) { + let cache_file = generate_cache_file(cache_directory, executable); + let _ = fs::remove_file(cache_file); } pub fn get_cache_from_file( @@ -35,11 +40,29 @@ pub fn get_cache_from_file( let file = File::open(cache_file.clone()).ok()?; let reader = BufReader::new(file); let cache: CacheEntry = serde_json::from_reader(reader).ok()?; + // Account for conflicts in the cache file + // i.e. the hash generated is same for another file, remember we only take the first 16 chars. + if !cache + .environment + .clone() + .symlinks + .unwrap_or_default() + .contains(executable) + { + trace!( + "Cache file {:?} {:?}, does not match executable {:?} (possible hash collision)", + cache_file, + cache.environment, + executable + ); + return None; + } // Check if any of the exes have changed since we last cached them. let cache_is_valid = cache.symlinks.iter().all(|symlink| { if let Ok(metadata) = symlink.0.metadata() { - metadata.modified().ok() == symlink.1 && metadata.created().ok() == symlink.2 + metadata.modified().ok() == Some(symlink.1) + && metadata.created().ok() == Some(symlink.2) } else { // File may have been deleted. false @@ -62,15 +85,27 @@ pub fn store_cache_in_file( symlinks_with_times: Vec, ) { let cache_file = generate_cache_file(cache_directory, executable); - let _ = std::fs::create_dir_all(cache_directory); - - let cache = CacheEntry { - environment: environment.clone(), - symlinks: symlinks_with_times, - }; - if let Ok(file) = std::fs::File::create(cache_file.clone()) { - trace!("Caching {:?} in {:?}", executable, cache_file); - let _ = serde_json::to_writer_pretty(file, &cache).ok(); + match std::fs::create_dir_all(cache_directory) { + Ok(_) => { + let cache = CacheEntry { + environment: environment.clone(), + symlinks: symlinks_with_times, + }; + match std::fs::File::create(cache_file.clone()) { + Ok(file) => { + trace!("Caching {:?} in {:?}", executable, cache_file); + match serde_json::to_writer_pretty(file, &cache) { + Ok(_) => (), + Err(err) => error!("Error writing cache file {:?} {:?}", cache_file, err), + } + } + Err(err) => error!("Error creating cache file {:?} {:?}", cache_file, err), + } + } + Err(err) => error!( + "Error creating cache directory {:?} {:?}", + cache_directory, err + ), } } diff --git a/crates/pet-python-utils/src/lib.rs b/crates/pet-python-utils/src/lib.rs index 9a6aedfa..2f3cdda5 100644 --- a/crates/pet-python-utils/src/lib.rs +++ b/crates/pet-python-utils/src/lib.rs @@ -4,7 +4,7 @@ pub mod cache; pub mod env; pub mod executable; -mod fs_cache; +pub mod fs_cache; mod headers; pub mod platform_dirs; pub mod version; diff --git a/crates/pet-python-utils/tests/cache_test.rs b/crates/pet-python-utils/tests/cache_test.rs new file mode 100644 index 00000000..64a6fb3b --- /dev/null +++ b/crates/pet-python-utils/tests/cache_test.rs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; +use std::{env, path::PathBuf, sync::Once}; + +use common::resolve_test_path; +use pet_python_utils::cache::{get_cache_directory, set_cache_directory}; + +static INIT: Once = Once::new(); + +/// Setup function that is only run once, even if called multiple times. +fn setup() { + INIT.call_once(|| { + env_logger::builder() + .filter(None, log::LevelFilter::Trace) + .init(); + + set_cache_directory(env::temp_dir().join("pet_cache")); + }); +} + +#[cfg_attr( + any( + feature = "ci", // Try to run this in all ci jobs/environments + feature = "ci-jupyter-container", + feature = "ci-homebrew-container", + feature = "ci-poetry-global", + feature = "ci-poetry-project", + feature = "ci-poetry-custom", + ), + test +)] +#[allow(dead_code)] +fn verify_cache() { + use std::fs; + + use pet_python_utils::{ + cache::{clear_cache, create_cache}, + env::ResolvedPythonEnv, + fs_cache::generate_cache_file, + }; + + setup(); + + let cache_dir = get_cache_directory().unwrap(); + let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); + let bin = prefix.join("bin"); + let python = bin.join("python"); + let python3 = bin.join("python3"); + let resolve_env = ResolvedPythonEnv { + executable: python.clone(), + version: "3.9.9".to_string(), + prefix: prefix.clone(), + is64_bit: true, + symlinks: Some(vec![python.clone(), python3.clone()]), + }; + + // Ensure the file does not exist. + let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); + let _ = fs::remove_file(&cache_file); + + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + // No cache file, so we should not have a value. + assert!(cache.get().is_none()); + assert!(!cache_file.exists()); + + // Store the value in cache and verify the file exists. + cache.store(resolve_env.clone()); + + assert!(cache.get().is_some()); + assert!(cache_file.exists()); + drop(cache); + + // Creating a new cache should load the value from the file. + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + assert!(cache.get().is_some()); + assert!(cache_file.exists()); + drop(cache); + + // Deleting the cache file and Creating a new cache should not load the value from the file. + let _ = clear_cache(); + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + assert!(cache.get().is_none()); + assert!(!cache_file.exists()); +} + +#[cfg_attr( + any( + feature = "ci", // Try to run this in all ci jobs/environments + feature = "ci-jupyter-container", + feature = "ci-homebrew-container", + feature = "ci-poetry-global", + feature = "ci-poetry-project", + feature = "ci-poetry-custom", + ), + test +)] +#[allow(dead_code)] +fn verify_invalidating_cache() { + use std::{fs, time::SystemTime}; + + use pet_python_utils::{ + cache::create_cache, env::ResolvedPythonEnv, fs_cache::generate_cache_file, + }; + + setup(); + + let cache_dir = get_cache_directory().unwrap(); + let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); + let bin = prefix.join("bin"); + let python = bin.join("python"); + let python3 = bin.join("python3"); + let resolve_env = ResolvedPythonEnv { + executable: python.clone(), + version: "3.9.9".to_string(), + prefix: prefix.clone(), + is64_bit: true, + symlinks: Some(vec![python.clone(), python3.clone()]), + }; + + // Ensure the file does not exist. + let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); + let _ = fs::remove_file(&cache_file); + + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + // Store the value in cache and verify the file exists. + cache.store(resolve_env.clone()); + + assert!(cache.get().is_some()); + assert!(cache_file.exists()); + + // Next update the executable, so as to cause the mtime to change. + // As a result of this the cache should no longer be valid. + let _ = fs::write(python.clone(), format!("{:?}", SystemTime::now())); + assert!(cache.get().is_none()); + assert!(!cache_file.exists()); +} + +#[cfg_attr( + any( + feature = "ci", // Try to run this in all ci jobs/environments + feature = "ci-jupyter-container", + feature = "ci-homebrew-container", + feature = "ci-poetry-global", + feature = "ci-poetry-project", + feature = "ci-poetry-custom", + ), + test +)] +#[allow(dead_code)] +fn verify_invalidating_cache_due_to_hash_conflicts() { + use std::fs; + + use pet_python_utils::{ + cache::{clear_cache, create_cache}, + env::ResolvedPythonEnv, + fs_cache::generate_cache_file, + }; + + setup(); + + let cache_dir = get_cache_directory().unwrap(); + let prefix: PathBuf = resolve_test_path(&["unix", "executables", ".venv"]).into(); + let bin = prefix.join("bin"); + let python = bin.join("python"); + let python3 = bin.join("python3"); + let resolve_env = ResolvedPythonEnv { + executable: python.clone(), + version: "3.9.9".to_string(), + prefix: prefix.clone(), + is64_bit: true, + symlinks: Some(vec![python.clone(), python3.clone()]), + }; + + // Ensure the file does not exist. + let cache_file = generate_cache_file(&cache_dir, &resolve_env.executable); + let _ = fs::remove_file(&cache_file); + + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + // Store the value in cache and verify the file exists. + cache.store(resolve_env.clone()); + assert!(cache.get().is_some()); + assert!(cache_file.exists()); + drop(cache); + + // Simulate a hash collision by changing the executable to a different value. + // I.e. the cached file points to another executable. + let contents = fs::read_to_string(&cache_file.clone()).unwrap(); + let contents = contents.replace( + python.to_string_lossy().to_string().as_str(), + "/usr/bin/python", + ); + let contents = contents.replace( + python + .to_string_lossy() + .to_string() + .replace("\\", "\\\\") // For windows paths stored in JSON + .as_str(), + "/usr/bin/python", + ); + let contents = contents.replace( + python3.to_string_lossy().to_string().as_str(), + "/usr/bin/python3", + ); + let contents = contents.replace( + python3 + .to_string_lossy() + .to_string() + .replace("\\", "\\\\") // For windows paths stored in JSON + .as_str(), + "/usr/bin/python3", + ); + + let _ = clear_cache(); // Clear in memory cache as well as the files.. + let _ = fs::create_dir_all(&cache_dir).unwrap(); + let _ = fs::write(&cache_file, contents.clone()); // Create the cache file with the invalid details. + let cache = create_cache(resolve_env.executable.clone()); + let cache = cache.lock().unwrap(); + + assert!(cache.get().is_none()); +} diff --git a/crates/pet-python-utils/tests/unix/executables/.venv/bin/python b/crates/pet-python-utils/tests/unix/executables/.venv/bin/python index e69de29b..a59f7c83 100644 --- a/crates/pet-python-utils/tests/unix/executables/.venv/bin/python +++ b/crates/pet-python-utils/tests/unix/executables/.venv/bin/python @@ -0,0 +1 @@ +SystemTime { tv_sec: 1721186754, tv_nsec: 324707000 } \ No newline at end of file diff --git a/crates/pet/src/jsonrpc.rs b/crates/pet/src/jsonrpc.rs index 990b8fdb..d4d24f50 100644 --- a/crates/pet/src/jsonrpc.rs +++ b/crates/pet/src/jsonrpc.rs @@ -26,6 +26,7 @@ use pet_jsonrpc::{ }; use pet_poetry::Poetry; use pet_poetry::PoetryLocator; +use pet_python_utils::cache::clear_cache; use pet_python_utils::cache::set_cache_directory; use pet_reporter::collect; use pet_reporter::{cache::CacheReporter, jsonrpc}; @@ -82,7 +83,7 @@ pub fn start_jsonrpc_server() { handlers.add_request_handler("resolve", handle_resolve); handlers.add_request_handler("find", handle_find); handlers.add_request_handler("condaInfo", handle_conda_telemetry); - handlers.add_request_handler("clearCache", handle_clear_cache); + handlers.add_request_handler("clear", handle_clear_cache); start_server(&handlers) } @@ -319,6 +320,7 @@ pub fn handle_resolve(context: Arc, id: u32, params: Value) { } #[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] pub struct FindOptions { /// Search path, can be a directory or a Python executable as well. /// If passing a directory, the assumption is that its a project directory (workspace folder). @@ -384,27 +386,13 @@ pub fn handle_conda_telemetry(context: Arc, id: u32, _params: Value) { }); } -pub fn handle_clear_cache(context: Arc, id: u32, _params: Value) { +pub fn handle_clear_cache(_context: Arc, id: u32, _params: Value) { thread::spawn(move || { - if let Some(cache_directory) = context - .configuration - .read() - .unwrap() - .cache_directory - .clone() - { - if let Err(e) = std::fs::remove_dir_all(&cache_directory) { - error!("Failed to clear cache {:?}: {}", cache_directory, e); - send_error( - Some(id), - -4, - format!("Failed to clear cache {:?}: {}", cache_directory, e), - ); - } else { - info!("Cleared cache {:?}", cache_directory); - send_reply(id, None::<()>); - } + if let Err(e) = clear_cache() { + error!("Failed to clear cache {:?}", e); + send_error(Some(id), -4, format!("Failed to clear cache {:?}", e)); } else { + info!("Cleared cache"); send_reply(id, None::<()>); } }); diff --git a/crates/pet/tests/ci_homebrew_container.rs b/crates/pet/tests/ci_homebrew_container.rs index 6e868bf3..54fa0bc1 100644 --- a/crates/pet/tests/ci_homebrew_container.rs +++ b/crates/pet/tests/ci_homebrew_container.rs @@ -1,8 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::sync::Once; + mod common; +static INIT: Once = Once::new(); + +/// Setup function that is only run once, even if called multiple times. +fn setup() { + INIT.call_once(|| { + env_logger::builder() + .filter(None, log::LevelFilter::Trace) + .init(); + }); +} + #[cfg(unix)] #[cfg_attr(feature = "ci-homebrew-container", test)] #[allow(dead_code)] @@ -17,6 +30,8 @@ fn verify_python_in_homebrew_contaner() { use pet_reporter::{cache::CacheReporter, collect}; use std::{path::PathBuf, sync::Arc}; + setup(); + let reporter = Arc::new(collect::create_reporter()); let environment = EnvironmentApi::new(); let conda_locator = Arc::new(Conda::from(&environment)); diff --git a/crates/pet/tests/ci_poetry.rs b/crates/pet/tests/ci_poetry.rs index ba95fe52..8c14d3b0 100644 --- a/crates/pet/tests/ci_poetry.rs +++ b/crates/pet/tests/ci_poetry.rs @@ -1,11 +1,24 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +use std::sync::Once; + use pet_poetry::Poetry; use pet_reporter::{cache::CacheReporter, collect}; mod common; +static INIT: Once = Once::new(); + +/// Setup function that is only run once, even if called multiple times. +fn setup() { + INIT.call_once(|| { + env_logger::builder() + .filter(None, log::LevelFilter::Trace) + .init(); + }); +} + #[cfg_attr(any(feature = "ci-poetry-global", feature = "ci-poetry-custom"), test)] #[allow(dead_code)] /// This is a test with Poetry for current directory with Python 3.12 and 3.11 and envs are created in regular global cache directory @@ -20,6 +33,8 @@ fn verify_ci_poetry_global() { }; use std::{env, path::PathBuf, sync::Arc}; + setup(); + let workspace_dir = PathBuf::from(env::var("GITHUB_WORKSPACE").unwrap_or_default()); let reporter = Arc::new(collect::create_reporter()); let environment = EnvironmentApi::new(); @@ -88,6 +103,8 @@ fn verify_ci_poetry_project() { }; use std::{env, path::PathBuf, sync::Arc}; + setup(); + let workspace_dir = PathBuf::from(env::var("GITHUB_WORKSPACE").unwrap_or_default()); let reporter = Arc::new(collect::create_reporter()); let environment = EnvironmentApi::new();