ruff/crates/ruff_server/tests/notebook.rs
Micha Reiser 015222900f
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Support cancellation requests (#18627)
2025-06-12 22:08:42 +02:00

385 lines
14 KiB
Rust

use std::{
path::{Path, PathBuf},
str::FromStr,
};
use lsp_types::{
ClientCapabilities, LSPObject, NotebookDocumentCellChange, NotebookDocumentChangeTextContent,
Position, Range, TextDocumentContentChangeEvent, VersionedTextDocumentIdentifier,
};
use ruff_notebook::SourceValue;
use ruff_server::{Client, ClientOptions, GlobalOptions, Workspace, Workspaces};
const SUPER_RESOLUTION_OVERVIEW_PATH: &str =
"./resources/test/fixtures/tensorflow_test_notebook.ipynb";
struct NotebookChange {
version: i32,
metadata: Option<LSPObject>,
updated_cells: lsp_types::NotebookDocumentCellChange,
}
#[test]
fn super_resolution_overview() {
let file_path =
std::fs::canonicalize(PathBuf::from_str(SUPER_RESOLUTION_OVERVIEW_PATH).unwrap()).unwrap();
let file_url = lsp_types::Url::from_file_path(&file_path).unwrap();
let notebook = create_notebook(&file_path).unwrap();
insta::assert_snapshot!("initial_notebook", notebook_source(&notebook));
let (main_loop_sender, main_loop_receiver) = crossbeam::channel::unbounded();
let (client_sender, client_receiver) = crossbeam::channel::unbounded();
let client = Client::new(main_loop_sender, client_sender);
let options = GlobalOptions::default();
let global = options.into_settings(client.clone());
let mut session = ruff_server::Session::new(
&ClientCapabilities::default(),
ruff_server::PositionEncoding::UTF16,
global,
&Workspaces::new(vec![
Workspace::new(lsp_types::Url::from_file_path(file_path.parent().unwrap()).unwrap())
.with_options(ClientOptions::default()),
]),
&client,
)
.unwrap();
session.open_notebook_document(file_url.clone(), notebook);
let changes = [NotebookChange {
version: 0,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 5),
version: 2,
},
changes: vec![
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 18,
character: 61,
},
end: Position {
line: 18,
character: 62,
},
}),
range_length: Some(1),
text: "\"".to_string(),
},
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 18,
character: 55,
},
end: Position {
line: 18,
character: 56,
},
}),
range_length: Some(1),
text: "\"".to_string(),
},
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 14,
character: 46,
},
end: Position {
line: 14,
character: 47,
},
}),
range_length: Some(1),
text: "\"".to_string(),
},
TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 14,
character: 40,
},
end: Position {
line: 14,
character: 41,
},
}),
range_length: Some(1),
text: "\"".to_string(),
},
],
}]),
},
},
NotebookChange {
version: 1,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 4),
version: 2
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 0
},
end: Position {
line: 0,
character: 181
} }),
range_length: Some(181),
text: "test_img_path = tf.keras.utils.get_file(\n \"lr.jpg\",\n \"https://raw.githubusercontent.com/tensorflow/examples/master/lite/examples/super_resolution/android/app/src/main/assets/lr-1.jpg\",\n)".to_string()
}
]
}
]
)
}
},
NotebookChange {
version: 2,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 2),
version: 2,
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 3,
character: 0,
},
end: Position {
line: 3,
character: 21,
},
}),
range_length: Some(21),
text: "\nprint(tf.__version__)".to_string(),
}],
}]),
}
},
NotebookChange {
version: 3,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 1),
version: 2,
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 49,
},
}),
range_length: Some(49),
text: "!pip install matplotlib tensorflow tensorflow-hub".to_string(),
}],
}]),
},
},
NotebookChange {
version: 4,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 3),
version: 2,
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 3,
character: 0,
},
end: Position {
line: 15,
character: 37,
},
}),
range_length: Some(457),
text: "\n@tf.function(input_signature=[tf.TensorSpec(shape=[1, 50, 50, 3], dtype=tf.float32)])\ndef f(input):\n return concrete_func(input)\n\n\nconverter = tf.lite.TFLiteConverter.from_concrete_functions(\n [f.get_concrete_function()], model\n)\nconverter.optimizations = [tf.lite.Optimize.DEFAULT]\ntflite_model = converter.convert()\n\n# Save the TF Lite model.\nwith tf.io.gfile.GFile(\"ESRGAN.tflite\", \"wb\") as f:\n f.write(tflite_model)\n\nesrgan_model_path = \"./ESRGAN.tflite\"".to_string(),
}],
}]),
},
},
NotebookChange {
version: 5,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 0),
version: 2,
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 2,
character: 0,
},
}),
range_length: Some(139),
text: "# @title Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n".to_string(),
}],
}]),
},
},
NotebookChange {
version: 6,
metadata: None,
updated_cells: NotebookDocumentCellChange {
structure: None,
data: None,
text_content: Some(vec![NotebookDocumentChangeTextContent {
document: VersionedTextDocumentIdentifier {
uri: make_cell_uri(&file_path, 6),
version: 2,
},
changes: vec![TextDocumentContentChangeEvent {
range: Some(Range {
start: Position {
line: 1,
character: 0,
},
end: Position {
line: 14,
character: 28,
},
}),
range_length: Some(361),
text: "plt.figure(figsize=(1, 1))\nplt.title(\"LR\")\nplt.imshow(lr.numpy())\nplt.figure(figsize=(10, 4))\nplt.subplot(1, 2, 1)\nplt.title(f\"ESRGAN (x4)\")\nplt.imshow(sr.numpy())\nbicubic = tf.image.resize(lr, [200, 200], tf.image.ResizeMethod.BICUBIC)\nbicubic = tf.cast(bicubic, tf.uint8)\nplt.subplot(1, 2, 2)\nplt.title(\"Bicubic\")\nplt.imshow(bicubic.numpy());".to_string(),
}],
}]),
},
}
];
let key = session.key_from_url(file_url.clone());
for NotebookChange {
version,
metadata,
updated_cells,
} in changes
{
session
.update_notebook_document(&key, Some(updated_cells), metadata, version)
.unwrap();
}
let snapshot = session.take_snapshot(file_url).unwrap();
insta::assert_snapshot!(
"changed_notebook",
notebook_source(snapshot.query().as_notebook().unwrap())
);
assert!(client_receiver.is_empty());
assert!(main_loop_receiver.is_empty());
}
fn notebook_source(notebook: &ruff_server::NotebookDocument) -> String {
notebook.make_ruff_notebook().source_code().to_string()
}
// produces an opaque URL based on a document path and a cell index
fn make_cell_uri(path: &Path, index: usize) -> lsp_types::Url {
lsp_types::Url::parse(&format!(
"notebook-cell:///Users/test/notebooks/{}.ipynb?cell={index}",
path.file_name().unwrap().to_string_lossy()
))
.unwrap()
}
fn create_notebook(file_path: &Path) -> anyhow::Result<ruff_server::NotebookDocument> {
let ruff_notebook = ruff_notebook::Notebook::from_path(file_path)?;
let mut cells = vec![];
let mut cell_documents = vec![];
for (i, cell) in ruff_notebook
.cells()
.iter()
.filter(|cell| cell.is_code_cell())
.enumerate()
{
let uri = make_cell_uri(file_path, i);
let (lsp_cell, cell_document) = cell_to_lsp_cell(cell, uri)?;
cells.push(lsp_cell);
cell_documents.push(cell_document);
}
let serde_json::Value::Object(metadata) = serde_json::to_value(ruff_notebook.metadata())?
else {
anyhow::bail!("Notebook metadata was not an object");
};
ruff_server::NotebookDocument::new(0, cells, metadata, cell_documents)
}
fn cell_to_lsp_cell(
cell: &ruff_notebook::Cell,
cell_uri: lsp_types::Url,
) -> anyhow::Result<(lsp_types::NotebookCell, lsp_types::TextDocumentItem)> {
let contents = match cell.source() {
SourceValue::String(string) => string.clone(),
SourceValue::StringArray(array) => array.join(""),
};
let metadata = match serde_json::to_value(cell.metadata())? {
serde_json::Value::Null => None,
serde_json::Value::Object(metadata) => Some(metadata),
_ => anyhow::bail!("Notebook cell metadata was not an object"),
};
Ok((
lsp_types::NotebookCell {
kind: match cell {
ruff_notebook::Cell::Code(_) => lsp_types::NotebookCellKind::Code,
ruff_notebook::Cell::Markdown(_) => lsp_types::NotebookCellKind::Markup,
ruff_notebook::Cell::Raw(_) => unreachable!(),
},
document: cell_uri.clone(),
metadata,
execution_summary: None,
},
lsp_types::TextDocumentItem::new(cell_uri, "python".to_string(), 1, contents),
))
}