aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-27 17:21:00 +0900
committernsfisis <nsfisis@gmail.com>2026-06-27 17:26:28 +0900
commite98823e599eb375b30037cc714710e3309d927d1 (patch)
treee1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/src
parent20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff)
downloadphp-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.gz
php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.zst
php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.zip
test: port Composer tests unblocked by mockall, add seams
Port 11 categories of previously-ignored Composer tests now reachable with the mockall crate: DownloadManager, VCS/Perforce/File downloaders, VersionSelector, PlatformRepository, Auditor, installer/FilesystemRepository, RootPackageLoader, util auth/http, commands, and Cache. Extract test seams additively on concrete structs as *Interface traits (Runtime, HhvmDetector, VersionGuesser, RepositorySet, Perforce, BinaryInstaller) plus mock-field seams (Cache, Filesystem); consumers take trait objects. Mocks are defined locally in the test crates via mockall::mock!, since automock-generated mocks are cfg(test)-gated and invisible across the integration-test boundary. dataProviders are ported in full; tests blocked by unported shims stay #[ignore] with documented reasons rather than reduced or weakened. Fix product bugs surfaced by the ports: - util/github: use the exception code, not the HTTP status, for 401/403 - advisory: serialize empty audit maps as [] to match PHP json_encode - repository/filesystem and downloader/file: fix RefCell double-borrow panics Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src')
-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
21 files changed, 549 insertions, 127 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)
+ }
+}