diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:21:00 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:26:28 +0900 |
| commit | e98823e599eb375b30037cc714710e3309d927d1 (patch) | |
| tree | e1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/downloader | |
| parent | 20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff) | |
| download | php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.gz php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.zst php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.zip | |
test: port Composer tests unblocked by mockall, add seams
Port 11 categories of previously-ignored Composer tests now reachable with
the mockall crate: DownloadManager, VCS/Perforce/File downloaders,
VersionSelector, PlatformRepository, Auditor, installer/FilesystemRepository,
RootPackageLoader, util auth/http, commands, and Cache.
Extract test seams additively on concrete structs as *Interface traits
(Runtime, HhvmDetector, VersionGuesser, RepositorySet, Perforce,
BinaryInstaller) plus mock-field seams (Cache, Filesystem); consumers take
trait objects. Mocks are defined locally in the test crates via
mockall::mock!, since automock-generated mocks are cfg(test)-gated and
invisible across the integration-test boundary.
dataProviders are ported in full; tests blocked by unported shims stay
#[ignore] with documented reasons rather than reduced or weakened.
Fix product bugs surfaced by the ports:
- util/github: use the exception code, not the HTTP status, for 401/403
- advisory: serialize empty audit maps as [] to match PHP json_encode
- repository/filesystem and downloader/file: fix RefCell double-borrow panics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/downloader')
4 files changed, 1298 insertions, 109 deletions
diff --git a/crates/shirabe/tests/downloader/download_manager_test.rs b/crates/shirabe/tests/downloader/download_manager_test.rs index 2405b4b..10fd1a9 100644 --- a/crates/shirabe/tests/downloader/download_manager_test.rs +++ b/crates/shirabe/tests/downloader/download_manager_test.rs @@ -3,23 +3,94 @@ use std::cell::RefCell; use std::rc::Rc; +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 crate::downloader_stub::DownloaderStub; use crate::io_stub::IOStub; +// 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<PackageInterfaceHandle>, + output: bool, + ) -> anyhow::Result<Option<PhpMixed>>; + async fn prepare( + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>>; + async fn install( + &mut self, + package: PackageInterfaceHandle, + path: &str, + output: bool, + ) -> anyhow::Result<Option<PhpMixed>>; + async fn update( + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + path: &str, + ) -> anyhow::Result<Option<PhpMixed>>; + async fn remove( + &mut self, + package: PackageInterfaceHandle, + path: &str, + output: bool, + ) -> anyhow::Result<Option<PhpMixed>>; + async fn cleanup( + &mut self, + r#type: &str, + package: PackageInterfaceHandle, + path: &str, + prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>>; + } +} + +fn run<F: std::future::Future>(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 { - let norm_version = VersionParser.normalize("1.0.0", None).unwrap(); - CompletePackageHandle::new("dummy/pkg".to_string(), norm_version, "1.0.0".to_string()).into() + 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 @@ -27,6 +98,22 @@ fn create_downloader_mock() -> Rc<RefCell<dyn DownloaderInterface>> { Rc::new(RefCell::new(DownloaderStub::new())) as Rc<RefCell<dyn DownloaderInterface>> } +/// 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<RefCell<dyn DownloaderInterface>> { + Rc::new(RefCell::new(downloader)) as Rc<RefCell<dyn DownloaderInterface>> +} + fn create_manager() -> DownloadManager { let io = Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>; DownloadManager::new(io, false, None) @@ -73,170 +160,698 @@ fn test_get_downloader_for_metapackage() { ); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"] #[test] fn test_get_downloader_for_correctly_installed_dist_package() { - todo!() + 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)); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"] +// 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() { - todo!() + 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()); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"] #[test] fn test_get_downloader_for_correctly_installed_source_package() { - todo!() + 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)); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"] +// 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() { - todo!() + 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()); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_full_package_download() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_full_package_download_failover() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"] #[test] fn test_bad_package_download() { - todo!() + let package = create_package_mock(); + // getSourceType() => null, getDistType() => null. + + let manager = create_manager(); + + assert!(run(manager.download(package, "target_dir", None)).is_err()); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_dist_only_package_download() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_source_only_package_download() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_metapackage_package_download() { - todo!() + // 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_full_package_download_with_source_preferred() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_dist_only_package_download_with_source_preferred() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_source_only_package_download_with_source_preferred() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"] #[test] fn test_bad_package_download_with_source_preferred() { - todo!() + 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()); } -#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"] #[test] fn test_update_dist_with_equal_types() { - todo!() + 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(); } -#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"] #[test] fn test_update_dist_with_not_equal_types() { - todo!() + 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(); } -#[ignore = "requires PHPUnit mock of PackageInterface and ReflectionMethod for private getAvailableSources"] #[test] fn test_get_available_sources_update_sticks_to_same_source() { - todo!() + // 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<String> = case.expected.iter().map(|s| s.to_string()).collect(); + assert_eq!(result, expected); + } } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_update_metapackage() { - todo!() + // 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() + ); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_remove() { - todo!() + 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(); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] #[test] fn test_metapackage_remove() { - todo!() + // 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() + ); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_preference_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_preference_no_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_match_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_without_match_no_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_auto_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_auto_no_dev() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_source() { - todo!() + 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")); } -#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"] +/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference #[test] fn test_install_preference_with_match_dist() { - todo!() + 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")); } diff --git a/crates/shirabe/tests/downloader/file_downloader_test.rs b/crates/shirabe/tests/downloader/file_downloader_test.rs index cb09c74..6f7025e 100644 --- a/crates/shirabe/tests/downloader/file_downloader_test.rs +++ b/crates/shirabe/tests/downloader/file_downloader_test.rs @@ -5,14 +5,16 @@ use std::rc::Rc; use indexmap::IndexMap; use serial_test::serial; +use shirabe::cache::{Cache, CacheMock}; use shirabe::config::Config; use shirabe::downloader::DownloaderInterface; use shirabe::downloader::FileDownloader; use shirabe::io::IOInterface; +use shirabe::io::io_interface; use shirabe::io::null_io::NullIO; use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; use shirabe::util::HttpDownloader; -use shirabe::util::filesystem::Filesystem; +use shirabe::util::filesystem::{Filesystem, FilesystemMock}; use shirabe::util::r#loop::Loop; use shirabe_php_shim::{ InvalidArgumentException, PhpMixed, RuntimeException, UnexpectedValueException, @@ -21,6 +23,7 @@ use shirabe_semver::VersionParser; use tempfile::TempDir; use crate::http_downloader_mock::get_http_downloader_mock; +use crate::io_mock::{Expectation, get_io_mock}; use shirabe::util::http_downloader::HttpDownloaderMockHandler; /// ref: TestCase::getPackage (default class CompletePackage) @@ -193,9 +196,61 @@ fn test_download_with_custom_cache_key() { } #[test] -#[ignore = "requires a Cache mock with gcIsNecessary/gc expectation tracking; Cache is a concrete struct with no test hook for asserting gc() was called once"] +#[serial] +#[ignore = "PHP Config::get('cache-files-ttl') casts the string via (int) (Config.php:396-398), turning '99999999' into 99999999, but shirabe's PhpMixed::as_int returns None for String so Config::get yields 0 and the assertion fails. Faithful port stays failing until the src is fixed."] fn test_cache_garbage_collection_is_called() { - todo!() + let expected_ttl: i64 = 99999999; + + let mut config_options: IndexMap<String, PhpMixed> = IndexMap::new(); + config_options.insert( + "cache-files-ttl".to_string(), + PhpMixed::String("99999999".to_string()), + ); + config_options.insert( + "cache-files-maxsize".to_string(), + PhpMixed::String("500M".to_string()), + ); + let config = get_config(config_options); + + // The PHP Cache mock forces gcIsNecessary() true and records the single gc() call; the CacheMock + // seam plays both roles here. + let tmp_dir = TempDir::new().unwrap(); + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let mut cache = Cache::new(io, tmp_dir.path().to_str().unwrap(), None, None, false); + cache.__set_mock(CacheMock { + gc_is_necessary: Some(true), + gc_calls: Some(Vec::new()), + ..Default::default() + }); + let cache = Rc::new(RefCell::new(cache)); + + let (http_downloader, _guard) = get_http_downloader_mock( + Vec::new(), + false, + HttpDownloaderMockHandler { + status: 200, + body: "file~".to_string(), + headers: Vec::new(), + }, + ); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let _downloader = FileDownloader::new( + io, + config, + http_downloader, + None, + Some(cache.clone()), + None, + None, + ); + + let gc_calls = cache.borrow().__gc_calls(); + assert_eq!(gc_calls.len(), 1, "gc should be called exactly once"); + assert_eq!( + gc_calls[0].0, expected_ttl, + "gc should receive the configured ttl" + ); } #[test] @@ -245,8 +300,86 @@ fn test_download_file_with_invalid_checksum() { } #[test] -#[ignore = "requires a Filesystem mock of removeDirectoryAsync/normalizePath plus IOMock output expectations; Filesystem is a concrete struct with no async-removal test hook"] +#[serial] fn test_downgrade_shows_appropriate_message() { - let _ = Filesystem::new(None); - todo!() + let old_package = get_package("dummy/pkg", "1.2.0"); + let new_package = get_package("dummy/pkg", "1.0.0"); + new_package.set_dist_url(Some("http://example.com/script.js".to_string())); + + let (io_mock, _io_guard) = get_io_mock(io_interface::DEBUG).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![ + Expectation::text_regex("{Downloading .*}"), + Expectation::text_regex("{Downgrading .*}"), + ], + false, + ) + .unwrap(); + + let tmp_dir = TempDir::new().unwrap(); + let path = tmp_dir.path().to_string_lossy().into_owned(); + let mut config_options: IndexMap<String, PhpMixed> = IndexMap::new(); + config_options.insert( + "vendor-dir".to_string(), + PhpMixed::String(format!("{}/vendor", path)), + ); + let config = get_config(config_options); + + // PHP mocks Filesystem so removeDirectoryAsync is a no-op (resolve(true)) and normalizePath is an + // identity; the FilesystemMock seam reproduces both so update()'s remove/install do not disturb + // the pre-staged download file. + let mut filesystem = Filesystem::new(None); + filesystem.__set_mock(FilesystemMock { + remove_directory_async_result: Some(true), + normalize_path_identity: true, + ..Default::default() + }); + let filesystem = Rc::new(RefCell::new(filesystem)); + + let (http_downloader, _guard) = get_http_downloader_mock( + Vec::new(), + false, + HttpDownloaderMockHandler { + status: 200, + body: "file~".to_string(), + headers: Vec::new(), + }, + ); + + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let mut downloader = FileDownloader::new( + io, + config, + http_downloader.clone(), + None, + None, + Some(filesystem.clone()), + None, + ); + + // make sure the file expected to be downloaded is on disk already + let dl_file = downloader.__get_file_name(new_package.clone(), &path); + let dir = std::path::Path::new(&dl_file).parent().unwrap(); + std::fs::create_dir_all(dir).unwrap(); + std::fs::write(&dl_file, b"").unwrap(); + + let mut loop_ = Loop::new(http_downloader, None); + let promise = Box::pin(async { + downloader + .download(new_package.clone(), &path, Some(old_package.clone()), true) + .await + .map(|_| ()) + }); + run(loop_.wait(vec![promise], None)).expect("download should succeed"); + + run(downloader.update(old_package.clone(), new_package.clone(), &path)) + .expect("update should succeed"); + + assert_eq!( + filesystem.borrow().__remove_directory_async_calls(), + 1, + "removeDirectoryAsync should be called exactly once" + ); } diff --git a/crates/shirabe/tests/downloader/git_downloader_test.rs b/crates/shirabe/tests/downloader/git_downloader_test.rs index 07dec08..77a976e 100644 --- a/crates/shirabe/tests/downloader/git_downloader_test.rs +++ b/crates/shirabe/tests/downloader/git_downloader_test.rs @@ -9,6 +9,8 @@ use shirabe::config::Config; use shirabe::downloader::VcsDownloader; use shirabe::downloader::git_downloader::GitDownloader; use shirabe::io::IOInterface; +use shirabe::io::io_interface; +use shirabe::package::Mirror; use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; use shirabe::util::Git as GitUtil; use shirabe::util::ProcessExecutor; @@ -18,6 +20,7 @@ use shirabe_semver::VersionParser; use tempfile::TempDir; use crate::config_stub::ConfigStubBuilder; +use crate::io_mock::{Expectation, get_io_mock}; use crate::io_stub::IOStub; use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock}; @@ -333,9 +336,11 @@ fn test_download_with_cache() { fs.remove_directory(&cache_path).ok(); } -#[ignore = "getSourceUrls returns a mirror list (['https://github.com/mirrors/composer', \ - 'https://github.com/composer/composer']) that a real CompletePackage cannot reproduce \ - from a single source_url; no PackageInterface mock with arbitrary getSourceUrls exists"] +#[ignore = "Source-side blocker: getSourceUrls() must list the mirror ahead of the \ + source url, which requires a preferred source mirror. A git source mirror is \ + processed through ComposerMirror::process_git_url, whose github regex lacks PCRE \ + delimiters and panics when compiled by the php-shim Preg. PHP mocks getSourceUrls \ + directly and never exercises that path."] #[test] fn test_download_uses_various_protocols_and_sets_push_url_for_github() { let working_dir = set_up(); @@ -344,14 +349,106 @@ fn test_download_uses_various_protocols_and_sets_push_url_for_github() { todo!() } -#[ignore = "pushUrlProvider configures getSourceUrls to a list a real CompletePackage cannot \ - reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"] +#[serial] #[test] fn test_download_and_set_push_url_use_custom_various_protocols_for_github() { - let working_dir = set_up(); - let _tear_down = TearDown::new(working_dir.path().to_path_buf()); - let _ = &working_dir; - todo!() + // ref: pushUrlProvider — (github-protocols, fetch url, push url). + let cases: Vec<(Vec<&str>, &str, &str)> = vec![ + ( + vec!["ssh"], + "git@github.com:composer/composer", + "git@github.com:composer/composer.git", + ), + ( + vec!["https", "ssh", "git"], + "https://github.com/composer/composer", + "git@github.com:composer/composer.git", + ), + ( + vec!["https"], + "https://github.com/composer/composer", + "https://github.com/composer/composer.git", + ), + ]; + + for (protocols, url, push_url) in cases { + let working_dir = set_up(); + let _tear_down = TearDown::new(working_dir.path().to_path_buf()); + + let package = get_package( + "composer/composer", + "1.0.0", + Some("ref"), + Some("https://github.com/composer/composer"), + ); + + let expected_path = working_dir + .path() + .join("composerPath") + .to_string_lossy() + .into_owned(); + + let (process, _guard) = get_process_executor_mock( + vec![ + cmd(vec![ + "git", + "clone", + "--no-checkout", + "--", + url, + &expected_path, + ]), + cmd(vec!["git", "remote", "add", "composer", "--", url]), + cmd(["git", "fetch", "composer"]), + cmd(vec!["git", "remote", "set-url", "origin", "--", url]), + cmd(vec!["git", "remote", "set-url", "composer", "--", url]), + cmd(vec![ + "git", "remote", "set-url", "--push", "origin", "--", push_url, + ]), + cmd(["git", "branch", "-r"]), + cmd(["git", "checkout", "ref", "--"]), + cmd(["git", "reset", "--hard", "ref", "--"]), + ], + true, + Default::default(), + ); + + let mut config = Config::new(false, None); + let mut top: IndexMap<String, PhpMixed> = IndexMap::new(); + let mut section: IndexMap<String, PhpMixed> = IndexMap::new(); + section.insert( + "github-protocols".to_string(), + PhpMixed::List( + protocols + .iter() + .map(|p| PhpMixed::String(p.to_string())) + .collect(), + ), + ); + top.insert("config".to_string(), PhpMixed::Array(section)); + config.merge(&top, Config::SOURCE_UNKNOWN); + + let mut downloader = get_downloader_mock(None, Some(config), process, None); + + run(async { + downloader + .download(package.clone(), &expected_path, None) + .await + .unwrap(); + downloader + .prepare("install", package.clone(), &expected_path, None) + .await + .unwrap(); + downloader + .install(package.clone(), &expected_path) + .await + .unwrap(); + downloader + .cleanup("install", package, &expected_path, None) + .await + .unwrap(); + }); + } } #[serial] @@ -589,18 +686,95 @@ fn test_update_with_new_repo_url() { }); } -#[ignore = "getSourceUrls returns a multi-URL list a real CompletePackage cannot reproduce; \ - no PackageInterface mock with arbitrary getSourceUrls exists"] +#[ignore = "Blocked by a Config bug: Config::get(\"github-protocols\") does not drop the \ + insecure \"git\" protocol under secure-http. array_search_mixed returns the \ + matched index as PhpMixed::Int, but config.rs reads it via as_string (which is \ + Some only for String), so the removal is skipped and get() returns \ + [https, ssh, git] instead of [https, ssh]. The downloader then attempts a third \ + git:// fetch (absent from the process mock) and the error message reads \ + \"via https, ssh, git protocols\", failing the assertion. PHP's new Config() \ + reduces it to two protocols."] +#[serial] #[test] fn test_update_throws_runtime_exception_if_git_command_fails() { let working_dir = set_up(); let _tear_down = TearDown::new(working_dir.path().to_path_buf()); - let _ = &working_dir; - todo!() + + let url = "https://github.com/composer/composer"; + let package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url)); + + let (process, _guard) = get_process_executor_mock( + vec![ + cmd(["git", "show-ref", "--head", "-d"]), + cmd(["git", "status", "--porcelain", "--untracked-files=no"]), + // commit not yet in so we try to fetch + cmd_full( + ["git", "rev-parse", "--quiet", "--verify", "ref^{commit}"], + 1, + "", + "", + ), + // fail first fetch + cmd(["git", "remote", "-v"]), + cmd(vec!["git", "remote", "set-url", "composer", "--", url]), + cmd_full(["git", "fetch", "composer"], 1, "", ""), + // fail second fetch + cmd(vec![ + "git", + "remote", + "set-url", + "composer", + "--", + "git@github.com:composer/composer", + ]), + cmd_full(["git", "fetch", "composer"], 1, "", ""), + cmd(["git", "--version"]), + ], + true, + Default::default(), + ); + + let mut fs = Filesystem::new(None); + fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy())) + .unwrap(); + let working_dir_str = working_dir.path().to_string_lossy().into_owned(); + + let config = Config::new(false, None); + + let mut downloader = get_downloader_mock(None, Some(config), process, None); + + let result = run(async { + downloader + .download(package.clone(), &working_dir_str, Some(package.clone())) + .await?; + downloader + .prepare( + "update", + package.clone(), + &working_dir_str, + Some(package.clone()), + ) + .await?; + downloader + .update(package.clone(), package.clone(), &working_dir_str) + .await?; + downloader + .cleanup("update", package.clone(), &working_dir_str, Some(package)) + .await + }); + + let e = result.expect_err("failing git fetch should throw"); + assert!(e.to_string().contains( + "Failed to clone https://github.com/composer/composer via https, ssh protocols, aborting." + )); + assert!(e.to_string().contains("git@github.com:composer/composer")); } -#[ignore = "getSourceUrls returns a multi-URL list (['/' , github]) a real CompletePackage cannot \ - reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"] +#[ignore = "Source-side blocker: getSourceUrls() == ['/', github] requires a source \ + mirror so the list has two entries. A git source mirror is processed through \ + ComposerMirror::process_git_url, whose github regex lacks PCRE delimiters and \ + panics when compiled by the php-shim Preg. PHP mocks getSourceUrls directly and \ + never exercises that path."] #[test] fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but_is_able_to_recover() { @@ -610,24 +784,142 @@ fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but todo!() } -#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \ - cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"] +#[serial] #[test] fn test_downgrade_shows_appropriate_message() { let working_dir = set_up(); let _tear_down = TearDown::new(working_dir.path().to_path_buf()); - let _ = &working_dir; - todo!() + + let url = "https://github.com/composer/composer"; + + let old_package = get_package("composer/composer", "1.2.0", Some("ref"), Some("/foo/bar")); + old_package.set_source_mirrors(Some(vec![Mirror { + url: url.to_string(), + preferred: false, + }])); + let new_package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url)); + + let (process, _guard) = get_process_executor_mock(vec![], false, Default::default()); + + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects(vec![Expectation::text_regex("{Downgrading .*}")], false) + .unwrap(); + let io = io_mock.clone() as Rc<RefCell<dyn IOInterface>>; + + let mut fs = Filesystem::new(None); + fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy())) + .unwrap(); + let working_dir_str = working_dir.path().to_string_lossy().into_owned(); + + let mut downloader = get_downloader_mock(Some(io), None, process, None); + + run(async { + downloader + .download( + new_package.clone(), + &working_dir_str, + Some(old_package.clone()), + ) + .await + .unwrap(); + downloader + .prepare( + "update", + new_package.clone(), + &working_dir_str, + Some(old_package.clone()), + ) + .await + .unwrap(); + downloader + .update(old_package.clone(), new_package.clone(), &working_dir_str) + .await + .unwrap(); + downloader + .cleanup("update", new_package, &working_dir_str, Some(old_package)) + .await + .unwrap(); + }); } -#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \ - cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"] +#[serial] #[test] fn test_not_using_downgrading_with_references() { let working_dir = set_up(); let _tear_down = TearDown::new(working_dir.path().to_path_buf()); - let _ = &working_dir; - todo!() + + let url = "https://github.com/composer/composer"; + + // dev versions: getVersion() is the (non-normalized) branch name. + let old_package = CompletePackageHandle::new( + "composer/composer".to_string(), + "dev-ref".to_string(), + "dev-ref".to_string(), + ); + old_package.__set_source_type(Some("git".to_string())); + old_package.set_source_reference(Some("ref".to_string())); + old_package.set_source_url(Some("/foo/bar".to_string())); + old_package.set_source_mirrors(Some(vec![Mirror { + url: url.to_string(), + preferred: false, + }])); + let old_package: PackageInterfaceHandle = old_package.into(); + + let new_package = CompletePackageHandle::new( + "composer/composer".to_string(), + "dev-ref2".to_string(), + "dev-ref2".to_string(), + ); + new_package.__set_source_type(Some("git".to_string())); + new_package.set_source_reference(Some("ref".to_string())); + new_package.set_source_url(Some(url.to_string())); + let new_package: PackageInterfaceHandle = new_package.into(); + + let (process, _guard) = get_process_executor_mock(vec![], false, Default::default()); + + let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects(vec![Expectation::text_regex("{Upgrading .*}")], false) + .unwrap(); + let io = io_mock.clone() as Rc<RefCell<dyn IOInterface>>; + + let mut fs = Filesystem::new(None); + fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy())) + .unwrap(); + let working_dir_str = working_dir.path().to_string_lossy().into_owned(); + + let mut downloader = get_downloader_mock(Some(io), None, process, None); + + run(async { + downloader + .download( + new_package.clone(), + &working_dir_str, + Some(old_package.clone()), + ) + .await + .unwrap(); + downloader + .prepare( + "update", + new_package.clone(), + &working_dir_str, + Some(old_package.clone()), + ) + .await + .unwrap(); + downloader + .update(old_package.clone(), new_package.clone(), &working_dir_str) + .await + .unwrap(); + downloader + .cleanup("update", new_package, &working_dir_str, Some(old_package)) + .await + .unwrap(); + }); } #[ignore = "PHP mocks Filesystem::removeDirectoryAsync (asserting it is called once with the \ diff --git a/crates/shirabe/tests/downloader/perforce_downloader_test.rs b/crates/shirabe/tests/downloader/perforce_downloader_test.rs index 63f1de4..f23888a 100644 --- a/crates/shirabe/tests/downloader/perforce_downloader_test.rs +++ b/crates/shirabe/tests/downloader/perforce_downloader_test.rs @@ -1,44 +1,193 @@ //! ref: composer/tests/Composer/Test/Downloader/PerforceDownloaderTest.php +use std::cell::RefCell; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::downloader::VcsDownloader; +use shirabe::downloader::perforce_downloader::PerforceDownloader; +use shirabe::io::IOInterface; +use shirabe::io::io_interface::NORMAL; +use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; +use shirabe::util::filesystem::Filesystem; +use shirabe::util::process_executor::MockHandler; +use shirabe_php_shim::PhpMixed; +use shirabe_semver::VersionParser; use tempfile::TempDir; -fn set_up() -> TempDir { - let test_path = TempDir::new().unwrap(); - // repoConfig/config/io/processExecutor/repository/package/downloader rely on - // ProcessExecutorMock and PHPUnit mocks of the repository and Package, which are not - // ported. - let () = todo!(); - #[allow(unreachable_code)] - test_path +use crate::io_mock::{Expectation, get_io_mock}; +use crate::io_stub::IOStub; +use crate::process_executor_mock::get_process_executor_mock; + +// A getMockBuilder('Composer\Util\Perforce') stand-in: the seam trait extracted from the +// concrete `Perforce` struct, mocked so the downloader's workflow can be verified. +mockall::mock! { + #[derive(Debug)] + pub Perforce {} + impl shirabe::util::PerforceInterface for Perforce { + fn initialize_path(&mut self, path: &str); + fn set_stream(&mut self, stream: &str); + fn p4_login(&mut self) -> anyhow::Result<()>; + fn check_stream(&mut self) -> bool; + fn write_p4_client_spec(&mut self) -> anyhow::Result<()>; + fn connect_client(&mut self) -> anyhow::Result<()>; + fn sync_code_base(&mut self, source_reference: Option<String>) -> anyhow::Result<()>; + fn cleanup_client_spec(&mut self); + fn get_commit_logs(&mut self, from_reference: &str, to_reference: &str) -> Option<String>; + fn get_file_content(&mut self, file: &str, identifier: &str) -> Option<String>; + fn get_branches(&mut self) -> IndexMap<String, String>; + fn get_tags(&mut self) -> IndexMap<String, String>; + fn get_user(&self) -> Option<String>; + fn get_composer_information( + &mut self, + identifier: &str, + ) -> anyhow::Result<Option<IndexMap<String, PhpMixed>>>; + } +} + +fn run<F: std::future::Future>(future: F) -> F::Output { + tokio::runtime::Builder::new_current_thread() + .build() + .unwrap() + .block_on(future) } -// These mock Perforce, the repository config and a Package to drive PerforceDownloader's -// initialization and install paths; mocking is not available here. +/// ref: PerforceDownloaderTest::getConfig (seeds `home` with the temp dir) +fn get_config(test_path: &std::path::Path) -> Config { + let mut config = Config::new(true, None); + let mut top: IndexMap<String, PhpMixed> = IndexMap::new(); + let mut section: IndexMap<String, PhpMixed> = IndexMap::new(); + section.insert( + "home".to_string(), + PhpMixed::String(test_path.to_string_lossy().into_owned()), + ); + top.insert("config".to_string(), PhpMixed::Array(section)); + config.merge(&top, Config::SOURCE_UNKNOWN); + config +} + +/// ref: PerforceDownloaderTest::getMockPackageInterface. A real CompletePackage stands in for +/// the PHPUnit PackageInterface mock; the source reference is returned by getSourceReference. +fn make_package(source_reference: Option<&str>) -> PackageInterfaceHandle { + let norm_version = VersionParser.normalize("1.0.0", None).unwrap(); + let package = + CompletePackageHandle::new("test/pkg".to_string(), norm_version, "1.0.0".to_string()); + package.set_source_reference(source_reference.map(|s| s.to_string())); + package.into() +} -#[ignore = "requires PHPUnit getMockBuilder mocks of IOInterface/PackageInterface/VcsRepository and ProcessExecutorMock via unported set_up()"] #[test] fn test_init_perforce_instantiates_a_new_perforce_object() { - let _test_path = set_up(); - todo!() + // @doesNotPerformAssertions: only verifies init_perforce instantiates a Perforce without + // error. PHP attaches a VcsRepository whose getRepoConfig seeds the config, but in the + // current port VcsRepository implements ConfigurableRepositoryInterface only (not + // RepositoryInterface), so it cannot be held in a RepositoryInterfaceHandle. The package + // is therefore built without a repository, yielding an empty repo config. + let test_path = TempDir::new().unwrap(); + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new())); + let config = Rc::new(RefCell::new(get_config(test_path.path()))); + let package = make_package(None); + + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + let fs = Rc::new(RefCell::new(Filesystem::new(None))); + let mut downloader = PerforceDownloader::new(io, config, process, fs); + + downloader.init_perforce( + package, + test_path.path().to_string_lossy().into_owned(), + "SOURCE_REF".to_string(), + ); } -#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce and expects()->never() verification, unavailable in the Rust port"] #[test] fn test_init_perforce_does_nothing_if_perforce_already_set() { - let _test_path = set_up(); - todo!() + let test_path = TempDir::new().unwrap(); + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new())); + let config = Rc::new(RefCell::new(get_config(test_path.path()))); + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + let fs = Rc::new(RefCell::new(Filesystem::new(None))); + let mut downloader = PerforceDownloader::new(io, config, process, fs); + + // The already-set perforce only sees initializePath; the repository's getRepoConfig is + // never consulted (the early return happens before reaching it). + let mut perforce = MockPerforce::new(); + perforce.expect_initialize_path().times(1).returning(|_| ()); + downloader.set_perforce(Box::new(perforce)); + + let package = make_package(None); + downloader.init_perforce( + package, + test_path.path().to_string_lossy().into_owned(), + "SOURCE_REF".to_string(), + ); } -#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce with expects()->once()->method() expectation verification, unavailable in the Rust port"] #[test] fn test_do_install_with_tag() { - let _test_path = set_up(); - todo!() + do_install_workflow("SOURCE_REF@123", Some("123".to_string())); } -#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce with expects()->once()->method() expectation verification, unavailable in the Rust port"] #[test] fn test_do_install_with_no_tag() { - let _test_path = set_up(); - todo!() + do_install_workflow("SOURCE_REF", None); +} + +// Shared body for testDoInstallWithTag / testDoInstallWithNoTag: enforce the install workflow +// against a mocked Perforce, asserting each step is invoked exactly once. +fn do_install_workflow(source_ref: &'static str, expected_label: Option<String>) { + let test_path = TempDir::new().unwrap(); + let test_path_str = test_path.path().to_string_lossy().into_owned(); + + let (io_mock, _io_guard) = get_io_mock(NORMAL).unwrap(); + io_mock + .borrow_mut() + .expects( + vec![Expectation::text_regex(format!("Cloning {}", source_ref))], + false, + ) + .unwrap(); + let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone(); + let config = Rc::new(RefCell::new(get_config(test_path.path()))); + let (process, _process_guard) = + get_process_executor_mock(vec![], false, MockHandler::default()); + let fs = Rc::new(RefCell::new(Filesystem::new(None))); + let mut downloader = PerforceDownloader::new(io, config, process, fs); + + let mut perforce = MockPerforce::new(); + let expected_path = test_path_str.clone(); + perforce + .expect_initialize_path() + .times(1) + .withf(move |path: &str| path == expected_path) + .returning(|_| ()); + perforce + .expect_set_stream() + .times(1) + .withf(move |stream: &str| stream == source_ref) + .returning(|_| ()); + perforce.expect_p4_login().times(1).returning(|| Ok(())); + perforce + .expect_write_p4_client_spec() + .times(1) + .returning(|| Ok(())); + perforce + .expect_connect_client() + .times(1) + .returning(|| Ok(())); + perforce + .expect_sync_code_base() + .times(1) + .withf(move |reference: &Option<String>| *reference == expected_label) + .returning(|_| Ok(())); + perforce + .expect_cleanup_client_spec() + .times(1) + .returning(|| ()); + downloader.set_perforce(Box::new(perforce)); + + let package = make_package(Some(source_ref)); + run(downloader.do_install(package, &test_path_str, "url")).unwrap(); } |
