[red-knot] Rework module resolver tests (#12260)

This commit is contained in:
Alex Waygood 2024-07-10 11:40:21 +01:00 committed by GitHub
parent 880c31d164
commit e8b5341c97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 642 additions and 471 deletions

View File

@ -25,13 +25,9 @@ pub(crate) mod tests {
use salsa::DebugWithDb;
use ruff_db::files::Files;
use ruff_db::system::{
DbWithTestSystem, MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem,
};
use ruff_db::system::{DbWithTestSystem, TestSystem};
use ruff_db::vendored::VendoredFileSystem;
use crate::resolver::{set_module_resolution_settings, RawModuleResolutionSettings};
use crate::supported_py_version::TargetVersion;
use crate::vendored_typeshed_stubs;
use super::*;
@ -127,131 +123,4 @@ pub(crate) mod tests {
})
}
}
pub(crate) struct TestCaseBuilder {
db: TestDb,
src: SystemPathBuf,
site_packages: SystemPathBuf,
target_version: Option<TargetVersion>,
with_vendored_stubs: bool,
}
impl TestCaseBuilder {
#[must_use]
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
self.target_version = Some(target_version);
self
}
#[must_use]
pub(crate) fn with_vendored_stubs_used(mut self) -> Self {
self.with_vendored_stubs = true;
self
}
fn create_mocked_typeshed(
typeshed_dir: &SystemPath,
fs: &MemoryFileSystem,
) -> std::io::Result<()> {
static VERSIONS_DATA: &str = "\
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11
collections: 3.9- # 'Regular' package on py39+
functools: 3.8-
importlib: 3.9- # Namespace package on py39+
xml: 3.8-3.8 # Namespace package on py38 only
";
fs.create_directory_all(typeshed_dir)?;
fs.write_file(typeshed_dir.join("stdlib/VERSIONS"), VERSIONS_DATA)?;
// Regular package on py38+
fs.create_directory_all(typeshed_dir.join("stdlib/asyncio"))?;
fs.touch(typeshed_dir.join("stdlib/asyncio/__init__.pyi"))?;
fs.write_file(
typeshed_dir.join("stdlib/asyncio/tasks.pyi"),
"class Task: ...",
)?;
// Regular package on py39+
fs.create_directory_all(typeshed_dir.join("stdlib/collections"))?;
fs.touch(typeshed_dir.join("stdlib/collections/__init__.pyi"))?;
// Namespace package on py38 only
fs.create_directory_all(typeshed_dir.join("stdlib/xml"))?;
fs.touch(typeshed_dir.join("stdlib/xml/etree.pyi"))?;
// Namespace package on py39+
fs.create_directory_all(typeshed_dir.join("stdlib/importlib"))?;
fs.touch(typeshed_dir.join("stdlib/importlib/abc.pyi"))?;
fs.write_file(
typeshed_dir.join("stdlib/functools.pyi"),
"def update_wrapper(): ...",
)
}
pub(crate) fn build(self) -> std::io::Result<TestCase> {
let TestCaseBuilder {
mut db,
src,
with_vendored_stubs,
site_packages,
target_version,
} = self;
let typeshed_dir = SystemPathBuf::from("/typeshed");
let custom_typeshed = if with_vendored_stubs {
None
} else {
Self::create_mocked_typeshed(&typeshed_dir, db.memory_file_system())?;
Some(typeshed_dir.clone())
};
let settings = RawModuleResolutionSettings {
target_version: target_version.unwrap_or_default(),
extra_paths: vec![],
workspace_root: src.clone(),
custom_typeshed: custom_typeshed.clone(),
site_packages: Some(site_packages.clone()),
};
set_module_resolution_settings(&mut db, settings);
Ok(TestCase {
db,
src: src.clone(),
custom_typeshed: typeshed_dir,
site_packages: site_packages.clone(),
})
}
}
pub(crate) struct TestCase {
pub(crate) db: TestDb,
pub(crate) src: SystemPathBuf,
pub(crate) custom_typeshed: SystemPathBuf,
pub(crate) site_packages: SystemPathBuf,
}
pub(crate) fn create_resolver_builder() -> std::io::Result<TestCaseBuilder> {
let db = TestDb::new();
let src = SystemPathBuf::from("/src");
let site_packages = SystemPathBuf::from("/site_packages");
let fs = db.memory_file_system();
fs.create_directory_all(&src)?;
fs.create_directory_all(&site_packages)?;
Ok(TestCaseBuilder {
db,
src,
with_vendored_stubs: false,
site_packages,
target_version: None,
})
}
}

View File

@ -7,6 +7,9 @@ mod state;
mod supported_py_version;
mod typeshed;
#[cfg(test)]
mod testing;
pub use db::{Db, Jar};
pub use module::{Module, ModuleKind};
pub use module_name::ModuleName;

View File

@ -254,6 +254,18 @@ impl fmt::Debug for ModuleResolutionPathBuf {
}
}
impl PartialEq<SystemPathBuf> for ModuleResolutionPathBuf {
fn eq(&self, other: &SystemPathBuf) -> bool {
ModuleResolutionPathRef::from(self) == **other
}
}
impl PartialEq<ModuleResolutionPathBuf> for SystemPathBuf {
fn eq(&self, other: &ModuleResolutionPathBuf) -> bool {
other.eq(self)
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum ModuleResolutionPathRefInner<'a> {
Extra(&'a SystemPath),
@ -643,9 +655,9 @@ impl PartialEq<ModuleResolutionPathRef<'_>> for VendoredPathBuf {
mod tests {
use insta::assert_debug_snapshot;
use crate::db::tests::{create_resolver_builder, TestCase, TestDb};
use crate::db::tests::TestDb;
use crate::supported_py_version::TargetVersion;
use crate::typeshed::LazyTypeshedVersions;
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use super::*;
@ -943,26 +955,41 @@ mod tests {
);
}
fn py38_stdlib_test_case() -> (TestDb, ModuleResolutionPathBuf) {
let TestCase {
db,
custom_typeshed,
..
} = create_resolver_builder().unwrap().build().unwrap();
let stdlib_module_path =
ModuleResolutionPathBuf::stdlib_from_custom_typeshed_root(&custom_typeshed).unwrap();
(db, stdlib_module_path)
fn typeshed_test_case(
typeshed: MockedTypeshed,
target_version: TargetVersion,
) -> (TestDb, ModuleResolutionPathBuf) {
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
.with_custom_typeshed(typeshed)
.with_target_version(target_version)
.build();
let stdlib = ModuleResolutionPathBuf::standard_library(FilePath::System(stdlib)).unwrap();
(db, stdlib)
}
fn py38_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, ModuleResolutionPathBuf) {
typeshed_test_case(typeshed, TargetVersion::Py38)
}
fn py39_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, ModuleResolutionPathBuf) {
typeshed_test_case(typeshed, TargetVersion::Py39)
}
#[test]
fn mocked_typeshed_existing_regular_stdlib_pkg_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const VERSIONS: &str = "\
asyncio: 3.8-
asyncio.tasks: 3.9-3.11
";
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: VERSIONS,
stdlib_files: &[("asyncio/__init__.pyi", ""), ("asyncio/tasks.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let asyncio_regular_package = stdlib_path.join("asyncio");
assert!(asyncio_regular_package.is_directory(&stdlib_path, &resolver));
assert!(asyncio_regular_package.is_regular_package(&stdlib_path, &resolver));
@ -986,13 +1013,14 @@ mod tests {
#[test]
fn mocked_typeshed_existing_namespace_stdlib_pkg_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "xml: 3.8-3.8",
stdlib_files: &[("xml/etree.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let xml_namespace_package = stdlib_path.join("xml");
assert!(xml_namespace_package.is_directory(&stdlib_path, &resolver));
// Paths to directories don't resolve to VfsFiles
@ -1007,13 +1035,14 @@ mod tests {
#[test]
fn mocked_typeshed_single_file_stdlib_module_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "functools: 3.8-",
stdlib_files: &[("functools.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let functools_module = stdlib_path.join("functools.pyi");
assert!(functools_module.to_file(&stdlib_path, &resolver).is_some());
assert!(!functools_module.is_directory(&stdlib_path, &resolver));
@ -1022,13 +1051,14 @@ mod tests {
#[test]
fn mocked_typeshed_nonexistent_regular_stdlib_pkg_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "collections: 3.9-",
stdlib_files: &[("collections/__init__.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let collections_regular_package = stdlib_path.join("collections");
assert_eq!(
collections_regular_package.to_file(&stdlib_path, &resolver),
@ -1040,13 +1070,14 @@ mod tests {
#[test]
fn mocked_typeshed_nonexistent_namespace_stdlib_pkg_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "importlib: 3.9-",
stdlib_files: &[("importlib/abc.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let importlib_namespace_package = stdlib_path.join("importlib");
assert_eq!(
importlib_namespace_package.to_file(&stdlib_path, &resolver),
@ -1063,43 +1094,42 @@ mod tests {
#[test]
fn mocked_typeshed_nonexistent_single_file_module_py38() {
let (db, stdlib_path) = py38_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py38,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "foo: 2.6-",
stdlib_files: &[("foo.pyi", "")],
};
let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py38);
let non_existent = stdlib_path.join("doesnt_even_exist");
assert_eq!(non_existent.to_file(&stdlib_path, &resolver), None);
assert!(!non_existent.is_directory(&stdlib_path, &resolver));
assert!(!non_existent.is_regular_package(&stdlib_path, &resolver));
}
fn py39_stdlib_test_case() -> (TestDb, ModuleResolutionPathBuf) {
let TestCase {
db,
custom_typeshed,
..
} = create_resolver_builder()
.unwrap()
.with_target_version(TargetVersion::Py39)
.build()
.unwrap();
let stdlib_module_path =
ModuleResolutionPathBuf::stdlib_from_custom_typeshed_root(&custom_typeshed).unwrap();
(db, stdlib_module_path)
}
#[test]
fn mocked_typeshed_existing_regular_stdlib_pkgs_py39() {
let (db, stdlib_path) = py39_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py39,
const VERSIONS: &str = "\
asyncio: 3.8-
asyncio.tasks: 3.9-3.11
collections: 3.9-
";
const STDLIB: &[FileSpec] = &[
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("collections/__init__.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: VERSIONS,
stdlib_files: STDLIB,
};
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py39);
// Since we've set the target version to Py39,
// `collections` should now exist as a directory, according to VERSIONS...
let collections_regular_package = stdlib_path.join("collections");
@ -1126,14 +1156,15 @@ mod tests {
#[test]
fn mocked_typeshed_existing_namespace_stdlib_pkg_py39() {
let (db, stdlib_path) = py39_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py39,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "importlib: 3.9-",
stdlib_files: &[("importlib/abc.pyi", "")],
};
// The `importlib` directory now also exists...
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py39);
// The `importlib` directory now also exists
let importlib_namespace_package = stdlib_path.join("importlib");
assert!(importlib_namespace_package.is_directory(&stdlib_path, &resolver));
assert!(!importlib_namespace_package.is_regular_package(&stdlib_path, &resolver));
@ -1143,7 +1174,7 @@ mod tests {
None
);
// ...As do submodules in the `importlib` namespace package:
// Submodules in the `importlib` namespace package also now exist:
let importlib_abc = importlib_namespace_package.join("abc.pyi");
assert!(!importlib_abc.is_directory(&stdlib_path, &resolver));
assert!(!importlib_abc.is_regular_package(&stdlib_path, &resolver));
@ -1152,13 +1183,14 @@ mod tests {
#[test]
fn mocked_typeshed_nonexistent_namespace_stdlib_pkg_py39() {
let (db, stdlib_path) = py39_stdlib_test_case();
let resolver = ResolverState {
db: &db,
typeshed_versions: LazyTypeshedVersions::new(),
target_version: TargetVersion::Py39,
const TYPESHED: MockedTypeshed = MockedTypeshed {
versions: "xml: 3.8-3.8",
stdlib_files: &[("xml/etree.pyi", "")],
};
let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
let resolver = ResolverState::new(&db, TargetVersion::Py39);
// The `xml` package no longer exists on py39:
let xml_namespace_package = stdlib_path.join("xml");
assert_eq!(xml_namespace_package.to_file(&stdlib_path, &resolver), None);

View File

@ -385,28 +385,22 @@ impl PackageKind {
#[cfg(test)]
mod tests {
use ruff_db::files::{system_path_to_file, File, FilePath};
use ruff_db::system::DbWithTestSystem;
use ruff_db::vendored::{VendoredPath, VendoredPathBuf};
use ruff_db::Upcast;
use ruff_db::system::{DbWithTestSystem, OsSystem, SystemPath};
use crate::db::tests::{create_resolver_builder, TestCase};
use crate::db::tests::TestDb;
use crate::module::ModuleKind;
use crate::module_name::ModuleName;
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
use super::*;
fn setup_resolver_test() -> TestCase {
create_resolver_builder().unwrap().build().unwrap()
}
#[test]
fn first_party_module() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
fn first_party_module() {
let TestCase { db, src, .. } = TestCaseBuilder::new()
.with_src_files(&[("foo.py", "print('Hello, world!')")])
.build();
let foo_module_name = ModuleName::new_static("foo").unwrap();
let foo_path = src.join("foo.py");
db.write_file(&foo_path, "print('Hello, world!')")?;
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
assert_eq!(
@ -418,25 +412,26 @@ mod tests {
assert_eq!(&src, &foo_module.search_path());
assert_eq!(ModuleKind::Module, foo_module.kind());
assert_eq!(&foo_path, foo_module.file().path(&db));
let expected_foo_path = src.join("foo.py");
assert_eq!(&expected_foo_path, foo_module.file().path(&db));
assert_eq!(
Some(foo_module),
path_to_module(&db, &FilePath::System(foo_path))
path_to_module(&db, &FilePath::System(expected_foo_path))
);
Ok(())
}
#[test]
fn stdlib() {
let TestCase {
db,
custom_typeshed,
..
} = setup_resolver_test();
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
versions: "functools: 3.8-",
};
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py38)
.build();
let stdlib_dir =
ModuleResolutionPathBuf::stdlib_from_custom_typeshed_root(&custom_typeshed).unwrap();
let functools_module_name = ModuleName::new_static("functools").unwrap();
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
@ -445,16 +440,15 @@ mod tests {
resolve_module(&db, functools_module_name).as_ref()
);
assert_eq!(stdlib_dir, functools_module.search_path().to_path_buf());
assert_eq!(&stdlib, &functools_module.search_path().to_path_buf());
assert_eq!(ModuleKind::Module, functools_module.kind());
let expected_functools_path =
FilePath::System(custom_typeshed.join("stdlib/functools.pyi"));
let expected_functools_path = stdlib.join("functools.pyi");
assert_eq!(&expected_functools_path, functools_module.file().path(&db));
assert_eq!(
Some(functools_module),
path_to_module(&db, &expected_functools_path)
path_to_module(&db, &FilePath::System(expected_functools_path))
);
}
@ -467,11 +461,29 @@ mod tests {
#[test]
fn stdlib_resolution_respects_versions_file_py38_existing_modules() {
let TestCase {
db,
custom_typeshed,
..
} = setup_resolver_test();
const VERSIONS: &str = "\
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
functools: 3.8- # Top-level single-file module
xml: 3.8-3.8 # Namespace package on py38 only
";
const STDLIB: &[FileSpec] = &[
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("functools.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: STDLIB,
versions: VERSIONS,
};
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py38)
.build();
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
for module_name in existing_modules {
@ -480,8 +492,7 @@ mod tests {
});
let search_path = resolved_module.search_path();
assert_eq!(
&custom_typeshed.join("stdlib"),
&search_path,
&stdlib, &search_path,
"Search path for {module_name} was unexpectedly {search_path:?}"
);
assert!(
@ -493,7 +504,32 @@ mod tests {
#[test]
fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() {
let TestCase { db, .. } = setup_resolver_test();
const VERSIONS: &str = "\
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
importlib: 3.9- # Namespace package on py39+
xml: 3.8-3.8 # Namespace package on 3.8 only
";
const STDLIB: &[FileSpec] = &[
("collections/__init__.pyi", ""),
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("importlib/abc.pyi", ""),
("xml/etree.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: STDLIB,
versions: VERSIONS,
};
let TestCase { db, .. } = TestCaseBuilder::new()
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py38)
.build();
let nonexisting_modules = create_module_names(&[
"collections",
"importlib",
@ -501,6 +537,7 @@ mod tests {
"xml",
"asyncio.tasks",
]);
for module_name in nonexisting_modules {
assert!(
resolve_module(&db, module_name.clone()).is_none(),
@ -511,15 +548,31 @@ mod tests {
#[test]
fn stdlib_resolution_respects_versions_file_py39_existing_modules() {
let TestCase {
db,
custom_typeshed,
..
} = create_resolver_builder()
.unwrap()
const VERSIONS: &str = "\
asyncio: 3.8- # 'Regular' package on py38+
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
collections: 3.9- # 'Regular' package on py39+
functools: 3.8- # Top-level single-file module
importlib: 3.9- # Namespace package on py39+
";
const STDLIB: &[FileSpec] = &[
("asyncio/__init__.pyi", ""),
("asyncio/tasks.pyi", ""),
("collections/__init__.pyi", ""),
("functools.pyi", ""),
("importlib/abc.pyi", ""),
];
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: STDLIB,
versions: VERSIONS,
};
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py39)
.build()
.unwrap();
.build();
let existing_modules = create_module_names(&[
"asyncio",
@ -528,14 +581,14 @@ mod tests {
"collections",
"asyncio.tasks",
]);
for module_name in existing_modules {
let resolved_module = resolve_module(&db, module_name.clone()).unwrap_or_else(|| {
panic!("Expected module {module_name} to exist in the mock stdlib")
});
let search_path = resolved_module.search_path();
assert_eq!(
&custom_typeshed.join("stdlib"),
&search_path,
&stdlib, &search_path,
"Search path for {module_name} was unexpectedly {search_path:?}"
);
assert!(
@ -546,11 +599,22 @@ mod tests {
}
#[test]
fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() {
let TestCase { db, .. } = create_resolver_builder()
.unwrap()
const VERSIONS: &str = "\
importlib: 3.9- # Namespace package on py39+
xml: 3.8-3.8 # Namespace package on 3.8 only
";
const STDLIB: &[FileSpec] = &[("importlib/abc.pyi", ""), ("xml/etree.pyi", "")];
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: STDLIB,
versions: VERSIONS,
};
let TestCase { db, .. } = TestCaseBuilder::new()
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py39)
.build()
.unwrap();
.build();
let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
for module_name in nonexisting_modules {
@ -562,11 +626,19 @@ mod tests {
}
#[test]
fn first_party_precedence_over_stdlib() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
fn first_party_precedence_over_stdlib() {
const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")];
let first_party_functools_path = src.join("functools.py");
db.write_file(&first_party_functools_path, "def update_wrapper(): ...")?;
const TYPESHED: MockedTypeshed = MockedTypeshed {
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
versions: "functools: 3.8-",
};
let TestCase { db, src, .. } = TestCaseBuilder::new()
.with_src_files(SRC)
.with_custom_typeshed(TYPESHED)
.with_target_version(TargetVersion::Py38)
.build();
let functools_module_name = ModuleName::new_static("functools").unwrap();
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
@ -577,49 +649,39 @@ mod tests {
);
assert_eq!(&src, &functools_module.search_path());
assert_eq!(ModuleKind::Module, functools_module.kind());
assert_eq!(
&first_party_functools_path,
functools_module.file().path(&db)
);
assert_eq!(&src.join("functools.py"), functools_module.file().path(&db));
assert_eq!(
Some(functools_module),
path_to_module(&db, &FilePath::System(first_party_functools_path))
path_to_module(&db, &FilePath::System(src.join("functools.py")))
);
Ok(())
}
#[test]
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
let TestCase { db, .. } = create_resolver_builder()
.unwrap()
.with_vendored_stubs_used()
.build()
.unwrap();
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
.with_vendored_typeshed()
.with_target_version(TargetVersion::default())
.build();
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
let pydoc_data_topics = resolve_module(&db, pydoc_data_topics_name).unwrap();
assert_eq!("pydoc_data.topics", pydoc_data_topics.name());
assert_eq!(pydoc_data_topics.search_path(), stdlib);
assert_eq!(
pydoc_data_topics.search_path(),
VendoredPathBuf::from("stdlib")
);
assert_eq!(
&pydoc_data_topics.file().path(db.upcast()),
&VendoredPath::new("stdlib/pydoc_data/topics.pyi")
pydoc_data_topics.file().path(&db),
&stdlib.join("pydoc_data/topics.pyi")
);
}
#[test]
fn resolve_package() -> anyhow::Result<()> {
let TestCase { src, mut db, .. } = setup_resolver_test();
let foo_dir = src.join("foo");
let foo_path = foo_dir.join("__init__.py");
db.write_file(&foo_path, "print('Hello, world!')")?;
fn resolve_package() {
let TestCase { src, db, .. } = TestCaseBuilder::new()
.with_src_files(&[("foo/__init__.py", "print('Hello, world!'")])
.build();
let foo_path = src.join("foo/__init__.py");
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
assert_eq!("foo", foo_module.name());
@ -632,96 +694,84 @@ mod tests {
);
// Resolving by directory doesn't resolve to the init file.
assert_eq!(None, path_to_module(&db, &FilePath::System(foo_dir)));
Ok(())
assert_eq!(
None,
path_to_module(&db, &FilePath::System(src.join("foo")))
);
}
#[test]
fn package_priority_over_module() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
fn package_priority_over_module() {
const SRC: &[FileSpec] = &[
("foo/__init__.py", "print('Hello, world!')"),
("foo.py", "print('Hello, world!')"),
];
let foo_dir = src.join("foo");
let foo_init = foo_dir.join("__init__.py");
db.write_file(&foo_init, "print('Hello, world!')")?;
let foo_py = src.join("foo.py");
db.write_file(&foo_py, "print('Hello, world!')")?;
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
let foo_init_path = src.join("foo/__init__.py");
assert_eq!(&src, &foo_module.search_path());
assert_eq!(&foo_init, foo_module.file().path(&db));
assert_eq!(&foo_init_path, foo_module.file().path(&db));
assert_eq!(ModuleKind::Package, foo_module.kind());
assert_eq!(
Some(foo_module),
path_to_module(&db, &FilePath::System(foo_init))
path_to_module(&db, &FilePath::System(foo_init_path))
);
assert_eq!(
None,
path_to_module(&db, &FilePath::System(src.join("foo.py")))
);
assert_eq!(None, path_to_module(&db, &FilePath::System(foo_py)));
Ok(())
}
#[test]
fn typing_stub_over_module() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
fn typing_stub_over_module() {
const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
let foo_stub = src.join("foo.pyi");
let foo_py = src.join("foo.py");
db.write_files([(&foo_stub, "x: int"), (&foo_py, "print('Hello, world!')")])?;
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
let foo = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
let foo_stub = src.join("foo.pyi");
assert_eq!(&src, &foo.search_path());
assert_eq!(&foo_stub, foo.file().path(&db));
assert_eq!(Some(foo), path_to_module(&db, &FilePath::System(foo_stub)));
assert_eq!(None, path_to_module(&db, &FilePath::System(foo_py)));
Ok(())
assert_eq!(
None,
path_to_module(&db, &FilePath::System(src.join("foo.py")))
);
}
#[test]
fn sub_packages() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
fn sub_packages() {
const SRC: &[FileSpec] = &[
("foo/__init__.py", ""),
("foo/bar/__init__.py", ""),
("foo/bar/baz.py", "print('Hello, world!)'"),
];
let foo = src.join("foo");
let bar = foo.join("bar");
let baz = bar.join("baz.py");
db.write_files([
(&foo.join("__init__.py"), ""),
(&bar.join("__init__.py"), ""),
(&baz, "print('Hello, world!')"),
])?;
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
let baz_module =
resolve_module(&db, ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
let baz_path = src.join("foo/bar/baz.py");
assert_eq!(&src, &baz_module.search_path());
assert_eq!(&baz, baz_module.file().path(&db));
assert_eq!(&baz_path, baz_module.file().path(&db));
assert_eq!(
Some(baz_module),
path_to_module(&db, &FilePath::System(baz))
path_to_module(&db, &FilePath::System(baz_path))
);
Ok(())
}
#[test]
fn namespace_package() -> anyhow::Result<()> {
let TestCase {
mut db,
src,
site_packages,
..
} = setup_resolver_test();
fn namespace_package() {
// From [PEP420](https://peps.python.org/pep-0420/#nested-namespace-packages).
// But uses `src` for `project1` and `site_packages2` for `project2`.
// But uses `src` for `project1` and `site-packages` for `project2`.
// ```
// src
// parent
@ -732,47 +782,33 @@ mod tests {
// child
// two.py
// ```
let parent1 = src.join("parent");
let child1 = parent1.join("child");
let one = child1.join("one.py");
let parent2 = site_packages.join("parent");
let child2 = parent2.join("child");
let two = child2.join("two.py");
db.write_files([
(&one, "print('Hello, world!')"),
(&two, "print('Hello, world!')"),
])?;
let one_module =
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap()).unwrap();
assert_eq!(
Some(one_module),
path_to_module(&db, &FilePath::System(one))
);
let two_module =
resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap()).unwrap();
assert_eq!(
Some(two_module),
path_to_module(&db, &FilePath::System(two))
);
Ok(())
}
#[test]
fn regular_package_in_namespace_package() -> anyhow::Result<()> {
let TestCase {
mut db,
db,
src,
site_packages,
..
} = setup_resolver_test();
} = TestCaseBuilder::new()
.with_src_files(&[("parent/child/one.py", "print('Hello, world!')")])
.with_site_packages_files(&[("parent/child/two.py", "print('Hello, world!')")])
.build();
let one_module_name = ModuleName::new_static("parent.child.one").unwrap();
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
assert_eq!(
resolve_module(&db, one_module_name),
path_to_module(&db, &one_module_path)
);
let two_module_name = ModuleName::new_static("parent.child.two").unwrap();
let two_module_path = FilePath::System(site_packages.join("parent/child/two.py"));
assert_eq!(
resolve_module(&db, two_module_name),
path_to_module(&db, &two_module_path)
);
}
#[test]
fn regular_package_in_namespace_package() {
// Adopted test case from the [PEP420 examples](https://peps.python.org/pep-0420/#nested-namespace-packages).
// The `src/parent/child` package is a regular package. Therefore, `site_packages/parent/child/two.py` should not be resolved.
// ```
@ -785,90 +821,69 @@ mod tests {
// child
// two.py
// ```
const SRC: &[FileSpec] = &[
("parent/child/__init__.py", "print('Hello, world!')"),
("parent/child/one.py", "print('Hello, world!')"),
];
let parent1 = src.join("parent");
let child1 = parent1.join("child");
let one = child1.join("one.py");
const SITE_PACKAGES: &[FileSpec] = &[("parent/child/two.py", "print('Hello, world!')")];
let parent2 = site_packages.join("parent");
let child2 = parent2.join("child");
let two = child2.join("two.py");
let TestCase { db, src, .. } = TestCaseBuilder::new()
.with_src_files(SRC)
.with_site_packages_files(SITE_PACKAGES)
.build();
db.write_files([
(&child1.join("__init__.py"), "print('Hello, world!')"),
(&one, "print('Hello, world!')"),
(&two, "print('Hello, world!')"),
])?;
let one_module =
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap()).unwrap();
assert_eq!(
Some(one_module),
path_to_module(&db, &FilePath::System(one))
);
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
let one_module_name =
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap());
assert_eq!(one_module_name, path_to_module(&db, &one_module_path));
assert_eq!(
None,
resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap())
);
Ok(())
}
#[test]
fn module_search_path_priority() -> anyhow::Result<()> {
fn module_search_path_priority() {
let TestCase {
mut db,
db,
src,
site_packages,
..
} = setup_resolver_test();
let foo_src = src.join("foo.py");
let foo_site_packages = site_packages.join("foo.py");
db.write_files([(&foo_src, ""), (&foo_site_packages, "")])?;
} = TestCaseBuilder::new()
.with_src_files(&[("foo.py", "")])
.with_site_packages_files(&[("foo.py", "")])
.build();
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
let foo_src_path = src.join("foo.py");
assert_eq!(&src, &foo_module.search_path());
assert_eq!(&foo_src, foo_module.file().path(&db));
assert_eq!(&foo_src_path, foo_module.file().path(&db));
assert_eq!(
Some(foo_module),
path_to_module(&db, &FilePath::System(foo_src))
);
assert_eq!(
None,
path_to_module(&db, &FilePath::System(foo_site_packages))
path_to_module(&db, &FilePath::System(foo_src_path))
);
Ok(())
assert_eq!(
None,
path_to_module(&db, &FilePath::System(site_packages.join("foo.py")))
);
}
#[test]
#[cfg(target_family = "unix")]
fn symlink() -> anyhow::Result<()> {
use ruff_db::system::{OsSystem, SystemPath};
fn make_relative(path: &SystemPath) -> &SystemPath {
path.strip_prefix("/").unwrap_or(path)
}
let TestCase {
mut db,
src,
site_packages,
custom_typeshed,
} = setup_resolver_test();
let mut db = TestDb::new();
let temp_dir = tempfile::tempdir()?;
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
db.use_os_system(OsSystem::new(root));
let src = root.join(make_relative(&src));
let site_packages = root.join(make_relative(&site_packages));
let custom_typeshed = root.join(make_relative(&custom_typeshed));
let src = root.join("src");
let site_packages = root.join("site-packages");
let custom_typeshed = root.join("typeshed");
let foo = src.join("foo.py");
let bar = src.join("bar.py");
@ -919,23 +934,22 @@ mod tests {
}
#[test]
fn deleting_an_unrelated_file_doesnt_change_module_resolution() -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
let foo_path = src.join("foo.py");
let bar_path = src.join("bar.py");
db.write_files([(&foo_path, "x = 1"), (&bar_path, "y = 2")])?;
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
.with_target_version(TargetVersion::Py38)
.build();
let foo_module_name = ModuleName::new_static("foo").unwrap();
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
let bar_path = src.join("bar.py");
let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist");
db.clear_salsa_events();
// Delete `bar.py`
db.memory_file_system().remove_file(&bar_path)?;
db.memory_file_system().remove_file(&bar_path).unwrap();
bar.touch(&mut db);
// Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant
@ -949,14 +963,12 @@ mod tests {
.any(|event| { matches!(event.kind, salsa::EventKind::WillExecute { .. }) }));
assert_eq!(Some(foo_module), foo_module2);
Ok(())
}
#[test]
fn adding_a_file_on_which_the_module_resolution_depends_on_invalidates_the_query(
) -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
let TestCase { mut db, src, .. } = TestCaseBuilder::new().build();
let foo_path = src.join("foo.py");
let foo_module_name = ModuleName::new_static("foo").unwrap();
@ -976,14 +988,13 @@ mod tests {
#[test]
fn removing_a_file_that_the_module_resolution_depends_on_invalidates_the_query(
) -> anyhow::Result<()> {
let TestCase { mut db, src, .. } = setup_resolver_test();
let foo_path = src.join("foo.py");
let foo_init_path = src.join("foo/__init__.py");
const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")];
db.write_files([(&foo_path, "x = 1"), (&foo_init_path, "x = 2")])?;
let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
let foo_module_name = ModuleName::new_static("foo").unwrap();
let foo_module = resolve_module(&db, foo_module_name.clone()).expect("foo module to exist");
let foo_init_path = src.join("foo/__init__.py");
assert_eq!(&foo_init_path, foo_module.file().path(&db));
@ -994,7 +1005,7 @@ mod tests {
File::touch_path(&mut db, &FilePath::System(foo_init_path));
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
assert_eq!(&foo_path, foo_module.file().path(&db));
assert_eq!(&src.join("foo.py"), foo_module.file().path(&db));
Ok(())
}

View File

@ -0,0 +1,290 @@
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
use ruff_db::vendored::VendoredPathBuf;
use crate::db::tests::TestDb;
use crate::resolver::{set_module_resolution_settings, RawModuleResolutionSettings};
use crate::supported_py_version::TargetVersion;
/// A test case for the module resolver.
///
/// You generally shouldn't construct instances of this struct directly;
/// instead, use the [`TestCaseBuilder`].
pub(crate) struct TestCase<T> {
pub(crate) db: TestDb,
pub(crate) src: SystemPathBuf,
pub(crate) stdlib: T,
pub(crate) site_packages: SystemPathBuf,
pub(crate) target_version: TargetVersion,
}
/// A `(file_name, file_contents)` tuple
pub(crate) type FileSpec = (&'static str, &'static str);
/// Specification for a typeshed mock to be created as part of a test
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct MockedTypeshed {
/// The stdlib files to be created in the typeshed mock
pub(crate) stdlib_files: &'static [FileSpec],
/// The contents of the `stdlib/VERSIONS` file
/// to be created in the typeshed mock
pub(crate) versions: &'static str,
}
#[derive(Debug)]
pub(crate) struct VendoredTypeshed;
#[derive(Debug)]
pub(crate) struct UnspecifiedTypeshed;
/// A builder for a module-resolver test case.
///
/// The builder takes care of creating a [`TestDb`]
/// instance, applying the module resolver settings,
/// and creating mock directories for the stdlib, `site-packages`,
/// first-party code, etc.
///
/// For simple tests that do not involve typeshed,
/// test cases can be created as follows:
///
/// ```rs
/// let test_case = TestCaseBuilder::new()
/// .with_src_files(...)
/// .build();
///
/// let test_case2 = TestCaseBuilder::new()
/// .with_site_packages_files(...)
/// .build();
/// ```
///
/// Any tests can specify the target Python version that should be used
/// in the module resolver settings:
///
/// ```rs
/// let test_case = TestCaseBuilder::new()
/// .with_src_files(...)
/// .with_target_version(...)
/// .build();
/// ```
///
/// For tests checking that standard-library module resolution is working
/// correctly, you should usually create a [`MockedTypeshed`] instance
/// and pass it to the [`TestCaseBuilder::with_custom_typeshed`] method.
/// If you need to check something that involves the vendored typeshed stubs
/// we include as part of the binary, you can instead use the
/// [`TestCaseBuilder::with_vendored_typeshed`] method.
/// For either of these, you should almost always try to be explicit
/// about the Python version you want to be specified in the module-resolver
/// settings for the test:
///
/// ```rs
/// const TYPESHED = MockedTypeshed { ... };
///
/// let test_case = resolver_test_case()
/// .with_custom_typeshed(TYPESHED)
/// .with_target_version(...)
/// .build();
///
/// let test_case2 = resolver_test_case()
/// .with_vendored_typeshed()
/// .with_target_version(...)
/// .build();
/// ```
///
/// If you have not called one of those options, the `stdlib` field
/// on the [`TestCase`] instance created from `.build()` will be set
/// to `()`.
pub(crate) struct TestCaseBuilder<T> {
typeshed_option: T,
target_version: TargetVersion,
first_party_files: Vec<FileSpec>,
site_packages_files: Vec<FileSpec>,
}
impl<T> TestCaseBuilder<T> {
/// Specify files to be created in the `src` mock directory
pub(crate) fn with_src_files(mut self, files: &[FileSpec]) -> Self {
self.first_party_files.extend(files.iter().copied());
self
}
/// Specify files to be created in the `site-packages` mock directory
pub(crate) fn with_site_packages_files(mut self, files: &[FileSpec]) -> Self {
self.site_packages_files.extend(files.iter().copied());
self
}
/// Specify the target Python version the module resolver should assume
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
self.target_version = target_version;
self
}
fn write_mock_directory(
db: &mut TestDb,
location: impl AsRef<SystemPath>,
files: impl IntoIterator<Item = FileSpec>,
) -> SystemPathBuf {
let root = location.as_ref().to_path_buf();
db.write_files(
files
.into_iter()
.map(|(relative_path, contents)| (root.join(relative_path), contents)),
)
.unwrap();
root
}
}
impl TestCaseBuilder<UnspecifiedTypeshed> {
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
Self {
typeshed_option: UnspecifiedTypeshed,
target_version: TargetVersion::default(),
first_party_files: vec![],
site_packages_files: vec![],
}
}
/// Use the vendored stdlib stubs included in the Ruff binary for this test case
pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder<VendoredTypeshed> {
let TestCaseBuilder {
typeshed_option: _,
target_version,
first_party_files,
site_packages_files,
} = self;
TestCaseBuilder {
typeshed_option: VendoredTypeshed,
target_version,
first_party_files,
site_packages_files,
}
}
/// Use a mock typeshed directory for this test case
pub(crate) fn with_custom_typeshed(
self,
typeshed: MockedTypeshed,
) -> TestCaseBuilder<MockedTypeshed> {
let TestCaseBuilder {
typeshed_option: _,
target_version,
first_party_files,
site_packages_files,
} = self;
TestCaseBuilder {
typeshed_option: typeshed,
target_version,
first_party_files,
site_packages_files,
}
}
pub(crate) fn build(self) -> TestCase<()> {
let TestCase {
db,
src,
stdlib: _,
site_packages,
target_version,
} = self.with_custom_typeshed(MockedTypeshed::default()).build();
TestCase {
db,
src,
stdlib: (),
site_packages,
target_version,
}
}
}
impl TestCaseBuilder<MockedTypeshed> {
pub(crate) fn build(self) -> TestCase<SystemPathBuf> {
let TestCaseBuilder {
typeshed_option,
target_version,
first_party_files,
site_packages_files,
} = self;
let mut db = TestDb::new();
let site_packages =
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option);
set_module_resolution_settings(
&mut db,
RawModuleResolutionSettings {
target_version,
extra_paths: vec![],
workspace_root: src.clone(),
custom_typeshed: Some(typeshed.clone()),
site_packages: Some(site_packages.clone()),
},
);
TestCase {
db,
src,
stdlib: typeshed.join("stdlib"),
site_packages,
target_version,
}
}
fn build_typeshed_mock(db: &mut TestDb, typeshed_to_build: &MockedTypeshed) -> SystemPathBuf {
let typeshed = SystemPathBuf::from("/typeshed");
let MockedTypeshed {
stdlib_files,
versions,
} = typeshed_to_build;
Self::write_mock_directory(
db,
typeshed.join("stdlib"),
stdlib_files
.iter()
.copied()
.chain(std::iter::once(("VERSIONS", *versions))),
);
typeshed
}
}
impl TestCaseBuilder<VendoredTypeshed> {
pub(crate) fn build(self) -> TestCase<VendoredPathBuf> {
let TestCaseBuilder {
typeshed_option: VendoredTypeshed,
target_version,
first_party_files,
site_packages_files,
} = self;
let mut db = TestDb::new();
let site_packages =
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
set_module_resolution_settings(
&mut db,
RawModuleResolutionSettings {
target_version,
extra_paths: vec![],
workspace_root: src.clone(),
custom_typeshed: None,
site_packages: Some(site_packages.clone()),
},
);
TestCase {
db,
src,
stdlib: VendoredPathBuf::from("stdlib"),
site_packages,
target_version,
}
}
}

View File

@ -131,7 +131,6 @@ pub enum TypeshedVersionsParseErrorKind {
version: String,
err: std::num::ParseIntError,
},
EmptyVersionsFile,
}
impl fmt::Display for TypeshedVersionsParseErrorKind {
@ -160,7 +159,6 @@ impl fmt::Display for TypeshedVersionsParseErrorKind {
f,
"Failed to convert '{version}' to a pair of integers due to {err}",
),
Self::EmptyVersionsFile => f.write_str("Versions file was empty!"),
}
}
}
@ -307,15 +305,8 @@ impl FromStr for TypeshedVersions {
};
}
if map.is_empty() {
Err(TypeshedVersionsParseError {
line_number: None,
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile,
})
} else {
Ok(Self(map))
}
}
}
impl fmt::Display for TypeshedVersions {
@ -685,31 +676,6 @@ foo: 3.8- # trailing comment
);
}
#[test]
fn invalid_empty_versions_file() {
assert_eq!(
TypeshedVersions::from_str(""),
Err(TypeshedVersionsParseError {
line_number: None,
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
})
);
assert_eq!(
TypeshedVersions::from_str(" "),
Err(TypeshedVersionsParseError {
line_number: None,
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
})
);
assert_eq!(
TypeshedVersions::from_str(" \n \n \n "),
Err(TypeshedVersionsParseError {
line_number: None,
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
})
);
}
#[test]
fn invalid_huge_versions_file() {
let offset = 100;