Files
ruff/crates/ty_server/tests/e2e/configuration.rs
T
Micha Reiser fd5d636bdd [ty] Watch script dependencies in the language server (#28522)
## Summary

Same as https://github.com/astral-sh/ruff/pull/28125 but for the server.

The main difference to the CLI is that, up to now, the server did not
support changing its watched paths after its initial initialization.
This PR adds the infrastructure so that the server now supports updating
its watched paths as search paths change.


## Test Plan

**Before**



https://github.com/user-attachments/assets/a6ace5a1-84c9-4acb-a15c-991389543109

Changes to `dependency.py` are not picked up

**Now**


https://github.com/user-attachments/assets/b3d6bf4b-496f-4bdc-a610-82eb9690ffb3
2026-09-17 19:56:58 +02:00

571 lines
16 KiB
Rust

use anyhow::{Context, Result};
use insta::assert_json_snapshot;
use lsp_types::RegistrationRequest;
use ruff_db::system::SystemPath;
use ruff_python_trivia::textwrap::dedent;
use serde_json::{Map, json};
use ty_server::{ClientOptions, WorkspaceOptions};
use crate::TestServerBuilder;
use crate::pull_diagnostics::filter_result_id;
use crate::workspace_folders::condensed_document_diagnostic_snapshot;
#[test]
fn configuration_file() -> Result<()> {
let _filter = filter_result_id();
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "\
def foo() -> str:
return a
";
let builder = TestServerBuilder::new()?;
let settings_path = builder.file_path("ty[dev].toml");
let mut server = builder
.with_workspace(
workspace_root,
Some(ClientOptions {
workspace: WorkspaceOptions {
configuration_file: Some(settings_path.to_string()),
..WorkspaceOptions::default()
},
..ClientOptions::default()
}),
)?
.with_file(foo, foo_content)?
.with_file(
settings_path,
r#"
[rules]
unresolved-reference="warn"
"#,
)?
.with_watched_file_support(true)
.build()
.wait_until_workspaces_are_initialized();
let (_, registrations) = server.await_request::<RegistrationRequest>();
let options = registrations
.registrations
.first()
.and_then(|registration| registration.register_options.as_ref())
.context("expected file watcher registration")?;
assert_json_snapshot!(options["watchers"], @r#"
[
{
"globPattern": {
"baseUri": "file://<temp_dir>/src",
"pattern": "**"
}
},
{
"globPattern": {
"baseUri": "file://<temp_dir>/",
"pattern": "ty[[]dev[]].toml"
}
}
]
"#);
server.open_text_document(foo, foo_content, 1);
let diagnostics = server.document_diagnostic_request(foo, None);
assert_json_snapshot!("configuration_file", diagnostics);
Ok(())
}
#[test]
fn invalid_configuration_file() -> Result<()> {
let _filter = filter_result_id();
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "\
def foo() -> str:
return a
";
let builder = TestServerBuilder::new()?;
let settings_path = builder.file_path("ty2.toml");
let mut server = builder
.with_workspace(
workspace_root,
Some(ClientOptions {
workspace: WorkspaceOptions {
configuration_file: Some(settings_path.to_string()),
configuration: Some(
Map::from_iter([
("rules".to_string(), json!({"unresolved-reference": "warn"})),
(
"environment".to_string(),
json!({"python": "./missing-environment"}),
),
])
.into(),
),
..WorkspaceOptions::default()
},
..ClientOptions::default()
}),
)?
.with_file(foo, foo_content)?
.with_file(
settings_path,
r#"
[rule]
unresolved-reference="warn"
"#,
)?
.build()
.wait_until_workspaces_are_initialized();
server.open_text_document(foo, foo_content, 1);
let show_message = server.await_notification::<lsp_types::ShowMessageNotification>();
let diagnostics = server.document_diagnostic_request(foo, None);
assert_json_snapshot!(show_message, @r#"
{
"type": 1,
"message": "Failed to load project for workspace file://<temp_dir>/src. Please refer to the logs for more details."
}
"#);
assert_json_snapshot!(diagnostics);
Ok(())
}
#[test]
fn invalid_python_environment_preserves_valid_configuration() -> Result<()> {
let main = SystemPath::new("src/main.py");
let content = dedent(
"
import sys
sys.platform
",
);
let mut server = TestServerBuilder::new()?
.with_workspace(SystemPath::new("src"), None)?
.with_file(
"src/ty.toml",
dedent(
r#"
[environment]
python = "./missing-environment"
python-platform = "win32"
"#,
),
)?
.with_file(main, &content)?
.build()
.wait_until_workspaces_are_initialized();
let message = server.await_notification::<lsp_types::ShowMessageNotification>();
assert_eq!(message.kind, lsp_types::MessageType::Error);
server.open_text_document(main, &content, 1);
let platform = server.hover_request(main, lsp_types::Position::new(2, 5));
assert_json_snapshot!(platform, @r#"
{
"contents": {
"kind": "plaintext",
"value": "Literal[\"win32\"]"
},
"range": {
"start": {
"line": 2,
"character": 4
},
"end": {
"line": 2,
"character": 12
}
}
}
"#);
Ok(())
}
#[test]
fn invalid_include_pattern_preserves_rules_and_diagnostics() -> Result<()> {
let main = SystemPath::new("src/main.py");
let content = "undefined";
let mut server = TestServerBuilder::new()?
.with_workspace(SystemPath::new("src"), None)?
.with_file(
"src/ty.toml",
dedent(
r#"
[src]
include = ["["]
[rules]
unresolved-reference = "warn"
unknown-rule = "warn"
"#,
),
)?
.with_file(main, content)?
.build()
.wait_until_workspaces_are_initialized();
let message = server.await_notification::<lsp_types::ShowMessageNotification>();
assert_eq!(message.kind, lsp_types::MessageType::Error);
let settings_diagnostics =
server.await_notification::<lsp_types::PublishDiagnosticsNotification>();
assert_json_snapshot!(settings_diagnostics, @r#"
{
"uri": "file://<temp_dir>/src/ty.toml",
"diagnostics": [
{
"range": {
"start": {
"line": 6,
"character": 0
},
"end": {
"line": 6,
"character": 12
}
},
"severity": 2,
"code": "unknown-rule",
"source": "ty",
"message": "Unknown rule `unknown-rule`. Did you mean `unknown-argument`?"
}
]
}
"#);
server.open_text_document(main, content, 1);
let diagnostics = server.document_diagnostic_request(main, None);
insta::assert_snapshot!(condensed_document_diagnostic_snapshot(diagnostics), @"0:0..0:9[WARNING]: Name `undefined` used when not defined");
Ok(())
}
#[test]
fn configuration_overrides() -> Result<()> {
let _filter = filter_result_id();
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "\
def foo() -> str:
return a
";
let mut server = TestServerBuilder::new()?
.with_workspace(
workspace_root,
Some(ClientOptions {
workspace: WorkspaceOptions {
configuration: Some(
Map::from_iter([(
"rules".to_string(),
json!({"unresolved-reference": "warn"}),
)])
.into(),
),
..WorkspaceOptions::default()
},
..ClientOptions::default()
}),
)?
.with_file(foo, foo_content)?
.build()
.wait_until_workspaces_are_initialized();
server.open_text_document(foo, foo_content, 1);
let diagnostics = server.document_diagnostic_request(foo, None);
assert_json_snapshot!(diagnostics);
Ok(())
}
#[test]
fn unsupported_editor_python_version() -> Result<()> {
let _filter = filter_result_id();
let workspace_root = SystemPath::new("src");
let main = SystemPath::new("src/main.py");
let python_home = "base/bin";
let base_python = if cfg!(target_os = "windows") {
"base/bin/python.exe"
} else {
"base/bin/python"
};
let python = if cfg!(target_os = "windows") {
"venv/Scripts/python.exe"
} else {
"venv/bin/python"
};
let site_packages_foo = if cfg!(target_os = "windows") {
"venv/Lib/site-packages/foo.py"
} else {
"venv/lib/python3.16/site-packages/foo.py"
};
// The import proves we still use the editor-selected environment for module resolution even
// when we ignore its unsupported reported Python version.
let foo_content = "\
import foo
import sys
from typing_extensions import reveal_type
reveal_type(sys.version_info[:2])
";
let builder = TestServerBuilder::new()?;
let python_home = builder.file_path(python_home);
let sys_prefix = builder.file_path("venv");
let workspace_options: ClientOptions = serde_json::from_value(json!({
"pythonExtension": {
"activeEnvironment": {
"executable": {
"sysPrefix": sys_prefix,
},
"version": {
"major": 3,
"minor": 16,
}
}
}
}))?;
let mut server = builder
.with_workspace(workspace_root, Some(workspace_options))?
.with_file(main, foo_content)?
.with_file(base_python, "")?
.with_file(python, "")?
.with_file(
"venv/pyvenv.cfg",
format!("version_info = 3.16.0\nhome = {python_home}\n"),
)?
.with_file(site_packages_foo, "")?
.build()
.wait_until_workspaces_are_initialized();
// The unsupported version inferred from the selected environment surfaces as a
// settings diagnostic on the environment's `pyvenv.cfg`.
let diagnostics = server.collect_publish_diagnostic_notifications(1);
assert_json_snapshot!(diagnostics, @r#"
{
"file://<temp_dir>/venv/pyvenv.cfg": [
{
"range": {
"start": {
"line": 0,
"character": 15
},
"end": {
"line": 0,
"character": 21
}
},
"severity": 2,
"code": "unsupported-python-version",
"source": "ty",
"message": "Ignoring unsupported inferred Python version `3.16`; ty will use Python 3.14 instead.\n\ninfo: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`.\ninfo: Set `environment.python-version` explicitly to override the inferred version.\ninfo: The version was inferred from your virtual environment metadata."
}
]
}
"#);
server.open_text_document(main, foo_content, 1);
let diagnostics = server.document_diagnostic_request(main, None);
assert_json_snapshot!(diagnostics, @r#"
{
"resultId": "[RESULT_ID]",
"items": [
{
"range": {
"start": {
"line": 4,
"character": 12
},
"end": {
"line": 4,
"character": 32
}
},
"severity": 3,
"code": "revealed-type",
"source": "ty",
"message": "Revealed type: `tuple[Literal[3], Literal[14]]`"
}
],
"kind": "full"
}
"#);
Ok(())
}
#[cfg_attr(windows, ignore = "site-packages layout inference is Unix-only")]
#[test]
fn unsupported_inferred_python_version_setting_diagnostic() -> Result<()> {
let workspace_root = SystemPath::new("project");
let main = SystemPath::new("project/main.py");
let python_home = "base/bin";
let base_python = if cfg!(target_os = "windows") {
"base/bin/python.exe"
} else {
"base/bin/python"
};
let python = if cfg!(target_os = "windows") {
"project/.venv/Scripts/python.exe"
} else {
"project/.venv/bin/python"
};
let site_packages = if cfg!(target_os = "windows") {
"project/.venv/Lib/site-packages/foo.py"
} else {
"project/.venv/lib/python3.16/site-packages/foo.py"
};
let builder = TestServerBuilder::new()?;
let python_home = builder.file_path(python_home);
let mut server = builder
.with_workspace(workspace_root, None)?
.with_file(main, "x = 1\n")?
.with_file(base_python, "")?
.with_file(python, "")?
.with_file(
"project/.venv/pyvenv.cfg",
format!("home = {python_home}\n"),
)?
.with_file(site_packages, "")?
.build()
.wait_until_workspaces_are_initialized();
let diagnostics = server.collect_publish_diagnostic_notifications(1);
assert_json_snapshot!(diagnostics);
Ok(())
}
#[cfg_attr(windows, ignore = "site-packages layout inference is Unix-only")]
#[test]
fn unsupported_inferred_python_version_setting_diagnostic_for_system_interpreter() -> Result<()> {
let workspace_root = SystemPath::new("project");
let main = SystemPath::new("project/main.py");
let python = "python/bin/python3.16";
let site_packages = "python/lib/python3.16/site-packages/foo.py";
let builder = TestServerBuilder::new()?;
let sys_prefix = builder.file_path("python");
let python_uri = builder.file_uri(python);
let workspace_options: ClientOptions = serde_json::from_value(json!({
"pythonExtension": {
"activeEnvironment": {
"executable": {
"uri": python_uri,
"sysPrefix": sys_prefix,
}
}
}
}))?;
let mut server = builder
.with_workspace(workspace_root, Some(workspace_options))?
.with_file(main, "x = 1\n")?
.with_file(python, "")?
.with_file(site_packages, "")?
.build()
.wait_until_workspaces_are_initialized();
let diagnostics = server.collect_publish_diagnostic_notifications(1);
assert_json_snapshot!(diagnostics, @r#"
{
"file://<temp_dir>/python/bin/python3.16": [
{
"range": {
"start": {
"line": 0,
"character": 0
},
"end": {
"line": 0,
"character": 0
}
},
"severity": 2,
"code": "unsupported-python-version",
"source": "ty",
"message": "Ignoring unsupported inferred Python version `3.16`; ty will use Python 3.14 instead.\n\ninfo: Expected one of `3.7`, `3.8`, `3.9`, `3.10`, `3.11`, `3.12`, `3.13`, `3.14`, `3.15`.\ninfo: Set `environment.python-version` explicitly to override the inferred version.\ninfo: The version was inferred from the `lib/python3.16/site-packages` directory layout."
}
]
}
"#);
Ok(())
}
#[test]
fn configuration_file_and_overrides() -> Result<()> {
let _filter = filter_result_id();
let workspace_root = SystemPath::new("src");
let foo = SystemPath::new("src/foo.py");
let foo_content = "\
def foo() -> str:
return a
";
let builder = TestServerBuilder::new()?;
let settings_path = builder.file_path("ty2.toml");
let mut server = builder
.with_workspace(
workspace_root,
Some(ClientOptions {
workspace: WorkspaceOptions {
configuration_file: Some(settings_path.to_string()),
configuration: Some(
Map::from_iter([(
"rules".to_string(),
json!({"unresolved-reference": "ignore"}),
)])
.into(),
),
..WorkspaceOptions::default()
},
..ClientOptions::default()
}),
)?
.with_file(foo, foo_content)?
.with_file(
settings_path,
r#"
[rules]
unresolved-reference="warn"
"#,
)?
.build()
.wait_until_workspaces_are_initialized();
server.open_text_document(foo, foo_content, 1);
let diagnostics = server.document_diagnostic_request(foo, None);
assert_json_snapshot!(diagnostics);
Ok(())
}