mirror of
https://github.com/astral-sh/ruff.git
synced 2025-07-29 07:54:40 +00:00
Add snapshot tests for resolver (#5404)
## Summary This PR adds some snapshot tests for the resolver based on executing resolutions within a "mock" of the Airflow repo (that is: a folder that contains a subset of the repo's files, but all empty, and with an only-partially-complete virtual environment). It's intended to act as a lightweight integration test, to enable us to test resolutions on a "real" project without adding a dependency on Airflow itself.
This commit is contained in:
parent
a68a86e18b
commit
6587fb844a
32 changed files with 391 additions and 22 deletions
|
@ -19,3 +19,4 @@ log = { workspace = true }
|
|||
[dev-dependencies]
|
||||
env_logger = "0.10.0"
|
||||
tempfile = "3.6.0"
|
||||
insta = { workspace = true }
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# airflow
|
||||
|
||||
This is a mock subset of the Airflow repository, used to test module resolution.
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
# Standard library.
|
||||
import os
|
||||
|
||||
# First-party.
|
||||
from airflow.jobs.scheduler_job_runner import SchedulerJobRunner
|
||||
|
||||
# Stub file.
|
||||
from airflow.compat.functools import cached_property
|
||||
|
||||
# Namespace package.
|
||||
from airflow.providers.google.cloud.hooks.gcs import GCSHook
|
||||
|
||||
# Third-party.
|
||||
from sqlalchemy.orm import Query
|
|
@ -0,0 +1,16 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -0,0 +1 @@
|
|||
"""Empty file included to support filesystem-based resolver tests."""
|
|
@ -1,4 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -24,8 +24,8 @@ pub(crate) struct ImplicitImport {
|
|||
}
|
||||
|
||||
/// Find the "implicit" imports within the namespace package at the given path.
|
||||
pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> HashMap<String, ImplicitImport> {
|
||||
let mut implicit_imports = HashMap::new();
|
||||
pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> BTreeMap<String, ImplicitImport> {
|
||||
let mut implicit_imports = BTreeMap::new();
|
||||
|
||||
// Enumerate all files and directories in the path, expanding links.
|
||||
let Ok(entries) = fs::read_dir(dir_path) else {
|
||||
|
@ -128,14 +128,14 @@ pub(crate) fn find(dir_path: &Path, exclusions: &[&Path]) -> HashMap<String, Imp
|
|||
|
||||
/// Filter a map of implicit imports to only include those that were actually imported.
|
||||
pub(crate) fn filter(
|
||||
implicit_imports: &HashMap<String, ImplicitImport>,
|
||||
implicit_imports: &BTreeMap<String, ImplicitImport>,
|
||||
imported_symbols: &[String],
|
||||
) -> Option<HashMap<String, ImplicitImport>> {
|
||||
) -> Option<BTreeMap<String, ImplicitImport>> {
|
||||
if implicit_imports.is_empty() || imported_symbols.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut filtered_imports = HashMap::new();
|
||||
let mut filtered_imports = BTreeMap::new();
|
||||
for implicit_import in implicit_imports.values() {
|
||||
if imported_symbols.contains(&implicit_import.name) {
|
||||
filtered_imports.insert(implicit_import.name.clone(), implicit_import.clone());
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
//! Interface that describes the output of the import resolver.
|
||||
|
||||
use crate::implicit_imports::ImplicitImport;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::implicit_imports::ImplicitImport;
|
||||
use crate::py_typed::PyTypedInfo;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -69,11 +69,11 @@ pub(crate) struct ImportResult {
|
|||
|
||||
/// A map from file to resolved path, for all implicitly imported
|
||||
/// modules that are part of a namespace package.
|
||||
pub(crate) implicit_imports: HashMap<String, ImplicitImport>,
|
||||
pub(crate) implicit_imports: BTreeMap<String, ImplicitImport>,
|
||||
|
||||
/// Any implicit imports whose symbols were explicitly imported (i.e., via
|
||||
/// a `from x import y` statement).
|
||||
pub(crate) filtered_implicit_imports: HashMap<String, ImplicitImport>,
|
||||
pub(crate) filtered_implicit_imports: BTreeMap<String, ImplicitImport>,
|
||||
|
||||
/// If the import resolved to a type hint (i.e., a `.pyi` file), then
|
||||
/// a non-type-hint resolution will be stored here.
|
||||
|
@ -105,8 +105,8 @@ impl ImportResult {
|
|||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: HashMap::default(),
|
||||
filtered_implicit_imports: HashMap::default(),
|
||||
implicit_imports: BTreeMap::default(),
|
||||
filtered_implicit_imports: BTreeMap::default(),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: None,
|
||||
|
|
|
@ -12,5 +12,3 @@ mod python_platform;
|
|||
mod python_version;
|
||||
mod resolver;
|
||||
mod search;
|
||||
|
||||
pub(crate) const SITE_PACKAGES: &str = "site-packages";
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
//! Resolves Python imports to their corresponding files on disk.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use log::debug;
|
||||
|
@ -36,7 +36,7 @@ fn resolve_module_descriptor(
|
|||
let mut is_stub_package = false;
|
||||
let mut is_stub_file = false;
|
||||
let mut is_native_lib = false;
|
||||
let mut implicit_imports = HashMap::new();
|
||||
let mut implicit_imports = BTreeMap::new();
|
||||
let mut package_directory = None;
|
||||
let mut py_typed_info = None;
|
||||
|
||||
|
@ -194,7 +194,7 @@ fn resolve_module_descriptor(
|
|||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports,
|
||||
filtered_implicit_imports: HashMap::default(),
|
||||
filtered_implicit_imports: BTreeMap::default(),
|
||||
non_stub_import_result: None,
|
||||
py_typed_info,
|
||||
package_directory,
|
||||
|
@ -424,7 +424,7 @@ fn resolve_best_absolute_import<Host: host::Host>(
|
|||
/// are all satisfied by submodules (as listed in the implicit imports).
|
||||
fn is_namespace_package_resolved(
|
||||
module_descriptor: &ImportModuleDescriptor,
|
||||
implicit_imports: &HashMap<String, ImplicitImport>,
|
||||
implicit_imports: &BTreeMap<String, ImplicitImport>,
|
||||
) -> bool {
|
||||
if !module_descriptor.imported_symbols.is_empty() {
|
||||
// Pyright uses `!Array.from(moduleDescriptor.importedSymbols.keys()).some((symbol) => implicitImports.has(symbol))`.
|
||||
|
@ -774,6 +774,7 @@ fn resolve_import<Host: host::Host>(
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_debug_snapshot;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
@ -823,6 +824,8 @@ mod tests {
|
|||
library: Option<PathBuf>,
|
||||
stub_path: Option<PathBuf>,
|
||||
typeshed_path: Option<PathBuf>,
|
||||
venv_path: Option<PathBuf>,
|
||||
venv: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn resolve_options(
|
||||
|
@ -836,6 +839,8 @@ mod tests {
|
|||
library,
|
||||
stub_path,
|
||||
typeshed_path,
|
||||
venv_path,
|
||||
venv,
|
||||
} = options;
|
||||
|
||||
let execution_environment = ExecutionEnvironment {
|
||||
|
@ -860,8 +865,8 @@ mod tests {
|
|||
let config = Config {
|
||||
typeshed_path,
|
||||
stub_path,
|
||||
venv_path: None,
|
||||
venv: None,
|
||||
venv_path,
|
||||
venv,
|
||||
};
|
||||
|
||||
let host = host::StaticHost::new(if let Some(library) = library {
|
||||
|
@ -1545,4 +1550,109 @@ mod tests {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_standard_library() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"os",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_first_party() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.jobs.scheduler_job_runner",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_stub_file() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.compat.functools",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_namespace_package() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"airflow.providers.google.cloud.hooks.gcs",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn airflow_third_party() {
|
||||
setup();
|
||||
|
||||
let root = PathBuf::from("./resources/test/airflow");
|
||||
let source_file = root.join("airflow/api/common/mark_tasks.py");
|
||||
|
||||
let result = resolve_options(
|
||||
source_file,
|
||||
"sqlalchemy.orm",
|
||||
root.clone(),
|
||||
ResolverOptions {
|
||||
venv_path: Some(root),
|
||||
venv: Some(PathBuf::from("venv")),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
assert_debug_snapshot!(result);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,9 +8,11 @@ use std::path::{Path, PathBuf};
|
|||
use log::debug;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::host;
|
||||
use crate::module_descriptor::ImportModuleDescriptor;
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::{host, SITE_PACKAGES};
|
||||
|
||||
const SITE_PACKAGES: &str = "site-packages";
|
||||
|
||||
/// Find the `site-packages` directory for the specified Python version.
|
||||
fn find_site_packages_path(
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/resolver.rs
|
||||
expression: result
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/jobs/__init__.py",
|
||||
"./resources/test/airflow/airflow/jobs/scheduler_job_runner.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/resolver.rs
|
||||
expression: result
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: true,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"",
|
||||
"./resources/test/airflow/airflow/providers/google/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/hooks/__init__.py",
|
||||
"./resources/test/airflow/airflow/providers/google/cloud/hooks/gcs.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/resolver.rs
|
||||
expression: result
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: false,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: false,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [],
|
||||
search_path: None,
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: None,
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/resolver.rs
|
||||
expression: result
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/functools.pyi",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: true,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: Some(
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: Local,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/airflow/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/__init__.py",
|
||||
"./resources/test/airflow/airflow/compat/functools.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
},
|
||||
),
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/airflow",
|
||||
),
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
---
|
||||
source: crates/ruff_python_resolver/src/resolver.rs
|
||||
expression: result
|
||||
---
|
||||
ImportResult {
|
||||
is_relative: false,
|
||||
is_import_found: true,
|
||||
is_partly_resolved: false,
|
||||
is_namespace_package: false,
|
||||
is_init_file_present: true,
|
||||
is_stub_package: false,
|
||||
import_type: ThirdParty,
|
||||
resolved_paths: [
|
||||
"./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/__init__.py",
|
||||
"./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/__init__.py",
|
||||
],
|
||||
search_path: Some(
|
||||
"./resources/test/airflow/venv/Lib/python3.11/site-packages",
|
||||
),
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
is_stdlib_typeshed_file: false,
|
||||
is_third_party_typeshed_file: false,
|
||||
is_local_typings_file: false,
|
||||
implicit_imports: {
|
||||
"base": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
name: "base",
|
||||
path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/base.py",
|
||||
py_typed: None,
|
||||
},
|
||||
"dependency": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
name: "dependency",
|
||||
path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/dependency.py",
|
||||
py_typed: None,
|
||||
},
|
||||
"query": ImplicitImport {
|
||||
is_stub_file: false,
|
||||
is_native_lib: false,
|
||||
name: "query",
|
||||
path: "./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy/orm/query.py",
|
||||
py_typed: None,
|
||||
},
|
||||
},
|
||||
filtered_implicit_imports: {},
|
||||
non_stub_import_result: None,
|
||||
py_typed_info: None,
|
||||
package_directory: Some(
|
||||
"./resources/test/airflow/venv/Lib/python3.11/site-packages/sqlalchemy",
|
||||
),
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue