From e8b5341c97ca1399f39ea7d81d56152f7d782216 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Wed, 10 Jul 2024 11:40:21 +0100 Subject: [PATCH] [red-knot] Rework module resolver tests (#12260) --- crates/red_knot_module_resolver/src/db.rs | 133 +---- crates/red_knot_module_resolver/src/lib.rs | 3 + crates/red_knot_module_resolver/src/path.rs | 178 ++++--- .../red_knot_module_resolver/src/resolver.rs | 473 +++++++++--------- .../red_knot_module_resolver/src/testing.rs | 290 +++++++++++ .../src/typeshed/versions.rs | 36 +- 6 files changed, 642 insertions(+), 471 deletions(-) create mode 100644 crates/red_knot_module_resolver/src/testing.rs diff --git a/crates/red_knot_module_resolver/src/db.rs b/crates/red_knot_module_resolver/src/db.rs index 05771856f5..82da0e6e94 100644 --- a/crates/red_knot_module_resolver/src/db.rs +++ b/crates/red_knot_module_resolver/src/db.rs @@ -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, - 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 { - 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 { - 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, - }) - } } diff --git a/crates/red_knot_module_resolver/src/lib.rs b/crates/red_knot_module_resolver/src/lib.rs index cc85b7160a..8f63cbfb68 100644 --- a/crates/red_knot_module_resolver/src/lib.rs +++ b/crates/red_knot_module_resolver/src/lib.rs @@ -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; diff --git a/crates/red_knot_module_resolver/src/path.rs b/crates/red_knot_module_resolver/src/path.rs index e0958dad3f..e529d32bc5 100644 --- a/crates/red_knot_module_resolver/src/path.rs +++ b/crates/red_knot_module_resolver/src/path.rs @@ -254,6 +254,18 @@ impl fmt::Debug for ModuleResolutionPathBuf { } } +impl PartialEq for ModuleResolutionPathBuf { + fn eq(&self, other: &SystemPathBuf) -> bool { + ModuleResolutionPathRef::from(self) == **other + } +} + +impl PartialEq 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> 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); diff --git a/crates/red_knot_module_resolver/src/resolver.rs b/crates/red_knot_module_resolver/src/resolver.rs index 047e51c3cf..bfdec08d88 100644 --- a/crates/red_knot_module_resolver/src/resolver.rs +++ b/crates/red_knot_module_resolver/src/resolver.rs @@ -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(()) } diff --git a/crates/red_knot_module_resolver/src/testing.rs b/crates/red_knot_module_resolver/src/testing.rs new file mode 100644 index 0000000000..b927ae7a1e --- /dev/null +++ b/crates/red_knot_module_resolver/src/testing.rs @@ -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 { + 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 { + typeshed_option: T, + target_version: TargetVersion, + first_party_files: Vec, + site_packages_files: Vec, +} + +impl TestCaseBuilder { + /// 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, + files: impl IntoIterator, + ) -> 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 { + pub(crate) fn new() -> TestCaseBuilder { + 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 { + 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 { + 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 { + pub(crate) fn build(self) -> TestCase { + 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 { + pub(crate) fn build(self) -> TestCase { + 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, + } + } +} diff --git a/crates/red_knot_module_resolver/src/typeshed/versions.rs b/crates/red_knot_module_resolver/src/typeshed/versions.rs index c4d2a9189f..3b5debd38f 100644 --- a/crates/red_knot_module_resolver/src/typeshed/versions.rs +++ b/crates/red_knot_module_resolver/src/typeshed/versions.rs @@ -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,14 +305,7 @@ impl FromStr for TypeshedVersions { }; } - if map.is_empty() { - Err(TypeshedVersionsParseError { - line_number: None, - reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile, - }) - } else { - Ok(Self(map)) - } + Ok(Self(map)) } } @@ -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;