aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/downloader
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-27 17:21:00 +0900
committernsfisis <nsfisis@gmail.com>2026-06-27 17:26:28 +0900
commite98823e599eb375b30037cc714710e3309d927d1 (patch)
treee1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/downloader
parent20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff)
downloadphp-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')
-rw-r--r--crates/shirabe/tests/downloader/download_manager_test.rs731
-rw-r--r--crates/shirabe/tests/downloader/file_downloader_test.rs145
-rw-r--r--crates/shirabe/tests/downloader/git_downloader_test.rs338
-rw-r--r--crates/shirabe/tests/downloader/perforce_downloader_test.rs193
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();
}