diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-22 23:42:07 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-22 23:42:07 +0900 |
| commit | 5ab5f3b316798c1411ce8e6a7f5b091fda93589c (patch) | |
| tree | c7669f94f5d9bd9adc07bc1aab3f14cfdec1ae5f /crates | |
| parent | b291e714bc739262140323e08fe2fb9e91e00ee7 (diff) | |
| download | php-shirabe-5ab5f3b316798c1411ce8e6a7f5b091fda93589c.tar.gz php-shirabe-5ab5f3b316798c1411ce8e6a7f5b091fda93589c.tar.zst php-shirabe-5ab5f3b316798c1411ce8e6a7f5b091fda93589c.zip | |
test: port previously-ignored Composer tests via __ test hatches
Re-evaluate the reason'd #[ignore] tests under the Phase D criterion:
a test is unportable ONLY if the APIs/types needed to WRITE it do not
exist. A test that compiles but panics at runtime (todo!() body, a
regex the regex crate cannot compile) or fails at runtime (incomplete
or incorrect impl behavior) is portable -- it is written in full and
marked with a reason-less #[ignore].
About 120 test functions move from reason'd #[ignore] to reason-less
#[ignore] (the ported-but-not-yet-passing signal). Impl crates gain
only additive __ test hatches (init_command, pool, file_downloader,
package handle link setters, artifact/path repository, repository
manager, svn); no existing logic changes. Tests whose required APIs
genuinely do not exist (mock/reflection harness, ApplicationTester,
solve() discarding SolverProblemsException, a script::Event that
cannot be passed as an originating event) keep their reason'd
#[ignore].
cargo check -p shirabe --tests passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
45 files changed, 3669 insertions, 243 deletions
diff --git a/crates/shirabe/src/command/init_command.rs b/crates/shirabe/src/command/init_command.rs index 22cd75b..f63014c 100644 --- a/crates/shirabe/src/command/init_command.rs +++ b/crates/shirabe/src/command/init_command.rs @@ -1075,6 +1075,31 @@ impl InitCommand { file_put_contents(ignore_file, format!("{}{}\n", contents, vendor).as_bytes()); } + /// For testing only: invoke the private `parse_author_string`. + pub fn __parse_author_string(&self, author: &str) -> Result<IndexMap<String, Option<String>>> { + self.parse_author_string(author) + } + + /// For testing only: invoke the crate-private `format_authors`. + pub fn __format_authors(&self, author: &str) -> Result<Vec<IndexMap<String, PhpMixed>>> { + self.format_authors(author) + } + + /// For testing only: invoke the crate-private `get_git_config`. + pub fn __get_git_config(&mut self) -> IndexMap<String, String> { + self.get_git_config() + } + + /// For testing only: invoke the crate-private `has_vendor_ignore`. + pub fn __has_vendor_ignore(&self, ignore_file: &str, vendor: &str) -> bool { + self.has_vendor_ignore(ignore_file, vendor) + } + + /// For testing only: invoke the crate-private `add_vendor_ignore`. + pub fn __add_vendor_ignore(&self, ignore_file: &str, vendor: &str) { + self.add_vendor_ignore(ignore_file, vendor) + } + pub(crate) fn is_valid_email(&self, email: &str) -> bool { shirabe_php_shim::filter_var_email(email) } diff --git a/crates/shirabe/src/dependency_resolver/pool.rs b/crates/shirabe/src/dependency_resolver/pool.rs index b25f348..3ea7835 100644 --- a/crates/shirabe/src/dependency_resolver/pool.rs +++ b/crates/shirabe/src/dependency_resolver/pool.rs @@ -379,6 +379,12 @@ impl Pool { pub fn get_unacceptable_fixed_or_locked_packages(&self) -> &Vec<BasePackageHandle> { &self.unacceptable_fixed_or_locked_packages } + + /// For testing only: mirrors PHP `Pool implements \Countable` (`count($pool)`), + /// returning how many packages have been loaded into the pool. + pub fn __count(&self) -> i64 { + self.packages.len() as i64 + } } impl fmt::Display for Pool { diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs index 702a58f..6b2162a 100644 --- a/crates/shirabe/src/downloader/file_downloader.rs +++ b/crates/shirabe/src/downloader/file_downloader.rs @@ -771,6 +771,16 @@ impl FileDownloader { String::new() } + /// For testing only: invoke the crate-private `get_file_name`. + pub fn __get_file_name(&self, package: PackageInterfaceHandle, path: &str) -> String { + self.get_file_name(package, path) + } + + /// For testing only: invoke the crate-private `process_url`. + pub fn __process_url(&self, package: PackageInterfaceHandle, url: &str) -> Result<String> { + self.process_url(package, url) + } + /// Process the download url pub(crate) fn process_url(&self, package: PackageInterfaceHandle, url: &str) -> Result<String> { if !shirabe_php_shim::extension_loaded("openssl") && Some(0) == strpos(url, "https:") { diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs index 5df4fc0..303983a 100644 --- a/crates/shirabe/src/package/handle.rs +++ b/crates/shirabe/src/package/handle.rs @@ -67,6 +67,17 @@ impl AnyPackage { } } + /// For testing only: reach the base `Package` of a real package variant. + /// Crate-private; the public `__set_*` test hatches are built on top of it. + pub(crate) fn as_package_mut(&mut self) -> Option<&mut Package> { + match self { + Self::Package(p) => Some(p), + Self::CompletePackage(p) => Some(&mut p.inner), + Self::RootPackage(p) => Some(&mut p.inner.inner), + _ => None, + } + } + pub fn as_root_package_interface(&self) -> Option<&dyn RootPackageInterface> { match self { Self::RootPackage(p) => Some(p), @@ -1119,6 +1130,97 @@ macro_rules! impl_root_package_interface_handle { }; } +macro_rules! impl_real_package_test_setters { + ($Handle:ty) => { + impl $Handle { + /// For testing only: mirrors PHP `Package::setRequires`. + pub fn __set_requires( + &self, + requires: indexmap::IndexMap<String, crate::package::Link>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_requires(requires); + } + + /// For testing only: mirrors PHP `Package::setDevRequires`. + pub fn __set_dev_requires( + &self, + dev_requires: indexmap::IndexMap<String, crate::package::Link>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_dev_requires(dev_requires); + } + + /// For testing only: mirrors PHP `Package::setConflicts`. + pub fn __set_conflicts( + &self, + conflicts: indexmap::IndexMap<String, crate::package::Link>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_conflicts(conflicts); + } + + /// For testing only: mirrors PHP `Package::setProvides`. + pub fn __set_provides( + &self, + provides: indexmap::IndexMap<String, crate::package::Link>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_provides(provides); + } + + /// For testing only: mirrors PHP `Package::setReplaces`. + pub fn __set_replaces( + &self, + replaces: indexmap::IndexMap<String, crate::package::Link>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_replaces(replaces); + } + + /// For testing only: mirrors PHP `Package::setExtra`. + pub fn __set_extra( + &self, + extra: indexmap::IndexMap<String, shirabe_php_shim::PhpMixed>, + ) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_extra(extra); + } + + /// For testing only: mirrors PHP `Package::setType`. + pub fn __set_type(&self, r#type: String) { + self.0 + .borrow_mut() + .as_package_mut() + .expect("real package handle invariant") + .set_type(r#type); + } + } + }; +} + +impl_real_package_test_setters!(PackageHandle); +impl_real_package_test_setters!(CompletePackageHandle); +impl_real_package_test_setters!(RootPackageHandle); + macro_rules! impl_handle_common { ($Handle:ty) => { impl $Handle { diff --git a/crates/shirabe/src/repository/artifact_repository.rs b/crates/shirabe/src/repository/artifact_repository.rs index b913edb..9a28d65 100644 --- a/crates/shirabe/src/repository/artifact_repository.rs +++ b/crates/shirabe/src/repository/artifact_repository.rs @@ -65,6 +65,15 @@ impl ArtifactRepository { format!("artifact repo ({})", self.lookup) } + /// For testing only: drives `initialize` (scanning the lookup directory) and returns the + /// packages collected by the inner `ArrayRepository`, mirroring the polymorphic + /// `RepositoryInterface::getPackages` dispatch in PHP. + pub fn __get_packages(&mut self) -> anyhow::Result<Vec<crate::package::BasePackageHandle>> { + self.initialize()?; + use crate::repository::RepositoryInterface; + self.inner.get_packages() + } + fn initialize(&mut self) -> anyhow::Result<()> { self.inner.initialize(); let lookup = self.lookup.clone(); diff --git a/crates/shirabe/src/repository/path_repository.rs b/crates/shirabe/src/repository/path_repository.rs index 4508460..b8c106a 100644 --- a/crates/shirabe/src/repository/path_repository.rs +++ b/crates/shirabe/src/repository/path_repository.rs @@ -113,6 +113,32 @@ impl PathRepository { ) } + /// For testing only: drives `initialize` (globbing the configured url and loading each + /// path package) and returns the packages collected by the inner `ArrayRepository`, + /// mirroring the polymorphic `RepositoryInterface::getPackages` dispatch in PHP. + pub fn __get_packages(&mut self) -> anyhow::Result<Vec<crate::package::BasePackageHandle>> { + self.initialize()?; + use crate::repository::RepositoryInterface; + self.inner.get_packages() + } + + /// For testing only: returns `RepositoryInterface::count` after `initialize`. + pub fn __count(&mut self) -> anyhow::Result<usize> { + self.initialize()?; + use crate::repository::RepositoryInterface; + self.inner.count() + } + + /// For testing only: returns `RepositoryInterface::has_package` after `initialize`. + pub fn __has_package( + &mut self, + package: crate::package::PackageInterfaceHandle, + ) -> anyhow::Result<bool> { + self.initialize()?; + use crate::repository::RepositoryInterface; + Ok(self.inner.has_package(package)) + } + pub(crate) fn initialize(&mut self) -> anyhow::Result<()> { self.inner.initialize(); diff --git a/crates/shirabe/src/repository/repository_manager.rs b/crates/shirabe/src/repository/repository_manager.rs index 69684e2..403b504 100644 --- a/crates/shirabe/src/repository/repository_manager.rs +++ b/crates/shirabe/src/repository/repository_manager.rs @@ -162,6 +162,12 @@ impl RepositoryManager { &self.repositories } + /// For testing only: exposes the private `repository_classes` map so tests + /// can assert on its registered type keys (PHP reads it via ReflectionProperty). + pub fn __repository_classes(&self) -> &IndexMap<String, String> { + &self.repository_classes + } + pub fn set_local_repository(&mut self, repository: RepositoryInterfaceHandle) { self.local_repository = Some(repository); } diff --git a/crates/shirabe/src/util/svn.rs b/crates/shirabe/src/util/svn.rs index 319edaf..0a3daf3 100644 --- a/crates/shirabe/src/util/svn.rs +++ b/crates/shirabe/src/util/svn.rs @@ -303,6 +303,21 @@ impl Svn { args } + /// For testing only: invoke the crate-private `get_command`. + pub fn __get_command( + &mut self, + cmd: Vec<String>, + url: &str, + path: Option<&str>, + ) -> Vec<String> { + self.get_command(cmd, url, path) + } + + /// For testing only: invoke the crate-private `get_credential_args`. + pub fn __get_credential_args(&mut self) -> Vec<String> { + self.get_credential_args() + } + /// Get the password for the svn command. Can be empty. /// /// @throws \LogicException diff --git a/crates/shirabe/tests/advisory/audit_config_test.rs b/crates/shirabe/tests/advisory/audit_config_test.rs index 30bbb92..85337a4 100644 --- a/crates/shirabe/tests/advisory/audit_config_test.rs +++ b/crates/shirabe/tests/advisory/audit_config_test.rs @@ -95,7 +95,7 @@ fn test_detailed_format_block_only() { } #[test] -#[ignore = "parse_ignore_with_apply does not handle integer keys, so the bare list entry is mis-parsed"] +#[ignore] fn test_mixed_formats() { let audit_config = audit_config_from(arr(vec![( "ignore", diff --git a/crates/shirabe/tests/command/init_command_test.rs b/crates/shirabe/tests/command/init_command_test.rs index 589afc5..6b118f3 100644 --- a/crates/shirabe/tests/command/init_command_test.rs +++ b/crates/shirabe/tests/command/init_command_test.rs @@ -1,38 +1,96 @@ //! ref: composer/tests/Composer/Test/Command/InitCommandTest.php -// The author/namespace/git-config helpers are protected methods exercised via reflection -// in PHP; the run cases need the ApplicationTester. Neither is available here. +// The run cases (testRunCommand, testRunCommandInvalid, testRunGuessNameFromDirSanitizesDir, +// testInteractiveRun) drive the full command via ApplicationTester / initTempComposer, which +// does not exist here; they remain reason'd-ignore. The unit-style cases call the helper +// methods directly via `__`-prefixed test-only wrappers. use shirabe::command::init_command::InitCommand; -use shirabe_php_shim::server_set; +use shirabe_php_shim::{PhpMixed, server_set}; +use tempfile::TempDir; fn set_up() { server_set("COMPOSER_DEFAULT_AUTHOR", "John Smith".to_string()); server_set("COMPOSER_DEFAULT_EMAIL", "john@example.com".to_string()); } -#[ignore = "InitCommand::parse_author_string is private; integration tests cannot reach it (PHP uses reflection)"] +/// @return iterable<string, array{0: string, 1: string|null, 2: string}> +fn valid_author_string_provider() -> Vec<(&'static str, Option<&'static str>, &'static str)> { + vec![ + // simple + ( + "John Smith", + Some("john@example.com"), + "John Smith <john@example.com>", + ), + // without email + ("John Smith", None, "John Smith"), + // UTF-8 + ( + "Matti Meikäläinen", + Some("matti@example.com"), + "Matti Meikäläinen <matti@example.com>", + ), + // UTF-8 with non-spacing marks (\xCC\x88 is U+0308 combining diaeresis) + ( + "Matti Meika\u{0308}la\u{0308}inen", + Some("matti@example.com"), + "Matti Meika\u{0308}la\u{0308}inen <matti@example.com>", + ), + // numeric author name + ("h4x0r", Some("h4x@example.com"), "h4x0r <h4x@example.com>"), + // alias 1 (issue #5631) + ( + "Johnathon \"Johnny\" Smith", + Some("john@example.com"), + "Johnathon \"Johnny\" Smith <john@example.com>", + ), + // alias 2 (issue #5631) + ( + "Johnathon (Johnny) Smith", + Some("john@example.com"), + "Johnathon (Johnny) Smith <john@example.com>", + ), + ] +} + +#[ignore] #[test] fn test_parse_valid_author_string() { set_up(); - todo!() + for (name, email, input) in valid_author_string_provider() { + let command = InitCommand::new(); + let author = command.__parse_author_string(input).unwrap(); + assert_eq!( + Some(name.to_string()), + author.get("name").cloned().flatten() + ); + assert_eq!( + email.map(|e| e.to_string()), + author.get("email").cloned().flatten() + ); + } } -#[ignore = "InitCommand::parse_author_string is private; integration tests cannot reach it (PHP uses reflection)"] +#[ignore] #[test] fn test_parse_empty_author_string() { set_up(); - todo!() + let command = InitCommand::new(); + let result = command.__parse_author_string(""); + assert!(result.is_err()); } -#[ignore = "InitCommand::parse_author_string is private; integration tests cannot reach it (PHP uses reflection)"] +#[ignore] #[test] fn test_parse_author_string_with_invalid_email() { set_up(); - todo!() + let command = InitCommand::new(); + let result = command.__parse_author_string("John Smith <john>"); + assert!(result.is_err()); } #[test] @@ -97,34 +155,74 @@ fn test_interactive_run() { todo!() } -#[ignore = "InitCommand::format_authors is pub(crate); integration tests cannot reach it (PHP subclasses via DummyInitCommand)"] +#[ignore] #[test] fn test_format_authors() { set_up(); - todo!() + let author_with_email = "John Smith <john@example.com>"; + let author_without_email = "John Smith"; + let command = InitCommand::new(); + + let authors = command.__format_authors(author_with_email).unwrap(); + let mut expected: indexmap::IndexMap<String, PhpMixed> = indexmap::IndexMap::new(); + expected.insert( + "name".to_string(), + PhpMixed::String("John Smith".to_string()), + ); + expected.insert( + "email".to_string(), + PhpMixed::String("john@example.com".to_string()), + ); + assert_eq!(expected, authors[0]); + + let authors = command.__format_authors(author_without_email).unwrap(); + let mut expected: indexmap::IndexMap<String, PhpMixed> = indexmap::IndexMap::new(); + expected.insert( + "name".to_string(), + PhpMixed::String("John Smith".to_string()), + ); + assert_eq!(expected, authors[0]); } -#[ignore = "InitCommand::get_git_config is pub(crate); integration tests cannot reach it (PHP subclasses via DummyInitCommand)"] +#[ignore] #[test] fn test_get_git_config() { set_up(); - todo!() + let mut command = InitCommand::new(); + let git_config = command.__get_git_config(); + assert!(git_config.contains_key("user.name")); + assert!(git_config.contains_key("user.email")); } -#[ignore = "InitCommand::add_vendor_ignore is pub(crate) and test needs TestCase::get_unique_tmp_directory; neither reachable from integration tests"] +#[ignore] #[test] fn test_add_vendor_ignore() { set_up(); - todo!() + let tmp = TempDir::new().unwrap(); + let ignore_file = tmp.path().join("ignore"); + let ignore_file = ignore_file.to_str().unwrap(); + + let command = InitCommand::new(); + command.__add_vendor_ignore(ignore_file, "/vendor/"); + assert!(std::path::Path::new(ignore_file).exists()); + let content = std::fs::read_to_string(ignore_file).unwrap(); + assert!(content.contains("/vendor/")); } -#[ignore = "InitCommand::has_vendor_ignore/add_vendor_ignore are pub(crate) and test needs TestCase::get_unique_tmp_directory; neither reachable from integration tests"] +#[ignore] #[test] fn test_has_vendor_ignore() { set_up(); - todo!() + let tmp = TempDir::new().unwrap(); + let ignore_file = tmp.path().join("ignore"); + let ignore_file = ignore_file.to_str().unwrap(); + + let command = InitCommand::new(); + assert!(!command.__has_vendor_ignore(ignore_file, "vendor")); + command.__add_vendor_ignore(ignore_file, "/vendor/"); + assert!(command.__has_vendor_ignore(ignore_file, "vendor")); } diff --git a/crates/shirabe/tests/composer_test.rs b/crates/shirabe/tests/composer_test.rs index 75a554c..28d9d47 100644 --- a/crates/shirabe/tests/composer_test.rs +++ b/crates/shirabe/tests/composer_test.rs @@ -55,7 +55,7 @@ fn test_set_get_package() { } #[test] -#[ignore = "Locker construction builds an HttpDownloader, which reaches curl_multi_init (todo!()) in the php-shim"] +#[ignore] fn test_set_get_locker() { let mut composer = Composer::new(); let io = null_io(); @@ -74,7 +74,7 @@ fn test_set_get_locker() { } #[test] -#[ignore = "RepositoryManager construction builds an HttpDownloader, which reaches curl_multi_init (todo!()) in the php-shim"] +#[ignore] fn test_set_get_repository_manager() { let mut composer = Composer::new(); let io = null_io(); @@ -101,7 +101,7 @@ fn test_set_get_download_manager() { } #[test] -#[ignore = "InstallationManager construction builds a Loop/HttpDownloader, which reaches curl_multi_init (todo!()) in the php-shim"] +#[ignore] fn test_set_get_installation_manager() { let mut composer = Composer::new(); let io = null_io(); diff --git a/crates/shirabe/tests/console/html_output_formatter_test.rs b/crates/shirabe/tests/console/html_output_formatter_test.rs index af75551..2274ffe 100644 --- a/crates/shirabe/tests/console/html_output_formatter_test.rs +++ b/crates/shirabe/tests/console/html_output_formatter_test.rs @@ -7,7 +7,7 @@ use shirabe_external_packages::symfony::console::formatter::{ }; #[test] -#[ignore = "HtmlOutputFormatter::format uses a regex pattern the regex crate cannot compile (backreferences)"] +#[ignore] fn test_formatting() { let mut styles: IndexMap<String, Box<dyn OutputFormatterStyleInterface>> = IndexMap::new(); styles.insert( diff --git a/crates/shirabe/tests/dependency_resolver/pool_builder_test.rs b/crates/shirabe/tests/dependency_resolver/pool_builder_test.rs index 4c72091..87f02aa 100644 --- a/crates/shirabe/tests/dependency_resolver/pool_builder_test.rs +++ b/crates/shirabe/tests/dependency_resolver/pool_builder_test.rs @@ -1,9 +1,647 @@ //! ref: composer/tests/Composer/Test/DependencyResolver/PoolBuilderTest.php -// testPoolBuilder is a large fixture-driven case that loads packages from test inputs and -// builds a Pool; constraint parsing uses a look-around regex the regex crate cannot compile. +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::dependency_resolver::default_policy::DefaultPolicy; +use shirabe::dependency_resolver::pool::Pool; +use shirabe::dependency_resolver::pool_optimizer::PoolOptimizer; +use shirabe::dependency_resolver::request::{Request, UpdateAllowTransitiveDeps}; +use shirabe::io::io_interface::IOInterface; +use shirabe::io::null_io::NullIO; +use shirabe::json::JsonFile; +use shirabe::package::BasePackageHandle; +use shirabe::package::STABILITIES; +use shirabe::package::loader::{ArrayLoader, LoaderInterface}; +use shirabe::package::version::version_parser::VersionParser; +use shirabe::repository::array_repository::ArrayRepository; +use shirabe::repository::filter_repository::FilterRepository; +use shirabe::repository::handle::{LockArrayRepositoryHandle, RepositoryInterfaceHandle}; +use shirabe::repository::lock_array_repository::LockArrayRepository; +use shirabe::repository::repository_factory::RepositoryFactory; +use shirabe::repository::repository_set::{RepositorySet, RootAliasInput}; +use shirabe_external_packages::composer::pcre::preg::Preg; +use shirabe_php_shim::PREG_SPLIT_DELIM_CAPTURE; +use shirabe_php_shim::PhpMixed; + +/// Maps the PHP `$loadPackage` closure: pops the optional `id` from the data, loads the +/// package and records it in `package_ids` keyed by that id (erroring on duplicates). +fn load_package( + loader: &ArrayLoader, + package_ids: &mut IndexMap<i64, BasePackageHandle>, + data: &IndexMap<String, PhpMixed>, +) -> BasePackageHandle { + let mut data = data.clone(); + + let mut id: Option<i64> = None; + // PHP: !empty($data['id']) + if let Some(id_val) = data.get("id") { + if let Some(i) = id_val.as_int() { + if i != 0 { + id = Some(i); + data.shift_remove("id"); + } + } + } + + let pkg = loader.load(data, None).unwrap(); + + if let Some(id) = id { + if package_ids.contains_key(&id) { + panic!("Duplicate package id {} defined", id); + } + package_ids.insert(id, pkg.clone()); + } + + pkg +} + +fn read_test_file(file: &str, fixtures_dir: &str) -> IndexMap<String, String> { + let contents = shirabe_php_shim::file_get_contents(file).unwrap(); + let tokens = Preg::split4( + r"#(?:^|\n*)--([A-Z-]+)--\n#", + &contents, + -1, + PREG_SPLIT_DELIM_CAPTURE, + ); + + // PHP section_info is a map of name => required flag. + let section_info: Vec<(&str, bool)> = vec![ + ("TEST", true), + ("ROOT", false), + ("REQUEST", true), + ("FIXED", false), + ("PACKAGE-REPOS", true), + ("EXPECT", true), + ("EXPECT-OPTIMIZED", false), + ]; + + let mut section: Option<String> = None; + let mut data: IndexMap<String, String> = IndexMap::new(); + for token in tokens { + if section.is_none() && token.is_empty() { + continue; + } + + if section.is_none() { + if !section_info.iter().any(|(name, _)| *name == token.as_str()) { + panic!( + "The test file \"{}\" must not contain a section named \"{}\".", + file.replace(&format!("{}/", fixtures_dir), ""), + token + ); + } + section = Some(token); + continue; + } + + let section_data = token; + data.insert(section.take().unwrap(), section_data); + } + + for (section, required) in §ion_info { + if *required && !data.contains_key(*section) { + panic!( + "The test file \"{}\" must have a section named \"{}\".", + file.replace(&format!("{}/", fixtures_dir), ""), + section + ); + } + } + + data +} + +fn collect_test_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) { + for entry in std::fs::read_dir(dir).unwrap() { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + collect_test_files(&path, out); + } else { + out.push(path); + } + } +} + +struct IntegrationTest { + file: String, + message: String, + expect: PhpMixed, + expect_optimized: PhpMixed, + root: PhpMixed, + request: PhpMixed, + package_repos: PhpMixed, + fixed: PhpMixed, +} + +fn get_integration_tests(fixtures_dir: &std::path::Path) -> IndexMap<String, IntegrationTest> { + let fixtures_dir_str = fixtures_dir.to_str().unwrap().to_string(); + + let mut files: Vec<PathBuf> = Vec::new(); + collect_test_files(fixtures_dir, &mut files); + + let mut tests: IndexMap<String, IntegrationTest> = IndexMap::new(); + for file in files { + let file = file.to_str().unwrap().to_string(); + + if !Preg::is_match(r"/\.test$/", &file) { + continue; + } + + let test_data = read_test_file(&file, &fixtures_dir_str); + + let message = test_data["TEST"].clone(); + + let request = JsonFile::parse_json(Some(&test_data["REQUEST"]), None).unwrap(); + // PHP: !empty($testData['ROOT']) ? parseJson(...) : [] + let root = match test_data.get("ROOT") { + Some(s) if !s.is_empty() => JsonFile::parse_json(Some(s), None).unwrap(), + _ => PhpMixed::List(vec![]), + }; + + let package_repos = JsonFile::parse_json(Some(&test_data["PACKAGE-REPOS"]), None).unwrap(); + let fixed = match test_data.get("FIXED") { + Some(s) if !s.is_empty() => JsonFile::parse_json(Some(s), None).unwrap(), + _ => PhpMixed::List(vec![]), + }; + let expect = JsonFile::parse_json(Some(&test_data["EXPECT"]), None).unwrap(); + let expect_optimized = match test_data.get("EXPECT-OPTIMIZED") { + Some(s) if !s.is_empty() => JsonFile::parse_json(Some(s), None).unwrap(), + _ => expect.clone(), + }; + + let basename = std::path::Path::new(&file) + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(); + tests.insert( + basename, + IntegrationTest { + file: file.replace(&format!("{}/", fixtures_dir_str), ""), + message, + expect, + expect_optimized, + root, + request, + package_repos, + fixed, + }, + ); + } + + tests +} + +/// PHP `sort()` with SORT_REGULAR over a mixed int/string list. Mirrors PHP 8 loose +/// comparison: two ints compare numerically; a string and an int that looks numeric +/// compare numerically; otherwise both are compared as strings. +fn php_sort_mixed(values: &mut [PhpMixed]) { + fn cmp(a: &PhpMixed, b: &PhpMixed) -> std::cmp::Ordering { + use std::cmp::Ordering; + let num = |m: &PhpMixed| -> Option<f64> { + match m { + PhpMixed::Int(i) => Some(*i as f64), + PhpMixed::Float(f) => Some(*f), + PhpMixed::String(s) => s.trim().parse::<f64>().ok(), + _ => None, + } + }; + let str_of = |m: &PhpMixed| -> String { + match m { + PhpMixed::Int(i) => i.to_string(), + PhpMixed::Float(f) => f.to_string(), + PhpMixed::String(s) => s.clone(), + _ => String::new(), + } + }; + match (a, b) { + (PhpMixed::Int(x), PhpMixed::Int(y)) => x.cmp(y), + _ => match (num(a), num(b)) { + (Some(x), Some(y)) => x.partial_cmp(&y).unwrap_or(Ordering::Equal), + _ => str_of(a).cmp(&str_of(b)), + }, + } + } + values.sort_by(cmp); +} + +/// ref: PoolBuilderTest::getPackageResultSet +fn get_package_result_set( + pool: &Pool, + package_ids: &IndexMap<i64, BasePackageHandle>, +) -> Vec<PhpMixed> { + let mut result: Vec<BasePackageHandle> = Vec::new(); + let count = pool.__count(); + // PHP: for ($i = 1; $i <= $count; $i++) + let mut i = 1; + while i <= count { + result.push(pool.package_by_id(i)); + i += 1; + } + + // PHP: array_search($package, $packageIds, true) — identity lookup. + let find_id = |package: &BasePackageHandle| -> Option<i64> { + for (id, candidate) in package_ids { + if candidate.ptr_id() == package.ptr_id() { + return Some(*id); + } + } + None + }; + + result + .into_iter() + .map(|package| { + // PHP: if ($id = array_search(...)) — id keys start at 1 so always truthy when found. + if let Some(id) = find_id(&package) { + return PhpMixed::Int(id); + } + + let mut suffix = String::new(); + if let Some(source_reference) = package.get_source_reference() { + suffix = format!("#{}", source_reference); + } + if let Some(repo) = package.get_repository() { + if repo.is::<LockArrayRepository>() { + suffix.push_str(" (locked)"); + } + } + + if let Some(alias) = package.as_alias() { + let alias_of = alias.get_alias_of(); + // PHP: array_search($package->getAliasOf(), $packageIds, true) + let mut matched: Option<i64> = None; + for (id, candidate) in package_ids { + if candidate.ptr_id() == alias_of.ptr_id() { + matched = Some(*id); + break; + } + } + if let Some(id) = matched { + return PhpMixed::String(format!( + "{}-{}{} (alias of {})", + package.get_name(), + package.get_version(), + suffix, + id + )); + } + + return PhpMixed::String(format!( + "{}-{}{} (alias of {})", + package.get_name(), + package.get_version(), + suffix, + alias_of.get_version() + )); + } + + PhpMixed::String(format!( + "{}-{}{}", + package.get_name(), + package.get_version(), + suffix + )) + }) + .collect() +} + +/// ref: PoolBuilderTest::testPoolBuilder +#[allow(clippy::too_many_arguments)] +fn run_test_pool_builder( + fixtures_dir: &std::path::Path, + _file: &str, + message: &str, + expect: &PhpMixed, + expect_optimized: &PhpMixed, + root: &PhpMixed, + request_data: &PhpMixed, + package_repos: &PhpMixed, + fixed: &PhpMixed, +) { + // PHP: $root is array; empty checks against keys. + let root_map = root.as_array(); + let get_root = |key: &str| -> Option<&PhpMixed> { root_map.and_then(|m| m.get(key)) }; + let is_empty = |v: Option<&PhpMixed>| -> bool { + match v { + None => true, + Some(PhpMixed::Null) => true, + Some(PhpMixed::Bool(false)) => true, + Some(PhpMixed::Int(0)) => true, + Some(PhpMixed::String(s)) => s.is_empty(), + Some(PhpMixed::List(l)) => l.is_empty(), + Some(PhpMixed::Array(a)) => a.is_empty(), + Some(PhpMixed::Object(a)) => a.is_empty(), + _ => false, + } + }; + + // PHP: $rootAliases = !empty($root['aliases']) ? $root['aliases'] : []; + let root_aliases_data: Vec<IndexMap<String, PhpMixed>> = if !is_empty(get_root("aliases")) { + get_root("aliases") + .and_then(|v| v.as_list()) + .map(|l| { + l.iter() + .filter_map(|v| v.as_array().cloned()) + .collect::<Vec<_>>() + }) + .unwrap_or_default() + } else { + vec![] + }; + + let minimum_stability: String = if !is_empty(get_root("minimum-stability")) { + get_root("minimum-stability") + .and_then(|v| v.as_string()) + .unwrap_or("stable") + .to_string() + } else { + "stable".to_string() + }; + + // PHP: $stabilityFlags map of name => stability string. + let stability_flags_data: IndexMap<String, String> = if !is_empty(get_root("stability-flags")) { + get_root("stability-flags") + .and_then(|v| v.as_array()) + .map(|m| { + m.iter() + .filter_map(|(k, v)| v.as_string().map(|s| (k.clone(), s.to_string()))) + .collect() + }) + .unwrap_or_default() + } else { + IndexMap::new() + }; + + let root_references: IndexMap<String, String> = if !is_empty(get_root("references")) { + get_root("references") + .and_then(|v| v.as_array()) + .map(|m| { + m.iter() + .filter_map(|(k, v)| v.as_string().map(|s| (k.clone(), s.to_string()))) + .collect() + }) + .unwrap_or_default() + } else { + IndexMap::new() + }; + + // PHP: array_map over stability flags, mapping to BasePackage::STABILITIES[$stability]. + let mut stability_flags: IndexMap<String, i64> = IndexMap::new(); + for (name, stability) in &stability_flags_data { + let Some(value) = STABILITIES.get(stability.as_str()) else { + panic!("Invalid stability given: {}", stability); + }; + stability_flags.insert(name.clone(), *value); + } + + let parser = VersionParser::new(); + // PHP: foreach ($rootAliases as $index => $alias) { normalize version + alias } + let mut root_aliases: Vec<RootAliasInput> = Vec::new(); + for alias in &root_aliases_data { + let package = alias + .get("package") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let version = parser + .normalize( + alias + .get("version") + .and_then(|v| v.as_string()) + .unwrap_or(""), + None, + ) + .unwrap(); + let alias_str = alias + .get("alias") + .and_then(|v| v.as_string()) + .unwrap_or("") + .to_string(); + let alias_normalized = parser + .normalize( + alias.get("alias").and_then(|v| v.as_string()).unwrap_or(""), + None, + ) + .unwrap(); + root_aliases.push(RootAliasInput { + package, + version, + alias: alias_str, + alias_normalized, + }); + } + + let loader = ArrayLoader::new(None, true); + let mut package_ids: IndexMap<i64, BasePackageHandle> = IndexMap::new(); + + // PHP: $oldCwd = Platform::getCwd(); chdir(__DIR__.'/Fixtures/poolbuilder/'); + let old_cwd = std::env::current_dir().unwrap(); + std::env::set_current_dir(fixtures_dir).unwrap(); + + let mut repository_set = RepositorySet::new( + &minimum_stability, + stability_flags, + root_aliases, + root_references, + IndexMap::new(), + IndexMap::new(), + ); + let config = Rc::new(RefCell::new(Config::new(false, None))); + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let mut rm = RepositoryFactory::manager(io.clone(), &config, None, None, None).unwrap(); + + // PHP: foreach ($packageRepos as $packages) + for repo_entry in package_repos.as_list().unwrap() { + // PHP: isset($packages['type']) + if let Some(repo_map) = repo_entry.as_array() { + if repo_map.contains_key("type") { + let repo = RepositoryFactory::create_repo( + io.clone(), + &config, + repo_map.clone(), + Some(&mut rm), + ) + .unwrap(); + repository_set.add_repository(repo).unwrap(); + continue; + } + } + + let repo = ArrayRepository::new(vec![]).unwrap(); + let repo_handle = RepositoryInterfaceHandle::new(repo); + + // PHP: isset($packages['canonical']) || isset($packages['only']) || isset($packages['exclude']) + let packages_list: Vec<IndexMap<String, PhpMixed>>; + if let Some(repo_map) = repo_entry.as_array() { + if repo_map.contains_key("canonical") + || repo_map.contains_key("only") + || repo_map.contains_key("exclude") + { + let mut options = repo_map.clone(); + let packages = options + .shift_remove("packages") + .unwrap_or(PhpMixed::List(vec![])); + repository_set + .add_repository(RepositoryInterfaceHandle::new( + FilterRepository::new(repo_handle.clone(), options).unwrap(), + )) + .unwrap(); + packages_list = packages + .as_list() + .map(|l| l.iter().filter_map(|v| v.as_array().cloned()).collect()) + .unwrap_or_default(); + } else { + repository_set.add_repository(repo_handle.clone()).unwrap(); + packages_list = repo_map + .values() + .filter_map(|v| v.as_array().cloned()) + .collect(); + } + } else { + repository_set.add_repository(repo_handle.clone()).unwrap(); + packages_list = repo_entry + .as_list() + .map(|l| l.iter().filter_map(|v| v.as_array().cloned()).collect()) + .unwrap_or_default(); + } + + let array_repo = repo_handle.downcast_rc::<ArrayRepository>().unwrap(); + for package in &packages_list { + array_repo + .borrow() + .add_package(load_package(&loader, &mut package_ids, package)) + .unwrap(); + } + } + + let locked_repo = LockArrayRepositoryHandle::new(LockArrayRepository::new(vec![]).unwrap()); + repository_set + .add_repository(locked_repo.clone().into()) + .unwrap(); + + let request_map = request_data.as_array().unwrap(); + + // PHP: if (isset($requestData['locked'])) + if let Some(locked) = request_map.get("locked") { + for package in locked.as_list().unwrap() { + locked_repo + .borrow() + .add_package(load_package( + &loader, + &mut package_ids, + package.as_array().unwrap(), + )) + .unwrap(); + } + } + + let mut request = Request::new(Some(locked_repo.clone())); + for (package, constraint) in request_map["require"].as_array().unwrap() { + request + .require_name( + package, + Some( + parser + .parse_constraints(constraint.as_string().unwrap()) + .unwrap(), + ), + ) + .unwrap(); + } + + // PHP: if (isset($requestData['allowList'])) + if let Some(allow_list) = request_map.get("allowList") { + let mut transitive_deps = UpdateAllowTransitiveDeps::UpdateOnlyListed; + if request_map + .get("allowTransitiveDepsNoRootRequire") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + transitive_deps = + UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDepsNoRootRequire; + } + if request_map + .get("allowTransitiveDeps") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + { + transitive_deps = UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDeps; + } + let allow_list_names: Vec<String> = allow_list + .as_list() + .unwrap() + .iter() + .filter_map(|v| v.as_string().map(|s| s.to_string())) + .collect(); + request.set_update_allow_list(allow_list_names, transitive_deps); + } + + // PHP: foreach ($fixed as $fixedPackage) + for fixed_package in fixed.as_list().unwrap() { + request.fix_package(load_package( + &loader, + &mut package_ids, + fixed_package.as_array().unwrap(), + )); + } + + let pool = repository_set + .create_pool(&mut request, io.clone(), None, None, vec![], None, None) + .unwrap(); + + let mut result = get_package_result_set(&pool, &package_ids); + + let mut expect = expect.as_list().unwrap().clone(); + php_sort_mixed(&mut expect); + php_sort_mixed(&mut result); + assert_eq!( + expect, result, + "Unoptimized pool does not match expected package set ({})", + message + ); + + let mut optimizer = PoolOptimizer::new(Rc::new(DefaultPolicy::new(false, false, None))); + let optimized = optimizer.optimize(&request, &pool); + let mut result = get_package_result_set(&optimized, &package_ids); + let mut expect_optimized = expect_optimized.as_list().unwrap().clone(); + php_sort_mixed(&mut expect_optimized); + php_sort_mixed(&mut result); + assert_eq!( + expect_optimized, result, + "Optimized pool does not match expected package set ({})", + message + ); + + // PHP: chdir($oldCwd); + std::env::set_current_dir(&old_cwd).unwrap(); +} + +#[ignore] #[test] -#[ignore = "ArrayLoader::load (single-package, pub) and Pool::count are not exposed: the loadPackage closure calls $loader->load($data) per package and getPackageResultSet uses count($pool)"] fn test_pool_builder() { - todo!() + let fixtures_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../composer/tests/Composer/Test/DependencyResolver/Fixtures/poolbuilder"); + let fixtures_dir = std::fs::canonicalize(&fixtures_dir).unwrap(); + + let tests = get_integration_tests(&fixtures_dir); + for (_name, test) in tests { + run_test_pool_builder( + &fixtures_dir, + &test.file, + &test.message, + &test.expect, + &test.expect_optimized, + &test.root, + &test.request, + &test.package_repos, + &test.fixed, + ); + } } diff --git a/crates/shirabe/tests/dependency_resolver/solver_test.rs b/crates/shirabe/tests/dependency_resolver/solver_test.rs index 26f1312..c729fea 100644 --- a/crates/shirabe/tests/dependency_resolver/solver_test.rs +++ b/crates/shirabe/tests/dependency_resolver/solver_test.rs @@ -9,11 +9,13 @@ use shirabe::dependency_resolver::default_policy::DefaultPolicy; use shirabe::dependency_resolver::request::Request; use shirabe::io::io_interface::IOInterface; use shirabe::io::null_io::NullIO; +use shirabe::package::Link; use shirabe::package::handle::PackageInterfaceHandle; use shirabe::repository::array_repository::ArrayRepository; use shirabe::repository::handle::{LockArrayRepositoryHandle, RepositoryInterfaceHandle}; use shirabe::repository::lock_array_repository::LockArrayRepository; use shirabe::repository::repository_set::RepositorySet; +use shirabe_semver::constraint::{AnyConstraint, MatchAllConstraint, MultiConstraint}; use crate::test_case::{get_alias_package, get_package, get_version_constraint}; @@ -50,6 +52,40 @@ fn set_up() -> Fixtures { } } +/// 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, + ) +} + +/// PHP `new Link($source, $target, $constraint, $type, $prettyConstraint)`. +fn link_pretty( + source: &str, + target: &str, + constraint: AnyConstraint, + r#type: &str, + pretty: &str, +) -> Link { + Link::new( + source.to_string(), + target.to_string(), + constraint, + Some(r#type.to_string()), + pretty.to_string(), + ) +} + +fn multi(constraints: Vec<AnyConstraint>) -> AnyConstraint { + MultiConstraint::new(constraints, true, None).into() +} + /// One expected solver job. Mirrors the PHP `['job' => ..., 'package'|'from'|'to' => ...]` rows. enum ExpectedJob { Single { @@ -67,7 +103,7 @@ fn check_solver_result( mut repo_set: RepositorySet, repo: ArrayRepository, repo_locked: LockArrayRepositoryHandle, - mut request: Request, + request: Request, expected: Vec<ExpectedJob>, ) { // reposComplete() @@ -76,6 +112,15 @@ fn check_solver_result( .unwrap(); repo_set.add_repository(repo_locked.into()).unwrap(); + check_solver_result_repo_set(&mut repo_set, request, expected); +} + +/// ref: SolverTest::checkSolverResult, against an already-completed RepositorySet (createSolver). +fn check_solver_result_repo_set( + repo_set: &mut RepositorySet, + mut request: Request, + expected: Vec<ExpectedJob>, +) { // createSolver() let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); let pool = repo_set @@ -137,6 +182,29 @@ fn check_solver_result( assert_eq!(expected_ids, result_ids); } +/// ref: SolverTest::createSolver + solve, returning the error for expectException-only tests. +fn solve_expecting_error( + mut repo_set: RepositorySet, + repo: ArrayRepository, + repo_locked: LockArrayRepositoryHandle, + mut request: Request, +) { + repo_set + .add_repository(RepositoryInterfaceHandle::new(repo)) + .unwrap(); + repo_set.add_repository(repo_locked.into()).unwrap(); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let pool = repo_set + .create_pool(&mut request, io.clone(), None, None, vec![], None, None) + .unwrap(); + let policy: Rc<dyn PolicyInterface> = Rc::new(DefaultPolicy::new(false, false, None)); + let mut solver = + shirabe::dependency_resolver::solver::Solver::new(policy, Rc::new(RefCell::new(pool)), io); + + assert!(solver.solve(&request, None).is_err()); +} + #[ignore] #[test] fn test_solver_install_single() { @@ -178,7 +246,7 @@ fn test_solver_remove_if_not_requested() { ); } -#[ignore = "solve() error path discards SolverProblemsException (returns placeholder anyhow error); getProblems/getCode/getPrettyString not retrievable"] +#[ignore = "asserts SolverProblemsException details (getProblems/getCode/getPrettyString) which solve() discards"] #[test] fn test_install_non_existing_package_fails() { let _fixtures = set_up(); @@ -222,40 +290,364 @@ fn test_solver_install_same_package_from_different_repositories() { ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_install_with_deps() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let new_package_b = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint("<", "1.1"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_install_honours_not_equal_operator() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let new_package_b11 = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b11.clone()).unwrap(); + let new_package_b12 = get_package("B", "1.2"); + fixtures.repo.add_package(new_package_b12.clone()).unwrap(); + let new_package_b13 = get_package("B", "1.3"); + fixtures.repo.add_package(new_package_b13.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + multi(vec![ + get_version_constraint("<=", "1.3"), + get_version_constraint("<>", "1.3"), + get_version_constraint("!=", "1.2"), + ]), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: new_package_b11, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_install_with_deps_in_order() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let package_c = get_package("C", "1.0"); + fixtures.repo.add_package(package_c.clone()).unwrap(); + + package_b + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "a".to_string(), + link( + "B", + "A", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "c".to_string(), + link( + "B", + "C", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + package_c + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "C", + "A", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + request.require_name("B", None).unwrap(); + request.require_name("C", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ExpectedJob::Single { + job: "install", + package: package_c, + }, + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_multi_package_name_version_resolution_depends_on_require_order() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let php74 = get_package("ourcustom/PHP", "7.4.23"); + fixtures.repo.add_package(php74.clone()).unwrap(); + let php80 = get_package("ourcustom/PHP", "8.0.10"); + fixtures.repo.add_package(php80.clone()).unwrap(); + let ext_for_php74 = get_package("ourcustom/ext-foobar", "1.0"); + fixtures.repo.add_package(ext_for_php74.clone()).unwrap(); + let ext_for_php80 = get_package("ourcustom/ext-foobar", "1.0"); + fixtures.repo.add_package(ext_for_php80.clone()).unwrap(); + + ext_for_php74 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "ourcustom/php".to_string(), + link( + "ourcustom/ext-foobar", + "ourcustom/PHP", + multi(vec![ + get_version_constraint(">=", "7.4.0"), + get_version_constraint("<", "7.5.0"), + ]), + Link::TYPE_REQUIRE, + ), + )])); + ext_for_php80 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "ourcustom/php".to_string(), + link( + "ourcustom/ext-foobar", + "ourcustom/PHP", + multi(vec![ + get_version_constraint(">=", "8.0.0"), + get_version_constraint("<", "8.1.0"), + ]), + Link::TYPE_REQUIRE, + ), + )])); + + // reposComplete() + let mut repo_set = fixtures.repo_set; + repo_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + repo_set + .add_repository(fixtures.repo_locked.clone().into()) + .unwrap(); + + let mut request = fixtures.request; + request.require_name("ourcustom/PHP", None).unwrap(); + request.require_name("ourcustom/ext-foobar", None).unwrap(); + + check_solver_result_repo_set( + &mut repo_set, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: php80.clone(), + }, + ExpectedJob::Single { + job: "install", + package: ext_for_php80, + }, + ], + ); + + // now we flip the requirements around: we request "ext-foobar" before "php" + let mut request = Request::new(Some(fixtures.repo_locked.clone())); + request.require_name("ourcustom/ext-foobar", None).unwrap(); + request.require_name("ourcustom/PHP", None).unwrap(); + + check_solver_result_repo_set( + &mut repo_set, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: php74, + }, + ExpectedJob::Single { + job: "install", + package: ext_for_php74, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_multi_package_name_version_resolution_is_independent_of_require_order_if_ordered_descending_by_requirement() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let php74 = get_package("ourcustom/PHP", "7.4"); + fixtures.repo.add_package(php74.clone()).unwrap(); + let php80 = get_package("ourcustom/PHP", "8.0"); + fixtures.repo.add_package(php80.clone()).unwrap(); + // note we are inserting this one into the repo first, unlike in the previous test + let ext_for_php80 = get_package("ourcustom/ext-foobar", "1.0"); + fixtures.repo.add_package(ext_for_php80.clone()).unwrap(); + let ext_for_php74 = get_package("ourcustom/ext-foobar", "1.0"); + fixtures.repo.add_package(ext_for_php74.clone()).unwrap(); + + ext_for_php80 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "ourcustom/php".to_string(), + link( + "ourcustom/ext-foobar", + "ourcustom/PHP", + multi(vec![ + get_version_constraint(">=", "8.0.0"), + get_version_constraint("<", "8.1.0"), + ]), + Link::TYPE_REQUIRE, + ), + )])); + ext_for_php74 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "ourcustom/php".to_string(), + link( + "ourcustom/ext-foobar", + "ourcustom/PHP", + multi(vec![ + get_version_constraint(">=", "7.4.0"), + get_version_constraint("<", "7.5.0"), + ]), + Link::TYPE_REQUIRE, + ), + )])); + + // reposComplete() + let mut repo_set = fixtures.repo_set; + repo_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + repo_set + .add_repository(fixtures.repo_locked.clone().into()) + .unwrap(); + + let mut request = fixtures.request; + request.require_name("ourcustom/PHP", None).unwrap(); + request.require_name("ourcustom/ext-foobar", None).unwrap(); + + check_solver_result_repo_set( + &mut repo_set, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: php80.clone(), + }, + ExpectedJob::Single { + job: "install", + package: ext_for_php80.clone(), + }, + ], + ); + + // unlike in the previous test, the order of requirements no longer matters now + let mut request = Request::new(Some(fixtures.repo_locked.clone())); + request.require_name("ourcustom/ext-foobar", None).unwrap(); + request.require_name("ourcustom/PHP", None).unwrap(); + + check_solver_result_repo_set( + &mut repo_set, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: php80, + }, + ExpectedJob::Single { + job: "install", + package: ext_for_php80, + }, + ], + ); } #[ignore] @@ -297,11 +689,46 @@ fn test_solver_fix_locked_with_alternative() { ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_update_does_only_update() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo_locked.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo_locked.add_package(package_b.clone()).unwrap(); + let new_package_b = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0.0.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.fix_package(package_a.clone()); + request + .require_name("B", Some(get_version_constraint("=", "1.1.0.0"))) + .unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ExpectedJob::Update { + from: package_b, + to: new_package_b, + }], + ); } #[ignore] @@ -328,11 +755,63 @@ fn test_solver_update_single() { ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_update_all() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo_locked.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo_locked.add_package(package_b.clone()).unwrap(); + let new_package_a = get_package("A", "1.1"); + fixtures.repo.add_package(new_package_a.clone()).unwrap(); + let new_package_b = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + MatchAllConstraint::new(None).into(), + Link::TYPE_REQUIRE, + ), + )])); + new_package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + MatchAllConstraint::new(None).into(), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Update { + from: package_b, + to: new_package_b, + }, + ExpectedJob::Update { + from: package_a, + to: new_package_a, + }, + ], + ); } #[ignore] @@ -475,25 +954,168 @@ fn test_solver_update_fully_constrained_prunes_installed_packages() { ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_all_jobs() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_d = get_package("D", "1.0"); + fixtures.repo_locked.add_package(package_d.clone()).unwrap(); + let old_package_c = get_package("C", "1.0"); + fixtures + .repo_locked + .add_package(old_package_c.clone()) + .unwrap(); + + let package_a = get_package("A", "2.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let new_package_b = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + let package_c = get_package("C", "1.1"); + fixtures.repo.add_package(package_c.clone()).unwrap(); + fixtures.repo.add_package(get_package("D", "1.0")).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint("<", "1.1"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + request.require_name("C", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "remove", + package: package_d, + }, + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ExpectedJob::Update { + from: old_package_c, + to: package_c, + }, + ], + ); } -#[ignore = "setRequires/setConflicts not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_three_alternative_require_and_conflict() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "2.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let middle_package_b = get_package("B", "1.0"); + fixtures.repo.add_package(middle_package_b.clone()).unwrap(); + let new_package_b = get_package("B", "1.1"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + let old_package_b = get_package("B", "0.9"); + fixtures.repo.add_package(old_package_b.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint("<", "1.1"), + Link::TYPE_REQUIRE, + ), + )])); + package_a + .as_complete_package() + .unwrap() + .__set_conflicts(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint("<", "1.0"), + Link::TYPE_CONFLICT, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: middle_package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_solver_obsolete() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo_locked.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + package_b + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + MatchAllConstraint::new(None).into(), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("B", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "remove", + package: package_a, + }, + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ], + ); } #[ignore] @@ -520,109 +1142,925 @@ fn test_install_one_of_two_alternatives() { ); } -#[ignore = "setRequires/setProvides not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_install_provider() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_q = get_package("Q", "1.0"); + fixtures.repo.add_package(package_q.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_q + .as_complete_package() + .unwrap() + .__set_provides(IndexMap::from([( + "b".to_string(), + link( + "Q", + "B", + get_version_constraint("=", "1.0"), + Link::TYPE_PROVIDE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + // must explicitly pick the provider, so error in this case + solve_expecting_error( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_skip_replacer_of_existing_package() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_q = get_package("Q", "1.0"); + fixtures.repo.add_package(package_q.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_q + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "b".to_string(), + link( + "Q", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_no_install_replacer_of_missing_package() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_q = get_package("Q", "1.0"); + fixtures.repo.add_package(package_q.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_q + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "b".to_string(), + link( + "Q", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + solve_expecting_error( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_skip_replaced_package_if_replacer_is_selected() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_q = get_package("Q", "1.0"); + fixtures.repo.add_package(package_q.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_q + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "b".to_string(), + link( + "Q", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + request.require_name("Q", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_q, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_pick_older_if_newer_conflicts() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_x = get_package("X", "1.0"); + fixtures.repo.add_package(package_x.clone()).unwrap(); + package_x + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "a".to_string(), + link( + "X", + "A", + get_version_constraint(">=", "2.0.0.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "b".to_string(), + link( + "X", + "B", + get_version_constraint(">=", "2.0.0.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + let package_a = get_package("A", "2.0.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let new_package_a = get_package("A", "2.1.0"); + fixtures.repo.add_package(new_package_a.clone()).unwrap(); + let new_package_b = get_package("B", "2.1.0"); + fixtures.repo.add_package(new_package_b.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "2.0.0.0"), + Link::TYPE_REQUIRE, + ), + )])); + + // new package A depends on version of package B that does not exist + // => new package A is not installable + new_package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "2.2.0.0"), + Link::TYPE_REQUIRE, + ), + )])); + + // add a package S replacing both A and B, so that S and B or S and A cannot be simultaneously installed + // but an alternative option for A and B both exists + // this creates a more difficult so solve conflict + let package_s = get_package("S", "2.0.0"); + fixtures.repo.add_package(package_s.clone()).unwrap(); + package_s + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([ + ( + "a".to_string(), + link( + "S", + "A", + get_version_constraint(">=", "2.0.0.0"), + Link::TYPE_REPLACE, + ), + ), + ( + "b".to_string(), + link( + "S", + "B", + get_version_constraint(">=", "2.0.0.0"), + Link::TYPE_REPLACE, + ), + ), + ])); + + let mut request = fixtures.request; + request.require_name("X", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: new_package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ExpectedJob::Single { + job: "install", + package: package_x, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_install_circular_require() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "1.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_b1 = get_package("B", "0.9"); + fixtures.repo.add_package(package_b1.clone()).unwrap(); + let package_b2 = get_package("B", "1.1"); + fixtures.repo.add_package(package_b2.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_b2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_b2, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires/setProvides not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_install_alternative_with_circular_require() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let package_c = get_package("C", "1.0"); + fixtures.repo.add_package(package_c.clone()).unwrap(); + let package_d = get_package("D", "1.0"); + fixtures.repo.add_package(package_d.clone()).unwrap(); + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_b + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "virtual".to_string(), + link( + "B", + "Virtual", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_c + .as_complete_package() + .unwrap() + .__set_provides(IndexMap::from([( + "virtual".to_string(), + link( + "C", + "Virtual", + get_version_constraint("==", "1.0"), + Link::TYPE_PROVIDE, + ), + )])); + package_d + .as_complete_package() + .unwrap() + .__set_provides(IndexMap::from([( + "virtual".to_string(), + link( + "D", + "Virtual", + get_version_constraint("==", "1.0"), + Link::TYPE_PROVIDE, + ), + )])); + + package_c + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "C", + "A", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_d + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "D", + "A", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + request.require_name("C", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ExpectedJob::Single { + job: "install", + package: package_c, + }, + ], + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_use_replacer_if_necessary() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let package_d = get_package("D", "1.0"); + fixtures.repo.add_package(package_d.clone()).unwrap(); + let package_d2 = get_package("D", "1.1"); + fixtures.repo.add_package(package_d2.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "b".to_string(), + link( + "A", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "c".to_string(), + link( + "A", + "C", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + package_d + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([ + ( + "b".to_string(), + link( + "D", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + ), + ( + "c".to_string(), + link( + "D", + "C", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + ), + ])); + + package_d2 + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([ + ( + "b".to_string(), + link( + "D", + "B", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + ), + ( + "c".to_string(), + link( + "D", + "C", + get_version_constraint(">=", "1.0"), + Link::TYPE_REPLACE, + ), + ), + ])); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + request.require_name("D", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_d2, + }, + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ], + ); } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_issue265() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a1 = get_package("A", "2.0.999999-dev"); + fixtures.repo.add_package(package_a1.clone()).unwrap(); + let package_a2 = get_package("A", "2.1-dev"); + fixtures.repo.add_package(package_a2.clone()).unwrap(); + let package_a3 = get_package("A", "2.2-dev"); + fixtures.repo.add_package(package_a3.clone()).unwrap(); + let package_b1 = get_package("B", "2.0.10"); + fixtures.repo.add_package(package_b1.clone()).unwrap(); + let package_b2 = get_package("B", "2.0.9"); + fixtures.repo.add_package(package_b2.clone()).unwrap(); + let package_c = get_package("C", "2.0-dev"); + fixtures.repo.add_package(package_c.clone()).unwrap(); + let package_d = get_package("D", "2.0.9"); + fixtures.repo.add_package(package_d.clone()).unwrap(); + + package_c + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "a".to_string(), + link( + "C", + "A", + get_version_constraint(">=", "2.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "d".to_string(), + link( + "C", + "D", + get_version_constraint(">=", "2.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + package_d + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "a".to_string(), + link( + "D", + "A", + get_version_constraint(">=", "2.1"), + Link::TYPE_REQUIRE, + ), + ), + ( + "b".to_string(), + link( + "D", + "B", + get_version_constraint(">=", "2.0-dev"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + package_b1 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + get_version_constraint("==", "2.1.0.0-dev"), + Link::TYPE_REQUIRE, + ), + )])); + package_b2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + get_version_constraint("==", "2.1.0.0-dev"), + Link::TYPE_REQUIRE, + ), + )])); + + package_b2 + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "d".to_string(), + link( + "B", + "D", + get_version_constraint("==", "2.0.9.0"), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request + .require_name("C", Some(get_version_constraint("==", "2.0.0.0-dev"))) + .unwrap(); + + solve_expecting_error( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + ); } -#[ignore = "setConflicts not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore = "asserts SolverProblemsException details (getProblems/getPrettyString) which solve() discards"] #[test] fn test_conflict_result_empty() { let _fixtures = set_up(); todo!() } -#[ignore = "setRequires not available on CompletePackageHandle; also asserts SolverProblemsException details which solve() discards"] +#[ignore = "asserts SolverProblemsException details (getProblems/getPrettyString) which solve() discards"] #[test] fn test_unsatisfiable_requires() { let _fixtures = set_up(); todo!() } -#[ignore = "setRequires not available on CompletePackageHandle; also asserts SolverProblemsException details which solve() discards"] +#[ignore = "asserts SolverProblemsException details (getProblems/getPrettyString) which solve() discards"] #[test] fn test_require_mismatch_exception() { let _fixtures = set_up(); todo!() } -#[ignore = "setRequires/setReplaces not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_learn_literals_with_sorted_rule_literals() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_twig2 = get_package("twig/twig", "2.0"); + fixtures.repo.add_package(package_twig2.clone()).unwrap(); + let package_twig16 = get_package("twig/twig", "1.6"); + fixtures.repo.add_package(package_twig16.clone()).unwrap(); + let package_twig15 = get_package("twig/twig", "1.5"); + fixtures.repo.add_package(package_twig15.clone()).unwrap(); + let package_symfony = get_package("symfony/symfony", "2.0"); + fixtures.repo.add_package(package_symfony.clone()).unwrap(); + let package_twig_bridge = get_package("symfony/twig-bridge", "2.0"); + fixtures + .repo + .add_package(package_twig_bridge.clone()) + .unwrap(); + + package_twig_bridge + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "twig/twig".to_string(), + link( + "symfony/twig-bridge", + "twig/twig", + get_version_constraint("<", "2.0"), + Link::TYPE_REQUIRE, + ), + )])); + + package_symfony + .as_complete_package() + .unwrap() + .__set_replaces(IndexMap::from([( + "symfony/twig-bridge".to_string(), + link( + "symfony/symfony", + "symfony/twig-bridge", + get_version_constraint("==", "2.0"), + Link::TYPE_REPLACE, + ), + )])); + + let mut request = fixtures.request; + request.require_name("symfony/twig-bridge", None).unwrap(); + request.require_name("twig/twig", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_twig16, + }, + ExpectedJob::Single { + job: "install", + package: package_twig_bridge, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_install_recursive_alias_dependencies() { - let _fixtures = set_up(); - todo!() + let 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 package_a2 = get_package("A", "2.0"); + fixtures.repo.add_package(package_a2.clone()).unwrap(); + + package_a2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "b".to_string(), + link_pretty( + "A", + "B", + get_version_constraint("==", "2.0"), + Link::TYPE_REQUIRE, + "== 2.0", + ), + )])); + package_b + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + get_version_constraint(">=", "2.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let package_a2_alias = get_alias_package(&package_a2, "1.1"); + fixtures.repo.add_package(package_a2_alias.clone()).unwrap(); + + let mut request = fixtures.request; + request + .require_name("A", Some(get_version_constraint("==", "1.1.0.0"))) + .unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ExpectedJob::Single { + job: "install", + package: package_a2, + }, + ExpectedJob::Single { + job: "markAliasInstalled", + package: package_a2_alias, + }, + ], + ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_install_dev_alias() { - let _fixtures = set_up(); - todo!() + let fixtures = set_up(); + let package_a = get_package("A", "2.0"); + fixtures.repo.add_package(package_a.clone()).unwrap(); + let package_b = get_package("B", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + + package_b + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "a".to_string(), + link( + "B", + "A", + get_version_constraint("<", "2.0"), + Link::TYPE_REQUIRE, + ), + )])); + + let package_a_alias = get_alias_package(&package_a, "1.1"); + fixtures.repo.add_package(package_a_alias.clone()).unwrap(); + + let mut request = fixtures.request; + request + .require_name("A", Some(get_version_constraint("==", "2.0"))) + .unwrap(); + request.require_name("B", None).unwrap(); + + check_solver_result( + fixtures.repo_set, + fixtures.repo, + fixtures.repo_locked, + request, + vec![ + ExpectedJob::Single { + job: "install", + package: package_a, + }, + ExpectedJob::Single { + job: "markAliasInstalled", + package: package_a_alias, + }, + ExpectedJob::Single { + job: "install", + package: package_b, + }, + ], + ); } #[ignore] @@ -699,9 +2137,200 @@ fn test_install_root_aliases_if_alias_of_is_installed() { ); } -#[ignore = "setRequires not available on CompletePackageHandle (only RootPackageHandle exposes link setters)"] +#[ignore] #[test] fn test_learn_positive_literal() { - let _fixtures = set_up(); - todo!() + let 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", "1.0"); + fixtures.repo.add_package(package_b.clone()).unwrap(); + let package_c1 = get_package("C", "1.0"); + fixtures.repo.add_package(package_c1.clone()).unwrap(); + let package_c2 = get_package("C", "2.0"); + fixtures.repo.add_package(package_c2.clone()).unwrap(); + let package_d = get_package("D", "1.0"); + fixtures.repo.add_package(package_d.clone()).unwrap(); + let package_e = get_package("E", "1.0"); + fixtures.repo.add_package(package_e.clone()).unwrap(); + let package_f1 = get_package("F", "1.0"); + fixtures.repo.add_package(package_f1.clone()).unwrap(); + let package_f2 = get_package("F", "2.0"); + fixtures.repo.add_package(package_f2.clone()).unwrap(); + let package_g1 = get_package("G", "1.0"); + fixtures.repo.add_package(package_g1.clone()).unwrap(); + let package_g2 = get_package("G", "2.0"); + fixtures.repo.add_package(package_g2.clone()).unwrap(); + let package_g3 = get_package("G", "3.0"); + fixtures.repo.add_package(package_g3.clone()).unwrap(); + + package_a + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "b".to_string(), + link( + "A", + "B", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "c".to_string(), + link( + "A", + "C", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "d".to_string(), + link( + "A", + "D", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + package_b + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "e".to_string(), + link( + "B", + "E", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + + package_c1 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "f".to_string(), + link( + "C", + "F", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + package_c2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "f".to_string(), + link( + "C", + "F", + get_version_constraint("==", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ( + "g".to_string(), + link( + "C", + "G", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + ), + ])); + + package_d + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "f".to_string(), + link( + "D", + "F", + get_version_constraint(">=", "1.0"), + Link::TYPE_REQUIRE, + ), + )])); + + package_e + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "g".to_string(), + link( + "E", + "G", + get_version_constraint("<=", "2.0"), + Link::TYPE_REQUIRE, + ), + )])); + + // reposComplete() + createSolver() inline so the testFlagLearnedPositiveLiteral flag can be + // asserted on the same solver instance used to solve. + let mut repo_set = fixtures.repo_set; + repo_set + .add_repository(RepositoryInterfaceHandle::new(fixtures.repo)) + .unwrap(); + repo_set + .add_repository(fixtures.repo_locked.into()) + .unwrap(); + + let mut request = fixtures.request; + request.require_name("A", None).unwrap(); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let pool = repo_set + .create_pool(&mut request, io.clone(), None, None, vec![], None, None) + .unwrap(); + let policy: Rc<dyn PolicyInterface> = Rc::new(DefaultPolicy::new(false, false, None)); + let mut solver = + shirabe::dependency_resolver::solver::Solver::new(policy, Rc::new(RefCell::new(pool)), io); + + // check correct setup for assertion later + assert!(!solver.test_flag_learned_positive_literal); + + let transaction = solver.solve(&request, None).unwrap(); + + let expected = vec![ + ("install".to_string(), package_f1.get_unique_name()), + ("install".to_string(), package_d.get_unique_name()), + ("install".to_string(), package_g2.get_unique_name()), + ("install".to_string(), package_c2.get_unique_name()), + ("install".to_string(), package_e.get_unique_name()), + ("install".to_string(), package_b.get_unique_name()), + ("install".to_string(), package_a.get_unique_name()), + ]; + let mut result: Vec<(String, String)> = Vec::new(); + for operation in transaction.get_operations() { + if let Some(update) = operation.as_update_operation() { + result.push(( + "update".to_string(), + format!( + "{} => {}", + update.get_initial_package().get_unique_name(), + update.get_target_package().get_unique_name() + ), + )); + } else { + let op_type = operation.get_operation_type(); + let job = if op_type == "uninstall" { + "remove".to_string() + } else { + op_type + }; + result.push((job, operation.get_package().get_unique_name())); + } + } + assert_eq!(expected, result); + + // verify that the code path leading to a negative literal resulting in a positive learned + // literal is actually executed + assert!(solver.test_flag_learned_positive_literal); } diff --git a/crates/shirabe/tests/dependency_resolver/transaction_test.rs b/crates/shirabe/tests/dependency_resolver/transaction_test.rs index a5ca310..7013816 100644 --- a/crates/shirabe/tests/dependency_resolver/transaction_test.rs +++ b/crates/shirabe/tests/dependency_resolver/transaction_test.rs @@ -1,7 +1,286 @@ //! ref: composer/tests/Composer/Test/DependencyResolver/TransactionTest.php +use indexmap::IndexMap; +use shirabe::dependency_resolver::transaction::Transaction; +use shirabe::package::Link; +use shirabe::package::handle::PackageInterfaceHandle; +use shirabe_php_shim::PhpMixed; + +use crate::test_case::{get_alias_package, get_package, get_version_constraint}; + +/// PHP `new Link($source, $target, $constraint, $type)`: prettyConstraint defaults to +/// `(string) $constraint`. +fn mk_link( + source: &str, + target: &str, + constraint: shirabe_semver::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, + ) +} + +/// Mirrors a PHP expected/actual operation entry. +#[derive(Debug)] +enum OperationEntry { + Job { + job: String, + package: PackageInterfaceHandle, + }, + Update { + from: PackageInterfaceHandle, + to: PackageInterfaceHandle, + }, +} + +impl PartialEq for OperationEntry { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + ( + Self::Job { + job: j1, + package: p1, + }, + Self::Job { + job: j2, + package: p2, + }, + ) => j1 == j2 && p1.ptr_eq(p2), + (Self::Update { from: f1, to: t1 }, Self::Update { from: f2, to: t2 }) => { + f1.ptr_eq(f2) && t1.ptr_eq(t2) + } + _ => false, + } + } +} + +fn check_transaction_operations(transaction: &Transaction, expected: Vec<OperationEntry>) { + let mut result: Vec<OperationEntry> = vec![]; + for operation in transaction.get_operations() { + if let Some(update) = operation.as_update_operation() { + result.push(OperationEntry::Update { + from: update.get_initial_package(), + to: update.get_target_package(), + }); + } else { + result.push(OperationEntry::Job { + job: operation.get_operation_type(), + package: operation.get_package(), + }); + } + } + + assert_eq!(expected, result); +} + #[test] -#[ignore = "CompletePackageHandle lacks set_type/set_requires/set_provides/set_extra; these setters exist only on RootPackageHandle, so the non-root package fixture cannot be expressed"] +#[ignore] fn test_transaction_generation_and_sorting() { - todo!() + let package_a = get_package("a/a", "dev-master"); + let package_a_alias = get_alias_package(&package_a, "1.0.x-dev"); + let package_b = get_package("b/b", "1.0.0"); + let package_e = get_package("e/e", "dev-foo"); + let package_e_alias = get_alias_package(&package_e, "1.0.x-dev"); + let package_c = get_package("c/c", "1.0.0"); + let present_packages = vec![ + package_a.clone(), + package_a_alias.clone(), + package_b.clone(), + package_e.clone(), + package_e_alias.clone(), + package_c.clone(), + ]; + + let package_b_new = get_package("b/b", "2.1.3"); + let package_d = get_package("d/d", "1.2.3"); + let package_f = get_package("f/f", "1.0.0"); + let package_f_alias1 = get_alias_package(&package_f, "dev-foo"); + let package_g = get_package("g/g", "1.0.0"); + let package_a0_first = get_package("a0/first", "1.2.3"); + let package_f_alias2 = get_alias_package(&package_f, "dev-bar"); + let plugin = get_package("x/plugin", "1.0.0"); + let plugin2_dep = get_package("x/plugin2-dep", "1.0.0"); + let plugin2 = get_package("x/plugin2", "1.0.0"); + let dl_modifying_plugin = get_package("x/downloads-modifying", "1.0.0"); + let dl_modifying_plugin2_dep = get_package("x/downloads-modifying2-dep", "1.0.0"); + let dl_modifying_plugin2 = get_package("x/downloads-modifying2", "1.0.0"); + let result_packages = vec![ + package_a.clone(), + package_a_alias.clone(), + package_b_new.clone(), + package_d.clone(), + package_f.clone(), + package_f_alias1.clone(), + package_g.clone(), + package_a0_first.clone(), + package_f_alias2.clone(), + plugin.clone(), + plugin2_dep.clone(), + plugin2.clone(), + dl_modifying_plugin.clone(), + dl_modifying_plugin2_dep.clone(), + dl_modifying_plugin2.clone(), + ]; + + plugin + .as_complete_package() + .unwrap() + .__set_type("composer-installer".to_string()); + for plugin_package in [&plugin2, &dl_modifying_plugin, &dl_modifying_plugin2] { + plugin_package + .as_complete_package() + .unwrap() + .__set_type("composer-plugin".to_string()); + } + + plugin2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "x/plugin2-dep".to_string(), + mk_link( + "x/plugin2", + "x/plugin2-dep", + get_version_constraint("=", "1.0.0"), + Link::TYPE_REQUIRE, + ), + )])); + dl_modifying_plugin2 + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([( + "x/downloads-modifying2-dep".to_string(), + mk_link( + "x/downloads-modifying2", + "x/downloads-modifying2-dep", + get_version_constraint("=", "1.0.0"), + Link::TYPE_REQUIRE, + ), + )])); + dl_modifying_plugin + .as_complete_package() + .unwrap() + .__set_extra(IndexMap::from([( + "plugin-modifies-downloads".to_string(), + PhpMixed::Bool(true), + )])); + dl_modifying_plugin2 + .as_complete_package() + .unwrap() + .__set_extra(IndexMap::from([( + "plugin-modifies-downloads".to_string(), + PhpMixed::Bool(true), + )])); + + package_d + .as_complete_package() + .unwrap() + .__set_requires(IndexMap::from([ + ( + "f/f".to_string(), + mk_link( + "d/d", + "f/f", + get_version_constraint(">", "0.2"), + Link::TYPE_REQUIRE, + ), + ), + ( + "g/provider".to_string(), + mk_link( + "d/d", + "g/provider", + get_version_constraint(">", "0.2"), + Link::TYPE_REQUIRE, + ), + ), + ])); + package_g + .as_complete_package() + .unwrap() + .__set_provides(IndexMap::from([( + "g/provider".to_string(), + mk_link( + "g/g", + "g/provider", + get_version_constraint("==", "1.0.0"), + Link::TYPE_PROVIDE, + ), + )])); + + let expected_operations = vec![ + OperationEntry::Job { + job: "uninstall".to_string(), + package: package_c.clone(), + }, + OperationEntry::Job { + job: "uninstall".to_string(), + package: package_e.clone(), + }, + OperationEntry::Job { + job: "markAliasUninstalled".to_string(), + package: package_e_alias.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: dl_modifying_plugin.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: dl_modifying_plugin2_dep.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: dl_modifying_plugin2.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: plugin.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: plugin2_dep.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: plugin2.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: package_a0_first.clone(), + }, + OperationEntry::Update { + from: package_b.clone(), + to: package_b_new.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: package_g.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: package_f.clone(), + }, + OperationEntry::Job { + job: "markAliasInstalled".to_string(), + package: package_f_alias2.clone(), + }, + OperationEntry::Job { + job: "markAliasInstalled".to_string(), + package: package_f_alias1.clone(), + }, + OperationEntry::Job { + job: "install".to_string(), + package: package_d.clone(), + }, + ]; + + let transaction = Transaction::new(present_packages, result_packages); + check_transaction_operations(&transaction, expected_operations); } diff --git a/crates/shirabe/tests/downloader/archive_downloader_test.rs b/crates/shirabe/tests/downloader/archive_downloader_test.rs index dfba3bf..7062d48 100644 --- a/crates/shirabe/tests/downloader/archive_downloader_test.rs +++ b/crates/shirabe/tests/downloader/archive_downloader_test.rs @@ -1,41 +1,174 @@ //! ref: composer/tests/Composer/Test/Downloader/ArchiveDownloaderTest.php -// These exercise getFileName/processUrl on an anonymous ArchiveDownloader subclass built -// with a mocked IO and a real HttpDownloader (curl_multi_init is todo!()), and pass mocked -// packages. +// The PHP test builds an anonymous ArchiveDownloader subclass; getFileName/processUrl are +// inherited unchanged from FileDownloader, so the concrete FileDownloader is exercised here +// directly. The PHP mocks of IOInterface/Config/PackageInterface are replaced by a NullIO, a +// real Config merged with `vendor-dir`, and real CompletePackage instances with dist +// url/reference set. + +use std::cell::RefCell; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::downloader::FileDownloader; +use shirabe::io::IOInterface; +use shirabe::io::null_io::NullIO; +use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle}; +use shirabe::util::HttpDownloader; +use shirabe_php_shim::PhpMixed; +use shirabe_semver::version_parser::VersionParser; + +/// ref: TestCase::getPackage (default class CompletePackage) +fn get_package(name: &str, version: &str) -> PackageInterfaceHandle { + let norm_version = VersionParser.normalize(version, None).unwrap(); + CompletePackageHandle::new(name.to_string(), norm_version, version.to_string()).into() +} + +/// ref: ArchiveDownloaderTest::getArchiveDownloaderMock (the inherited getFileName/processUrl +/// live on FileDownloader, so the concrete downloader is built directly). +fn get_archive_downloader(vendor_dir: Option<&str>) -> FileDownloader { + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + + let mut config = Config::new(false, None); + if let Some(vendor_dir) = vendor_dir { + let mut config_options: IndexMap<String, PhpMixed> = IndexMap::new(); + config_options.insert( + "vendor-dir".to_string(), + PhpMixed::String(vendor_dir.to_string()), + ); + let mut merged: IndexMap<String, PhpMixed> = IndexMap::new(); + merged.insert("config".to_string(), PhpMixed::Array(config_options)); + config.merge(&merged, "test"); + } + let config = Rc::new(RefCell::new(config)); + + let http_downloader = Rc::new(RefCell::new(HttpDownloader::new( + io.clone(), + config.clone(), + IndexMap::new(), + false, + ))); + + FileDownloader::new(io, config, http_downloader, None, None, None, None) +} #[test] -#[ignore = "FileDownloader::get_file_name is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_get_file_name() { - todo!() + let package = get_package("dummy/pkg", "1.0.0"); + package.set_dist_url(Some("http://example.com/script.js".to_string())); + + let downloader = get_archive_downloader(Some("/vendor")); + + let first = downloader.__get_file_name(package.clone(), "/path"); + let re = regex::Regex::new(r"/vendor/composer/tmp-[a-z0-9]+\.js").unwrap(); + assert!(re.is_match(&first)); + assert_eq!(first, downloader.__get_file_name(package, "/path")); } #[test] -#[ignore = "FileDownloader::process_url is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_process_url() { - todo!() + let downloader = get_archive_downloader(None); + + let expected = "https://github.com/composer/composer/zipball/master"; + let package = get_package("dummy/pkg", "1.0.0"); + let url = downloader.__process_url(package, expected).unwrap(); + + assert_eq!(expected, url); } #[test] -#[ignore = "FileDownloader::process_url is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_process_url2() { - todo!() + let downloader = get_archive_downloader(None); + + let expected = "https://github.com/composer/composer/archive/master.tar.gz"; + let package = get_package("dummy/pkg", "1.0.0"); + let url = downloader.__process_url(package, expected).unwrap(); + + assert_eq!(expected, url); } #[test] -#[ignore = "FileDownloader::process_url is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_process_url3() { - todo!() + let downloader = get_archive_downloader(None); + + let expected = "https://api.github.com/repos/composer/composer/zipball/master"; + let package = get_package("dummy/pkg", "1.0.0"); + let url = downloader.__process_url(package, expected).unwrap(); + + assert_eq!(expected, url); +} + +/// ref: ArchiveDownloaderTest::provideUrls +fn provide_urls() -> Vec<&'static str> { + vec![ + "https://api.github.com/repos/composer/composer/zipball/master", + "https://api.github.com/repos/composer/composer/tarball/master", + "https://github.com/composer/composer/zipball/master", + "https://www.github.com/composer/composer/tarball/master", + "https://github.com/composer/composer/archive/master.zip", + "https://github.com/composer/composer/archive/master.tar.gz", + ] } #[test] -#[ignore = "FileDownloader::process_url is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_process_url_rewrite_dist() { - todo!() + let downloader = get_archive_downloader(None); + + for url in provide_urls() { + let r#type = if url.contains("tar") { "tar" } else { "zip" }; + let expected = format!( + "https://api.github.com/repos/composer/composer/{}ball/ref", + r#type + ); + + let package = get_package("dummy/pkg", "1.0.0"); + package.set_dist_reference(Some("ref".to_string())); + let url = downloader.__process_url(package, url).unwrap(); + + assert_eq!(expected, url); + } +} + +/// ref: ArchiveDownloaderTest::provideBitbucketUrls +fn provide_bitbucket_urls() -> Vec<(&'static str, &'static str)> { + vec![ + ( + "https://bitbucket.org/davereid/drush-virtualhost/get/77ca490c26ac818e024d1138aa8bd3677d1ef21f", + "zip", + ), + ( + "https://bitbucket.org/davereid/drush-virtualhost/get/master", + "tar.gz", + ), + ( + "https://bitbucket.org/davereid/drush-virtualhost/get/v1.0", + "tar.bz2", + ), + ] } #[test] -#[ignore = "FileDownloader::process_url is pub(crate); not reachable from the integration test crate"] +#[ignore] fn test_process_url_rewrite_bitbucket_dist() { - todo!() + let downloader = get_archive_downloader(None); + + for (url, extension) in provide_bitbucket_urls() { + let url = format!("{}.{}", url, extension); + let expected = format!( + "https://bitbucket.org/davereid/drush-virtualhost/get/ref.{}", + extension + ); + + let package = get_package("dummy/pkg", "1.0.0"); + package.set_dist_reference(Some("ref".to_string())); + let url = downloader.__process_url(package, &url).unwrap(); + + assert_eq!(expected, url); + } } diff --git a/crates/shirabe/tests/io/buffer_io_test.rs b/crates/shirabe/tests/io/buffer_io_test.rs index 62abe0a..7205808 100644 --- a/crates/shirabe/tests/io/buffer_io_test.rs +++ b/crates/shirabe/tests/io/buffer_io_test.rs @@ -6,7 +6,7 @@ use shirabe_external_packages::symfony::console::output::output_interface::VERBO use shirabe_php_shim::PhpMixed; #[test] -#[ignore = "BufferIO::set_user_inputs is todo!() (needs StreamableInputInterface downcast wiring)"] +#[ignore] fn test_set_user_inputs() { let mut buffer_io = BufferIO::new(String::new(), VERBOSITY_NORMAL, None).unwrap(); diff --git a/crates/shirabe/tests/json/json_file_test.rs b/crates/shirabe/tests/json/json_file_test.rs index e8d87a3..032d0fd 100644 --- a/crates/shirabe/tests/json/json_file_test.rs +++ b/crates/shirabe/tests/json/json_file_test.rs @@ -54,70 +54,70 @@ fn create_temp_file() -> String { } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_extra_comma() { let json = "{\n \"foo\": \"bar\",\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_extra_comma_in_array() { let json = "{\n \"foo\": [\n \"bar\",\n ]\n}"; expect_parse_exception("Parse error on line 3", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_unescaped_backslash() { let json = "{\n \"fo\\o\": \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_skips_escaped_backslash() { let json = "{\n \"fo\\\\o\": \"bar\"\n \"a\": \"b\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_single_quotes() { let json = "{\n 'foo': \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_missing_quotes() { let json = "{\n foo: \"bar\"\n}"; expect_parse_exception("Parse error on line 1", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_array_as_hash() { let json = "{\n \"foo\": [\"bar\": \"baz\"]\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_missing_comma() { let json = "{\n \"foo\": \"bar\"\n \"bar\": \"foo\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_missing_comma_multiline() { let json = "{\n \"foo\": \"barbar\"\n\n \"bar\": \"foo\"\n}"; expect_parse_exception("Parse error on line 2", json); } #[test] -#[ignore = "JsonFile::parse_json error path reaches JsonParser::new()/lint() (todo!()); the JSON linter is not yet ported"] +#[ignore] fn test_parse_error_detect_missing_colon() { let json = "{\n \"foo\": \"bar\",\n \"bar\" \"foo\"\n}"; expect_parse_exception("Parse error on line 3", json); diff --git a/crates/shirabe/tests/package/archiver/git_exclude_filter_test.rs b/crates/shirabe/tests/package/archiver/git_exclude_filter_test.rs index 431cb4f..80f9f31 100644 --- a/crates/shirabe/tests/package/archiver/git_exclude_filter_test.rs +++ b/crates/shirabe/tests/package/archiver/git_exclude_filter_test.rs @@ -3,7 +3,7 @@ use shirabe::package::archiver::git_exclude_filter::GitExcludeFilter; #[test] -#[ignore = "Preg::split panics on the `\\s+` pattern in the regex-crate-based shim"] +#[ignore] fn test_pattern_escape() { for (ignore, expected) in provide_patterns() { let filter = GitExcludeFilter::new("/".to_string()); diff --git a/crates/shirabe/tests/package/root_alias_package_test.rs b/crates/shirabe/tests/package/root_alias_package_test.rs index f168e13..c275da3 100644 --- a/crates/shirabe/tests/package/root_alias_package_test.rs +++ b/crates/shirabe/tests/package/root_alias_package_test.rs @@ -37,7 +37,7 @@ fn test_update_requires() { } #[test] -#[ignore = "RootAliasPackage::set_dev_requires only updates alias_of, not the alias's own copy, so get_dev_requires stays empty"] +#[ignore] fn test_update_dev_requires() { let alias = alias(); assert!(alias.get_dev_requires().is_empty()); @@ -46,7 +46,7 @@ fn test_update_dev_requires() { } #[test] -#[ignore = "RootAliasPackage::set_conflicts only updates alias_of, not the alias's own copy, so get_conflicts stays empty"] +#[ignore] fn test_update_conflicts() { let alias = alias(); assert!(alias.get_conflicts().is_empty()); @@ -55,7 +55,7 @@ fn test_update_conflicts() { } #[test] -#[ignore = "RootAliasPackage::set_provides only updates alias_of, not the alias's own copy, so get_provides stays empty"] +#[ignore] fn test_update_provides() { let alias = alias(); assert!(alias.get_provides().is_empty()); @@ -64,7 +64,7 @@ fn test_update_provides() { } #[test] -#[ignore = "RootAliasPackage::set_replaces only updates alias_of, not the alias's own copy, so get_replaces stays empty"] +#[ignore] fn test_update_replaces() { let alias = alias(); assert!(alias.get_replaces().is_empty()); diff --git a/crates/shirabe/tests/package/version/version_bumper_test.rs b/crates/shirabe/tests/package/version/version_bumper_test.rs index 08217f7..2c376fa 100644 --- a/crates/shirabe/tests/package/version/version_bumper_test.rs +++ b/crates/shirabe/tests/package/version/version_bumper_test.rs @@ -8,7 +8,7 @@ use shirabe_php_shim::PhpMixed; use shirabe_semver::version_parser::VersionParser; #[test] -#[ignore = "VersionBumper::bump_requirement uses a look-around regex the regex crate cannot compile"] +#[ignore] fn test_bump_requirement() { for (requirement, pretty_version, expected_requirement, branch_alias) in provide_bump_requirement_tests() diff --git a/crates/shirabe/tests/package/version/version_parser_test.rs b/crates/shirabe/tests/package/version/version_parser_test.rs index c403580..2e93d03 100644 --- a/crates/shirabe/tests/package/version/version_parser_test.rs +++ b/crates/shirabe/tests/package/version/version_parser_test.rs @@ -15,7 +15,7 @@ fn entry(fields: &[(&str, &str)]) -> IndexMap<String, String> { } #[test] -#[ignore = "parseNameVersionPairs uses a lookbehind/lookahead pattern the regex crate cannot compile"] +#[ignore] fn test_parse_name_version_pairs() { for (input, result) in provide_parse_name_version_pairs_data() { let version_parser = VersionParser::new(); diff --git a/crates/shirabe/tests/platform/version_test.rs b/crates/shirabe/tests/platform/version_test.rs index c2511d5..3fe0259 100644 --- a/crates/shirabe/tests/platform/version_test.rs +++ b/crates/shirabe/tests/platform/version_test.rs @@ -57,7 +57,7 @@ fn provide_openssl_versions() -> Vec<(&'static str, &'static str, bool, Option<& } #[test] -#[ignore = "compile_php_pattern in the php-shim cannot yet parse Version's PCRE patterns"] +#[ignore] fn test_parse_openssl_versions() { for (input, parsed_version, fips_expected, normalized_version) in provide_openssl_versions() { let mut is_fips = false; @@ -76,7 +76,7 @@ fn test_parse_openssl_versions() { } #[test] -#[ignore = "compile_php_pattern in the php-shim cannot yet parse Version's PCRE patterns"] +#[ignore] fn test_parse_libjpeg_version() { let cases = [ ("9", "9.0"), @@ -95,7 +95,7 @@ fn test_parse_libjpeg_version() { } #[test] -#[ignore = "compile_php_pattern in the php-shim cannot yet parse Version's PCRE patterns"] +#[ignore] fn test_parse_zoneinfo_version() { let cases = [ ("2019c", "2019.3"), diff --git a/crates/shirabe/tests/repository/array_repository_test.rs b/crates/shirabe/tests/repository/array_repository_test.rs index ee6dfb9..60bea16 100644 --- a/crates/shirabe/tests/repository/array_repository_test.rs +++ b/crates/shirabe/tests/repository/array_repository_test.rs @@ -100,7 +100,7 @@ fn test_find_packages() { } #[test] -#[ignore = "AliasPackage::get_unique_name returns the aliased package's name, so has_package misses the alias version"] +#[ignore] fn test_automatically_add_aliased_package_but_not_remove() { let mut repo = ArrayRepository::new(vec![]).unwrap(); diff --git a/crates/shirabe/tests/repository/artifact_repository_test.rs b/crates/shirabe/tests/repository/artifact_repository_test.rs index 14475da..765bdac 100644 --- a/crates/shirabe/tests/repository/artifact_repository_test.rs +++ b/crates/shirabe/tests/repository/artifact_repository_test.rs @@ -1,9 +1,12 @@ //! ref: composer/tests/Composer/Test/Repository/ArtifactRepositoryTest.php -// ArtifactRepository::getPackages scans the fixture directory and opens each archive via -// ZipArchive / PharData, both of which are todo!() in the php-shim. +use std::cell::RefCell; +use std::rc::Rc; -use shirabe_php_shim::extension_loaded; +use indexmap::IndexMap; +use shirabe::io::{IOInterface, NullIO}; +use shirabe::repository::ArtifactRepository; +use shirabe_php_shim::{PhpMixed, extension_loaded}; fn set_up() { if !extension_loaded("zip") { @@ -12,23 +15,104 @@ fn set_up() { } } +fn artifacts_dir() -> String { + format!( + "{}/../../composer/tests/Composer/Test/Repository/Fixtures/artifacts", + env!("CARGO_MANIFEST_DIR") + ) +} + +fn create_repo(url: &str) -> ArtifactRepository { + let mut coordinates: IndexMap<String, PhpMixed> = IndexMap::new(); + coordinates.insert("type".to_string(), PhpMixed::String("artifact".to_string())); + coordinates.insert("url".to_string(), PhpMixed::String(url.to_string())); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + ArtifactRepository::new(coordinates, io).unwrap() +} + #[test] -#[ignore = "ArtifactRepository exposes no public get_packages(); it does not impl RepositoryInterface and initialize/scan_directory are private"] +#[ignore] fn test_extracts_configs_from_zip_archives() { set_up(); - todo!() + + let mut expected_packages = vec![ + "vendor0/package0-0.0.1".to_string(), + "composer/composer-1.0.0-alpha6".to_string(), + "vendor1/package2-4.3.2".to_string(), + "vendor3/package1-5.4.3".to_string(), + "test/jsonInRoot-1.0.0".to_string(), + "test/jsonInRootTarFile-1.0.0".to_string(), + "test/jsonInFirstLevel-1.0.0".to_string(), + // The files not-an-artifact.zip and jsonSecondLevel are not valid + // artifacts and do not get detected. + ]; + + let mut repo = create_repo(&artifacts_dir()); + + let mut found_packages: Vec<String> = repo + .__get_packages() + .unwrap() + .iter() + .map(|package| { + format!( + "{}-{}", + package.get_pretty_name(), + package.get_pretty_version() + ) + }) + .collect(); + + expected_packages.sort(); + found_packages.sort(); + + assert_eq!(expected_packages, found_packages); + + let tar_package: Vec<_> = repo + .__get_packages() + .unwrap() + .into_iter() + .filter(|package| package.get_pretty_name() == "test/jsonInRootTarFile") + .collect(); + assert_eq!(1, tar_package.len()); + let tar_package = tar_package.into_iter().next_back().unwrap(); + assert_eq!(Some("tar".to_string()), tar_package.get_dist_type()); } #[test] -#[ignore = "ArtifactRepository exposes no public get_packages(); it does not impl RepositoryInterface and initialize/scan_directory are private"] +#[ignore] fn test_absolute_repo_url_creates_absolute_url_packages() { set_up(); - todo!() + + let absolute_path = artifacts_dir(); + let mut repo = create_repo(&absolute_path); + + for package in repo.__get_packages().unwrap() { + assert_eq!( + package + .get_dist_url() + .unwrap_or_default() + .find(&absolute_path.replace('\\', "/")), + Some(0) + ); + } } #[test] -#[ignore = "ArtifactRepository exposes no public get_packages(); it does not impl RepositoryInterface and initialize/scan_directory are private"] +#[ignore] fn test_relative_repo_url_creates_relative_url_packages() { set_up(); - todo!() + + let relative_path = "tests/Composer/Test/Repository/Fixtures/artifacts"; + let mut repo = create_repo(relative_path); + + for package in repo.__get_packages().unwrap() { + assert_eq!( + package + .get_dist_url() + .unwrap_or_default() + .find(relative_path), + Some(0) + ); + } } diff --git a/crates/shirabe/tests/repository/composite_repository_test.rs b/crates/shirabe/tests/repository/composite_repository_test.rs index e21ac65..f01a3fd 100644 --- a/crates/shirabe/tests/repository/composite_repository_test.rs +++ b/crates/shirabe/tests/repository/composite_repository_test.rs @@ -27,7 +27,7 @@ fn test_has_package() { } #[test] -#[ignore = "constraint parsing uses a look-around regex the regex crate does not support"] +#[ignore] fn test_find_package() { let mut repo = CompositeRepository::new(vec![ array_repo(vec![get_package("foo", "1")]), diff --git a/crates/shirabe/tests/repository/installed_repository_test.rs b/crates/shirabe/tests/repository/installed_repository_test.rs index b2c88a6..6720260 100644 --- a/crates/shirabe/tests/repository/installed_repository_test.rs +++ b/crates/shirabe/tests/repository/installed_repository_test.rs @@ -34,7 +34,7 @@ fn provided_link() -> PhpMixed { } #[test] -#[ignore = "InstalledRepository::add_repository asserts on a fixed set of repo types that omits InstalledRepositoryInterface, so adding an InstalledArrayRepository panics"] +#[ignore] fn test_find_packages_with_replacers_and_providers() { let foo = loaded("foo", "1", vec![("replace", provided_link())]); let foo2 = get_package("foo", "2"); diff --git a/crates/shirabe/tests/repository/path_repository_test.rs b/crates/shirabe/tests/repository/path_repository_test.rs index c2cf892..cea6a85 100644 --- a/crates/shirabe/tests/repository/path_repository_test.rs +++ b/crates/shirabe/tests/repository/path_repository_test.rs @@ -1,54 +1,253 @@ //! ref: composer/tests/Composer/Test/Repository/PathRepositoryTest.php -// PathRepository does not implement RepositoryInterface and exposes no public -// getPackages/count/hasPackage; the delegation to its inner ArrayRepository is not yet -// ported, so there is no way to drive these tests. The unversioned cases additionally -// require the VersionGuesser (git). +use std::cell::RefCell; +use std::rc::Rc; -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::io::{IOInterface, NullIO}; +use shirabe::repository::PathRepository; +use shirabe::util::{Platform, ProcessExecutor}; +use shirabe_php_shim::{ + DIRECTORY_SEPARATOR, PhpMixed, file_get_contents, hash, realpath, serialize, +}; + +use crate::test_case::get_package; + +fn fixtures_dir() -> String { + format!( + "{}/../../composer/tests/Composer/Test/Repository/Fixtures", + env!("CARGO_MANIFEST_DIR") + ) +} + +/// ref: PathRepositoryTest::createPathRepo +fn create_path_repo(options: IndexMap<String, PhpMixed>) -> PathRepository { + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + + let config = Rc::new(RefCell::new(Config::new(true, None))); + let proc = Rc::new(RefCell::new(ProcessExecutor::new(None))); + + PathRepository::new(options, io, config, None, None, Some(proc)).unwrap() +} + +fn coordinates(pairs: Vec<(&str, PhpMixed)>) -> IndexMap<String, PhpMixed> { + let mut map: IndexMap<String, PhpMixed> = IndexMap::new(); + for (key, value) in pairs { + map.insert(key.to_string(), value); + } + map +} + +#[ignore] #[test] fn test_load_package_from_file_system_with_incorrect_path() { - todo!() + let repository_url = + [fixtures_dir(), "path".to_string(), "missing".to_string()].join(DIRECTORY_SEPARATOR); + let mut repository = + create_path_repo(coordinates(vec![("url", PhpMixed::String(repository_url))])); + assert!(repository.__get_packages().is_err()); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_load_package_from_file_system_with_version() { - todo!() + let repository_url = [ + fixtures_dir(), + "path".to_string(), + "with-version".to_string(), + ] + .join(DIRECTORY_SEPARATOR); + let mut repository = + create_path_repo(coordinates(vec![("url", PhpMixed::String(repository_url))])); + repository.__get_packages().unwrap(); + + assert_eq!(1, repository.__count().unwrap()); + assert!( + repository + .__has_package(get_package("test/path-versioned", "0.0.2")) + .unwrap() + ); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_load_package_from_file_system_without_version() { - todo!() + let repository_url = [ + fixtures_dir(), + "path".to_string(), + "without-version".to_string(), + ] + .join(DIRECTORY_SEPARATOR); + let mut repository = + create_path_repo(coordinates(vec![("url", PhpMixed::String(repository_url))])); + let packages = repository.__get_packages().unwrap(); + + assert!(repository.__count().unwrap() >= 1); + + let package = &packages[0]; + assert_eq!("test/path-unversioned", package.get_name()); + + let package_version = package.get_version(); + assert!(!package_version.is_empty()); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_load_package_from_file_system_with_wildcard() { - todo!() + let repository_url = + [fixtures_dir(), "path".to_string(), "*".to_string()].join(DIRECTORY_SEPARATOR); + let mut repository = + create_path_repo(coordinates(vec![("url", PhpMixed::String(repository_url))])); + let packages = repository.__get_packages().unwrap(); + let mut names: Vec<String> = Vec::new(); + + assert!(repository.__count().unwrap() >= 2); + + let package = &packages[0]; + names.push(package.get_name()); + + let package = &packages[1]; + names.push(package.get_name()); + + names.sort(); + assert_eq!( + vec![ + "test/path-unversioned".to_string(), + "test/path-versioned".to_string() + ], + names + ); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_load_package_with_explicit_versions() { - todo!() + let mut versions: IndexMap<String, PhpMixed> = IndexMap::new(); + versions.insert( + "test/path-unversioned".to_string(), + PhpMixed::String("4.3.2.1".to_string()), + ); + versions.insert( + "test/path-versioned".to_string(), + PhpMixed::String("3.2.1.0".to_string()), + ); + let options = coordinates(vec![("versions", PhpMixed::Array(versions))]); + + let repository_url = + [fixtures_dir(), "path".to_string(), "*".to_string()].join(DIRECTORY_SEPARATOR); + let mut repository = create_path_repo(coordinates(vec![ + ("url", PhpMixed::String(repository_url)), + ("options", PhpMixed::Array(options)), + ])); + let packages = repository.__get_packages().unwrap(); + + let mut versions: IndexMap<String, String> = IndexMap::new(); + + assert_eq!(2, repository.__count().unwrap()); + + let package = &packages[0]; + versions.insert(package.get_name(), package.get_version()); + + let package = &packages[1]; + versions.insert(package.get_name(), package.get_version()); + + versions.sort_keys(); + let expected: IndexMap<String, String> = [ + ("test/path-unversioned".to_string(), "4.3.2.1".to_string()), + ("test/path-versioned".to_string(), "3.2.1.0".to_string()), + ] + .into_iter() + .collect(); + assert_eq!(expected, versions); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +/// Verify relative repository URLs remain relative, see #4439 +#[ignore] #[test] fn test_url_remains_relative() { - todo!() + // realpath() does not fully expand the paths + // PHP Bug https://bugs.php.net/bug.php?id=72642 + let repository_url = [ + realpath(&realpath(&fixtures_dir().replace("/Fixtures", "")).unwrap_or_default()) + .unwrap_or_default(), + "Fixtures".to_string(), + "path".to_string(), + "with-version".to_string(), + ] + .join(DIRECTORY_SEPARATOR); + // getcwd() not necessarily match __DIR__ + // PHP Bug https://bugs.php.net/bug.php?id=73797 + let cwd = realpath(&realpath(&Platform::get_cwd(false).unwrap()).unwrap_or_default()) + .unwrap_or_default(); + let relative_url = repository_url[cwd.len().min(repository_url.len())..] + .trim_start_matches(DIRECTORY_SEPARATOR) + .to_string(); + + let mut repository = create_path_repo(coordinates(vec![( + "url", + PhpMixed::String(relative_url.clone()), + )])); + let packages = repository.__get_packages().unwrap(); + + assert_eq!(1, repository.__count().unwrap()); + + let package = &packages[0]; + assert_eq!("test/path-versioned", package.get_name()); + + // Convert platform specific separators back to generic URL slashes + let relative_url = relative_url.replace(DIRECTORY_SEPARATOR, "/"); + assert_eq!(Some(relative_url), package.get_dist_url()); } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_reference_none() { - todo!() + let options = coordinates(vec![("reference", PhpMixed::String("none".to_string()))]); + let repository_url = + [fixtures_dir(), "path".to_string(), "*".to_string()].join(DIRECTORY_SEPARATOR); + let mut repository = create_path_repo(coordinates(vec![ + ("url", PhpMixed::String(repository_url)), + ("options", PhpMixed::Array(options)), + ])); + let packages = repository.__get_packages().unwrap(); + + assert!(repository.__count().unwrap() >= 2); + + for package in &packages { + assert_eq!(package.get_dist_reference(), None); + } } -#[ignore = "PathRepository exposes no public RepositoryInterface::get_packages/count/has_package; the inner ArrayRepository delegation is not ported"] +#[ignore] #[test] fn test_reference_config() { - todo!() + let options = coordinates(vec![ + ("reference", PhpMixed::String("config".to_string())), + ("relative", PhpMixed::Bool(true)), + ]); + let repository_url = + [fixtures_dir(), "path".to_string(), "*".to_string()].join(DIRECTORY_SEPARATOR); + let mut repository = create_path_repo(coordinates(vec![ + ("url", PhpMixed::String(repository_url)), + ("options", PhpMixed::Array(options.clone())), + ])); + let packages = repository.__get_packages().unwrap(); + + assert!(repository.__count().unwrap() >= 2); + + for package in &packages { + let dist_url = package.get_dist_url().unwrap_or_default(); + assert_eq!( + package.get_dist_reference(), + Some(hash( + "sha1", + &format!( + "{}{}", + file_get_contents(format!("{}/composer.json", dist_url)).unwrap_or_default(), + serialize(&PhpMixed::Array(options.clone())) + ) + )) + ); + } } diff --git a/crates/shirabe/tests/repository/repository_factory_test.rs b/crates/shirabe/tests/repository/repository_factory_test.rs index a0eb81f..c018c45 100644 --- a/crates/shirabe/tests/repository/repository_factory_test.rs +++ b/crates/shirabe/tests/repository/repository_factory_test.rs @@ -1,13 +1,57 @@ //! ref: composer/tests/Composer/Test/Repository/RepositoryFactoryTest.php +use std::cell::RefCell; +use std::rc::Rc; + use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::io::IOInterface; +use shirabe::io::null_io::NullIO; use shirabe::repository::RepositoryFactory; +use shirabe::util::http_downloader::HttpDownloader; use shirabe_php_shim::PhpMixed; #[test] -#[ignore = "PHP test uses ReflectionProperty to read the private RepositoryManager::repository_classes field; no public accessor for repository_classes keys exists in the Rust impl"] +#[ignore] fn test_manager_with_all_repository_types() { - todo!() + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let config = Rc::new(RefCell::new(Config::new(false, None))); + let http_downloader = Rc::new(RefCell::new(HttpDownloader::new( + io.clone(), + config.clone(), + IndexMap::new(), + true, + ))); + + let manager = + RepositoryFactory::manager(io, &config, Some(http_downloader), None, None).unwrap(); + + let repository_classes: Vec<&str> = manager + .__repository_classes() + .keys() + .map(|k| k.as_str()) + .collect(); + + assert_eq!( + vec![ + "composer", + "vcs", + "package", + "pear", + "git", + "bitbucket", + "git-bitbucket", + "github", + "gitlab", + "svn", + "fossil", + "perforce", + "hg", + "artifact", + "path", + ], + repository_classes + ); } fn generate_repository_name_provider() -> Vec<( @@ -53,7 +97,7 @@ fn generate_repository_name_provider() -> Vec<( } #[test] -#[ignore = "generate_repository_name does not stringify an integer index (PhpMixed::as_string returns None for Int), so a numeric index with no url yields \"\" instead of e.g. \"0\""] +#[ignore] fn test_generate_repository_name() { for (index, repo_pairs, existing_keys, expected) in generate_repository_name_provider() { let repo: IndexMap<String, PhpMixed> = repo_pairs diff --git a/crates/shirabe/tests/repository/repository_utils_test.rs b/crates/shirabe/tests/repository/repository_utils_test.rs index 3f55bfd..3ea1439 100644 --- a/crates/shirabe/tests/repository/repository_utils_test.rs +++ b/crates/shirabe/tests/repository/repository_utils_test.rs @@ -153,7 +153,7 @@ fn provide_filter_require_tests() -> Vec<FilterCase> { } #[test] -#[ignore = "building packages with links via ArrayLoader parses constraints with a look-around regex the regex crate cannot compile"] +#[ignore] fn test_filter_required_packages() { for case in provide_filter_require_tests() { let pkgs = build_packages(); diff --git a/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs b/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs index 0836fc5..ea130ea 100644 --- a/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs @@ -76,7 +76,7 @@ fn supports_provider() -> Vec<(bool, &'static str)> { } #[test] -#[ignore = "ForgejoDriver::supports uses a regex with a character class the regex crate cannot compile (unclosed character class)"] +#[ignore] fn test_supports() { for (expected, repo_url) in supports_provider() { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); diff --git a/crates/shirabe/tests/repository/vcs/github_driver_test.rs b/crates/shirabe/tests/repository/vcs/github_driver_test.rs index 5313c0b..f5958a4 100644 --- a/crates/shirabe/tests/repository/vcs/github_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/github_driver_test.rs @@ -64,7 +64,7 @@ fn supports_provider() -> Vec<(bool, &'static str)> { } #[test] -#[ignore = "GitHubDriver::supports reaches non-strict in_array, which is todo!() in the php-shim"] +#[ignore] fn test_supports() { let SetUp { home, config: _ } = set_up(); let _tear_down = TearDown::new(home.path().to_path_buf()); diff --git a/crates/shirabe/tests/script/event_test.rs b/crates/shirabe/tests/script/event_test.rs index b045bc4..e7e574b 100644 --- a/crates/shirabe/tests/script/event_test.rs +++ b/crates/shirabe/tests/script/event_test.rs @@ -7,6 +7,7 @@ use indexmap::IndexMap; use shirabe::composer::{ComposerHandle, PartialOrFullComposer}; use shirabe::config::Config; use shirabe::event_dispatcher::Event as BaseEvent; +use shirabe::event_dispatcher::EventInterface; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; use shirabe::package::{RootPackageHandle, RootPackageInterfaceHandle}; @@ -28,7 +29,7 @@ fn create_composer_instance() -> ComposerHandle { } #[test] -#[ignore = "Event::calculate_originating_event (called by set_originating_event) is todo!()"] +#[ignore] fn test_event_sets_originating_event() { let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); let composer = create_composer_instance(); @@ -51,7 +52,14 @@ fn test_event_sets_originating_event() { script_event.set_originating_event(originating_event); - assert!(script_event.get_originating_event().is_some()); + // assertSame: the originating event passed in is returned unchanged. + assert_eq!( + script_event + .get_originating_event() + .map(EventInterface::get_name), + Some("originatingEvent"), + "getOriginatingEvent() SHOULD return test event" + ); } // In PHP, the intermediate originating event is itself a Script\Event, and diff --git a/crates/shirabe/tests/util/config_validator_test.rs b/crates/shirabe/tests/util/config_validator_test.rs index da962e0..2e4aed0 100644 --- a/crates/shirabe/tests/util/config_validator_test.rs +++ b/crates/shirabe/tests/util/config_validator_test.rs @@ -29,7 +29,7 @@ fn validate(file: &str) -> Vec<String> { /// Test ConfigValidator warns on commit reference #[test] -#[ignore = "ConfigValidator::validate reaches a todo!() in the php-shim"] +#[ignore] fn test_config_validator_commit_ref_warning() { let warnings = validate(&fixture("composer_commit-ref.json")); @@ -39,7 +39,7 @@ fn test_config_validator_commit_ref_warning() { } #[test] -#[ignore = "ConfigValidator::validate reaches a todo!() in the php-shim"] +#[ignore] fn test_config_validator_warns_on_script_description_for_nonexistent_script() { let warnings = validate(&fixture("composer_scripts-descriptions.json")); @@ -52,7 +52,7 @@ fn test_config_validator_warns_on_script_description_for_nonexistent_script() { } #[test] -#[ignore = "ConfigValidator::validate reaches a todo!() in the php-shim"] +#[ignore] fn test_config_validator_warns_on_script_alias_for_nonexistent_script() { let warnings = validate(&fixture("composer_scripts-aliases.json")); @@ -62,7 +62,7 @@ fn test_config_validator_warns_on_script_alias_for_nonexistent_script() { } #[test] -#[ignore = "ConfigValidator::validate reaches a todo!() in the php-shim"] +#[ignore] fn test_config_validator_warns_on_unnecessary_provide_replace() { let warnings = validate(&fixture("composer_provide-replace-requirements.json")); diff --git a/crates/shirabe/tests/util/forgejo_url_test.rs b/crates/shirabe/tests/util/forgejo_url_test.rs index e29c05e..5c33596 100644 --- a/crates/shirabe/tests/util/forgejo_url_test.rs +++ b/crates/shirabe/tests/util/forgejo_url_test.rs @@ -3,7 +3,7 @@ use shirabe::util::forgejo_url::ForgejoUrl; #[test] -#[ignore = "Preg::match panics: ForgejoUrl::URL_REGEX is an undelimited pattern"] +#[ignore] fn test_create() { for repo_url in create_provider() { let forgejo_url = ForgejoUrl::try_from(Some(repo_url)); @@ -29,13 +29,13 @@ fn create_provider() -> Vec<&'static str> { } #[test] -#[ignore = "Preg::match panics: ForgejoUrl::URL_REGEX is an undelimited pattern"] +#[ignore] fn test_create_invalid() { assert!(ForgejoUrl::create("https://example.org").is_err()); } #[test] -#[ignore = "Preg::match panics: ForgejoUrl::URL_REGEX is an undelimited pattern"] +#[ignore] fn test_generate_ssh_url() { let forgejo_url = ForgejoUrl::create("git@codeberg.org:acme/repo.git").unwrap(); diff --git a/crates/shirabe/tests/util/http/request_proxy_test.rs b/crates/shirabe/tests/util/http/request_proxy_test.rs index 502ec31..3e96a7b 100644 --- a/crates/shirabe/tests/util/http/request_proxy_test.rs +++ b/crates/shirabe/tests/util/http/request_proxy_test.rs @@ -128,7 +128,7 @@ fn test_get_curl_options() { } #[test] -#[ignore = "secure proxies require curl_version, which is a todo!() in the php-shim"] +#[ignore] fn test_get_curl_options_with_ssl() { let mut cafile_opts: IndexMap<String, PhpMixed> = IndexMap::new(); cafile_opts.insert( diff --git a/crates/shirabe/tests/util/ini_helper_test.rs b/crates/shirabe/tests/util/ini_helper_test.rs index 59dc967..f511d76 100644 --- a/crates/shirabe/tests/util/ini_helper_test.rs +++ b/crates/shirabe/tests/util/ini_helper_test.rs @@ -7,10 +7,11 @@ use shirabe_php_shim::{PATH_SEPARATOR, getenv, putenv}; #[allow(dead_code)] fn set_up() -> TearDown { // Register our name with XdebugHandler. - // TODO: XdebugHandler is the external composer/xdebug-handler package and is not ported. - todo!(); + // The PHP test constructs `new XdebugHandler('composer')` so that + // getAllIniFiles() reads the COMPOSER_ORIGINAL_INIS env var. The Rust + // XdebugHandler is a unit struct with no name-registration API, so this + // step is a no-op here. // Save current state - #[allow(unreachable_code)] let env_original = getenv("COMPOSER_ORIGINAL_INIS"); TearDown { env_original } } @@ -44,7 +45,7 @@ fn set_env(paths: &[&str]) { } #[test] -#[ignore = "IniHelper::get_all reaches XdebugHandler::get_all_ini_files, which is todo!()"] +#[ignore] fn test_with_no_ini() { let paths = [""]; @@ -57,7 +58,7 @@ fn test_with_no_ini() { } #[test] -#[ignore = "IniHelper::get_all reaches XdebugHandler::get_all_ini_files, which is todo!()"] +#[ignore] fn test_with_loaded_ini_only() { let paths = ["loaded.ini"]; @@ -66,7 +67,7 @@ fn test_with_loaded_ini_only() { } #[test] -#[ignore = "IniHelper::get_all reaches XdebugHandler::get_all_ini_files, which is todo!()"] +#[ignore] fn test_with_loaded_ini_and_additional() { let paths = ["loaded.ini", "one.ini", "two.ini"]; @@ -79,7 +80,7 @@ fn test_with_loaded_ini_and_additional() { } #[test] -#[ignore = "IniHelper::get_all reaches XdebugHandler::get_all_ini_files, which is todo!()"] +#[ignore] fn test_without_loaded_ini_and_additional() { let paths = ["", "one.ini", "two.ini"]; diff --git a/crates/shirabe/tests/util/no_proxy_pattern_test.rs b/crates/shirabe/tests/util/no_proxy_pattern_test.rs index 7a423b9..51017ea 100644 --- a/crates/shirabe/tests/util/no_proxy_pattern_test.rs +++ b/crates/shirabe/tests/util/no_proxy_pattern_test.rs @@ -42,7 +42,7 @@ fn test_host_name() { } #[test] -#[ignore = "NoProxyPattern does not match IPv4-mapped IPv6 addresses against IPv4 rules (port logic gap)"] +#[ignore] fn test_ip_address() { let noproxy = "192.168.1.1, 2001:db8::52:0:1"; @@ -55,7 +55,7 @@ fn test_ip_address() { } #[test] -#[ignore = "NoProxyPattern does not match IPv4-mapped IPv6 addresses against IPv4/CIDR rules (port logic gap)"] +#[ignore] fn test_ip_range() { let noproxy = "10.0.0.0/30, 2002:db8:a::45/121"; diff --git a/crates/shirabe/tests/util/package_sorter_test.rs b/crates/shirabe/tests/util/package_sorter_test.rs index f84ff09..0a8d395 100644 --- a/crates/shirabe/tests/util/package_sorter_test.rs +++ b/crates/shirabe/tests/util/package_sorter_test.rs @@ -1,30 +1,30 @@ //! ref: composer/tests/Composer/Test/Util/PackageSorterTest.php use indexmap::IndexMap; -use shirabe::package::handle::PackageInterfaceHandle; -use shirabe::package::loader::array_loader::ArrayLoader; +use shirabe::package::Link; +use shirabe::package::handle::{PackageHandle, PackageInterfaceHandle}; use shirabe::util::package_sorter::PackageSorter; -use shirabe_php_shim::PhpMixed; +use shirabe_semver::constraint::MatchAllConstraint; -/// PHP `createPackage` sets requires directly on a `Package`; the public handle API only allows -/// link setters on root packages, so the equivalent here builds the package through ArrayLoader. fn create_package(name: &str, requires: &[&str]) -> PackageInterfaceHandle { - let mut config: IndexMap<String, PhpMixed> = IndexMap::new(); - config.insert("name".to_string(), PhpMixed::String(name.to_string())); - config.insert("version".to_string(), PhpMixed::String("1.0.0".to_string())); + let package = PackageHandle::new(name.to_string(), "1.0.0.0".to_string(), "1.0.0".to_string()); - if !requires.is_empty() { - let mut links: IndexMap<String, PhpMixed> = IndexMap::new(); - for require_name in requires { - links.insert(require_name.to_string(), PhpMixed::String("*".to_string())); - } - config.insert("require".to_string(), PhpMixed::Array(links)); + let mut links: IndexMap<String, Link> = IndexMap::new(); + for require_name in requires { + links.insert( + require_name.to_string(), + Link::new( + package.get_name(), + require_name.to_string(), + MatchAllConstraint::new(None).into(), + None, + "*".to_string(), + ), + ); } + package.__set_requires(links); - ArrayLoader::new(None, false) - .load_packages(vec![config]) - .unwrap() - .remove(0) + package.into() } fn names(packages: &[PackageInterfaceHandle]) -> Vec<String> { @@ -141,7 +141,7 @@ fn sorting_orders_dependencies_higher_than_package_cases() -> Vec<( } #[test] -#[ignore = "constraint parsing reaches a look-around regex unsupported by the regex-crate shim (Preg)"] +#[ignore] fn test_sorting_orders_dependencies_higher_than_package() { for (packages, expected_ordered_list, weights) in sorting_orders_dependencies_higher_than_package_cases() diff --git a/crates/shirabe/tests/util/platform_test.rs b/crates/shirabe/tests/util/platform_test.rs index 626e358..c5fe190 100644 --- a/crates/shirabe/tests/util/platform_test.rs +++ b/crates/shirabe/tests/util/platform_test.rs @@ -4,7 +4,7 @@ use shirabe::util::platform::Platform; use shirabe_php_shim::defined; #[test] -#[ignore = "Platform::expand_path does not read the env var set via put_env in this runtime"] +#[ignore] fn test_expand_path() { Platform::put_env("TESTENV", "/home/test"); assert_eq!( diff --git a/crates/shirabe/tests/util/silencer_test.rs b/crates/shirabe/tests/util/silencer_test.rs index 88d33c6..1b65324 100644 --- a/crates/shirabe/tests/util/silencer_test.rs +++ b/crates/shirabe/tests/util/silencer_test.rs @@ -7,7 +7,7 @@ use shirabe_php_shim::{ /// Test succeeds when no warnings are emitted externally, and original level is restored. #[test] -#[ignore = "trigger_error is todo!() in the php-shim"] +#[ignore] fn test_silencer() { let before = error_reporting(None); diff --git a/crates/shirabe/tests/util/svn_test.rs b/crates/shirabe/tests/util/svn_test.rs index 90a6b45..25e3f4e 100644 --- a/crates/shirabe/tests/util/svn_test.rs +++ b/crates/shirabe/tests/util/svn_test.rs @@ -1,31 +1,145 @@ //! ref: composer/tests/Composer/Test/Util/SvnTest.php -#[ignore = "Svn::get_credential_args is pub(crate) (crate-private); unreachable from an integration test"] +use std::cell::RefCell; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::io::IOInterface; +use shirabe::io::null_io::NullIO; +use shirabe::util::svn::Svn; +use shirabe_php_shim::PhpMixed; + +fn map(pairs: Vec<(&str, PhpMixed)>) -> IndexMap<String, PhpMixed> { + pairs.into_iter().map(|(k, v)| (k.to_string(), v)).collect() +} + +/// Builds a `['config' => ['http-basic' => [host => ['username' => .., 'password' => ..]]]]` +/// map for `Config::merge`. +fn http_basic_config(host: &str, username: &str, password: &str) -> IndexMap<String, PhpMixed> { + let creds = map(vec![ + ("username", PhpMixed::String(username.to_string())), + ("password", PhpMixed::String(password.to_string())), + ]); + let http_basic = map(vec![(host, PhpMixed::Array(creds))]); + let config = map(vec![("http-basic", PhpMixed::Array(http_basic))]); + map(vec![("config", PhpMixed::Array(config))]) +} + +/// ref: SvnTest::urlProvider +fn url_provider() -> Vec<(&'static str, Vec<&'static str>)> { + vec![ + ( + "http://till:test@svn.example.org/", + vec!["--username", "till", "--password", "test"], + ), + ("http://svn.apache.org/", vec![]), + ( + "svn://johndoe@example.org", + vec!["--username", "johndoe", "--password", ""], + ), + ] +} + #[test] +#[ignore] fn test_credentials() { - todo!() + for (url, expect) in url_provider() { + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let config = Rc::new(RefCell::new(Config::new(true, None))); + let mut svn = Svn::new(url.to_string(), io, config, None); + + let expect: Vec<String> = expect.iter().map(|s| s.to_string()).collect(); + assert_eq!(expect, svn.__get_credential_args()); + } } -#[ignore = "Svn::get_command is pub(crate) (crate-private); unreachable from an integration test"] #[test] +#[ignore] fn test_interactive_string() { - todo!() + let url = "http://svn.example.org"; + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let config = Rc::new(RefCell::new(Config::new(true, None))); + let mut svn = Svn::new(url.to_string(), io, config, None); + + assert_eq!( + vec![ + "svn".to_string(), + "ls".to_string(), + "--non-interactive".to_string(), + "--".to_string(), + "http://svn.example.org".to_string(), + ], + svn.__get_command(vec!["svn".to_string(), "ls".to_string()], url, None) + ); } -#[ignore = "Svn::get_credential_args is pub(crate) (crate-private); unreachable from an integration test"] #[test] +#[ignore] fn test_credentials_from_config() { - todo!() + let url = "http://svn.apache.org"; + + let mut config = Config::new(true, None); + config.merge(&http_basic_config("svn.apache.org", "foo", "bar"), "test"); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let mut svn = Svn::new(url.to_string(), io, Rc::new(RefCell::new(config)), None); + + assert_eq!( + vec![ + "--username".to_string(), + "foo".to_string(), + "--password".to_string(), + "bar".to_string(), + ], + svn.__get_credential_args() + ); } -#[ignore = "Svn::get_credential_args is pub(crate) (crate-private); unreachable from an integration test"] #[test] +#[ignore] fn test_credentials_from_config_with_cache_credentials_true() { - todo!() + let url = "http://svn.apache.org"; + + let mut config = Config::new(true, None); + config.merge(&http_basic_config("svn.apache.org", "foo", "bar"), "test"); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let mut svn = Svn::new(url.to_string(), io, Rc::new(RefCell::new(config)), None); + svn.set_cache_credentials(true); + + assert_eq!( + vec![ + "--username".to_string(), + "foo".to_string(), + "--password".to_string(), + "bar".to_string(), + ], + svn.__get_credential_args() + ); } -#[ignore = "Svn::get_credential_args is pub(crate) (crate-private); unreachable from an integration test"] #[test] +#[ignore] fn test_credentials_from_config_with_cache_credentials_false() { - todo!() + let url = "http://svn.apache.org"; + + let mut config = Config::new(true, None); + config.merge(&http_basic_config("svn.apache.org", "foo", "bar"), "test"); + + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let mut svn = Svn::new(url.to_string(), io, Rc::new(RefCell::new(config)), None); + svn.set_cache_credentials(false); + + assert_eq!( + vec![ + "--no-auth-cache".to_string(), + "--username".to_string(), + "foo".to_string(), + "--password".to_string(), + "bar".to_string(), + ], + svn.__get_credential_args() + ); } diff --git a/crates/shirabe/tests/util/tar_test.rs b/crates/shirabe/tests/util/tar_test.rs index 6234108..48f1cbe 100644 --- a/crates/shirabe/tests/util/tar_test.rs +++ b/crates/shirabe/tests/util/tar_test.rs @@ -12,7 +12,7 @@ fn fixture(name: &str) -> String { } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_returns_nullif_the_tar_is_not_found() { let result = Tar::get_composer_json(&fixture("invalid.zip")).unwrap(); @@ -20,26 +20,26 @@ fn test_returns_nullif_the_tar_is_not_found() { } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_returns_null_if_the_tar_is_empty() { let result = Tar::get_composer_json(&fixture("empty.tar.gz")).unwrap(); assert_eq!(None, result); } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_throws_exception_if_the_tar_has_no_composer_json() { assert!(Tar::get_composer_json(&fixture("nojson.tar.gz")).is_err()); } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_throws_exception_if_the_composer_json_is_in_a_sub_subfolder() { assert!(Tar::get_composer_json(&fixture("subfolders.tar.gz")).is_err()); } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_returns_composer_json_in_tar_root() { let result = Tar::get_composer_json(&fixture("root.tar.gz")).unwrap(); assert_eq!( @@ -49,7 +49,7 @@ fn test_returns_composer_json_in_tar_root() { } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_returns_composer_json_in_first_folder() { let result = Tar::get_composer_json(&fixture("folder.tar.gz")).unwrap(); assert_eq!( @@ -59,7 +59,7 @@ fn test_returns_composer_json_in_first_folder() { } #[test] -#[ignore = "PharData::new is todo!() in the php-shim"] +#[ignore] fn test_multiple_top_level_dirs_is_invalid() { assert!(Tar::get_composer_json(&fixture("multiple.tar.gz")).is_err()); } diff --git a/crates/shirabe/tests/util/url_test.rs b/crates/shirabe/tests/util/url_test.rs index cbbfb64..4adb4f2 100644 --- a/crates/shirabe/tests/util/url_test.rs +++ b/crates/shirabe/tests/util/url_test.rs @@ -22,7 +22,7 @@ fn conf(entries: &[(&str, &[&str])]) -> IndexMap<String, PhpMixed> { } #[test] -#[ignore = "update_dist_reference uses a regex pattern the regex crate cannot compile"] +#[ignore] fn test_update_dist_reference() { for (url, expected_url, c, r#ref) in dist_refs_provider() { let mut config = Config::new(true, None); @@ -143,7 +143,7 @@ fn dist_refs_provider() -> Vec<( } #[test] -#[ignore = "Url::sanitize uses a regex pattern the regex crate cannot compile"] +#[ignore] fn test_sanitize() { for (expected, url) in sanitize_provider() { assert_eq!(expected, Url::sanitize(url.to_string())); diff --git a/crates/shirabe/tests/util/zip_test.rs b/crates/shirabe/tests/util/zip_test.rs index b80dd17..4d883e0 100644 --- a/crates/shirabe/tests/util/zip_test.rs +++ b/crates/shirabe/tests/util/zip_test.rs @@ -18,7 +18,7 @@ fn test_throws_exception_if_zip_extension_is_not_loaded() { } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_returns_nullif_the_zip_is_not_found() { let result = Zip::get_composer_json(&fixture("invalid.zip")).unwrap(); @@ -26,7 +26,7 @@ fn test_returns_nullif_the_zip_is_not_found() { } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_returns_null_if_the_zip_is_empty() { let result = Zip::get_composer_json(&fixture("empty.zip")).unwrap(); @@ -34,19 +34,19 @@ fn test_returns_null_if_the_zip_is_empty() { } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_throws_exception_if_the_zip_has_no_composer_json() { assert!(Zip::get_composer_json(&fixture("nojson.zip")).is_err()); } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_throws_exception_if_the_composer_json_is_in_a_sub_subfolder() { assert!(Zip::get_composer_json(&fixture("subfolders.zip")).is_err()); } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_returns_composer_json_in_zip_root() { let result = Zip::get_composer_json(&fixture("root.zip")).unwrap(); @@ -57,7 +57,7 @@ fn test_returns_composer_json_in_zip_root() { } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_returns_composer_json_in_first_folder() { let result = Zip::get_composer_json(&fixture("folder.zip")).unwrap(); @@ -68,13 +68,13 @@ fn test_returns_composer_json_in_first_folder() { } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_multiple_top_level_dirs_is_invalid() { assert!(Zip::get_composer_json(&fixture("multiple.zip")).is_err()); } #[test] -#[ignore = "ZipArchive::new is todo!() in the php-shim"] +#[ignore] fn test_returns_composer_json_from_first_subfolder() { let result = Zip::get_composer_json(&fixture("single-sub.zip")).unwrap(); |
