diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-24 00:47:20 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-24 00:47:53 +0900 |
| commit | 340164c64e90d44b1bdf620b514166db1f76cc98 (patch) | |
| tree | 86195fe5ff8582b64e0324e9f40619e36c04948f /crates | |
| parent | 8fe3390d064303b86133a1d2983144a4818a7121 (diff) | |
| download | php-shirabe-340164c64e90d44b1bdf620b514166db1f76cc98.tar.gz php-shirabe-340164c64e90d44b1bdf620b514166db1f76cc98.tar.zst php-shirabe-340164c64e90d44b1bdf620b514166db1f76cc98.zip | |
test: port more unimplemented tests
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
28 files changed, 1775 insertions, 186 deletions
diff --git a/crates/shirabe-php-shim/src/fs.rs b/crates/shirabe-php-shim/src/fs.rs index ecbe5c4..f12d7bf 100644 --- a/crates/shirabe-php-shim/src/fs.rs +++ b/crates/shirabe-php-shim/src/fs.rs @@ -260,8 +260,11 @@ pub fn fopen(file: &str, mode: &str) -> Result<PhpResource, std::io::Error> { StreamBacking::Memory(std::io::Cursor::new(Vec::new())), readable, writable, + mode.to_string(), + file.to_string(), )); } + let uri = file.to_string(); let mut options = std::fs::OpenOptions::new(); match base_mode.as_str() { "r" => options.read(true), @@ -282,6 +285,8 @@ pub fn fopen(file: &str, mode: &str) -> Result<PhpResource, std::io::Error> { StreamBacking::File(file), readable, writable, + mode.to_string(), + uri, )) } diff --git a/crates/shirabe-php-shim/src/lib.rs b/crates/shirabe-php-shim/src/lib.rs index 267c3b7..8eb8f2f 100644 --- a/crates/shirabe-php-shim/src/lib.rs +++ b/crates/shirabe-php-shim/src/lib.rs @@ -383,16 +383,28 @@ pub struct StreamState { /// only reports true after a read has hit the end; cleared by a seek. pub(crate) eof: bool, pub(crate) closed: bool, + /// The mode string passed to `fopen`, reported back by `stream_get_meta_data`. + pub(crate) mode: String, + /// The path/URI the stream was opened from, reported back by `stream_get_meta_data`. + pub(crate) uri: String, } impl StreamState { - pub(crate) fn new(backing: StreamBacking, readable: bool, writable: bool) -> PhpResource { + pub(crate) fn new( + backing: StreamBacking, + readable: bool, + writable: bool, + mode: String, + uri: String, + ) -> PhpResource { PhpResource::Stream(std::rc::Rc::new(std::cell::RefCell::new(StreamState { backing, readable, writable, eof: false, closed: false, + mode, + uri, }))) } } diff --git a/crates/shirabe-php-shim/src/stream.rs b/crates/shirabe-php-shim/src/stream.rs index 44ba0a4..5e43316 100644 --- a/crates/shirabe-php-shim/src/stream.rs +++ b/crates/shirabe-php-shim/src/stream.rs @@ -67,6 +67,14 @@ pub fn stream_context_create( todo!() } +pub fn stream_context_get_options(_stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { + todo!() +} + +pub fn stream_context_get_params(_stream_or_context: &PhpMixed) -> IndexMap<String, PhpMixed> { + todo!() +} + pub fn stream_isatty(stream: PhpResource) -> bool { stream_isatty_resource(&stream) } @@ -124,8 +132,76 @@ pub fn stream_isatty_resource(resource: &PhpResource) -> bool { } } -pub fn stream_get_meta_data(_resource: &PhpResource) -> IndexMap<String, PhpMixed> { - todo!() +pub fn stream_get_meta_data(resource: &PhpResource) -> IndexMap<String, PhpMixed> { + // (timed_out, blocked, eof, wrapper_type, stream_type, mode, seekable, uri) + let (eof, wrapper_type, stream_type, mode, seekable, uri) = match resource { + PhpResource::Stdin => (false, "PHP", "STDIO", "r".to_string(), false, "php://stdin"), + PhpResource::Stdout => ( + false, + "PHP", + "STDIO", + "w".to_string(), + false, + "php://stdout", + ), + PhpResource::Stderr => ( + false, + "PHP", + "STDIO", + "w".to_string(), + false, + "php://stderr", + ), + PhpResource::Stream(state) => { + let state = state.borrow(); + let (wrapper_type, stream_type) = match &state.backing { + StreamBacking::Memory(_) => { + if state.uri.starts_with("php://temp") { + ("PHP", "TEMP") + } else { + ("PHP", "MEMORY") + } + } + StreamBacking::File(_) => ("plainfile", "STDIO"), + }; + return build_meta_data( + state.eof, + wrapper_type, + stream_type, + state.mode.clone(), + true, + &state.uri, + ); + } + }; + build_meta_data(eof, wrapper_type, stream_type, mode, seekable, uri) +} + +fn build_meta_data( + eof: bool, + wrapper_type: &str, + stream_type: &str, + mode: String, + seekable: bool, + uri: &str, +) -> IndexMap<String, PhpMixed> { + let mut map = IndexMap::new(); + map.insert("timed_out".to_string(), PhpMixed::Bool(false)); + map.insert("blocked".to_string(), PhpMixed::Bool(true)); + map.insert("eof".to_string(), PhpMixed::Bool(eof)); + map.insert( + "wrapper_type".to_string(), + PhpMixed::String(wrapper_type.to_string()), + ); + map.insert( + "stream_type".to_string(), + PhpMixed::String(stream_type.to_string()), + ); + map.insert("mode".to_string(), PhpMixed::String(mode)); + map.insert("unread_bytes".to_string(), PhpMixed::Int(0)); + map.insert("seekable".to_string(), PhpMixed::Bool(seekable)); + map.insert("uri".to_string(), PhpMixed::String(uri.to_string())); + map } pub fn stream_set_blocking(_resource: &PhpResource, _enable: bool) -> bool { diff --git a/crates/shirabe-php-shim/src/zip.rs b/crates/shirabe-php-shim/src/zip.rs index 5ef99bb..08662e6 100644 --- a/crates/shirabe-php-shim/src/zip.rs +++ b/crates/shirabe-php-shim/src/zip.rs @@ -49,6 +49,10 @@ impl ZipArchive { todo!() } + pub fn get_from_name(&self, _name: &str) -> Option<String> { + todo!() + } + pub fn get_stream(&self, _name: &str) -> Option<crate::PhpResource> { todo!() } diff --git a/crates/shirabe/src/installed_versions.rs b/crates/shirabe/src/installed_versions.rs index c4ef526..33286ad 100644 --- a/crates/shirabe/src/installed_versions.rs +++ b/crates/shirabe/src/installed_versions.rs @@ -352,6 +352,20 @@ impl InstalledVersions { /// Returns the raw data of all installed.php which are currently loaded for custom implementations /// /// @return array[] + /// Returns the first dataset loaded, which may not be what you expect. Use get_all_raw_data + /// instead, which returns all datasets for all autoloaders present in the process. + pub fn get_raw_data() -> IndexMap<String, PhpMixed> { + // PHP emits an E_USER_DEPRECATED notice here; there is no Rust equivalent. + let mut installed = INSTALLED.lock().unwrap(); + if installed.is_none() { + // PHP only includes __DIR__/installed.php when loaded from its dumped location; the + // shim is always the source location (PHP's `else` branch), so the data is empty. + *installed = Some(IndexMap::new()); + } + + installed.clone().unwrap() + } + pub fn get_all_raw_data() -> Vec<IndexMap<String, PhpMixed>> { Self::get_installed() } diff --git a/crates/shirabe/src/package/archiver/archivable_files_finder.rs b/crates/shirabe/src/package/archiver/archivable_files_finder.rs index 08f8df1..67f2fda 100644 --- a/crates/shirabe/src/package/archiver/archivable_files_finder.rs +++ b/crates/shirabe/src/package/archiver/archivable_files_finder.rs @@ -57,7 +57,7 @@ impl ArchivableFilesFinder { } let relative_path = Preg::replace( - &format!("^{}", preg_quote(&sources_clone, Some('#'))), + &format!("#^{}#", preg_quote(&sources_clone, Some('#'))), "", &fs.normalize_path(&realpath.to_string_lossy()), ); diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs index 303983a..665efb6 100644 --- a/crates/shirabe/src/package/handle.rs +++ b/crates/shirabe/src/package/handle.rs @@ -1213,6 +1213,15 @@ macro_rules! impl_real_package_test_setters { .expect("real package handle invariant") .set_type(r#type); } + + /// For testing only: mirrors PHP `Package::setSourceType`. + pub fn __set_source_type(&self, r#type: Option<String>) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_source_type(r#type); + } } }; } diff --git a/crates/shirabe/src/question/strict_confirmation_question.rs b/crates/shirabe/src/question/strict_confirmation_question.rs index 87290ac..8d4239b 100644 --- a/crates/shirabe/src/question/strict_confirmation_question.rs +++ b/crates/shirabe/src/question/strict_confirmation_question.rs @@ -35,6 +35,10 @@ impl StrictConfirmationQuestion { &self.inner } + pub fn inner_mut(&mut self) -> &mut Question { + &mut self.inner + } + fn get_default_normalizer(&self) -> Box<dyn Fn(PhpMixed) -> PhpMixed> { let default = self.inner.get_default(); let true_regex = self.true_answer_regex.clone(); diff --git a/crates/shirabe/src/repository/package_repository.rs b/crates/shirabe/src/repository/package_repository.rs index 554e24a..e19d04c 100644 --- a/crates/shirabe/src/repository/package_repository.rs +++ b/crates/shirabe/src/repository/package_repository.rs @@ -2,12 +2,18 @@ use crate::advisory::SecurityAdvisory; use crate::advisory::{AnySecurityAdvisory, PartialSecurityAdvisory}; +use crate::package::BasePackageHandle; +use crate::package::PackageInterfaceHandle; use crate::package::loader::ArrayLoader; use crate::package::loader::ValidatingArrayLoader; use crate::package::version::VersionParser; use crate::repository::ArrayRepository; use crate::repository::InvalidRepositoryException; -use crate::repository::{AdvisoryProviderInterface, SecurityAdvisoryResult}; +use crate::repository::RepositoryInterfaceWeakHandle; +use crate::repository::{ + AdvisoryProviderInterface, FindPackageConstraint, LoadPackagesResult, ProviderInfo, + RepositoryInterface, SearchResult, SecurityAdvisoryResult, +}; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_php_shim::{Exception, PhpMixed, RuntimeException, var_export}; @@ -80,6 +86,85 @@ impl PackageRepository { } } +impl RepositoryInterface for PackageRepository { + // The structural methods are inherited from ArrayRepository in PHP, where they trigger the + // overridden initialize() that loads packages from config. Wiring that virtual dispatch is a + // Phase C concern; the advisory paths below are what is exercised so far. + fn count(&self) -> anyhow::Result<usize> { + todo!() + } + + fn has_package(&self, _package: PackageInterfaceHandle) -> bool { + todo!() + } + + fn find_package( + &mut self, + _name: &str, + _constraint: FindPackageConstraint, + ) -> anyhow::Result<Option<BasePackageHandle>> { + todo!() + } + + fn find_packages( + &mut self, + _name: &str, + _constraint: Option<FindPackageConstraint>, + ) -> anyhow::Result<Vec<BasePackageHandle>> { + todo!() + } + + fn get_packages(&mut self) -> anyhow::Result<Vec<BasePackageHandle>> { + todo!() + } + + fn load_packages( + &mut self, + _package_name_map: IndexMap<String, Option<shirabe_semver::constraint::AnyConstraint>>, + _acceptable_stabilities: IndexMap<String, i64>, + _stability_flags: IndexMap<String, i64>, + _already_loaded: IndexMap<String, IndexMap<String, PackageInterfaceHandle>>, + ) -> anyhow::Result<LoadPackagesResult> { + todo!() + } + + fn search( + &mut self, + _query: String, + _mode: i64, + _type: Option<String>, + ) -> anyhow::Result<Vec<SearchResult>> { + todo!() + } + + fn get_providers( + &mut self, + _package_name: String, + ) -> anyhow::Result<IndexMap<String, ProviderInfo>> { + todo!() + } + + fn get_repo_name(&self) -> String { + PackageRepository::get_repo_name(self) + } + + fn as_advisory_provider(&self) -> Option<&dyn AdvisoryProviderInterface> { + Some(self) + } + + fn as_advisory_provider_mut(&mut self) -> Option<&mut dyn AdvisoryProviderInterface> { + Some(self) + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn set_self_handle(&self, weak: RepositoryInterfaceWeakHandle) { + self.inner.set_self_handle(weak); + } +} + impl AdvisoryProviderInterface for PackageRepository { fn has_security_advisories(&mut self) -> anyhow::Result<bool> { Ok(!self.security_advisories.is_empty()) diff --git a/crates/shirabe/src/util/http/proxy_manager.rs b/crates/shirabe/src/util/http/proxy_manager.rs index 37d73c0..d0ff9d9 100644 --- a/crates/shirabe/src/util/http/proxy_manager.rs +++ b/crates/shirabe/src/util/http/proxy_manager.rs @@ -1,5 +1,6 @@ //! ref: composer/src/Composer/Util/Http/ProxyManager.php +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Mutex, OnceLock}; use crate::downloader::TransportException; @@ -9,12 +10,17 @@ use crate::util::http::RequestProxy; static INSTANCE: OnceLock<Mutex<Option<ProxyManager>>> = OnceLock::new(); +// Distinguishes ProxyManager instances so tests can mirror PHP `===` identity of the singleton, +// which the Rust value-based singleton does not otherwise expose. +static NEXT_GENERATION: AtomicU64 = AtomicU64::new(0); + #[derive(Debug)] pub struct ProxyManager { error: Option<String>, http_proxy: Option<ProxyItem>, https_proxy: Option<ProxyItem>, no_proxy_handler: std::cell::RefCell<Option<NoProxyPattern>>, + generation: u64, } impl ProxyManager { @@ -24,6 +30,7 @@ impl ProxyManager { http_proxy: None, https_proxy: None, no_proxy_handler: std::cell::RefCell::new(None), + generation: NEXT_GENERATION.fetch_add(1, Ordering::Relaxed), }; if let Err(e) = instance.get_proxy_data() { instance.error = Some(e.to_string()); @@ -41,6 +48,12 @@ impl ProxyManager { } } + /// For testing only: a unique id per constructed instance, used to mirror PHP `===` identity + /// comparison of the ProxyManager singleton across `get_instance`/`reset`. + pub fn __generation(&self) -> u64 { + self.generation + } + pub fn has_proxy(&self) -> bool { self.http_proxy.is_some() || self.https_proxy.is_some() } diff --git a/crates/shirabe/tests/config_test.rs b/crates/shirabe/tests/config_test.rs index fb40cac..75e1a24 100644 --- a/crates/shirabe/tests/config_test.rs +++ b/crates/shirabe/tests/config_test.rs @@ -486,10 +486,37 @@ fn test_prohibited_urls_throw_exception() { } } -#[ignore = "requires getIOMock with expects() output expectations; no IO mocking infrastructure exists"] +// PHP asserts the warning via getIOMock()->expects(); here a real BufferIO captures the output +// instead. The case stays #[ignore] because BufferIO::get_output is todo!() (its PhpResource +// stream model is unfinished). +#[ignore = "BufferIO::get_output is todo!() (PhpResource stream model unfinished)"] #[test] fn test_prohibited_urls_warning_verify_peer() { - todo!() + let io = std::rc::Rc::new(std::cell::RefCell::new( + shirabe::io::buffer_io::BufferIO::new( + String::new(), + shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL, + None, + ) + .unwrap(), + )); + + let mut config = Config::new(false, None); + + let mut ssl: IndexMap<String, PhpMixed> = IndexMap::new(); + ssl.insert("verify_peer".to_string(), PhpMixed::Bool(false)); + ssl.insert("verify_peer_name".to_string(), PhpMixed::Bool(false)); + let mut repo_options: IndexMap<String, PhpMixed> = IndexMap::new(); + repo_options.insert("ssl".to_string(), PhpMixed::Array(ssl)); + + config + .prohibit_url_by_config("https://example.org", Some(io.clone()), &repo_options) + .unwrap(); + + assert_eq!( + "<warning>Warning: Accessing example.org with verify_peer and verify_peer_name disabled.</warning>", + io.borrow().get_output() + ); } #[ignore] diff --git a/crates/shirabe/tests/dependency_resolver/default_policy_test.rs b/crates/shirabe/tests/dependency_resolver/default_policy_test.rs index 776de6c..fafe0e2 100644 --- a/crates/shirabe/tests/dependency_resolver/default_policy_test.rs +++ b/crates/shirabe/tests/dependency_resolver/default_policy_test.rs @@ -3,6 +3,7 @@ use indexmap::IndexMap; use shirabe::dependency_resolver::PolicyInterface; use shirabe::dependency_resolver::default_policy::DefaultPolicy; +use shirabe::package::Link; use shirabe::package::handle::{CompleteAliasPackageHandle, CompletePackageHandle}; use shirabe::repository::array_repository::ArrayRepository; use shirabe::repository::handle::{LockArrayRepositoryHandle, RepositoryInterfaceHandle}; @@ -556,28 +557,217 @@ fn test_select_local_repos_first() { assert_eq!(expected, selected); } -#[ignore = "set_provides/set_replaces only exist on RootPackage handles; CompletePackage from get_package has no set_provides"] +/// PHP `new Link($source, $target, $constraint, $type)`: prettyConstraint defaults to +/// `(string) $constraint`. +fn link(source: &str, target: &str, constraint: AnyConstraint, r#type: &str) -> Link { + let pretty = constraint.get_pretty_string(); + Link::new( + source.to_string(), + target.to_string(), + constraint, + Some(r#type.to_string()), + pretty, + ) +} + +fn as_complete( + package: &shirabe::package::handle::PackageInterfaceHandle, +) -> CompletePackageHandle { + CompletePackageHandle::from_rc_unchecked(package.as_rc().clone()) +} + +fn constraint(operator: &str, version: &str) -> AnyConstraint { + SimpleConstraint::new(operator.to_string(), version.to_string(), None).into() +} + +#[ignore] #[test] fn test_select_all_providers() { let _tear_down = TearDown; - let _fixtures = set_up(); - todo!() + let mut fixtures = set_up(); + + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "2.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + + let mut provides_a: IndexMap<String, Link> = IndexMap::new(); + provides_a.insert( + "x".to_string(), + link("A", "X", constraint("==", "1.0"), Link::TYPE_PROVIDE), + ); + as_complete(&package_a).__set_provides(provides_a); + let mut provides_b: IndexMap<String, Link> = IndexMap::new(); + provides_b.insert( + "x".to_string(), + link("B", "X", constraint("==", "1.0"), Link::TYPE_PROVIDE), + ); + as_complete(&package_b).__set_provides(provides_b); + + fixtures + .repository_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + + let pool = fixtures + .repository_set + .create_pool_for_packages( + vec!["A".to_string(), "B".to_string()], + Some(fixtures.repo_locked.clone()), + ) + .unwrap(); + + let literals = vec![package_a.get_id(), package_b.get_id()]; + let expected = literals.clone(); + + let selected = fixtures + .policy + .select_preferred_packages(&pool, literals, None); + + assert_eq!(expected, selected); } -#[ignore = "set_provides/set_replaces only exist on RootPackage handles; CompletePackage from get_package has no set_replaces"] +#[ignore] #[test] fn test_prefer_non_replacing_from_same_repo() { let _tear_down = TearDown; - let _fixtures = set_up(); - todo!() + let mut fixtures = set_up(); + + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "2.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + + let mut replaces_b: IndexMap<String, Link> = IndexMap::new(); + replaces_b.insert( + "a".to_string(), + link("B", "A", constraint("==", "1.0"), Link::TYPE_REPLACE), + ); + as_complete(&package_b).__set_replaces(replaces_b); + + fixtures + .repository_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + + let pool = fixtures + .repository_set + .create_pool_for_packages( + vec!["A".to_string(), "B".to_string()], + Some(fixtures.repo_locked.clone()), + ) + .unwrap(); + + let literals = vec![package_a.get_id(), package_b.get_id()]; + let expected = literals.clone(); + + let selected = fixtures + .policy + .select_preferred_packages(&pool, literals, None); + + assert_eq!(expected, selected); } -#[ignore = "set_replaces only exists on RootPackage handles; CompletePackage from get_package has no set_replaces"] +#[ignore] #[test] fn test_prefer_replacing_package_from_same_vendor() { let _tear_down = TearDown; - let _fixtures = set_up(); - todo!() + let mut fixtures = set_up(); + + // test with default order + let package_b = get_package("vendor-b/replacer", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let package_a = get_package("vendor-a/replacer", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + + let mut replaces_a: IndexMap<String, Link> = IndexMap::new(); + replaces_a.insert( + "vendor-a/package".to_string(), + link( + "vendor-a/replacer", + "vendor-a/package", + constraint("==", "1.0"), + Link::TYPE_REPLACE, + ), + ); + as_complete(&package_a).__set_replaces(replaces_a); + let mut replaces_b: IndexMap<String, Link> = IndexMap::new(); + replaces_b.insert( + "vendor-a/package".to_string(), + link( + "vendor-b/replacer", + "vendor-a/package", + constraint("==", "1.0"), + Link::TYPE_REPLACE, + ), + ); + as_complete(&package_b).__set_replaces(replaces_b); + + fixtures + .repository_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + + let pool = fixtures + .repository_set + .create_pool_for_packages( + vec![ + "vendor-a/replacer".to_string(), + "vendor-b/replacer".to_string(), + ], + Some(fixtures.repo_locked.clone()), + ) + .unwrap(); + + let literals = vec![package_a.get_id(), package_b.get_id()]; + let expected = literals.clone(); + + let selected = fixtures.policy.select_preferred_packages( + &pool, + literals, + Some("vendor-a/package".to_string()), + ); + assert_eq!(expected, selected); + + // test with reversed order in repo + let repo = ArrayRepository::new(vec![]).unwrap(); + let package_a = CompletePackageHandle::dup(&as_complete(&package_a)); + repo.add_package(package_a.clone().into()).unwrap(); + let package_b = CompletePackageHandle::dup(&as_complete(&package_b)); + repo.add_package(package_b.clone().into()).unwrap(); + + let mut repository_set = RepositorySet::new( + "dev", + IndexMap::new(), + vec![], + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + repository_set + .add_repository(RepositoryInterfaceHandle::new(repo)) + .unwrap(); + + let pool = fixtures + .repository_set + .create_pool_for_packages( + vec![ + "vendor-a/replacer".to_string(), + "vendor-b/replacer".to_string(), + ], + Some(fixtures.repo_locked.clone()), + ) + .unwrap(); + + let literals = vec![package_a.get_id(), package_b.get_id()]; + let expected = literals.clone(); + + let selected = fixtures.policy.select_preferred_packages( + &pool, + literals, + Some("vendor-a/package".to_string()), + ); + assert_eq!(expected, selected); } #[ignore] diff --git a/crates/shirabe/tests/dependency_resolver/security_advisory_pool_filter_test.rs b/crates/shirabe/tests/dependency_resolver/security_advisory_pool_filter_test.rs index 98ac131..7938657 100644 --- a/crates/shirabe/tests/dependency_resolver/security_advisory_pool_filter_test.rs +++ b/crates/shirabe/tests/dependency_resolver/security_advisory_pool_filter_test.rs @@ -12,24 +12,272 @@ use shirabe::dependency_resolver::SecurityAdvisoryPoolFilter; use shirabe::dependency_resolver::pool::Pool; use shirabe::dependency_resolver::request::Request; use shirabe::package::handle::{CompletePackageHandle, PackageHandle, PackageInterfaceHandle}; -use shirabe_php_shim::PhpMixed; +use shirabe::repository::{PackageRepository, RepositoryInterfaceHandle}; +use shirabe_php_shim::{PhpMixed, uniqid}; +use shirabe_semver::constraint::{AnyConstraint, SimpleConstraint}; +#[ignore] #[test] -#[ignore = "requires PackageRepository to implement RepositoryInterface (filter takes Vec<RepositoryInterfaceHandle>) and php-shim uniqid() used by generateSecurityAdvisory; neither exists"] fn test_filter_packages_by_advisories() { - todo!() + let audit_config = AuditConfig::new( + true, + Auditor::FORMAT_SUMMARY.to_string(), + Auditor::ABANDONED_FAIL.to_string(), + true, + true, + false, + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filter = SecurityAdvisoryPoolFilter::new(Auditor, audit_config); + + let advisory1 = + generate_security_advisory("acme/package", Some("CVE-1999-1000"), ">=1.0.0,<1.1.0"); + let advisory2 = + generate_security_advisory("acme/package", Some("CVE-1999-1001"), ">=1.0.0,<1.1.0"); + let mut security_advisories: IndexMap<String, PhpMixed> = IndexMap::new(); + security_advisories.insert( + "acme/package".to_string(), + PhpMixed::List(vec![ + PhpMixed::Array(advisory1.clone()), + PhpMixed::Array(advisory2.clone()), + ]), + ); + let mut config: IndexMap<String, PhpMixed> = IndexMap::new(); + config.insert("package".to_string(), PhpMixed::List(vec![])); + config.insert( + "security-advisories".to_string(), + PhpMixed::Array(security_advisories), + ); + let repository = RepositoryInterfaceHandle::new(PackageRepository::new(config)); + + let package: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "1.0.0.0".to_string(), + "1.0".to_string(), + ) + .into(); + let expected_package1: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "2.0.0.0".to_string(), + "2.0".to_string(), + ) + .into(); + let expected_package2: PackageInterfaceHandle = PackageHandle::new( + "acme/other".to_string(), + "1.0.0.0".to_string(), + "1.0".to_string(), + ) + .into(); + + let pool = Pool::new( + vec![ + package.clone(), + expected_package1.clone(), + expected_package2.clone(), + ], + vec![], + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filtered_pool = filter + .filter(pool, vec![repository], &Request::new(None)) + .unwrap(); + + let packages = filtered_pool.get_packages(); + assert_eq!(packages.len(), 2); + assert!(packages[0].ptr_eq(&expected_package1)); + assert!(packages[1].ptr_eq(&expected_package2)); + + let constraint: AnyConstraint = + SimpleConstraint::new("==".to_string(), "1.0.0.0".to_string(), None).into(); + assert!(filtered_pool.is_security_removed_package_version("acme/package", Some(&constraint))); + assert_eq!( + filtered_pool + .get_all_abandoned_removed_package_versions() + .len(), + 0 + ); + + let advisory_map = filtered_pool.get_all_security_removed_package_versions(); + assert!(advisory_map.contains_key("acme/package")); + assert!(advisory_map["acme/package"].contains_key("1.0.0.0")); + assert_eq!( + vec![ + advisory1["advisoryId"].as_string().unwrap().to_string(), + advisory2["advisoryId"].as_string().unwrap().to_string(), + ], + filtered_pool.get_security_advisory_identifiers_for_package_version( + "acme/package", + Some(&constraint) + ), + ); } +#[ignore] #[test] -#[ignore = "requires PackageRepository to implement RepositoryInterface (filter takes Vec<RepositoryInterfaceHandle>) and php-shim uniqid() used by generateSecurityAdvisory; neither exists"] fn test_dont_filter_packages_by_ignored_advisories() { - todo!() + let mut ignore_list: IndexMap<String, Option<String>> = IndexMap::new(); + ignore_list.insert("CVE-2024-1234".to_string(), None); + let audit_config = AuditConfig::new( + true, + Auditor::FORMAT_SUMMARY.to_string(), + Auditor::ABANDONED_FAIL.to_string(), + true, + true, + false, + ignore_list.clone(), + ignore_list, + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filter = SecurityAdvisoryPoolFilter::new(Auditor, audit_config); + + let mut security_advisories: IndexMap<String, PhpMixed> = IndexMap::new(); + security_advisories.insert( + "acme/package".to_string(), + PhpMixed::List(vec![PhpMixed::Array(generate_security_advisory( + "acme/package", + Some("CVE-2024-1234"), + ">=1.0.0,<1.1.0", + ))]), + ); + let mut config: IndexMap<String, PhpMixed> = IndexMap::new(); + config.insert("package".to_string(), PhpMixed::List(vec![])); + config.insert( + "security-advisories".to_string(), + PhpMixed::Array(security_advisories), + ); + let repository = RepositoryInterfaceHandle::new(PackageRepository::new(config)); + + let expected_package1: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "1.0.0.0".to_string(), + "1.0".to_string(), + ) + .into(); + let expected_package2: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "1.1.0.0".to_string(), + "1.1".to_string(), + ) + .into(); + + let pool = Pool::new( + vec![expected_package1.clone(), expected_package2.clone()], + vec![], + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filtered_pool = filter + .filter(pool, vec![repository], &Request::new(None)) + .unwrap(); + + let packages = filtered_pool.get_packages(); + assert_eq!(packages.len(), 2); + assert!(packages[0].ptr_eq(&expected_package1)); + assert!(packages[1].ptr_eq(&expected_package2)); + assert_eq!( + filtered_pool + .get_all_abandoned_removed_package_versions() + .len(), + 0 + ); + assert_eq!( + filtered_pool + .get_all_security_removed_package_versions() + .len(), + 0 + ); } +#[ignore] #[test] -#[ignore = "requires PackageRepository to implement RepositoryInterface (filter takes Vec<RepositoryInterfaceHandle>) and php-shim uniqid() used by generateSecurityAdvisory; neither exists"] fn test_dont_filter_packages_with_block_insecure_disabled() { - todo!() + let audit_config = AuditConfig::new( + true, + Auditor::FORMAT_SUMMARY.to_string(), + Auditor::ABANDONED_FAIL.to_string(), + false, + true, + false, + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filter = SecurityAdvisoryPoolFilter::new(Auditor, audit_config); + + let mut security_advisories: IndexMap<String, PhpMixed> = IndexMap::new(); + security_advisories.insert( + "acme/package".to_string(), + PhpMixed::List(vec![PhpMixed::Array(generate_security_advisory( + "acme/package", + Some("CVE-2024-1234"), + ">=1.0.0,<1.1.0", + ))]), + ); + let mut config: IndexMap<String, PhpMixed> = IndexMap::new(); + config.insert("package".to_string(), PhpMixed::List(vec![])); + config.insert( + "security-advisories".to_string(), + PhpMixed::Array(security_advisories), + ); + let repository = RepositoryInterfaceHandle::new(PackageRepository::new(config)); + + let expected_package1: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "1.0.0.0".to_string(), + "1.0".to_string(), + ) + .into(); + let expected_package2: PackageInterfaceHandle = PackageHandle::new( + "acme/package".to_string(), + "1.1.0.0".to_string(), + "1.1".to_string(), + ) + .into(); + + let pool = Pool::new( + vec![expected_package1.clone(), expected_package2.clone()], + vec![], + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + IndexMap::new(), + ); + let filtered_pool = filter + .filter(pool, vec![repository], &Request::new(None)) + .unwrap(); + + let packages = filtered_pool.get_packages(); + assert_eq!(packages.len(), 2); + assert!(packages[0].ptr_eq(&expected_package1)); + assert!(packages[1].ptr_eq(&expected_package2)); + assert_eq!( + filtered_pool + .get_all_abandoned_removed_package_versions() + .len(), + 0 + ); + assert_eq!( + filtered_pool + .get_all_security_removed_package_versions() + .len(), + 0 + ); } #[test] @@ -106,3 +354,58 @@ fn test_dont_filter_packages_with_abandoned_package() { 0 ); } + +fn generate_security_advisory( + package_name: &str, + cve: Option<&str>, + affected_versions: &str, +) -> IndexMap<String, PhpMixed> { + let mut source: IndexMap<String, PhpMixed> = IndexMap::new(); + source.insert( + "name".to_string(), + PhpMixed::String("Security Advisory".to_string()), + ); + source.insert("remoteId".to_string(), PhpMixed::String("test".to_string())); + + let mut advisory: IndexMap<String, PhpMixed> = IndexMap::new(); + advisory.insert( + "advisoryId".to_string(), + PhpMixed::String(uniqid("PKSA-", false)), + ); + advisory.insert( + "packageName".to_string(), + PhpMixed::String(package_name.to_string()), + ); + advisory.insert("remoteId".to_string(), PhpMixed::String("test".to_string())); + advisory.insert( + "title".to_string(), + PhpMixed::String("Security Advisory".to_string()), + ); + advisory.insert("link".to_string(), PhpMixed::Null); + advisory.insert( + "cve".to_string(), + match cve { + Some(cve) => PhpMixed::String(cve.to_string()), + None => PhpMixed::Null, + }, + ); + advisory.insert( + "affectedVersions".to_string(), + PhpMixed::String(affected_versions.to_string()), + ); + advisory.insert("source".to_string(), PhpMixed::String("Tests".to_string())); + advisory.insert( + "reportedAt".to_string(), + PhpMixed::String("2024-04-31 12:37:47".to_string()), + ); + advisory.insert( + "composerRepository".to_string(), + PhpMixed::String("Package Repository".to_string()), + ); + advisory.insert("severity".to_string(), PhpMixed::String("high".to_string())); + advisory.insert( + "sources".to_string(), + PhpMixed::List(vec![PhpMixed::Array(source)]), + ); + advisory +} diff --git a/crates/shirabe/tests/dependency_resolver/solver_test.rs b/crates/shirabe/tests/dependency_resolver/solver_test.rs index c9c4d26..a688190 100644 --- a/crates/shirabe/tests/dependency_resolver/solver_test.rs +++ b/crates/shirabe/tests/dependency_resolver/solver_test.rs @@ -277,7 +277,7 @@ fn test_solver_remove_if_not_requested() { ); } -#[ignore = "get_pretty_string path reaches an unimplemented todo!() (in_array non-strict in php-shim)"] +#[ignore = "get_pretty_string path reaches an unimplemented stub (in_array non-strict in php-shim)"] #[test] fn test_install_non_existing_package_fails() { let fixtures = set_up(); @@ -1921,7 +1921,7 @@ fn test_issue265() { ); } -#[ignore = "get_pretty_string path reaches an unimplemented todo!() (in_array non-strict in php-shim)"] +#[ignore = "get_pretty_string path reaches an unimplemented stub (in_array non-strict in php-shim)"] #[test] fn test_conflict_result_empty() { let fixtures = set_up(); @@ -1979,7 +1979,7 @@ fn test_conflict_result_empty() { ); } -#[ignore = "get_pretty_string path reaches an unimplemented todo!() (in_array non-strict in php-shim)"] +#[ignore = "get_pretty_string path reaches an unimplemented stub (in_array non-strict in php-shim)"] #[test] fn test_unsatisfiable_requires() { let fixtures = set_up(); @@ -2027,7 +2027,7 @@ fn test_unsatisfiable_requires() { ); } -#[ignore = "get_pretty_string path reaches an unimplemented todo!() (Intervals::is_subset_of)"] +#[ignore = "get_pretty_string path reaches an unimplemented stub (Intervals::is_subset_of)"] #[test] fn test_require_mismatch_exception() { let fixtures = set_up(); diff --git a/crates/shirabe/tests/installed_versions_test.rs b/crates/shirabe/tests/installed_versions_test.rs index 6757e03..622a796 100644 --- a/crates/shirabe/tests/installed_versions_test.rs +++ b/crates/shirabe/tests/installed_versions_test.rs @@ -342,11 +342,12 @@ fn test_get_root_package() { assert_eq!(expected, InstalledVersions::get_root_package()); } -#[ignore = "InstalledVersions::get_raw_data not ported (only get_all_raw_data exists)"] +#[ignore] #[test] fn test_get_raw_data() { - let _root = set_up(); - todo!() + let (_root, dir) = set_up(); + + assert_eq!(fixture(&dir), InstalledVersions::get_raw_data()); } #[ignore] diff --git a/crates/shirabe/tests/installer/metapackage_installer_test.rs b/crates/shirabe/tests/installer/metapackage_installer_test.rs index 7766ed1..20fe8ea 100644 --- a/crates/shirabe/tests/installer/metapackage_installer_test.rs +++ b/crates/shirabe/tests/installer/metapackage_installer_test.rs @@ -13,13 +13,6 @@ use shirabe::repository::{InstalledArrayRepository, RepositoryInterface}; use crate::test_case::get_package; -/// Builds mocked repository/IO and a MetapackageInstaller over them. The mocks -/// are not available here; the live tests below intentionally diverge to use a -/// real InstalledArrayRepository instead, so this stub is not injected. -fn set_up() { - todo!() -} - fn run<F: std::future::Future>(future: F) -> F::Output { tokio::runtime::Builder::new_current_thread() .build() diff --git a/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs b/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs index 48f0fc5..aed0dc3 100644 --- a/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs +++ b/crates/shirabe/tests/package/archiver/archivable_files_finder_test.rs @@ -1,45 +1,235 @@ //! ref: composer/tests/Composer/Test/Package/Archiver/ArchivableFilesFinderTest.php -/// Builds a temp directory tree of fixture files under a unique tmp dir; the Filesystem and -/// getUniqueTmpDirectory infrastructure is not ported. -#[allow(dead_code)] -fn set_up() -> String { - todo!() -} +use shirabe::package::archiver::ArchivableFilesFinder; +use shirabe::util::Filesystem; +use shirabe_external_packages::composer::pcre::Preg; +use shirabe_php_shim::{dirname, file_put_contents, preg_quote}; +use tempfile::TempDir; -#[allow(dead_code)] -fn tear_down(_sources: &str) { - // Removes the temp directory tree created in set_up. - todo!() +struct SetUp { + _sources_dir: TempDir, + sources: String, + fs: Filesystem, } -#[allow(dead_code)] -struct TearDown { - sources: String, +fn set_up() -> SetUp { + let mut fs = Filesystem::new(None); + + let sources_dir = TempDir::new().unwrap(); + let sources = fs.normalize_path(&sources_dir.path().canonicalize().unwrap().to_string_lossy()); + + let file_tree = [ + ".foo", + "A/prefixA.foo", + "A/prefixB.foo", + "A/prefixC.foo", + "A/prefixD.foo", + "A/prefixE.foo", + "A/prefixF.foo", + "B/sub/prefixA.foo", + "B/sub/prefixB.foo", + "B/sub/prefixC.foo", + "B/sub/prefixD.foo", + "B/sub/prefixE.foo", + "B/sub/prefixF.foo", + "C/prefixA.foo", + "C/prefixB.foo", + "C/prefixC.foo", + "C/prefixD.foo", + "C/prefixE.foo", + "C/prefixF.foo", + "D/prefixA", + "D/prefixB", + "D/prefixC", + "D/prefixD", + "D/prefixE", + "D/prefixF", + "E/subtestA.foo", + "F/subtestA.foo", + "G/subtestA.foo", + "H/subtestA.foo", + "I/J/subtestA.foo", + "K/dirJ/subtestA.foo", + "toplevelA.foo", + "toplevelB.foo", + "prefixA.foo", + "prefixB.foo", + "prefixC.foo", + "prefixD.foo", + "prefixE.foo", + "prefixF.foo", + "parameters.yml", + "parameters.yml.dist", + "!important!.txt", + "!important_too!.txt", + "#weirdfile", + ]; + + for relative_path in file_tree { + let path = format!("{}/{}", sources, relative_path); + fs.ensure_directory_exists(&dirname(&path)).unwrap(); + file_put_contents(&path, b""); + } + + SetUp { + _sources_dir: sources_dir, + sources, + fs, + } } -impl Drop for TearDown { - fn drop(&mut self) { - tear_down(&self.sources); +fn get_archivable_files(set_up: &SetUp, finder: ArchivableFilesFinder) -> Vec<String> { + let mut files: Vec<String> = vec![]; + for file in finder { + if !file.is_dir() { + let real_path = file.canonicalize().unwrap(); + files.push(Preg::replace( + &format!("#^{}#", preg_quote(&set_up.sources, Some('#'))), + "", + &set_up.fs.normalize_path(&real_path.to_string_lossy()), + )); + } } + + files.sort(); + + files +} + +fn assert_archivable_files(set_up: &SetUp, finder: ArchivableFilesFinder, expected_files: &[&str]) { + let actual_files = get_archivable_files(set_up, finder); + + let expected_files: Vec<String> = expected_files.iter().map(|s| s.to_string()).collect(); + assert_eq!(expected_files, actual_files); } -// These set up a temp directory tree (including a git repo) and assert the files the finder -// selects with manual/git/skip excludes; the git-backed fixture setup is not ported. -#[ignore = "setUp needs TestCase::getUniqueTmpDirectory to build the on-disk fixture tree; not ported"] +// The manual exclude patterns (e.g. `.*`, `prefixC.*`) compile, via ComposerExcludeFilter, to +// look-ahead regexes like `(?=[^\.])...(?=$|/)`, which the regex crate cannot compile. +#[ignore = "ComposerExcludeFilter builds look-ahead regexes the regex crate does not support"] #[test] fn test_manual_excludes() { - todo!() + let set_up = set_up(); + + let excludes = vec![ + "prefixB.foo".to_string(), + "!/prefixB.foo".to_string(), + "/prefixA.foo".to_string(), + "prefixC.*".to_string(), + "!*/*/*/prefixC.foo".to_string(), + ".*".to_string(), + ]; + + let finder = ArchivableFilesFinder::new(&set_up.sources, excludes, false).unwrap(); + + assert_archivable_files( + &set_up, + finder, + &[ + "/!important!.txt", + "/!important_too!.txt", + "/#weirdfile", + "/A/prefixA.foo", + "/A/prefixD.foo", + "/A/prefixE.foo", + "/A/prefixF.foo", + "/B/sub/prefixA.foo", + "/B/sub/prefixC.foo", + "/B/sub/prefixD.foo", + "/B/sub/prefixE.foo", + "/B/sub/prefixF.foo", + "/C/prefixA.foo", + "/C/prefixD.foo", + "/C/prefixE.foo", + "/C/prefixF.foo", + "/D/prefixA", + "/D/prefixB", + "/D/prefixC", + "/D/prefixD", + "/D/prefixE", + "/D/prefixF", + "/E/subtestA.foo", + "/F/subtestA.foo", + "/G/subtestA.foo", + "/H/subtestA.foo", + "/I/J/subtestA.foo", + "/K/dirJ/subtestA.foo", + "/parameters.yml", + "/parameters.yml.dist", + "/prefixB.foo", + "/prefixD.foo", + "/prefixE.foo", + "/prefixF.foo", + "/toplevelA.foo", + "/toplevelB.foo", + ], + ); } -#[ignore = "setUp needs TestCase::getUniqueTmpDirectory plus skipIfNotExecutable/Process::fromShellCommandline/PharData/RecursiveIteratorIterator; none ported"] +// getArchivedFiles drives a git pipeline (Process::fromShellCommandline) and reads the produced +// zip back through PharData + RecursiveIteratorIterator; neither the process helper nor the zip +// archive reader is ported, so this case cannot run. +#[ignore = "getArchivedFiles needs a git process pipeline plus PharData/RecursiveIteratorIterator zip reading; not ported"] #[test] fn test_git_excludes() { todo!() } -#[ignore = "setUp needs TestCase::getUniqueTmpDirectory to build the on-disk fixture tree; not ported"] #[test] fn test_skip_excludes() { - todo!() + let set_up = set_up(); + + let excludes = vec!["prefixB.foo".to_string()]; + + let finder = ArchivableFilesFinder::new(&set_up.sources, excludes, true).unwrap(); + + assert_archivable_files( + &set_up, + finder, + &[ + "/!important!.txt", + "/!important_too!.txt", + "/#weirdfile", + "/.foo", + "/A/prefixA.foo", + "/A/prefixB.foo", + "/A/prefixC.foo", + "/A/prefixD.foo", + "/A/prefixE.foo", + "/A/prefixF.foo", + "/B/sub/prefixA.foo", + "/B/sub/prefixB.foo", + "/B/sub/prefixC.foo", + "/B/sub/prefixD.foo", + "/B/sub/prefixE.foo", + "/B/sub/prefixF.foo", + "/C/prefixA.foo", + "/C/prefixB.foo", + "/C/prefixC.foo", + "/C/prefixD.foo", + "/C/prefixE.foo", + "/C/prefixF.foo", + "/D/prefixA", + "/D/prefixB", + "/D/prefixC", + "/D/prefixD", + "/D/prefixE", + "/D/prefixF", + "/E/subtestA.foo", + "/F/subtestA.foo", + "/G/subtestA.foo", + "/H/subtestA.foo", + "/I/J/subtestA.foo", + "/K/dirJ/subtestA.foo", + "/parameters.yml", + "/parameters.yml.dist", + "/prefixA.foo", + "/prefixB.foo", + "/prefixC.foo", + "/prefixD.foo", + "/prefixE.foo", + "/prefixF.foo", + "/toplevelA.foo", + "/toplevelB.foo", + ], + ); } diff --git a/crates/shirabe/tests/package/archiver/archive_manager_test.rs b/crates/shirabe/tests/package/archiver/archive_manager_test.rs index 2712da6..fdc4bf0 100644 --- a/crates/shirabe/tests/package/archiver/archive_manager_test.rs +++ b/crates/shirabe/tests/package/archiver/archive_manager_test.rs @@ -29,31 +29,31 @@ impl Drop for TearDown { // the filename-derivation helpers over packages; the archiving and fixture setup are not // ported. #[test] -#[ignore = "setupPackage requires CompletePackage::setSourceType, not exposed on the package handle API; also needs Factory-built ArchiveManager (set_up) which is unported"] +#[ignore = "needs the Factory/FactoryMock-built ArchiveManager (createDownloadManager/createHttpDownloader/FactoryMock::createConfig) in set_up, which is unported"] fn test_unknown_format() { todo!() } #[test] -#[ignore = "needs setupGitRepo + PharData tar archiving and Factory-built ArchiveManager (set_up), and setupPackage's setSourceType which is not exposed on the package handle API"] +#[ignore = "needs setupGitRepo + PharData tar archiving and the Factory/FactoryMock-built ArchiveManager (set_up), which is unported"] fn test_archive_tar() { todo!() } #[test] -#[ignore = "needs setupGitRepo + PharData tar archiving and Factory-built ArchiveManager (set_up), and setupPackage's setSourceType which is not exposed on the package handle API"] +#[ignore = "needs setupGitRepo + PharData tar archiving and the Factory/FactoryMock-built ArchiveManager (set_up), which is unported"] fn test_archive_custom_file_name() { todo!() } #[test] -#[ignore = "setupPackage requires CompletePackage::setSourceType, not exposed on the package handle API; also needs Factory-built ArchiveManager (set_up) which is unported"] +#[ignore = "needs the Factory/FactoryMock-built ArchiveManager (createDownloadManager/createHttpDownloader/FactoryMock::createConfig) in set_up, which is unported"] fn test_get_package_filename_parts() { todo!() } #[test] -#[ignore = "setupPackage requires CompletePackage::setSourceType, not exposed on the package handle API; also needs Factory-built ArchiveManager (set_up) which is unported"] +#[ignore = "needs the Factory/FactoryMock-built ArchiveManager (createDownloadManager/createHttpDownloader/FactoryMock::createConfig) in set_up, which is unported"] fn test_get_package_filename() { todo!() } diff --git a/crates/shirabe/tests/package/archiver/phar_archiver_test.rs b/crates/shirabe/tests/package/archiver/phar_archiver_test.rs index 385177b..fe6185d 100644 --- a/crates/shirabe/tests/package/archiver/phar_archiver_test.rs +++ b/crates/shirabe/tests/package/archiver/phar_archiver_test.rs @@ -1,13 +1,130 @@ //! ref: composer/tests/Composer/Test/Package/Archiver/PharArchiverTest.php -#[ignore = "setUp/setupDummyRepo need TestCase::getUniqueTmpDirectory plus Filesystem to build the on-disk fixture tree; not ported"] +use shirabe::package::archiver::{ArchiverInterface, PharArchiver}; +use shirabe::package::handle::CompletePackageHandle; +use shirabe::util::{Filesystem, Platform}; +use shirabe_php_shim::{dirname, file_exists, file_put_contents, mkdir, realpath}; +use tempfile::TempDir; + +// ref: ArchiverTestCase. +struct ArchiverTestCase { + filesystem: Filesystem, + test_dir: String, + _test_dir_guard: TempDir, +} + +impl ArchiverTestCase { + fn set_up() -> Self { + let guard = TempDir::new().unwrap(); + let test_dir = guard.path().to_string_lossy().to_string(); + Self { + filesystem: Filesystem::new(None), + test_dir, + _test_dir_guard: guard, + } + } + + fn setup_package(&self) -> CompletePackageHandle { + let package = CompletePackageHandle::new( + "archivertest/archivertest".to_string(), + "master".to_string(), + "master".to_string(), + ); + package.set_source_url(Some(realpath(&self.test_dir).unwrap_or_default())); + package.set_source_reference(Some("master".to_string())); + package.__set_source_type(Some("git".to_string())); + + package + } + + fn setup_dummy_repo(&self) { + let current_work_dir = Platform::get_cwd(false).unwrap(); + std::env::set_current_dir(&self.test_dir).unwrap(); + + self.write_file("file.txt", "content", ¤t_work_dir); + self.write_file("foo/bar/baz", "content", ¤t_work_dir); + self.write_file("foo/bar/ignoreme", "content", ¤t_work_dir); + self.write_file("x/baz", "content", ¤t_work_dir); + self.write_file("x/includeme", "content", ¤t_work_dir); + + std::env::set_current_dir(¤t_work_dir).unwrap(); + } + + fn write_file(&self, path: &str, content: &str, current_work_dir: &str) { + if !file_exists(dirname(path)) { + mkdir(&dirname(path), 0o777, true); + } + + let result = file_put_contents(path, content.as_bytes()); + if result.is_none() { + std::env::set_current_dir(current_work_dir).unwrap(); + panic!("Could not save file."); + } + } +} + +#[ignore = "PharArchiver::archive builds the archive via PharData, which is todo!() in the php-shim"] #[test] fn test_tar_archive() { - todo!() + let mut test_case = ArchiverTestCase::set_up(); + + test_case.setup_dummy_repo(); + let package = test_case.setup_package(); + let target_dir = TempDir::new().unwrap(); + let target = format!( + "{}/composer_archiver_test.tar", + target_dir.path().to_string_lossy() + ); + + let archiver = PharArchiver::new(); + archiver + .archive( + package.get_source_url().unwrap(), + target.clone(), + "tar".to_string(), + vec![ + "foo/bar".to_string(), + "baz".to_string(), + "!/foo/bar/baz".to_string(), + ], + false, + ) + .unwrap(); + assert!(file_exists(&target)); + + test_case + .filesystem + .remove_directory(dirname(&target)) + .unwrap(); } -#[ignore = "setUp/setupDummyRepo need TestCase::getUniqueTmpDirectory plus Filesystem to build the on-disk fixture tree; not ported"] +#[ignore = "PharArchiver::archive builds the archive via PharData, which is todo!() in the php-shim"] #[test] fn test_zip_archive() { - todo!() + let mut test_case = ArchiverTestCase::set_up(); + + test_case.setup_dummy_repo(); + let package = test_case.setup_package(); + let target_dir = TempDir::new().unwrap(); + let target = format!( + "{}/composer_archiver_test.zip", + target_dir.path().to_string_lossy() + ); + + let archiver = PharArchiver::new(); + archiver + .archive( + package.get_source_url().unwrap(), + target.clone(), + "zip".to_string(), + vec![], + false, + ) + .unwrap(); + assert!(file_exists(&target)); + + test_case + .filesystem + .remove_directory(dirname(&target)) + .unwrap(); } diff --git a/crates/shirabe/tests/package/archiver/zip_archiver_test.rs b/crates/shirabe/tests/package/archiver/zip_archiver_test.rs index 5b51ce9..7785976 100644 --- a/crates/shirabe/tests/package/archiver/zip_archiver_test.rs +++ b/crates/shirabe/tests/package/archiver/zip_archiver_test.rs @@ -1,46 +1,174 @@ //! ref: composer/tests/Composer/Test/Package/Archiver/ZipArchiverTest.php -/// Creates the Filesystem/ProcessExecutor and a unique tmp testDir; that infrastructure is -/// not ported. Returns testDir. -#[allow(dead_code)] -fn set_up() -> String { - todo!() -} - -/// Unlinks the zip files collected in filesToCleanup, then (parent) removes testDir. -#[allow(dead_code)] -fn tear_down(_files_to_cleanup: &[String], _test_dir: &str) { - todo!() -} +use indexmap::IndexMap; +use shirabe::package::archiver::{ArchiverInterface, ZipArchiver}; +use shirabe::package::handle::CompletePackageHandle; +use shirabe::util::Platform; +use shirabe_php_shim::{ + ZipArchive, class_exists, dirname, file_exists, file_put_contents, mkdir, realpath, + sys_get_temp_dir, unlink, +}; +use tempfile::TempDir; -#[allow(dead_code)] -struct TearDown { - files_to_cleanup: Vec<String>, +// ref: ArchiverTestCase. Holds the unique tmp testDir plus the ZipArchiverTest cleanup list. +struct ArchiverTestCase { test_dir: String, + _test_dir_guard: TempDir, + files_to_cleanup: Vec<String>, } -impl Drop for TearDown { +impl Drop for ArchiverTestCase { fn drop(&mut self) { - tear_down(&self.files_to_cleanup, &self.test_dir); + for file in &self.files_to_cleanup { + unlink(file); + } } } -// ZipArchiver::archive builds a zip via ZipArchive, which is todo!() in the php-shim. +impl ArchiverTestCase { + fn set_up() -> Self { + let guard = TempDir::new().unwrap(); + let test_dir = guard.path().to_string_lossy().to_string(); + Self { + test_dir, + _test_dir_guard: guard, + files_to_cleanup: vec![], + } + } + + fn setup_package(&self) -> CompletePackageHandle { + let package = CompletePackageHandle::new( + "archivertest/archivertest".to_string(), + "master".to_string(), + "master".to_string(), + ); + package.set_source_url(Some(realpath(&self.test_dir).unwrap_or_default())); + package.set_source_reference(Some("master".to_string())); + package.__set_source_type(Some("git".to_string())); + + package + } + fn assert_zip_archive(&mut self, mut files: IndexMap<String, Option<String>>) { + if !class_exists("ZipArchive") { + // markTestSkipped('Cannot run ZipArchiverTest, missing class "ZipArchive".') + return; + } + + self.setup_dummy_repo(&mut files); + let package = self.setup_package(); + let target = format!("{}/composer_archiver_test.zip", sys_get_temp_dir()); + self.files_to_cleanup.push(target.clone()); + + let archiver = ZipArchiver::new(); + archiver + .archive( + package.get_source_url().unwrap(), + target.clone(), + "zip".to_string(), + vec![], + false, + ) + .unwrap(); + assert!(file_exists(&target)); + let mut zip = ZipArchive::new(); + let res = zip.open(&target, 0); + assert!(res.is_ok(), "Failed asserting that Zip file can be opened"); + + let mut zip_contents: IndexMap<String, Option<String>> = IndexMap::new(); + for i in 0..zip.num_files { + let path = zip.get_name_index(i); + zip_contents.insert(path.clone(), zip.get_from_name(&path)); + } + zip.close(); + + let files: IndexMap<String, Option<String>> = files; + assert_eq!( + files, zip_contents, + "Failed asserting that Zip created with the ZipArchiver contains all files from the repository." + ); + } + + fn setup_dummy_repo(&self, files: &mut IndexMap<String, Option<String>>) { + let current_work_dir = Platform::get_cwd(false).unwrap(); + std::env::set_current_dir(&self.test_dir).unwrap(); + let paths: Vec<String> = files.keys().cloned().collect(); + for path in paths { + if files[&path].is_none() { + files.insert(path.clone(), Some("content".to_string())); + } + self.write_file(&path, files[&path].clone().unwrap(), ¤t_work_dir); + } + + std::env::set_current_dir(¤t_work_dir).unwrap(); + } + + fn write_file(&self, path: &str, content: String, current_work_dir: &str) { + if !file_exists(dirname(path)) { + mkdir(&dirname(path), 0o777, true); + } + + let result = file_put_contents(path, content.as_bytes()); + if result.is_none() { + std::env::set_current_dir(current_work_dir).unwrap(); + panic!("Could not save file."); + } + } +} + +#[ignore = "ZipArchiver::archive and the ZipArchive reader (open/get_from_name/...) are todo!() in the php-shim"] #[test] -#[ignore = "ArchiverTestCase infrastructure (setup_package/setup_dummy_repo/test_dir) not ported; ZipArchive shim lacks get_from_name"] fn test_simple_files() { - todo!() + let mut test_case = ArchiverTestCase::set_up(); + + let mut files: IndexMap<String, Option<String>> = IndexMap::new(); + files.insert("file.txt".to_string(), None); + files.insert("foo/bar/baz".to_string(), None); + files.insert("x/baz".to_string(), None); + files.insert("x/includeme".to_string(), None); + + if !Platform::is_windows() { + files.insert( + format!("zfoo{}/file.txt", Platform::get_cwd(false).unwrap()), + None, + ); + } + + test_case.assert_zip_archive(files); } +#[ignore = "ZipArchiver::archive and the ZipArchive reader (open/get_from_name/...) are todo!() in the php-shim"] #[test] -#[ignore = "ArchiverTestCase infrastructure (setup_package/setup_dummy_repo/test_dir) not ported; ZipArchive shim lacks get_from_name"] fn test_gitignore_exclude_negation() { - todo!() + for include in ["!/docs", "!/docs/"] { + let mut test_case = ArchiverTestCase::set_up(); + + let mut files: IndexMap<String, Option<String>> = IndexMap::new(); + files.insert( + ".gitignore".to_string(), + Some(format!("/*\n.*\n!.git*\n{}", include)), + ); + files.insert("docs/README.md".to_string(), Some("# The doc".to_string())); + + test_case.assert_zip_archive(files); + } } +#[ignore = "ZipArchiver::archive and the ZipArchive reader (open/get_from_name/...) are todo!() in the php-shim"] #[test] -#[ignore = "ArchiverTestCase infrastructure (setup_package/setup_dummy_repo/test_dir) not ported; ZipArchive shim lacks get_from_name"] fn test_folder_with_backslashes() { - todo!() + if Platform::is_windows() { + // markTestSkipped('Folder names cannot contain backslashes on Windows.') + return; + } + + let mut test_case = ArchiverTestCase::set_up(); + + let mut files: IndexMap<String, Option<String>> = IndexMap::new(); + files.insert( + "folder\\with\\backslashes/README.md".to_string(), + Some("# doc".to_string()), + ); + + test_case.assert_zip_archive(files); } diff --git a/crates/shirabe/tests/package/base_package_test.rs b/crates/shirabe/tests/package/base_package_test.rs index 1056de1..16d3a5b 100644 --- a/crates/shirabe/tests/package/base_package_test.rs +++ b/crates/shirabe/tests/package/base_package_test.rs @@ -1,7 +1,10 @@ //! ref: composer/tests/Composer/Test/Package/BasePackageTest.php +use shirabe::package::DisplayMode; use shirabe::package::base_package::package_names_to_regexp; +use shirabe::package::handle::PackageHandle; use shirabe::repository::{ArrayRepository, RepositoryInterfaceHandle}; +use shirabe_semver::version_parser::VersionParser; use crate::test_case::get_package; @@ -31,14 +34,40 @@ fn test_set_another_repository() { assert!(package.set_repository(repository2.clone()).is_err()); } -// In PHP this mocks isDev()/getSourceType()/getPrettyVersion()/getSourceReference() -// on an abstract BasePackage to drive getFullPrettyVersion(). Reproducing those exact -// getter values requires mocking; a real package cannot carry the pretty version -// "PrettyVersion" together with isDev() == true. -#[ignore = "requires mocking isDev/getSourceType/getPrettyVersion/getSourceReference on an abstract BasePackage; no mock infrastructure exists and a real package cannot carry prettyVersion \"PrettyVersion\" with isDev() == true"] +// PHP mocks isDev()/getSourceType()/getPrettyVersion()/getSourceReference() on an abstract +// BasePackage. A real Package reproduces the same observable getters: a "dev-master" version makes +// isDev() true, while the pretty version is an independent constructor argument. #[test] fn test_format_version_for_dev_package() { - todo!() + let cases: Vec<(&str, bool, &str)> = vec![ + ("v2.1.0-RC2", true, "PrettyVersion v2.1.0-RC2"), + ( + "bbf527a27356414bfa9bf520f018c5cb7af67c77", + true, + "PrettyVersion bbf527a", + ), + ("v1.0.0", false, "PrettyVersion v1.0.0"), + ( + "bbf527a27356414bfa9bf520f018c5cb7af67c77", + false, + "PrettyVersion bbf527a27356414bfa9bf520f018c5cb7af67c77", + ), + ]; + + for (source_reference, truncate, expected) in cases { + let package = PackageHandle::new( + "dummy/pkg".to_string(), + VersionParser.normalize("dev-master", None).unwrap(), + "PrettyVersion".to_string(), + ); + package.__set_source_type(Some("git".to_string())); + package.set_source_reference(Some(source_reference.to_string())); + + assert_eq!( + expected, + package.get_full_pretty_version(truncate, DisplayMode::SourceRefIfDev) + ); + } } #[test] diff --git a/crates/shirabe/tests/package/dumper/array_dumper_test.rs b/crates/shirabe/tests/package/dumper/array_dumper_test.rs index 64880f1..1e5fd05 100644 --- a/crates/shirabe/tests/package/dumper/array_dumper_test.rs +++ b/crates/shirabe/tests/package/dumper/array_dumper_test.rs @@ -79,11 +79,16 @@ fn test_dump_abandoned_replacement() { ); } -// PHP drives ~25 heterogeneous setters dynamically (set{ucfirst(method)}) across strings, -// arrays, a DateTime and Link maps, then checks the corresponding dumped key. Reproducing -// that dynamic dispatch faithfully would require per-case wiring for every property type. +// PHP drives 26 heterogeneous setters dynamically (set{ucfirst(method)}) across strings, +// arrays, a DateTime and Link maps, then checks the corresponding dumped key. Three of the +// data sets cannot be expressed faithfully because the ported production types were narrowed +// from PHP's loose `array`: the `authors` set passes plain strings (Rust set_authors wants +// Vec<IndexMap<String,String>>), `scripts` passes a bare string value (set_scripts wants +// IndexMap<String,Vec<String>>), and `funding` passes a single map (set_funding wants +// Vec<IndexMap<String,PhpMixed>>); the dumper additionally re-wraps each of these. Porting the +// remaining 23 would drop those three cases, so testKeys stays unported (all-or-nothing). #[test] -#[ignore = "RootPackageHandle lacks set_type/set_release_date/set_binaries/set_php_ext setters required by the data provider's type/time/bin/php-ext cases"] +#[ignore = "authors/scripts/funding data sets pass loosely-typed PHP arrays the narrowed Rust set_authors/set_scripts/set_funding types cannot represent, and the dumper re-wraps them; faithful all-or-nothing port blocked without loosening those production types"] fn test_keys() { todo!() } diff --git a/crates/shirabe/tests/platform/hhvm_detector_test.rs b/crates/shirabe/tests/platform/hhvm_detector_test.rs index 12a966a..f0a8550 100644 --- a/crates/shirabe/tests/platform/hhvm_detector_test.rs +++ b/crates/shirabe/tests/platform/hhvm_detector_test.rs @@ -4,7 +4,7 @@ use shirabe::platform::hhvm_detector::HhvmDetector; use shirabe::util::Platform; use shirabe::util::ProcessExecutor; use shirabe_external_packages::symfony::process::ExecutableFinder; -use shirabe_php_shim::{PhpMixed, defined}; +use shirabe_php_shim::{PhpMixed, constant, defined}; use shirabe_semver::version_parser::VersionParser; fn set_up() -> HhvmDetector { @@ -13,11 +13,16 @@ fn set_up() -> HhvmDetector { hhvm_detector } +#[ignore] #[test] -#[ignore = "requires HHVM_VERSION_ID constant, which the shim does not define"] fn test_hhvm_version_when_executing_in_hhvm() { - let _hhvm_detector = set_up(); - todo!() + let mut hhvm_detector = set_up(); + if !defined("HHVM_VERSION_ID") { + // markTestSkipped('Not running with HHVM') + return; + } + let version = hhvm_detector.get_version(); + assert_eq!(version_id_to_version(), version); } #[ignore] @@ -66,3 +71,17 @@ fn test_hhvm_version_when_executing_in_php() { VersionParser.normalize(&detected_version, None).unwrap() ); } + +fn version_id_to_version() -> Option<String> { + if !defined("HHVM_VERSION_ID") { + return None; + } + + let hhvm_version_id = constant("HHVM_VERSION_ID").as_int().unwrap(); + Some(format!( + "{}.{}.{}", + hhvm_version_id / 10000, + (hhvm_version_id / 100) % 100, + hhvm_version_id % 100, + )) +} diff --git a/crates/shirabe/tests/question/strict_confirmation_question_test.rs b/crates/shirabe/tests/question/strict_confirmation_question_test.rs index c59a0fd..de8911b 100644 --- a/crates/shirabe/tests/question/strict_confirmation_question_test.rs +++ b/crates/shirabe/tests/question/strict_confirmation_question_test.rs @@ -1,23 +1,138 @@ //! ref: composer/tests/Composer/Test/Question/StrictConfirmationQuestionTest.php -// These drive StrictConfirmationQuestion through Symfony's QuestionHelper::ask using -// ArrayInput/StreamOutput, none of which are ported. The question's normalizer and -// validator are private, so they cannot be exercised directly either. +use std::cell::RefCell; +use std::rc::Rc; + +use shirabe::question::StrictConfirmationQuestion; +use shirabe_external_packages::symfony::console::helper::question_helper::QuestionHelper; +use shirabe_external_packages::symfony::console::input::array_input::ArrayInput; +use shirabe_external_packages::symfony::console::input::streamable_input_interface::StreamableInputInterface; +use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface; +use shirabe_external_packages::symfony::console::output::stream_output::StreamOutput; +use shirabe_php_shim::PhpMixed; + +const TRUE_ANSWER_REGEX: &str = "/^y(?:es)?$/i"; +const FALSE_ANSWER_REGEX: &str = "/^no?$/i"; + +/// @return string[][] +fn get_ask_confirmation_bad_data() -> Vec<&'static str> { + vec!["not correct", "no more", "yes please", "yellow"] +} #[test] -#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"] fn test_ask_confirmation_bad_answer() { - todo!() + for answer in get_ask_confirmation_bad_data() { + let (mut input, mut dialog) = create_input(&format!("{}\n", answer)); + + let mut question = StrictConfirmationQuestion::new( + "Do you like French fries?".to_string(), + true, + TRUE_ANSWER_REGEX.to_string(), + FALSE_ANSWER_REGEX.to_string(), + ); + question.inner_mut().set_max_attempts(Some(1)).unwrap(); + + let error = dialog + .ask(&mut input, create_output_interface(), question.inner()) + .expect_err("expected an InvalidArgumentException"); + assert_eq!(error.to_string(), "Please answer yes, y, no, or n."); + } } #[test] -#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"] fn test_ask_confirmation() { - todo!() + for (question, expected, default) in get_ask_confirmation_data() { + let (mut input, mut dialog) = create_input(&format!("{}\n", question)); + + let question = StrictConfirmationQuestion::new( + "Do you like French fries?".to_string(), + default, + TRUE_ANSWER_REGEX.to_string(), + FALSE_ANSWER_REGEX.to_string(), + ); + assert_eq!( + dialog + .ask(&mut input, create_output_interface(), question.inner()) + .unwrap() + .unwrap(), + PhpMixed::Bool(expected), + "confirmation question should {}", + if expected { "pass" } else { "cancel" } + ); + } +} + +/// @return mixed[][] +fn get_ask_confirmation_data() -> Vec<(&'static str, bool, bool)> { + vec![ + ("", true, true), + ("", false, false), + ("y", true, true), + ("yes", true, true), + ("n", false, true), + ("no", false, true), + ] } #[test] -#[ignore = "ArrayInput exposes no public set_stream (its inner Input is pub(crate)); fopen now returns a PhpResource that bridges to StreamOutput::new, but the input-stream wiring is still missing"] fn test_ask_confirmation_with_custom_true_and_false_answer() { - todo!() + let question = StrictConfirmationQuestion::new( + "Do you like French fries?".to_string(), + false, + "/^ja$/i".to_string(), + "/^nein$/i".to_string(), + ); + + let (mut input, mut dialog) = create_input("ja\n"); + assert_eq!( + dialog + .ask(&mut input, create_output_interface(), question.inner()) + .unwrap() + .unwrap(), + PhpMixed::Bool(true) + ); + + let (mut input, mut dialog) = create_input("nein\n"); + assert_eq!( + dialog + .ask(&mut input, create_output_interface(), question.inner()) + .unwrap() + .unwrap(), + PhpMixed::Bool(false) + ); +} + +/// @return resource +fn get_input_stream(input: &str) -> shirabe_php_shim::PhpResource { + let stream = shirabe_php_shim::php_fopen_resource("php://memory", "r+"); + + shirabe_php_shim::fwrite_resource(&stream, input); + shirabe_php_shim::rewind(&stream); + + stream +} + +fn create_output_interface() -> Rc<RefCell<dyn OutputInterface>> { + let stream = shirabe_php_shim::php_fopen_resource("php://memory", "r+"); + let output = StreamOutput::new(stream, None, None, None) + .unwrap() + .expect("php://memory is a valid stream"); + Rc::new(RefCell::new(output)) +} + +/// @return array{ArrayInput, QuestionHelper} +fn create_input(entry: &str) -> (ArrayInput, QuestionHelper) { + let mut input = ArrayInput::new( + vec![( + PhpMixed::Int(0), + PhpMixed::String("--no-interaction".to_string()), + )], + None, + ) + .unwrap(); + input.set_stream(get_input_stream(entry)); + + let dialog = QuestionHelper::default(); + + (input, dialog) } diff --git a/crates/shirabe/tests/repository/artifact_repository_test.rs b/crates/shirabe/tests/repository/artifact_repository_test.rs index 765bdac..63fd238 100644 --- a/crates/shirabe/tests/repository/artifact_repository_test.rs +++ b/crates/shirabe/tests/repository/artifact_repository_test.rs @@ -8,11 +8,14 @@ use shirabe::io::{IOInterface, NullIO}; use shirabe::repository::ArtifactRepository; use shirabe_php_shim::{PhpMixed, extension_loaded}; -fn set_up() { +/// Returns true when the test should be skipped because the zip extension is +/// unavailable, mirroring PHP's markTestSkipped in setUp. +fn set_up() -> bool { if !extension_loaded("zip") { // markTestSkipped('You need the zip extension to run this test.') - todo!() + return true; } + false } fn artifacts_dir() -> String { @@ -34,7 +37,9 @@ fn create_repo(url: &str) -> ArtifactRepository { #[test] #[ignore] fn test_extracts_configs_from_zip_archives() { - set_up(); + if set_up() { + return; + } let mut expected_packages = vec![ "vendor0/package0-0.0.1".to_string(), @@ -82,7 +87,9 @@ fn test_extracts_configs_from_zip_archives() { #[test] #[ignore] fn test_absolute_repo_url_creates_absolute_url_packages() { - set_up(); + if set_up() { + return; + } let absolute_path = artifacts_dir(); let mut repo = create_repo(&absolute_path); @@ -101,7 +108,9 @@ fn test_absolute_repo_url_creates_absolute_url_packages() { #[test] #[ignore] fn test_relative_repo_url_creates_relative_url_packages() { - set_up(); + if set_up() { + return; + } let relative_path = "tests/Composer/Test/Repository/Fixtures/artifacts"; let mut repo = create_repo(relative_path); diff --git a/crates/shirabe/tests/util/filesystem_test.rs b/crates/shirabe/tests/util/filesystem_test.rs index f5896b0..e0bb46b 100644 --- a/crates/shirabe/tests/util/filesystem_test.rs +++ b/crates/shirabe/tests/util/filesystem_test.rs @@ -9,50 +9,9 @@ use shirabe_php_shim::{ dirname, file_exists, file_put_contents, is_dir, is_file, mkdir, symlink, touch, }; -#[allow(dead_code)] -struct SetUp { - fs: Filesystem, - working_dir: String, - test_file: String, -} - -#[allow(dead_code)] -fn set_up() -> SetUp { - let fs = Filesystem::new(None); - // getUniqueTmpDirectory is base TestCase infrastructure that is not ported. - let working_dir: String = todo!(); - #[allow(unreachable_code)] - let unique_tmp: String = todo!(); - #[allow(unreachable_code)] - let test_file: String = format!("{unique_tmp}/composer_test_file"); - #[allow(unreachable_code)] - SetUp { - fs, - working_dir, - test_file, - } -} - -#[allow(dead_code)] -fn tear_down(set_up: &mut SetUp) { - if is_dir(&set_up.working_dir) { - let _ = set_up.fs.remove_directory(&set_up.working_dir); - } - if is_file(&set_up.test_file) { - let _ = set_up.fs.remove_directory(dirname(&set_up.test_file)); - } -} - -#[allow(dead_code)] -struct TearDown { - set_up: SetUp, -} - -impl Drop for TearDown { - fn drop(&mut self) { - tear_down(&mut self.set_up); - } -} +// PHP's setUp/tearDown build workingDir/testFile under TestCase::getUniqueTmpDirectory; the +// on-disk tests below instead create their own tempfile::TempDir inline, so no shared fixture +// helper is needed. /// providePathCouplesAsCode: (a, b, directory, expected, static, prefer_relative) fn provide_path_couples_as_code() diff --git a/crates/shirabe/tests/util/http/proxy_manager_test.rs b/crates/shirabe/tests/util/http/proxy_manager_test.rs index dede524..7beafe1 100644 --- a/crates/shirabe/tests/util/http/proxy_manager_test.rs +++ b/crates/shirabe/tests/util/http/proxy_manager_test.rs @@ -38,11 +38,32 @@ impl Drop for TearDown { } #[test] -#[ignore = "ProxyManager::get_instance returns a shared &'static Mutex, so instance identity (=== / after reset) cannot be expressed through the public API"] +#[serial_test::serial] fn test_instantiation() { let _tear_down = TearDown; set_up(); - todo!() + + // PHP compares object identity (===); the value-based Rust singleton exposes a per-instance + // generation id instead, which changes only when a new ProxyManager is constructed. + let original_instance = { + let mutex = ProxyManager::get_instance(); + let guard = mutex.lock().unwrap(); + guard.as_ref().unwrap().__generation() + }; + let same_instance = { + let mutex = ProxyManager::get_instance(); + let guard = mutex.lock().unwrap(); + guard.as_ref().unwrap().__generation() + }; + assert_eq!(original_instance, same_instance); + + ProxyManager::reset(); + let new_instance = { + let mutex = ProxyManager::get_instance(); + let guard = mutex.lock().unwrap(); + guard.as_ref().unwrap().__generation() + }; + assert_ne!(same_instance, new_instance); } #[test] diff --git a/crates/shirabe/tests/util/stream_context_factory_test.rs b/crates/shirabe/tests/util/stream_context_factory_test.rs index 23d4392..6491883 100644 --- a/crates/shirabe/tests/util/stream_context_factory_test.rs +++ b/crates/shirabe/tests/util/stream_context_factory_test.rs @@ -7,7 +7,33 @@ use indexmap::IndexMap; use shirabe::util::http::proxy_manager::ProxyManager; use shirabe::util::platform::Platform; use shirabe::util::stream_context_factory::StreamContextFactory; -use shirabe_php_shim::{PhpMixed, implode, stripos}; +use shirabe_php_shim::{ + PhpMixed, base64_encode, extension_loaded, implode, stream_context_get_options, stripos, +}; + +fn s(value: &str) -> PhpMixed { + PhpMixed::String(value.to_string()) +} + +fn arr(entries: Vec<(&str, PhpMixed)>) -> PhpMixed { + PhpMixed::Array( + entries + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + ) +} + +fn list(items: Vec<PhpMixed>) -> PhpMixed { + PhpMixed::List(items) +} + +fn map(entries: Vec<(&str, PhpMixed)>) -> IndexMap<String, PhpMixed> { + entries + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect() +} fn set_up() { Platform::clear_env("HTTP_PROXY"); @@ -37,8 +63,11 @@ impl Drop for TearDown { } } +// PHP's dataGetContext second data set passes a `notification` closure in both the default and +// expected params; PhpMixed has no closure variant, so that data set (and thus the all-or-nothing +// testGetContext) cannot be expressed. #[test] -#[ignore = "stream_context_get_options/stream_context_get_params shim functions do not exist; cannot read back created context to assert"] +#[ignore = "dataGetContext passes a notification closure in params; PhpMixed cannot represent a PHP closure, so the data set is unportable"] fn test_get_context() { let _tear_down = TearDown; set_up(); @@ -46,67 +75,299 @@ fn test_get_context() { } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_http_proxy() { let _tear_down = TearDown; set_up(); - todo!() + Platform::put_env( + "http_proxy", + "http://username:p%40ssword@proxyserver.net:3128/", + ); + Platform::put_env("HTTP_PROXY", "http://proxyserver/"); + + let default_options = map(vec![( + "http", + arr(vec![("method", s("GET")), ("header", s("User-Agent: foo"))]), + )]); + let context = + StreamContextFactory::get_context("http://example.org", default_options, IndexMap::new()) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected = map(vec![( + "http", + arr(vec![ + ("proxy", s("tcp://proxyserver.net:3128")), + ("request_fulluri", PhpMixed::Bool(true)), + ("method", s("GET")), + ( + "header", + list(vec![ + s("User-Agent: foo"), + s(&format!( + "Proxy-Authorization: Basic {}", + base64_encode("username:p@ssword") + )), + ]), + ), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ]), + )]); + assert_eq!(expected, options); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_http_proxy_with_no_proxy() { let _tear_down = TearDown; set_up(); - todo!() + Platform::put_env( + "http_proxy", + "http://username:password@proxyserver.net:3128/", + ); + Platform::put_env("no_proxy", "foo,example.org"); + + let default_options = map(vec![( + "http", + arr(vec![("method", s("GET")), ("header", s("User-Agent: foo"))]), + )]); + let context = + StreamContextFactory::get_context("http://example.org", default_options, IndexMap::new()) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected = map(vec![( + "http", + arr(vec![ + ("method", s("GET")), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ("header", list(vec![s("User-Agent: foo")])), + ]), + )]); + assert_eq!(expected, options); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_http_proxy_with_no_proxy_wildcard() { let _tear_down = TearDown; set_up(); - todo!() + Platform::put_env( + "http_proxy", + "http://username:password@proxyserver.net:3128/", + ); + Platform::put_env("no_proxy", "*"); + + let default_options = map(vec![( + "http", + arr(vec![("method", s("GET")), ("header", s("User-Agent: foo"))]), + )]); + let context = + StreamContextFactory::get_context("http://example.org", default_options, IndexMap::new()) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected = map(vec![( + "http", + arr(vec![ + ("method", s("GET")), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ("header", list(vec![s("User-Agent: foo")])), + ]), + )]); + assert_eq!(expected, options); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_options_are_preserved() { let _tear_down = TearDown; set_up(); - todo!() + Platform::put_env( + "http_proxy", + "http://username:password@proxyserver.net:3128/", + ); + + let default_options = map(vec![( + "http", + arr(vec![ + ("method", s("GET")), + ("header", list(vec![s("User-Agent: foo"), s("X-Foo: bar")])), + ("request_fulluri", PhpMixed::Bool(false)), + ]), + )]); + let context = + StreamContextFactory::get_context("http://example.org", default_options, IndexMap::new()) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected = map(vec![( + "http", + arr(vec![ + ("proxy", s("tcp://proxyserver.net:3128")), + ("request_fulluri", PhpMixed::Bool(false)), + ("method", s("GET")), + ( + "header", + list(vec![ + s("User-Agent: foo"), + s("X-Foo: bar"), + s(&format!( + "Proxy-Authorization: Basic {}", + base64_encode("username:password") + )), + ]), + ), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ]), + )]); + assert_eq!(expected, options); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_http_proxy_without_port() { let _tear_down = TearDown; set_up(); - todo!() + Platform::put_env("https_proxy", "http://username:password@proxyserver.net"); + + let default_options = map(vec![( + "http", + arr(vec![("method", s("GET")), ("header", s("User-Agent: foo"))]), + )]); + let context = + StreamContextFactory::get_context("https://example.org", default_options, IndexMap::new()) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected = map(vec![( + "http", + arr(vec![ + ("proxy", s("tcp://proxyserver.net:80")), + ("method", s("GET")), + ( + "header", + list(vec![ + s("User-Agent: foo"), + s(&format!( + "Proxy-Authorization: Basic {}", + base64_encode("username:password") + )), + ]), + ), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ]), + )]); + assert_eq!(expected, options); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_https_proxy_override() { let _tear_down = TearDown; set_up(); - todo!() + if !extension_loaded("openssl") { + // markTestSkipped('Requires openssl') + return; + } + + Platform::put_env("http_proxy", "http://username:password@proxyserver.net"); + Platform::put_env("https_proxy", "https://woopproxy.net"); + + // Pointless test replaced by ProxyHelperTest.php + // expectException('Composer\Downloader\TransportException') + let result = StreamContextFactory::get_context( + "https://example.org", + map(vec![( + "http", + arr(vec![("method", s("GET")), ("header", s("User-Agent: foo"))]), + )]), + IndexMap::new(), + ); + assert!(result.is_err()); } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_ssl_proxy() { let _tear_down = TearDown; - set_up(); - todo!() + for (expected, proxy) in [ + ("ssl://proxyserver:443", "https://proxyserver/"), + ("ssl://proxyserver:8443", "https://proxyserver:8443"), + ] { + set_up(); + Platform::put_env("http_proxy", proxy); + + if extension_loaded("openssl") { + let context = StreamContextFactory::get_context( + "http://example.org", + map(vec![("http", arr(vec![("header", s("User-Agent: foo"))]))]), + IndexMap::new(), + ) + .unwrap(); + let options = stream_context_get_options(&context); + + let expected_options = map(vec![( + "http", + arr(vec![ + ("proxy", s(expected)), + ("request_fulluri", PhpMixed::Bool(true)), + ("max_redirects", PhpMixed::Int(20)), + ("follow_location", PhpMixed::Int(1)), + ("header", list(vec![s("User-Agent: foo")])), + ]), + )]); + assert_eq!(expected_options, options); + } else { + match StreamContextFactory::get_context( + "http://example.org", + IndexMap::new(), + IndexMap::new(), + ) { + // The catch in PHP asserts the exception is a TransportException; the return type + // here already guarantees that. + Ok(_) => panic!(), + Err(_) => {} + } + } + } } #[test] -#[ignore = "stream_context_get_options shim function does not exist; cannot read back created context to assert"] +#[ignore] fn test_ensure_thatfix_http_header_field_moves_content_type_to_end_of_options() { let _tear_down = TearDown; set_up(); - todo!() + let options = map(vec![( + "http", + arr(vec![( + "header", + s( + "User-agent: foo\r\nX-Foo: bar\r\nContent-Type: application/json\r\nAuthorization: Basic aW52YWxpZA==", + ), + )]), + )]); + let expected_header = vec![ + s("User-agent: foo"), + s("X-Foo: bar"), + s("Authorization: Basic aW52YWxpZA=="), + s("Content-Type: application/json"), + ]; + let context = + StreamContextFactory::get_context("http://example.org", options, IndexMap::new()).unwrap(); + let ctxoptions = stream_context_get_options(&context); + let ctx_header = ctxoptions + .get("http") + .and_then(|v| v.as_array()) + .and_then(|a| a.get("header")) + .and_then(|v| v.as_list()) + .unwrap(); + assert_eq!(expected_header.last().unwrap(), ctx_header.last().unwrap()); } #[test] |
