diff --git a/Cargo.lock b/Cargo.lock index dca496dd267518..de4b7ac70c35fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,6 +161,21 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +[[package]] +name = "assert_fs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efdb1fdb47602827a342857666feb372712cbc64b414172bd6b167a02927674" +dependencies = [ + "anstyle", + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + [[package]] name = "autocfg" version = "1.2.0" @@ -725,6 +740,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -776,6 +797,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "drop_bomb" version = "0.1.5" @@ -1878,6 +1905,33 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.0" @@ -2205,6 +2259,7 @@ version = "0.6.5" dependencies = [ "anyhow", "argfile", + "assert_fs", "bincode", "bitflags 2.6.0", "cachedir", @@ -2216,6 +2271,7 @@ dependencies = [ "filetime", "globwalk", "ignore", + "indoc", "insta", "insta-cmd", "is-macro", @@ -3235,6 +3291,12 @@ dependencies = [ "phf_codegen", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "test-case" version = "3.3.1" diff --git a/Cargo.toml b/Cargo.toml index af1ab019c6f49f..7455b5b6bd174c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ red_knot_workspace = { path = "crates/red_knot_workspace" } aho-corasick = { version = "1.1.3" } annotate-snippets = { version = "0.9.2", features = ["color"] } anyhow = { version = "1.0.80" } +assert_fs = { version = "1.1.0" } argfile = { version = "0.2.0" } bincode = { version = "1.3.3" } bitflags = { version = "2.5.0" } diff --git a/crates/ruff/Cargo.toml b/crates/ruff/Cargo.toml index 733b91414275ec..5c6583f64e91b7 100644 --- a/crates/ruff/Cargo.toml +++ b/crates/ruff/Cargo.toml @@ -62,8 +62,11 @@ wild = { workspace = true } [dev-dependencies] # Enable test rules during development ruff_linter = { workspace = true, features = ["clap", "test-rules"] } + +assert_fs = { workspace = true } # Avoid writing colored snapshots when running tests from the terminal colored = { workspace = true, features = ["no-color"] } +indoc = { workspace = true } insta = { workspace = true, features = ["filters", "json"] } insta-cmd = { workspace = true } tempfile = { workspace = true } diff --git a/crates/ruff/tests/analyze_graph.rs b/crates/ruff/tests/analyze_graph.rs new file mode 100644 index 00000000000000..641995b5036f98 --- /dev/null +++ b/crates/ruff/tests/analyze_graph.rs @@ -0,0 +1,197 @@ +//! Tests the interaction of the `analyze graph` command. + +#![cfg(not(target_family = "wasm"))] + +use assert_fs::prelude::*; +use std::process::Command; +use std::str; + +use anyhow::Result; +use assert_fs::fixture::ChildPath; +use insta_cmd::{assert_cmd_snapshot, get_cargo_bin}; +use tempfile::TempDir; + +fn command() -> Command { + let mut command = Command::new(get_cargo_bin("ruff")); + command.arg("analyze"); + command.arg("graph"); + command.arg("--preview"); + command +} + +#[test] +fn dependencies() -> Result<()> { + let tempdir = TempDir::new()?; + let root = ChildPath::new(tempdir.path()); + + root.child("ruff").child("__init__.py").write_str("")?; + root.child("ruff") + .child("a.py") + .write_str(indoc::indoc! {r#" + import ruff.b + "#})?; + root.child("ruff") + .child("b.py") + .write_str(indoc::indoc! {r#" + from ruff import c + "#})?; + root.child("ruff") + .child("c.py") + .write_str(indoc::indoc! {r#" + from . import d + "#})?; + root.child("ruff") + .child("d.py") + .write_str(indoc::indoc! {r#" + from .e import f + "#})?; + root.child("ruff") + .child("e.py") + .write_str(indoc::indoc! {r#" + def f(): pass + "#})?; + + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [ + "ruff/c.py" + ], + "ruff/c.py": [ + "ruff/d.py" + ], + "ruff/d.py": [ + "ruff/e.py" + ], + "ruff/e.py": [] + } + + ----- stderr ----- + "###); + + Ok(()) +} + +#[test] +fn dependents() -> Result<()> { + let tempdir = TempDir::new()?; + + let root = ChildPath::new(tempdir.path()); + + root.child("ruff").child("__init__.py").write_str("")?; + root.child("ruff") + .child("a.py") + .write_str(indoc::indoc! {r#" + import ruff.b + "#})?; + root.child("ruff") + .child("b.py") + .write_str(indoc::indoc! {r#" + from ruff import c + "#})?; + root.child("ruff") + .child("c.py") + .write_str(indoc::indoc! {r#" + from . import d + "#})?; + root.child("ruff") + .child("d.py") + .write_str(indoc::indoc! {r#" + from .e import f + "#})?; + root.child("ruff") + .child("e.py") + .write_str(indoc::indoc! {r#" + def f(): pass + "#})?; + + assert_cmd_snapshot!(command().arg("--direction").arg("dependents").current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [], + "ruff/b.py": [ + "ruff/a.py" + ], + "ruff/c.py": [ + "ruff/b.py" + ], + "ruff/d.py": [ + "ruff/c.py" + ], + "ruff/e.py": [ + "ruff/d.py" + ] + } + + ----- stderr ----- + "###); + + Ok(()) +} + +#[test] +fn string_detection() -> Result<()> { + let tempdir = TempDir::new()?; + + let root = ChildPath::new(tempdir.path()); + + root.child("ruff").child("__init__.py").write_str("")?; + root.child("ruff") + .child("a.py") + .write_str(indoc::indoc! {r#" + import ruff.b + "#})?; + root.child("ruff") + .child("b.py") + .write_str(indoc::indoc! {r#" + import importlib + + importlib.import_module("ruff.c") + "#})?; + root.child("ruff").child("c.py").write_str("")?; + + assert_cmd_snapshot!(command().current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [], + "ruff/c.py": [] + } + + ----- stderr ----- + "###); + + assert_cmd_snapshot!(command().arg("--detect-string-imports").current_dir(&root), @r###" + success: true + exit_code: 0 + ----- stdout ----- + { + "ruff/__init__.py": [], + "ruff/a.py": [ + "ruff/b.py" + ], + "ruff/b.py": [ + "ruff/c.py" + ], + "ruff/c.py": [] + } + + ----- stderr ----- + "###); + + Ok(()) +}