ruff/crates/ruff_python_parser/tests/generate_inline_tests.rs
renovate[bot] 61bb2a8245
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 instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run
Update Rust crate anyhow to v1.0.100 (#20499)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Micha Reiser <micha@reiser.io>
2025-09-22 09:51:52 +02:00

303 lines
8.7 KiB
Rust

//! This module takes specially formatted comments from `ruff_python_parser` code
//! and turns them into test fixtures. The code is derived from `rust-analyzer`
//! and `biome`.
//!
//! References:
//! - <https://github.com/rust-lang/rust-analyzer/blob/e4a405f877efd820bef9c0e77a02494e47c17512/crates/parser/src/tests/sourcegen_inline_tests.rs>
//! - <https://github.com/biomejs/biome/blob/b9f8ffea9967b098ec4c8bf74fa96826a879f043/xtask/codegen/src/parser_tests.rs>
use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::ops::{AddAssign, Deref, DerefMut};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
fn project_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../")
.canonicalize()
.unwrap()
}
#[test]
fn generate_inline_tests() -> Result<()> {
let parser_dir = project_root().join("crates/ruff_python_parser/src/");
let tests = TestCollection::try_from(parser_dir.as_path())?;
let mut test_files = TestFiles::default();
test_files += install_tests(&tests.ok, "crates/ruff_python_parser/resources/inline/ok")?;
test_files += install_tests(&tests.err, "crates/ruff_python_parser/resources/inline/err")?;
if !test_files.is_empty() {
anyhow::bail!("{test_files}");
}
Ok(())
}
#[derive(Debug, Default)]
struct TestFiles {
unreferenced: Vec<PathBuf>,
updated: Vec<PathBuf>,
}
impl TestFiles {
fn is_empty(&self) -> bool {
self.unreferenced.is_empty() && self.updated.is_empty()
}
}
impl AddAssign<TestFiles> for TestFiles {
fn add_assign(&mut self, other: TestFiles) {
self.unreferenced.extend(other.unreferenced);
self.updated.extend(other.updated);
}
}
impl fmt::Display for TestFiles {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
writeln!(f, "No unreferenced or updated test files found")
} else {
let root_dir = project_root();
if !self.unreferenced.is_empty() {
writeln!(
f,
"Unreferenced test files found for which no comment exists:",
)?;
for path in &self.unreferenced {
writeln!(f, " {}", path.strip_prefix(&root_dir).unwrap().display())?;
}
writeln!(f, "Please delete these files manually")?;
}
if !self.updated.is_empty() {
if !self.unreferenced.is_empty() {
writeln!(f)?;
}
writeln!(
f,
"Following files were not up-to date and has been updated:",
)?;
for path in &self.updated {
writeln!(f, " {}", path.strip_prefix(&root_dir).unwrap().display())?;
}
writeln!(
f,
"Re-run the tests with `cargo test` to update the test snapshots"
)?;
if std::env::var("CI").is_ok() {
writeln!(
f,
"NOTE: Run the tests locally and commit the updated files"
)?;
}
}
Ok(())
}
}
}
fn install_tests(tests: &HashMap<String, Test>, target_dir: &str) -> Result<TestFiles> {
let root_dir = project_root();
let tests_dir = root_dir.join(target_dir);
if !tests_dir.is_dir() {
fs::create_dir_all(&tests_dir)?;
}
// Test kind is irrelevant for existing test cases.
let existing = existing_tests(&tests_dir)?;
let mut updated_files = vec![];
for (name, test) in tests {
let path = match existing.get(name) {
Some(path) => path.clone(),
None => tests_dir.join(name).with_extension("py"),
};
match fs::read_to_string(&path) {
Ok(old_contents) if old_contents == test.contents => continue,
_ => {}
}
fs::write(&path, &test.contents)
.with_context(|| format!("Failed to write to {:?}", path.display()))?;
updated_files.push(path);
}
Ok(TestFiles {
unreferenced: existing
.into_iter()
.filter(|(name, _)| !tests.contains_key(name))
.map(|(_, path)| path)
.collect::<Vec<_>>(),
updated: updated_files,
})
}
#[derive(Default, Debug)]
struct TestCollection {
ok: HashMap<String, Test>,
err: HashMap<String, Test>,
}
impl TryFrom<&Path> for TestCollection {
type Error = anyhow::Error;
fn try_from(path: &Path) -> Result<Self> {
let mut tests = TestCollection::default();
for entry in walkdir::WalkDir::new(path) {
let entry = entry?;
if !entry.file_type().is_file() {
continue;
}
if entry.path().extension().unwrap_or_default() != "rs" {
continue;
}
let text = fs::read_to_string(entry.path())?;
for test in collect_tests(&text) {
if test.is_ok() {
if let Some(old_test) = tests.ok.insert(test.name.clone(), test) {
anyhow::bail!(
"Duplicate test found: {name:?} (search '// test_ok {name}' for the location)\n",
name = old_test.name
);
}
} else if let Some(old_test) = tests.err.insert(test.name.clone(), test) {
anyhow::bail!(
"Duplicate test found: {name:?} (search '// test_err {name}' for the location)\n",
name = old_test.name
);
}
}
}
Ok(tests)
}
}
#[derive(Debug, Clone, Copy)]
enum TestKind {
Ok,
Err,
}
/// A test of the following form:
///
/// ```text
/// // (test_ok|test_err) name
/// // <code>
/// ```
#[derive(Debug)]
struct Test {
name: String,
contents: String,
kind: TestKind,
}
impl Test {
const fn is_ok(&self) -> bool {
matches!(self.kind, TestKind::Ok)
}
}
/// Collect the tests from the given source text.
fn collect_tests(text: &str) -> Vec<Test> {
let mut tests = Vec::new();
for comment_block in extract_comment_blocks(text) {
let first_line = &comment_block[0];
let (kind, name) = match first_line.split_once(' ') {
Some(("test_ok", suffix)) => (TestKind::Ok, suffix),
Some(("test_err", suffix)) => (TestKind::Err, suffix),
_ => continue,
};
let text: String = comment_block[1..]
.iter()
.cloned()
.chain([String::new()])
.collect::<Vec<_>>()
.join("\n");
assert!(!text.trim().is_empty() && text.ends_with('\n'));
tests.push(Test {
name: name.to_string(),
contents: text,
kind,
});
}
tests
}
#[derive(Debug, Default)]
struct CommentBlock(Vec<String>);
impl Deref for CommentBlock {
type Target = Vec<String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for CommentBlock {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/// Extract the comment blocks from the given source text.
///
/// A comment block is a sequence of lines that start with `// ` and are separated
/// by an empty line. An empty comment line (`//`) is also part of the block.
fn extract_comment_blocks(text: &str) -> Vec<CommentBlock> {
const COMMENT_PREFIX: &str = "// ";
const COMMENT_PREFIX_LEN: usize = COMMENT_PREFIX.len();
let mut comment_blocks = Vec::new();
let mut block = CommentBlock::default();
for line in text.lines().map(str::trim_start) {
if line == "//" {
block.push(String::new());
continue;
}
if line.starts_with(COMMENT_PREFIX) {
block.push(line[COMMENT_PREFIX_LEN..].to_string());
} else {
if !block.is_empty() {
comment_blocks.push(std::mem::take(&mut block));
}
}
}
if !block.is_empty() {
comment_blocks.push(block);
}
comment_blocks
}
/// Returns the existing tests in the given directory.
fn existing_tests(dir: &Path) -> Result<HashMap<String, PathBuf>> {
let mut tests = HashMap::new();
for file in fs::read_dir(dir)? {
let path = file?.path();
if path.extension().unwrap_or_default() != "py" {
continue;
}
let name = path
.file_stem()
.map(|x| x.to_string_lossy().to_string())
.unwrap();
if let Some(old) = tests.insert(name, path) {
anyhow::bail!("Multiple test file exists for {old:?}");
}
}
Ok(tests)
}