Tidy knot CLI tests (#15685)

This commit is contained in:
Micha Reiser 2025-01-23 14:06:07 +01:00 committed by GitHub
parent 0173738eef
commit 43160b4c3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 228 additions and 200 deletions

4
.gitignore vendored
View File

@ -29,6 +29,10 @@ tracing.folded
tracing-flamechart.svg
tracing-flamegraph.svg
# insta
.rs.pending-snap
###
# Rust.gitignore
###

1
Cargo.lock generated
View File

@ -2332,6 +2332,7 @@ dependencies = [
"red_knot_server",
"regex",
"ruff_db",
"ruff_python_trivia",
"salsa",
"tempfile",
"toml",

View File

@ -33,6 +33,7 @@ tracing-tree = { workspace = true }
[dev-dependencies]
ruff_db = { workspace = true, features = ["testing"] }
ruff_python_trivia = { workspace = true }
insta = { workspace = true, features = ["filters"] }
insta-cmd = { workspace = true }

View File

@ -1,6 +1,7 @@
use anyhow::Context;
use insta::Settings;
use insta_cmd::{assert_cmd_snapshot, get_cargo_bin};
use std::path::Path;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
@ -8,30 +9,27 @@ use tempfile::TempDir;
/// project's configuration.
#[test]
fn config_override() -> anyhow::Result<()> {
let tempdir = TempDir::new()?;
std::fs::write(
tempdir.path().join("pyproject.toml"),
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
"#,
)
.context("Failed to write settings")?;
std::fs::write(
tempdir.path().join("test.py"),
[tool.knot.environment]
python-version = "3.11"
"#,
),
(
"test.py",
r#"
import sys
import sys
# Access `sys.last_exc` that was only added in Python 3.12
print(sys.last_exc)
"#,
)
.context("Failed to write test.py")?;
# Access `sys.last_exc` that was only added in Python 3.12
print(sys.last_exc)
"#,
),
])?;
insta::with_settings!({filters => vec![(&*tempdir_filter(tempdir.path()), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()), @r"
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@ -39,15 +37,15 @@ print(sys.last_exc)
----- stderr -----
");
});
assert_cmd_snapshot!(knot().arg("--project").arg(tempdir.path()).arg("--python-version").arg("3.12"), @r"
assert_cmd_snapshot!(case.command().arg("--python-version").arg("3.12"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
");
});
Ok(())
}
@ -69,53 +67,34 @@ print(sys.last_exc)
/// And the command is run in the `child` directory.
#[test]
fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
let tempdir = TempDir::new()?;
let project_dir = tempdir.path().canonicalize()?;
let libs = project_dir.join("libs");
std::fs::create_dir_all(&libs).context("Failed to create `libs` directory")?;
let child = project_dir.join("child");
std::fs::create_dir(&child).context("Failed to create `child` directory")?;
std::fs::write(
tempdir.path().join("pyproject.toml"),
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
"#,
)
.context("Failed to write `pyproject.toml`")?;
std::fs::write(
libs.join("utils.py"),
[tool.knot.environment]
python-version = "3.11"
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
def add(a: int, b: int) -> int:
a + b
"#,
)
.context("Failed to write `utils.py`")?;
std::fs::write(
child.join("test.py"),
"#,
),
(
"child/test.py",
r#"
from utils import add
from utils import add
stat = add(10, 15)
"#,
)
.context("Failed to write `child/test.py`")?;
let project_filter = tempdir_filter(&project_dir);
let filters = vec![
(&*project_filter, "<temp_dir>/"),
(r#"\\(\w\w|\s|\.|")"#, "/$1"),
];
stat = add(10, 15)
"#,
),
])?;
case.insta_settings().bind(|| {
// Make sure that the CLI fails when the `libs` directory is not in the search path.
insta::with_settings!({filters => filters}, {
assert_cmd_snapshot!(knot().current_dir(&child), @r#"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r#"
success: false
exit_code: 1
----- stdout -----
@ -123,10 +102,8 @@ stat = add(10, 15)
----- stderr -----
"#);
});
insta::with_settings!({filters => vec![(&*tempdir_filter(&project_dir), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().current_dir(child).arg("--extra-search-path").arg("../libs"), @r"
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")).arg("--extra-search-path").arg("../libs"), @r"
success: true
exit_code: 0
----- stdout -----
@ -153,47 +130,34 @@ stat = add(10, 15)
/// ```
#[test]
fn paths_in_configuration_files_are_relative_to_the_project_root() -> anyhow::Result<()> {
let tempdir = TempDir::new()?;
let project_dir = tempdir.path();
let libs = project_dir.join("libs");
std::fs::create_dir_all(&libs).context("Failed to create `libs` directory")?;
let child = project_dir.join("child");
std::fs::create_dir(&child).context("Failed to create `child` directory")?;
std::fs::write(
tempdir.path().join("pyproject.toml"),
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.environment]
python-version = "3.11"
extra-paths = ["libs"]
"#,
)
.context("Failed to write `pyproject.toml`")?;
std::fs::write(
libs.join("utils.py"),
[tool.knot.environment]
python-version = "3.11"
extra-paths = ["libs"]
"#,
),
(
"libs/utils.py",
r#"
def add(a: int, b: int) -> int:
def add(a: int, b: int) -> int:
a + b
"#,
)
.context("Failed to write `utils.py`")?;
std::fs::write(
child.join("test.py"),
"#,
),
(
"child/test.py",
r#"
from utils import add
from utils import add
stat = add(10, 15)
"#,
)
.context("Failed to write `child/test.py`")?;
stat = add(10, 15)
"#,
),
])?;
insta::with_settings!({filters => vec![(&*tempdir_filter(tempdir.path()), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().current_dir(child), @r"
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command().current_dir(case.project_dir().join("child")), @r"
success: true
exit_code: 0
----- stdout -----
@ -208,27 +172,22 @@ stat = add(10, 15)
/// The rule severity can be changed in the configuration file
#[test]
fn rule_severity() -> anyhow::Result<()> {
let tempdir = TempDir::new()?;
let project_dir = tempdir.path().canonicalize()?;
std::fs::write(
project_dir.join("test.py"),
let case = TestCase::with_file(
"test.py",
r#"
y = 4 / 0
y = 4 / 0
for a in range(0, y):
for a in range(0, y):
x = a
print(x) # possibly-unresolved-reference
"#,
)
.context("Failed to write `test.py`")?;
print(x) # possibly-unresolved-reference
"#,
)?;
case.insta_settings().bind(|| {
// Assert that there's a possibly unresolved reference diagnostic
// and that division-by-zero has a severity of error by default.
insta::with_settings!({filters => vec![(&*tempdir_filter(&project_dir), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().current_dir(&project_dir), @r"
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@ -237,20 +196,14 @@ print(x) # possibly-unresolved-reference
----- stderr -----
");
});
std::fs::write(
project_dir.join("pyproject.toml"),
r#"
[tool.knot.rules]
division-by-zero = "warn" # demote to warn
possibly-unresolved-reference = "ignore"
"#,
)
.context("Failed to write `pyproject.toml`")?;
case.write_file("pyproject.toml", r#"
[tool.knot.rules]
division-by-zero = "warn" # demote to warn
possibly-unresolved-reference = "ignore"
"#)?;
insta::with_settings!({filters => vec![(&*tempdir_filter(&project_dir), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().current_dir(project_dir), @r"
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@ -258,32 +211,27 @@ possibly-unresolved-reference = "ignore"
----- stderr -----
");
});
Ok(())
})
}
/// Red Knot warns about unknown rules
#[test]
fn unknown_rules() -> anyhow::Result<()> {
let tempdir = TempDir::new()?;
let project_dir = tempdir.path().canonicalize()?;
std::fs::write(
project_dir.join("pyproject.toml"),
let case = TestCase::with_files([
(
"pyproject.toml",
r#"
[tool.knot.rules]
division-by-zer = "warn" # incorrect rule name
"#,
)
.context("Failed to write `pyproject.toml`")?;
[tool.knot.rules]
division-by-zer = "warn" # incorrect rule name
"#,
),
("test.py", "print(10)"),
])?;
std::fs::write(project_dir.join("test.py"), r#"print(10)"#)
.context("Failed to write `test.py`")?;
insta::with_settings!({filters => vec![(&*tempdir_filter(&project_dir), "<temp_dir>/")]}, {
assert_cmd_snapshot!(knot().current_dir(project_dir), @r"
case.insta_settings().bind(|| {
assert_cmd_snapshot!(case.command(), @r"
success: false
exit_code: 1
----- stdout -----
@ -296,8 +244,82 @@ division-by-zer = "warn" # incorrect rule name
Ok(())
}
fn knot() -> Command {
Command::new(get_cargo_bin("red_knot"))
struct TestCase {
_temp_dir: TempDir,
project_dir: PathBuf,
}
impl TestCase {
fn new() -> anyhow::Result<Self> {
let temp_dir = TempDir::new()?;
// Canonicalize the tempdir path because macos uses symlinks for tempdirs
// and that doesn't play well with our snapshot filtering.
let project_dir = temp_dir
.path()
.canonicalize()
.context("Failed to canonicalize project path")?;
Ok(Self {
project_dir,
_temp_dir: temp_dir,
})
}
fn with_files<'a>(files: impl IntoIterator<Item = (&'a str, &'a str)>) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_files(files)?;
Ok(case)
}
fn with_file(path: impl AsRef<Path>, content: &str) -> anyhow::Result<Self> {
let case = Self::new()?;
case.write_file(path, content)?;
Ok(case)
}
fn write_files<'a>(
&self,
files: impl IntoIterator<Item = (&'a str, &'a str)>,
) -> anyhow::Result<()> {
for (path, content) in files {
self.write_file(path, content)?;
}
Ok(())
}
fn write_file(&self, path: impl AsRef<Path>, content: &str) -> anyhow::Result<()> {
let path = path.as_ref();
let path = self.project_dir.join(path);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create directory `{}`", parent.display()))?;
}
std::fs::write(&path, &*ruff_python_trivia::textwrap::dedent(content))
.with_context(|| format!("Failed to write file `{path}`", path = path.display()))?;
Ok(())
}
fn project_dir(&self) -> &Path {
&self.project_dir
}
// Returns the insta filters to escape paths in snapshots
fn insta_settings(&self) -> Settings {
let mut settings = insta::Settings::clone_current();
settings.add_filter(&tempdir_filter(&self.project_dir), "<temp_dir>/");
settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
settings
}
fn command(&self) -> Command {
let mut command = Command::new(get_cargo_bin("red_knot"));
command.current_dir(&self.project_dir);
command
}
}
fn tempdir_filter(path: &Path) -> String {