mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-01 06:11:43 +00:00
204 lines
6 KiB
Rust
204 lines
6 KiB
Rust
#![cfg(not(target_family = "wasm"))]
|
|
|
|
use std::io::{ErrorKind, Read, Write};
|
|
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream};
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::{Command, Stdio};
|
|
use std::thread::sleep;
|
|
use std::time::Duration;
|
|
use std::{fs, process, str};
|
|
|
|
use anyhow::{anyhow, bail, Context, Result};
|
|
use insta_cmd::get_cargo_bin;
|
|
use log::info;
|
|
use walkdir::WalkDir;
|
|
|
|
use ruff_linter::logging::{set_up_logging, LogLevel};
|
|
|
|
/// Handles `blackd` process and allows submitting code to it for formatting.
|
|
struct Blackd {
|
|
address: SocketAddr,
|
|
server: process::Child,
|
|
client: ureq::Agent,
|
|
}
|
|
|
|
const BIN_NAME: &str = "ruff";
|
|
|
|
impl Blackd {
|
|
pub(crate) fn new() -> Result<Self> {
|
|
// Get free TCP port to run on
|
|
let address = TcpListener::bind(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 0))?.local_addr()?;
|
|
|
|
let args = [
|
|
"--bind-host",
|
|
&address.ip().to_string(),
|
|
"--bind-port",
|
|
&address.port().to_string(),
|
|
];
|
|
let server = Command::new("blackd")
|
|
.args(args)
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.spawn()
|
|
.context("Starting blackd")?;
|
|
|
|
// Wait up to four seconds for `blackd` to be ready.
|
|
for _ in 0..20 {
|
|
match TcpStream::connect(address) {
|
|
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
|
|
info!("`blackd` not ready yet; retrying...");
|
|
sleep(Duration::from_millis(200));
|
|
}
|
|
Err(e) => return Err(e.into()),
|
|
Ok(_) => {
|
|
info!("`blackd` ready");
|
|
return Ok(Self {
|
|
address,
|
|
server,
|
|
client: ureq::agent(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
bail!("blackd {:?} failed to start", args)
|
|
}
|
|
|
|
/// Format given code with blackd.
|
|
pub(crate) fn check(&self, code: &[u8]) -> Result<Vec<u8>> {
|
|
match self
|
|
.client
|
|
.post(&format!("http://{}/", self.address))
|
|
.set("X-Line-Length", "88")
|
|
.send_bytes(code)
|
|
{
|
|
// 204 indicates the input wasn't changed during formatting, so
|
|
// we return the original.
|
|
Ok(response) => {
|
|
if response.status() == 204 {
|
|
Ok(code.to_vec())
|
|
} else {
|
|
let mut buf = vec![];
|
|
response
|
|
.into_reader()
|
|
.take((1024 * 1024) as u64)
|
|
.read_to_end(&mut buf)?;
|
|
Ok(buf)
|
|
}
|
|
}
|
|
Err(ureq::Error::Status(_, response)) => Err(anyhow::anyhow!(
|
|
"Formatting with `black` failed: {}",
|
|
response.into_string()?
|
|
)),
|
|
Err(e) => Err(e.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for Blackd {
|
|
fn drop(&mut self) {
|
|
self.server.kill().expect("Couldn't end `blackd` process");
|
|
}
|
|
}
|
|
|
|
fn run_test(path: &Path, blackd: &Blackd, ruff_args: &[&str]) -> Result<()> {
|
|
let input = fs::read(path)?;
|
|
|
|
// Step 1: Run `ruff` on the input.
|
|
let mut step_1 = Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(ruff_args)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()?;
|
|
if let Some(mut stdin) = step_1.stdin.take() {
|
|
stdin.write_all(input.as_ref())?;
|
|
}
|
|
let step_1_output = step_1.wait_with_output()?;
|
|
if !step_1_output.status.success() {
|
|
return Err(anyhow!(
|
|
"Running input through ruff failed:\n{}",
|
|
str::from_utf8(&step_1_output.stderr)?
|
|
));
|
|
}
|
|
|
|
// Step 2: Run `blackd` on the input.
|
|
let step_2_output = blackd.check(&step_1_output.stdout.clone())?;
|
|
|
|
// Step 3: Re-run `ruff` on the input.
|
|
let mut step_3 = Command::new(get_cargo_bin(BIN_NAME))
|
|
.args(ruff_args)
|
|
.stdin(Stdio::piped())
|
|
.stdout(Stdio::piped())
|
|
.spawn()?;
|
|
if let Some(mut stdin) = step_3.stdin.take() {
|
|
stdin.write_all(step_2_output.as_ref())?;
|
|
}
|
|
let step_3_output = step_3.wait_with_output()?;
|
|
if !step_3_output.status.success() {
|
|
return Err(anyhow!(
|
|
"Running input through ruff after black failed:\n{}",
|
|
str::from_utf8(&step_3_output.stderr)?
|
|
));
|
|
}
|
|
let step_3_output = step_3_output.stdout.clone();
|
|
|
|
assert_eq!(
|
|
str::from_utf8(&step_2_output),
|
|
str::from_utf8(&step_3_output),
|
|
"Mismatch found for {}",
|
|
path.display()
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn test_ruff_black_compatibility() -> Result<()> {
|
|
set_up_logging(&LogLevel::Default)?;
|
|
|
|
let blackd = Blackd::new()?;
|
|
|
|
let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("resources")
|
|
.join("test")
|
|
.join("fixtures");
|
|
|
|
// Ignore some fixtures that currently trigger errors. `E999.py` especially, as
|
|
// that is triggering a syntax error on purpose.
|
|
let excludes = ["E999.py", "W605_1.py"];
|
|
|
|
let paths = WalkDir::new(fixtures_dir)
|
|
.into_iter()
|
|
.flatten()
|
|
.filter(|entry| {
|
|
entry
|
|
.path()
|
|
.extension()
|
|
.is_some_and(|ext| ext == "py" || ext == "pyi")
|
|
&& !excludes.contains(&entry.path().file_name().unwrap().to_str().unwrap())
|
|
});
|
|
|
|
let ruff_args = [
|
|
"-",
|
|
"--silent",
|
|
"--exit-zero",
|
|
"--fix",
|
|
"--line-length",
|
|
"88",
|
|
"--select",
|
|
"ALL",
|
|
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
|
|
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
|
|
// newline, and ruff removes the `# noqa: W292` again.
|
|
"--ignore",
|
|
"RUF",
|
|
];
|
|
|
|
for entry in paths {
|
|
let path = entry.path();
|
|
run_test(path, &blackd, &ruff_args).context(format!("Testing {}", path.display()))?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|