diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 479902eae7..f1c06fd726 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -405,9 +405,6 @@ jobs: # This step is just to get nice GitHub annotations on the PR diff in the files-changed tab. run: cargo test -p ty_python_semantic --test mdtest || true - name: "Run tests" - env: - # Tests create disposable uv projects that must be resolved without a lockfile. - UV_LOCKED: 0 run: uv run --locked --only-dev cargo insta test --all-features --unreferenced reject --test-runner nextest --disable-nextest-doctest - name: "Run doctests" run: cargo test --doc --all-features @@ -464,9 +461,6 @@ jobs: version: "0.12.9" enable-cache: "true" - name: "Run tests" - env: - # Tests create disposable uv projects that must be resolved without a lockfile. - UV_LOCKED: 0 run: uv run --locked --only-dev cargo nextest run --cargo-profile profiling --all-features - name: "Run doctests" run: cargo test --doc --profile profiling --all-features @@ -502,9 +496,6 @@ jobs: version: "0.12.9" enable-cache: "true" - name: "Run tests" - env: - # Tests create disposable uv projects that must be resolved without a lockfile. - UV_LOCKED: 0 run: | uv run --locked --only-dev cargo nextest run --all-features --profile ci cargo test --all-features --doc diff --git a/.github/workflows/sync_typeshed.yaml b/.github/workflows/sync_typeshed.yaml index 75085dc5f4..58bf61e739 100644 --- a/.github/workflows/sync_typeshed.yaml +++ b/.github/workflows/sync_typeshed.yaml @@ -281,9 +281,6 @@ jobs: # Work around https://github.com/mitsuhiko/insta/pull/924 until its fix is released. CI: "0" MDTEST_UPDATE_SNAPSHOTS: "1" - # `UV_LOCKED: 1` will cause spurious changes to snapshots - # until https://github.com/astral-sh/ty/issues/4460 is fixed. - UV_LOCKED: 0 run: uv run --only-dev cargo insta test --accept --all-features --test-runner nextest --disable-nextest-doctest - name: Commit and push snapshot changes if: ${{ success() }} diff --git a/crates/ruff_benchmark/benches/ty_script.rs b/crates/ruff_benchmark/benches/ty_script.rs index f33cdecf79..1793a158ee 100644 --- a/crates/ruff_benchmark/benches/ty_script.rs +++ b/crates/ruff_benchmark/benches/ty_script.rs @@ -3,23 +3,18 @@ use std::time::Duration; use divan::{Bencher, bench}; use rayon::ThreadPoolBuilder; -use ruff_db::system::{OsSystem, SystemPath, TestSystem}; -use ty_project::{Db, ProjectDatabase, ProjectMetadata, ScriptEnvironmentAvailability}; +use ruff_db::system::{OsSystem, System, SystemPath, TestSystem}; +use ty_project::{ + Db, ProjectDatabase, ProjectMetadata, ScriptEnvironmentAvailability, uv_test_env_vars, +}; use ty_static::EnvVars; -fn setup_iteration(root: &SystemPath) -> ProjectDatabase { +fn setup_iteration(root: &SystemPath, uv: &SystemPath) -> ProjectDatabase { let system = TestSystem::new(OsSystem::new(root)); - for name in [ - EnvVars::VIRTUAL_ENV, - EnvVars::CONDA_PREFIX, - EnvVars::CONDA_DEFAULT_ENV, - EnvVars::CONDA_ROOT, - EnvVars::PYTHONPATH, - ] { - system.remove_env_var(name); - } + system.clear_env_vars(); + system.set_env_vars(uv_test_env_vars()); system.set_env_var(EnvVars::TY_UV, "scripts"); - system.set_env_var(EnvVars::UV, "uv"); + system.set_env_var(EnvVars::UV, uv.as_str()); let metadata = ProjectMetadata::discover(root, &system).unwrap(); ProjectDatabase::fallible(metadata, system).unwrap() @@ -29,6 +24,7 @@ fn setup_iteration(root: &SystemPath) -> ProjectDatabase { fn simple_script(bencher: Bencher) { let directory = tempfile::tempdir().unwrap(); let root = SystemPath::from_std_path(directory.path()).unwrap(); + let uv = OsSystem::default().which("uv").unwrap(); std::fs::write( root.join("script.py"), r#"# /// script @@ -50,7 +46,7 @@ def greet(user: User) -> str: .unwrap(); // Synchronize once before measuring so this benchmark covers the warm uv cache case. - let output = Command::new("uv") + let output = Command::new(uv.as_std_path()) .args(["workspace", "metadata", "--sync", "--script"]) .arg(root.join("script.py").as_std_path()) .current_dir(root.as_std_path()) @@ -63,7 +59,7 @@ def greet(user: User) -> str: ); bencher - .with_inputs(|| setup_iteration(root)) + .with_inputs(|| setup_iteration(root, &uv)) .bench_local_refs(|db| { // Include environment initialization in the measurement, as in the CLI. let environments = db.uv_environments().clone(); diff --git a/crates/ruff_db/src/system/command.rs b/crates/ruff_db/src/system/command.rs index 155e63d865..e55a1fea47 100644 --- a/crates/ruff_db/src/system/command.rs +++ b/crates/ruff_db/src/system/command.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::process::Output; use super::{Result, SystemPath, SystemPathBuf}; @@ -8,6 +9,7 @@ pub struct Command { executable: String, arguments: Vec, current_directory: Option, + environment: CommandEnv, } impl Command { @@ -17,6 +19,7 @@ impl Command { executable: executable.into(), arguments: Vec::new(), current_directory: None, + environment: CommandEnv::default(), } } @@ -42,6 +45,48 @@ impl Command { self } + /// Clears the inherited environment and any variables previously set on this command. + pub fn env_clear(&mut self) -> &mut Self { + self.environment.clear(); + self + } + + /// Sets an environment variable for the command. + pub fn env(&mut self, name: impl Into, value: impl Into) -> &mut Self { + self.environment.set(name, value); + self + } + + /// Removes an environment variable from the command. + pub fn env_remove(&mut self, name: impl Into) -> &mut Self { + self.environment.remove(name); + self + } + + /// Returns the environment variables explicitly set or removed for the command. + /// + /// Removed variables have a value of `None`. Variables inherited from the parent process + /// are not included. + #[cfg_attr( + not(feature = "os"), + expect(dead_code, reason = "available to non-OS command executors") + )] + pub(crate) fn get_envs(&self) -> impl Iterator)> { + self.environment + .vars + .iter() + .map(|(name, value)| (name.as_str(), value.as_deref())) + } + + /// Returns whether the command clears environment variables inherited from its parent process. + #[cfg_attr( + not(feature = "os"), + expect(dead_code, reason = "available to non-OS command executors") + )] + pub(crate) fn get_env_clear(&self) -> bool { + self.environment.get_clear() + } + /// Returns the executable to invoke. pub fn get_executable(&self) -> &str { &self.executable @@ -56,6 +101,59 @@ impl Command { pub fn get_current_dir(&self) -> Option<&SystemPath> { self.current_directory.as_deref() } + + pub(super) fn env_merge(&mut self, environment: &CommandEnv) { + self.environment.merge(environment); + } +} + +/// Environment changes relative to an inherited environment. +#[derive(Debug, Default)] +pub(super) struct CommandEnv { + clear: bool, + vars: BTreeMap>, +} + +impl CommandEnv { + pub(super) fn get_clear(&self) -> bool { + self.clear + } + + pub(super) fn get(&self, name: &str) -> Option<&Option> { + self.vars.get(name) + } + + pub(super) fn clear(&mut self) { + self.clear = true; + self.vars.clear(); + } + + pub(super) fn set(&mut self, name: impl Into, value: impl Into) { + self.vars.insert(name.into(), Some(value.into())); + } + + pub(super) fn remove(&mut self, name: impl Into) { + let name = name.into(); + if self.clear { + self.vars.remove(&name); + } else { + self.vars.insert(name, None); + } + } + + /// Uses `environment` as a base without replacing explicit changes. + fn merge(&mut self, environment: &Self) { + if self.clear { + return; + } + + self.clear = environment.clear; + for (name, value) in &environment.vars { + self.vars + .entry(name.clone()) + .or_insert_with(|| value.clone()); + } + } } /// Executes [`Command`]s. diff --git a/crates/ruff_db/src/system/os.rs b/crates/ruff_db/src/system/os.rs index 622a71e733..07d5525bad 100644 --- a/crates/ruff_db/src/system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -234,10 +234,22 @@ impl CommandExecutor for OsSystem { .get_current_dir() .unwrap_or_else(|| self.current_directory()); - std::process::Command::new(command.get_executable()) + let mut process = std::process::Command::new(command.get_executable()); + process .args(command.get_args()) - .current_dir(directory.as_std_path()) - .output() + .current_dir(directory.as_std_path()); + if command.get_env_clear() { + process.env_clear(); + } + + for (name, value) in command.get_envs() { + if let Some(value) = value { + process.env(name, value); + } else { + process.env_remove(name); + } + } + process.output() } fn dyn_clone(&self) -> Box { diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs index 6055cc5017..31c1e0107d 100644 --- a/crates/ruff_db/src/system/test.rs +++ b/crates/ruff_db/src/system/test.rs @@ -1,16 +1,17 @@ use ruff_notebook::{Notebook, NotebookError}; -use rustc_hash::FxHashMap; use std::panic::RefUnwindSafe; +use std::process::Output; use std::sync::{Arc, Mutex}; use crate::Db; use crate::files::File; use crate::system::{ - CommandExecutor, DirectoryEntry, MemoryFileSystem, Metadata, Result, System, SystemPath, - SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, + Command, CommandExecutor, DirectoryEntry, MemoryFileSystem, Metadata, Result, System, + SystemPath, SystemPathBuf, SystemVirtualPath, WhichError, WhichResult, }; use super::WritableSystem; +use super::command::CommandEnv; use super::walk_directory::WalkDirectoryBuilder; /// System implementation intended for testing. @@ -23,16 +24,15 @@ use super::walk_directory::WalkDirectoryBuilder; #[derive(Debug)] pub struct TestSystem { inner: Arc, - /// Environment variable overrides. If a key is present here, it takes precedence - /// over the inner system's environment variables. - env_overrides: Arc>>>, + /// Environment changes shared by system lookups and cloned command executors. + environment: Arc>, } impl Clone for TestSystem { fn clone(&self) -> Self { Self { inner: self.inner.clone(), - env_overrides: self.env_overrides.clone(), + environment: self.environment.clone(), } } } @@ -41,21 +41,36 @@ impl TestSystem { pub fn new(inner: impl WritableSystem + RefUnwindSafe + Send + Sync + 'static) -> Self { Self { inner: Arc::new(inner), - env_overrides: Arc::new(Mutex::new(FxHashMap::default())), + environment: Arc::new(Mutex::new(CommandEnv::default())), } } + /// Clears the inherited environment and any previously set variables. + pub fn clear_env_vars(&self) { + self.environment.lock().unwrap().clear(); + } + /// Sets an environment variable override. This takes precedence over the inner system. pub fn set_env_var(&self, name: impl Into, value: impl Into) { - self.env_overrides - .lock() - .unwrap() - .insert(name.into(), Some(value.into())); + self.environment.lock().unwrap().set(name, value); + } + + /// Sets multiple environment variable overrides. + pub fn set_env_vars(&self, variables: I) + where + I: IntoIterator, + K: Into, + V: Into, + { + let mut environment = self.environment.lock().unwrap(); + for (name, value) in variables { + environment.set(name, value); + } } /// Removes an environment variable override, making it appear as not set. pub fn remove_env_var(&self, name: impl Into) { - self.env_overrides.lock().unwrap().insert(name.into(), None); + self.environment.lock().unwrap().remove(name); } /// Returns the [`InMemorySystem`]. @@ -140,8 +155,20 @@ impl System for TestSystem { Err(WhichError::CannotFindBinaryPath) } + fn run_command(&self, mut command: Command) -> Result { + let system = self.system(); + let Some(executor) = system.command_executor() else { + return system.run_command(command); + }; + + command.env_merge(&self.environment.lock().unwrap()); + + executor.execute(command) + } + fn command_executor(&self) -> Option<&dyn CommandExecutor> { - self.system().command_executor() + self.system().command_executor()?; + Some(self) } fn read_directory<'a>( @@ -168,15 +195,16 @@ impl System for TestSystem { } fn env_var(&self, name: &str) -> std::result::Result { - // Check overrides first - if let Some(override_value) = self.env_overrides.lock().unwrap().get(name) { - return match override_value { - Some(value) => Ok(value.clone()), - None => Err(std::env::VarError::NotPresent), - }; + let (value, clear) = { + let environment = self.environment.lock().unwrap(); + (environment.get(name).cloned(), environment.get_clear()) + }; + match value { + Some(Some(value)) => Ok(value), + Some(None) => Err(std::env::VarError::NotPresent), + None if clear => Err(std::env::VarError::NotPresent), + None => self.system().env_var(name), } - // Fall back to inner system - self.system().env_var(name) } fn dyn_clone(&self) -> Box { @@ -184,6 +212,16 @@ impl System for TestSystem { } } +impl CommandExecutor for TestSystem { + fn execute(&self, command: Command) -> Result { + self.run_command(command) + } + + fn dyn_clone(&self) -> Box { + Box::new(self.clone()) + } +} + impl Default for TestSystem { fn default() -> Self { Self::new(InMemorySystem::default()) diff --git a/crates/ty/tests/cli/main.rs b/crates/ty/tests/cli/main.rs index c39718d8e2..52142cca4e 100644 --- a/crates/ty/tests/cli/main.rs +++ b/crates/ty/tests/cli/main.rs @@ -1001,20 +1001,6 @@ impl CliTest { command } - #[cfg(feature = "test-uv")] - pub(crate) fn command_inheriting_environment(&self) -> Command { - let mut command = Command::new(&self.ty_binary_path); - command.current_dir(&self.project_dir).arg("check"); - - // Point user config discovery at a test-local directory to avoid picking up host config. - command.env( - user_config_directory_env_var(), - self.user_config_directory(), - ); - - command - } - fn user_config_directory(&self) -> PathBuf { self.project_dir .parent() diff --git a/crates/ty/tests/cli/scripts.rs b/crates/ty/tests/cli/scripts.rs index b836dbc385..93b3f60cc8 100644 --- a/crates/ty/tests/cli/scripts.rs +++ b/crates/ty/tests/cli/scripts.rs @@ -1437,14 +1437,16 @@ mod uv_metadata { use std::{fs, process::Command}; use insta_cmd::assert_cmd_snapshot; + use ty_project::uv_test_env_vars; use ty_static::EnvVars; use crate::CliTest; use crate::uv_workspace::{uv_sync_command, write_dependency_wheel}; fn command_with_script_uv(case: &CliTest) -> Command { - let mut command = case.command_inheriting_environment(); + let mut command = case.command(); command + .envs(uv_test_env_vars()) .env(EnvVars::TY_UV, "1") .env(EnvVars::UV, "uv") .env("UV_CACHE_DIR", case.root().join("cache")); @@ -1453,6 +1455,8 @@ mod uv_metadata { fn assert_uv_supports_script_metadata() -> anyhow::Result<()> { let output = Command::new("uv") + .env_clear() + .envs(uv_test_env_vars()) .args(["workspace", "metadata", "--help"]) .output()?; @@ -1809,6 +1813,8 @@ mod uv_metadata { // come from the separate environment that uv creates for the script. let environment = case.root().join(".venv"); let output = Command::new("uv") + .env_clear() + .envs(uv_test_env_vars()) .args(["venv", "--no-project", "--python", "3.12"]) .arg(&environment) .env("UV_CACHE_DIR", case.root().join("cache")) diff --git a/crates/ty/tests/cli/uv_workspace.rs b/crates/ty/tests/cli/uv_workspace.rs index 2bac200b65..820762ce39 100644 --- a/crates/ty/tests/cli/uv_workspace.rs +++ b/crates/ty/tests/cli/uv_workspace.rs @@ -7,6 +7,8 @@ use std::{fmt::Write as _, fs::File, io::Write, path::Path, process::Command}; use insta_cmd::assert_cmd_snapshot; +#[cfg(feature = "test-uv")] +use ty_project::uv_test_env_vars; use ty_static::EnvVars; #[cfg(feature = "test-uv")] use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions}; @@ -55,6 +57,8 @@ requires-python = ">=3.8" fn uv_command(case: &CliTest) -> Command { let mut command = Command::new("uv"); command + .env_clear() + .envs(uv_test_env_vars()) .current_dir(case.root()) .env("UV_CACHE_DIR", case.root().join("cache")) .env("UV_OFFLINE", "1") @@ -79,17 +83,13 @@ pub(super) fn uv_sync_command( let mut command = case.command(); command + .envs(uv_test_env_vars()) .env("TY_UV", "1") .env("UV", "uv") .env("UV_CACHE_DIR", case.root().join("cache")) .env("UV_OFFLINE", "1") .env("UV_PYTHON_DOWNLOADS", "never") - .env("TY_OUTPUT_FORMAT", "concise") - .env("PATH", std::env::var_os("PATH").unwrap_or_default()); - #[cfg(windows)] - if let Some(path_ext) = std::env::var_os("PATHEXT") { - command.env("PATHEXT", path_ext); - } + .env("TY_OUTPUT_FORMAT", "concise"); if let Some(virtual_env) = virtual_env { command.env("VIRTUAL_ENV", virtual_env); } diff --git a/crates/ty/tests/file_watching.rs b/crates/ty/tests/file_watching.rs index 98e2dbf127..770eac0acd 100644 --- a/crates/ty/tests/file_watching.rs +++ b/crates/ty/tests/file_watching.rs @@ -20,7 +20,6 @@ use ty_project::metadata::value::{RelativeGlobPattern, RelativePathBuf}; use ty_project::watch::{ChangeEvent, ProjectWatcher, directory_watcher}; use ty_project::{ChangeResult, Db, ProjectDatabase, ProjectMetadata}; use ty_python_core::platform::PythonPlatform; -use ty_static::EnvVars; struct TestCase { db: ProjectDatabase, @@ -431,7 +430,7 @@ where let os_system = OsSystem::new(&project_path); let user_config_directory_override = os_system.with_user_config_directory(None); let system = TestSystem::new(os_system.clone()); - isolate_environment(&system); + system.clear_env_vars(); configure_system(&system); let mut setup_context = SetupContext { @@ -541,18 +540,6 @@ where Ok(test_case) } -fn isolate_environment(system: &TestSystem) { - for name in [ - EnvVars::VIRTUAL_ENV, - EnvVars::CONDA_PREFIX, - EnvVars::CONDA_DEFAULT_ENV, - EnvVars::CONDA_ROOT, - EnvVars::PYTHONPATH, - ] { - system.remove_env_var(name); - } -} - /// Dedents and updates a file's content, ensuring that its last modified time changes. fn update_file(path: impl AsRef, content: &str) -> anyhow::Result<()> { let path = path.as_ref().as_std_path(); @@ -2634,7 +2621,7 @@ mod uv_metadata { use ruff_db::diagnostic::DiagnosticId; use ruff_db::files::File; use ruff_db::system::{OsSystem, System as _}; - use ty_project::{Db, ScriptEnvironmentAvailability, UseUv, UvSyncChanges}; + use ty_project::{Db, ScriptEnvironmentAvailability, UseUv, UvSyncChanges, uv_test_env_vars}; use ty_static::EnvVars; use super::{SetupContext, TestCase, event_for_file, setup_with_system, update_file}; @@ -2893,6 +2880,8 @@ mod uv_metadata { } if use_uv == UseUv::On { let output = Command::new(uv.as_std_path()) + .env_clear() + .envs(uv_test_env_vars()) .current_dir(context.project_path()) .args(["sync", "--offline"]) .output()?; @@ -2905,6 +2894,7 @@ mod uv_metadata { Ok(()) }, |system| { + system.set_env_vars(uv_test_env_vars()); system.set_env_var( EnvVars::TY_UV, match use_uv { diff --git a/crates/ty_project/src/lib.rs b/crates/ty_project/src/lib.rs index abc0774318..10bab24b9a 100644 --- a/crates/ty_project/src/lib.rs +++ b/crates/ty_project/src/lib.rs @@ -35,7 +35,9 @@ pub use ty_python_semantic::Db as SemanticDb; use ty_python_semantic::dependency::{DependencyMetadata, DependencyProjectKind}; use ty_python_semantic::lint::RuleSelection; use uv::DependencyMetadataError; -pub use uv::{ScriptEnvironmentAvailability, UseUv, UvEnvironments, UvSyncChanges}; +pub use uv::{ + ScriptEnvironmentAvailability, UseUv, UvEnvironments, UvSyncChanges, uv_test_env_vars, +}; mod db; mod files; diff --git a/crates/ty_project/src/uv.rs b/crates/ty_project/src/uv.rs index 1bf2d22d8b..6a82c2a302 100644 --- a/crates/ty_project/src/uv.rs +++ b/crates/ty_project/src/uv.rs @@ -69,6 +69,32 @@ impl Combine for UseUv { } } +/// Host variables needed by uv integration tests to find executables, Python installations, and caches. +/// +/// Tests use this allowlist after clearing the inherited environment so host settings such as +/// `UV_LOCKED` and `PYTHONPATH` cannot affect subprocesses. +#[allow( + clippy::disallowed_methods, + reason = "Test only code, intentionally inherit variables from the host's environment." +)] +pub fn uv_test_env_vars() -> impl Iterator { + [ + "PATH", + "PATHEXT", + "SYSTEMROOT", + "HOME", + "USERPROFILE", + "XDG_DATA_HOME", + "TMPDIR", + "TEMP", + "TMP", + "UV_CACHE_DIR", + "UV_PYTHON_INSTALL_DIR", + ] + .into_iter() + .filter_map(|name| std::env::var(name).ok().map(|value| (name, value))) +} + #[cfg(test)] mod tests { use ruff_db::system::TestSystem; diff --git a/crates/ty_project/src/uv/environments.rs b/crates/ty_project/src/uv/environments.rs index 01c109a478..ee82a336e6 100644 --- a/crates/ty_project/src/uv/environments.rs +++ b/crates/ty_project/src/uv/environments.rs @@ -960,7 +960,7 @@ mod tests { use super::super::{ScriptEnvironmentAvailability, UvSyncChanges, script_environment}; use crate::db::testing::TestDb; - use crate::{Db as _, ProjectMetadata, UseUv}; + use crate::{Db as _, ProjectMetadata, UseUv, uv_test_env_vars}; #[test] fn newer_project_refresh_discards_old_metadata() -> anyhow::Result<()> { @@ -1248,6 +1248,8 @@ mod tests { fn sync_workspace(&self) -> anyhow::Result<()> { let output = Command::new(self.db.test_system().env_var(EnvVars::UV)?) + .env_clear() + .envs(uv_test_env_vars()) .current_dir(self.db.project().root(&self.db)) .args(["sync", "--offline"]) .output()?; @@ -1277,6 +1279,8 @@ mod tests { root.join("bin/python") }; let output = Command::new(python.as_std_path()) + .env_clear() + .envs(uv_test_env_vars()) .args(["-c", &format!("import {module}")]) .output()?; @@ -1298,18 +1302,11 @@ mod tests { let metadata = ProjectMetadata::new("test", root.clone()).with_use_uv(use_uv); let mut db = TestDb::new(metadata); db.use_system(OsSystem::new(&root)); + db.test_system().clear_env_vars(); + db.test_system().set_env_vars(uv_test_env_vars()); let uv = OsSystem::default().which("uv")?; db.test_system().set_env_var(EnvVars::UV, uv.as_str()); - for name in [ - EnvVars::VIRTUAL_ENV, - EnvVars::CONDA_PREFIX, - EnvVars::CONDA_DEFAULT_ENV, - EnvVars::CONDA_ROOT, - EnvVars::PYTHONPATH, - ] { - db.test_system().remove_env_var(name); - } let path = root.join(file_name); db.write_dedented(path.as_str(), source)?; diff --git a/crates/ty_server/tests/e2e/main.rs b/crates/ty_server/tests/e2e/main.rs index f0edfb3e9e..d7a6989b13 100644 --- a/crates/ty_server/tests/e2e/main.rs +++ b/crates/ty_server/tests/e2e/main.rs @@ -88,6 +88,8 @@ use rustc_hash::FxHashMap; use serde_json::{Value, json}; use tempfile::TempDir; use ty_project::UseUv; +#[cfg(feature = "test-uv")] +use ty_project::uv_test_env_vars; use ty_server::{ClientOptions, LogLevel, Server, init_logging}; /// Number of times to retry receiving a message before giving up @@ -232,6 +234,7 @@ impl TestServer { // Create test system and set environment variable overrides let test_system = Arc::new(TestSystem::new(os_system)); + test_system.clear_env_vars(); for (name, value) in env_vars { match value { Some(value) => { @@ -1279,11 +1282,7 @@ impl TestServerBuilder { test_context: TestContext::new()?, initialization_options: None, client_capabilities, - env_vars: vec![ - ("HOME".into(), None), - ("PATH".into(), None), - ("VIRTUAL_ENV".into(), None), - ], + env_vars: Vec::new(), }) } @@ -1300,8 +1299,10 @@ impl TestServerBuilder { /// Enable uv integration using the uv executable on the test process's PATH. #[cfg(feature = "test-uv")] - pub(crate) fn with_real_uv(self, use_uv: UseUv) -> Result { + pub(crate) fn with_real_uv(mut self, use_uv: UseUv) -> Result { let uv = OsSystem::default().which("uv")?; + self.env_vars + .extend(uv_test_env_vars().map(|(name, value)| (name.to_owned(), Some(value)))); Ok(self.with_use_uv(use_uv).with_env_var("UV", uv.as_str())) } diff --git a/crates/ty_server/tests/e2e/publish_diagnostics.rs b/crates/ty_server/tests/e2e/publish_diagnostics.rs index f68a2a384a..c4fd056be4 100644 --- a/crates/ty_server/tests/e2e/publish_diagnostics.rs +++ b/crates/ty_server/tests/e2e/publish_diagnostics.rs @@ -968,6 +968,8 @@ mod uv_metadata { use ruff_db::system::SystemPath; use serde_json::json; use ty_project::UseUv; + #[cfg(feature = "test-uv")] + use ty_project::uv_test_env_vars; use crate::TestServerBuilder; @@ -1008,6 +1010,8 @@ package = "invalid" server.write_file("src/pyproject.toml", manifest)?; let output = Command::new("uv") + .env_clear() + .envs(uv_test_env_vars()) .current_dir(server.file_path("src")) .args(["sync", "--offline"]) .output()?; diff --git a/hawk.toml b/hawk.toml index abe71d8b70..e2e23fb3a1 100644 --- a/hawk.toml +++ b/hawk.toml @@ -878,6 +878,30 @@ kind = "inherent_method" level = "expect" reason = "command descriptions expose a complete argument builder and inspection API" +[[override]] +lint = "hawk::dead_public" +crate = "ruff_db" +item = "system::command::Command::env_clear" +kind = "inherent_method" +level = "expect" +reason = "command environment methods mirror the standard library Command API" + +[[override]] +lint = "hawk::dead_public" +crate = "ruff_db" +item = "system::command::Command::env" +kind = "inherent_method" +level = "expect" +reason = "command environment methods mirror the standard library Command API" + +[[override]] +lint = "hawk::dead_public" +crate = "ruff_db" +item = "system::command::Command::env_remove" +kind = "inherent_method" +level = "expect" +reason = "command environment methods mirror the standard library Command API" + [[override]] lint = "hawk::dead_public" crate = "ty_python_core"