aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe/src/advisory/auditor.rs38
-rw-r--r--crates/shirabe/src/cache.rs94
-rw-r--r--crates/shirabe/src/command/archive_command.rs61
-rw-r--r--crates/shirabe/src/downloader/download_manager.rs11
-rw-r--r--crates/shirabe/src/downloader/file_downloader.rs25
-rw-r--r--crates/shirabe/src/downloader/perforce_downloader.rs14
-rw-r--r--crates/shirabe/src/factory.rs2
-rw-r--r--crates/shirabe/src/installer/binary_installer.rs28
-rw-r--r--crates/shirabe/src/installer/library_installer.rs52
-rw-r--r--crates/shirabe/src/package/loader/root_package_loader.rs9
-rw-r--r--crates/shirabe/src/package/version/version_guesser.rs27
-rw-r--r--crates/shirabe/src/package/version/version_selector.rs6
-rw-r--r--crates/shirabe/src/platform/hhvm_detector.rs14
-rw-r--r--crates/shirabe/src/platform/runtime.rs58
-rw-r--r--crates/shirabe/src/repository/filesystem_repository.rs18
-rw-r--r--crates/shirabe/src/repository/platform_repository.rs60
-rw-r--r--crates/shirabe/src/repository/repository_set.rs23
-rw-r--r--crates/shirabe/src/repository/vcs/perforce_driver.rs13
-rw-r--r--crates/shirabe/src/util/filesystem.rs39
-rw-r--r--crates/shirabe/src/util/github.rs2
-rw-r--r--crates/shirabe/src/util/perforce.rs82
-rw-r--r--crates/shirabe/tests/advisory/auditor_test.rs1015
-rw-r--r--crates/shirabe/tests/advisory/main.rs5
-rw-r--r--crates/shirabe/tests/cache_test.rs62
-rw-r--r--crates/shirabe/tests/command/archive_command_test.rs271
-rw-r--r--crates/shirabe/tests/command/run_script_command_test.rs14
-rw-r--r--crates/shirabe/tests/command/status_command_test.rs8
-rw-r--r--crates/shirabe/tests/downloader/download_manager_test.rs731
-rw-r--r--crates/shirabe/tests/downloader/file_downloader_test.rs145
-rw-r--r--crates/shirabe/tests/downloader/git_downloader_test.rs338
-rw-r--r--crates/shirabe/tests/downloader/perforce_downloader_test.rs193
-rw-r--r--crates/shirabe/tests/installer/library_installer_test.rs63
-rw-r--r--crates/shirabe/tests/package/loader/root_package_loader_test.rs232
-rw-r--r--crates/shirabe/tests/package/version/version_selector_test.rs521
-rw-r--r--crates/shirabe/tests/platform/hhvm_detector_test.rs1
-rw-r--r--crates/shirabe/tests/repository/filesystem_repository_test.rs117
-rw-r--r--crates/shirabe/tests/repository/platform_repository_test.rs1844
-rw-r--r--crates/shirabe/tests/repository/vcs/perforce_driver_test.rs229
-rw-r--r--crates/shirabe/tests/util/github_test.rs178
-rw-r--r--crates/shirabe/tests/util/http_downloader_test.rs47
40 files changed, 6245 insertions, 445 deletions
diff --git a/crates/shirabe/src/advisory/auditor.rs b/crates/shirabe/src/advisory/auditor.rs
index 627b900..4331b7b 100644
--- a/crates/shirabe/src/advisory/auditor.rs
+++ b/crates/shirabe/src/advisory/auditor.rs
@@ -23,6 +23,7 @@ use crate::util::PackageInfo;
/// Shape of the `--format=json` audit output.
#[derive(serde::Serialize)]
struct AuditJsonReport<'a> {
+ #[serde(serialize_with = "serialize_advisories_field")]
advisories: &'a IndexMap<String, Vec<AnySecurityAdvisory>>,
#[serde(rename = "ignored-advisories", skip_serializing_if = "Option::is_none")]
ignored_advisories: Option<&'a IndexMap<String, Vec<AnySecurityAdvisory>>>,
@@ -31,9 +32,46 @@ struct AuditJsonReport<'a> {
skip_serializing_if = "Option::is_none"
)]
unreachable_repositories: Option<&'a Vec<String>>,
+ #[serde(serialize_with = "serialize_abandoned_field")]
abandoned: IndexMap<String, Option<String>>,
}
+/// PHP `json_encode` emits an empty associative array as `[]`, not `{}`. Mirror that so an empty
+/// map serializes as an empty JSON array.
+fn serialize_php_array<S, V>(map: &IndexMap<String, V>, serializer: S) -> Result<S::Ok, S::Error>
+where
+ S: serde::Serializer,
+ V: serde::Serialize,
+{
+ use serde::ser::SerializeSeq;
+ if map.is_empty() {
+ let seq = serializer.serialize_seq(Some(0))?;
+ seq.end()
+ } else {
+ serializer.collect_map(map.iter())
+ }
+}
+
+fn serialize_advisories_field<S>(
+ map: &&IndexMap<String, Vec<AnySecurityAdvisory>>,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: serde::Serializer,
+{
+ serialize_php_array(*map, serializer)
+}
+
+fn serialize_abandoned_field<S>(
+ map: &IndexMap<String, Option<String>>,
+ serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+ S: serde::Serializer,
+{
+ serialize_php_array(map, serializer)
+}
+
/// @internal
#[derive(Debug)]
pub struct Auditor;
diff --git a/crates/shirabe/src/cache.rs b/crates/shirabe/src/cache.rs
index 696378a..20197cd 100644
--- a/crates/shirabe/src/cache.rs
+++ b/crates/shirabe/src/cache.rs
@@ -27,6 +27,29 @@ pub struct Cache {
allowlist: String,
filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>,
read_only: bool,
+ /// Test-only seam. Always `None` in production; configured via [`Cache::__set_mock`].
+ mock: Option<CacheMock>,
+}
+
+/// Test-only seam mirroring the PHP CacheTest/FileDownloaderTest mocks of `Cache`.
+#[derive(Debug, Default)]
+pub struct CacheMock {
+ /// Overrides the file lists `gc()` derives from `get_finder()`, mirroring the PHP CacheTest
+ /// which mocks `getFinder` to feed `gc()` a controlled iterator.
+ pub finder: Option<GcFinderMock>,
+ /// When `Some`, `gc_is_necessary` returns it verbatim.
+ pub gc_is_necessary: Option<bool>,
+ /// When `Some`, `gc` records each `(ttl, max_size)` call here and skips its real body.
+ pub gc_calls: Option<Vec<(i64, i64)>>,
+}
+
+/// The controlled file lists used by a mocked `gc()` pass.
+#[derive(Debug, Default)]
+pub struct GcFinderMock {
+ /// Entries yielded by `get_finder().date(...)`, i.e. the outdated files.
+ pub outdated: Vec<std::path::PathBuf>,
+ /// Entries yielded by `get_finder().sort_by_accessed_time().get_iterator()`.
+ pub by_accessed_time: Vec<std::path::PathBuf>,
}
/// @var bool|null
@@ -55,6 +78,7 @@ impl Cache {
filesystem,
read_only,
enabled: None,
+ mock: None,
};
if !Self::is_usable(cache_dir) {
@@ -253,6 +277,12 @@ impl Cache {
}
pub fn gc_is_necessary(&self) -> bool {
+ if let Some(mock) = &self.mock
+ && let Some(necessary) = mock.gc_is_necessary
+ {
+ return necessary;
+ }
+
let mut cache_collected = CACHE_COLLECTED.lock().unwrap();
if cache_collected.unwrap_or(false) {
return false;
@@ -316,26 +346,32 @@ impl Cache {
}
pub fn gc(&mut self, ttl: i64, max_size: i64) -> bool {
+ if let Some(mock) = self.mock.as_mut()
+ && let Some(calls) = mock.gc_calls.as_mut()
+ {
+ calls.push((ttl, max_size));
+ return true;
+ }
+
if self.is_enabled() && !self.read_only {
let expire = Utc::now() - chrono::Duration::seconds(ttl);
-
- let mut finder = self.get_finder();
- finder.date(&format!(
+ let expire_str = format!(
"until {}",
expire.format(date_format_to_strftime("Y-m-d H:i:s"))
- ));
- for file in &mut finder {
+ );
+
+ for file in self.gc_outdated_files(&expire_str) {
let _ = self.filesystem.borrow_mut().unlink(&file);
}
let mut total_size = self.filesystem.borrow_mut().size(&self.root).unwrap_or(0);
if total_size > max_size {
- let mut iterator = self.get_finder().sort_by_accessed_time().get_iterator();
- while total_size > max_size && iterator.valid() {
- let filepath = iterator.current();
+ for filepath in self.gc_files_by_accessed_time() {
+ if total_size <= max_size {
+ break;
+ }
total_size -= self.filesystem.borrow_mut().size(&filepath).unwrap_or(0);
let _ = self.filesystem.borrow_mut().unlink(&filepath);
- iterator.next();
}
}
@@ -347,6 +383,46 @@ impl Cache {
false
}
+ /// Files matching `get_finder().date(...)`. Honours the [`CacheMock`] seam when set.
+ fn gc_outdated_files(&self, expire_str: &str) -> Vec<std::path::PathBuf> {
+ if let Some(mock) = &self.mock
+ && let Some(finder) = &mock.finder
+ {
+ return finder.outdated.clone();
+ }
+
+ let mut finder = self.get_finder();
+ finder.date(expire_str);
+ finder.into_iter().collect()
+ }
+
+ /// Files from `get_finder().sort_by_accessed_time()`. Honours the [`CacheMock`] seam when set.
+ fn gc_files_by_accessed_time(&self) -> Vec<std::path::PathBuf> {
+ if let Some(mock) = &self.mock
+ && let Some(finder) = &mock.finder
+ {
+ return finder.by_accessed_time.clone();
+ }
+
+ self.get_finder()
+ .sort_by_accessed_time()
+ .get_iterator()
+ .collect()
+ }
+
+ /// For testing only: install the [`CacheMock`] seam used by CacheTest/FileDownloaderTest.
+ pub fn __set_mock(&mut self, mock: CacheMock) {
+ self.mock = Some(mock);
+ }
+
+ /// For testing only: read back the `(ttl, max_size)` calls recorded by the [`CacheMock`] seam.
+ pub fn __gc_calls(&self) -> Vec<(i64, i64)> {
+ self.mock
+ .as_ref()
+ .and_then(|m| m.gc_calls.clone())
+ .unwrap_or_default()
+ }
+
pub fn gc_vcs_cache(&mut self, ttl: i64) -> bool {
if self.is_enabled() {
let expire = Utc::now() - chrono::Duration::seconds(ttl);
diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs
index 9aae885..d593912 100644
--- a/crates/shirabe/src/command/archive_command.rs
+++ b/crates/shirabe/src/command/archive_command.rs
@@ -36,6 +36,28 @@ use crate::util::r#loop::Loop;
#[derive(Debug)]
pub struct ArchiveCommand {
base_command_data: BaseCommandData,
+ /// For testing only: partial-mock seam mirroring PHPUnit `onlyMethods(['initialize', 'archive'])`.
+ test_hooks: RefCell<ArchiveCommandTestHooks>,
+}
+
+/// For testing only: records and stubs for the `ArchiveCommand` partial-mock seam.
+#[derive(Debug, Default)]
+pub struct ArchiveCommandTestHooks {
+ skip_initialize: bool,
+ archive_stub_return: Option<i64>,
+ archive_calls: Vec<ArchiveCallRecord>,
+}
+
+/// For testing only: the scalar arguments captured from a stubbed `archive` call.
+#[derive(Debug, Clone)]
+pub struct ArchiveCallRecord {
+ pub package_name: Option<String>,
+ pub version: Option<String>,
+ pub format: String,
+ pub dest: String,
+ pub file_name: Option<String>,
+ pub ignore_filters: bool,
+ pub had_composer: bool,
}
impl Default for ArchiveCommand {
@@ -50,6 +72,7 @@ impl ArchiveCommand {
pub fn new() -> Self {
let command = ArchiveCommand {
base_command_data: BaseCommandData::new(None),
+ test_hooks: RefCell::new(ArchiveCommandTestHooks::default()),
};
command
.configure()
@@ -188,12 +211,33 @@ impl Command for ArchiveCommand {
input: Rc<RefCell<dyn InputInterface>>,
output: Rc<RefCell<dyn OutputInterface>>,
) -> anyhow::Result<()> {
+ if self.test_hooks.borrow().skip_initialize {
+ return Ok(());
+ }
base_command_initialize(self, input, output)
}
shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data);
}
+impl ArchiveCommand {
+ /// For testing only: makes `initialize` a no-op (PHPUnit `onlyMethods(['initialize'])`).
+ pub fn __test_skip_initialize(&self) {
+ self.test_hooks.borrow_mut().skip_initialize = true;
+ }
+
+ /// For testing only: stubs `archive` to record its arguments and return `return_value`
+ /// without running (PHPUnit `onlyMethods(['archive'])`).
+ pub fn __test_stub_archive(&self, return_value: i64) {
+ self.test_hooks.borrow_mut().archive_stub_return = Some(return_value);
+ }
+
+ /// For testing only: the arguments captured from stubbed `archive` calls.
+ pub fn __test_archive_calls(&self) -> Vec<ArchiveCallRecord> {
+ self.test_hooks.borrow().archive_calls.clone()
+ }
+}
+
impl BaseCommand for ArchiveCommand {
fn command_data(
&self,
@@ -218,6 +262,23 @@ impl ArchiveCommand {
ignore_filters: bool,
composer: Option<&PartialComposerHandle>,
) -> Result<i64> {
+ let archive_stub_return = self.test_hooks.borrow().archive_stub_return;
+ if let Some(return_value) = archive_stub_return {
+ self.test_hooks
+ .borrow_mut()
+ .archive_calls
+ .push(ArchiveCallRecord {
+ package_name,
+ version,
+ format: format.to_string(),
+ dest: dest.to_string(),
+ file_name,
+ ignore_filters,
+ had_composer: composer.is_some(),
+ });
+ return Ok(return_value);
+ }
+
let composer_guard = composer.map(crate::composer::composer_full);
let mut owned_archive_manager;
let composer_archive_manager;
diff --git a/crates/shirabe/src/downloader/download_manager.rs b/crates/shirabe/src/downloader/download_manager.rs
index a5a83a6..b978368 100644
--- a/crates/shirabe/src/downloader/download_manager.rs
+++ b/crates/shirabe/src/downloader/download_manager.rs
@@ -532,6 +532,17 @@ impl DownloadManager {
Ok(sources)
}
+ /// For testing only: exposes the private `getAvailableSources` for the
+ /// DownloadManagerTest::testGetAvailableSourcesUpdateSticksToSameSource case,
+ /// which reaches it through `ReflectionMethod` in PHP.
+ pub fn __get_available_sources(
+ &self,
+ package: PackageInterfaceHandle,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> Result<Vec<String>> {
+ self.get_available_sources(package, prev_package)
+ }
+
/// Downloaders expect a /path/to/dir without trailing slash
///
/// If any Installer provides a path with a trailing slash, this can cause bugs so make sure we remove them
diff --git a/crates/shirabe/src/downloader/file_downloader.rs b/crates/shirabe/src/downloader/file_downloader.rs
index b03db38..66f3390 100644
--- a/crates/shirabe/src/downloader/file_downloader.rs
+++ b/crates/shirabe/src/downloader/file_downloader.rs
@@ -124,18 +124,19 @@ impl FileDownloader {
{
// PHP: writeError('Running cache garbage collection', true, io_interface::VERY_VERBOSE)
this.io.write_error("Running cache garbage collection");
- cache.borrow_mut().gc(
- this.config
- .borrow_mut()
- .get("cache-files-ttl")
- .as_int()
- .unwrap_or(0),
- this.config
- .borrow_mut()
- .get("cache-files-maxsize")
- .as_int()
- .unwrap_or(0),
- );
+ let ttl = this
+ .config
+ .borrow_mut()
+ .get("cache-files-ttl")
+ .as_int()
+ .unwrap_or(0);
+ let max_size = this
+ .config
+ .borrow_mut()
+ .get("cache-files-maxsize")
+ .as_int()
+ .unwrap_or(0);
+ cache.borrow_mut().gc(ttl, max_size);
}
this
diff --git a/crates/shirabe/src/downloader/perforce_downloader.rs b/crates/shirabe/src/downloader/perforce_downloader.rs
index f93cfa8..56d6957 100644
--- a/crates/shirabe/src/downloader/perforce_downloader.rs
+++ b/crates/shirabe/src/downloader/perforce_downloader.rs
@@ -12,6 +12,7 @@ use crate::package::PackageInterfaceHandle;
use crate::repository::VcsRepository;
use crate::util::Filesystem;
use crate::util::Perforce;
+use crate::util::PerforceInterface;
use crate::util::ProcessExecutor;
use anyhow::Result;
use indexmap::IndexMap;
@@ -20,7 +21,7 @@ use shirabe_php_shim::PhpMixed;
#[derive(Debug)]
pub struct PerforceDownloader {
inner: VcsDownloaderBase,
- pub(crate) perforce: Option<Perforce>,
+ pub(crate) perforce: Option<Box<dyn PerforceInterface>>,
}
impl PerforceDownloader {
@@ -61,20 +62,20 @@ impl PerforceDownloader {
} else {
None
};
- self.perforce = Some(Perforce::create(
+ self.perforce = Some(Box::new(Perforce::create(
repo_config.unwrap_or_default(),
url,
path,
self.inner.process.clone(),
self.inner.io.clone(),
- ));
+ )));
}
fn get_repo_config(&self, repository: &VcsRepository) -> IndexMap<String, PhpMixed> {
repository.get_repo_config().clone()
}
- pub fn set_perforce(&mut self, perforce: Perforce) {
+ pub fn set_perforce(&mut self, perforce: Box<dyn PerforceInterface>) {
self.perforce = Some(perforce);
}
}
@@ -135,10 +136,7 @@ impl VcsDownloader for PerforceDownloader {
self.perforce.as_mut().unwrap().p4_login();
self.perforce.as_mut().unwrap().write_p4_client_spec();
self.perforce.as_mut().unwrap().connect_client();
- self.perforce
- .as_mut()
- .unwrap()
- .sync_code_base(label.as_deref());
+ self.perforce.as_mut().unwrap().sync_code_base(label);
self.perforce.as_mut().unwrap().cleanup_client_spec();
Ok(None)
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index 8a97313..2c27015 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -1270,7 +1270,7 @@ impl Factory {
guesser: VersionGuesser,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
) -> RootPackageLoader {
- RootPackageLoader::new(rm, config, Some(parser), Some(guesser), Some(io))
+ RootPackageLoader::new(rm, config, Some(parser), Some(Box::new(guesser)), Some(io))
}
pub fn create(
diff --git a/crates/shirabe/src/installer/binary_installer.rs b/crates/shirabe/src/installer/binary_installer.rs
index a76f69e..ea73f6f 100644
--- a/crates/shirabe/src/installer/binary_installer.rs
+++ b/crates/shirabe/src/installer/binary_installer.rs
@@ -17,6 +17,34 @@ use crate::util::Platform;
use crate::util::ProcessExecutor;
use crate::util::Silencer;
+/// Seam over the BinaryInstaller methods reached through LibraryInstaller, so tests can inject a
+/// recording double. Composer has no PHP `BinaryInstallerInterface`; this exists only to allow the
+/// `LibraryInstaller` collaborator to be mocked the way PHPUnit mocks the concrete class.
+pub trait BinaryInstallerInterface: std::fmt::Debug {
+ fn install_binaries(
+ &mut self,
+ package: PackageInterfaceHandle,
+ install_path: &str,
+ warn_on_overwrite: bool,
+ );
+ fn remove_binaries(&mut self, package: PackageInterfaceHandle);
+}
+
+impl BinaryInstallerInterface for BinaryInstaller {
+ fn install_binaries(
+ &mut self,
+ package: PackageInterfaceHandle,
+ install_path: &str,
+ warn_on_overwrite: bool,
+ ) {
+ BinaryInstaller::install_binaries(self, package, install_path, warn_on_overwrite);
+ }
+
+ fn remove_binaries(&mut self, package: PackageInterfaceHandle) {
+ BinaryInstaller::remove_binaries(self, package);
+ }
+}
+
/// Utility to handle installation of package "bin"/binaries
#[derive(Debug)]
pub struct BinaryInstaller {
diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs
index e1c3aec..96f38c7 100644
--- a/crates/shirabe/src/installer/library_installer.rs
+++ b/crates/shirabe/src/installer/library_installer.rs
@@ -10,6 +10,7 @@ use shirabe_php_shim::{
use crate::composer::PartialComposerWeakHandle;
use crate::downloader::DownloadManagerInterface;
use crate::installer::BinaryInstaller;
+use crate::installer::BinaryInstallerInterface;
use crate::installer::BinaryPresenceInterface;
use crate::installer::InstallerInterface;
use crate::io::IOInterface;
@@ -29,7 +30,7 @@ pub struct LibraryInstaller {
pub(crate) io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
pub(crate) r#type: Option<String>,
pub(crate) filesystem: std::rc::Rc<std::cell::RefCell<Filesystem>>,
- pub(crate) binary_installer: BinaryInstaller,
+ pub(crate) binary_installer: Box<dyn BinaryInstallerInterface>,
}
impl LibraryInstaller {
@@ -61,28 +62,31 @@ impl LibraryInstaller {
.unwrap_or_default(),
Some("/"),
);
- let binary_installer = binary_installer.unwrap_or_else(|| {
- let bin_dir = rtrim(
- &composer_ref
+ let binary_installer: Box<dyn BinaryInstallerInterface> = match binary_installer {
+ Some(binary_installer) => Box::new(binary_installer),
+ None => {
+ let bin_dir = rtrim(
+ &composer_ref
+ .get_config()
+ .borrow_mut()
+ .get_str("bin-dir")
+ .unwrap_or_default(),
+ Some("/"),
+ );
+ let bin_compat = composer_ref
.get_config()
.borrow_mut()
- .get_str("bin-dir")
- .unwrap_or_default(),
- Some("/"),
- );
- let bin_compat = composer_ref
- .get_config()
- .borrow_mut()
- .get_str("bin-compat")
- .unwrap_or_default();
- BinaryInstaller::new(
- io.clone(),
- bin_dir,
- bin_compat,
- Some(filesystem.clone()),
- Some(vendor_dir.clone()),
- )
- });
+ .get_str("bin-compat")
+ .unwrap_or_default();
+ Box::new(BinaryInstaller::new(
+ io.clone(),
+ bin_dir,
+ bin_compat,
+ Some(filesystem.clone()),
+ Some(vendor_dir.clone()),
+ ))
+ }
+ };
Self {
composer,
@@ -95,6 +99,12 @@ impl LibraryInstaller {
}
}
+ /// For testing only: swap the binary installer for a recording double, mirroring the
+ /// constructor injection PHPUnit performs with a mocked BinaryInstaller.
+ pub fn __set_binary_installer(&mut self, binary_installer: Box<dyn BinaryInstallerInterface>) {
+ self.binary_installer = binary_installer;
+ }
+
/// Make sure binaries are installed for a given package.
pub fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle) {
let install_path = self.get_install_path(package.clone()).unwrap();
diff --git a/crates/shirabe/src/package/loader/root_package_loader.rs b/crates/shirabe/src/package/loader/root_package_loader.rs
index 556f36e..c0101ac 100644
--- a/crates/shirabe/src/package/loader/root_package_loader.rs
+++ b/crates/shirabe/src/package/loader/root_package_loader.rs
@@ -11,6 +11,7 @@ use crate::package::loader::ArrayLoader;
use crate::package::loader::LoaderInterface;
use crate::package::loader::ValidatingArrayLoader;
use crate::package::version::VersionGuesser;
+use crate::package::version::VersionGuesserInterface;
use crate::package::version::VersionParser;
use crate::package::{RootPackage, STABILITIES, SUPPORTED_LINK_TYPES};
use crate::repository::RepositoryFactory;
@@ -23,7 +24,7 @@ pub struct RootPackageLoader {
inner: ArrayLoader,
manager: std::rc::Rc<std::cell::RefCell<RepositoryManager>>,
config: std::rc::Rc<std::cell::RefCell<Config>>,
- version_guesser: VersionGuesser,
+ version_guesser: Box<dyn VersionGuesserInterface>,
io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>,
}
@@ -32,19 +33,19 @@ impl RootPackageLoader {
manager: std::rc::Rc<std::cell::RefCell<RepositoryManager>>,
config: std::rc::Rc<std::cell::RefCell<Config>>,
parser: Option<VersionParser>,
- version_guesser: Option<VersionGuesser>,
+ version_guesser: Option<Box<dyn VersionGuesserInterface>>,
io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>,
) -> Self {
let inner = ArrayLoader::new(parser, true);
let version_guesser = version_guesser.unwrap_or_else(|| {
let mut process_executor = ProcessExecutor::new(io.clone());
process_executor.enable_async();
- VersionGuesser::new(
+ Box::new(VersionGuesser::new(
config.clone(),
std::rc::Rc::new(std::cell::RefCell::new(process_executor)),
inner.version_parser.clone(),
io.clone(),
- )
+ ))
});
Self {
inner,
diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs
index c5bf4c2..6642201 100644
--- a/crates/shirabe/src/package/version/version_guesser.rs
+++ b/crates/shirabe/src/package/version/version_guesser.rs
@@ -20,6 +20,19 @@ use crate::util::Platform;
use crate::util::ProcessExecutor;
use crate::util::Svn as SvnUtil;
+/// Seam over the parts of [`VersionGuesser`] that consumers depend on, so they can be exercised
+/// with a test double. PHP has no such interface; this exists only to allow mocking the concrete
+/// `VersionGuesser` (a Phase C seam).
+pub trait VersionGuesserInterface: std::fmt::Debug {
+ fn guess_version(
+ &mut self,
+ package_config: &IndexMap<String, PhpMixed>,
+ path: &str,
+ ) -> Result<Option<VersionData>>;
+
+ fn get_root_version_from_env(&self) -> Result<String>;
+}
+
/// Try to guess the current version number based on different VCS configuration.
///
/// @phpstan-type Version array{version: string, commit: string|null, pretty_version: string|null}|array{version: string, commit: string|null, pretty_version: string|null, feature_version: string|null, feature_pretty_version: string|null}
@@ -747,6 +760,20 @@ impl VersionGuesser {
}
}
+impl VersionGuesserInterface for VersionGuesser {
+ fn guess_version(
+ &mut self,
+ package_config: &IndexMap<String, PhpMixed>,
+ path: &str,
+ ) -> Result<Option<VersionData>> {
+ VersionGuesser::guess_version(self, package_config, path)
+ }
+
+ fn get_root_version_from_env(&self) -> Result<String> {
+ VersionGuesser::get_root_version_from_env(self)
+ }
+}
+
#[derive(Debug)]
pub struct FeatureVersionResult {
pub version: Option<String>,
diff --git a/crates/shirabe/src/package/version/version_selector.rs b/crates/shirabe/src/package/version/version_selector.rs
index 793dd4e..8a0d037 100644
--- a/crates/shirabe/src/package/version/version_selector.rs
+++ b/crates/shirabe/src/package/version/version_selector.rs
@@ -23,18 +23,18 @@ use crate::package::loader::ArrayLoader;
use crate::package::version::VersionParser;
use crate::repository::PlatformRepository;
use crate::repository::RepositoryInterface;
-use crate::repository::RepositorySet;
+use crate::repository::RepositorySetInterface;
#[derive(Debug)]
pub struct VersionSelector {
- repository_set: std::rc::Rc<std::cell::RefCell<RepositorySet>>,
+ repository_set: std::rc::Rc<std::cell::RefCell<dyn RepositorySetInterface>>,
platform_constraints: IndexMap<String, Vec<AnyConstraint>>,
parser: Option<VersionParser>,
}
impl VersionSelector {
pub fn new(
- repository_set: std::rc::Rc<std::cell::RefCell<RepositorySet>>,
+ repository_set: std::rc::Rc<std::cell::RefCell<dyn RepositorySetInterface>>,
platform_repo: Option<&mut crate::repository::PlatformRepository>,
) -> anyhow::Result<Self> {
let mut platform_constraints: IndexMap<String, Vec<AnyConstraint>> = IndexMap::new();
diff --git a/crates/shirabe/src/platform/hhvm_detector.rs b/crates/shirabe/src/platform/hhvm_detector.rs
index da062d5..69871f1 100644
--- a/crates/shirabe/src/platform/hhvm_detector.rs
+++ b/crates/shirabe/src/platform/hhvm_detector.rs
@@ -9,6 +9,14 @@ use std::sync::Mutex;
// None = null (uninitialized), Some(None) = false (not found), Some(Some(v)) = version
static HHVM_VERSION_CACHE: Mutex<Option<Option<String>>> = Mutex::new(None);
+/// Seam over HHVM detection so PlatformRepository can be tested with a mocked version.
+/// PHP mocks the concrete `Composer\Platform\HhvmDetector` directly; the trait is
+/// introduced here to keep the consumer dependent only on trait methods.
+pub trait HhvmDetectorInterface: std::fmt::Debug {
+ fn reset(&self);
+ fn get_version(&mut self) -> Option<String>;
+}
+
#[derive(Debug)]
pub struct HhvmDetector {
executable_finder: Option<ExecutableFinder>,
@@ -25,12 +33,14 @@ impl HhvmDetector {
process_executor,
}
}
+}
- pub fn reset(&self) {
+impl HhvmDetectorInterface for HhvmDetector {
+ fn reset(&self) {
*HHVM_VERSION_CACHE.lock().unwrap() = None;
}
- pub fn get_version(&mut self) -> Option<String> {
+ fn get_version(&mut self) -> Option<String> {
let cached = HHVM_VERSION_CACHE.lock().unwrap().clone();
if cached.is_some() {
return cached.flatten();
diff --git a/crates/shirabe/src/platform/runtime.rs b/crates/shirabe/src/platform/runtime.rs
index aa2f4d7..fcee930 100644
--- a/crates/shirabe/src/platform/runtime.rs
+++ b/crates/shirabe/src/platform/runtime.rs
@@ -8,41 +8,53 @@ use shirabe_php_shim::{
html_entity_decode, implode, instantiate_class, ltrim, phpversion, strip_tags, trim,
};
+/// Seam over the PHP runtime so PlatformRepository can be tested against mocked
+/// extension/constant/function probes. PHP has no such interface (the test mocks the
+/// concrete `Composer\Platform\Runtime` directly); it is introduced here to keep the
+/// consumer dependent only on trait methods.
+pub trait RuntimeInterface: std::fmt::Debug {
+ fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool;
+ fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed;
+ /// `callable` carries the PHP callable spec (a function name string or a
+ /// `[class, method]` list), matching PHP `invoke($callable, $arguments)`.
+ fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed;
+ fn has_class(&self, class: &str) -> bool;
+ fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> Result<PhpMixed>;
+ fn get_extensions(&self) -> Vec<String>;
+ fn get_extension_version(&self, extension: &str) -> String;
+ fn get_extension_info(&self, extension: &str) -> Result<String>;
+}
+
#[derive(Debug)]
pub struct Runtime;
-impl Runtime {
- pub fn has_constant(&self, constant_name: &str, class: Option<&str>) -> bool {
+impl RuntimeInterface for Runtime {
+ fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool {
defined(&ltrim(
- &format!("{}::{}", class.unwrap_or(""), constant_name),
+ &format!("{}::{}", class.as_deref().unwrap_or(""), constant_name),
Some(":"),
))
}
- pub fn get_constant(&self, constant_name: &str, class: Option<&str>) -> PhpMixed {
+ fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed {
constant(&ltrim(
- &format!("{}::{}", class.unwrap_or(""), constant_name),
+ &format!("{}::{}", class.as_deref().unwrap_or(""), constant_name),
Some(":"),
))
}
- pub fn has_function(&self, f: &str) -> bool {
- function_exists(f)
- }
-
- pub fn invoke(
- &self,
- callable: Box<dyn Fn(Vec<PhpMixed>) -> PhpMixed>,
- arguments: Vec<PhpMixed>,
- ) -> PhpMixed {
- callable(arguments)
+ fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed {
+ // PHP: return $callable(...$arguments);
+ // Dispatching an arbitrary PHP callable needs a PHP runtime; no shim exists.
+ let _ = (callable, arguments);
+ todo!()
}
- pub fn has_class(&self, class: &str) -> bool {
+ fn has_class(&self, class: &str) -> bool {
class_exists(class)
}
- pub fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> Result<PhpMixed> {
+ fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> Result<PhpMixed> {
if arguments.is_empty() {
Ok(instantiate_class(class, vec![]))
} else {
@@ -50,20 +62,26 @@ impl Runtime {
}
}
- pub fn get_extensions(&self) -> Vec<String> {
+ fn get_extensions(&self) -> Vec<String> {
get_loaded_extensions()
}
- pub fn get_extension_version(&self, extension: &str) -> String {
+ fn get_extension_version(&self, extension: &str) -> String {
let version = phpversion(extension);
version.unwrap_or_else(|| "0".to_string())
}
- pub fn get_extension_info(&self, extension: &str) -> Result<String> {
+ fn get_extension_info(&self, extension: &str) -> Result<String> {
// Depends on \ReflectionExtension::info() and output buffering; no shim equivalent exists.
let _ = extension;
todo!()
}
+}
+
+impl Runtime {
+ pub fn has_function(&self, f: &str) -> bool {
+ function_exists(f)
+ }
pub fn parse_html_extension_info(html: &str) -> String {
let mut result: Vec<String> = vec![];
diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs
index 0dae316..14365f2 100644
--- a/crates/shirabe/src/repository/filesystem_repository.rs
+++ b/crates/shirabe/src/repository/filesystem_repository.rs
@@ -14,7 +14,7 @@ use shirabe_php_shim::{
use crate::config::is_php_integer_key;
use crate::installed_versions::InstalledVersions;
-use crate::installer::InstallationManager;
+use crate::installer::InstallationManagerInterface;
use crate::json::JsonFile;
use crate::package::BasePackageHandle;
use crate::package::PackageInterfaceHandle;
@@ -209,7 +209,7 @@ impl FilesystemRepository {
pub fn write(
&mut self,
dev_mode: bool,
- installation_manager: &mut InstallationManager,
+ installation_manager: &mut dyn InstallationManagerInterface,
) -> Result<()> {
let mut data: IndexMap<String, PhpMixed> = IndexMap::new();
data.insert("packages".to_string(), PhpMixed::List(vec![]));
@@ -239,11 +239,7 @@ impl FilesystemRepository {
if let Some(path_str) = &path
&& !path_str.is_empty()
{
- let normalized_path = self.filesystem.borrow_mut().normalize_path(&if self
- .filesystem
- .borrow()
- .is_absolute_path(path_str)
- {
+ let path_to_normalize = if self.filesystem.borrow().is_absolute_path(path_str) {
path_str.clone()
} else {
format!(
@@ -251,7 +247,11 @@ impl FilesystemRepository {
Platform::get_cwd(false).unwrap_or_default(),
path_str
)
- });
+ };
+ let normalized_path = self
+ .filesystem
+ .borrow_mut()
+ .normalize_path(&path_to_normalize);
install_path = Some(self.filesystem.borrow_mut().find_shortest_path(
&repo_dir,
&normalized_path,
@@ -453,7 +453,7 @@ impl FilesystemRepository {
/// @param array<string, string> $installPaths
fn generate_installed_versions(
&mut self,
- installation_manager: &InstallationManager,
+ installation_manager: &dyn InstallationManagerInterface,
install_paths: &IndexMap<String, Option<String>>,
dev_mode: bool,
repo_dir: &str,
diff --git a/crates/shirabe/src/repository/platform_repository.rs b/crates/shirabe/src/repository/platform_repository.rs
index 981ad8d..ee1d26e 100644
--- a/crates/shirabe/src/repository/platform_repository.rs
+++ b/crates/shirabe/src/repository/platform_repository.rs
@@ -23,7 +23,9 @@ use crate::package::PackageInterface;
use crate::package::PackageInterfaceHandle;
use crate::package::version::VersionParser;
use crate::platform::HhvmDetector;
+use crate::platform::HhvmDetectorInterface;
use crate::platform::Runtime;
+use crate::platform::RuntimeInterface;
use crate::platform::Version;
use crate::plugin::plugin_interface::{self};
use crate::repository::ArrayRepository;
@@ -48,8 +50,8 @@ pub struct PlatformRepository {
pub(crate) version_parser: Option<VersionParser>,
pub(crate) overrides: IndexMap<String, PlatformOverride>,
pub(crate) disabled_packages: IndexMap<String, CompletePackageInterfaceHandle>,
- pub(crate) runtime: Runtime,
- pub(crate) hhvm_detector: HhvmDetector,
+ pub(crate) runtime: Box<dyn RuntimeInterface>,
+ pub(crate) hhvm_detector: Box<dyn HhvmDetectorInterface>,
}
impl PlatformRepository {
@@ -65,11 +67,12 @@ impl PlatformRepository {
pub fn new4(
packages: Vec<PackageInterfaceHandle>,
overrides: IndexMap<String, PhpMixed>,
- runtime: Option<Runtime>,
- hhvm_detector: Option<HhvmDetector>,
+ runtime: Option<Box<dyn RuntimeInterface>>,
+ hhvm_detector: Option<Box<dyn HhvmDetectorInterface>>,
) -> anyhow::Result<Self> {
- let runtime = runtime.unwrap_or(Runtime);
- let hhvm_detector = hhvm_detector.unwrap_or_else(|| HhvmDetector::new(None, None));
+ let runtime: Box<dyn RuntimeInterface> = runtime.unwrap_or_else(|| Box::new(Runtime));
+ let hhvm_detector: Box<dyn HhvmDetectorInterface> =
+ hhvm_detector.unwrap_or_else(|| Box::new(HhvmDetector::new(None, None)));
let mut overrides_map: IndexMap<String, PlatformOverride> = IndexMap::new();
for (name, version) in overrides {
if !is_string(&version) && !matches!(version, PhpMixed::Bool(false)) {
@@ -281,17 +284,10 @@ impl PlatformRepository {
// IPv6 support might still be available.
let has_inet6 = self.runtime.has_constant("AF_INET6", None);
// PHP: Silencer::call([$this->runtime, 'invoke'], 'inet_pton', ['::'])
- // TODO(phase-c): Composer's Platform\Runtime class is entirely a todo!() stub (invoke,
- // has_constant, ...), so the inet_pton invocation cannot run. The placeholder callable
- // returns false; resolving this requires implementing the Runtime wrapper (separate Phase C
- // work) and a closure that forwards to the inet_pton shim.
let inet_pton_check = Silencer::call(|| {
Ok::<PhpMixed, anyhow::Error>(self.runtime.invoke(
- Box::new(|_args| PhpMixed::Bool(false)),
- vec![
- PhpMixed::String("inet_pton".to_string()),
- PhpMixed::String("::".to_string()),
- ],
+ PhpMixed::String("inet_pton".to_string()),
+ vec![PhpMixed::String("::".to_string())],
))
})
.unwrap_or(PhpMixed::Bool(false));
@@ -406,10 +402,9 @@ impl PlatformRepository {
}
"curl" => {
- let curl_version = self.runtime.invoke(
- Box::new(|_args| PhpMixed::Null),
- vec![PhpMixed::String("curl_version".to_string())],
- );
+ let curl_version = self
+ .runtime
+ .invoke(PhpMixed::String("curl_version".to_string()), vec![]);
let curl_version_str = curl_version
.as_array()
.and_then(|m| m.get("version"))
@@ -819,17 +814,14 @@ impl PlatformRepository {
// Add a separate version for the CLDR library version
if self.runtime.has_class("ResourceBundle") {
let resource_bundle = self.runtime.invoke(
- Box::new(|_args| PhpMixed::Null),
+ PhpMixed::List(vec![
+ PhpMixed::String("ResourceBundle".to_string()),
+ PhpMixed::String("create".to_string()),
+ ]),
vec![
- PhpMixed::List(vec![
- PhpMixed::String("ResourceBundle".to_string()),
- PhpMixed::String("create".to_string()),
- ]),
- PhpMixed::List(vec![
- PhpMixed::String("root".to_string()),
- PhpMixed::String("ICUDATA".to_string()),
- PhpMixed::Bool(false),
- ]),
+ PhpMixed::String("root".to_string()),
+ PhpMixed::String("ICUDATA".to_string()),
+ PhpMixed::Bool(false),
],
);
if !matches!(resource_bundle, PhpMixed::Null) {
@@ -853,11 +845,11 @@ impl PlatformRepository {
if self.runtime.has_class("IntlChar") {
let intl_char_versions = self.runtime.invoke(
- Box::new(|_args| PhpMixed::Null),
- vec![PhpMixed::List(vec![
+ PhpMixed::List(vec![
PhpMixed::String("IntlChar".to_string()),
PhpMixed::String("getUnicodeVersion".to_string()),
- ])],
+ ]),
+ vec![],
);
let sliced =
shirabe_php_shim::array_slice_mixed(&intl_char_versions, 0, Some(3));
@@ -1421,11 +1413,11 @@ impl PlatformRepository {
"zip" => {
if self
.runtime
- .has_constant("LIBZIP_VERSION", Some("ZipArchive"))
+ .has_constant("LIBZIP_VERSION", Some("ZipArchive".to_string()))
{
let libzip = self
.runtime
- .get_constant("LIBZIP_VERSION", Some("ZipArchive"));
+ .get_constant("LIBZIP_VERSION", Some("ZipArchive".to_string()));
let libzip_str = match &libzip {
PhpMixed::String(s) => Some(s.clone()),
_ => None,
diff --git a/crates/shirabe/src/repository/repository_set.rs b/crates/shirabe/src/repository/repository_set.rs
index 393639f..f8514cb 100644
--- a/crates/shirabe/src/repository/repository_set.rs
+++ b/crates/shirabe/src/repository/repository_set.rs
@@ -648,6 +648,29 @@ impl RepositorySet {
}
}
+/// Seam over `RepositorySet` so consumers (e.g. `VersionSelector`) can receive a mockable
+/// repository set in tests. The concrete `RepositorySet` implements it by delegating to its
+/// inherent methods. New consumers may extend this trait additively.
+pub trait RepositorySetInterface: std::fmt::Debug {
+ fn find_packages(
+ &self,
+ name: &str,
+ constraint: Option<AnyConstraint>,
+ flags: i64,
+ ) -> anyhow::Result<Vec<BasePackageHandle>>;
+}
+
+impl RepositorySetInterface for RepositorySet {
+ fn find_packages(
+ &self,
+ name: &str,
+ constraint: Option<AnyConstraint>,
+ flags: i64,
+ ) -> anyhow::Result<Vec<BasePackageHandle>> {
+ RepositorySet::find_packages(self, name, constraint, flags)
+ }
+}
+
#[derive(Debug)]
pub struct SecurityAdvisoriesResult {
pub advisories: IndexMap<String, Vec<AnySecurityAdvisory>>,
diff --git a/crates/shirabe/src/repository/vcs/perforce_driver.rs b/crates/shirabe/src/repository/vcs/perforce_driver.rs
index 50d5eb9..cb280e5 100644
--- a/crates/shirabe/src/repository/vcs/perforce_driver.rs
+++ b/crates/shirabe/src/repository/vcs/perforce_driver.rs
@@ -9,6 +9,7 @@ use crate::config::Config;
use crate::io::IOInterface;
use crate::repository::vcs::VcsDriverBase;
use crate::util::Perforce;
+use crate::util::PerforceInterface;
use crate::util::ProcessExecutor;
use crate::util::http::Response;
@@ -17,7 +18,7 @@ pub struct PerforceDriver {
inner: VcsDriverBase,
pub(crate) depot: String,
pub(crate) branch: String,
- pub(crate) perforce: Option<Perforce>,
+ pub(crate) perforce: Option<Box<dyn PerforceInterface>>,
}
impl PerforceDriver {
@@ -86,17 +87,23 @@ impl PerforceDriver {
}
let repo_dir = format!("{}/{}", cache_vcs_dir, self.depot);
- self.perforce = Some(Perforce::create(
+ self.perforce = Some(Box::new(Perforce::create(
repo_config.clone(),
self.inner.url.clone(),
repo_dir,
self.inner.process.clone(),
self.inner.io.clone(),
- ));
+ )));
Ok(())
}
+ /// For testing only. Rust equivalent of the reflection-based
+ /// `overrideDriverInternalPerforce` helper in PerforceDriverTest.
+ pub fn __override_perforce(&mut self, perforce: Box<dyn PerforceInterface>) {
+ self.perforce = Some(perforce);
+ }
+
pub fn get_file_content(
&mut self,
file: &str,
diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs
index 6dd2ab9..b224767 100644
--- a/crates/shirabe/src/util/filesystem.rs
+++ b/crates/shirabe/src/util/filesystem.rs
@@ -22,15 +22,41 @@ use crate::util::Silencer;
#[derive(Debug)]
pub struct Filesystem {
process_executor: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>,
+ /// Test-only seam. Always `None` in production; configured via [`Filesystem::__set_mock`].
+ mock: Option<FilesystemMock>,
+}
+
+/// Test-only seam mirroring the PHP FileDownloaderTest mock of `Filesystem`.
+#[derive(Debug, Default)]
+pub struct FilesystemMock {
+ /// When `Some`, `remove_directory_async` returns it without touching disk and counts the call.
+ pub remove_directory_async_result: Option<bool>,
+ pub remove_directory_async_calls: usize,
+ /// When true, `normalize_path` returns its argument unchanged.
+ pub normalize_path_identity: bool,
}
impl Filesystem {
pub fn new(executor: Option<std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>) -> Self {
Self {
process_executor: executor,
+ mock: None,
}
}
+ /// For testing only: install the [`FilesystemMock`] seam used by FileDownloaderTest.
+ pub fn __set_mock(&mut self, mock: FilesystemMock) {
+ self.mock = Some(mock);
+ }
+
+ /// For testing only: number of `remove_directory_async` calls intercepted by the seam.
+ pub fn __remove_directory_async_calls(&self) -> usize {
+ self.mock
+ .as_ref()
+ .map(|m| m.remove_directory_async_calls)
+ .unwrap_or(0)
+ }
+
pub fn remove(&mut self, file: impl AsRef<Path>) -> anyhow::Result<bool> {
let file = file.as_ref();
if is_dir(file) {
@@ -140,6 +166,13 @@ impl Filesystem {
/// Uses the process component if proc_open is enabled on the PHP
/// installation.
pub async fn remove_directory_async(&mut self, directory: &str) -> anyhow::Result<bool> {
+ if let Some(mock) = self.mock.as_mut()
+ && let Some(result) = mock.remove_directory_async_result
+ {
+ mock.remove_directory_async_calls += 1;
+ return Ok(result);
+ }
+
let edge_case_result = self.remove_edge_cases(directory, true)?;
if let Some(r) = edge_case_result {
return Ok(r);
@@ -702,6 +735,12 @@ impl Filesystem {
/// Normalize a path. This replaces backslashes with slashes, removes ending
/// slash and collapses redundant separators and up-level references.
pub fn normalize_path(&self, path: &str) -> String {
+ if let Some(mock) = &self.mock
+ && mock.normalize_path_identity
+ {
+ return path.to_string();
+ }
+
let mut parts: Vec<String> = vec![];
let mut path = strtr(path, "\\", "/");
let mut prefix = String::new();
diff --git a/crates/shirabe/src/util/github.rs b/crates/shirabe/src/util/github.rs
index dae4954..285052e 100644
--- a/crates/shirabe/src/util/github.rs
+++ b/crates/shirabe/src/util/github.rs
@@ -241,7 +241,7 @@ impl GitHub {
Err(te) => {
let code = te
.downcast_ref::<crate::downloader::TransportException>()
- .and_then(|t| t.get_status_code())
+ .map(|t| t.get_code())
.unwrap_or(0);
if code == 403 || code == 401 {
self.io.write_error3(
diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs
index 4d4ddef..d8cd39d 100644
--- a/crates/shirabe/src/util/perforce.rs
+++ b/crates/shirabe/src/util/perforce.rs
@@ -838,3 +838,85 @@ impl Perforce {
.clone()
}
}
+
+/// Seam over `Perforce` so consumers can be exercised with a mock in tests.
+pub trait PerforceInterface: std::fmt::Debug {
+ fn initialize_path(&mut self, path: &str);
+ fn set_stream(&mut self, stream: &str);
+ fn p4_login(&mut self) -> Result<()>;
+ fn check_stream(&mut self) -> bool;
+ fn write_p4_client_spec(&mut self) -> Result<()>;
+ fn connect_client(&mut self) -> Result<()>;
+ fn sync_code_base(&mut self, source_reference: Option<String>) -> Result<()>;
+ fn cleanup_client_spec(&mut self);
+ fn get_commit_logs(&mut self, from_reference: &str, to_reference: &str) -> Option<String>;
+ fn get_file_content(&mut self, file: &str, identifier: &str) -> Option<String>;
+ fn get_branches(&mut self) -> IndexMap<String, String>;
+ fn get_tags(&mut self) -> IndexMap<String, String>;
+ fn get_user(&self) -> Option<String>;
+ fn get_composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<IndexMap<String, PhpMixed>>>;
+}
+
+impl PerforceInterface for Perforce {
+ fn initialize_path(&mut self, path: &str) {
+ Perforce::initialize_path(self, path)
+ }
+
+ fn set_stream(&mut self, stream: &str) {
+ Perforce::set_stream(self, stream)
+ }
+
+ fn p4_login(&mut self) -> Result<()> {
+ Perforce::p4_login(self)
+ }
+
+ fn check_stream(&mut self) -> bool {
+ Perforce::check_stream(self)
+ }
+
+ fn write_p4_client_spec(&mut self) -> Result<()> {
+ Perforce::write_p4_client_spec(self)
+ }
+
+ fn connect_client(&mut self) -> Result<()> {
+ Perforce::connect_client(self)
+ }
+
+ fn sync_code_base(&mut self, source_reference: Option<String>) -> Result<()> {
+ Perforce::sync_code_base(self, source_reference.as_deref())
+ }
+
+ fn cleanup_client_spec(&mut self) {
+ Perforce::cleanup_client_spec(self)
+ }
+
+ fn get_commit_logs(&mut self, from_reference: &str, to_reference: &str) -> Option<String> {
+ Perforce::get_commit_logs(self, from_reference, to_reference)
+ }
+
+ fn get_file_content(&mut self, file: &str, identifier: &str) -> Option<String> {
+ Perforce::get_file_content(self, file, identifier)
+ }
+
+ fn get_branches(&mut self) -> IndexMap<String, String> {
+ Perforce::get_branches(self)
+ }
+
+ fn get_tags(&mut self) -> IndexMap<String, String> {
+ Perforce::get_tags(self)
+ }
+
+ fn get_user(&self) -> Option<String> {
+ Perforce::get_user(self)
+ }
+
+ fn get_composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> Result<Option<IndexMap<String, PhpMixed>>> {
+ Perforce::get_composer_information(self, identifier)
+ }
+}
diff --git a/crates/shirabe/tests/advisory/auditor_test.rs b/crates/shirabe/tests/advisory/auditor_test.rs
index 7e02db8..516c4d1 100644
--- a/crates/shirabe/tests/advisory/auditor_test.rs
+++ b/crates/shirabe/tests/advisory/auditor_test.rs
@@ -6,8 +6,30 @@ use shirabe::advisory::AnySecurityAdvisory;
use shirabe::advisory::Auditor;
use shirabe::advisory::PartialSecurityAdvisory;
use shirabe::advisory::SecurityAdvisory;
+use shirabe::downloader::TransportException;
+use shirabe::io::BufferIO;
+use shirabe::io::io_interface;
+use shirabe::package::BasePackageHandle;
+use shirabe::package::PackageInterfaceHandle;
+use shirabe::repository::AdvisoryProviderInterface;
+use shirabe::repository::FindPackageConstraint;
+use shirabe::repository::LoadPackagesResult;
+use shirabe::repository::ProviderInfo;
+use shirabe::repository::RepositoryInterface;
+use shirabe::repository::RepositoryInterfaceHandle;
+use shirabe::repository::RepositorySet;
+use shirabe::repository::SearchResult;
+use shirabe::repository::SecurityAdvisoryResult;
+use shirabe_external_packages::symfony::console::output::output_interface;
+use shirabe_php_shim::date_create;
+use shirabe_semver::VersionParser;
+use shirabe_semver::constraint::AnyConstraint;
use shirabe_semver::constraint::SimpleConstraint;
+use crate::io_mock::{Expectation, get_io_mock};
+use crate::test_case::get_complete_package;
+use crate::test_case::get_package;
+
fn constraint(operator: &str, version: &str) -> shirabe_semver::constraint::AnyConstraint {
SimpleConstraint::new(operator.to_string(), version.to_string(), None).into()
}
@@ -61,30 +83,1003 @@ fn ignore_list(pairs: Vec<(&str, Option<&str>)>) -> IndexMap<String, Option<Stri
.collect()
}
-// These run the Auditor against a mocked HttpDownloader/IO and packages built from version
-// constraints (parsed via a look-around regex the regex crate cannot compile).
+/// Behavior of the mocked repository's advisory provider. Replaces PHP's
+/// `getMockBuilder(ComposerRepository::class)` partial mock; the real `RepositorySet` still drives
+/// `getMatchingSecurityAdvisories`, only the per-repo advisory lookup is stubbed.
+#[derive(Debug)]
+enum AdvisorySource {
+ /// Mirrors `AuditorTest::getMockAdvisories()` filtered by the package constraint map.
+ Table,
+ /// Returns a fixed advisory map regardless of the request (reachable repo).
+ Fixed(IndexMap<String, Vec<AnySecurityAdvisory>>),
+ /// Throws a `TransportException`, simulating an unreachable repository.
+ Unreachable(String),
+}
+
+#[derive(Debug)]
+struct MockAdvisoryRepository {
+ source: AdvisorySource,
+}
+
+impl AdvisoryProviderInterface for MockAdvisoryRepository {
+ fn has_security_advisories(&mut self) -> anyhow::Result<bool> {
+ Ok(true)
+ }
+
+ fn get_security_advisories(
+ &mut self,
+ package_constraint_map: IndexMap<String, AnyConstraint>,
+ _allow_partial_advisories: bool,
+ ) -> anyhow::Result<SecurityAdvisoryResult> {
+ match &self.source {
+ AdvisorySource::Unreachable(message) => {
+ Err(TransportException::new(message.clone(), 404).into())
+ }
+ AdvisorySource::Fixed(advisories) => Ok(SecurityAdvisoryResult {
+ names_found: package_constraint_map.keys().cloned().collect(),
+ advisories: advisories.clone(),
+ }),
+ AdvisorySource::Table => {
+ let mut advisories: IndexMap<String, Vec<AnySecurityAdvisory>> = IndexMap::new();
+ for (package, list) in mock_advisories() {
+ let Some(constraint) = package_constraint_map.get(&package) else {
+ continue;
+ };
+ let filtered: Vec<AnySecurityAdvisory> = list
+ .into_iter()
+ .filter(|advisory| advisory.affected_versions().matches(constraint))
+ .collect();
+ if !filtered.is_empty() {
+ advisories.insert(package, filtered);
+ }
+ }
+ Ok(SecurityAdvisoryResult {
+ names_found: package_constraint_map.keys().cloned().collect(),
+ advisories,
+ })
+ }
+ }
+ }
+}
+
+impl RepositoryInterface for MockAdvisoryRepository {
+ fn count(&self) -> anyhow::Result<usize> {
+ unimplemented!("not used by Auditor")
+ }
+ fn has_package(&self, _package: PackageInterfaceHandle) -> bool {
+ unimplemented!("not used by Auditor")
+ }
+ fn find_package(
+ &mut self,
+ _name: &str,
+ _constraint: FindPackageConstraint,
+ ) -> anyhow::Result<Option<BasePackageHandle>> {
+ unimplemented!("not used by Auditor")
+ }
+ fn find_packages(
+ &mut self,
+ _name: &str,
+ _constraint: Option<FindPackageConstraint>,
+ ) -> anyhow::Result<Vec<BasePackageHandle>> {
+ unimplemented!("not used by Auditor")
+ }
+ fn get_packages(&mut self) -> anyhow::Result<Vec<BasePackageHandle>> {
+ unimplemented!("not used by Auditor")
+ }
+ fn load_packages(
+ &mut self,
+ _package_name_map: IndexMap<String, Option<AnyConstraint>>,
+ _acceptable_stabilities: IndexMap<String, i64>,
+ _stability_flags: IndexMap<String, i64>,
+ _already_loaded: IndexMap<String, IndexMap<String, PackageInterfaceHandle>>,
+ ) -> anyhow::Result<LoadPackagesResult> {
+ unimplemented!("not used by Auditor")
+ }
+ fn search(
+ &mut self,
+ _query: String,
+ _mode: i64,
+ _type: Option<String>,
+ ) -> anyhow::Result<Vec<SearchResult>> {
+ unimplemented!("not used by Auditor")
+ }
+ fn get_providers(
+ &mut self,
+ _package_name: String,
+ ) -> anyhow::Result<IndexMap<String, ProviderInfo>> {
+ unimplemented!("not used by Auditor")
+ }
+ fn get_repo_name(&self) -> String {
+ "mock advisory repo".to_string()
+ }
+ fn as_advisory_provider_mut(&mut self) -> Option<&mut dyn AdvisoryProviderInterface> {
+ Some(self)
+ }
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+}
+
+/// ref: AuditorTest::getMockAdvisories. All entries carry full data so they load as
+/// `SecurityAdvisory` (never partial) regardless of `allowPartialAdvisories`.
+fn mock_advisories() -> IndexMap<String, Vec<AnySecurityAdvisory>> {
+ let mut advisories: IndexMap<String, Vec<AnySecurityAdvisory>> = IndexMap::new();
+ advisories.insert(
+ "vendor1/package1".to_string(),
+ vec![
+ mock_advisory(
+ "vendor1/package1",
+ "ID1",
+ "advisory1",
+ "https://advisory.example.com/advisory1",
+ "CVE1",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source1",
+ "RemoteID1",
+ "2022-05-25 13:21:00",
+ "medium",
+ ),
+ mock_advisory(
+ "vendor1/package1",
+ "ID4",
+ "advisory4",
+ "https://advisory.example.com/advisory4",
+ "CVE3",
+ ">=8,<8.2.2|>=1,<2.5.6",
+ "source2",
+ "RemoteID4",
+ "2022-05-25 13:21:00",
+ "high",
+ ),
+ mock_advisory(
+ "vendor1/package1",
+ "ID5",
+ "advisory5",
+ "https://advisory.example.com/advisory5",
+ "",
+ ">=8,<8.2.2|>=1,<2.5.6",
+ "source1",
+ "RemoteID3",
+ "2022-05-25 13:21:00",
+ "medium",
+ ),
+ ],
+ );
+ advisories.insert(
+ "vendor1/package2".to_string(),
+ vec![mock_advisory(
+ "vendor1/package2",
+ "ID2",
+ "advisory2",
+ "https://advisory.example.com/advisory2",
+ "",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source1",
+ "RemoteID2",
+ "2022-05-25 13:21:00",
+ "medium",
+ )],
+ );
+ advisories.insert(
+ "vendorx/packagex".to_string(),
+ vec![mock_advisory(
+ "vendorx/packagex",
+ "IDx",
+ "advisory17",
+ "https://advisory.example.com/advisory17",
+ "CVE5",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source2",
+ "RemoteIDx",
+ "2015-05-25 13:21:00",
+ "medium",
+ )],
+ );
+ advisories.insert(
+ "vendor2/package1".to_string(),
+ vec![
+ mock_advisory(
+ "vendor2/package1",
+ "ID3",
+ "advisory3",
+ "https://advisory.example.com/advisory3",
+ "CVE2",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source2",
+ "RemoteID1",
+ "2022-05-25 13:21:00",
+ "medium",
+ ),
+ mock_advisory(
+ "vendor2/package1",
+ "ID6",
+ "advisory6",
+ "https://advisory.example.com/advisory6",
+ "CVE4",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source2",
+ "RemoteID3",
+ "2015-05-25 13:21:00",
+ "medium",
+ ),
+ ],
+ );
+ advisories.insert(
+ "vendory/packagey".to_string(),
+ vec![mock_advisory(
+ "vendory/packagey",
+ "IDy",
+ "advisory7",
+ "https://advisory.example.com/advisory7",
+ "CVE5",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source2",
+ "RemoteID4",
+ "2015-05-25 13:21:00",
+ "medium",
+ )],
+ );
+ advisories.insert(
+ "vendor3/package1".to_string(),
+ vec![mock_advisory(
+ "vendor3/package1",
+ "ID7",
+ "advisory7",
+ "https://advisory.example.com/advisory7",
+ "CVE5",
+ ">=3,<3.4.3|>=1,<2.5.6",
+ "source2",
+ "RemoteID4",
+ "2015-05-25 13:21:00",
+ "medium",
+ )],
+ );
+ advisories
+}
+
+#[allow(
+ clippy::too_many_arguments,
+ reason = "mirrors the PHP advisory data shape"
+)]
+fn mock_advisory(
+ package_name: &str,
+ advisory_id: &str,
+ title: &str,
+ link: &str,
+ cve: &str,
+ affected_versions: &str,
+ source_name: &str,
+ remote_id: &str,
+ reported_at: &str,
+ severity: &str,
+) -> AnySecurityAdvisory {
+ let mut source: IndexMap<String, String> = IndexMap::new();
+ source.insert("name".to_string(), source_name.to_string());
+ source.insert("remoteId".to_string(), remote_id.to_string());
+ AnySecurityAdvisory::Full(SecurityAdvisory::new(
+ package_name.to_string(),
+ advisory_id.to_string(),
+ VersionParser.parse_constraints(affected_versions).unwrap(),
+ title.to_string(),
+ vec![source],
+ date_create::<Utc>(reported_at).unwrap(),
+ Some(cve.to_string()),
+ Some(link.to_string()),
+ Some(severity.to_string()),
+ ))
+}
+
+/// ref: AuditorTest::getRepoSet. Real `RepositorySet` holding a single repository whose advisory
+/// provider serves `getMockAdvisories()`.
+fn get_repo_set() -> RepositorySet {
+ let mut repo_set = RepositorySet::new(
+ "stable",
+ IndexMap::new(),
+ vec![],
+ IndexMap::new(),
+ IndexMap::new(),
+ IndexMap::new(),
+ );
+ repo_set
+ .add_repository(RepositoryInterfaceHandle::new(MockAdvisoryRepository {
+ source: AdvisorySource::Table,
+ }))
+ .unwrap();
+ repo_set
+}
+
+/// ref: AuditorTest::testAudit (auditProvider).
#[test]
-#[ignore = "requires PHPUnit getMockBuilder partial mock of ComposerRepository (hasSecurityAdvisories/getSecurityAdvisories) and BufferIO; no mocking infrastructure exists"]
+#[ignore = "all 10 cases faithfully ported; the 2 FORMAT_TABLE cases cannot run because \
+ BufferIO does not downcast to ConsoleIO (PHP's `BufferIO extends ConsoleIO` is \
+ modeled as composition in Rust), so the table-rendering path is unreachable and \
+ the whole function stays ignored until the src supports it"]
fn test_audit() {
- todo!()
+ #[derive(Clone)]
+ struct Case {
+ packages: Vec<PackageInterfaceHandle>,
+ warning_only: bool,
+ abandoned: &'static str,
+ format: &'static str,
+ ignore_abandoned: IndexMap<String, Option<String>>,
+ expected: i64,
+ output: &'static str,
+ }
+
+ let abandoned_with_replacement = || {
+ let p = get_complete_package("vendor/abandoned", "1.0.0");
+ p.set_abandoned(shirabe_php_shim::PhpMixed::String("foo/bar".to_string()));
+ let handle: PackageInterfaceHandle = p.into();
+ handle
+ };
+ let abandoned_no_replacement = || {
+ let p = get_complete_package("vendor/abandoned2", "1.0.0");
+ p.set_abandoned(shirabe_php_shim::PhpMixed::Bool(true));
+ let handle: PackageInterfaceHandle = p.into();
+ handle
+ };
+
+ let cases: Vec<Case> = vec![
+ Case {
+ packages: vec![
+ get_package("vendor1/package2", "9.0.0"),
+ get_package("vendor1/package1", "9.0.0"),
+ get_package("vendor3/package1", "9.0.0"),
+ ],
+ warning_only: true,
+ abandoned: Auditor::ABANDONED_IGNORE,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_OK,
+ output: "No security vulnerability advisories found.",
+ },
+ Case {
+ packages: vec![
+ get_package("vendor1/package2", "9.0.0"),
+ get_package("vendor1/package1", "8.2.1"),
+ get_package("vendor3/package1", "9.0.0"),
+ ],
+ warning_only: true,
+ abandoned: Auditor::ABANDONED_IGNORE,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_VULNERABLE,
+ output:
+ "<warning>Found 2 security vulnerability advisories affecting 1 package:</warning>
+Package: vendor1/package1
+Severity: high
+Advisory ID: ID4
+CVE: CVE3
+Title: advisory4
+URL: https://advisory.example.com/advisory4
+Affected versions: >=8,<8.2.2|>=1,<2.5.6
+Reported at: 2022-05-25T13:21:00+00:00
+--------
+Package: vendor1/package1
+Severity: medium
+Advisory ID: ID5
+CVE: \nTitle: advisory5
+URL: https://advisory.example.com/advisory5
+Affected versions: >=8,<8.2.2|>=1,<2.5.6
+Reported at: 2022-05-25T13:21:00+00:00",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_IGNORE,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_OK,
+ output: "No security vulnerability advisories found.",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: ignore_list(vec![("vendor/*", None)]),
+ expected: Auditor::STATUS_OK,
+ output: "No security vulnerability advisories found.",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: ignore_list(vec![
+ ("vendor/abandoned", None),
+ ("vendor/abandoned2", None),
+ ]),
+ expected: Auditor::STATUS_OK,
+ output: "No security vulnerability advisories found.",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: ignore_list(vec![("acme/test", Some("ignoring because yolo"))]),
+ expected: Auditor::STATUS_ABANDONED,
+ output: "No security vulnerability advisories found.
+Found 2 abandoned packages:
+vendor/abandoned is abandoned. Use foo/bar instead.
+vendor/abandoned2 is abandoned. No replacement was suggested.",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: true,
+ abandoned: Auditor::ABANDONED_REPORT,
+ format: Auditor::FORMAT_PLAIN,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_OK,
+ output: "No security vulnerability advisories found.
+Found 2 abandoned packages:
+vendor/abandoned is abandoned. Use foo/bar instead.
+vendor/abandoned2 is abandoned. No replacement was suggested.",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_TABLE,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_ABANDONED,
+ output: "No security vulnerability advisories found.
+Found 2 abandoned packages:
++-------------------+----------------------------------------------------------------------------------+
+| Abandoned Package | Suggested Replacement |
++-------------------+----------------------------------------------------------------------------------+
+| vendor/abandoned | foo/bar |
+| vendor/abandoned2 | none |
++-------------------+----------------------------------------------------------------------------------+",
+ },
+ Case {
+ packages: vec![
+ get_package("vendor1/package1", "8.2.1"),
+ abandoned_with_replacement(),
+ abandoned_no_replacement(),
+ ],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_TABLE,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_VULNERABLE | Auditor::STATUS_ABANDONED,
+ output: "Found 2 security vulnerability advisories affecting 1 package:
++-------------------+----------------------------------------------------------------------------------+
+| Package | vendor1/package1 |
+| Severity | high |
+| Advisory ID | ID4 |
+| CVE | CVE3 |
+| Title | advisory4 |
+| URL | https://advisory.example.com/advisory4 |
+| Affected versions | >=8,<8.2.2|>=1,<2.5.6 |
+| Reported at | 2022-05-25T13:21:00+00:00 |
++-------------------+----------------------------------------------------------------------------------+
++-------------------+----------------------------------------------------------------------------------+
+| Package | vendor1/package1 |
+| Severity | medium |
+| Advisory ID | ID5 |
+| CVE | |
+| Title | advisory5 |
+| URL | https://advisory.example.com/advisory5 |
+| Affected versions | >=8,<8.2.2|>=1,<2.5.6 |
+| Reported at | 2022-05-25T13:21:00+00:00 |
++-------------------+----------------------------------------------------------------------------------+
+Found 2 abandoned packages:
++-------------------+----------------------------------------------------------------------------------+
+| Abandoned Package | Suggested Replacement |
++-------------------+----------------------------------------------------------------------------------+
+| vendor/abandoned | foo/bar |
+| vendor/abandoned2 | none |
++-------------------+----------------------------------------------------------------------------------+",
+ },
+ Case {
+ packages: vec![abandoned_with_replacement(), abandoned_no_replacement()],
+ warning_only: false,
+ abandoned: Auditor::ABANDONED_FAIL,
+ format: Auditor::FORMAT_JSON,
+ ignore_abandoned: IndexMap::new(),
+ expected: Auditor::STATUS_ABANDONED,
+ output: "{
+ \"advisories\": [],
+ \"abandoned\": {
+ \"vendor/abandoned\": \"foo/bar\",
+ \"vendor/abandoned2\": null
+ }
+}",
+ },
+ ];
+
+ for case in cases {
+ let repo_set = get_repo_set();
+ let mut io =
+ BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap();
+ let auditor = Auditor;
+ let result = auditor
+ .audit(
+ &mut io,
+ &repo_set,
+ case.packages.clone(),
+ case.format,
+ case.warning_only,
+ IndexMap::new(),
+ case.abandoned,
+ IndexMap::new(),
+ false,
+ case.ignore_abandoned.clone(),
+ )
+ .unwrap();
+ assert_eq!(case.expected, result);
+ assert_eq!(case.output, io.get_output().replace('\r', "").trim());
+ }
}
+/// ref: AuditorTest::testAuditWithIgnore (ignoredIdsProvider).
#[test]
-#[ignore = "requires getIOMock with expects() output expectations and getMockBuilder mock of ComposerRepository; no mocking infrastructure exists"]
fn test_audit_with_ignore() {
- todo!()
+ struct Case {
+ packages: Vec<PackageInterfaceHandle>,
+ ignored_ids: IndexMap<String, Option<String>>,
+ exit_code: i64,
+ expected_output: Vec<Expectation>,
+ }
+
+ let cases: Vec<Case> = vec![
+ Case {
+ packages: vec![get_package("vendor1/package1", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![("CVE1", None)]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendor1/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID1"),
+ Expectation::text("CVE: CVE1"),
+ Expectation::text("Title: advisory1"),
+ Expectation::text("URL: https://advisory.example.com/advisory1"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![("CVE1", Some("A good reason"))]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendor1/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID1"),
+ Expectation::text("CVE: CVE1"),
+ Expectation::text("Title: advisory1"),
+ Expectation::text("URL: https://advisory.example.com/advisory1"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ Expectation::text("Ignore reason: A good reason"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package2", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![("ID2", None)]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendor1/package2"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID2"),
+ Expectation::text("CVE: "),
+ Expectation::text("Title: advisory2"),
+ Expectation::text("URL: https://advisory.example.com/advisory2"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendorx/packagex", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![("RemoteIDx", None)]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendorx/packagex"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: IDx"),
+ Expectation::text("CVE: CVE5"),
+ Expectation::text("Title: advisory17"),
+ Expectation::text("URL: https://advisory.example.com/advisory17"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2015-05-25T13:21:00+00:00"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![("vendor1/package1", None)]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendor1/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID1"),
+ Expectation::text("CVE: CVE1"),
+ Expectation::text("Title: advisory1"),
+ Expectation::text("URL: https://advisory.example.com/advisory1"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "3.0.0.0")],
+ ignored_ids: ignore_list(vec![(
+ "vendor1/package1",
+ Some("Package has known safe usage"),
+ )]),
+ exit_code: 0,
+ expected_output: vec![
+ Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ ),
+ Expectation::text("Package: vendor1/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID1"),
+ Expectation::text("CVE: CVE1"),
+ Expectation::text("Title: advisory1"),
+ Expectation::text("URL: https://advisory.example.com/advisory1"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ Expectation::text("Ignore reason: Package has known safe usage"),
+ ],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "3.0.0.0")],
+ ignored_ids: IndexMap::new(),
+ exit_code: 1,
+ expected_output: vec![
+ Expectation::text("Found 1 security vulnerability advisory affecting 1 package:"),
+ Expectation::text("Package: vendor1/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID1"),
+ Expectation::text("CVE: CVE1"),
+ Expectation::text("Title: advisory1"),
+ Expectation::text("URL: https://advisory.example.com/advisory1"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ ],
+ },
+ Case {
+ packages: vec![
+ get_package("vendor3/package1", "3.0.0.0"),
+ get_package("vendorx/packagex", "3.0.0.0"),
+ get_package("vendor2/package1", "3.0.0.0"),
+ ],
+ ignored_ids: ignore_list(vec![("RemoteIDx", None), ("ID3", None), ("ID6", None)]),
+ exit_code: 1,
+ expected_output: vec![
+ Expectation::text(
+ "Found 3 ignored security vulnerability advisories affecting 2 packages:",
+ ),
+ Expectation::text("Package: vendor2/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID3"),
+ Expectation::text("CVE: CVE2"),
+ Expectation::text("Title: advisory3"),
+ Expectation::text("URL: https://advisory.example.com/advisory3"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2022-05-25T13:21:00+00:00"),
+ Expectation::text("Ignore reason: None specified"),
+ Expectation::text("--------"),
+ Expectation::text("Package: vendor2/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID6"),
+ Expectation::text("CVE: CVE4"),
+ Expectation::text("Title: advisory6"),
+ Expectation::text("URL: https://advisory.example.com/advisory6"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2015-05-25T13:21:00+00:00"),
+ Expectation::text("Ignore reason: None specified"),
+ Expectation::text("--------"),
+ Expectation::text("Package: vendorx/packagex"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: IDx"),
+ Expectation::text("CVE: CVE5"),
+ Expectation::text("Title: advisory17"),
+ Expectation::text("URL: https://advisory.example.com/advisory17"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2015-05-25T13:21:00+00:00"),
+ Expectation::text("Ignore reason: None specified"),
+ Expectation::text("Found 1 security vulnerability advisory affecting 1 package:"),
+ Expectation::text("Package: vendor3/package1"),
+ Expectation::text("Severity: medium"),
+ Expectation::text("Advisory ID: ID7"),
+ Expectation::text("CVE: CVE5"),
+ Expectation::text("Title: advisory7"),
+ Expectation::text("URL: https://advisory.example.com/advisory7"),
+ Expectation::text("Affected versions: >=3,<3.4.3|>=1,<2.5.6"),
+ Expectation::text("Reported at: 2015-05-25T13:21:00+00:00"),
+ ],
+ },
+ ];
+
+ for case in cases {
+ let repo_set = get_repo_set();
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ let auditor = Auditor;
+ let result = {
+ let mut io_guard = io_mock.borrow_mut();
+ auditor
+ .audit(
+ &mut *io_guard,
+ &repo_set,
+ case.packages.clone(),
+ Auditor::FORMAT_PLAIN,
+ false,
+ case.ignored_ids.clone(),
+ Auditor::ABANDONED_FAIL,
+ IndexMap::new(),
+ false,
+ IndexMap::new(),
+ )
+ .unwrap()
+ };
+ io_mock
+ .borrow_mut()
+ .expects(case.expected_output.clone(), true)
+ .unwrap();
+ assert_eq!(case.exit_code, result);
+ }
}
+/// ref: AuditorTest::testAuditWithIgnoreUnreachable. The whole `RepositorySet` is mocked in PHP;
+/// here a real `RepositorySet` holds a reachable repo (fixed advisories) followed by an
+/// unreachable repo (throws `TransportException`), reproducing the same merged result.
#[test]
-#[ignore = "requires getMockBuilder partial mock of RepositorySet (getMatchingSecurityAdvisories willReturnCallback); no mocking infrastructure exists"]
fn test_audit_with_ignore_unreachable() {
- todo!()
+ let packages = vec![get_package("vendor1/package1", "3.0.0.0")];
+
+ let error_message =
+ "The \"https://example.org/packages.json\" file could not be downloaded: HTTP/1.1 404 Not Found"
+ .to_string();
+
+ let make_repo_set = || {
+ let mut fixed: IndexMap<String, Vec<AnySecurityAdvisory>> = IndexMap::new();
+ fixed.insert(
+ "vendor1/package1".to_string(),
+ vec![
+ AnySecurityAdvisory::Full(SecurityAdvisory::new(
+ "vendor1/package1".to_string(),
+ "CVE-2023-12345".to_string(),
+ constraint("=", "3.0.0.0"),
+ "First repo advisory".to_string(),
+ vec![{
+ let mut s: IndexMap<String, String> = IndexMap::new();
+ s.insert("name".to_string(), "test".to_string());
+ s.insert("remoteId".to_string(), "1".to_string());
+ s
+ }],
+ date_create::<Utc>("2023-01-01").unwrap(),
+ Some("CVE-2023-12345".to_string()),
+ Some("https://example.com/advisory/1".to_string()),
+ Some("medium".to_string()),
+ )),
+ AnySecurityAdvisory::Full(SecurityAdvisory::new(
+ "vendor1/package1".to_string(),
+ "CVE-2023-67890".to_string(),
+ constraint("=", "3.0.0.0"),
+ "Third repo advisory".to_string(),
+ vec![{
+ let mut s: IndexMap<String, String> = IndexMap::new();
+ s.insert("name".to_string(), "test".to_string());
+ s.insert("remoteId".to_string(), "3".to_string());
+ s
+ }],
+ date_create::<Utc>("2023-01-01").unwrap(),
+ Some("CVE-2023-67890".to_string()),
+ Some("https://example.com/advisory/3".to_string()),
+ Some("high".to_string()),
+ )),
+ ],
+ );
+
+ let mut repo_set = RepositorySet::new(
+ "stable",
+ IndexMap::new(),
+ vec![],
+ IndexMap::new(),
+ IndexMap::new(),
+ IndexMap::new(),
+ );
+ repo_set
+ .add_repository(RepositoryInterfaceHandle::new(MockAdvisoryRepository {
+ source: AdvisorySource::Fixed(fixed),
+ }))
+ .unwrap();
+ repo_set
+ .add_repository(RepositoryInterfaceHandle::new(MockAdvisoryRepository {
+ source: AdvisorySource::Unreachable(error_message.clone()),
+ }))
+ .unwrap();
+ repo_set
+ };
+
+ let auditor = Auditor;
+
+ // Without the ignoreUnreachable flag the TransportException propagates.
+ {
+ let repo_set = make_repo_set();
+ let mut io =
+ BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap();
+ let err = auditor
+ .audit(
+ &mut io,
+ &repo_set,
+ packages.clone(),
+ Auditor::FORMAT_PLAIN,
+ false,
+ IndexMap::new(),
+ Auditor::ABANDONED_IGNORE,
+ IndexMap::new(),
+ false,
+ IndexMap::new(),
+ )
+ .expect_err("Expected TransportException was not thrown");
+ assert!(err.to_string().contains("HTTP/1.1 404 Not Found"));
+ }
+
+ // With the ignoreUnreachable flag the advisories from reachable repos are reported.
+ {
+ let repo_set = make_repo_set();
+ let mut io =
+ BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap();
+ let result = auditor
+ .audit(
+ &mut io,
+ &repo_set,
+ packages.clone(),
+ Auditor::FORMAT_PLAIN,
+ false,
+ IndexMap::new(),
+ Auditor::ABANDONED_IGNORE,
+ IndexMap::new(),
+ true,
+ IndexMap::new(),
+ )
+ .unwrap();
+ assert_eq!(Auditor::STATUS_VULNERABLE, result);
+
+ let output = io.get_output();
+ assert!(output.contains("The following repositories were unreachable:"));
+ assert!(output.contains("HTTP/1.1 404 Not Found"));
+ assert!(output.contains("First repo advisory"));
+ assert!(output.contains("Third repo advisory"));
+ assert!(output.contains("CVE-2023-12345"));
+ assert!(output.contains("CVE-2023-67890"));
+ }
+
+ // With JSON format the unreachable repositories and advisories are both included.
+ {
+ let repo_set = make_repo_set();
+ let mut io =
+ BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap();
+ let result = auditor
+ .audit(
+ &mut io,
+ &repo_set,
+ packages.clone(),
+ Auditor::FORMAT_JSON,
+ false,
+ IndexMap::new(),
+ Auditor::ABANDONED_IGNORE,
+ IndexMap::new(),
+ true,
+ IndexMap::new(),
+ )
+ .unwrap();
+ assert_eq!(Auditor::STATUS_VULNERABLE, result);
+
+ let json: serde_json::Value = serde_json::from_str(&io.get_output()).unwrap();
+ let unreachable = json
+ .get("unreachable-repositories")
+ .and_then(|v| v.as_array())
+ .unwrap();
+ assert_eq!(1, unreachable.len());
+ assert!(
+ unreachable[0]
+ .as_str()
+ .unwrap()
+ .contains("HTTP/1.1 404 Not Found")
+ );
+
+ let pkg_advisories = json
+ .get("advisories")
+ .and_then(|v| v.get("vendor1/package1"))
+ .and_then(|v| v.as_array())
+ .unwrap();
+ assert_eq!(2, pkg_advisories.len());
+ assert_eq!("CVE-2023-12345", pkg_advisories[0]["cve"].as_str().unwrap());
+ assert_eq!(
+ "First repo advisory",
+ pkg_advisories[0]["title"].as_str().unwrap()
+ );
+ assert_eq!("CVE-2023-67890", pkg_advisories[1]["cve"].as_str().unwrap());
+ assert_eq!(
+ "Third repo advisory",
+ pkg_advisories[1]["title"].as_str().unwrap()
+ );
+ }
}
+/// ref: AuditorTest::testAuditWithIgnoreSeverity (ignoreSeverityProvider).
#[test]
-#[ignore = "requires getIOMock with expects() output expectations and getMockBuilder mock of ComposerRepository; no mocking infrastructure exists"]
fn test_audit_with_ignore_severity() {
- todo!()
+ struct Case {
+ packages: Vec<PackageInterfaceHandle>,
+ ignored_severities: IndexMap<String, Option<String>>,
+ exit_code: i64,
+ expected_output: Vec<Expectation>,
+ }
+
+ let cases: Vec<Case> = vec![
+ Case {
+ packages: vec![get_package("vendor1/package1", "2.0.0.0")],
+ ignored_severities: ignore_list(vec![("medium", None)]),
+ exit_code: 1,
+ expected_output: vec![Expectation::text(
+ "Found 2 ignored security vulnerability advisories affecting 1 package:",
+ )],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "2.0.0.0")],
+ ignored_severities: ignore_list(vec![("high", None)]),
+ exit_code: 1,
+ expected_output: vec![Expectation::text(
+ "Found 1 ignored security vulnerability advisory affecting 1 package:",
+ )],
+ },
+ Case {
+ packages: vec![get_package("vendor1/package1", "2.0.0.0")],
+ ignored_severities: ignore_list(vec![("high", None), ("medium", None)]),
+ exit_code: 0,
+ expected_output: vec![Expectation::text(
+ "Found 3 ignored security vulnerability advisories affecting 1 package:",
+ )],
+ },
+ ];
+
+ for case in cases {
+ let repo_set = get_repo_set();
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ let auditor = Auditor;
+ let result = {
+ let mut io_guard = io_mock.borrow_mut();
+ auditor
+ .audit(
+ &mut *io_guard,
+ &repo_set,
+ case.packages.clone(),
+ Auditor::FORMAT_PLAIN,
+ false,
+ IndexMap::new(),
+ Auditor::ABANDONED_IGNORE,
+ case.ignored_severities.clone(),
+ false,
+ IndexMap::new(),
+ )
+ .unwrap()
+ };
+ io_mock
+ .borrow_mut()
+ .expects(case.expected_output.clone(), true)
+ .unwrap();
+ assert_eq!(case.exit_code, result);
+ }
}
#[test]
diff --git a/crates/shirabe/tests/advisory/main.rs b/crates/shirabe/tests/advisory/main.rs
index e0b920d..51b470e 100644
--- a/crates/shirabe/tests/advisory/main.rs
+++ b/crates/shirabe/tests/advisory/main.rs
@@ -1,2 +1,7 @@
+#[path = "../common/io_mock.rs"]
+mod io_mock;
+#[path = "../common/test_case.rs"]
+mod test_case;
+
mod audit_config_test;
mod auditor_test;
diff --git a/crates/shirabe/tests/cache_test.rs b/crates/shirabe/tests/cache_test.rs
index 2e18856..7705714 100644
--- a/crates/shirabe/tests/cache_test.rs
+++ b/crates/shirabe/tests/cache_test.rs
@@ -4,7 +4,7 @@ use std::cell::RefCell;
use std::fs;
use std::rc::Rc;
-use shirabe::cache::Cache;
+use shirabe::cache::{Cache, CacheMock, GcFinderMock};
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::util::filesystem::Filesystem;
@@ -27,8 +27,10 @@ fn set_up() -> SetUp {
files.push(path);
}
- // The finder/filesystem/IO mocks and the Cache mock overriding getFinder are not ported.
- let cache: Cache = todo!();
+ // PHP mocks Cache::getFinder and keeps the real Filesystem; here the CacheMock finder seam plays
+ // that role and the Cache otherwise operates on the real temp directory.
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let cache = Cache::new(io, root.path().to_str().unwrap(), None, None, false);
SetUp { root, files, cache }
}
@@ -56,25 +58,57 @@ impl Drop for TearDown {
}
}
-// In PHP these mock Cache::getFinder() to feed the gc() routine a controlled set of
-// files. getFinder is pub(crate) and cannot be overridden from a test, so the
-// finder-driven removal paths cannot be exercised faithfully here.
-#[ignore = "requires mocking Cache::get_finder (pub(crate), PHPUnit MockObject) to feed gc() a controlled Finder iterator; not overridable from a test"]
#[test]
fn test_remove_outdated_files() {
- let SetUp { root, files, cache } = set_up();
+ let SetUp {
+ root,
+ files,
+ mut cache,
+ } = set_up();
let _tear_down = TearDown::new(root.path().to_path_buf());
- let _ = (&files, &cache);
- todo!()
+
+ // The date('until ...') finder yields the outdated entries (files 1..3).
+ let outdated = files[1..].to_vec();
+ cache.__set_mock(CacheMock {
+ finder: Some(GcFinderMock {
+ outdated,
+ by_accessed_time: Vec::new(),
+ }),
+ ..Default::default()
+ });
+
+ cache.gc(600, 1024 * 1024 * 1024);
+
+ for (i, file) in files.iter().enumerate().skip(1) {
+ assert!(!file.exists(), "cached.file{i}.zip should be removed");
+ }
+ assert!(files[0].exists(), "cached.file0.zip should still exist");
}
-#[ignore = "requires mocking Cache::get_finder (pub(crate), PHPUnit MockObject) to feed gc() a controlled Finder iterator; not overridable from a test"]
#[test]
fn test_remove_files_when_cache_is_too_large() {
- let SetUp { root, files, cache } = set_up();
+ let SetUp {
+ root,
+ files,
+ mut cache,
+ } = set_up();
let _tear_down = TearDown::new(root.path().to_path_buf());
- let _ = (&files, &cache);
- todo!()
+
+ // The date filter matches nothing; the size-bound pass walks all files by accessed time.
+ cache.__set_mock(CacheMock {
+ finder: Some(GcFinderMock {
+ outdated: Vec::new(),
+ by_accessed_time: files.clone(),
+ }),
+ ..Default::default()
+ });
+
+ cache.gc(600, 1500);
+
+ for (i, file) in files.iter().enumerate().take(3) {
+ assert!(!file.exists(), "cached.file{i}.zip should be removed");
+ }
+ assert!(files[3].exists(), "cached.file3.zip should still exist");
}
#[test]
diff --git a/crates/shirabe/tests/command/archive_command_test.rs b/crates/shirabe/tests/command/archive_command_test.rs
index b381129..c25ea3c 100644
--- a/crates/shirabe/tests/command/archive_command_test.rs
+++ b/crates/shirabe/tests/command/archive_command_test.rs
@@ -1,19 +1,278 @@
//! ref: composer/tests/Composer/Test/Command/ArchiveCommandTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use indexmap::IndexMap;
+use shirabe::command::BaseCommand;
+use shirabe::command::archive_command::ArchiveCommand;
+use shirabe::composer::{Composer, PartialComposerHandle, PartialOrFullComposer};
+use shirabe::config::Config;
+use shirabe::dependency_resolver::Transaction;
+use shirabe::event_dispatcher::{Callable, EventDispatcherInterface, EventInterface};
+use shirabe::package::{
+ ArchiveManagerInterface, CompletePackageInterfaceHandle, RootPackageHandle,
+};
+use shirabe::repository::{
+ InstalledArrayRepository, RepositoryInterfaceHandle, RepositoryManagerInterface,
+};
+use shirabe::util::Platform;
+use shirabe_external_packages::symfony::console::command::command::Command;
+use shirabe_external_packages::symfony::console::input::InputInterface;
+use shirabe_external_packages::symfony::console::input::array_input::ArrayInput;
+use shirabe_external_packages::symfony::console::output::OutputInterface;
+use shirabe_external_packages::symfony::console::output::buffered_output::BufferedOutput;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::VersionParser;
+
+use crate::config_stub::ConfigStubBuilder;
+
+/// A recorded `ArchiveManager::archive()` call (PHPUnit `->with(...)->willReturn(...)`).
+#[derive(Debug, Clone)]
+struct ArchiveCall {
+ package_name: String,
+ format: String,
+ target_dir: String,
+ file_name: Option<String>,
+ ignore_filters: bool,
+}
+
+/// Equivalent to a `getMockBuilder(ArchiveManager::class)` mock whose `archive` method is
+/// stubbed to record its arguments and return a fixed path.
+#[derive(Debug)]
+struct ArchiveManagerMock {
+ calls: Rc<RefCell<Vec<ArchiveCall>>>,
+ return_value: String,
+}
+
+impl ArchiveManagerInterface for ArchiveManagerMock {
+ fn archive(
+ &mut self,
+ package: CompletePackageInterfaceHandle,
+ format: String,
+ target_dir: String,
+ file_name: Option<String>,
+ ignore_filters: bool,
+ ) -> anyhow::Result<String> {
+ self.calls.borrow_mut().push(ArchiveCall {
+ package_name: package.get_name(),
+ format,
+ target_dir,
+ file_name,
+ ignore_filters,
+ });
+ Ok(self.return_value.clone())
+ }
+}
+
+/// Equivalent to a `getMockBuilder(EventDispatcher::class)->disableOriginalConstructor()` mock
+/// with no behavioral expectations: every dispatch is a no-op.
+#[derive(Debug, Default)]
+struct NoopEventDispatcher;
+
+impl EventDispatcherInterface for NoopEventDispatcher {
+ fn dispatch(
+ &mut self,
+ _event_name: Option<&str>,
+ _event: Option<&mut dyn EventInterface>,
+ ) -> anyhow::Result<i64> {
+ Ok(0)
+ }
+
+ fn dispatch_script(
+ &mut self,
+ _event_name: &str,
+ _dev_mode: bool,
+ _additional_args: Vec<String>,
+ _flags: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<i64> {
+ Ok(0)
+ }
+
+ fn dispatch_installer_event(
+ &mut self,
+ _event_name: &str,
+ _dev_mode: bool,
+ _execute_operations: bool,
+ _transaction: Transaction,
+ ) -> anyhow::Result<i64> {
+ Ok(0)
+ }
+
+ fn add_listener(&mut self, _event_name: &str, _listener: Callable, _priority: i64) {}
+
+ fn has_event_listeners(&mut self, _event: &dyn EventInterface) -> bool {
+ false
+ }
+}
+
+fn root_package(name: &str, version: &str) -> RootPackageHandle {
+ let normalized = VersionParser.normalize(version, None).unwrap();
+ RootPackageHandle::new(name.to_string(), normalized, version.to_string())
+}
+
+fn full_composer(composer: Composer) -> PartialComposerHandle {
+ PartialComposerHandle::from_rc(Rc::new(RefCell::new(PartialOrFullComposer::Full(composer))))
+}
+
#[test]
-#[ignore = "requires PHPUnit getMockBuilder/onlyMethods partial mock of ArchiveCommand (override tryComposer/requireComposer) plus expects()/willReturn() mocks for OutputInterface/EventDispatcher/ArchiveManager/RootPackageInterface; no mocking infrastructure exists"]
fn test_uses_config_from_composer_object() {
- todo!()
+ let input: Rc<RefCell<dyn InputInterface>> =
+ Rc::new(RefCell::new(ArrayInput::new(vec![], None).unwrap()));
+ let output: Rc<RefCell<dyn OutputInterface>> =
+ Rc::new(RefCell::new(BufferedOutput::new(None, false, None)));
+
+ let config: Rc<RefCell<Config>> = ConfigStubBuilder::new()
+ .with("archive-format", PhpMixed::from("zip"))
+ .build_shared();
+
+ let calls = Rc::new(RefCell::new(Vec::new()));
+ let manager = ArchiveManagerMock {
+ calls: calls.clone(),
+ return_value: Platform::get_cwd(false).unwrap(),
+ };
+
+ let package = root_package("archive/test", "1.0.0");
+
+ let mut composer = Composer::new();
+ composer.set_config(config);
+ composer.set_archive_manager(Rc::new(RefCell::new(manager)));
+ composer.set_event_dispatcher(Rc::new(RefCell::new(NoopEventDispatcher)));
+ composer.set_package(package.into());
+ let composer = full_composer(composer);
+
+ // tryComposer()/requireComposer() resolve to the pre-set Composer (PHPUnit overrides both).
+ let command = ArchiveCommand::new();
+ command.set_composer(composer);
+
+ command.run(input, output).unwrap();
+
+ let calls = calls.borrow();
+ assert_eq!(1, calls.len());
+ // PHP asserts archive() receives `$package` itself; we check the identity via the package name
+ // we set on the Composer.
+ assert_eq!("archive/test", calls[0].package_name);
+ assert_eq!("zip", calls[0].format);
+ assert_eq!(".", calls[0].target_dir);
+ assert_eq!(None, calls[0].file_name);
+ assert!(!calls[0].ignore_filters);
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder/onlyMethods partial mock of ArchiveCommand (override tryComposer/archive) with expects()/with()/willReturn() expectation verification and Factory::createConfig mock OutputInterface; no mocking infrastructure exists"]
fn test_uses_config_from_factory_when_composer_is_not_defined() {
- todo!()
+ let input: Rc<RefCell<dyn InputInterface>> =
+ Rc::new(RefCell::new(ArrayInput::new(vec![], None).unwrap()));
+ let output: Rc<RefCell<dyn OutputInterface>> =
+ Rc::new(RefCell::new(BufferedOutput::new(None, false, None)));
+
+ // tryComposer() returns null (no Composer set), so execute() builds the Config via the Factory
+ // and `archive` is stubbed (PHPUnit overrides initialize/tryComposer/archive).
+ let command = ArchiveCommand::new();
+ command.__test_skip_initialize();
+ command.__test_stub_archive(0);
+
+ assert_eq!(0, command.run(input, output).unwrap());
+
+ let calls = command.__test_archive_calls();
+ assert_eq!(1, calls.len());
+ // PHP additionally asserts arg2 === Factory::createConfig() (config-resolution path identity).
+ // Not reproduced: capturing arg2 needs a src-side hook on ArchiveCommand's ArchiveCallRecord,
+ // and Config has no identity equality. The factory path is only indirectly evidenced here by
+ // had_composer == false plus format == "tar" (the archive-format default when no Composer config).
+ assert_eq!(None, calls[0].package_name);
+ assert_eq!(None, calls[0].version);
+ assert_eq!("tar", calls[0].format);
+ assert_eq!(".", calls[0].dest);
+ assert_eq!(None, calls[0].file_name);
+ assert!(!calls[0].ignore_filters);
+ assert!(!calls[0].had_composer);
+}
+
+/// Equivalent to a `getMockBuilder(RepositoryManager::class)->disableOriginalConstructor()` mock:
+/// `getLocalRepository` returns the installed repository, `getRepositories` returns `[]`.
+#[derive(Debug)]
+struct RepositoryManagerMock {
+ local: RepositoryInterfaceHandle,
+ repositories: Vec<RepositoryInterfaceHandle>,
+}
+
+impl RepositoryManagerInterface for RepositoryManagerMock {
+ fn get_local_repository(&self) -> RepositoryInterfaceHandle {
+ self.local.clone()
+ }
+
+ fn get_repositories(&self) -> &Vec<RepositoryInterfaceHandle> {
+ &self.repositories
+ }
+
+ fn create_repository(
+ &self,
+ _type: &str,
+ _config: IndexMap<String, PhpMixed>,
+ _name: Option<&str>,
+ ) -> anyhow::Result<RepositoryInterfaceHandle> {
+ unreachable!("ArchiveCommand does not create repositories")
+ }
+
+ fn add_repository(&mut self, _repository: RepositoryInterfaceHandle) {
+ unreachable!("ArchiveCommand does not add repositories")
+ }
+
+ fn set_local_repository(&mut self, _repository: RepositoryInterfaceHandle) {
+ unreachable!("ArchiveCommand does not set the local repository")
+ }
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder/onlyMethods partial mock of ArchiveCommand plus expects()/willReturn() mocks for OutputInterface/EventDispatcher/ArchiveManager/InstalledRepositoryInterface/RepositoryManager; no mocking infrastructure exists"]
fn test_uses_config_from_composer_object_with_package_name() {
- todo!()
+ let input: Rc<RefCell<dyn InputInterface>> = Rc::new(RefCell::new(
+ ArrayInput::new(
+ vec![(PhpMixed::from("package"), PhpMixed::from("foo/bar"))],
+ None,
+ )
+ .unwrap(),
+ ));
+ let output: Rc<RefCell<dyn OutputInterface>> =
+ Rc::new(RefCell::new(BufferedOutput::new(None, false, None)));
+
+ let config: Rc<RefCell<Config>> = ConfigStubBuilder::new()
+ .with("archive-format", PhpMixed::from("zip"))
+ .build_shared();
+
+ let calls = Rc::new(RefCell::new(Vec::new()));
+ let manager = ArchiveManagerMock {
+ calls: calls.clone(),
+ return_value: Platform::get_cwd(false).unwrap(),
+ };
+
+ let package = root_package("foo/bar", "1.0.0");
+
+ // The local repository resolves `foo/bar` (PHPUnit mocks InstalledRepositoryInterface::loadPackages).
+ let installed =
+ InstalledArrayRepository::new_with_packages(vec![package.clone().into()]).unwrap();
+ let repository_manager = RepositoryManagerMock {
+ local: RepositoryInterfaceHandle::new(installed),
+ repositories: vec![],
+ };
+
+ let mut composer = Composer::new();
+ composer.set_config(config);
+ composer.set_archive_manager(Rc::new(RefCell::new(manager)));
+ composer.set_event_dispatcher(Rc::new(RefCell::new(NoopEventDispatcher)));
+ composer.set_package(package.into());
+ composer.set_repository_manager(Rc::new(RefCell::new(repository_manager)));
+ let composer = full_composer(composer);
+
+ let command = ArchiveCommand::new();
+ command.set_composer(composer);
+
+ command.run(input, output).unwrap();
+
+ let calls = calls.borrow();
+ assert_eq!(1, calls.len());
+ assert_eq!("foo/bar", calls[0].package_name);
+ assert_eq!("zip", calls[0].format);
+ assert_eq!(".", calls[0].target_dir);
+ assert_eq!(None, calls[0].file_name);
+ assert!(!calls[0].ignore_filters);
}
diff --git a/crates/shirabe/tests/command/run_script_command_test.rs b/crates/shirabe/tests/command/run_script_command_test.rs
index 8e96cf3..6cd4a2e 100644
--- a/crates/shirabe/tests/command/run_script_command_test.rs
+++ b/crates/shirabe/tests/command/run_script_command_test.rs
@@ -1,11 +1,21 @@
//! ref: composer/tests/Composer/Test/Command/RunScriptCommandTest.php
+use shirabe_php_shim::PhpMixed;
+
use crate::test_case::{RunOptions, get_application_tester, init_temp_composer};
use serial_test::serial;
-use shirabe_php_shim::PhpMixed;
+/// ref: RunScriptCommandTest::testDetectAndPassDevModeToEventAndToDispatching
+///
+/// The `getDevOptions` dataProvider drives four `(dev, noDev)` cases; for each, PHP asserts that the
+/// `ScriptEvent` passed to `hasEventListeners` matches the script name AND its `isDevMode()` equals
+/// the computed dev mode (`dev || !noDev`) -- the latter being the whole point of the test.
#[test]
-#[ignore = "requires PHPUnit getMockBuilder/onlyMethods partial mock of RunScriptCommand (override requireComposer/initialize/etc) plus expects()/with()/willReturn()/returnValueMap mocks of InputInterface/OutputInterface/EventDispatcher with a callback constraint on ScriptEvent; no mocking infrastructure exists"]
+#[ignore = "PHP asserts the ScriptEvent passed to hasEventListeners has isDevMode() == (dev || !no_dev), \
+ but EventInterface (src/event_dispatcher/event.rs:49) has no as_any/downcast seam, so a \
+ trait object cannot reach the concrete ScriptEvent::is_dev_mode (src/script/event.rs:51). \
+ Reaching it would require adding as_any to EventInterface, i.e. a src change, which is \
+ forbidden here -- so the faithful body is currently inexpressible and is left as todo!()."]
fn test_detect_and_pass_dev_mode_to_event_and_to_dispatching() {
todo!()
}
diff --git a/crates/shirabe/tests/command/status_command_test.rs b/crates/shirabe/tests/command/status_command_test.rs
index aa0ecc6..f10df7b 100644
--- a/crates/shirabe/tests/command/status_command_test.rs
+++ b/crates/shirabe/tests/command/status_command_test.rs
@@ -39,9 +39,11 @@ fn test_no_local_changes() {
drop(tear_down);
}
-#[ignore = "exercises `install` over the network (downloads composer/class-map-generator from a git \
- source or smarty/smarty from a dist zip), then mutates the installed package and runs \
- `status`; the install path needs real network access"]
+#[ignore = "not a partial-mock test: it runs `install` to download composer/class-map-generator (git \
+ source) or smarty/smarty (dist zip), mutates the installed package, then runs `status`. \
+ Porting without network would require fabricating a real git checkout under vendor/, a \
+ `.git` metadata repo, and wiring download-manager routing so GitDownloader::get_local_changes \
+ runs `git status` against it — a full VCS integration fixture, not a SUT seam"]
#[test]
fn test_locally_modified_packages() {
todo!()
diff --git a/crates/shirabe/tests/downloader/download_manager_test.rs b/crates/shirabe/tests/downloader/download_manager_test.rs
index 2405b4b..10fd1a9 100644
--- a/crates/shirabe/tests/downloader/download_manager_test.rs
+++ b/crates/shirabe/tests/downloader/download_manager_test.rs
@@ -3,23 +3,94 @@
use std::cell::RefCell;
use std::rc::Rc;
+use indexmap::IndexMap;
use shirabe::downloader::DownloaderInterface;
use shirabe::downloader::download_manager::DownloadManager;
use shirabe::io::IOInterface;
use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe_php_shim::{PhpMixed, RuntimeException};
use shirabe_semver::VersionParser;
use crate::downloader_stub::DownloaderStub;
use crate::io_stub::IOStub;
+// PHP mocks `Composer\Downloader\DownloaderInterface` with getMockBuilder.
+mockall::mock! {
+ #[derive(Debug)]
+ pub Downloader {}
+ #[async_trait::async_trait(?Send)]
+ impl DownloaderInterface for Downloader {
+ fn get_installation_source(&self) -> String;
+ async fn download(
+ &mut self,
+ package: PackageInterfaceHandle,
+ path: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ output: bool,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn prepare(
+ &mut self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ path: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn install(
+ &mut self,
+ package: PackageInterfaceHandle,
+ path: &str,
+ output: bool,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn update(
+ &mut self,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ path: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn remove(
+ &mut self,
+ package: PackageInterfaceHandle,
+ path: &str,
+ output: bool,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn cleanup(
+ &mut self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ path: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ }
+}
+
+fn run<F: std::future::Future>(future: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .build()
+ .unwrap()
+ .block_on(future)
+}
+
/// ref: DownloadManagerTest::createPackageMock
///
/// PHPUnit returns a `PackageInterface` mock; a real CompletePackage with the
/// relevant fields left at their defaults is an equivalent stand-in for the
/// installation-source/type dispatch logic exercised by the ported cases.
fn create_package_mock() -> PackageInterfaceHandle {
- let norm_version = VersionParser.normalize("1.0.0", None).unwrap();
- CompletePackageHandle::new("dummy/pkg".to_string(), norm_version, "1.0.0".to_string()).into()
+ make_package("dummy/pkg", false)
+}
+
+/// Real package stand-in whose dev flag is derived from the version stability, so
+/// `isDev()` can be controlled (`dev-master` => dev, `1.0.0` => stable).
+fn make_package(name: &str, is_dev: bool) -> PackageInterfaceHandle {
+ let (version, pretty) = if is_dev {
+ ("dev-master".to_string(), "dev-master".to_string())
+ } else {
+ (
+ VersionParser.normalize("1.0.0", None).unwrap(),
+ "1.0.0".to_string(),
+ )
+ };
+ CompletePackageHandle::new(name.to_string(), version, pretty).into()
}
/// ref: DownloadManagerTest::createDownloaderMock
@@ -27,6 +98,22 @@ fn create_downloader_mock() -> Rc<RefCell<dyn DownloaderInterface>> {
Rc::new(RefCell::new(DownloaderStub::new())) as Rc<RefCell<dyn DownloaderInterface>>
}
+/// A `createDownloaderMock()` whose `getInstallationSource()` reports the given
+/// source, matching the type under which it is registered with the manager so the
+/// real `getDownloaderForPackage` dispatch resolves to it.
+fn downloader_mock(installation_source: &str) -> MockDownloader {
+ let mut downloader = MockDownloader::new();
+ let source = installation_source.to_string();
+ downloader
+ .expect_get_installation_source()
+ .returning(move || source.clone());
+ downloader
+}
+
+fn as_dyn(downloader: MockDownloader) -> Rc<RefCell<dyn DownloaderInterface>> {
+ Rc::new(RefCell::new(downloader)) as Rc<RefCell<dyn DownloaderInterface>>
+}
+
fn create_manager() -> DownloadManager {
let io = Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>;
DownloadManager::new(io, false, None)
@@ -73,170 +160,698 @@ fn test_get_downloader_for_metapackage() {
);
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_correctly_installed_dist_package() {
- todo!()
+ let package = create_package_mock();
+ package.set_installation_source(Some("dist".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let downloader = as_dyn(downloader_mock("dist"));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", downloader.clone());
+
+ let result = manager
+ .get_downloader_for_package(package)
+ .unwrap()
+ .unwrap();
+ assert!(Rc::ptr_eq(&downloader, &result));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
+// The LogicException message uses get_class($downloader); the equivalent
+// `shirabe_php_shim::get_class_obj` is still a `todo!()`, so building the error
+// panics before `getDownloaderForPackage` can return it.
+#[ignore = "requires shirabe_php_shim::get_class_obj (PHP get_class), still todo!()"]
#[test]
fn test_get_downloader_for_incorrectly_installed_dist_package() {
- todo!()
+ let package = create_package_mock();
+ package.set_installation_source(Some("dist".to_string()));
+ package.set_dist_type(Some("git".to_string()));
+
+ let downloader = as_dyn(downloader_mock("source"));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", downloader);
+
+ // LogicException: the resolved downloader is a source downloader.
+ assert!(manager.get_downloader_for_package(package).is_err());
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_correctly_installed_source_package() {
- todo!()
+ let package = create_package_mock();
+ package.set_installation_source(Some("source".to_string()));
+ package.__set_source_type(Some("git".to_string()));
+
+ let downloader = as_dyn(downloader_mock("source"));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", downloader.clone());
+
+ let result = manager
+ .get_downloader_for_package(package)
+ .unwrap()
+ .unwrap();
+ assert!(Rc::ptr_eq(&downloader, &result));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
+// See test_get_downloader_for_incorrectly_installed_dist_package: the LogicException
+// path depends on the still-unimplemented get_class_obj shim.
+#[ignore = "requires shirabe_php_shim::get_class_obj (PHP get_class), still todo!()"]
#[test]
fn test_get_downloader_for_incorrectly_installed_source_package() {
- todo!()
+ let package = create_package_mock();
+ package.set_installation_source(Some("source".to_string()));
+ package.__set_source_type(Some("pear".to_string()));
+
+ let downloader = as_dyn(downloader_mock("dist"));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", downloader);
+
+ assert!(manager.get_downloader_for_package(package).is_err());
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download() {
- todo!()
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download_failover() {
- todo!()
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ // dist downloader fails, source downloader (tried next) succeeds.
+ let mut downloader_fail = downloader_mock("dist");
+ downloader_fail
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| {
+ Err(RuntimeException {
+ message: "Foo".to_string(),
+ code: 0,
+ }
+ .into())
+ });
+
+ let mut downloader_success = downloader_mock("source");
+ downloader_success
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader_fail));
+ manager.set_downloader("git", as_dyn(downloader_success));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ // PHP asserts setInstallationSource was called with 'dist' then 'source'
+ // (withConsecutive/exactly(2)). The real package retains only the final
+ // installation_source, so the dist-before-failover step is instead evidenced by
+ // both downloaders' expect_download().times(1): the 'dist' downloader is reached
+ // and fails, then the 'source' downloader is reached and succeeds. A faithful
+ // ordering check would need a src-side set-history hook on Package.
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
#[test]
fn test_bad_package_download() {
- todo!()
+ let package = create_package_mock();
+ // getSourceType() => null, getDistType() => null.
+
+ let manager = create_manager();
+
+ assert!(run(manager.download(package, "target_dir", None)).is_err());
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_dist_only_package_download() {
- todo!()
+ let package = create_package_mock();
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_source_only_package_download() {
- todo!()
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_metapackage_package_download() {
- todo!()
+ // There is no downloader for metapackages, so getDownloaderForPackage yields none.
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+ package.__set_type("metapackage".to_string());
+
+ let manager = create_manager();
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download_with_source_preferred() {
- todo!()
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+
+ manager.set_prefer_source(true);
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_dist_only_package_download_with_source_preferred() {
- todo!()
+ let package = create_package_mock();
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+
+ manager.set_prefer_source(true);
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_source_only_package_download_with_source_preferred() {
- todo!()
+ let package = create_package_mock();
+ package.__set_source_type(Some("git".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+
+ manager.set_prefer_source(true);
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
#[test]
fn test_bad_package_download_with_source_preferred() {
- todo!()
+ let package = create_package_mock();
+ // getSourceType() => null, getDistType() => null.
+
+ let mut manager = create_manager();
+ manager.set_prefer_source(true);
+
+ assert!(run(manager.download(package, "target_dir", None)).is_err());
}
-#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"]
#[test]
fn test_update_dist_with_equal_types() {
- todo!()
+ let initial = create_package_mock();
+ initial.set_installation_source(Some("dist".to_string()));
+ initial.set_dist_type(Some("zip".to_string()));
+
+ let target = create_package_mock();
+ target.set_installation_source(Some("dist".to_string()));
+ target.set_dist_type(Some("zip".to_string()));
+
+ let mut zip_downloader = downloader_mock("dist");
+ zip_downloader
+ .expect_update()
+ .times(1)
+ .withf(|_initial, _target, path| path == "vendor/bundles/FOS/UserBundle")
+ .returning(|_, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("zip", as_dyn(zip_downloader));
+
+ run(manager.update(initial, target, "vendor/bundles/FOS/UserBundle")).unwrap();
}
-#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"]
#[test]
fn test_update_dist_with_not_equal_types() {
- todo!()
+ let initial = create_package_mock();
+ initial.set_installation_source(Some("dist".to_string()));
+ initial.set_dist_type(Some("xz".to_string()));
+
+ let target = create_package_mock();
+ target.set_installation_source(Some("dist".to_string()));
+ target.set_dist_type(Some("zip".to_string()));
+
+ let mut xz_downloader = downloader_mock("dist");
+ xz_downloader
+ .expect_remove()
+ .times(1)
+ .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle")
+ .returning(|_, _, _| Ok(None));
+
+ let mut zip_downloader = downloader_mock("dist");
+ zip_downloader
+ .expect_install()
+ .times(1)
+ .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle")
+ .returning(|_, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("xz", as_dyn(xz_downloader));
+ manager.set_downloader("zip", as_dyn(zip_downloader));
+
+ run(manager.update(initial, target, "vendor/bundles/FOS/UserBundle")).unwrap();
}
-#[ignore = "requires PHPUnit mock of PackageInterface and ReflectionMethod for private getAvailableSources"]
#[test]
fn test_get_available_sources_update_sticks_to_same_source() {
- todo!()
+ // ref: updatesProvider. Columns: prevPkgSource, prevPkgIsDev, targetAvailable,
+ // targetIsDev, expected.
+ struct Case {
+ prev_pkg_source: Option<&'static str>,
+ prev_pkg_is_dev: bool,
+ target_available: &'static [&'static str],
+ target_is_dev: bool,
+ expected: &'static [&'static str],
+ }
+
+ let cases = [
+ // updates keep previous source as preference
+ Case {
+ prev_pkg_source: Some("source"),
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: false,
+ expected: &["source", "dist"],
+ },
+ Case {
+ prev_pkg_source: Some("dist"),
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: false,
+ expected: &["dist", "source"],
+ },
+ // updates do not keep previous source if target package does not have it
+ Case {
+ prev_pkg_source: Some("source"),
+ prev_pkg_is_dev: false,
+ target_available: &["dist"],
+ target_is_dev: false,
+ expected: &["dist"],
+ },
+ Case {
+ prev_pkg_source: Some("dist"),
+ prev_pkg_is_dev: false,
+ target_available: &["source"],
+ target_is_dev: false,
+ expected: &["source"],
+ },
+ // updates do not keep previous source if target is dev and prev wasn't dev and installed from dist
+ Case {
+ prev_pkg_source: Some("source"),
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: true,
+ expected: &["source", "dist"],
+ },
+ Case {
+ prev_pkg_source: Some("dist"),
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: true,
+ expected: &["source", "dist"],
+ },
+ // install picks the right default
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: true,
+ expected: &["source", "dist"],
+ },
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["dist"],
+ target_is_dev: true,
+ expected: &["dist"],
+ },
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["source"],
+ target_is_dev: true,
+ expected: &["source"],
+ },
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["source", "dist"],
+ target_is_dev: false,
+ expected: &["dist", "source"],
+ },
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["dist"],
+ target_is_dev: false,
+ expected: &["dist"],
+ },
+ Case {
+ prev_pkg_source: None,
+ prev_pkg_is_dev: false,
+ target_available: &["source"],
+ target_is_dev: false,
+ expected: &["source"],
+ },
+ ];
+
+ let manager = create_manager();
+
+ for case in cases {
+ let initial = case.prev_pkg_source.map(|source| {
+ let package = make_package("dummy/pkg", case.prev_pkg_is_dev);
+ package.set_installation_source(Some(source.to_string()));
+ package
+ });
+
+ let target = make_package("dummy/pkg", case.target_is_dev);
+ if case.target_available.contains(&"source") {
+ target.__set_source_type(Some("git".to_string()));
+ }
+ if case.target_available.contains(&"dist") {
+ target.set_dist_type(Some("zip".to_string()));
+ }
+
+ let result = manager.__get_available_sources(target, initial).unwrap();
+ let expected: Vec<String> = case.expected.iter().map(|s| s.to_string()).collect();
+ assert_eq!(result, expected);
+ }
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_update_metapackage() {
- todo!()
+ // There is no downloader for metapackages.
+ let initial = create_package_mock();
+ initial.__set_type("metapackage".to_string());
+ let target = create_package_mock();
+ target.__set_type("metapackage".to_string());
+
+ let manager = create_manager();
+
+ assert!(
+ run(manager.update(initial, target, "vendor/pkg"))
+ .unwrap()
+ .is_none()
+ );
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_remove() {
- todo!()
+ let package = create_package_mock();
+ package.set_installation_source(Some("dist".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut pear_downloader = downloader_mock("dist");
+ pear_downloader
+ .expect_remove()
+ .times(1)
+ .withf(|_pkg, path, _output| path == "vendor/bundles/FOS/UserBundle")
+ .returning(|_, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(pear_downloader));
+
+ run(manager.remove(package, "vendor/bundles/FOS/UserBundle")).unwrap();
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_metapackage_remove() {
- todo!()
+ // There is no downloader for metapackages.
+ let package = create_package_mock();
+ package.__set_type("metapackage".to_string());
+
+ let manager = create_manager();
+
+ assert!(
+ run(manager.remove(package, "vendor/bundles/FOS/UserBundle"))
+ .unwrap()
+ .is_none()
+ );
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_without_preference_dev() {
- todo!()
+ let package = make_package("dummy/pkg", true);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_without_preference_no_dev() {
- todo!()
+ let package = make_package("dummy/pkg", false);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_without_match_dev() {
- todo!()
+ let package = make_package("bar/package", true);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([(
+ "foo/*".to_string(),
+ "source".to_string(),
+ )]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_without_match_no_dev() {
- todo!()
+ let package = make_package("bar/package", false);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([(
+ "foo/*".to_string(),
+ "source".to_string(),
+ )]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_with_match_auto_dev() {
- todo!()
+ let package = make_package("foo/package", true);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([("foo/*".to_string(), "auto".to_string())]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_with_match_auto_no_dev() {
- todo!()
+ let package = make_package("foo/package", false);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([("foo/*".to_string(), "auto".to_string())]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_with_match_source() {
- todo!()
+ let package = make_package("foo/package", false);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("source");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("git", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([(
+ "foo/*".to_string(),
+ "source".to_string(),
+ )]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("source"));
}
-#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
+/// @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
#[test]
fn test_install_preference_with_match_dist() {
- todo!()
+ let package = make_package("foo/package", false);
+ package.__set_source_type(Some("git".to_string()));
+ package.set_dist_type(Some("pear".to_string()));
+
+ let mut downloader = downloader_mock("dist");
+ downloader
+ .expect_download()
+ .times(1)
+ .withf(|_pkg, path, _prev, _output| path == "target_dir")
+ .returning(|_, _, _, _| Ok(None));
+
+ let mut manager = create_manager();
+ manager.set_downloader("pear", as_dyn(downloader));
+ manager.set_preferences(IndexMap::from([("foo/*".to_string(), "dist".to_string())]));
+
+ run(manager.download(package.clone(), "target_dir", None)).unwrap();
+
+ assert_eq!(package.get_installation_source().as_deref(), Some("dist"));
}
diff --git a/crates/shirabe/tests/downloader/file_downloader_test.rs b/crates/shirabe/tests/downloader/file_downloader_test.rs
index cb09c74..6f7025e 100644
--- a/crates/shirabe/tests/downloader/file_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/file_downloader_test.rs
@@ -5,14 +5,16 @@ use std::rc::Rc;
use indexmap::IndexMap;
use serial_test::serial;
+use shirabe::cache::{Cache, CacheMock};
use shirabe::config::Config;
use shirabe::downloader::DownloaderInterface;
use shirabe::downloader::FileDownloader;
use shirabe::io::IOInterface;
+use shirabe::io::io_interface;
use shirabe::io::null_io::NullIO;
use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
use shirabe::util::HttpDownloader;
-use shirabe::util::filesystem::Filesystem;
+use shirabe::util::filesystem::{Filesystem, FilesystemMock};
use shirabe::util::r#loop::Loop;
use shirabe_php_shim::{
InvalidArgumentException, PhpMixed, RuntimeException, UnexpectedValueException,
@@ -21,6 +23,7 @@ use shirabe_semver::VersionParser;
use tempfile::TempDir;
use crate::http_downloader_mock::get_http_downloader_mock;
+use crate::io_mock::{Expectation, get_io_mock};
use shirabe::util::http_downloader::HttpDownloaderMockHandler;
/// ref: TestCase::getPackage (default class CompletePackage)
@@ -193,9 +196,61 @@ fn test_download_with_custom_cache_key() {
}
#[test]
-#[ignore = "requires a Cache mock with gcIsNecessary/gc expectation tracking; Cache is a concrete struct with no test hook for asserting gc() was called once"]
+#[serial]
+#[ignore = "PHP Config::get('cache-files-ttl') casts the string via (int) (Config.php:396-398), turning '99999999' into 99999999, but shirabe's PhpMixed::as_int returns None for String so Config::get yields 0 and the assertion fails. Faithful port stays failing until the src is fixed."]
fn test_cache_garbage_collection_is_called() {
- todo!()
+ let expected_ttl: i64 = 99999999;
+
+ let mut config_options: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_options.insert(
+ "cache-files-ttl".to_string(),
+ PhpMixed::String("99999999".to_string()),
+ );
+ config_options.insert(
+ "cache-files-maxsize".to_string(),
+ PhpMixed::String("500M".to_string()),
+ );
+ let config = get_config(config_options);
+
+ // The PHP Cache mock forces gcIsNecessary() true and records the single gc() call; the CacheMock
+ // seam plays both roles here.
+ let tmp_dir = TempDir::new().unwrap();
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let mut cache = Cache::new(io, tmp_dir.path().to_str().unwrap(), None, None, false);
+ cache.__set_mock(CacheMock {
+ gc_is_necessary: Some(true),
+ gc_calls: Some(Vec::new()),
+ ..Default::default()
+ });
+ let cache = Rc::new(RefCell::new(cache));
+
+ let (http_downloader, _guard) = get_http_downloader_mock(
+ Vec::new(),
+ false,
+ HttpDownloaderMockHandler {
+ status: 200,
+ body: "file~".to_string(),
+ headers: Vec::new(),
+ },
+ );
+
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let _downloader = FileDownloader::new(
+ io,
+ config,
+ http_downloader,
+ None,
+ Some(cache.clone()),
+ None,
+ None,
+ );
+
+ let gc_calls = cache.borrow().__gc_calls();
+ assert_eq!(gc_calls.len(), 1, "gc should be called exactly once");
+ assert_eq!(
+ gc_calls[0].0, expected_ttl,
+ "gc should receive the configured ttl"
+ );
}
#[test]
@@ -245,8 +300,86 @@ fn test_download_file_with_invalid_checksum() {
}
#[test]
-#[ignore = "requires a Filesystem mock of removeDirectoryAsync/normalizePath plus IOMock output expectations; Filesystem is a concrete struct with no async-removal test hook"]
+#[serial]
fn test_downgrade_shows_appropriate_message() {
- let _ = Filesystem::new(None);
- todo!()
+ let old_package = get_package("dummy/pkg", "1.2.0");
+ let new_package = get_package("dummy/pkg", "1.0.0");
+ new_package.set_dist_url(Some("http://example.com/script.js".to_string()));
+
+ let (io_mock, _io_guard) = get_io_mock(io_interface::DEBUG).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![
+ Expectation::text_regex("{Downloading .*}"),
+ Expectation::text_regex("{Downgrading .*}"),
+ ],
+ false,
+ )
+ .unwrap();
+
+ let tmp_dir = TempDir::new().unwrap();
+ let path = tmp_dir.path().to_string_lossy().into_owned();
+ let mut config_options: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_options.insert(
+ "vendor-dir".to_string(),
+ PhpMixed::String(format!("{}/vendor", path)),
+ );
+ let config = get_config(config_options);
+
+ // PHP mocks Filesystem so removeDirectoryAsync is a no-op (resolve(true)) and normalizePath is an
+ // identity; the FilesystemMock seam reproduces both so update()'s remove/install do not disturb
+ // the pre-staged download file.
+ let mut filesystem = Filesystem::new(None);
+ filesystem.__set_mock(FilesystemMock {
+ remove_directory_async_result: Some(true),
+ normalize_path_identity: true,
+ ..Default::default()
+ });
+ let filesystem = Rc::new(RefCell::new(filesystem));
+
+ let (http_downloader, _guard) = get_http_downloader_mock(
+ Vec::new(),
+ false,
+ HttpDownloaderMockHandler {
+ status: 200,
+ body: "file~".to_string(),
+ headers: Vec::new(),
+ },
+ );
+
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+ let mut downloader = FileDownloader::new(
+ io,
+ config,
+ http_downloader.clone(),
+ None,
+ None,
+ Some(filesystem.clone()),
+ None,
+ );
+
+ // make sure the file expected to be downloaded is on disk already
+ let dl_file = downloader.__get_file_name(new_package.clone(), &path);
+ let dir = std::path::Path::new(&dl_file).parent().unwrap();
+ std::fs::create_dir_all(dir).unwrap();
+ std::fs::write(&dl_file, b"").unwrap();
+
+ let mut loop_ = Loop::new(http_downloader, None);
+ let promise = Box::pin(async {
+ downloader
+ .download(new_package.clone(), &path, Some(old_package.clone()), true)
+ .await
+ .map(|_| ())
+ });
+ run(loop_.wait(vec![promise], None)).expect("download should succeed");
+
+ run(downloader.update(old_package.clone(), new_package.clone(), &path))
+ .expect("update should succeed");
+
+ assert_eq!(
+ filesystem.borrow().__remove_directory_async_calls(),
+ 1,
+ "removeDirectoryAsync should be called exactly once"
+ );
}
diff --git a/crates/shirabe/tests/downloader/git_downloader_test.rs b/crates/shirabe/tests/downloader/git_downloader_test.rs
index 07dec08..77a976e 100644
--- a/crates/shirabe/tests/downloader/git_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/git_downloader_test.rs
@@ -9,6 +9,8 @@ use shirabe::config::Config;
use shirabe::downloader::VcsDownloader;
use shirabe::downloader::git_downloader::GitDownloader;
use shirabe::io::IOInterface;
+use shirabe::io::io_interface;
+use shirabe::package::Mirror;
use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
use shirabe::util::Git as GitUtil;
use shirabe::util::ProcessExecutor;
@@ -18,6 +20,7 @@ use shirabe_semver::VersionParser;
use tempfile::TempDir;
use crate::config_stub::ConfigStubBuilder;
+use crate::io_mock::{Expectation, get_io_mock};
use crate::io_stub::IOStub;
use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
@@ -333,9 +336,11 @@ fn test_download_with_cache() {
fs.remove_directory(&cache_path).ok();
}
-#[ignore = "getSourceUrls returns a mirror list (['https://github.com/mirrors/composer', \
- 'https://github.com/composer/composer']) that a real CompletePackage cannot reproduce \
- from a single source_url; no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[ignore = "Source-side blocker: getSourceUrls() must list the mirror ahead of the \
+ source url, which requires a preferred source mirror. A git source mirror is \
+ processed through ComposerMirror::process_git_url, whose github regex lacks PCRE \
+ delimiters and panics when compiled by the php-shim Preg. PHP mocks getSourceUrls \
+ directly and never exercises that path."]
#[test]
fn test_download_uses_various_protocols_and_sets_push_url_for_github() {
let working_dir = set_up();
@@ -344,14 +349,106 @@ fn test_download_uses_various_protocols_and_sets_push_url_for_github() {
todo!()
}
-#[ignore = "pushUrlProvider configures getSourceUrls to a list a real CompletePackage cannot \
- reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[serial]
#[test]
fn test_download_and_set_push_url_use_custom_various_protocols_for_github() {
- let working_dir = set_up();
- let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+ // ref: pushUrlProvider — (github-protocols, fetch url, push url).
+ let cases: Vec<(Vec<&str>, &str, &str)> = vec![
+ (
+ vec!["ssh"],
+ "git@github.com:composer/composer",
+ "git@github.com:composer/composer.git",
+ ),
+ (
+ vec!["https", "ssh", "git"],
+ "https://github.com/composer/composer",
+ "git@github.com:composer/composer.git",
+ ),
+ (
+ vec!["https"],
+ "https://github.com/composer/composer",
+ "https://github.com/composer/composer.git",
+ ),
+ ];
+
+ for (protocols, url, push_url) in cases {
+ let working_dir = set_up();
+ let _tear_down = TearDown::new(working_dir.path().to_path_buf());
+
+ let package = get_package(
+ "composer/composer",
+ "1.0.0",
+ Some("ref"),
+ Some("https://github.com/composer/composer"),
+ );
+
+ let expected_path = working_dir
+ .path()
+ .join("composerPath")
+ .to_string_lossy()
+ .into_owned();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(vec![
+ "git",
+ "clone",
+ "--no-checkout",
+ "--",
+ url,
+ &expected_path,
+ ]),
+ cmd(vec!["git", "remote", "add", "composer", "--", url]),
+ cmd(["git", "fetch", "composer"]),
+ cmd(vec!["git", "remote", "set-url", "origin", "--", url]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd(vec![
+ "git", "remote", "set-url", "--push", "origin", "--", push_url,
+ ]),
+ cmd(["git", "branch", "-r"]),
+ cmd(["git", "checkout", "ref", "--"]),
+ cmd(["git", "reset", "--hard", "ref", "--"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut config = Config::new(false, None);
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut section: IndexMap<String, PhpMixed> = IndexMap::new();
+ section.insert(
+ "github-protocols".to_string(),
+ PhpMixed::List(
+ protocols
+ .iter()
+ .map(|p| PhpMixed::String(p.to_string()))
+ .collect(),
+ ),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+
+ let mut downloader = get_downloader_mock(None, Some(config), process, None);
+
+ run(async {
+ downloader
+ .download(package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .prepare("install", package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .install(package.clone(), &expected_path)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("install", package, &expected_path, None)
+ .await
+ .unwrap();
+ });
+ }
}
#[serial]
@@ -589,18 +686,95 @@ fn test_update_with_new_repo_url() {
});
}
-#[ignore = "getSourceUrls returns a multi-URL list a real CompletePackage cannot reproduce; \
- no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[ignore = "Blocked by a Config bug: Config::get(\"github-protocols\") does not drop the \
+ insecure \"git\" protocol under secure-http. array_search_mixed returns the \
+ matched index as PhpMixed::Int, but config.rs reads it via as_string (which is \
+ Some only for String), so the removal is skipped and get() returns \
+ [https, ssh, git] instead of [https, ssh]. The downloader then attempts a third \
+ git:// fetch (absent from the process mock) and the error message reads \
+ \"via https, ssh, git protocols\", failing the assertion. PHP's new Config() \
+ reduces it to two protocols."]
+#[serial]
#[test]
fn test_update_throws_runtime_exception_if_git_command_fails() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://github.com/composer/composer";
+ let package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(["git", "show-ref", "--head", "-d"]),
+ cmd(["git", "status", "--porcelain", "--untracked-files=no"]),
+ // commit not yet in so we try to fetch
+ cmd_full(
+ ["git", "rev-parse", "--quiet", "--verify", "ref^{commit}"],
+ 1,
+ "",
+ "",
+ ),
+ // fail first fetch
+ cmd(["git", "remote", "-v"]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd_full(["git", "fetch", "composer"], 1, "", ""),
+ // fail second fetch
+ cmd(vec![
+ "git",
+ "remote",
+ "set-url",
+ "composer",
+ "--",
+ "git@github.com:composer/composer",
+ ]),
+ cmd_full(["git", "fetch", "composer"], 1, "", ""),
+ cmd(["git", "--version"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy()))
+ .unwrap();
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let config = Config::new(false, None);
+
+ let mut downloader = get_downloader_mock(None, Some(config), process, None);
+
+ let result = run(async {
+ downloader
+ .download(package.clone(), &working_dir_str, Some(package.clone()))
+ .await?;
+ downloader
+ .prepare(
+ "update",
+ package.clone(),
+ &working_dir_str,
+ Some(package.clone()),
+ )
+ .await?;
+ downloader
+ .update(package.clone(), package.clone(), &working_dir_str)
+ .await?;
+ downloader
+ .cleanup("update", package.clone(), &working_dir_str, Some(package))
+ .await
+ });
+
+ let e = result.expect_err("failing git fetch should throw");
+ assert!(e.to_string().contains(
+ "Failed to clone https://github.com/composer/composer via https, ssh protocols, aborting."
+ ));
+ assert!(e.to_string().contains("git@github.com:composer/composer"));
}
-#[ignore = "getSourceUrls returns a multi-URL list (['/' , github]) a real CompletePackage cannot \
- reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[ignore = "Source-side blocker: getSourceUrls() == ['/', github] requires a source \
+ mirror so the list has two entries. A git source mirror is processed through \
+ ComposerMirror::process_git_url, whose github regex lacks PCRE delimiters and \
+ panics when compiled by the php-shim Preg. PHP mocks getSourceUrls directly and \
+ never exercises that path."]
#[test]
fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but_is_able_to_recover()
{
@@ -610,24 +784,142 @@ fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but
todo!()
}
-#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \
- cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[serial]
#[test]
fn test_downgrade_shows_appropriate_message() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://github.com/composer/composer";
+
+ let old_package = get_package("composer/composer", "1.2.0", Some("ref"), Some("/foo/bar"));
+ old_package.set_source_mirrors(Some(vec![Mirror {
+ url: url.to_string(),
+ preferred: false,
+ }]));
+ let new_package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url));
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(vec![Expectation::text_regex("{Downgrading .*}")], false)
+ .unwrap();
+ let io = io_mock.clone() as Rc<RefCell<dyn IOInterface>>;
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy()))
+ .unwrap();
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let mut downloader = get_downloader_mock(Some(io), None, process, None);
+
+ run(async {
+ downloader
+ .download(
+ new_package.clone(),
+ &working_dir_str,
+ Some(old_package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .prepare(
+ "update",
+ new_package.clone(),
+ &working_dir_str,
+ Some(old_package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(old_package.clone(), new_package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", new_package, &working_dir_str, Some(old_package))
+ .await
+ .unwrap();
+ });
}
-#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \
- cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
+#[serial]
#[test]
fn test_not_using_downgrading_with_references() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://github.com/composer/composer";
+
+ // dev versions: getVersion() is the (non-normalized) branch name.
+ let old_package = CompletePackageHandle::new(
+ "composer/composer".to_string(),
+ "dev-ref".to_string(),
+ "dev-ref".to_string(),
+ );
+ old_package.__set_source_type(Some("git".to_string()));
+ old_package.set_source_reference(Some("ref".to_string()));
+ old_package.set_source_url(Some("/foo/bar".to_string()));
+ old_package.set_source_mirrors(Some(vec![Mirror {
+ url: url.to_string(),
+ preferred: false,
+ }]));
+ let old_package: PackageInterfaceHandle = old_package.into();
+
+ let new_package = CompletePackageHandle::new(
+ "composer/composer".to_string(),
+ "dev-ref2".to_string(),
+ "dev-ref2".to_string(),
+ );
+ new_package.__set_source_type(Some("git".to_string()));
+ new_package.set_source_reference(Some("ref".to_string()));
+ new_package.set_source_url(Some(url.to_string()));
+ let new_package: PackageInterfaceHandle = new_package.into();
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(vec![Expectation::text_regex("{Upgrading .*}")], false)
+ .unwrap();
+ let io = io_mock.clone() as Rc<RefCell<dyn IOInterface>>;
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy()))
+ .unwrap();
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let mut downloader = get_downloader_mock(Some(io), None, process, None);
+
+ run(async {
+ downloader
+ .download(
+ new_package.clone(),
+ &working_dir_str,
+ Some(old_package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .prepare(
+ "update",
+ new_package.clone(),
+ &working_dir_str,
+ Some(old_package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(old_package.clone(), new_package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", new_package, &working_dir_str, Some(old_package))
+ .await
+ .unwrap();
+ });
}
#[ignore = "PHP mocks Filesystem::removeDirectoryAsync (asserting it is called once with the \
diff --git a/crates/shirabe/tests/downloader/perforce_downloader_test.rs b/crates/shirabe/tests/downloader/perforce_downloader_test.rs
index 63f1de4..f23888a 100644
--- a/crates/shirabe/tests/downloader/perforce_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/perforce_downloader_test.rs
@@ -1,44 +1,193 @@
//! ref: composer/tests/Composer/Test/Downloader/PerforceDownloaderTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::downloader::VcsDownloader;
+use shirabe::downloader::perforce_downloader::PerforceDownloader;
+use shirabe::io::IOInterface;
+use shirabe::io::io_interface::NORMAL;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe::util::filesystem::Filesystem;
+use shirabe::util::process_executor::MockHandler;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::VersionParser;
use tempfile::TempDir;
-fn set_up() -> TempDir {
- let test_path = TempDir::new().unwrap();
- // repoConfig/config/io/processExecutor/repository/package/downloader rely on
- // ProcessExecutorMock and PHPUnit mocks of the repository and Package, which are not
- // ported.
- let () = todo!();
- #[allow(unreachable_code)]
- test_path
+use crate::io_mock::{Expectation, get_io_mock};
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::get_process_executor_mock;
+
+// A getMockBuilder('Composer\Util\Perforce') stand-in: the seam trait extracted from the
+// concrete `Perforce` struct, mocked so the downloader's workflow can be verified.
+mockall::mock! {
+ #[derive(Debug)]
+ pub Perforce {}
+ impl shirabe::util::PerforceInterface for Perforce {
+ fn initialize_path(&mut self, path: &str);
+ fn set_stream(&mut self, stream: &str);
+ fn p4_login(&mut self) -> anyhow::Result<()>;
+ fn check_stream(&mut self) -> bool;
+ fn write_p4_client_spec(&mut self) -> anyhow::Result<()>;
+ fn connect_client(&mut self) -> anyhow::Result<()>;
+ fn sync_code_base(&mut self, source_reference: Option<String>) -> anyhow::Result<()>;
+ fn cleanup_client_spec(&mut self);
+ fn get_commit_logs(&mut self, from_reference: &str, to_reference: &str) -> Option<String>;
+ fn get_file_content(&mut self, file: &str, identifier: &str) -> Option<String>;
+ fn get_branches(&mut self) -> IndexMap<String, String>;
+ fn get_tags(&mut self) -> IndexMap<String, String>;
+ fn get_user(&self) -> Option<String>;
+ fn get_composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> anyhow::Result<Option<IndexMap<String, PhpMixed>>>;
+ }
+}
+
+fn run<F: std::future::Future>(future: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .build()
+ .unwrap()
+ .block_on(future)
}
-// These mock Perforce, the repository config and a Package to drive PerforceDownloader's
-// initialization and install paths; mocking is not available here.
+/// ref: PerforceDownloaderTest::getConfig (seeds `home` with the temp dir)
+fn get_config(test_path: &std::path::Path) -> Config {
+ let mut config = Config::new(true, None);
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut section: IndexMap<String, PhpMixed> = IndexMap::new();
+ section.insert(
+ "home".to_string(),
+ PhpMixed::String(test_path.to_string_lossy().into_owned()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ config
+}
+
+/// ref: PerforceDownloaderTest::getMockPackageInterface. A real CompletePackage stands in for
+/// the PHPUnit PackageInterface mock; the source reference is returned by getSourceReference.
+fn make_package(source_reference: Option<&str>) -> PackageInterfaceHandle {
+ let norm_version = VersionParser.normalize("1.0.0", None).unwrap();
+ let package =
+ CompletePackageHandle::new("test/pkg".to_string(), norm_version, "1.0.0".to_string());
+ package.set_source_reference(source_reference.map(|s| s.to_string()));
+ package.into()
+}
-#[ignore = "requires PHPUnit getMockBuilder mocks of IOInterface/PackageInterface/VcsRepository and ProcessExecutorMock via unported set_up()"]
#[test]
fn test_init_perforce_instantiates_a_new_perforce_object() {
- let _test_path = set_up();
- todo!()
+ // @doesNotPerformAssertions: only verifies init_perforce instantiates a Perforce without
+ // error. PHP attaches a VcsRepository whose getRepoConfig seeds the config, but in the
+ // current port VcsRepository implements ConfigurableRepositoryInterface only (not
+ // RepositoryInterface), so it cannot be held in a RepositoryInterfaceHandle. The package
+ // is therefore built without a repository, yielding an empty repo config.
+ let test_path = TempDir::new().unwrap();
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new()));
+ let config = Rc::new(RefCell::new(get_config(test_path.path())));
+ let package = make_package(None);
+
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ let mut downloader = PerforceDownloader::new(io, config, process, fs);
+
+ downloader.init_perforce(
+ package,
+ test_path.path().to_string_lossy().into_owned(),
+ "SOURCE_REF".to_string(),
+ );
}
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce and expects()->never() verification, unavailable in the Rust port"]
#[test]
fn test_init_perforce_does_nothing_if_perforce_already_set() {
- let _test_path = set_up();
- todo!()
+ let test_path = TempDir::new().unwrap();
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new()));
+ let config = Rc::new(RefCell::new(get_config(test_path.path())));
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ let mut downloader = PerforceDownloader::new(io, config, process, fs);
+
+ // The already-set perforce only sees initializePath; the repository's getRepoConfig is
+ // never consulted (the early return happens before reaching it).
+ let mut perforce = MockPerforce::new();
+ perforce.expect_initialize_path().times(1).returning(|_| ());
+ downloader.set_perforce(Box::new(perforce));
+
+ let package = make_package(None);
+ downloader.init_perforce(
+ package,
+ test_path.path().to_string_lossy().into_owned(),
+ "SOURCE_REF".to_string(),
+ );
}
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce with expects()->once()->method() expectation verification, unavailable in the Rust port"]
#[test]
fn test_do_install_with_tag() {
- let _test_path = set_up();
- todo!()
+ do_install_workflow("SOURCE_REF@123", Some("123".to_string()));
}
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Util\\Perforce with expects()->once()->method() expectation verification, unavailable in the Rust port"]
#[test]
fn test_do_install_with_no_tag() {
- let _test_path = set_up();
- todo!()
+ do_install_workflow("SOURCE_REF", None);
+}
+
+// Shared body for testDoInstallWithTag / testDoInstallWithNoTag: enforce the install workflow
+// against a mocked Perforce, asserting each step is invoked exactly once.
+fn do_install_workflow(source_ref: &'static str, expected_label: Option<String>) {
+ let test_path = TempDir::new().unwrap();
+ let test_path_str = test_path.path().to_string_lossy().into_owned();
+
+ let (io_mock, _io_guard) = get_io_mock(NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![Expectation::text_regex(format!("Cloning {}", source_ref))],
+ false,
+ )
+ .unwrap();
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+ let config = Rc::new(RefCell::new(get_config(test_path.path())));
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ let mut downloader = PerforceDownloader::new(io, config, process, fs);
+
+ let mut perforce = MockPerforce::new();
+ let expected_path = test_path_str.clone();
+ perforce
+ .expect_initialize_path()
+ .times(1)
+ .withf(move |path: &str| path == expected_path)
+ .returning(|_| ());
+ perforce
+ .expect_set_stream()
+ .times(1)
+ .withf(move |stream: &str| stream == source_ref)
+ .returning(|_| ());
+ perforce.expect_p4_login().times(1).returning(|| Ok(()));
+ perforce
+ .expect_write_p4_client_spec()
+ .times(1)
+ .returning(|| Ok(()));
+ perforce
+ .expect_connect_client()
+ .times(1)
+ .returning(|| Ok(()));
+ perforce
+ .expect_sync_code_base()
+ .times(1)
+ .withf(move |reference: &Option<String>| *reference == expected_label)
+ .returning(|_| Ok(()));
+ perforce
+ .expect_cleanup_client_spec()
+ .times(1)
+ .returning(|| ());
+ downloader.set_perforce(Box::new(perforce));
+
+ let package = make_package(Some(source_ref));
+ run(downloader.do_install(package, &test_path_str, "url")).unwrap();
}
diff --git a/crates/shirabe/tests/installer/library_installer_test.rs b/crates/shirabe/tests/installer/library_installer_test.rs
index 0796efe..42730fe 100644
--- a/crates/shirabe/tests/installer/library_installer_test.rs
+++ b/crates/shirabe/tests/installer/library_installer_test.rs
@@ -323,8 +323,67 @@ fn test_get_install_path_with_target_dir() {
tear_down(&mut setup);
}
+/// Records the calls a `BinaryInstaller` double receives, standing in for the PHPUnit mock that
+/// asserts `removeBinaries` is never called and `installBinaries` is called once.
+#[derive(Debug, Default)]
+struct BinaryInstallerCalls {
+ install_binaries: Vec<(shirabe::package::PackageInterfaceHandle, String, bool)>,
+ remove_binaries: Vec<shirabe::package::PackageInterfaceHandle>,
+}
+
+#[derive(Debug)]
+struct RecordingBinaryInstaller {
+ calls: Rc<RefCell<BinaryInstallerCalls>>,
+}
+
+impl shirabe::installer::BinaryInstallerInterface for RecordingBinaryInstaller {
+ fn install_binaries(
+ &mut self,
+ package: shirabe::package::PackageInterfaceHandle,
+ install_path: &str,
+ warn_on_overwrite: bool,
+ ) {
+ self.calls.borrow_mut().install_binaries.push((
+ package,
+ install_path.to_string(),
+ warn_on_overwrite,
+ ));
+ }
+
+ fn remove_binaries(&mut self, package: shirabe::package::PackageInterfaceHandle) {
+ self.calls.borrow_mut().remove_binaries.push(package);
+ }
+}
+
#[test]
-#[ignore = "requires PHPUnit mock of BinaryInstaller (expects(never)->removeBinaries, expects(once)->installBinaries) injected via LibraryInstaller's binaryInstaller argument"]
fn test_ensure_binaries_installed() {
- todo!()
+ let mut setup = set_up();
+ let calls = Rc::new(RefCell::new(BinaryInstallerCalls::default()));
+ let mut library = LibraryInstaller::new(
+ setup.io.clone(),
+ setup.composer.clone(),
+ Some("library".to_string()),
+ None,
+ None,
+ );
+ library.__set_binary_installer(Box::new(RecordingBinaryInstaller {
+ calls: calls.clone(),
+ }));
+ let package = get_package("foo/bar", "1.0.0");
+ let expected_path = library.get_install_path(package.clone()).unwrap();
+
+ library.ensure_binaries_presence(package.clone());
+
+ let recorded = calls.borrow();
+ // PHP asserts removeBinaries is never called.
+ assert!(recorded.remove_binaries.is_empty());
+ // PHP asserts installBinaries is called once with ($package, getInstallPath, false).
+ assert_eq!(recorded.install_binaries.len(), 1);
+ let (recorded_package, recorded_path, warn_on_overwrite) = &recorded.install_binaries[0];
+ assert!(Rc::ptr_eq(recorded_package.as_rc(), package.as_rc()));
+ assert_eq!(recorded_path, &expected_path);
+ assert!(!warn_on_overwrite);
+ drop(recorded);
+
+ tear_down(&mut setup);
}
diff --git a/crates/shirabe/tests/package/loader/root_package_loader_test.rs b/crates/shirabe/tests/package/loader/root_package_loader_test.rs
index 23c8beb..7fa970a 100644
--- a/crates/shirabe/tests/package/loader/root_package_loader_test.rs
+++ b/crates/shirabe/tests/package/loader/root_package_loader_test.rs
@@ -4,21 +4,28 @@
// ProcessExecutor / VersionGuesser or require constraints whose parsing goes through a
// look-around regex the regex crate cannot compile.
-use std::cell::RefCell;
+use std::cell::{Cell, RefCell};
use std::rc::Rc;
use indexmap::IndexMap;
+use serial_test::serial;
use shirabe::config::Config;
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
+use shirabe::package::RootPackage;
use shirabe::package::loader::RootPackageLoader;
-use shirabe::package::version::{VersionGuesser, VersionParser};
+use shirabe::package::version::{
+ VersionData, VersionGuesser, VersionGuesserInterface, VersionParser,
+};
use shirabe::package::{STABILITY_ALPHA, STABILITY_DEV, STABILITY_RC};
use shirabe::repository::RepositoryManager;
+use shirabe::util::Git as GitUtil;
use shirabe::util::http_downloader::HttpDownloader;
-use shirabe::util::process_executor::ProcessExecutor;
+use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
use shirabe_php_shim::PhpMixed;
+use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
+
fn null_io() -> Rc<RefCell<dyn IOInterface>> {
Rc::new(RefCell::new(NullIO::new()))
}
@@ -35,6 +42,73 @@ fn http_downloader(
)))
}
+// `$config = new Config; $config->merge(['repositories' => ['packagist' => false]]);`
+fn make_config() -> Rc<RefCell<Config>> {
+ let config = Rc::new(RefCell::new(Config::new(true, None)));
+ let mut repositories: IndexMap<String, PhpMixed> = IndexMap::new();
+ repositories.insert("packagist".to_string(), PhpMixed::Bool(false));
+ let mut merge: IndexMap<String, PhpMixed> = IndexMap::new();
+ merge.insert("repositories".to_string(), PhpMixed::Array(repositories));
+ config.borrow_mut().merge(&merge, "test");
+ config
+}
+
+// Stands in for `getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()`.
+// The loader only stores it and feeds it default repositories, so a real instance suffices.
+fn make_manager(
+ io: &Rc<RefCell<dyn IOInterface>>,
+ config: &Rc<RefCell<Config>>,
+) -> Rc<RefCell<RepositoryManager>> {
+ Rc::new(RefCell::new(RepositoryManager::new(
+ io.clone(),
+ config.clone(),
+ http_downloader(io, config),
+ None,
+ None,
+ )))
+}
+
+fn require_map(entries: &[(&str, &str)]) -> PhpMixed {
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (k, v) in entries {
+ m.insert(k.to_string(), PhpMixed::String(v.to_string()));
+ }
+ PhpMixed::Array(m)
+}
+
+// Resets the cached git `version` static on drop so a seeded value does not leak into other
+// tests in this binary (VersionGuesserTest seeds/resets the same static).
+struct GitVersionGuard;
+
+impl Drop for GitVersionGuard {
+ fn drop(&mut self) {
+ GitUtil::__reset_version();
+ }
+}
+
+// A test double for the concrete VersionGuesser, supplied through the VersionGuesserInterface seam.
+#[derive(Debug)]
+struct VersionGuesserMock {
+ version_data: VersionData,
+ guess_version_calls: Rc<Cell<u32>>,
+}
+
+impl VersionGuesserInterface for VersionGuesserMock {
+ fn guess_version(
+ &mut self,
+ _package_config: &IndexMap<String, PhpMixed>,
+ _path: &str,
+ ) -> anyhow::Result<Option<VersionData>> {
+ self.guess_version_calls
+ .set(self.guess_version_calls.get() + 1);
+ Ok(Some(self.version_data.clone()))
+ }
+
+ fn get_root_version_from_env(&self) -> anyhow::Result<String> {
+ unreachable!("COMPOSER_ROOT_VERSION is not set in this test")
+ }
+}
+
#[test]
#[ignore = "process_executor.enable_async() drives the async stream path, which calls stream_set_blocking (fcntl(2) todo!() in shirabe-php-shim::stream)"]
fn test_stability_flags_parsing() {
@@ -70,7 +144,8 @@ fn test_stability_flags_parsing() {
Some(io.clone()),
);
- let mut loader = RootPackageLoader::new(manager, config.clone(), None, Some(guesser), None);
+ let mut loader =
+ RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None);
let mut data = IndexMap::new();
data.insert(
@@ -136,25 +211,158 @@ fn test_stability_flags_parsing() {
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects(['return' => 1]); no ProcessExecutorMock mocking infrastructure exists"]
+#[serial]
fn test_no_version_is_visible_in_pretty_version() {
- todo!()
+ GitUtil::__reset_version();
+ let _git_guard = GitVersionGuard;
+
+ let io = null_io();
+ let config = make_config();
+ let manager = make_manager(&io, &config);
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![],
+ false,
+ MockHandler {
+ r#return: 1,
+ ..Default::default()
+ },
+ );
+ let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None);
+
+ let mut loader =
+ RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None);
+
+ let package = loader
+ .load(IndexMap::new(), "Composer\\Package\\RootPackage", None)
+ .unwrap();
+ let package = package.as_root().unwrap();
+
+ assert_eq!("1.0.0.0", package.get_version());
+ assert_eq!(
+ RootPackage::DEFAULT_PRETTY_VERSION,
+ package.get_pretty_version()
+ );
}
#[test]
-#[ignore = "requires getMockBuilder VersionGuesser mock with guessVersion expectation; no VersionGuesser mocking infrastructure exists"]
+#[serial]
fn test_pretty_version_for_root_package_in_version_branch() {
- todo!()
+ // see #6845
+ let io = null_io();
+ let config = make_config();
+ let manager = make_manager(&io, &config);
+
+ let guess_version_calls = Rc::new(Cell::new(0u32));
+ let version_guesser = VersionGuesserMock {
+ version_data: VersionData {
+ version: Some("3.0.9999999.9999999-dev".to_string()),
+ commit: Some("aabbccddee".to_string()),
+ pretty_version: Some("3.0-dev".to_string()),
+ feature_version: None,
+ feature_pretty_version: None,
+ },
+ guess_version_calls: guess_version_calls.clone(),
+ };
+
+ let mut loader = RootPackageLoader::new(
+ manager,
+ config.clone(),
+ None,
+ Some(Box::new(version_guesser)),
+ None,
+ );
+
+ let package = loader
+ .load(IndexMap::new(), "Composer\\Package\\RootPackage", None)
+ .unwrap();
+
+ assert!(guess_version_calls.get() >= 1);
+ assert_eq!("3.0-dev", package.as_root().unwrap().get_pretty_version());
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() git command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"]
+#[serial]
fn test_feature_branch_pretty_version() {
- todo!()
+ // proc_open() is always available; the PHP markTestSkipped guard does not apply here.
+ GitUtil::__set_version(Some("2.52.0".to_string()));
+ let _git_guard = GitVersionGuard;
+
+ let io = null_io();
+ let config = make_config();
+ let manager = make_manager(&io, &config);
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* latest-production 38137d2f6c70e775e137b2d8a7a7d3eaebf7c7e5 Commit message\n master 4f6ed96b0bc363d2aa4404c3412de1c011f67c66 Commit message\n",
+ "",
+ ),
+ cmd(["git", "rev-list", "master..latest-production"]),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+ let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None);
+
+ let mut loader =
+ RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None);
+
+ let mut data = IndexMap::new();
+ data.insert(
+ "require".to_string(),
+ require_map(&[("foo/bar", "self.version")]),
+ );
+
+ let package = loader
+ .load(data, "Composer\\Package\\RootPackage", None)
+ .unwrap();
+
+ assert_eq!(
+ "dev-master",
+ package.as_root().unwrap().get_pretty_version()
+ );
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() git command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+#[serial]
fn test_non_feature_branch_pretty_version() {
- todo!()
+ // proc_open() is always available; the PHP markTestSkipped guard does not apply here.
+ GitUtil::__set_version(Some("2.52.0".to_string()));
+ let _git_guard = GitVersionGuard;
+
+ let io = null_io();
+ let config = make_config();
+ let manager = make_manager(&io, &config);
+
+ let expectations: Vec<MockExpectation> = vec![cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* latest-production 38137d2f6c70e775e137b2d8a7a7d3eaebf7c7e5 Commit message\n master 4f6ed96b0bc363d2aa4404c3412de1c011f67c66 Commit message\n",
+ "",
+ )];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+ let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None);
+
+ let mut loader =
+ RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None);
+
+ let mut data = IndexMap::new();
+ data.insert(
+ "require".to_string(),
+ require_map(&[("foo/bar", "self.version")]),
+ );
+ data.insert(
+ "non-feature-branches".to_string(),
+ PhpMixed::List(vec![PhpMixed::String("latest-.*".to_string())]),
+ );
+
+ let package = loader
+ .load(data, "Composer\\Package\\RootPackage", None)
+ .unwrap();
+
+ assert_eq!(
+ "dev-latest-production",
+ package.as_root().unwrap().get_pretty_version()
+ );
}
diff --git a/crates/shirabe/tests/package/version/version_selector_test.rs b/crates/shirabe/tests/package/version/version_selector_test.rs
index fa288cd..f48a6ce 100644
--- a/crates/shirabe/tests/package/version/version_selector_test.rs
+++ b/crates/shirabe/tests/package/version/version_selector_test.rs
@@ -1,77 +1,546 @@
//! ref: composer/tests/Composer/Test/Package/Version/VersionSelectorTest.php
-// VersionSelector ranks candidate packages whose versions/constraints are parsed through a
-// look-around regex the regex crate cannot compile; the setup also mocks a repository.
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use indexmap::IndexMap;
+use shirabe::filter::platform_requirement_filter::PlatformRequirementFilterFactory;
+use shirabe::io::BufferIO;
+use shirabe::io::IOInterface;
+use shirabe::package::BasePackageHandle;
+use shirabe::package::CompleteAliasPackageHandle;
+use shirabe::package::CompletePackageHandle;
+use shirabe::package::Link;
+use shirabe::package::PackageInterfaceHandle;
+use shirabe::package::version::VersionSelector;
+use shirabe::package::version::version_parser::VersionParser;
+use shirabe::repository::PlatformRepository;
+use shirabe::repository::RepositorySetInterface;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::constraint::AnyConstraint;
+
+use shirabe_external_packages::symfony::console::output::output_interface;
+
+use crate::test_case::get_package;
+
+mockall::mock! {
+ RepositorySet {}
+ impl RepositorySetInterface for RepositorySet {
+ fn find_packages(
+ &self,
+ name: &str,
+ constraint: Option<AnyConstraint>,
+ flags: i64,
+ ) -> anyhow::Result<Vec<BasePackageHandle>>;
+ }
+}
+
+// `RepositorySetInterface` requires `Debug`; mockall does not generate it.
+impl std::fmt::Debug for MockRepositorySet {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("MockRepositorySet")
+ }
+}
+
+fn into_seam(mock: MockRepositorySet) -> Rc<RefCell<dyn RepositorySetInterface>> {
+ Rc::new(RefCell::new(mock))
+}
+
+/// Mirrors PHPUnit `assertSame($expected, $best)`: object identity, not value equality.
+fn assert_same(
+ best: &Option<PackageInterfaceHandle>,
+ expected: &PackageInterfaceHandle,
+ msg: &str,
+) {
+ let best = best
+ .as_ref()
+ .unwrap_or_else(|| panic!("{msg}: expected Some(_), got None"));
+ assert!(best.ptr_eq(expected), "{msg}");
+}
+
+fn require_link(package_name: &str, target: &str, pretty_constraint: &str) -> Link {
+ let parser = VersionParser::new();
+ Link::new(
+ package_name.to_string(),
+ target.to_string(),
+ parser.parse_constraints(pretty_constraint).unwrap(),
+ Some(Link::TYPE_REQUIRE.to_string()),
+ pretty_constraint.to_string(),
+ )
+}
+
+fn find_best(
+ version_selector: &mut VersionSelector,
+ package_name: &str,
+ preferred_stability: &str,
+ platform_requirement_filter: Option<
+ std::rc::Rc<
+ dyn shirabe::filter::platform_requirement_filter::PlatformRequirementFilterInterface,
+ >,
+ >,
+ io: Option<Rc<RefCell<dyn IOInterface>>>,
+) -> Option<PackageInterfaceHandle> {
+ version_selector
+ .find_best_candidate(
+ package_name,
+ None,
+ preferred_stability,
+ platform_requirement_filter,
+ 0,
+ io,
+ PhpMixed::Bool(true),
+ )
+ .unwrap()
+}
+
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_latest_version_is_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package1 = get_package("foo/bar", "1.2.1");
+ let package2 = get_package("foo/bar", "1.2.2");
+ let package3 = get_package("foo/bar", "1.2.0");
+ let packages = vec![package1.clone(), package2.clone(), package3.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+
+ // 1.2.2 should be returned because it's the latest of the returned versions
+ assert_same(&best, &package2, "Latest version should be 1.2.2");
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
+#[ignore = "PlatformRepository initialization calls shirabe_php_shim::runtime::constant() which is still todo!(); unrelated to the RepositorySet seam"]
fn test_latest_version_is_returned_that_matches_php_requirements() {
- todo!()
+ let package_name = "foo/bar";
+
+ let mut overrides: IndexMap<String, PhpMixed> = IndexMap::new();
+ overrides.insert("php".to_string(), PhpMixed::String("5.5.0".to_string()));
+ let mut platform = PlatformRepository::new(vec![], overrides).unwrap();
+
+ let package0 = get_package("foo/bar", "0.9.0");
+ package0.__set_requires(IndexMap::from([(
+ "php".to_string(),
+ require_link(package_name, "php", ">=5.6"),
+ )]));
+ let package1 = get_package("foo/bar", "1.0.0");
+ package1.__set_requires(IndexMap::from([(
+ "php".to_string(),
+ require_link(package_name, "php", ">=5.4"),
+ )]));
+ let package2 = get_package("foo/bar", "2.0.0");
+ package2.__set_requires(IndexMap::from([(
+ "php".to_string(),
+ require_link(package_name, "php", ">=5.6"),
+ )]));
+ let package3 = get_package("foo/bar", "2.1.0");
+ package3.__set_requires(IndexMap::from([(
+ "php".to_string(),
+ require_link(package_name, "php", ">=5.6"),
+ )]));
+ let packages = vec![
+ package0.clone(),
+ package1.clone(),
+ package2.clone(),
+ package3.clone(),
+ ];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(3)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector =
+ VersionSelector::new(into_seam(repository_set), Some(&mut platform)).unwrap();
+
+ let io = Rc::new(RefCell::new(
+ BufferIO::new(String::new(), output_interface::VERBOSITY_NORMAL, None).unwrap(),
+ ));
+ let io_dyn: Rc<RefCell<dyn IOInterface>> = io.clone();
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ None,
+ Some(io_dyn),
+ );
+ assert_same(
+ &best,
+ &package1,
+ "Latest version supporting php 5.5 should be returned (1.0.0)",
+ );
+ assert_eq!(
+ "<warning>Cannot use foo/bar's latest version 2.1.0 as it requires php >=5.6 which is not satisfied by your platform.\n",
+ io.borrow().get_output()
+ );
+
+ let io = Rc::new(RefCell::new(
+ BufferIO::new(String::new(), output_interface::VERBOSITY_VERBOSE, None).unwrap(),
+ ));
+ let io_dyn: Rc<RefCell<dyn IOInterface>> = io.clone();
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ None,
+ Some(io_dyn),
+ );
+ assert_same(
+ &best,
+ &package1,
+ "Latest version supporting php 5.5 should be returned (1.0.0)",
+ );
+ assert_eq!(
+ "<warning>Cannot use foo/bar's latest version 2.1.0 as it requires php >=5.6 which is not satisfied by your platform.\n\
+ <warning>Cannot use foo/bar 2.0.0 as it requires php >=5.6 which is not satisfied by your platform.\n",
+ io.borrow().get_output()
+ );
+
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ Some(PlatformRequirementFilterFactory::ignore_all()),
+ None,
+ );
+ assert_same(
+ &best,
+ &package3,
+ "Latest version should be returned when ignoring platform reqs (2.1.0)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
+#[ignore = "PlatformRepository initialization calls shirabe_php_shim::runtime::constant() which is still todo!(); unrelated to the RepositorySet seam"]
fn test_latest_version_is_returned_that_matches_ext_requirements() {
- todo!()
+ let package_name = "foo/bar";
+
+ let mut overrides: IndexMap<String, PhpMixed> = IndexMap::new();
+ overrides.insert("ext-zip".to_string(), PhpMixed::String("5.3.0".to_string()));
+ let mut platform = PlatformRepository::new(vec![], overrides).unwrap();
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ package1.__set_requires(IndexMap::from([(
+ "ext-zip".to_string(),
+ require_link(package_name, "ext-zip", "^5.2"),
+ )]));
+ let package2 = get_package("foo/bar", "2.0.0");
+ package2.__set_requires(IndexMap::from([(
+ "ext-zip".to_string(),
+ require_link(package_name, "ext-zip", "^5.4"),
+ )]));
+ let packages = vec![package1.clone(), package2.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(2)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector =
+ VersionSelector::new(into_seam(repository_set), Some(&mut platform)).unwrap();
+
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+ assert_same(
+ &best,
+ &package1,
+ "Latest version supporting ext-zip 5.3.0 should be returned (1.0.0)",
+ );
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ Some(PlatformRequirementFilterFactory::ignore_all()),
+ None,
+ );
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned when ignoring platform reqs (2.0.0)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
+#[ignore = "PlatformRepository initialization calls shirabe_php_shim::runtime::constant() which is still todo!(); unrelated to the RepositorySet seam"]
fn test_latest_version_is_returned_that_matches_platform_ext() {
- todo!()
+ let package_name = "foo/bar";
+
+ let mut platform = PlatformRepository::new(vec![], IndexMap::new()).unwrap();
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ let package2 = get_package("foo/bar", "2.0.0");
+ package2.__set_requires(IndexMap::from([(
+ "ext-barfoo".to_string(),
+ require_link(package_name, "ext-barfoo", "*"),
+ )]));
+ let packages = vec![package1.clone(), package2.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(2)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector =
+ VersionSelector::new(into_seam(repository_set), Some(&mut platform)).unwrap();
+
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+ assert_same(
+ &best,
+ &package1,
+ "Latest version not requiring ext-barfoo should be returned (1.0.0)",
+ );
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ Some(PlatformRequirementFilterFactory::ignore_all()),
+ None,
+ );
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned when ignoring platform reqs (2.0.0)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
+#[ignore = "PlatformRepository initialization calls shirabe_php_shim::runtime::constant() which is still todo!(); unrelated to the RepositorySet seam"]
fn test_latest_version_is_returned_that_matches_composer_requirements() {
- todo!()
+ let package_name = "foo/bar";
+
+ let mut overrides: IndexMap<String, PhpMixed> = IndexMap::new();
+ overrides.insert(
+ "composer-runtime-api".to_string(),
+ PhpMixed::String("1.0.0".to_string()),
+ );
+ let mut platform = PlatformRepository::new(vec![], overrides).unwrap();
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ package1.__set_requires(IndexMap::from([(
+ "composer-runtime-api".to_string(),
+ require_link(package_name, "composer-runtime-api", "^1.0"),
+ )]));
+ let package2 = get_package("foo/bar", "1.1.0");
+ package2.__set_requires(IndexMap::from([(
+ "composer-runtime-api".to_string(),
+ require_link(package_name, "composer-runtime-api", "^2.0"),
+ )]));
+ let packages = vec![package1.clone(), package2.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(2)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector =
+ VersionSelector::new(into_seam(repository_set), Some(&mut platform)).unwrap();
+
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+ assert_same(
+ &best,
+ &package1,
+ "Latest version supporting composer 1 should be returned (1.0.0)",
+ );
+ let best = find_best(
+ &mut version_selector,
+ package_name,
+ "stable",
+ Some(PlatformRequirementFilterFactory::ignore_all()),
+ None,
+ );
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned when ignoring platform reqs (1.1.0)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_most_stable_version_is_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ let package2 = get_package("foo/bar", "1.1.0-beta");
+ let packages = vec![package1.clone(), package2.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+
+ assert_same(
+ &best,
+ &package1,
+ "Latest most stable version should be returned (1.0.0)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages with willReturnOnConsecutiveCalls; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_most_stable_version_is_returned_regardless_of_order() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package1 = get_package("foo/bar", "2.x-dev");
+ let package2 = get_package("foo/bar", "2.0.0-beta3");
+ let packages = vec![package1.clone(), package2.clone()];
+ let reversed: Vec<PackageInterfaceHandle> = packages.iter().rev().cloned().collect();
+
+ let mut repository_set = MockRepositorySet::new();
+ let mut seq = mockall::Sequence::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .in_sequence(&mut seq)
+ .returning_st(move |_, _, _| Ok(reversed.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+ assert_same(
+ &best,
+ &package2,
+ "Expecting 2.0.0-beta3, cause beta is more stable than dev",
+ );
+
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+ assert_same(
+ &best,
+ &package2,
+ "Expecting 2.0.0-beta3, cause beta is more stable than dev",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_highest_version_is_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ let package2 = get_package("foo/bar", "1.1.0-beta");
+ let packages = vec![package1.clone(), package2.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "dev", None, None);
+
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned (1.1.0-beta)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_highest_version_matching_stability_is_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package1 = get_package("foo/bar", "1.0.0");
+ let package2 = get_package("foo/bar", "1.1.0-beta");
+ let package3 = get_package("foo/bar", "1.2.0-alpha");
+ let packages = vec![package1.clone(), package2.clone(), package3.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "beta", None, None);
+
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned (1.1.0-beta)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_most_stable_unstable_version_is_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package2 = get_package("foo/bar", "1.1.0-beta");
+ let package3 = get_package("foo/bar", "1.2.0-alpha");
+ let packages = vec![package2.clone(), package3.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "stable", None, None);
+
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned (1.1.0-beta)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return fixed package objects; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_default_branch_alias_is_never_returned() {
- todo!()
+ let package_name = "foo/bar";
+
+ let package = get_package("foo/bar", "1.1.0-beta");
+ let package2 = get_package("foo/bar", "dev-main");
+ let package2_complete = CompletePackageHandle::from_rc_unchecked(package2.as_rc().clone());
+ let package2_alias: PackageInterfaceHandle = CompleteAliasPackageHandle::new(
+ package2_complete,
+ VersionParser::DEFAULT_BRANCH_ALIAS.to_string(),
+ VersionParser::DEFAULT_BRANCH_ALIAS.to_string(),
+ )
+ .into();
+ let packages = vec![package.clone(), package2_alias.clone()];
+
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(move |_, _, _| Ok(packages.clone()));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, package_name, "dev", None, None);
+
+ assert_same(
+ &best,
+ &package2,
+ "Latest version should be returned (dev-main)",
+ );
}
#[test]
-#[ignore = "requires mocking RepositorySet::find_packages to return an empty list; RepositorySet is a concrete struct with no injectable/overridable find_packages"]
fn test_false_returned_on_no_packages() {
- todo!()
+ let mut repository_set = MockRepositorySet::new();
+ repository_set
+ .expect_find_packages()
+ .times(1)
+ .returning_st(|_, _, _| Ok(vec![]));
+
+ let mut version_selector = VersionSelector::new(into_seam(repository_set), None).unwrap();
+ let best = find_best(&mut version_selector, "foobaz", "stable", None, None);
+ assert!(best.is_none(), "No versions are available returns false");
}
#[test]
diff --git a/crates/shirabe/tests/platform/hhvm_detector_test.rs b/crates/shirabe/tests/platform/hhvm_detector_test.rs
index 71da314..f1ec656 100644
--- a/crates/shirabe/tests/platform/hhvm_detector_test.rs
+++ b/crates/shirabe/tests/platform/hhvm_detector_test.rs
@@ -1,6 +1,7 @@
//! ref: composer/tests/Composer/Test/Platform/HhvmDetectorTest.php
use shirabe::platform::hhvm_detector::HhvmDetector;
+use shirabe::platform::hhvm_detector::HhvmDetectorInterface;
use shirabe::util::Platform;
use shirabe::util::ProcessExecutor;
use shirabe_external_packages::symfony::process::ExecutableFinder;
diff --git a/crates/shirabe/tests/repository/filesystem_repository_test.rs b/crates/shirabe/tests/repository/filesystem_repository_test.rs
index 1914927..0d0e621 100644
--- a/crates/shirabe/tests/repository/filesystem_repository_test.rs
+++ b/crates/shirabe/tests/repository/filesystem_repository_test.rs
@@ -1,12 +1,23 @@
//! ref: composer/tests/Composer/Test/Repository/FilesystemRepositoryTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
use indexmap::IndexMap;
+use shirabe::dependency_resolver::operation::OperationInterface;
use shirabe::installed_versions::InstalledVersions;
+use shirabe::installer::{InstallationManagerInterface, InstallerInterface};
+use shirabe::io::IOInterface;
use shirabe::json::json_file::JsonFile;
+use shirabe::package::PackageInterfaceHandle;
+use shirabe::repository::InstalledRepositoryInterface;
use shirabe::repository::RepositoryInterface;
use shirabe::repository::filesystem_repository::FilesystemRepository;
+use shirabe::util::filesystem::Filesystem;
use shirabe_php_shim::PhpMixed;
+use crate::test_case::get_package;
+
/// PHP mocks JsonFile::read()/exists(); without a mocking framework the canned read value is
/// materialized as a real temp file whose decoded JSON reproduces the mock return value exactly.
fn create_temp_json_file(contents: &str) -> String {
@@ -78,14 +89,114 @@ fn test_unexistent_repository_file() {
assert_eq!(packages.len(), 0);
}
+/// Stub for `InstallationManagerInterface` whose `getInstallPath` returns a fixed path, mirroring
+/// the PHPUnit mock built with `disableOriginalConstructor()`. Only `get_install_path` is reached by
+/// `FilesystemRepository::write`; the call counter backs PHP's `expects($this->exactly(2))`.
+#[derive(Debug)]
+struct InstallPathStub {
+ calls: Rc<RefCell<i64>>,
+ fixed_path: String,
+}
+
+impl InstallationManagerInterface for InstallPathStub {
+ fn add_installer(&mut self, _installer: Box<dyn InstallerInterface>) {
+ unimplemented!()
+ }
+ fn remove_installer(&mut self, _installer: &dyn InstallerInterface) {
+ unimplemented!()
+ }
+ fn disable_plugins(&mut self) {
+ unimplemented!()
+ }
+ fn is_package_installed(
+ &mut self,
+ _repo: &dyn InstalledRepositoryInterface,
+ _package: PackageInterfaceHandle,
+ ) -> anyhow::Result<bool> {
+ unimplemented!()
+ }
+ fn ensure_binaries_presence(&mut self, _package: PackageInterfaceHandle) {
+ unimplemented!()
+ }
+ fn execute(
+ &mut self,
+ _repo: &mut dyn InstalledRepositoryInterface,
+ _operations: Vec<Rc<dyn OperationInterface>>,
+ _dev_mode: bool,
+ _run_scripts: bool,
+ _download_only: bool,
+ ) -> anyhow::Result<()> {
+ unimplemented!()
+ }
+ fn get_install_path(&mut self, _package: PackageInterfaceHandle) -> Option<String> {
+ *self.calls.borrow_mut() += 1;
+ Some(self.fixed_path.clone())
+ }
+ fn set_output_progress(&mut self, _output_progress: bool) {
+ unimplemented!()
+ }
+ fn notify_installs(&mut self, _io: Rc<RefCell<dyn IOInterface>>) {
+ unimplemented!()
+ }
+}
+
#[test]
-#[ignore = "requires mocking InstallationManager::get_install_path; write() takes a concrete InstallationManager with no trait/seam to stub the canned per-package paths the PHP test relies on"]
fn test_repository_write() {
- todo!()
+ // PHP mocks JsonFile::write/read/getPath; here a real JsonFile under a temp repo dir is written
+ // and read back. write() never reads the file (it dumps the in-memory packages), so the mocked
+ // read()/exists() return values are irrelevant to the result.
+ let base = std::fs::canonicalize(std::env::temp_dir()).unwrap();
+ let repo_dir = format!(
+ "{}/shirabe_repo_write_test_{}_{}",
+ base.display(),
+ std::process::id(),
+ std::time::SystemTime::now()
+ .duration_since(std::time::UNIX_EPOCH)
+ .unwrap()
+ .as_nanos()
+ );
+ let mut fs = Filesystem::new(None);
+ fs.remove_directory(&repo_dir).ok();
+
+ let json_path = format!("{}/vendor/composer/installed.json", repo_dir);
+ let json = JsonFile::new(json_path.clone(), None, None).unwrap();
+ let mut repository = FilesystemRepository::new(json, false, None, None).unwrap();
+
+ let calls = Rc::new(RefCell::new(0i64));
+ let mut im = InstallPathStub {
+ calls: calls.clone(),
+ fixed_path: format!("{}/vendor/woop/woop", repo_dir),
+ };
+
+ repository.set_dev_package_names(vec!["mypkg2".to_string()]);
+ repository
+ .add_package(get_package("mypkg2", "1.2.3"))
+ .unwrap();
+ repository
+ .add_package(get_package("mypkg", "0.1.10"))
+ .unwrap();
+ repository.write(true, &mut im).unwrap();
+
+ // PHP asserts getInstallPath is called exactly twice (once per installed package).
+ assert_eq!(*calls.borrow(), 2);
+
+ let written = std::fs::read_to_string(&json_path).unwrap();
+ let actual: serde_json::Value = serde_json::from_str(&written).unwrap();
+ let expected = serde_json::json!({
+ "packages": [
+ {"name": "mypkg", "type": "library", "version": "0.1.10", "version_normalized": "0.1.10.0", "install-path": "../woop/woop"},
+ {"name": "mypkg2", "type": "library", "version": "1.2.3", "version_normalized": "1.2.3.0", "install-path": "../woop/woop"},
+ ],
+ "dev": true,
+ "dev-package-names": ["mypkg2"],
+ });
+ assert_eq!(actual, expected);
+
+ fs.remove_directory(&repo_dir).ok();
}
#[test]
-#[ignore = "requires mocking InstallationManager::get_install_path (concrete method, no stub seam) plus missing test helpers get_root_package and configure_links"]
+#[ignore = "needs get_root_package + configure_links test helpers (not present in tests/common; project_test_port_link_setters describes them via ArrayLoader::load_packages but this branch lacks them) plus exact byte-match of the generated installed.php fixture under a chdir"]
fn test_repository_writes_installed_php() {
todo!()
}
diff --git a/crates/shirabe/tests/repository/platform_repository_test.rs b/crates/shirabe/tests/repository/platform_repository_test.rs
index c85af4b..7d2a328 100644
--- a/crates/shirabe/tests/repository/platform_repository_test.rs
+++ b/crates/shirabe/tests/repository/platform_repository_test.rs
@@ -1,38 +1,1854 @@
//! ref: composer/tests/Composer/Test/Repository/PlatformRepositoryTest.php
-use shirabe::repository::PlatformRepository;
+use indexmap::IndexMap;
+use mockall::predicate::eq;
+use shirabe::package::{BasePackageHandle, Link};
+use shirabe::platform::{HhvmDetectorInterface, RuntimeInterface};
+use shirabe::repository::{
+ FindPackageConstraint, PlatformRepository, RepositoryInterface, SEARCH_NAME,
+};
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::constraint::SimpleConstraint;
+
+// The Runtime/HhvmDetector seams are concrete structs in PHP; the tests mock them
+// directly.
+mockall::mock! {
+ pub Runtime {}
+ impl RuntimeInterface for Runtime {
+ fn has_constant(&self, constant_name: &str, class: Option<String>) -> bool;
+ fn get_constant(&self, constant_name: &str, class: Option<String>) -> PhpMixed;
+ fn invoke(&self, callable: PhpMixed, arguments: Vec<PhpMixed>) -> PhpMixed;
+ fn has_class(&self, class: &str) -> bool;
+ fn construct(&self, class: &str, arguments: Vec<PhpMixed>) -> anyhow::Result<PhpMixed>;
+ fn get_extensions(&self) -> Vec<String>;
+ fn get_extension_version(&self, extension: &str) -> String;
+ fn get_extension_info(&self, extension: &str) -> anyhow::Result<String>;
+ }
+}
+
+mockall::mock! {
+ pub HhvmDetector {}
+ impl HhvmDetectorInterface for HhvmDetector {
+ fn reset(&self);
+ fn get_version(&mut self) -> Option<String>;
+ }
+}
+
+// The seam traits require `Debug` (so `PlatformRepository` can derive it); mockall does
+// not generate it for mocks.
+impl std::fmt::Debug for MockRuntime {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("MockRuntime")
+ }
+}
+
+impl std::fmt::Debug for MockHhvmDetector {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("MockHhvmDetector")
+ }
+}
+
+/// PHP: ltrim($class.'::'.$constant, ':')
+fn constant_key(constant_name: &str, class: Option<&str>) -> String {
+ format!("{}::{}", class.unwrap_or(""), constant_name)
+ .trim_start_matches(':')
+ .to_string()
+}
+
+// All five tests below are fully ported but kept `#[ignore]`: every code path through
+// `PlatformRepository::initialize()` reaches `XdebugHandler::get_skipped_version()`,
+// which is an unrelated `todo!()` stub in `shirabe-external-packages`, so the tests
+// panic before any assertion (Phase D: visible failure is acceptable, and the category
+// brief permits leaving confidently-panicking tests ignored). The seam/mocks are in
+// place so they will execute once that stub lands.
-// These probe runtime/extension/library versions and assert the synthesized platform
-// packages; the detection mocks ProcessExecutor/Runtime/HhvmDetector and the package
-// versions are parsed through a look-around regex.
#[test]
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Platform\\HhvmDetector (willReturn getVersion); Runtime/HhvmDetector are concrete structs with no mock/override mechanism"]
+#[ignore = "seam + HhvmDetector mock ported; execution panics at the unrelated \
+ XdebugHandler::get_skipped_version() todo!() during initialize(), and the \
+ default (real) Runtime also relies on PHP-runtime shim todo!()s"]
fn test_hhvm_package() {
- todo!()
+ let mut hhvm_detector = MockHhvmDetector::new();
+ hhvm_detector
+ .expect_get_version()
+ .times(..)
+ .returning(|| Some("2.1.0".to_string()));
+
+ let mut platform_repository =
+ PlatformRepository::new4(vec![], IndexMap::new(), None, Some(Box::new(hhvm_detector)))
+ .unwrap();
+
+ let hhvm = platform_repository
+ .find_package("hhvm", FindPackageConstraint::String("*".to_string()))
+ .unwrap();
+ assert!(hhvm.is_some(), "hhvm found");
+
+ assert_eq!("2.1.0", hhvm.unwrap().get_pretty_version());
+}
+
+fn php_flavor_test_cases() -> Vec<(
+ IndexMap<String, PhpMixed>,
+ Vec<(&'static str, &'static str)>,
+ Vec<(PhpMixed, Vec<PhpMixed>, PhpMixed)>,
+)> {
+ let ubuntu = "7.2.31-1+ubuntu16.04.1+deb.sury.org+1";
+ let s = |v: &str| PhpMixed::String(v.to_string());
+
+ vec![
+ (
+ IndexMap::from([("PHP_VERSION".to_string(), s("7.1.33"))]),
+ vec![("php", "7.1.33")],
+ vec![],
+ ),
+ (
+ IndexMap::from([
+ ("PHP_VERSION".to_string(), s(ubuntu)),
+ ("PHP_DEBUG".to_string(), PhpMixed::Bool(true)),
+ ]),
+ vec![("php", "7.2.31"), ("php-debug", "7.2.31")],
+ vec![],
+ ),
+ (
+ IndexMap::from([
+ ("PHP_VERSION".to_string(), s(ubuntu)),
+ ("PHP_ZTS".to_string(), PhpMixed::Bool(true)),
+ ]),
+ vec![("php", "7.2.31"), ("php-zts", "7.2.31")],
+ vec![],
+ ),
+ (
+ IndexMap::from([
+ ("PHP_VERSION".to_string(), s(ubuntu)),
+ ("PHP_INT_SIZE".to_string(), PhpMixed::Int(8)),
+ ]),
+ vec![("php", "7.2.31"), ("php-64bit", "7.2.31")],
+ vec![],
+ ),
+ (
+ IndexMap::from([
+ ("PHP_VERSION".to_string(), s(ubuntu)),
+ ("AF_INET6".to_string(), PhpMixed::Int(30)),
+ ]),
+ vec![("php", "7.2.31"), ("php-ipv6", "7.2.31")],
+ vec![],
+ ),
+ (
+ IndexMap::from([("PHP_VERSION".to_string(), s(ubuntu))]),
+ vec![("php", "7.2.31"), ("php-ipv6", "7.2.31")],
+ vec![(s("inet_pton"), vec![s("::")], s(""))],
+ ),
+ (
+ IndexMap::from([("PHP_VERSION".to_string(), s(ubuntu))]),
+ vec![("php", "7.2.31")],
+ vec![(s("inet_pton"), vec![s("::")], PhpMixed::Bool(false))],
+ ),
+ ]
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Platform\\Runtime (willReturnCallback/willReturnMap for hasConstant/getConstant/invoke/getExtensions); no mocking framework and Runtime is a concrete struct"]
+#[ignore = "seam + Runtime mock ported (hasConstant/getConstant/invoke/getExtensions); \
+ execution panics at the unrelated XdebugHandler::get_skipped_version() todo!()"]
fn test_php_version() {
- todo!()
+ for (constants, packages, functions) in php_flavor_test_cases() {
+ let constants_has = constants.clone();
+ let constants_get = constants.clone();
+
+ let mut runtime = MockRuntime::new();
+ runtime
+ .expect_get_extensions()
+ .times(..)
+ .returning(Vec::new);
+ runtime
+ .expect_has_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants_has.contains_key(&constant_key(constant, class.as_deref()))
+ });
+ runtime
+ .expect_get_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants_get
+ .get(&constant_key(constant, class.as_deref()))
+ .cloned()
+ .unwrap_or(PhpMixed::Null)
+ });
+ runtime
+ .expect_invoke()
+ .times(..)
+ .returning(move |callable, arguments| {
+ for (c, a, ret) in &functions {
+ if *c == callable && *a == arguments {
+ return ret.clone();
+ }
+ }
+ PhpMixed::Null
+ });
+
+ let mut repository =
+ PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None)
+ .unwrap();
+
+ for (package_name, version) in packages {
+ let package = repository
+ .find_package(package_name, FindPackageConstraint::String("*".to_string()))
+ .unwrap();
+ assert!(
+ package.is_some(),
+ "Expected to find package \"{}\"",
+ package_name
+ );
+ assert_eq!(
+ version,
+ package.unwrap().get_pretty_version(),
+ "Expected package \"{}\" version to be {}",
+ package_name,
+ version
+ );
+ }
+ }
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Platform\\Runtime (expects(once)/with/willReturn for invoke, willReturnCallback for getConstant); no mocking framework"]
+#[ignore = "seam + Runtime mock ported (invoke once + getConstant callback); \
+ execution panics at the unrelated XdebugHandler::get_skipped_version() todo!()"]
fn test_inet_pton_regression() {
- todo!()
+ let mut runtime = MockRuntime::new();
+ // PHP: ->expects(self::once())->method('invoke')->with('inet_pton', ['::'])->willReturn(false).
+ runtime
+ .expect_invoke()
+ .with(
+ eq(PhpMixed::String("inet_pton".to_string())),
+ eq(vec![PhpMixed::String("::".to_string())]),
+ )
+ .times(1)
+ .returning(|_callable, _arguments| PhpMixed::Bool(false));
+ // suppressing PHP_ZTS & AF_INET6
+ runtime
+ .expect_has_constant()
+ .times(..)
+ .returning(|_, _| false);
+
+ let constants: IndexMap<String, PhpMixed> = IndexMap::from([
+ (
+ "PHP_VERSION".to_string(),
+ PhpMixed::String("7.0.0".to_string()),
+ ),
+ ("PHP_DEBUG".to_string(), PhpMixed::Bool(false)),
+ ]);
+ runtime
+ .expect_get_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants
+ .get(&constant_key(constant, class.as_deref()))
+ .cloned()
+ .unwrap_or(PhpMixed::Null)
+ });
+ runtime
+ .expect_get_extensions()
+ .times(..)
+ .returning(Vec::new);
+
+ let mut repository =
+ PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None).unwrap();
+ let package = repository
+ .find_package("php-ipv6", FindPackageConstraint::String("*".to_string()))
+ .unwrap();
+ assert!(package.is_none());
+}
+
+enum Exp {
+ Version(&'static str),
+ Missing,
+ Full(
+ &'static str,
+ &'static [&'static str],
+ &'static [&'static str],
+ ),
+}
+
+impl Exp {
+ fn parts(
+ &self,
+ ) -> (
+ Option<&'static str>,
+ &'static [&'static str],
+ &'static [&'static str],
+ ) {
+ match self {
+ Exp::Version(v) => (Some(*v), &[], &[]),
+ Exp::Missing => (None, &[], &[]),
+ Exp::Full(v, replaces, provides) => (Some(*v), *replaces, *provides),
+ }
+ }
+}
+
+#[derive(Clone)]
+struct ClassDef {
+ class: &'static str,
+ construct: Option<(Vec<PhpMixed>, PhpMixed)>,
+}
+
+struct LibCase {
+ extensions: Vec<&'static str>,
+ info: Option<&'static str>,
+ expectations: Vec<(&'static str, Exp)>,
+ functions: Vec<(PhpMixed, Vec<PhpMixed>, PhpMixed)>,
+ constants: Vec<(&'static str, Option<&'static str>, PhpMixed)>,
+ class_definitions: Vec<ClassDef>,
+}
+
+// Port of PlatformRepositoryTest::provideLibraryTestCases (all 59 datasets). The intl and
+// imagick cases reference the PHP ResourceBundleStub/ImagickStub objects (modelled as
+// PhpMixed::Object holding the version they would expose); their runtime read paths are
+// still `TODO(plugin)` stubs, but the data is ported faithfully.
+fn library_test_cases() -> Vec<LibCase> {
+ let s = |v: &str| PhpMixed::String(v.to_string());
+ let curl = |v: &str| -> (PhpMixed, Vec<PhpMixed>, PhpMixed) {
+ (
+ s("curl_version"),
+ vec![],
+ PhpMixed::Array(IndexMap::from([("version".to_string(), s(v))])),
+ )
+ };
+
+ vec![
+ LibCase {
+ extensions: vec!["amqp"],
+ info: Some(
+ "
+
+amqp
+
+Version => 1.9.4
+Revision => release
+Compiled => Nov 19 2019 @ 08:44:26
+AMQP protocol version => 0-9-1
+librabbitmq version => 0.9.0
+Default max channels per connection => 256
+Default max frame size => 131072
+Default heartbeats interval => 0",
+ ),
+ expectations: vec![
+ ("lib-amqp-protocol", Exp::Version("0.9.1")),
+ ("lib-amqp-librabbitmq", Exp::Version("0.9.0")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["bz2"],
+ info: Some(
+ "
+bz2
+
+BZip2 Support => Enabled
+Stream Wrapper support => compress.bzip2://
+Stream Filter support => bzip2.decompress, bzip2.compress
+BZip2 Version => 1.0.5, 6-Sept-2010",
+ ),
+ expectations: vec![("lib-bz2", Exp::Version("1.0.5"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 7.38.0
+Age => 3
+Features
+AsynchDNS => Yes
+CharConv => No
+Debug => No
+GSS-Negotiate => No
+IDN => Yes
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => Yes
+SSL => Yes
+SSPI => No
+TLS-SRP => Yes
+HTTP2 => No
+GSSAPI => Yes
+Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smtp, smtps, telnet, tftp
+Host => x86_64-pc-linux-gnu
+SSL Version => OpenSSL/1.0.1t
+ZLib Version => 1.2.8
+libSSH Version => libssh2/1.4.3
+
+Directive => Local Value => Master Value
+curl.cainfo => no value => no value",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("2.0.0")),
+ ("lib-curl-openssl", Exp::Version("1.0.1.20")),
+ ("lib-curl-zlib", Exp::Version("1.2.8")),
+ ("lib-curl-libssh2", Exp::Version("1.4.3")),
+ ],
+ functions: vec![curl("2.0.0")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 7.38.0
+Age => 3
+Features
+AsynchDNS => Yes
+CharConv => No
+Debug => No
+GSS-Negotiate => No
+IDN => Yes
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => Yes
+SSL => Yes
+SSPI => No
+TLS-SRP => Yes
+HTTP2 => No
+GSSAPI => Yes
+Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smtp, smtps, telnet, tftp
+Host => x86_64-pc-linux-gnu
+SSL Version => OpenSSL/1.0.1t-fips
+ZLib Version => 1.2.8
+libSSH Version => libssh2/1.4.3
+
+Directive => Local Value => Master Value
+curl.cainfo => no value => no value",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("2.0.0")),
+ (
+ "lib-curl-openssl-fips",
+ Exp::Full("1.0.1.20", &[], &["lib-curl-openssl"]),
+ ),
+ ("lib-curl-zlib", Exp::Version("1.2.8")),
+ ("lib-curl-libssh2", Exp::Version("1.4.3")),
+ ],
+ functions: vec![curl("2.0.0")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 7.22.0
+Age => 3
+Features
+AsynchDNS => No
+CharConv => No
+Debug => No
+GSS-Negotiate => Yes
+IDN => Yes
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => No
+SSL => Yes
+SSPI => No
+TLS-SRP => Yes
+Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, pop3, pop3s, rtmp, rtsp, smtp, smtps, telnet, tftp
+Host => x86_64-pc-linux-gnu
+SSL Version => GnuTLS/2.12.14
+ZLib Version => 1.2.3.4",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("7.22.0")),
+ ("lib-curl-zlib", Exp::Version("1.2.3.4")),
+ (
+ "lib-curl-gnutls",
+ Exp::Full("2.12.14", &["lib-curl-openssl"], &[]),
+ ),
+ ],
+ functions: vec![curl("7.22.0")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 7.24.0
+Age => 3
+Features
+AsynchDNS => Yes
+Debug => No
+GSS-Negotiate => Yes
+IDN => Yes
+IPv6 => Yes
+Largefile => Yes
+NTLM => Yes
+SPNEGO => No
+SSL => Yes
+SSPI => No
+krb4 => No
+libz => Yes
+CharConv => No
+Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtsp, scp, sftp, smtp, smtps, telnet, tftp
+Host => x86_64-redhat-linux-gnu
+SSL Version => NSS/3.13.3.0
+ZLib Version => 1.2.5
+libSSH Version => libssh2/1.4.1",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("7.24.0")),
+ (
+ "lib-curl-nss",
+ Exp::Full("3.13.3.0", &["lib-curl-openssl"], &[]),
+ ),
+ ("lib-curl-zlib", Exp::Version("1.2.5")),
+ ("lib-curl-libssh2", Exp::Version("1.4.1")),
+ ],
+ functions: vec![curl("7.24.0")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 7.68.0
+Age => 5
+Features
+AsynchDNS => Yes
+CharConv => No
+Debug => No
+GSS-Negotiate => No
+IDN => Yes
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => Yes
+SSL => Yes
+SSPI => No
+TLS-SRP => Yes
+HTTP2 => Yes
+GSSAPI => Yes
+KERBEROS5 => Yes
+UNIX_SOCKETS => Yes
+PSL => Yes
+HTTPS_PROXY => Yes
+MULTI_SSL => No
+BROTLI => Yes
+Protocols => dict, file, ftp, ftps, gopher, http, https, imap, imaps, ldap, ldaps, pop3, pop3s, rtmp, rtsp, scp, sftp, smb, smbs, smtp, smtps, telnet, tftp
+Host => x86_64-pc-linux-gnu
+SSL Version => OpenSSL/1.1.1g
+ZLib Version => 1.2.11
+libSSH Version => libssh/0.9.3/openssl/zlib",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("7.68.0")),
+ ("lib-curl-openssl", Exp::Version("1.1.1.7")),
+ ("lib-curl-zlib", Exp::Version("1.2.11")),
+ ("lib-curl-libssh", Exp::Version("0.9.3")),
+ ],
+ functions: vec![curl("7.68.0")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 8.1.2
+Age => 10
+Features
+AsynchDNS => Yes
+CharConv => No
+Debug => No
+GSS-Negotiate => No
+IDN => Yes
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => Yes
+SSL => Yes
+SSPI => No
+TLS-SRP => Yes
+HTTP2 => Yes
+GSSAPI => Yes
+KERBEROS5 => Yes
+UNIX_SOCKETS => Yes
+PSL => No
+HTTPS_PROXY => Yes
+MULTI_SSL => Yes
+BROTLI => Yes
+ALTSVC => Yes
+HTTP3 => No
+UNICODE => No
+ZSTD => Yes
+HSTS => Yes
+GSASL => No
+Protocols => dict, file, ftp, ftps, gopher, gophers, http, https, imap, imaps, ldap, ldaps, mqtt, pop3, pop3s, rtmp, rtmpe, rtmps, rtmpt, rtmpte, rtmpts, rtsp, scp, sftp, smb, smbs, smtp, smtps, telnet, tftp
+Host => aarch64-apple-darwin22.4.0
+SSL Version => (SecureTransport) OpenSSL/3.1.1
+ZLib Version => 1.2.11
+libSSH Version => libssh2/1.11.0",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("8.1.2")),
+ (
+ "lib-curl-securetransport",
+ Exp::Full("3.1.1", &["lib-curl-openssl"], &[]),
+ ),
+ ("lib-curl-zlib", Exp::Version("1.2.11")),
+ ("lib-curl-libssh2", Exp::Version("1.11.0")),
+ ],
+ functions: vec![curl("8.1.2")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["curl"],
+ info: Some(
+ "
+curl
+
+cURL support => enabled
+cURL Information => 8.1.2
+Age => 10
+Features
+AsynchDNS => Yes
+CharConv => No
+Debug => No
+GSS-Negotiate => No
+IDN => No
+IPv6 => Yes
+krb4 => No
+Largefile => Yes
+libz => Yes
+NTLM => Yes
+NTLMWB => Yes
+SPNEGO => Yes
+SSL => Yes
+SSPI => No
+TLS-SRP => No
+HTTP2 => Yes
+GSSAPI => Yes
+KERBEROS5 => Yes
+UNIX_SOCKETS => Yes
+PSL => No
+HTTPS_PROXY => Yes
+MULTI_SSL => Yes
+BROTLI => No
+ALTSVC => Yes
+HTTP3 => No
+UNICODE => No
+ZSTD => No
+HSTS => Yes
+GSASL => No
+Protocols => dict, file, ftp, ftps, gopher, gophers, http, https, imap, imaps, ldap, ldaps, mqtt, pop3, pop3s, rtsp, smb, smbs, smtp, smtps, telnet, tftp
+Host => x86_64-apple-darwin20.0
+SSL Version => (SecureTransport) LibreSSL/2.8.3
+ZLib Version => 1.2.11",
+ ),
+ expectations: vec![
+ ("lib-curl", Exp::Version("8.1.2")),
+ (
+ "lib-curl-securetransport",
+ Exp::Full("2.8.3", &["lib-curl-libressl"], &[]),
+ ),
+ ("lib-curl-zlib", Exp::Version("1.2.11")),
+ ],
+ functions: vec![curl("8.1.2")],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["date"],
+ info: Some(
+ "
+date
+
+date/time support => enabled
+timelib version => 2018.03
+\"Olson\" Timezone Database Version => 2020.1
+Timezone Database => external
+Default timezone => Europe/Berlin",
+ ),
+ expectations: vec![
+ ("lib-date-timelib", Exp::Version("2018.03")),
+ ("lib-date-zoneinfo", Exp::Version("2020.1")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["date"],
+ info: Some(
+ "
+date
+
+date/time support => enabled
+\"Olson\" Timezone Database Version => 2013.2
+Timezone Database => internal
+Default timezone => Europe/Amsterdam",
+ ),
+ expectations: vec![
+ ("lib-date-zoneinfo", Exp::Version("2013.2")),
+ ("lib-date-timelib", Exp::Missing),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["date", "timezonedb"],
+ info: Some(
+ "
+date
+
+date/time support => enabled
+\"Olson\" Timezone Database Version => 2020.1
+Timezone Database => internal
+Default timezone => UTC",
+ ),
+ expectations: vec![("lib-date-zoneinfo", Exp::Version("2020.1"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["date", "timezonedb"],
+ info: Some(
+ "
+date
+
+date/time support => enabled
+\"Olson\" Timezone Database Version => 2020.1
+Timezone Database => external
+Default timezone => UTC",
+ ),
+ expectations: vec![(
+ "lib-timezonedb-zoneinfo",
+ Exp::Full("2020.1", &["lib-date-zoneinfo"], &[]),
+ )],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["date"],
+ info: Some(
+ "
+
+
+date/time support => enabled
+timelib version => 2018.03
+\"Olson\" Timezone Database Version => 0.system
+Timezone Database => internal
+Default timezone => Europe/Berlin
+
+Directive => Local Value => Master Value
+date.timezone => no value => no value
+date.default_latitude => 31.7667 => 31.7667
+date.default_longitude => 35.2333 => 35.2333
+date.sunset_zenith => 90.583333 => 90.583333
+date.sunrise_zenith => 90.583333 => 90.583333",
+ ),
+ expectations: vec![
+ ("lib-date-zoneinfo", Exp::Version("0")),
+ ("lib-date-timelib", Exp::Version("2018.03")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["fileinfo"],
+ info: Some(
+ "
+fileinfo
+
+fileinfo support => enabled
+libmagic => 537",
+ ),
+ expectations: vec![("lib-fileinfo-libmagic", Exp::Version("537"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["gd"],
+ info: Some(
+ "
+gd
+
+GD Support => enabled
+GD Version => bundled (2.1.0 compatible)
+FreeType Support => enabled
+FreeType Linkage => with freetype
+FreeType Version => 2.10.0
+GIF Read Support => enabled
+GIF Create Support => enabled
+JPEG Support => enabled
+libJPEG Version => 9 compatible
+PNG Support => enabled
+libPNG Version => 1.6.34
+WBMP Support => enabled
+XBM Support => enabled
+WebP Support => enabled
+
+Directive => Local Value => Master Value
+gd.jpeg_ignore_warning => 1 => 1",
+ ),
+ expectations: vec![
+ ("lib-gd", Exp::Version("1.2.3")),
+ ("lib-gd-freetype", Exp::Version("2.10.0")),
+ ("lib-gd-libjpeg", Exp::Version("9.0")),
+ ("lib-gd-libpng", Exp::Version("1.6.34")),
+ ],
+ functions: vec![],
+ constants: vec![("GD_VERSION", None, s("1.2.3"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["gd"],
+ info: Some(
+ "
+gd
+
+GD Support => enabled
+GD Version => bundled (2.1.0 compatible)
+FreeType Support => enabled
+FreeType Linkage => with freetype
+FreeType Version => 2.9.1
+GIF Read Support => enabled
+GIF Create Support => enabled
+JPEG Support => enabled
+libJPEG Version => 6b
+PNG Support => enabled
+libPNG Version => 1.6.35
+WBMP Support => enabled
+XBM Support => enabled
+WebP Support => enabled
+
+Directive => Local Value => Master Value
+gd.jpeg_ignore_warning => 1 => 1",
+ ),
+ expectations: vec![
+ ("lib-gd", Exp::Version("1.2.3")),
+ ("lib-gd-freetype", Exp::Version("2.9.1")),
+ ("lib-gd-libjpeg", Exp::Version("6.2")),
+ ("lib-gd-libpng", Exp::Version("1.6.35")),
+ ],
+ functions: vec![],
+ constants: vec![("GD_VERSION", None, s("1.2.3"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["gd"],
+ info: Some(
+ "
+gd
+
+GD Support => enabled
+GD headers Version => 2.2.5
+GD library Version => 2.2.5
+FreeType Support => enabled
+FreeType Linkage => with freetype
+FreeType Version => 2.6.3
+GIF Read Support => enabled
+GIF Create Support => enabled
+JPEG Support => enabled
+libJPEG Version => 6b
+PNG Support => enabled
+libPNG Version => 1.6.28
+WBMP Support => enabled
+XPM Support => enabled
+libXpm Version => 30411
+XBM Support => enabled
+WebP Support => enabled
+
+Directive => Local Value => Master Value
+gd.jpeg_ignore_warning => 1 => 1",
+ ),
+ expectations: vec![
+ ("lib-gd", Exp::Version("2.2.5")),
+ ("lib-gd-freetype", Exp::Version("2.6.3")),
+ ("lib-gd-libjpeg", Exp::Version("6.2")),
+ ("lib-gd-libpng", Exp::Version("1.6.28")),
+ ("lib-gd-libxpm", Exp::Version("3.4.11")),
+ ],
+ functions: vec![],
+ constants: vec![("GD_VERSION", None, s("2.2.5"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["iconv"],
+ info: None,
+ expectations: vec![("lib-iconv", Exp::Version("1.2.4"))],
+ functions: vec![],
+ constants: vec![("ICONV_VERSION", None, s("1.2.4"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["gmp"],
+ info: None,
+ expectations: vec![("lib-gmp", Exp::Version("6.1.0"))],
+ functions: vec![],
+ constants: vec![("GMP_VERSION", None, s("6.1.0"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["intl"],
+ info: Some(
+ "
+intl
+
+Internationalization support => enabled
+ICU version => 57.1
+ICU Data version => 57.1
+ICU TZData version => 2016b
+ICU Unicode version => 8.0
+
+Directive => Local Value => Master Value
+intl.default_locale => no value => no value
+intl.error_level => 0 => 0
+intl.use_exceptions => 0 => 0",
+ ),
+ expectations: vec![
+ ("lib-icu", Exp::Version("100")),
+ ("lib-icu-cldr", Exp::Version("32.0.1")),
+ ("lib-icu-unicode", Exp::Version("7.0.0")),
+ ("lib-icu-zoneinfo", Exp::Version("2016.2")),
+ ],
+ functions: vec![
+ (
+ PhpMixed::List(vec![s("ResourceBundle"), s("create")]),
+ vec![s("root"), s("ICUDATA"), PhpMixed::Bool(false)],
+ PhpMixed::Object(IndexMap::from([("Version".to_string(), s("32.0.1"))])),
+ ),
+ (
+ PhpMixed::List(vec![s("IntlChar"), s("getUnicodeVersion")]),
+ vec![],
+ PhpMixed::List(vec![
+ PhpMixed::Int(7),
+ PhpMixed::Int(0),
+ PhpMixed::Int(0),
+ PhpMixed::Int(0),
+ ]),
+ ),
+ ],
+ constants: vec![("INTL_ICU_VERSION", None, s("100"))],
+ class_definitions: vec![
+ ClassDef {
+ class: "ResourceBundle",
+ construct: None,
+ },
+ ClassDef {
+ class: "IntlChar",
+ construct: None,
+ },
+ ],
+ },
+ LibCase {
+ extensions: vec!["intl"],
+ info: Some(
+ "
+intl
+
+Internationalization support => enabled
+version => 1.1.0
+ICU version => 57.1
+ICU Data version => 57.1",
+ ),
+ expectations: vec![("lib-icu", Exp::Version("57.1"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["imagick"],
+ info: None,
+ expectations: vec![(
+ "lib-imagick-imagemagick",
+ Exp::Full("6.2.9", &["lib-imagick"], &[]),
+ )],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![ClassDef {
+ class: "Imagick",
+ construct: Some((
+ vec![],
+ PhpMixed::Object(IndexMap::from([(
+ "versionString".to_string(),
+ s("ImageMagick 6.2.9 Q16 x86_64 2018-05-18 http://www.imagemagick.org"),
+ )])),
+ )),
+ }],
+ },
+ LibCase {
+ extensions: vec!["imagick"],
+ info: None,
+ expectations: vec![(
+ "lib-imagick-imagemagick",
+ Exp::Full("7.0.8.34", &["lib-imagick"], &[]),
+ )],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![ClassDef {
+ class: "Imagick",
+ construct: Some((
+ vec![],
+ PhpMixed::Object(IndexMap::from([(
+ "versionString".to_string(),
+ s("ImageMagick 7.0.8-34 Q16 x86_64 2019-03-23 https://imagemagick.org"),
+ )])),
+ )),
+ }],
+ },
+ LibCase {
+ extensions: vec!["ldap"],
+ info: Some(
+ "
+ldap
+
+LDAP Support => enabled
+RCS Version => $Id: 5f1913de8e05a346da913956f81e0c0d8991c7cb $
+Total Links => 0/unlimited
+API Version => 3001
+Vendor Name => OpenLDAP
+Vendor Version => 20450
+SASL Support => Enabled
+
+Directive => Local Value => Master Value
+ldap.max_links => Unlimited => Unlimited",
+ ),
+ expectations: vec![("lib-ldap-openldap", Exp::Version("2.4.50"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["libxml"],
+ info: None,
+ expectations: vec![("lib-libxml", Exp::Version("2.1.5"))],
+ functions: vec![],
+ constants: vec![("LIBXML_DOTTED_VERSION", None, s("2.1.5"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["libxml", "dom", "simplexml", "xml", "xmlreader", "xmlwriter"],
+ info: None,
+ expectations: vec![(
+ "lib-libxml",
+ Exp::Full(
+ "2.1.5",
+ &[],
+ &[
+ "lib-dom-libxml",
+ "lib-simplexml-libxml",
+ "lib-xml-libxml",
+ "lib-xmlreader-libxml",
+ "lib-xmlwriter-libxml",
+ ],
+ ),
+ )],
+ functions: vec![],
+ constants: vec![("LIBXML_DOTTED_VERSION", None, s("2.1.5"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["mbstring"],
+ info: Some(
+ "
+mbstring
+
+Multibyte Support => enabled
+Multibyte string engine => libmbfl
+HTTP input encoding translation => disabled
+libmbfl version => 1.3.2
+
+mbstring extension makes use of \"streamable kanji code filter and converter\", which is distributed under the GNU Lesser General Public License version 2.1.
+
+Multibyte (japanese) regex support => enabled
+Multibyte regex (oniguruma) version => 6.1.3",
+ ),
+ expectations: vec![
+ ("lib-mbstring-libmbfl", Exp::Version("1.3.2")),
+ ("lib-mbstring-oniguruma", Exp::Version("7.0.0")),
+ ],
+ functions: vec![],
+ constants: vec![("MB_ONIGURUMA_VERSION", None, s("7.0.0"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["mbstring"],
+ info: Some(
+ "
+mbstring
+
+Multibyte Support => enabled
+Multibyte string engine => libmbfl
+HTTP input encoding translation => disabled
+libmbfl version => 1.3.2
+
+mbstring extension makes use of \"streamable kanji code filter and converter\", which is distributed under the GNU Lesser General Public License version 2.1.
+
+Multibyte (japanese) regex support => enabled
+Multibyte regex (oniguruma) version => 6.1.3",
+ ),
+ expectations: vec![
+ ("lib-mbstring-libmbfl", Exp::Version("1.3.2")),
+ ("lib-mbstring-oniguruma", Exp::Version("6.1.3")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["mbstring"],
+ info: Some(
+ "
+mbstring
+
+Multibyte Support => enabled
+Multibyte string engine => libmbfl
+HTTP input encoding translation => disabled
+libmbfl version => 1.3.2
+oniguruma version => 6.9.4
+
+mbstring extension makes use of \"streamable kanji code filter and converter\", which is distributed under the GNU Lesser General Public License version 2.1.
+
+Multibyte (japanese) regex support => enabled
+Multibyte regex (oniguruma) backtrack check => On",
+ ),
+ expectations: vec![
+ ("lib-mbstring-libmbfl", Exp::Version("1.3.2")),
+ ("lib-mbstring-oniguruma", Exp::Version("6.9.4")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["memcached"],
+ info: Some(
+ "
+memcached
+
+memcached support => enabled
+Version => 3.1.5
+libmemcached version => 1.0.18
+SASL support => yes
+Session support => yes
+igbinary support => yes
+json support => yes
+msgpack support => yes",
+ ),
+ expectations: vec![("lib-memcached-libmemcached", Exp::Version("1.0.18"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7"))],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("OpenSSL 1.1.1g 21 Apr 2020"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7"))],
+ functions: vec![],
+ constants: vec![(
+ "OPENSSL_VERSION_TEXT",
+ None,
+ s("OpenSSL 1.1.1g-freebsd 21 Apr 2020"),
+ )],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("0.9.8.33"))],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("OpenSSL 0.9.8zg 21 Apr 2020"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7-alpha1"))],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("OpenSSL 1.1.1g-pre1 21 Apr 2020"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7-beta2"))],
+ functions: vec![],
+ constants: vec![(
+ "OPENSSL_VERSION_TEXT",
+ None,
+ s("OpenSSL 1.1.1g-beta2 21 Apr 2020"),
+ )],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7-alpha4"))],
+ functions: vec![],
+ constants: vec![(
+ "OPENSSL_VERSION_TEXT",
+ None,
+ s("OpenSSL 1.1.1g-alpha4 21 Apr 2020"),
+ )],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("1.1.1.7-rc2"))],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("OpenSSL 1.1.1g-rc2 21 Apr 2020"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![(
+ "lib-openssl-fips",
+ Exp::Full("1.1.1.7", &[], &["lib-openssl"]),
+ )],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("OpenSSL 1.1.1g-fips 21 Apr 2020"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["openssl"],
+ info: None,
+ expectations: vec![("lib-openssl", Exp::Version("2.0.1.0"))],
+ functions: vec![],
+ constants: vec![("OPENSSL_VERSION_TEXT", None, s("LibreSSL 2.0.1"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["mysqlnd"],
+ info: Some(
+ "
+ mysqlnd
+
+mysqlnd => enabled
+Version => mysqlnd 5.0.11-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $
+Compression => supported
+core SSL => supported
+extended SSL => supported
+Command buffer size => 4096
+Read buffer size => 32768
+Read timeout => 31536000
+Collecting statistics => Yes
+Collecting memory statistics => Yes
+Tracing => n/a
+Loaded plugins => mysqlnd,debug_trace,auth_plugin_mysql_native_password,auth_plugin_mysql_clear_password,auth_plugin_sha256_password
+API Extensions => pdo_mysql,mysqli",
+ ),
+ expectations: vec![("lib-mysqlnd-mysqlnd", Exp::Version("5.0.11-dev"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pdo_mysql"],
+ info: Some(
+ "
+ pdo_mysql
+
+PDO Driver for MySQL => enabled
+Client API version => mysqlnd 5.0.10-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $
+
+Directive => Local Value => Master Value
+pdo_mysql.default_socket => /tmp/mysql.sock => /tmp/mysql.sock",
+ ),
+ expectations: vec![("lib-pdo_mysql-mysqlnd", Exp::Version("5.0.10-dev"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["mongodb"],
+ info: Some(
+ "
+ mongodb
+
+MongoDB support => enabled
+MongoDB extension version => 1.6.1
+MongoDB extension stability => stable
+libbson bundled version => 1.15.2
+libmongoc bundled version => 1.15.2
+libmongoc SSL => enabled
+libmongoc SSL library => OpenSSL
+libmongoc crypto => enabled
+libmongoc crypto library => libcrypto
+libmongoc crypto system profile => disabled
+libmongoc SASL => disabled
+libmongoc ICU => enabled
+libmongoc compression => enabled
+libmongoc compression snappy => disabled
+libmongoc compression zlib => enabled
+
+Directive => Local Value => Master Value
+mongodb.debug => no value => no value",
+ ),
+ expectations: vec![
+ ("lib-mongodb-libmongoc", Exp::Version("1.15.2")),
+ ("lib-mongodb-libbson", Exp::Version("1.15.2")),
+ ],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pcre"],
+ info: Some(
+ "
+pcre
+
+PCRE (Perl Compatible Regular Expressions) Support => enabled
+PCRE Library Version => 10.33 2019-04-16
+PCRE Unicode Version => 11.0.0
+PCRE JIT Support => enabled
+PCRE JIT Target => x86 64bit (little endian + unaligned)",
+ ),
+ expectations: vec![
+ ("lib-pcre", Exp::Version("10.33")),
+ ("lib-pcre-unicode", Exp::Version("11.0.0")),
+ ],
+ functions: vec![],
+ constants: vec![("PCRE_VERSION", None, s("10.33 2019-04-16"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pcre"],
+ info: Some(
+ "
+pcre
+
+PCRE (Perl Compatible Regular Expressions) Support => enabled
+PCRE Library Version => 8.38 2015-11-23
+
+Directive => Local Value => Master Value
+pcre.backtrack_limit => 1000000 => 1000000
+pcre.recursion_limit => 100000 => 100000
+ ",
+ ),
+ expectations: vec![("lib-pcre", Exp::Version("8.38"))],
+ functions: vec![],
+ constants: vec![("PCRE_VERSION", None, s("8.38 2015-11-23"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pgsql"],
+ info: Some(
+ "
+pgsql
+
+PostgreSQL Support => enabled
+PostgreSQL(libpq) Version => 12.2
+PostgreSQL(libpq) => PostgreSQL 12.3 on x86_64-apple-darwin18.7.0, compiled by Apple clang version 11.0.0 (clang-1100.0.33.17), 64-bit
+Multibyte character support => enabled
+SSL support => enabled
+Active Persistent Links => 0
+Active Links => 0
+
+Directive => Local Value => Master Value
+pgsql.allow_persistent => On => On
+pgsql.max_persistent => Unlimited => Unlimited
+pgsql.max_links => Unlimited => Unlimited
+pgsql.auto_reset_persistent => Off => Off
+pgsql.ignore_notice => Off => Off
+pgsql.log_notice => Off => Off",
+ ),
+ expectations: vec![("lib-pgsql-libpq", Exp::Version("12.2"))],
+ functions: vec![],
+ constants: vec![("PGSQL_LIBPQ_VERSION", None, s("12.2"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pdo_pgsql"],
+ info: Some(
+ "
+ pdo_pgsql
+
+PDO Driver for PostgreSQL => enabled
+PostgreSQL(libpq) Version => 12.1
+Module version => 7.1.33
+Revision => $Id: 9c5f356c77143981d2e905e276e439501fe0f419 $",
+ ),
+ expectations: vec![("lib-pdo_pgsql-libpq", Exp::Version("12.1"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pq"],
+ info: Some(
+ "pq
+
+PQ Support => enabled
+Extension Version => 2.2.0
+
+Used Library => Compiled => Linked
+libpq => 14.3 (Ubuntu 14.3-1.pgdg22.04+1) => 15.0.2
+ ",
+ ),
+ expectations: vec![("lib-pq-libpq", Exp::Version("15.0.2"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["rdkafka"],
+ info: None,
+ expectations: vec![("lib-rdkafka-librdkafka", Exp::Version("1.9.2"))],
+ functions: vec![],
+ constants: vec![("RD_KAFKA_VERSION", None, PhpMixed::Int(17367807))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["libsodium"],
+ info: None,
+ expectations: vec![("lib-libsodium", Exp::Version("1.0.17"))],
+ functions: vec![],
+ constants: vec![("SODIUM_LIBRARY_VERSION", None, s("1.0.17"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["sodium"],
+ info: None,
+ expectations: vec![("lib-libsodium", Exp::Version("1.0.15"))],
+ functions: vec![],
+ constants: vec![("SODIUM_LIBRARY_VERSION", None, s("1.0.15"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["pdo_sqlite"],
+ info: Some(
+ "
+pdo_sqlite
+
+PDO Driver for SQLite 3.x => enabled
+SQLite Library => 3.32.3
+ ",
+ ),
+ expectations: vec![("lib-pdo_sqlite-sqlite", Exp::Version("3.32.3"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["sqlite3"],
+ info: Some(
+ "
+sqlite3
+
+SQLite3 support => enabled
+SQLite3 module version => 7.1.33
+SQLite Library => 3.31.0
+
+Directive => Local Value => Master Value
+sqlite3.extension_dir => no value => no value
+sqlite3.defensive => 1 => 1",
+ ),
+ expectations: vec![("lib-sqlite3-sqlite", Exp::Version("3.31.0"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["ssh2"],
+ info: Some(
+ "
+ssh2
+
+SSH2 support => enabled
+extension version => 1.2
+libssh2 version => 1.8.0
+banner => SSH-2.0-libssh2_1.8.0",
+ ),
+ expectations: vec![("lib-ssh2-libssh2", Exp::Version("1.8.0"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["yaml"],
+ info: Some(
+ "
+ yaml
+
+LibYAML Support => enabled
+Module Version => 2.0.2
+LibYAML Version => 0.2.2
+
+Directive => Local Value => Master Value
+yaml.decode_binary => 0 => 0
+yaml.decode_timestamp => 0 => 0
+yaml.decode_php => 0 => 0
+yaml.output_canonical => 0 => 0
+yaml.output_indent => 2 => 2
+yaml.output_width => 80 => 80",
+ ),
+ expectations: vec![("lib-yaml-libyaml", Exp::Version("0.2.2"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["xsl"],
+ info: Some(
+ "
+xsl
+
+XSL => enabled
+libxslt Version => 1.1.33
+libxslt compiled against libxml Version => 2.9.8
+EXSLT => enabled
+libexslt Version => 1.1.29",
+ ),
+ expectations: vec![
+ ("lib-libxslt", Exp::Full("1.1.29", &["lib-xsl"], &[])),
+ ("lib-libxslt-libxml", Exp::Version("2.9.8")),
+ ],
+ functions: vec![],
+ constants: vec![("LIBXSLT_DOTTED_VERSION", None, s("1.1.29"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["zip"],
+ info: None,
+ expectations: vec![("lib-zip-libzip", Exp::Full("1.5.0", &["lib-zip"], &[]))],
+ functions: vec![],
+ constants: vec![("LIBZIP_VERSION", Some("ZipArchive"), s("1.5.0"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["zlib"],
+ info: None,
+ expectations: vec![("lib-zlib", Exp::Version("1.2.10"))],
+ functions: vec![],
+ constants: vec![("ZLIB_VERSION", None, s("1.2.10"))],
+ class_definitions: vec![],
+ },
+ LibCase {
+ extensions: vec!["zlib"],
+ info: Some(
+ "
+zlib
+
+ZLib Support => enabled
+Stream Wrapper => compress.zlib://
+Stream Filter => zlib.inflate, zlib.deflate
+Compiled Version => 1.2.8
+Linked Version => 1.2.11",
+ ),
+ expectations: vec![("lib-zlib", Exp::Version("1.2.11"))],
+ functions: vec![],
+ constants: vec![],
+ class_definitions: vec![],
+ },
+ ]
+}
+
+fn assert_package_links(
+ context: &str,
+ expected_links: &[&str],
+ source_package: &BasePackageHandle,
+ links: IndexMap<String, Link>,
+) {
+ assert_eq!(
+ expected_links.len(),
+ links.len(),
+ "{}: expected package count to match",
+ context
+ );
+
+ for link in links.values() {
+ assert_eq!(source_package.get_name(), link.get_source());
+ assert!(
+ expected_links.contains(&link.get_target()),
+ "{}: package {} not in {:?}",
+ context,
+ link.get_target(),
+ expected_links
+ );
+ let version_constraint =
+ SimpleConstraint::new("=".to_string(), source_package.get_version(), None);
+ assert!(link.get_constraint().matches(&version_constraint.into()));
+ }
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Platform\\Runtime (willReturnMap/willReturnCallback for getExtensions/getExtensionVersion/getExtensionInfo/invoke/hasConstant/getConstant/hasClass/construct) plus ResourceBundleStub/ImagickStub; no mocking framework"]
+#[ignore = "all 59 provideLibraryTestCases datasets ported faithfully; execution still \
+ panics at the unrelated XdebugHandler::get_skipped_version() todo!() (and a few \
+ cases additionally rely on the ResourceBundle/Imagick stub paths), so it stays \
+ ignored"]
fn test_library_information() {
- todo!()
+ let extension_version = "100.200.300";
+
+ for case in library_test_cases() {
+ let extensions: Vec<String> = case.extensions.iter().map(|e| e.to_string()).collect();
+
+ let mut constants: Vec<(String, Option<String>, PhpMixed)> = case
+ .constants
+ .iter()
+ .map(|(n, c, v)| (n.to_string(), c.map(|s| s.to_string()), v.clone()))
+ .collect();
+ constants.push((
+ "PHP_VERSION".to_string(),
+ None,
+ PhpMixed::String("7.1.0".to_string()),
+ ));
+
+ let functions = case.functions.clone();
+ let info = case.info.map(|s| s.to_string());
+
+ let exts_for_get = extensions.clone();
+ let constants_has = constants.clone();
+ let constants_get = constants.clone();
+
+ let mut runtime = MockRuntime::new();
+ runtime
+ .expect_get_extensions()
+ .times(..)
+ .returning(move || exts_for_get.clone());
+ runtime
+ .expect_get_extension_version()
+ .times(..)
+ .returning(move |_extension| extension_version.to_string());
+ runtime
+ .expect_get_extension_info()
+ .times(..)
+ .returning(move |_extension| Ok(info.clone().unwrap_or_default()));
+ runtime
+ .expect_invoke()
+ .times(..)
+ .returning(move |callable, arguments| {
+ for (c, a, ret) in &functions {
+ if *c == callable && *a == arguments {
+ return ret.clone();
+ }
+ }
+ PhpMixed::Null
+ });
+ runtime
+ .expect_has_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants_has
+ .iter()
+ .any(|(n, c, _)| n == constant && c.as_deref() == class.as_deref())
+ });
+ runtime
+ .expect_get_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants_get
+ .iter()
+ .find(|(n, c, _)| n == constant && c.as_deref() == class.as_deref())
+ .map(|(_, _, v)| v.clone())
+ .unwrap_or(PhpMixed::Null)
+ });
+ let class_definitions_has = case.class_definitions.clone();
+ let class_definitions_construct = case.class_definitions.clone();
+ runtime
+ .expect_has_class()
+ .times(..)
+ .returning(move |class| class_definitions_has.iter().any(|d| d.class == class));
+ runtime
+ .expect_construct()
+ .times(..)
+ .returning(move |class, arguments| {
+ for d in &class_definitions_construct {
+ if d.class == class
+ && let Some((args, ret)) = &d.construct
+ && *args == arguments
+ {
+ return Ok(ret.clone());
+ }
+ }
+ Ok(PhpMixed::Null)
+ });
+
+ let mut platform_repository =
+ PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None)
+ .unwrap();
+
+ let libraries: Vec<String> = platform_repository
+ .search("lib".to_string(), SEARCH_NAME, None)
+ .unwrap()
+ .into_iter()
+ .map(|package| package.name)
+ .filter(|name| name.starts_with("lib-"))
+ .collect();
+ let expected_libraries: Vec<&str> = case
+ .expectations
+ .iter()
+ .filter(|(_, exp)| !matches!(exp, Exp::Missing))
+ .map(|(name, _)| *name)
+ .collect();
+ assert_eq!(
+ expected_libraries.len(),
+ libraries.len(),
+ "Expected: {:?}, got {:?}",
+ expected_libraries,
+ libraries
+ );
+
+ let mut all_expectations: Vec<(String, (Option<&str>, &[&str], &[&str]))> = case
+ .expectations
+ .iter()
+ .map(|(name, exp)| (name.to_string(), exp.parts()))
+ .collect();
+ for extension in &extensions {
+ all_expectations.push((
+ format!("ext-{}", extension),
+ (Some(extension_version), &[], &[]),
+ ));
+ }
+
+ for (package_name, (expected_version, expected_replaces, expected_provides)) in
+ all_expectations
+ {
+ let package = platform_repository
+ .find_package(
+ &package_name,
+ FindPackageConstraint::String("*".to_string()),
+ )
+ .unwrap();
+ match expected_version {
+ None => assert!(
+ package.is_none(),
+ "Expected to not find package \"{}\"",
+ package_name
+ ),
+ Some(expected_version) => {
+ assert!(
+ package.is_some(),
+ "Expected to find package \"{}\"",
+ package_name
+ );
+ let package = package.unwrap();
+ assert_eq!(
+ expected_version,
+ package.get_pretty_version(),
+ "Expected version {} for {}",
+ expected_version,
+ package_name
+ );
+ assert_package_links(
+ "replaces",
+ expected_replaces,
+ &package,
+ package.get_replaces(),
+ );
+ assert_package_links(
+ "provides",
+ expected_provides,
+ &package,
+ package.get_provides(),
+ );
+ }
+ }
+ }
+ }
}
#[test]
-#[ignore = "requires PHPUnit getMockBuilder mock of Composer\\Platform\\Runtime (willReturnMap for getConstant/getExtensions); no mocking framework and Runtime is a concrete struct"]
+#[ignore = "seam + Runtime mock ported (getConstant/getExtensions map); execution panics \
+ at the unrelated XdebugHandler::get_skipped_version() todo!()"]
fn test_composer_platform_version() {
- todo!()
+ let constants: IndexMap<String, PhpMixed> = IndexMap::from([
+ (
+ "PHP_VERSION".to_string(),
+ PhpMixed::String("7.0.0".to_string()),
+ ),
+ ("PHP_DEBUG".to_string(), PhpMixed::Bool(false)),
+ ]);
+
+ let mut runtime = MockRuntime::new();
+ runtime
+ .expect_get_extensions()
+ .times(..)
+ .returning(Vec::new);
+ runtime
+ .expect_get_constant()
+ .times(..)
+ .returning(move |constant, class| {
+ constants
+ .get(&constant_key(constant, class.as_deref()))
+ .cloned()
+ .unwrap_or(PhpMixed::Null)
+ });
+ // PHP only stubs getExtensions/getConstant; PHPUnit auto-returns null/false for the
+ // other probed methods. Mirror that so initialize() does not hit unset expectations.
+ runtime
+ .expect_has_constant()
+ .times(..)
+ .returning(|_, _| false);
+ runtime
+ .expect_invoke()
+ .times(..)
+ .returning(|_, _| PhpMixed::Null);
+
+ let mut platform_repository =
+ PlatformRepository::new4(vec![], IndexMap::new(), Some(Box::new(runtime)), None).unwrap();
+
+ let package = platform_repository
+ .find_package(
+ "composer",
+ FindPackageConstraint::String(format!("={}", shirabe::composer::get_version())),
+ )
+ .unwrap();
+ assert!(package.is_some(), "Composer package exists");
}
#[test]
diff --git a/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs b/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
index 5e9471f..ef55c75 100644
--- a/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
+++ b/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
@@ -8,25 +8,48 @@ use shirabe::config::Config;
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::repository::vcs::PerforceDriver;
+use shirabe::util::HttpDownloader;
use shirabe::util::filesystem::Filesystem;
+use shirabe::util::process_executor::MockHandler;
use shirabe_php_shim::PhpMixed;
use tempfile::TempDir;
+use crate::process_executor_mock::{ProcessExecutorMockGuard, get_process_executor_mock};
+
const TEST_URL: &str = "TEST_PERFORCE_URL";
const TEST_DEPOT: &str = "TEST_DEPOT_CONFIG";
const TEST_BRANCH: &str = "TEST_BRANCH_CONFIG";
+// A getMockBuilder('Composer\Util\Perforce') stand-in: the seam trait extracted from the
+// concrete `Perforce` struct, injected via overrideDriverInternalPerforce's Rust equivalent.
+mockall::mock! {
+ #[derive(Debug)]
+ pub Perforce {}
+ impl shirabe::util::PerforceInterface for Perforce {
+ fn initialize_path(&mut self, path: &str);
+ fn set_stream(&mut self, stream: &str);
+ fn p4_login(&mut self) -> anyhow::Result<()>;
+ fn check_stream(&mut self) -> bool;
+ fn write_p4_client_spec(&mut self) -> anyhow::Result<()>;
+ fn connect_client(&mut self) -> anyhow::Result<()>;
+ fn sync_code_base(&mut self, source_reference: Option<String>) -> anyhow::Result<()>;
+ fn cleanup_client_spec(&mut self);
+ fn get_commit_logs(&mut self, from_reference: &str, to_reference: &str) -> Option<String>;
+ fn get_file_content(&mut self, file: &str, identifier: &str) -> Option<String>;
+ fn get_branches(&mut self) -> IndexMap<String, String>;
+ fn get_tags(&mut self) -> IndexMap<String, String>;
+ fn get_user(&self) -> Option<String>;
+ fn get_composer_information(
+ &mut self,
+ identifier: &str,
+ ) -> anyhow::Result<Option<IndexMap<String, PhpMixed>>>;
+ }
+}
+
struct SetUp {
test_path: TempDir,
config: Config,
repo_config: IndexMap<String, PhpMixed>,
- // The IOInterface, ProcessExecutor, HttpDownloader and Perforce mocks, the
- // driver instance and the reflection-based perforce override are not ported.
- io: (),
- process: (),
- http_downloader: (),
- perforce: (),
- driver: (),
}
fn get_test_config(test_path: &std::path::Path) -> Config {
@@ -57,25 +80,34 @@ fn set_up() -> SetUp {
PhpMixed::String(TEST_BRANCH.to_string()),
);
- let io = ();
- let process = ();
- let http_downloader = ();
- let perforce = ();
- // The driver construction and overrideDriverInternalPerforce (reflection) are not ported.
- let driver = ();
-
SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
}
}
+// ref: setUp builds the driver from a mock IO, an empty ProcessExecutorMock and a mock
+// HttpDownloader. The empty, non-strict process mock lets any p4 query Perforce issues resolve
+// with the default (exit 0) result.
+fn make_driver(
+ config: Config,
+ repo_config: &IndexMap<String, PhpMixed>,
+) -> (PerforceDriver, ProcessExecutorMockGuard) {
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let config = Rc::new(RefCell::new(config));
+ let http_downloader = Rc::new(RefCell::new(HttpDownloader::new(
+ io.clone(),
+ config.clone(),
+ IndexMap::new(),
+ false,
+ )));
+ let (process, guard) = get_process_executor_mock(vec![], false, MockHandler::default());
+ let driver = PerforceDriver::new(repo_config.clone(), io, config, http_downloader, process);
+
+ (driver, guard)
+}
+
fn tear_down(test_path: &std::path::Path) {
// cleanup directory under test path
let mut fs = Filesystem::new(None);
@@ -106,146 +138,127 @@ fn test_supports_returns_false_no_deep_check() {
assert!(!PerforceDriver::supports(io, config, "existing.url", false).unwrap());
}
-// The remaining cases mock Perforce, the repository config and IO to drive initialization,
-// composer-file detection and cleanup; mocking is not available here.
#[test]
fn test_initialize_captures_variables_from_repo_config() {
let SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
} = set_up();
let _tear_down = TearDown::new(test_path.path().to_path_buf());
- let _ = (&io, &process, &http_downloader, &perforce, &driver);
- let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
- let config = Rc::new(RefCell::new(config));
- let http_downloader = Rc::new(RefCell::new(shirabe::util::HttpDownloader::new(
- io.clone(),
- config.clone(),
- IndexMap::new(),
- false,
- )));
- // ref: setUp uses getProcessExecutorMock(); the empty, non-strict mock lets Perforce's p4
- // queries resolve with the default (exit 0) result so initialize() never shells out to p4.
- let (process, _process_guard) = crate::process_executor_mock::get_process_executor_mock(
- vec![],
- false,
- shirabe::util::process_executor::MockHandler::default(),
- );
-
- let mut driver = PerforceDriver::new(repo_config, io, config, http_downloader, process);
+ let (mut driver, _process_guard) = make_driver(config, &repo_config);
driver.initialize().unwrap();
assert_eq!(TEST_URL, driver.get_url());
assert_eq!(TEST_DEPOT, driver.get_depot());
assert_eq!(TEST_BRANCH, driver.get_branch());
}
-#[ignore = "needs a Perforce mock injected via reflection (overrideDriverInternalPerforce) to assert p4Login/checkStream/writeP4ClientSpec/connectClient are each called once; the perforce field is pub(crate) and no mock infra exists"]
#[test]
fn test_initialize_logs_in_and_connects_client() {
let SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
} = set_up();
let _tear_down = TearDown::new(test_path.path().to_path_buf());
- let _ = (
- &config,
- &repo_config,
- &io,
- &process,
- &http_downloader,
- &perforce,
- &driver,
- );
- todo!()
+
+ let (mut driver, _process_guard) = make_driver(config, &repo_config);
+ let mut perforce = MockPerforce::new();
+ perforce.expect_p4_login().times(1).returning(|| Ok(()));
+ perforce.expect_check_stream().times(1).returning(|| false);
+ perforce
+ .expect_write_p4_client_spec()
+ .times(1)
+ .returning(|| Ok(()));
+ perforce
+ .expect_connect_client()
+ .times(1)
+ .returning(|| Ok(()));
+ driver.__override_perforce(Box::new(perforce));
+
+ driver.initialize().unwrap();
}
-#[ignore = "needs a Perforce mock injected via reflection (overrideDriverInternalPerforce) to stub getComposerInformation; the perforce field is pub(crate) and no mock infra exists"]
#[test]
fn test_has_composer_file_returns_false_on_no_composer_file() {
let SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
} = set_up();
let _tear_down = TearDown::new(test_path.path().to_path_buf());
- let _ = (
- &config,
- &repo_config,
- &io,
- &process,
- &http_downloader,
- &perforce,
- &driver,
- );
- todo!()
+
+ let identifier = "TEST_IDENTIFIER";
+ let formatted_depot_path = format!("//{}/{}", TEST_DEPOT, identifier);
+
+ let (mut driver, _process_guard) = make_driver(config, &repo_config);
+ let mut perforce = MockPerforce::new();
+ perforce.expect_p4_login().returning(|| Ok(()));
+ perforce.expect_check_stream().returning(|| false);
+ perforce.expect_write_p4_client_spec().returning(|| Ok(()));
+ perforce.expect_connect_client().returning(|| Ok(()));
+ perforce
+ .expect_get_composer_information()
+ .withf(move |id: &str| id == formatted_depot_path)
+ .returning(|_| Ok(Some(IndexMap::new())));
+ driver.__override_perforce(Box::new(perforce));
+
+ driver.initialize().unwrap();
+ let result = driver.has_composer_file(identifier);
+ assert!(!result);
}
-#[ignore = "needs a Perforce mock injected via reflection (overrideDriverInternalPerforce) to stub getComposerInformation; the perforce field is pub(crate) and no mock infra exists"]
#[test]
fn test_has_composer_file_returns_true_with_one_or_more_composer_files() {
let SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
} = set_up();
let _tear_down = TearDown::new(test_path.path().to_path_buf());
- let _ = (
- &config,
- &repo_config,
- &io,
- &process,
- &http_downloader,
- &perforce,
- &driver,
- );
- todo!()
+
+ let identifier = "TEST_IDENTIFIER";
+ let formatted_depot_path = format!("//{}/{}", TEST_DEPOT, identifier);
+
+ let (mut driver, _process_guard) = make_driver(config, &repo_config);
+ let mut perforce = MockPerforce::new();
+ perforce.expect_p4_login().returning(|| Ok(()));
+ perforce.expect_check_stream().returning(|| false);
+ perforce.expect_write_p4_client_spec().returning(|| Ok(()));
+ perforce.expect_connect_client().returning(|| Ok(()));
+ perforce
+ .expect_get_composer_information()
+ .withf(move |id: &str| id == formatted_depot_path)
+ .returning(|_| {
+ // ref: returnValue(['']) — a non-empty list with one empty-string element.
+ let mut info: IndexMap<String, PhpMixed> = IndexMap::new();
+ info.insert("0".to_string(), PhpMixed::String(String::new()));
+ Ok(Some(info))
+ });
+ driver.__override_perforce(Box::new(perforce));
+
+ driver.initialize().unwrap();
+ let result = driver.has_composer_file(identifier);
+ assert!(result);
}
-#[ignore = "needs a Perforce mock injected via reflection (overrideDriverInternalPerforce) to assert cleanupClientSpec is called once; the perforce field is pub(crate) and no mock infra exists"]
#[test]
fn test_cleanup() {
let SetUp {
test_path,
config,
repo_config,
- io,
- process,
- http_downloader,
- perforce,
- driver,
} = set_up();
let _tear_down = TearDown::new(test_path.path().to_path_buf());
- let _ = (
- &config,
- &repo_config,
- &io,
- &process,
- &http_downloader,
- &perforce,
- &driver,
- );
- todo!()
+
+ let (mut driver, _process_guard) = make_driver(config, &repo_config);
+ let mut perforce = MockPerforce::new();
+ perforce
+ .expect_cleanup_client_spec()
+ .times(1)
+ .returning(|| ());
+ driver.__override_perforce(Box::new(perforce));
+
+ driver.cleanup().unwrap();
}
diff --git a/crates/shirabe/tests/util/github_test.rs b/crates/shirabe/tests/util/github_test.rs
index 5caf5c7..10b5dd7 100644
--- a/crates/shirabe/tests/util/github_test.rs
+++ b/crates/shirabe/tests/util/github_test.rs
@@ -1,17 +1,181 @@
//! ref: composer/tests/Composer/Test/Util/GitHubTest.php
-// Both cases construct GitHub with a mocked IO/Config/JsonConfigSource and a mocked
-// HttpDownloader to drive the username/password authentication flow. Mocking is not
-// available, and a real HttpDownloader reaches curl_multi_init (todo!()).
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use shirabe::config::{Config, ConfigSourceInterface};
+use shirabe::io::IOInterface;
+use shirabe::io::io_interface;
+use shirabe::util::GitHub;
+use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler};
+use shirabe_php_shim::PhpMixed;
+
+use crate::config_stub::ConfigStubBuilder;
+use crate::http_downloader_mock::{expect_full, get_http_downloader_mock};
+use crate::io_mock::{Expectation, IOMock, get_io_mock};
+
+const PASSWORD: &str = "password";
+const MESSAGE: &str = "mymessage";
+const ORIGIN: &str = "github.com";
+
+// Records the config setting names a source has had removed, plus a fixed getName,
+// mirroring GitHubTest's JsonConfigSource mocks (getName -> "auth.json", and the
+// config source stubbing removeConfigSetting('github-oauth.<origin>')).
+#[derive(Debug)]
+struct ConfigSourceMock {
+ name: String,
+ removed: Rc<RefCell<Vec<String>>>,
+}
+
+impl ConfigSourceMock {
+ fn new(name: &str) -> (Box<Self>, Rc<RefCell<Vec<String>>>) {
+ let removed = Rc::new(RefCell::new(Vec::new()));
+ (
+ Box::new(Self {
+ name: name.to_string(),
+ removed: removed.clone(),
+ }),
+ removed,
+ )
+ }
+}
+
+impl ConfigSourceInterface for ConfigSourceMock {
+ fn add_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _append: bool,
+ ) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn insert_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _reference_name: &str,
+ _offset: i64,
+ ) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()> {
+ self.removed.borrow_mut().push(name.to_string());
+ Ok(())
+ }
+ fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn add_link(&mut self, _type: &str, _name: &str, _value: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn remove_link(&mut self, _type: &str, _name: &str) -> anyhow::Result<()> {
+ unreachable!()
+ }
+ fn get_name(&self) -> String {
+ self.name.clone()
+ }
+}
+
+fn build_github(
+ io_mock: &Rc<RefCell<IOMock>>,
+ config: Rc<RefCell<Config>>,
+ http_downloader: Rc<RefCell<HttpDownloader>>,
+) -> GitHub {
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+ GitHub::new(io, config, None, Some(http_downloader)).unwrap()
+}
+
+// The PHP Config mock returns null for `get('github-expose-hostname')`, which is
+// falsy and skips the `hostname` process call. A real Config defaults that key to
+// true, so the stub seeds false to reproduce the mock's behaviour.
+fn build_config() -> Rc<RefCell<Config>> {
+ ConfigStubBuilder::new()
+ .with("github-expose-hostname", PhpMixed::Bool(false))
+ .build_shared()
+}
#[test]
-#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"]
fn test_username_password_authentication_flow() {
- todo!()
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![
+ Expectation::text(MESSAGE),
+ Expectation::ask("Token (hidden): ", PASSWORD),
+ ],
+ false,
+ )
+ .unwrap();
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![expect_full(
+ format!("https://api.{}/", ORIGIN),
+ None,
+ 200,
+ "{}",
+ vec![],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let config = build_config();
+ let (auth_source, _) = ConfigSourceMock::new("auth.json");
+ let (conf_source, conf_removed) = ConfigSourceMock::new("config.json");
+ config.borrow_mut().set_auth_config_source(auth_source);
+ config.borrow_mut().set_config_source(conf_source);
+
+ let mut github = build_github(&io_mock, config, http_downloader);
+
+ assert!(
+ github
+ .authorize_oauth_interactively(ORIGIN, Some(MESSAGE))
+ .unwrap()
+ );
+ assert_eq!(
+ *conf_removed.borrow(),
+ vec![format!("github-oauth.{}", ORIGIN)]
+ );
}
#[test]
-#[ignore = "requires getIOMock/getHttpDownloaderMock and getMockBuilder mocks of Config/JsonConfigSource with expects()/willReturn(); no mocking infrastructure exists"]
fn test_username_password_failure() {
- todo!()
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(vec![Expectation::ask("Token (hidden): ", PASSWORD)], false)
+ .unwrap();
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![expect_full(
+ format!("https://api.{}/", ORIGIN),
+ None,
+ 401,
+ "",
+ vec![],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let config = build_config();
+ let (auth_source, _) = ConfigSourceMock::new("auth.json");
+ config.borrow_mut().set_auth_config_source(auth_source);
+
+ let mut github = build_github(&io_mock, config, http_downloader);
+
+ assert!(!github.authorize_oauth_interactively(ORIGIN, None).unwrap());
}
diff --git a/crates/shirabe/tests/util/http_downloader_test.rs b/crates/shirabe/tests/util/http_downloader_test.rs
index 59807a9..078fd57 100644
--- a/crates/shirabe/tests/util/http_downloader_test.rs
+++ b/crates/shirabe/tests/util/http_downloader_test.rs
@@ -4,16 +4,59 @@ use std::cell::RefCell;
use std::rc::Rc;
use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::downloader::TransportException;
use shirabe::io::IOInterface;
use shirabe::io::buffer_io::BufferIO;
+use shirabe::io::io_interface;
+use shirabe::util::Platform;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL;
use shirabe_php_shim::{PHP_EOL, PhpMixed};
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_mock::{Expectation, get_io_mock};
+
+// PHP performs a live HTTP get to assert the URL's user:pass is captured via
+// setAuthentication. The credential capture happens in `add_job`, before any
+// network I/O, so COMPOSER_DISABLE_NETWORK short-circuits the actual request
+// (yielding a non-200 TransportException, as PHP's live 404 would) while the
+// setAuthentication side effect still runs and is verified through the IOMock.
#[test]
-#[ignore = "asserts IOInterface mock ->expects()->method('setAuthentication')->with(...) and performs a live HTTP get; no mock infrastructure exists"]
+#[serial_test::serial]
fn test_capture_authentication_params_from_url() {
- todo!()
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![Expectation::auth(
+ "github.com",
+ "user",
+ Some("pass".to_string()),
+ )],
+ false,
+ )
+ .unwrap();
+
+ // The PHP Config mock returns [] for github-domains/gitlab-domains.
+ let config: Rc<RefCell<Config>> = ConfigStubBuilder::new()
+ .with("github-domains", PhpMixed::Array(IndexMap::new()))
+ .with("gitlab-domains", PhpMixed::Array(IndexMap::new()))
+ .build_shared();
+
+ let io: Rc<RefCell<dyn IOInterface>> = io_mock.clone();
+
+ Platform::put_env("COMPOSER_DISABLE_NETWORK", "1");
+ let mut fs = HttpDownloader::new(io, config, IndexMap::new(), false);
+ Platform::clear_env("COMPOSER_DISABLE_NETWORK");
+
+ if let Err(e) = fs.get(
+ "https://user:pass@github.com/composer/composer/404",
+ IndexMap::new(),
+ ) && let Some(te) = e.downcast_ref::<TransportException>()
+ {
+ assert_ne!(200, te.get_code());
+ }
}
#[test]