diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index f7102dd5342a..19556a38e30c 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -311,6 +311,30 @@ jobs: run: npm run test:functional if: matrix.test-suite == 'functional' + native-tests: + name: Native Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + path: ${{ env.special-working-directory-relative }} + + - name: Native Locator tests + run: cargo test + working-directory: ${{ env.special-working-directory }}/native_locator + smoke-tests: name: Smoke tests # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. diff --git a/native_locator/Cargo.toml b/native_locator/Cargo.toml index f20396b09a9d..29fdba160ce9 100644 --- a/native_locator/Cargo.toml +++ b/native_locator/Cargo.toml @@ -7,7 +7,13 @@ edition = "2021" winreg = "0.52.0" [dependencies] -serde = {version ="1.0.152", features = ["derive"]} +serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" serde_repr = "0.1.10" regex = "1.10.4" + +[features] +test = [] + +[lib] +doctest = false diff --git a/native_locator/src/common_python.rs b/native_locator/src/common_python.rs index c5482bbfc16a..f3fd8d682009 100644 --- a/native_locator/src/common_python.rs +++ b/native_locator/src/common_python.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::known; use crate::messaging; use crate::utils; use std::env; @@ -20,10 +21,10 @@ fn get_env_path(path: &str) -> Option { } } -fn report_path_python(path: &str) { +fn report_path_python(dispatcher: &mut impl messaging::MessageDispatcher, path: &str) { let version = utils::get_version(path); let env_path = get_env_path(path); - messaging::send_message(messaging::PythonEnvironment::new( + dispatcher.send_message(messaging::PythonEnvironment::new( "Python".to_string(), vec![path.to_string()], "System".to_string(), @@ -33,20 +34,26 @@ fn report_path_python(path: &str) { )); } -fn report_python_on_path() { - let bin = if cfg!(windows) { - "python.exe" - } else { - "python" - }; - if let Ok(paths) = env::var("PATH") { +fn report_python_on_path( + dispatcher: &mut impl messaging::MessageDispatcher, + environment: &impl known::Environment, +) { + if let Some(paths) = environment.get_env_var("PATH".to_string()) { + let bin = if cfg!(windows) { + "python.exe" + } else { + "python" + }; env::split_paths(&paths) .map(|p| p.join(bin)) .filter(|p| p.exists()) - .for_each(|full_path| report_path_python(full_path.to_str().unwrap())); + .for_each(|full_path| report_path_python(dispatcher, full_path.to_str().unwrap())); } } -pub fn find_and_report() { - report_python_on_path(); +pub fn find_and_report( + dispatcher: &mut impl messaging::MessageDispatcher, + environment: &impl known::Environment, +) { + report_python_on_path(dispatcher, environment); } diff --git a/native_locator/src/conda.rs b/native_locator/src/conda.rs index db0bc3bc10f1..1bb579e5d218 100644 --- a/native_locator/src/conda.rs +++ b/native_locator/src/conda.rs @@ -84,7 +84,6 @@ fn get_version_from_meta_json(json_file: &Path) -> Option { fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option { let package_name = format!("{}-", package); let conda_meta_path = get_conda_meta_path(any_path)?; - std::fs::read_dir(conda_meta_path).ok()?.find_map(|entry| { let path = entry.ok()?.path(); let file_name = path.file_name()?.to_string_lossy(); @@ -97,6 +96,7 @@ fn get_conda_package_json_path(any_path: &Path, package: &str) -> Option bool { let conda_python_json_path = get_conda_package_json_path(any_path, "python"); match conda_python_json_path { @@ -127,11 +127,9 @@ fn get_conda_bin_names() -> Vec<&'static str> { } /// Find the conda binary on the PATH environment variable -fn find_conda_binary_on_path() -> Option { - let paths = env::var("PATH").ok()?; - let paths = env::split_paths(&paths); - for path in paths { - let path = Path::new(&path); +fn find_conda_binary_on_path(environment: &impl known::Environment) -> Option { + let paths = environment.get_env_var("PATH".to_string())?; + for path in env::split_paths(&paths) { for bin in get_conda_bin_names() { let conda_path = path.join(bin); match std::fs::metadata(&conda_path) { @@ -161,11 +159,13 @@ fn find_python_binary_path(env_path: &Path) -> Option { } #[cfg(windows)] -fn get_known_conda_locations() -> Vec { - let user_profile = env::var("USERPROFILE").unwrap(); - let program_data = env::var("PROGRAMDATA").unwrap(); - let all_user_profile = env::var("ALLUSERSPROFILE").unwrap(); - let home_drive = env::var("HOMEDRIVE").unwrap(); +fn get_known_conda_locations(environment: &impl known::Environment) -> Vec { + let user_profile = environment.get_env_var("USERPROFILE".to_string()).unwrap(); + let program_data = environment.get_env_var("PROGRAMDATA".to_string()).unwrap(); + let all_user_profile = environment + .get_env_var("ALLUSERSPROFILE".to_string()) + .unwrap(); + let home_drive = environment.get_env_var("HOMEDRIVE".to_string()).unwrap(); let mut known_paths = vec![ Path::new(&user_profile).join("Anaconda3\\Scripts"), Path::new(&program_data).join("Anaconda3\\Scripts"), @@ -176,12 +176,12 @@ fn get_known_conda_locations() -> Vec { Path::new(&all_user_profile).join("Miniconda3\\Scripts"), Path::new(&home_drive).join("Miniconda3\\Scripts"), ]; - known_paths.append(&mut known::get_know_global_search_locations()); + known_paths.append(&mut environment.get_know_global_search_locations()); known_paths } #[cfg(unix)] -fn get_known_conda_locations() -> Vec { +fn get_known_conda_locations(environment: &impl known::Environment) -> Vec { let mut known_paths = vec![ PathBuf::from("/opt/anaconda3/bin"), PathBuf::from("/opt/miniconda3/bin"), @@ -202,14 +202,14 @@ fn get_known_conda_locations() -> Vec { PathBuf::from("/anaconda3/bin"), PathBuf::from("/miniconda3/bin"), ]; - known_paths.append(&mut known::get_know_global_search_locations()); + known_paths.append(&mut environment.get_know_global_search_locations()); known_paths } /// Find conda binary in known locations -fn find_conda_binary_in_known_locations() -> Option { +fn find_conda_binary_in_known_locations(environment: &impl known::Environment) -> Option { let conda_bin_names = get_conda_bin_names(); - let known_locations = get_known_conda_locations(); + let known_locations = get_known_conda_locations(environment); for location in known_locations { for bin in &conda_bin_names { let conda_path = location.join(bin); @@ -223,17 +223,17 @@ fn find_conda_binary_in_known_locations() -> Option { } /// Find the conda binary on the system -pub fn find_conda_binary() -> Option { - let conda_binary_on_path = find_conda_binary_on_path(); +pub fn find_conda_binary(environment: &impl known::Environment) -> Option { + let conda_binary_on_path = find_conda_binary_on_path(environment); match conda_binary_on_path { Some(conda_binary_on_path) => Some(conda_binary_on_path), - None => find_conda_binary_in_known_locations(), + None => find_conda_binary_in_known_locations(environment), } } -fn get_conda_envs_from_environment_txt() -> Vec { +fn get_conda_envs_from_environment_txt(environment: &impl known::Environment) -> Vec { let mut envs = vec![]; - let home = known::get_user_home(); + let home = environment.get_user_home(); match home { Some(home) => { let home = Path::new(&home); @@ -252,9 +252,12 @@ fn get_conda_envs_from_environment_txt() -> Vec { envs } -fn get_known_env_locations(conda_bin: PathBuf) -> Vec { +fn get_known_env_locations( + conda_bin: PathBuf, + environment: &impl known::Environment, +) -> Vec { let mut paths = vec![]; - let home = known::get_user_home(); + let home = environment.get_user_home(); match home { Some(home) => { let home = Path::new(&home); @@ -284,9 +287,12 @@ fn get_known_env_locations(conda_bin: PathBuf) -> Vec { paths } -fn get_conda_envs_from_known_env_locations(conda_bin: PathBuf) -> Vec { +fn get_conda_envs_from_known_env_locations( + conda_bin: PathBuf, + environment: &impl known::Environment, +) -> Vec { let mut envs = vec![]; - for location in get_known_env_locations(conda_bin) { + for location in get_known_env_locations(conda_bin, environment) { if is_conda_environment(&Path::new(&location)) { envs.push(location.to_string()); } @@ -324,14 +330,18 @@ struct CondaEnv { path: PathBuf, } -fn get_distinct_conda_envs(conda_bin: PathBuf) -> Vec { - let mut envs = get_conda_envs_from_environment_txt(); - let mut known_envs = get_conda_envs_from_known_env_locations(conda_bin.to_path_buf()); +fn get_distinct_conda_envs( + conda_bin: PathBuf, + environment: &impl known::Environment, +) -> Vec { + let mut envs = get_conda_envs_from_environment_txt(environment); + let mut known_envs = + get_conda_envs_from_known_env_locations(conda_bin.to_path_buf(), environment); envs.append(&mut known_envs); envs.sort(); envs.dedup(); - let locations = get_known_env_locations(conda_bin); + let locations = get_known_env_locations(conda_bin, environment); let mut conda_envs = vec![]; for env in envs { let env = Path::new(&env); @@ -367,16 +377,19 @@ fn get_distinct_conda_envs(conda_bin: PathBuf) -> Vec { conda_envs } -pub fn find_and_report() { - let conda_binary = find_conda_binary(); +pub fn find_and_report( + dispatcher: &mut impl messaging::MessageDispatcher, + environment: &impl known::Environment, +) { + let conda_binary = find_conda_binary(environment); match conda_binary { Some(conda_binary) => { let params = messaging::EnvManager::new(vec![conda_binary.to_string_lossy().to_string()], None); let message = messaging::EnvManagerMessage::new(params); - messaging::send_message(message); + dispatcher.send_message(message); - let envs = get_distinct_conda_envs(conda_binary.to_path_buf()); + let envs = get_distinct_conda_envs(conda_binary.to_path_buf(), environment); for env in envs { let executable = find_python_binary_path(Path::new(&env.path)); let params = messaging::PythonEnvironment::new( @@ -407,7 +420,7 @@ pub fn find_and_report() { Some(env.path.to_string_lossy().to_string()), ); let message = messaging::PythonEnvironmentMessage::new(params); - messaging::send_message(message); + dispatcher.send_message(message); } } None => (), diff --git a/native_locator/src/known.rs b/native_locator/src/known.rs index d1d09e8aeda6..8c2fdb4386e1 100644 --- a/native_locator/src/known.rs +++ b/native_locator/src/known.rs @@ -2,33 +2,64 @@ // Licensed under the MIT License. use std::{env, path::PathBuf}; +pub trait Environment { + fn get_user_home(&self) -> Option; + fn get_env_var(&self, key: String) -> Option; + fn get_know_global_search_locations(&self) -> Vec; +} + +pub struct EnvironmentApi {} + #[cfg(windows)] -pub fn get_know_global_search_locations() -> Vec { - vec![] +impl Environment for EnvironmentApi { + fn get_user_home(&self) -> Option { + get_user_home() + } + fn get_env_var(&self, key: String) -> Option { + get_env_var(key) + } + fn get_know_global_search_locations(&self) -> Vec { + vec![] + } } #[cfg(unix)] -pub fn get_know_global_search_locations() -> Vec { - vec![ - PathBuf::from("/usr/bin"), - PathBuf::from("/usr/local/bin"), - PathBuf::from("/bin"), - PathBuf::from("/home/bin"), - PathBuf::from("/sbin"), - PathBuf::from("/usr/sbin"), - PathBuf::from("/usr/local/sbin"), - PathBuf::from("/home/sbin"), - PathBuf::from("/opt"), - PathBuf::from("/opt/bin"), - PathBuf::from("/opt/sbin"), - PathBuf::from("/opt/homebrew/bin"), - ] +impl Environment for EnvironmentApi { + fn get_user_home(&self) -> Option { + get_user_home() + } + fn get_env_var(&self, key: String) -> Option { + get_env_var(key) + } + fn get_know_global_search_locations(&self) -> Vec { + vec![ + PathBuf::from("/usr/bin"), + PathBuf::from("/usr/local/bin"), + PathBuf::from("/bin"), + PathBuf::from("/home/bin"), + PathBuf::from("/sbin"), + PathBuf::from("/usr/sbin"), + PathBuf::from("/usr/local/sbin"), + PathBuf::from("/home/sbin"), + PathBuf::from("/opt"), + PathBuf::from("/opt/bin"), + PathBuf::from("/opt/sbin"), + PathBuf::from("/opt/homebrew/bin"), + ] + } } -pub fn get_user_home() -> Option { +fn get_user_home() -> Option { let home = env::var("HOME").or_else(|_| env::var("USERPROFILE")); match home { Ok(home) => Some(home), Err(_) => None, } } + +fn get_env_var(key: String) -> Option { + match env::var(key) { + Ok(path) => Some(path), + Err(_) => None, + } +} diff --git a/native_locator/src/lib.rs b/native_locator/src/lib.rs new file mode 100644 index 000000000000..d95a4300d253 --- /dev/null +++ b/native_locator/src/lib.rs @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub mod messaging; +pub mod utils; +pub mod common_python; +pub mod logging; +pub mod conda; +pub mod known; diff --git a/native_locator/src/logging.rs b/native_locator/src/logging.rs index 7dc2b495442a..66532ff67eff 100644 --- a/native_locator/src/logging.rs +++ b/native_locator/src/logging.rs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -use crate::messaging; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, PartialEq, Debug, Eq, Clone)] @@ -40,23 +39,3 @@ impl LogMessage { } } } - -pub fn log_debug(message: &str) { - messaging::send_message(LogMessage::new(message.to_string(), LogLevel::Debug)); -} - -pub fn log_info(message: &str) { - messaging::send_message(LogMessage::new(message.to_string(), LogLevel::Info)); -} - -pub fn log_warning(message: &str) { - messaging::send_message(LogMessage::new(message.to_string(), LogLevel::Warning)); -} - -pub fn log_error(message: &str) { - messaging::send_message(LogMessage::new(message.to_string(), LogLevel::Error)); -} - -pub fn log_msg(message: &str, level: LogLevel) { - messaging::send_message(LogMessage::new(message.to_string(), level)); -} diff --git a/native_locator/src/main.rs b/native_locator/src/main.rs index aa339e4e2b6d..8a11101404dd 100644 --- a/native_locator/src/main.rs +++ b/native_locator/src/main.rs @@ -3,6 +3,9 @@ use std::time::SystemTime; +use known::EnvironmentApi; +use messaging::{create_dispatcher, MessageDispatcher}; + mod common_python; mod conda; mod known; @@ -12,30 +15,33 @@ mod utils; mod windows_python; fn main() { + let mut dispatcher = create_dispatcher(); + let environment = EnvironmentApi {}; + + dispatcher.log_info("Starting Native Locator"); let now = SystemTime::now(); - logging::log_info("Starting Native Locator"); // Finds python on PATH - common_python::find_and_report(); + common_python::find_and_report(&mut dispatcher, &environment); // Finds conda binary and conda environments - conda::find_and_report(); + conda::find_and_report(&mut dispatcher, &environment); // Finds Windows Store, Known Path, and Registry pythons #[cfg(windows)] - windows_python::find_and_report(); + windows_python::find_and_report(&mut dispatcher, &known_paths); match now.elapsed() { Ok(elapsed) => { - logging::log_info(&format!( + dispatcher.log_info(&format!( "Native Locator took {} milliseconds.", elapsed.as_millis() )); } Err(e) => { - logging::log_error(&format!("Error getting elapsed time: {:?}", e)); + dispatcher.log_error(&format!("Error getting elapsed time: {:?}", e)); } } - messaging::send_message(messaging::ExitMessage::new()); + dispatcher.send_message(messaging::ExitMessage::new()); } diff --git a/native_locator/src/messaging.rs b/native_locator/src/messaging.rs index 540ba0595988..a433604f059a 100644 --- a/native_locator/src/messaging.rs +++ b/native_locator/src/messaging.rs @@ -1,8 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +use crate::logging::{LogLevel, LogMessage}; use serde::{Deserialize, Serialize}; +pub trait MessageDispatcher { + fn send_message(&mut self, message: T) -> (); + fn log_debug(&mut self, message: &str) -> (); + fn log_info(&mut self, message: &str) -> (); + fn log_warning(&mut self, message: &str) -> (); + fn log_error(&mut self, message: &str) -> (); +} + #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EnvManager { @@ -59,7 +68,7 @@ impl PythonEnvironment { ) -> Self { Self { name, - python_executable_path: python_executable_path, + python_executable_path, category, version, activated_run, @@ -104,15 +113,30 @@ impl ExitMessage { } } -fn send_rpc_message(message: String) -> () { - print!( - "Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}", - message.len(), - message - ); +pub struct JsonRpcDispatcher {} +impl MessageDispatcher for JsonRpcDispatcher { + fn send_message(&mut self, message: T) -> () { + let message = serde_json::to_string(&message).unwrap(); + print!( + "Content-Length: {}\r\nContent-Type: application/vscode-jsonrpc; charset=utf-8\r\n\r\n{}", + message.len(), + message + ); + } + fn log_debug(&mut self, message: &str) -> () { + self.send_message(LogMessage::new(message.to_string(), LogLevel::Debug)); + } + fn log_error(&mut self, message: &str) -> () { + self.send_message(LogMessage::new(message.to_string(), LogLevel::Error)); + } + fn log_info(&mut self, message: &str) -> () { + self.send_message(LogMessage::new(message.to_string(), LogLevel::Info)); + } + fn log_warning(&mut self, message: &str) -> () { + self.send_message(LogMessage::new(message.to_string(), LogLevel::Warning)); + } } -pub fn send_message(message: T) -> () { - let message = serde_json::to_string(&message).unwrap(); - send_rpc_message(message); +pub fn create_dispatcher() -> JsonRpcDispatcher { + JsonRpcDispatcher {} } diff --git a/native_locator/src/utils.rs b/native_locator/src/utils.rs index 001f56c815ca..d3573e3190a1 100644 --- a/native_locator/src/utils.rs +++ b/native_locator/src/utils.rs @@ -3,7 +3,7 @@ use std::process::Command; -pub fn get_version(path: &str) -> Option { +fn get_version_impl(path: &str) -> Option { let output = Command::new(path) .arg("-c") .arg("import sys; print(sys.version)") @@ -14,3 +14,21 @@ pub fn get_version(path: &str) -> Option { let output = output.split_whitespace().next().unwrap_or(output); Some(output.to_string()) } + +#[cfg(not(feature = "test"))] +pub fn get_version(path: &str) -> Option { + get_version_impl(path) +} + +// Tests + +#[cfg(feature = "test")] +pub fn get_version(path: &str) -> Option { + use std::path::PathBuf; + let version_file = PathBuf::from(path.to_owned() + ".version"); + if version_file.exists() { + let version = std::fs::read_to_string(version_file).ok()?; + return Some(version.trim().to_string()); + } + get_version_impl(path) +} diff --git a/native_locator/src/windows_python.rs b/native_locator/src/windows_python.rs index 0cb49d975685..5f5d53fafa2e 100644 --- a/native_locator/src/windows_python.rs +++ b/native_locator/src/windows_python.rs @@ -6,9 +6,9 @@ use crate::messaging; use crate::utils; use std::path::Path; -fn report_path_python(path: &str) { +fn report_path_python(path: &str, dispatcher: &mut impl messaging::MessageDispatcher) { let version = utils::get_version(path); - messaging::send_message(messaging::PythonEnvironment::new( + dispatcher.send_message(messaging::PythonEnvironment::new( "Python".to_string(), vec![path.to_string()], "WindowsStore".to_string(), @@ -18,8 +18,11 @@ fn report_path_python(path: &str) { )); } -fn report_windows_store_python() { - let home = known::get_user_home(); +fn report_windows_store_python( + dispatcher: &mut impl messaging::MessageDispatcher, + environment: &impl known::Environment, +) { + let home = environment.get_user_home(); match home { Some(home) => { let apps_path = Path::new(&home) @@ -38,7 +41,7 @@ fn report_windows_store_python() { Some(name) => { let name = name.to_string_lossy().to_lowercase(); if name.starts_with("python3.") && name.ends_with(".exe") { - report_path_python(&path.to_string_lossy()); + report_path_python(&path.to_string_lossy(), dispatcher); } } None => {} @@ -57,7 +60,11 @@ fn report_windows_store_python() { fn report_registry_pythons() {} -pub fn find_and_report() { - report_windows_store_python(); +#[allow(dead_code)] +pub fn find_and_report( + dispatcher: &mut impl messaging::MessageDispatcher, + environment: &impl known::Environment, +) { + report_windows_store_python(dispatcher, environment); report_registry_pythons(); } diff --git a/native_locator/tests/common.rs b/native_locator/tests/common.rs new file mode 100644 index 000000000000..5ff67224d258 --- /dev/null +++ b/native_locator/tests/common.rs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::{collections::HashMap, path::PathBuf}; + +use python_finder::{known::Environment, messaging::MessageDispatcher}; +use serde_json::Value; + +#[allow(dead_code)] +pub fn test_file_path(paths: &[&str]) -> String { + // let parts: Vec = paths.iter().map(|p| p.to_string()).collect(); + let mut root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + + paths.iter().for_each(|p| root.push(p)); + + root.to_string_lossy().to_string() +} + +#[allow(dead_code)] +pub fn join_test_paths(paths: &[&str]) -> String { + let path: PathBuf = paths.iter().map(|p| p.to_string()).collect(); + path.to_string_lossy().to_string() +} + +pub struct TestDispatcher { + pub messages: Vec, +} +pub trait TestMessages { + fn get_messages(&self) -> Vec; +} + +#[allow(dead_code)] +pub fn create_test_dispatcher() -> TestDispatcher { + impl MessageDispatcher for TestDispatcher { + fn send_message(&mut self, message: T) -> () { + self.messages.push(serde_json::to_string(&message).unwrap()); + } + fn log_debug(&mut self, _message: &str) -> () {} + fn log_error(&mut self, _message: &str) -> () {} + fn log_info(&mut self, _message: &str) -> () {} + fn log_warning(&mut self, _message: &str) -> () {} + } + impl TestMessages for TestDispatcher { + fn get_messages(&self) -> Vec { + self.messages.clone() + } + } + TestDispatcher { + messages: Vec::new(), + } +} +pub struct TestEnvironment { + vars: HashMap, + home: Option, + globals_locations: Vec, +} +#[allow(dead_code)] +pub fn create_test_environment( + vars: HashMap, + home: Option, + globals_locations: Vec, +) -> TestEnvironment { + impl Environment for TestEnvironment { + fn get_env_var(&self, key: String) -> Option { + self.vars.get(&key).cloned() + } + fn get_user_home(&self) -> Option { + self.home.clone() + } + fn get_know_global_search_locations(&self) -> Vec { + self.globals_locations.clone() + } + } + TestEnvironment { + vars, + home, + globals_locations, + } +} + +#[allow(dead_code)] +pub fn assert_messages(expected_json: &[Value], dispatcher: &TestDispatcher) { + assert_eq!( + expected_json.len(), + dispatcher.messages.len(), + "Incorrect number of messages" + ); + + if expected_json.len() == 0 { + return; + } + + let actual: serde_json::Value = serde_json::from_str(dispatcher.messages[0].as_str()).unwrap(); + assert_eq!(expected_json[0], actual); +} diff --git a/native_locator/tests/common_python_test.rs b/native_locator/tests/common_python_test.rs new file mode 100644 index 000000000000..a05be51e7218 --- /dev/null +++ b/native_locator/tests/common_python_test.rs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; + +#[test] +#[cfg(unix)] +fn find_python_in_path_this() { + use crate::common::{ + assert_messages, create_test_dispatcher, create_test_environment, join_test_paths, + test_file_path, + }; + use python_finder::common_python; + use serde_json::json; + use std::collections::HashMap; + + let unix_python = test_file_path(&["tests/unix/known"]); + let unix_python_exe = join_test_paths(&[unix_python.as_str(), "python"]); + + let mut dispatcher = create_test_dispatcher(); + let known = create_test_environment( + HashMap::from([("PATH".to_string(), unix_python.clone())]), + Some(unix_python.clone()), + Vec::new(), + ); + + common_python::find_and_report(&mut dispatcher, &known); + + assert_eq!(dispatcher.messages.len(), 1); + let expected_json = json!({"name":"Python","pythonExecutablePath":[unix_python_exe.clone()],"category":"System","version":null,"activatedRun":null,"envPath":unix_python.clone()}); + assert_messages(&[expected_json], &dispatcher); +} diff --git a/native_locator/tests/conda_test.rs b/native_locator/tests/conda_test.rs new file mode 100644 index 000000000000..ef4d72c6c552 --- /dev/null +++ b/native_locator/tests/conda_test.rs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; + +#[test] +#[cfg(unix)] +fn does_not_find_any_conda_envs() { + use crate::common::{create_test_dispatcher, create_test_environment}; + use python_finder::conda; + use std::collections::HashMap; + + let mut dispatcher = create_test_dispatcher(); + let known = create_test_environment( + HashMap::from([("PATH".to_string(), "".to_string())]), + Some("SOME_BOGUS_HOME_DIR".to_string()), + Vec::new(), + ); + + conda::find_and_report(&mut dispatcher, &known); + + assert_eq!(dispatcher.messages.len(), 0); +} + +#[test] +#[cfg(unix)] +fn find_conda_exe_and_empty_envs() { + use crate::common::{ + assert_messages, create_test_dispatcher, create_test_environment, join_test_paths, + test_file_path, + }; + use python_finder::conda; + use serde_json::json; + use std::collections::HashMap; + let conda_dir = test_file_path(&["tests/unix/conda_without_envs"]); + + let mut dispatcher = create_test_dispatcher(); + let known = create_test_environment( + HashMap::from([("PATH".to_string(), conda_dir.clone())]), + Some("SOME_BOGUS_HOME_DIR".to_string()), + Vec::new(), + ); + + conda::find_and_report(&mut dispatcher, &known); + + let conda_exe = join_test_paths(&[conda_dir.clone().as_str(), "conda"]); + let expected_json = json!({"jsonrpc":"2.0","method":"envManager","params":{"executablePath":[conda_exe.clone()],"version":null}}); + assert_messages(&[expected_json], &dispatcher) +} +#[test] +#[cfg(unix)] +fn finds_two_conda_envs_from_txt() { + use crate::common::{ + assert_messages, create_test_dispatcher, create_test_environment, join_test_paths, + test_file_path, + }; + use python_finder::conda; + use serde_json::json; + use std::collections::HashMap; + use std::fs; + + let conda_dir = test_file_path(&["tests/unix/conda"]); + let conda_1 = join_test_paths(&[conda_dir.clone().as_str(), "envs/one"]); + let conda_2 = join_test_paths(&[conda_dir.clone().as_str(), "envs/two"]); + let _ = fs::write( + "tests/unix/conda/.conda/environments.txt", + format!("{}\n{}", conda_1.clone(), conda_2.clone()), + ); + + let mut dispatcher = create_test_dispatcher(); + let known = create_test_environment( + HashMap::from([("PATH".to_string(), conda_dir.clone())]), + Some(conda_dir.clone()), + Vec::new(), + ); + + conda::find_and_report(&mut dispatcher, &known); + + let conda_exe = join_test_paths(&[conda_dir.clone().as_str(), "conda"]); + let conda_1_exe = join_test_paths(&[conda_1.clone().as_str(), "python"]); + let conda_2_exe = join_test_paths(&[conda_2.clone().as_str(), "python"]); + + let expected_conda_env = json!({"jsonrpc":"2.0","method":"envManager","params":{"executablePath":[conda_exe.clone()],"version":null}}); + let expected_conda_1 = json!({"jsonrpc":"2.0","method":"pythonEnvironment","params":{"name":"envs/one","pythonExecutablePath":[conda_1_exe.clone()],"category":"conda","version":"10.0.1","activatedRun":[conda_exe.clone(),"run","-n","envs/one","python"],"envPath":conda_1.clone()}}); + let expected_conda_2 = json!({"jsonrpc":"2.0","method":"pythonEnvironment","params":{"name":"envs/two","pythonExecutablePath":[conda_2_exe.clone()],"category":"conda","version":null,"activatedRun":[conda_exe.clone(),"run","-n","envs/two","python"],"envPath":conda_2.clone()}}); + assert_messages( + &[expected_conda_env, expected_conda_1, expected_conda_2], + &dispatcher, + ) +} diff --git a/native_locator/tests/unix/conda/.conda/environments.txt b/native_locator/tests/unix/conda/.conda/environments.txt new file mode 100644 index 000000000000..908019719b55 --- /dev/null +++ b/native_locator/tests/unix/conda/.conda/environments.txt @@ -0,0 +1,2 @@ +/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/envs/one +/Users/donjayamanne/Development/vsc/vscode-python/native_locator/tests/unix/conda/envs/two \ No newline at end of file diff --git a/native_locator/tests/unix/conda/conda b/native_locator/tests/unix/conda/conda new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json b/native_locator/tests/unix/conda/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json new file mode 100644 index 000000000000..23127993ac05 --- /dev/null +++ b/native_locator/tests/unix/conda/envs/one/conda-meta/python-10.0.1-hdf0ec26_0_cpython.json @@ -0,0 +1 @@ +10.1.1 diff --git a/native_locator/tests/unix/conda/envs/one/python b/native_locator/tests/unix/conda/envs/one/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda/envs/two/python b/native_locator/tests/unix/conda/envs/two/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda_without_envs/.conda/environments.txt b/native_locator/tests/unix/conda_without_envs/.conda/environments.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/conda_without_envs/conda b/native_locator/tests/unix/conda_without_envs/conda new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/known/python b/native_locator/tests/unix/known/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/native_locator/tests/unix/known/python.version b/native_locator/tests/unix/known/python.version new file mode 100644 index 000000000000..4044f90867df --- /dev/null +++ b/native_locator/tests/unix/known/python.version @@ -0,0 +1 @@ +12.0.0