ruff/crates/ruff_cli/tests/integration_test.rs
konsti 92f471a666
Handle io errors gracefully (#5611)
## Summary

It can happen that we can't read a file (a python file, a jupyter
notebook or pyproject.toml), which needs to be handled and handled
consistently for all file types. Instead of using `Err` or `error!`, we
emit E602 with the io error as message and continue. This PR makes sure
we handle all three cases consistently, emit E602.

I'm not convinced that it should be possible to disable io errors, but
we now handle the regular case consistently and at least print warning
consistently.

I went with `warn!` but i can change them all to `error!`, too.

It also checks the error case when a pyproject.toml is not readable. The
error message is not very helpful, but it's now a bit clearer that
actually ruff itself failed instead vs this being a diagnostic.

## Examples

This is how an Err of `run` looks now:


![image](890f7ab2-2309-4b6f-a4b3-67161947cc83)

With an unreadable file and `IOError` disabled:


![image](fd3d6959-fa23-4ddf-b2e5-8d6022df54b1)

(we lint zero files but count files before linting not during so we exit
0)

I'm not sure if it should (or if we should take a different path with
manual ExitStatus), but this currently also triggers when `files` is
empty:


![image](f7ede301-41b5-4743-97fd-49149f750337)

## Test Plan

Unix only: Create a temporary directory with files with permissions
`000` (not readable by the owner) and run on that directory. Since this
breaks the assumptions of most of the test code (single file, `ruff`
instead of `ruff_cli`), the test code is rather cumbersome and looks a
bit misplaced; i'm happy about suggestions to fit it in closer with the
other tests or streamline it in other ways. I added another test for
when the entire directory is not readable.
2023-07-20 11:30:14 +02:00

347 lines
8.8 KiB
Rust

#![cfg(not(target_family = "wasm"))]
#[cfg(unix)]
use std::fs;
#[cfg(unix)]
use std::fs::Permissions;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
#[cfg(unix)]
use std::path::Path;
use std::str;
#[cfg(unix)]
use anyhow::Context;
use anyhow::Result;
use assert_cmd::Command;
#[cfg(unix)]
use clap::Parser;
#[cfg(unix)]
use path_absolutize::path_dedot;
#[cfg(unix)]
use tempfile::TempDir;
use ruff_cli::args::Args;
use ruff_cli::run;
const BIN_NAME: &str = "ruff";
#[test]
fn stdin_success() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--format", "text", "--isolated"])
.write_stdin("")
.assert()
.success();
Ok(())
}
#[test]
fn stdin_error() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--isolated"])
.write_stdin("import os\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
r#"-:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
"#
);
Ok(())
}
#[test]
fn stdin_filename() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args([
"-",
"--format",
"text",
"--stdin-filename",
"F401.py",
"--isolated",
])
.write_stdin("import os\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
r#"F401.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 potentially fixable with the --fix option.
"#
);
Ok(())
}
#[cfg(unix)]
#[test]
fn stdin_json() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args([
"-",
"--format",
"json",
"--stdin-filename",
"F401.py",
"--isolated",
])
.write_stdin("import os\n")
.assert()
.failure();
let directory = path_dedot::CWD.to_str().unwrap();
let binding = Path::new(directory).join("F401.py");
let file_path = binding.display();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
format!(
r#"[
{{
"code": "F401",
"end_location": {{
"column": 10,
"row": 1
}},
"filename": "{file_path}",
"fix": {{
"applicability": "Automatic",
"edits": [
{{
"content": "",
"end_location": {{
"column": 1,
"row": 2
}},
"location": {{
"column": 1,
"row": 1
}}
}}
],
"message": "Remove unused import: `os`"
}},
"location": {{
"column": 8,
"row": 1
}},
"message": "`os` imported but unused",
"noqa_row": 1,
"url": "https://beta.ruff.rs/docs/rules/unused-import"
}}
]"#
)
);
Ok(())
}
#[test]
fn stdin_autofix() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import os\nimport sys\n\nprint(sys.version)\n")
.assert()
.success();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"import sys\n\nprint(sys.version)\n"
);
Ok(())
}
#[test]
fn stdin_autofix_when_not_fixable_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import os\nimport sys\n\nif (1, 2):\n print(sys.version)\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"import sys\n\nif (1, 2):\n print(sys.version)\n"
);
Ok(())
}
#[test]
fn stdin_autofix_when_no_issues_should_still_print_contents() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--fix", "--isolated"])
.write_stdin("import sys\n\nprint(sys.version)\n")
.assert()
.success();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
"import sys\n\nprint(sys.version)\n"
);
Ok(())
}
#[test]
fn show_source() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["-", "--format", "text", "--show-source", "--isolated"])
.write_stdin("l = 1")
.assert()
.failure();
assert!(str::from_utf8(&output.get_output().stdout)?.contains("l = 1"));
Ok(())
}
#[test]
fn explain_status_codes() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["--explain", "F401"]).assert().success();
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["--explain", "RUF404"]).assert().failure();
Ok(())
}
#[test]
fn show_statistics() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args([
"-",
"--format",
"text",
"--select",
"F401",
"--statistics",
"--isolated",
])
.write_stdin("import sys\nimport os\n\nprint(os.getuid())\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?
.lines()
.last()
.unwrap(),
"1\tF401\t[*] `sys` imported but unused"
);
Ok(())
}
#[test]
fn nursery_prefix() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
// `--select E` should detect E741, but not E225, which is in the nursery.
let output = cmd
.args(["-", "--format", "text", "--isolated", "--select", "E"])
.write_stdin("I=42\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
r#"-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
"#
);
Ok(())
}
#[test]
fn nursery_all() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
// `--select ALL` should detect E741, but not E225, which is in the nursery.
let output = cmd
.args(["-", "--format", "text", "--isolated", "--select", "E"])
.write_stdin("I=42\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
r#"-:1:1: E741 Ambiguous variable name: `I`
Found 1 error.
"#
);
Ok(())
}
#[test]
fn nursery_direct() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
// `--select E225` should detect E225.
let output = cmd
.args(["-", "--format", "text", "--isolated", "--select", "E225"])
.write_stdin("I=42\n")
.assert()
.failure();
assert_eq!(
str::from_utf8(&output.get_output().stdout)?,
r#"-:1:2: E225 Missing whitespace around operator
Found 1 error.
"#
);
Ok(())
}
/// An unreadable pyproject.toml in non-isolated mode causes ruff to hard-error trying to build up
/// configuration globs
#[cfg(unix)]
#[test]
fn unreadable_pyproject_toml() -> Result<()> {
let tempdir = TempDir::new()?;
let pyproject_toml = tempdir.path().join("pyproject.toml");
// Create an empty file with 000 permissions
fs::OpenOptions::new()
.create(true)
.write(true)
.mode(0o000)
.open(pyproject_toml)?;
// Don't `--isolated` since the configuration discovery is where the error happens
let args = Args::parse_from(["", "check", "--no-cache", tempdir.path().to_str().unwrap()]);
let err = run(args).err().context("Unexpected success")?;
assert_eq!(
err.chain()
.map(std::string::ToString::to_string)
.collect::<Vec<_>>(),
vec!["Permission denied (os error 13)".to_string()],
);
Ok(())
}
/// Check the output with an unreadable directory
#[cfg(unix)]
#[test]
fn unreadable_dir() -> Result<()> {
// Create a directory with 000 (not iterable/readable) permissions
let tempdir = TempDir::new()?;
let unreadable_dir = tempdir.path().join("unreadable_dir");
fs::create_dir(&unreadable_dir)?;
fs::set_permissions(&unreadable_dir, Permissions::from_mode(0o000))?;
// We (currently?) have to use a subcommand to check exit status (currently wrong) and logging
// output
let mut cmd = Command::cargo_bin(BIN_NAME)?;
let output = cmd
.args(["--no-cache", "--isolated"])
.arg(&unreadable_dir)
.assert()
// TODO(konstin): This should be a failure, but we currently can't track that
.success();
assert_eq!(
str::from_utf8(&output.get_output().stderr)?,
"warning: Encountered error: Permission denied (os error 13)\n"
);
Ok(())
}