From 68d983988df586bf518cd884915a7fdb2e79ca8b Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Wed, 23 Sep 2026 13:37:26 -0400 Subject: [PATCH 1/3] Avoid running `uv format` in untrusted workspaces Summary -- This PR adds the `--untrusted-workspace` flag to the server CLI, which informs the server that it's running in an untrusted workspace. When this is the case, we fall back on the internal formatting backend instead of using `uv format` to avoid reading uv configuration and potentially installing packages based on it. Test Plan -- New server tests --- Cargo.lock | 1 + .../language-server-threat-model.md | 1 + crates/ruff/src/args.rs | 12 ++++ crates/ruff/src/commands/server.rs | 8 ++- crates/ruff/src/lib.rs | 2 +- crates/ruff_server/Cargo.toml | 1 + crates/ruff_server/src/lib.rs | 11 ++- crates/ruff_server/src/server.rs | 4 +- .../src/server/api/requests/format.rs | 10 +-- .../src/server/api/requests/format_range.rs | 5 +- crates/ruff_server/src/session.rs | 71 ++++++++++++++++++- crates/ruff_server/tests/e2e/main.rs | 10 ++- docs/editors/settings.md | 4 ++ 13 files changed, 119 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8d6f469924586..0a9201a5650038 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3756,6 +3756,7 @@ dependencies = [ "shellexpand", "smallvec", "tempfile", + "test-case", "thiserror 2.0.20", "toml 1.1.4+spec-1.1.0", "tracing", diff --git a/agents/references/language-server-threat-model.md b/agents/references/language-server-threat-model.md index fb52c81bff7a8d..3080be35893c36 100644 --- a/agents/references/language-server-threat-model.md +++ b/agents/references/language-server-threat-model.md @@ -14,6 +14,7 @@ when deciding which inputs are trusted. Browser integrations have a separate - **Trusted local input:** the editor, its extensions and their bundled files, the local machine and its file system, and the user's configuration and trust decisions. +Ruff treats workspaces as trusted unless the server starts with `--untrusted-workspace`. ty treats workspaces as trusted unless `untrustedWorkspace` is true at initialization. Trust extends to everything in the workspace, including source code, configuration, notebooks, and the targets of symlinks that point outside it. diff --git a/crates/ruff/src/args.rs b/crates/ruff/src/args.rs index 4b3cfa131d535f..de5d497a688aaa 100644 --- a/crates/ruff/src/args.rs +++ b/crates/ruff/src/args.rs @@ -627,6 +627,10 @@ pub struct FormatCommand { #[derive(Copy, Clone, Debug, clap::Parser)] pub struct ServerCommand { + /// Treat all workspaces as untrusted, disabling the uv formatter backend. + #[arg(long)] + untrusted_workspace: bool, + /// Enable preview mode. Use `--no-preview` to disable. /// /// This enables unstable server features and turns on the preview mode for the linter @@ -638,6 +642,14 @@ pub struct ServerCommand { } impl ServerCommand { + pub(crate) fn resolve_workspace_trust(self) -> ruff_server::WorkspaceTrust { + if self.untrusted_workspace { + ruff_server::WorkspaceTrust::Untrusted + } else { + ruff_server::WorkspaceTrust::Trusted + } + } + pub(crate) fn resolve_preview(self) -> Option { resolve_bool_arg(self.preview, self.no_preview) } diff --git a/crates/ruff/src/commands/server.rs b/crates/ruff/src/commands/server.rs index 837662e7d3c211..eba983ccb8f129 100644 --- a/crates/ruff/src/commands/server.rs +++ b/crates/ruff/src/commands/server.rs @@ -1,7 +1,11 @@ use crate::ExitStatus; use anyhow::Result; +use ruff_server::WorkspaceTrust; -pub(crate) fn run_server(preview: Option) -> Result { - ruff_server::run(preview)?; +pub(crate) fn run_server( + preview: Option, + workspace_trust: WorkspaceTrust, +) -> Result { + ruff_server::run(preview, workspace_trust)?; Ok(ExitStatus::Success) } diff --git a/crates/ruff/src/lib.rs b/crates/ruff/src/lib.rs index f64359e5675ac4..b0cbd385b2c5c4 100644 --- a/crates/ruff/src/lib.rs +++ b/crates/ruff/src/lib.rs @@ -231,7 +231,7 @@ fn analyze_graph( } fn server(args: ServerCommand) -> Result { - commands::server::run_server(args.resolve_preview()) + commands::server::run_server(args.resolve_preview(), args.resolve_workspace_trust()) } pub fn check(args: CheckCommand, global_options: GlobalConfigArgs) -> Result { diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 08453f24c58326..f28c071220420f 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -54,6 +54,7 @@ dunce = { workspace = true } regex = { workspace = true } smallvec = { workspace = true } tempfile = { workspace = true } +test-case = { workspace = true } [features] test-uv = [] diff --git a/crates/ruff_server/src/lib.rs b/crates/ruff_server/src/lib.rs index 2df7a8bdfdc495..366d5efd24c56d 100644 --- a/crates/ruff_server/src/lib.rs +++ b/crates/ruff_server/src/lib.rs @@ -36,11 +36,18 @@ const NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind = /// result type is needed. pub(crate) type Result = anyhow::Result; +/// Whether the server may execute code from the workspace. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum WorkspaceTrust { + Trusted, + Untrusted, +} + fn version() -> &'static str { ruff_linter::VERSION } -pub fn run(preview: Option) -> Result<()> { +pub fn run(preview: Option, workspace_trust: WorkspaceTrust) -> Result<()> { let four = NonZeroUsize::new(4).unwrap(); // by default, we set the number of worker threads to `num_cpus`, with a maximum of 4. @@ -50,7 +57,7 @@ pub fn run(preview: Option) -> Result<()> { let (connection, io_threads) = ConnectionInitializer::stdio(); - let server_result = Server::new(worker_threads, connection, preview, false) + let server_result = Server::new(worker_threads, connection, preview, workspace_trust, false) .context("Failed to start server")? .run(); diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index c046266d447cac..2cbb5994386cef 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -24,11 +24,11 @@ use types::WorkspaceFoldersServerCapabilities; pub use self::connection::ConnectionInitializer; pub(crate) use self::connection::ConnectionSender; use self::schedule::spawn_main_loop; -use crate::PositionEncoding; pub(crate) use crate::server::main_loop::MainLoopSender; pub(crate) use crate::server::main_loop::{Event, MainLoopReceiver}; use crate::session::{AllOptions, Client, Session}; use crate::workspace::Workspaces; +use crate::{PositionEncoding, WorkspaceTrust}; pub(crate) use api::Error; mod api; @@ -52,6 +52,7 @@ impl Server { worker_threads: NonZeroUsize, connection: ConnectionInitializer, preview: Option, + workspace_trust: WorkspaceTrust, is_test: bool, ) -> crate::Result { let (id, init_params) = connection.initialize_start()?; @@ -121,6 +122,7 @@ impl Server { global, &workspaces, &client, + workspace_trust, )?, client_capabilities, }) diff --git a/crates/ruff_server/src/server/api/requests/format.rs b/crates/ruff_server/src/server/api/requests/format.rs index 36948e52662440..4b201613b12b91 100644 --- a/crates/ruff_server/src/server/api/requests/format.rs +++ b/crates/ruff_server/src/server/api/requests/format.rs @@ -45,10 +45,7 @@ impl super::BackgroundDocumentRequestHandler for Format { pub(super) fn format_full_document(snapshot: &DocumentSnapshot) -> Result { let mut fixes = Fixes::default(); let query = snapshot.query(); - let backend = snapshot - .client_settings() - .editor_settings() - .format_backend(); + let backend = snapshot.format_backend(); match snapshot.query() { DocumentQuery::Notebook { notebook, .. } => { @@ -84,10 +81,7 @@ fn format_document(snapshot: &DocumentSnapshot) -> Result .context("Failed to get text document for the format request") .unwrap(); let query = snapshot.query(); - let backend = snapshot - .client_settings() - .editor_settings() - .format_backend(); + let backend = snapshot.format_backend(); format_text_document( text_document, query, diff --git a/crates/ruff_server/src/server/api/requests/format_range.rs b/crates/ruff_server/src/server/api/requests/format_range.rs index 3329a3358d3fdd..1e3d6f0dc75d80 100644 --- a/crates/ruff_server/src/server/api/requests/format_range.rs +++ b/crates/ruff_server/src/server/api/requests/format_range.rs @@ -47,10 +47,7 @@ fn format_document_range( .context("Failed to get text document for the format range request") .unwrap(); let query = snapshot.query(); - let backend = snapshot - .client_settings() - .editor_settings() - .format_backend(); + let backend = snapshot.format_backend(); format_text_document_range(text_document, range, query, snapshot.encoding(), backend) } diff --git a/crates/ruff_server/src/session.rs b/crates/ruff_server/src/session.rs index a6dda25a22bae3..0b3a1d35a125a6 100644 --- a/crates/ruff_server/src/session.rs +++ b/crates/ruff_server/src/session.rs @@ -7,10 +7,11 @@ use lsp_types::{ClientCapabilities, FileEvent, NotebookDocumentCellChanges, Uri} use settings::ClientSettings; use crate::edit::{DocumentKey, DocumentVersion, NotebookDocument}; +use crate::format::FormatBackend; use crate::session::request_queue::RequestQueue; use crate::session::settings::GlobalClientSettings; use crate::workspace::Workspaces; -use crate::{PositionEncoding, TextDocument}; +use crate::{PositionEncoding, TextDocument, WorkspaceTrust}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub(crate) use self::index::DocumentQuery; @@ -34,6 +35,9 @@ pub(crate) struct Session { /// Global settings provided by the client. global_settings: GlobalClientSettings, + /// Set at startup so client settings cannot enable external formatting in an untrusted workspace. + workspace_trust: WorkspaceTrust, + /// Tracks what LSP features the client supports and doesn't support. resolved_client_capabilities: Arc, @@ -51,6 +55,7 @@ pub(crate) struct DocumentSnapshot { client_settings: Arc, document_ref: index::DocumentQuery, position_encoding: PositionEncoding, + workspace_trust: WorkspaceTrust, } impl Session { @@ -60,11 +65,13 @@ impl Session { global: GlobalClientSettings, workspaces: &Workspaces, client: &Client, + workspace_trust: WorkspaceTrust, ) -> crate::Result { Ok(Self { position_encoding, index: index::Index::new(workspaces, &global, client)?, global_settings: global, + workspace_trust, resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( client_capabilities, )), @@ -104,6 +111,7 @@ impl Session { .unwrap_or_else(|| self.global_settings.to_settings_arc()), document_ref: self.index.make_document_ref(key, &self.global_settings)?, position_encoding: self.position_encoding, + workspace_trust: self.workspace_trust, }) } @@ -215,6 +223,21 @@ impl Session { } impl DocumentSnapshot { + pub(crate) fn format_backend(&self) -> FormatBackend { + let backend = self.client_settings.editor_settings().format_backend(); + match self.workspace_trust { + WorkspaceTrust::Trusted => backend, + WorkspaceTrust::Untrusted => { + if backend == FormatBackend::Uv { + tracing::info!( + "Using the internal formatter because the workspace is untrusted; the uv backend is disabled" + ); + } + FormatBackend::Internal + } + } + } + pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities { &self.resolved_client_capabilities } @@ -242,3 +265,49 @@ impl DocumentSnapshot { ) } } + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use anyhow::{Context, Result}; + use lsp_types::Uri; + use serde_json::json; + use test_case::test_case; + + use super::index::Index; + use super::options::GlobalOptions; + use super::{Client, Session}; + use crate::format::FormatBackend; + use crate::session::request_queue::RequestQueue; + use crate::{PositionEncoding, TextDocument, WorkspaceTrust}; + + #[test_case(WorkspaceTrust::Untrusted, FormatBackend::Internal)] + #[test_case(WorkspaceTrust::Trusted, FormatBackend::Uv)] + fn format_backend(workspace_trust: WorkspaceTrust, expected: FormatBackend) -> Result<()> { + let (main_loop_sender, _) = crossbeam::channel::unbounded(); + let (client_sender, _) = crossbeam::channel::unbounded(); + let client = Client::new(main_loop_sender, client_sender); + let options: GlobalOptions = serde_json::from_value(json!({ + "format": {"backend": "uv"}, + }))?; + let mut session = Session { + index: Index::default(), + position_encoding: PositionEncoding::default(), + global_settings: options.into_settings(client), + workspace_trust, + resolved_client_capabilities: Arc::default(), + request_queue: RequestQueue::new(), + shutdown_requested: false, + }; + let uri: Uri = "untitled:test.py".parse()?; + session.open_text_document(uri.clone(), TextDocument::new(String::new(), 1)); + let snapshot = session + .take_snapshot(uri) + .context("missing document snapshot")?; + + assert_eq!(snapshot.format_backend(), expected); + + Ok(()) + } +} diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index d8e53ee40d6ac0..1cd945105fc890 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -66,7 +66,7 @@ use lsp_types::{ DidChangeWorkspaceFoldersNotification, DidCloseTextDocumentNotification, DidOpenTextDocumentNotification, ExitNotification, InitializedNotification, Notification, }; -use ruff_server::{ConnectionInitializer, LogLevel, Server, init_logging}; +use ruff_server::{ConnectionInitializer, LogLevel, Server, WorkspaceTrust, init_logging}; use rustc_hash::FxHashMap; use tempfile::TempDir; @@ -207,7 +207,13 @@ impl TestServer { // TODO: This should probably be configurable to test concurrency issues let worker_threads = NonZeroUsize::new(1).unwrap(); - match Server::new(worker_threads, server_connection, None, true) { + match Server::new( + worker_threads, + server_connection, + None, + WorkspaceTrust::Trusted, + true, + ) { Ok(server) => { if let Err(err) = server.run() { panic!("Server stopped with error: {err:?}"); diff --git a/docs/editors/settings.md b/docs/editors/settings.md index 5d5949a7fd9460..ab9a9c5b83759b 100644 --- a/docs/editors/settings.md +++ b/docs/editors/settings.md @@ -981,6 +981,10 @@ The backend to use for formatting files. Following options are available: For `internal`, the formatter version will match the selected Ruff version while for `uv`, the formatter version may differ. +Starting the server with `ruff server --untrusted-workspace` disables the uv backend. With this flag, +the server always uses the internal formatter, regardless of this setting. This prevents workspace +configuration from causing uv to execute untrusted code. + **Default value**: `"internal"` **Type**: `"internal" | "uv"` From f18ceda64d766eb5a222cfb4e55aff7324f52fcf Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Thu, 24 Sep 2026 17:15:50 -0400 Subject: [PATCH 2/3] e2e tests --- crates/ruff_server/tests/e2e/main.rs | 12 ++- crates/ruff_server/tests/e2e/workspace.rs | 101 +++++++++++++++++++++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 1cd945105fc890..25f7d5df41c0c7 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -196,6 +196,7 @@ impl TestServer { test_context: TestContext, capabilities: ClientCapabilities, initialization_options: Option, + workspace_trust: WorkspaceTrust, ) -> Self { setup_tracing(); @@ -211,7 +212,7 @@ impl TestServer { worker_threads, server_connection, None, - WorkspaceTrust::Trusted, + workspace_trust, true, ) { Ok(server) => { @@ -974,6 +975,7 @@ pub(crate) struct TestServerBuilder { workspaces: Vec, initialization_options: Option, client_capabilities: ClientCapabilities, + workspace_trust: WorkspaceTrust, } impl TestServerBuilder { @@ -1008,11 +1010,16 @@ impl TestServerBuilder { test_context: TestContext::new()?, initialization_options: None, client_capabilities, + workspace_trust: WorkspaceTrust::Trusted, }) } + pub(crate) fn with_workspace_trust(mut self, workspace_trust: WorkspaceTrust) -> Self { + self.workspace_trust = workspace_trust; + self + } + /// Set the initial client options for the test server - #[expect(dead_code)] pub(crate) fn with_initialization_options(mut self, options: serde_json::Value) -> Self { self.initialization_options = Some(options); self @@ -1155,6 +1162,7 @@ impl TestServerBuilder { self.test_context, self.client_capabilities, self.initialization_options, + self.workspace_trust, ) } } diff --git a/crates/ruff_server/tests/e2e/workspace.rs b/crates/ruff_server/tests/e2e/workspace.rs index 38405599bf5831..1956e68c7c1f42 100644 --- a/crates/ruff_server/tests/e2e/workspace.rs +++ b/crates/ruff_server/tests/e2e/workspace.rs @@ -1,7 +1,18 @@ -use anyhow::{Context, Result}; +use std::assert_matches; +use std::process::Command; + +use anyhow::{Context, Result, ensure}; use insta::{assert_json_snapshot, assert_snapshot}; +use lsp_types::{ + DocumentFormattingParams, DocumentFormattingRequest, DocumentRangeFormattingParams, + DocumentRangeFormattingRequest, Position, Range, ShowMessageNotification, + TextDocumentIdentifier, TextEdit, +}; +use ruff_server::WorkspaceTrust; +use serde_json::json; +use test_case::test_case; -use crate::{TestServer, TestServerBuilder}; +use crate::{AwaitResponseError, TestServer, TestServerBuilder}; const SOURCE: &str = "value= \"hello\"\n"; @@ -300,3 +311,89 @@ fn unavailable_document_diagnostic_returns_empty_response() -> Result<()> { Ok(()) } + +#[test_case(WorkspaceTrust::Trusted; "trusted")] +#[test_case(WorkspaceTrust::Untrusted; "untrusted")] +fn uv_formatting(workspace_trust: WorkspaceTrust) -> Result<()> { + without_uv(|| { + let mut server = TestServerBuilder::new()? + .with_workspace(".")? + .with_workspace_trust(workspace_trust) + .with_initialization_options(json!({"settings": {"format": {"backend": "uv"}}})) + .build(); + server.open_text_document("test.py", "x=1\n", 1); + + let document = server.send_request::(DocumentFormattingParams { + text_document: TextDocumentIdentifier { + uri: server.file_uri("test.py"), + }, + options: Default::default(), + work_done_progress_params: Default::default(), + }); + let document = server.try_await_response::(&document, None); + + let range = + server.send_request::(DocumentRangeFormattingParams { + text_document: TextDocumentIdentifier { + uri: server.file_uri("test.py"), + }, + range: Range::new(Position::new(0, 0), Position::new(1, 0)), + options: Default::default(), + work_done_progress_params: Default::default(), + }); + let range = server.try_await_response::(&range, None); + + match workspace_trust { + WorkspaceTrust::Trusted => { + assert_matches!( + document, + Err(AwaitResponseError::RequestFailed(error)) + if error.message.contains("uv was not found"), + ); + assert_matches!( + range, + Err(AwaitResponseError::RequestFailed(error)) + if error.message.contains("uv was not found"), + ); + server.await_notification::(); + server.await_notification::(); + } + WorkspaceTrust::Untrusted => { + let expected = Some(vec![TextEdit { + range: Range::new(Position::new(0, 0), Position::new(1, 0)), + new_text: "x = 1\n".to_string(), + }]); + assert_eq!(document?, expected); + assert_eq!(range?, expected); + } + } + Ok(()) + }) +} + +/// Isolate PATH from other tests, which run their servers in the same process. +/// Without uv, formatting only succeeds if the internal backend is used. +fn without_uv(body: impl FnOnce() -> Result<()>) -> Result<()> { + const CHILD: &str = "RUFF_TEST_ISOLATED_CHILD"; + let thread = std::thread::current(); + let name = thread.name().context("missing test name")?; + if std::env::var(CHILD).as_deref() == Ok(name) { + return body(); + } + + let empty_path = tempfile::tempdir()?; + let output = Command::new(std::env::current_exe()?) + .args(["--exact", name, "--nocapture"]) + .env(CHILD, name) + .env("PATH", empty_path.path()) + .current_dir(empty_path.path()) + .output()?; + let stdout = String::from_utf8_lossy(&output.stdout); + // An unmatched `--exact` filter exits successfully without running any tests. + ensure!( + output.status.success() && stdout.contains("test result: ok. 1 passed;"), + "{stdout}\n{}", + String::from_utf8_lossy(&output.stderr), + ); + Ok(()) +} From e5f9bf388f5b140e872b22f08a81aff125525935 Mon Sep 17 00:00:00 2001 From: Brent Westbrook Date: Mon, 28 Sep 2026 16:12:06 -0400 Subject: [PATCH 3/3] Revert "e2e tests" This reverts commit f18ceda64d766eb5a222cfb4e55aff7324f52fcf. --- crates/ruff_server/tests/e2e/main.rs | 12 +-- crates/ruff_server/tests/e2e/workspace.rs | 101 +--------------------- 2 files changed, 4 insertions(+), 109 deletions(-) diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 25f7d5df41c0c7..1cd945105fc890 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -196,7 +196,6 @@ impl TestServer { test_context: TestContext, capabilities: ClientCapabilities, initialization_options: Option, - workspace_trust: WorkspaceTrust, ) -> Self { setup_tracing(); @@ -212,7 +211,7 @@ impl TestServer { worker_threads, server_connection, None, - workspace_trust, + WorkspaceTrust::Trusted, true, ) { Ok(server) => { @@ -975,7 +974,6 @@ pub(crate) struct TestServerBuilder { workspaces: Vec, initialization_options: Option, client_capabilities: ClientCapabilities, - workspace_trust: WorkspaceTrust, } impl TestServerBuilder { @@ -1010,16 +1008,11 @@ impl TestServerBuilder { test_context: TestContext::new()?, initialization_options: None, client_capabilities, - workspace_trust: WorkspaceTrust::Trusted, }) } - pub(crate) fn with_workspace_trust(mut self, workspace_trust: WorkspaceTrust) -> Self { - self.workspace_trust = workspace_trust; - self - } - /// Set the initial client options for the test server + #[expect(dead_code)] pub(crate) fn with_initialization_options(mut self, options: serde_json::Value) -> Self { self.initialization_options = Some(options); self @@ -1162,7 +1155,6 @@ impl TestServerBuilder { self.test_context, self.client_capabilities, self.initialization_options, - self.workspace_trust, ) } } diff --git a/crates/ruff_server/tests/e2e/workspace.rs b/crates/ruff_server/tests/e2e/workspace.rs index 1956e68c7c1f42..38405599bf5831 100644 --- a/crates/ruff_server/tests/e2e/workspace.rs +++ b/crates/ruff_server/tests/e2e/workspace.rs @@ -1,18 +1,7 @@ -use std::assert_matches; -use std::process::Command; - -use anyhow::{Context, Result, ensure}; +use anyhow::{Context, Result}; use insta::{assert_json_snapshot, assert_snapshot}; -use lsp_types::{ - DocumentFormattingParams, DocumentFormattingRequest, DocumentRangeFormattingParams, - DocumentRangeFormattingRequest, Position, Range, ShowMessageNotification, - TextDocumentIdentifier, TextEdit, -}; -use ruff_server::WorkspaceTrust; -use serde_json::json; -use test_case::test_case; -use crate::{AwaitResponseError, TestServer, TestServerBuilder}; +use crate::{TestServer, TestServerBuilder}; const SOURCE: &str = "value= \"hello\"\n"; @@ -311,89 +300,3 @@ fn unavailable_document_diagnostic_returns_empty_response() -> Result<()> { Ok(()) } - -#[test_case(WorkspaceTrust::Trusted; "trusted")] -#[test_case(WorkspaceTrust::Untrusted; "untrusted")] -fn uv_formatting(workspace_trust: WorkspaceTrust) -> Result<()> { - without_uv(|| { - let mut server = TestServerBuilder::new()? - .with_workspace(".")? - .with_workspace_trust(workspace_trust) - .with_initialization_options(json!({"settings": {"format": {"backend": "uv"}}})) - .build(); - server.open_text_document("test.py", "x=1\n", 1); - - let document = server.send_request::(DocumentFormattingParams { - text_document: TextDocumentIdentifier { - uri: server.file_uri("test.py"), - }, - options: Default::default(), - work_done_progress_params: Default::default(), - }); - let document = server.try_await_response::(&document, None); - - let range = - server.send_request::(DocumentRangeFormattingParams { - text_document: TextDocumentIdentifier { - uri: server.file_uri("test.py"), - }, - range: Range::new(Position::new(0, 0), Position::new(1, 0)), - options: Default::default(), - work_done_progress_params: Default::default(), - }); - let range = server.try_await_response::(&range, None); - - match workspace_trust { - WorkspaceTrust::Trusted => { - assert_matches!( - document, - Err(AwaitResponseError::RequestFailed(error)) - if error.message.contains("uv was not found"), - ); - assert_matches!( - range, - Err(AwaitResponseError::RequestFailed(error)) - if error.message.contains("uv was not found"), - ); - server.await_notification::(); - server.await_notification::(); - } - WorkspaceTrust::Untrusted => { - let expected = Some(vec![TextEdit { - range: Range::new(Position::new(0, 0), Position::new(1, 0)), - new_text: "x = 1\n".to_string(), - }]); - assert_eq!(document?, expected); - assert_eq!(range?, expected); - } - } - Ok(()) - }) -} - -/// Isolate PATH from other tests, which run their servers in the same process. -/// Without uv, formatting only succeeds if the internal backend is used. -fn without_uv(body: impl FnOnce() -> Result<()>) -> Result<()> { - const CHILD: &str = "RUFF_TEST_ISOLATED_CHILD"; - let thread = std::thread::current(); - let name = thread.name().context("missing test name")?; - if std::env::var(CHILD).as_deref() == Ok(name) { - return body(); - } - - let empty_path = tempfile::tempdir()?; - let output = Command::new(std::env::current_exe()?) - .args(["--exact", name, "--nocapture"]) - .env(CHILD, name) - .env("PATH", empty_path.path()) - .current_dir(empty_path.path()) - .output()?; - let stdout = String::from_utf8_lossy(&output.stdout); - // An unmatched `--exact` filter exits successfully without running any tests. - ensure!( - output.status.success() && stdout.contains("test result: ok. 1 passed;"), - "{stdout}\n{}", - String::from_utf8_lossy(&output.stderr), - ); - Ok(()) -}