//! ref: composer/tests/Composer/Test/Downloader/DownloadManagerTest.php use crate::io_stub::IOStub; use indexmap::IndexMap; use shirabe::downloader::DownloaderInterface; use shirabe::downloader::download_manager::DownloadManager; use shirabe::io::IOInterface; use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; use shirabe_php_shim::{PhpMixed, RuntimeException}; use shirabe_semver::VersionParser; use std::cell::RefCell; use std::rc::Rc; // PHP mocks `Composer\Downloader\DownloaderInterface` with getMockBuilder. mockall::mock! { #[derive(Debug)] pub Downloader {} #[async_trait::async_trait(?Send)] impl DownloaderInterface for Downloader { fn get_installation_source(&self) -> String; async fn download( &mut self, package: PackageInterfaceHandle, path: &str, prev_package: Option, output: bool, ) -> anyhow::Result>; async fn prepare( &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, prev_package: Option, ) -> anyhow::Result>; async fn install( &mut self, package: PackageInterfaceHandle, path: &str, output: bool, ) -> anyhow::Result>; async fn update( &mut self, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, path: &str, ) -> anyhow::Result>; async fn remove( &mut self, package: PackageInterfaceHandle, path: &str, output: bool, ) -> anyhow::Result>; async fn cleanup( &mut self, r#type: &str, package: PackageInterfaceHandle, path: &str, prev_package: Option, ) -> anyhow::Result>; } } fn run(future: F) -> F::Output { tokio::runtime::Builder::new_current_thread() .build() .unwrap() .block_on(future) } /// ref: DownloadManagerTest::createPackageMock /// /// PHPUnit returns a `PackageInterface` mock; a real CompletePackage with the /// relevant fields left at their defaults is an equivalent stand-in for the /// installation-source/type dispatch logic exercised by the ported cases. fn create_package_mock() -> PackageInterfaceHandle { make_package("dummy/pkg", false) } /// Real package stand-in whose dev flag is derived from the version stability, so /// `isDev()` can be controlled (`dev-master` => dev, `1.0.0` => stable). fn make_package(name: &str, is_dev: bool) -> PackageInterfaceHandle { let (version, pretty) = if is_dev { ("dev-master".to_string(), "dev-master".to_string()) } else { ( VersionParser.normalize("1.0.0", None).unwrap(), "1.0.0".to_string(), ) }; CompletePackageHandle::new(name.to_string(), version, pretty).into() } /// ref: DownloadManagerTest::createDownloaderMock fn create_downloader_mock() -> Rc> { as_dyn(MockDownloader::new()) } /// A `createDownloaderMock()` whose `getInstallationSource()` reports the given /// source, matching the type under which it is registered with the manager so the /// real `getDownloaderForPackage` dispatch resolves to it. fn downloader_mock(installation_source: &str) -> MockDownloader { let mut downloader = MockDownloader::new(); let source = installation_source.to_string(); downloader .expect_get_installation_source() .returning(move || source.clone()); downloader } fn as_dyn(downloader: MockDownloader) -> Rc> { Rc::new(RefCell::new(downloader)) as Rc> } fn create_manager() -> DownloadManager { let io = Rc::new(RefCell::new(IOStub::new())) as Rc>; DownloadManager::new(io, false, None) } #[test] fn test_set_get_downloader() { let downloader = create_downloader_mock(); let mut manager = create_manager(); manager.set_downloader("test", downloader.clone()); assert!(Rc::ptr_eq( &downloader, &manager.get_downloader("test").unwrap() )); let result = manager.get_downloader("unregistered"); assert!(result.is_err()); } #[test] fn test_get_downloader_for_incorrectly_installed_package() { // getInstallationSource() => null (the default for a fresh package). let package = create_package_mock(); let manager = create_manager(); let result = manager.get_downloader_for_package(package); assert!(result.is_err()); } #[test] fn test_get_downloader_for_metapackage() { let package = create_package_mock(); package.__set_type("metapackage".to_string()); let manager = create_manager(); assert!( manager .get_downloader_for_package(package) .unwrap() .is_none() ); } #[test] fn test_get_downloader_for_correctly_installed_dist_package() { let package = create_package_mock(); package.set_installation_source(Some("dist".to_string())); package.set_dist_type(Some("pear".to_string())); let downloader = as_dyn(downloader_mock("dist")); let mut manager = create_manager(); manager.set_downloader("pear", downloader.clone()); let result = manager .get_downloader_for_package(package) .unwrap() .unwrap(); assert!(Rc::ptr_eq(&downloader, &result)); } // The LogicException message uses get_class($downloader); the equivalent // `shirabe_php_shim::get_class_obj` is still a `todo!()`, so building the error // panics before `getDownloaderForPackage` can return it. #[ignore = "requires shirabe_php_shim::get_class_obj (PHP get_class), still todo!()"] #[test] fn test_get_downloader_for_incorrectly_installed_dist_package() { let package = create_package_mock(); package.set_installation_source(Some("dist".to_string())); package.set_dist_type(Some("git".to_string())); let downloader = as_dyn(downloader_mock("source")); let mut manager = create_manager(); manager.set_downloader("git", downloader); // LogicException: the resolved downloader is a source downloader. assert!(manager.get_downloader_for_package(package).is_err()); } #[test] fn test_get_downloader_for_correctly_installed_source_package() { let package = create_package_mock(); package.set_installation_source(Some("source".to_string())); package.__set_source_type(Some("git".to_string())); let downloader = as_dyn(downloader_mock("source")); let mut manager = create_manager(); manager.set_downloader("git", downloader.clone()); let result = manager .get_downloader_for_package(package) .unwrap() .unwrap(); assert!(Rc::ptr_eq(&downloader, &result)); } // See test_get_downloader_for_incorrectly_installed_dist_package: the LogicException // path depends on the still-unimplemented get_class_obj shim. #[ignore = "requires shirabe_php_shim::get_class_obj (PHP get_class), still todo!()"] #[test] fn test_get_downloader_for_incorrectly_installed_source_package() { let package = create_package_mock(); package.set_installation_source(Some("source".to_string())); package.__set_source_type(Some("pear".to_string())); let downloader = as_dyn(downloader_mock("dist")); let mut manager = create_manager(); manager.set_downloader("pear", downloader); assert!(manager.get_downloader_for_package(package).is_err()); } #[test] fn test_full_package_download() { let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } #[test] fn test_full_package_download_failover() { let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); // dist downloader fails, source downloader (tried next) succeeds. let mut downloader_fail = downloader_mock("dist"); downloader_fail .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| { Err(RuntimeException { message: "Foo".to_string(), code: 0, } .into()) }); let mut downloader_success = downloader_mock("source"); downloader_success .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader_fail)); manager.set_downloader("git", as_dyn(downloader_success)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); // PHP asserts setInstallationSource was called with 'dist' then 'source' // (withConsecutive/exactly(2)). The real package retains only the final // installation_source, so the dist-before-failover step is instead evidenced by // both downloaders' expect_download().times(1): the 'dist' downloader is reached // and fails, then the 'source' downloader is reached and succeeds. A faithful // ordering check would need a src-side set-history hook on Package. assert_eq!(package.get_installation_source().as_deref(), Some("source")); } #[test] fn test_bad_package_download() { let package = create_package_mock(); // getSourceType() => null, getDistType() => null. let manager = create_manager(); assert!(run(manager.download(package, "target_dir", None)).is_err()); } #[test] fn test_dist_only_package_download() { let package = create_package_mock(); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } #[test] fn test_source_only_package_download() { let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } #[test] fn test_metapackage_package_download() { // There is no downloader for metapackages, so getDownloaderForPackage yields none. let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); package.__set_type("metapackage".to_string()); let manager = create_manager(); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } #[test] fn test_full_package_download_with_source_preferred() { let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); manager.set_prefer_source(true); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } #[test] fn test_dist_only_package_download_with_source_preferred() { let package = create_package_mock(); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); manager.set_prefer_source(true); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } #[test] fn test_source_only_package_download_with_source_preferred() { let package = create_package_mock(); package.__set_source_type(Some("git".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); manager.set_prefer_source(true); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } #[test] fn test_bad_package_download_with_source_preferred() { let package = create_package_mock(); // getSourceType() => null, getDistType() => null. let mut manager = create_manager(); manager.set_prefer_source(true); assert!(run(manager.download(package, "target_dir", None)).is_err()); } #[test] fn test_update_dist_with_equal_types() { let initial = create_package_mock(); initial.set_installation_source(Some("dist".to_string())); initial.set_dist_type(Some("zip".to_string())); let target = create_package_mock(); target.set_installation_source(Some("dist".to_string())); target.set_dist_type(Some("zip".to_string())); let mut zip_downloader = downloader_mock("dist"); zip_downloader .expect_update() .times(1) .withf(|_initial, _target, path| path == "vendor/bundles/FOS/UserBundle") .returning(|_, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("zip", as_dyn(zip_downloader)); run(manager.update(initial, target, "vendor/bundles/FOS/UserBundle")).unwrap(); } #[test] fn test_update_dist_with_not_equal_types() { let initial = create_package_mock(); initial.set_installation_source(Some("dist".to_string())); initial.set_dist_type(Some("xz".to_string())); let target = create_package_mock(); target.set_installation_source(Some("dist".to_string())); target.set_dist_type(Some("zip".to_string())); let mut xz_downloader = downloader_mock("dist"); xz_downloader .expect_remove() .times(1) .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle") .returning(|_, _, _| Ok(None)); let mut zip_downloader = downloader_mock("dist"); zip_downloader .expect_install() .times(1) .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle") .returning(|_, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("xz", as_dyn(xz_downloader)); manager.set_downloader("zip", as_dyn(zip_downloader)); run(manager.update(initial, target, "vendor/bundles/FOS/UserBundle")).unwrap(); } #[test] fn test_get_available_sources_update_sticks_to_same_source() { // ref: updatesProvider. Columns: prevPkgSource, prevPkgIsDev, targetAvailable, // targetIsDev, expected. struct Case { prev_pkg_source: Option<&'static str>, prev_pkg_is_dev: bool, target_available: &'static [&'static str], target_is_dev: bool, expected: &'static [&'static str], } let cases = [ // updates keep previous source as preference Case { prev_pkg_source: Some("source"), prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: false, expected: &["source", "dist"], }, Case { prev_pkg_source: Some("dist"), prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: false, expected: &["dist", "source"], }, // updates do not keep previous source if target package does not have it Case { prev_pkg_source: Some("source"), prev_pkg_is_dev: false, target_available: &["dist"], target_is_dev: false, expected: &["dist"], }, Case { prev_pkg_source: Some("dist"), prev_pkg_is_dev: false, target_available: &["source"], target_is_dev: false, expected: &["source"], }, // updates do not keep previous source if target is dev and prev wasn't dev and installed from dist Case { prev_pkg_source: Some("source"), prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: true, expected: &["source", "dist"], }, Case { prev_pkg_source: Some("dist"), prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: true, expected: &["source", "dist"], }, // install picks the right default Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: true, expected: &["source", "dist"], }, Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["dist"], target_is_dev: true, expected: &["dist"], }, Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["source"], target_is_dev: true, expected: &["source"], }, Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["source", "dist"], target_is_dev: false, expected: &["dist", "source"], }, Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["dist"], target_is_dev: false, expected: &["dist"], }, Case { prev_pkg_source: None, prev_pkg_is_dev: false, target_available: &["source"], target_is_dev: false, expected: &["source"], }, ]; let manager = create_manager(); for case in cases { let initial = case.prev_pkg_source.map(|source| { let package = make_package("dummy/pkg", case.prev_pkg_is_dev); package.set_installation_source(Some(source.to_string())); package }); let target = make_package("dummy/pkg", case.target_is_dev); if case.target_available.contains(&"source") { target.__set_source_type(Some("git".to_string())); } if case.target_available.contains(&"dist") { target.set_dist_type(Some("zip".to_string())); } let result = manager.__get_available_sources(target, initial).unwrap(); let expected: Vec = case.expected.iter().map(|s| s.to_string()).collect(); assert_eq!(result, expected); } } #[test] fn test_update_metapackage() { // There is no downloader for metapackages. let initial = create_package_mock(); initial.__set_type("metapackage".to_string()); let target = create_package_mock(); target.__set_type("metapackage".to_string()); let manager = create_manager(); assert!( run(manager.update(initial, target, "vendor/pkg")) .unwrap() .is_none() ); } #[test] fn test_remove() { let package = create_package_mock(); package.set_installation_source(Some("dist".to_string())); package.set_dist_type(Some("pear".to_string())); let mut pear_downloader = downloader_mock("dist"); pear_downloader .expect_remove() .times(1) .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle") .returning(|_, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(pear_downloader)); run(manager.remove(package, "vendor/bundles/FOS/UserBundle")).unwrap(); } #[test] fn test_metapackage_remove() { // There is no downloader for metapackages. let package = create_package_mock(); package.__set_type("metapackage".to_string()); let manager = create_manager(); assert!( run(manager.remove(package, "vendor/bundles/FOS/UserBundle")) .unwrap() .is_none() ); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_preference_dev() { let package = make_package("dummy/pkg", true); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_preference_no_dev() { let package = make_package("dummy/pkg", false); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_match_dev() { let package = make_package("bar/package", true); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); manager.set_preferences(IndexMap::from([( "foo/*".to_string(), "source".to_string(), )])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_match_no_dev() { let package = make_package("bar/package", false); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); manager.set_preferences(IndexMap::from([( "foo/*".to_string(), "source".to_string(), )])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_auto_dev() { let package = make_package("foo/package", true); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); manager.set_preferences(IndexMap::from([("foo/*".to_string(), "auto".to_string())])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_auto_no_dev() { let package = make_package("foo/package", false); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); manager.set_preferences(IndexMap::from([("foo/*".to_string(), "auto".to_string())])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_source() { let package = make_package("foo/package", false); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("source"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("git", as_dyn(downloader)); manager.set_preferences(IndexMap::from([( "foo/*".to_string(), "source".to_string(), )])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("source")); } /// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_dist() { let package = make_package("foo/package", false); package.__set_source_type(Some("git".to_string())); package.set_dist_type(Some("pear".to_string())); let mut downloader = downloader_mock("dist"); downloader .expect_download() .times(1) .withf(|_pkg, path, _prev, _output| path == "target_dir") .returning(|_, _, _, _| Ok(None)); let mut manager = create_manager(); manager.set_downloader("pear", as_dyn(downloader)); manager.set_preferences(IndexMap::from([("foo/*".to_string(), "dist".to_string())])); run(manager.download(package.clone(), "target_dir", None)).unwrap(); assert_eq!(package.get_installation_source().as_deref(), Some("dist")); }