aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/command/archive_command.rs2
-rw-r--r--crates/shirabe/src/factory.rs124
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs148
-rw-r--r--crates/shirabe/src/package/version/version_guesser.rs24
-rw-r--r--crates/shirabe/src/repository/installed_filesystem_repository.rs26
5 files changed, 313 insertions, 11 deletions
diff --git a/crates/shirabe/src/command/archive_command.rs b/crates/shirabe/src/command/archive_command.rs
index 91e8036..ce1d496 100644
--- a/crates/shirabe/src/command/archive_command.rs
+++ b/crates/shirabe/src/command/archive_command.rs
@@ -288,7 +288,7 @@ impl ArchiveCommand {
composer_archive_manager_ref = composer_archive_manager.borrow_mut();
&mut *composer_archive_manager_ref
} else {
- let factory = Factory;
+ let factory = Factory::default();
let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None)));
let http_downloader = std::rc::Rc::new(std::cell::RefCell::new(
Factory::create_http_downloader(io.clone(), config, indexmap::IndexMap::new())?,
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index b3d478c..dcbdf5d 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -85,7 +85,12 @@ impl DisablePlugins {
}
/// Creates a configured instance of composer.
-pub struct Factory;
+#[derive(Default)]
+pub struct Factory {
+ /// For testing only: when true, the overridable `create_*`/`add_*` hooks behave like
+ /// `Composer\Test\Mock\FactoryMock`. `false` in production.
+ mock: bool,
+}
impl Factory {
fn get_home_dir() -> anyhow::Result<String> {
@@ -501,7 +506,11 @@ impl Factory {
}
// Load config and override with local config/auth config
- let mut config = Self::create_config(Some(io.clone()), Some(&cwd))?;
+ let mut config = if self.mock {
+ Self::__create_config_mock(Some(&cwd))?
+ } else {
+ Self::create_config(Some(io.clone()), Some(&cwd))?
+ };
let is_global = local_config_source != Config::SOURCE_UNKNOWN
&& realpath(&config.get_str("home")?) == realpath(&dirname(&local_config_source));
config.merge(&local_config_data, &local_config_source);
@@ -653,12 +662,22 @@ impl Factory {
// load package
let parser = VersionParser::new();
- let guesser = VersionGuesser::new(
- config.clone(),
- process.clone(),
- parser.clone(),
- Some(io.clone()),
- );
+ // FactoryMock::loadRootPackage swaps in a VersionGuesserMock (guessVersion returns null).
+ let guesser = if self.mock {
+ VersionGuesser::__new_mock(
+ config.clone(),
+ process.clone(),
+ parser.clone(),
+ Some(io.clone()),
+ )
+ } else {
+ VersionGuesser::new(
+ config.clone(),
+ process.clone(),
+ parser.clone(),
+ Some(io.clone()),
+ )
+ };
let mut loader = self.load_root_package(
rm.clone(),
config.clone(),
@@ -890,7 +909,7 @@ impl Factory {
disable_plugins: DisablePlugins,
disable_scripts: bool,
) -> Option<PartialComposerHandle> {
- let factory = Self;
+ let factory = Self::default();
let config = Self::create_config(Some(io.clone()), None).ok()?;
factory.create_global_composer(io, &config, disable_plugins, disable_scripts, true)
@@ -904,6 +923,15 @@ impl Factory {
root_package: RootPackageInterfaceHandle,
process: Option<&std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>,
) {
+ // FactoryMock::addLocalRepository installs a bare InstalledArrayRepository instead.
+ if self.mock {
+ rm.set_local_repository(crate::repository::RepositoryInterfaceHandle::new(
+ crate::repository::InstalledArrayRepository::new()
+ .expect("InstalledArrayRepository::new should not fail"),
+ ));
+ return;
+ }
+
let fs = process
.map(|p| std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(Some(p.clone())))));
@@ -1210,6 +1238,10 @@ impl Factory {
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>,
) -> InstallationManager {
+ // FactoryMock::createInstallationManager returns a recording InstallationManagerMock.
+ if self.mock {
+ return InstallationManager::__new_mock(r#loop, io, event_dispatcher);
+ }
InstallationManager::new(r#loop, io, event_dispatcher)
}
@@ -1220,6 +1252,11 @@ impl Factory {
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
process: Option<&std::rc::Rc<std::cell::RefCell<ProcessExecutor>>>,
) {
+ // FactoryMock::createDefaultInstallers is a noop (no installers registered).
+ if self.mock {
+ return;
+ }
+
let fs = std::rc::Rc::new(std::cell::RefCell::new(Filesystem::new(
process.map(std::rc::Rc::clone),
)));
@@ -1307,7 +1344,7 @@ impl Factory {
disable_plugins: DisablePlugins,
disable_scripts: bool,
) -> anyhow::Result<ComposerHandle> {
- let factory = Self;
+ let factory = Self::default();
// for BC reasons, if a config is passed in either as array or a path that is not the default composer.json path
// we disable local plugins as they really should not be loaded from CWD
@@ -1338,6 +1375,73 @@ impl Factory {
})
}
+ /// For testing only: equivalent of `Composer\Test\Mock\FactoryMock::create`. Builds the Composer
+ /// the same way as `create`, but with the FactoryMock overrides enabled (see the `self.mock`
+ /// branches in `create_composer`/`add_local_repository`/`create_installation_manager`/
+ /// `create_default_installers`).
+ pub fn __create_mock(
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config: Option<LocalConfigInput>,
+ disable_plugins: DisablePlugins,
+ disable_scripts: bool,
+ ) -> anyhow::Result<ComposerHandle> {
+ let factory = Self { mock: true };
+
+ let default_composer_file = Self::get_composer_file()?;
+ let config_is_default = matches!(
+ config.as_ref(),
+ Some(LocalConfigInput::Path(p)) if *p == default_composer_file
+ );
+ let disable_plugins = if config.is_some()
+ && !config_is_default
+ && matches!(disable_plugins, DisablePlugins::None)
+ {
+ DisablePlugins::Local
+ } else {
+ disable_plugins
+ };
+
+ let composer =
+ factory.create_composer(io, config, disable_plugins, None, true, disable_scripts)?;
+ composer.as_full().ok_or_else(|| {
+ anyhow::anyhow!(RuntimeException {
+ message: "Composer expected with fullLoad=true".to_string(),
+ code: 0,
+ })
+ })
+ }
+
+ /// For testing only: equivalent of `FactoryMock::createConfig`. Builds a `Config` with a unique
+ /// temp home and packagist disabled, without loading the global config/auth files.
+ fn __create_config_mock(cwd: Option<&str>) -> anyhow::Result<Config> {
+ use std::sync::atomic::{AtomicU64, Ordering};
+ static NEXT: AtomicU64 = AtomicU64::new(0);
+
+ let mut config = Config::new(true, cwd.map(|s| s.to_string()));
+
+ let home = std::env::temp_dir().join(format!(
+ "shirabe-test-{}-{}",
+ std::process::id(),
+ NEXT.fetch_add(1, Ordering::Relaxed)
+ ));
+ let _ = std::fs::create_dir_all(&home);
+
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "home".to_string(),
+ PhpMixed::String(home.to_string_lossy().into_owned()),
+ );
+ 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("config".to_string(), PhpMixed::Array(config_section));
+ merge.insert("repositories".to_string(), PhpMixed::Array(repositories));
+ config.merge(&merge, Config::SOURCE_UNKNOWN);
+
+ Ok(config)
+ }
+
/// If you are calling this in a plugin, you probably should instead use `$composer->getLoop()->getHttpDownloader()`
pub fn create_http_downloader(
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs
index a2cc228..ea6f668 100644
--- a/crates/shirabe/src/installer/installation_manager.rs
+++ b/crates/shirabe/src/installer/installation_manager.rs
@@ -40,6 +40,19 @@ pub struct InstallationManager {
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>,
output_progress: bool,
+ /// For testing only: present iff this manager behaves like
+ /// `Composer\Test\Mock\InstallationManagerMock`, recording operations instead of executing
+ /// them. `None` in production.
+ mock: Option<InstallationManagerMockState>,
+}
+
+/// For testing only: recorded operations for the `InstallationManagerMock` behavior.
+#[derive(Debug, Default)]
+struct InstallationManagerMockState {
+ installed: Vec<PackageInterfaceHandle>,
+ updated: Vec<(PackageInterfaceHandle, PackageInterfaceHandle)>,
+ uninstalled: Vec<PackageInterfaceHandle>,
+ trace: Vec<String>,
}
impl InstallationManager {
@@ -56,9 +69,55 @@ impl InstallationManager {
io,
event_dispatcher,
output_progress: false,
+ mock: None,
}
}
+ /// For testing only: builds a manager that records operations instead of executing them,
+ /// mirroring `Composer\Test\Mock\InstallationManagerMock`.
+ pub fn __new_mock(
+ loop_: std::rc::Rc<std::cell::RefCell<Loop>>,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>,
+ ) -> Self {
+ Self {
+ mock: Some(InstallationManagerMockState::default()),
+ ..Self::new(loop_, io, event_dispatcher)
+ }
+ }
+
+ /// For testing only: the trace of stringified operations recorded by the mock.
+ pub fn __get_trace(&self) -> Vec<String> {
+ self.mock
+ .as_ref()
+ .map(|m| m.trace.clone())
+ .unwrap_or_default()
+ }
+
+ /// For testing only: packages passed to install (and markAliasInstalled) operations.
+ pub fn __get_installed_packages(&self) -> Vec<PackageInterfaceHandle> {
+ self.mock
+ .as_ref()
+ .map(|m| m.installed.clone())
+ .unwrap_or_default()
+ }
+
+ /// For testing only: (initial, target) package pairs passed to update operations.
+ pub fn __get_updated_packages(&self) -> Vec<(PackageInterfaceHandle, PackageInterfaceHandle)> {
+ self.mock
+ .as_ref()
+ .map(|m| m.updated.clone())
+ .unwrap_or_default()
+ }
+
+ /// For testing only: packages passed to uninstall (and markAliasUninstalled) operations.
+ pub fn __get_uninstalled_packages(&self) -> Vec<PackageInterfaceHandle> {
+ self.mock
+ .as_ref()
+ .map(|m| m.uninstalled.clone())
+ .unwrap_or_default()
+ }
+
pub fn reset(&mut self) {
self.notifiable_packages = IndexMap::new();
FileDownloader::reset_download_metadata();
@@ -126,6 +185,11 @@ impl InstallationManager {
repo: &dyn InstalledRepositoryInterface,
package: PackageInterfaceHandle,
) -> Result<bool> {
+ // For testing only (ref InstallationManagerMock::isPackageInstalled).
+ if self.mock.is_some() {
+ return Ok(repo.has_package(package));
+ }
+
if let Some(alias) = package.as_alias() {
let alias_of: PackageInterfaceHandle = alias.get_alias_of().into();
return Ok(
@@ -165,6 +229,70 @@ impl InstallationManager {
run_scripts: bool,
download_only: bool,
) -> Result<()> {
+ // For testing only: the mock records each operation and mutates the repo directly,
+ // skipping the download step (ref InstallationManagerMock::execute). The alias operations'
+ // repo mutation is inlined (rather than calling mark_alias_*) so `self.mock` can stay
+ // borrowed across the loop without also borrowing `&self`.
+ if let Some(mock) = self.mock.as_mut() {
+ let _ = (dev_mode, run_scripts, download_only);
+ for operation in operations {
+ let trace = shirabe_php_shim::strip_tags(&operation.to_string());
+ match operation.get_operation_type().as_str() {
+ "install" => {
+ let op = operation.as_install_operation().expect("install operation");
+ let package = op.get_package();
+ mock.installed.push(package.clone());
+ mock.trace.push(trace);
+ repo.add_package(PackageInterfaceHandle::dup(&package));
+ }
+ "update" => {
+ let op = operation.as_update_operation().expect("update operation");
+ let initial = op.get_initial_package().clone();
+ let target = op.get_target_package().clone();
+ mock.updated.push((initial.clone(), target.clone()));
+ mock.trace.push(trace);
+ repo.remove_package(initial);
+ if !repo.has_package(target.clone()) {
+ repo.add_package(PackageInterfaceHandle::dup(&target));
+ }
+ }
+ "uninstall" => {
+ let op = operation
+ .as_uninstall_operation()
+ .expect("uninstall operation");
+ let package = op.get_package();
+ mock.uninstalled.push(package.clone());
+ mock.trace.push(trace);
+ repo.remove_package(package);
+ }
+ "markAliasInstalled" => {
+ let op = operation
+ .as_any()
+ .downcast_ref::<MarkAliasInstalledOperation>()
+ .expect("markAliasInstalled operation");
+ let package = op.get_package();
+ mock.installed.push(package.clone().into());
+ mock.trace.push(trace);
+ if !repo.has_package(package.clone().into()) {
+ repo.add_package(PackageInterfaceHandle::dup(&package.into()));
+ }
+ }
+ "markAliasUninstalled" => {
+ let op = operation
+ .as_any()
+ .downcast_ref::<MarkAliasUninstalledOperation>()
+ .expect("markAliasUninstalled operation");
+ let package = op.get_package();
+ mock.uninstalled.push(package.clone().into());
+ mock.trace.push(trace);
+ repo.remove_package(package.into());
+ }
+ other => panic!("unknown operation type: {}", other),
+ }
+ }
+ return Ok(());
+ }
+
// @var array<callable(): ?PromiseInterface<void|null>> $cleanupPromises
let mut cleanup_promises: IndexMap<
i64,
@@ -623,6 +751,11 @@ impl InstallationManager {
/// Returns the installation path of a package
pub fn get_install_path(&mut self, package: PackageInterfaceHandle) -> Option<String> {
+ // For testing only (ref InstallationManagerMock::getInstallPath).
+ if self.mock.is_some() {
+ return Some(format!("vendor/{}", package.get_name()));
+ }
+
let installer = self.get_installer(&package.get_type()).ok()?;
installer.get_install_path(package)
@@ -633,6 +766,11 @@ impl InstallationManager {
}
pub fn notify_installs(&mut self, _io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) {
+ // For testing only (ref InstallationManagerMock::notifyInstalls is a noop).
+ if self.mock.is_some() {
+ return;
+ }
+
// TODO(phase-c-promise): PHP collects every http_downloader.add() promise and runs them via
// Loop::wait; the single-threaded sync bridge block_on's each notification serially instead.
let result: Result<()> = (|| -> Result<()> {
@@ -792,6 +930,12 @@ impl InstallationManager {
// plugins may swap in a replacement. The interface captures the methods reached through Composer's
// accessor and through the `&mut dyn InstallationManagerInterface` references fed from it.
pub trait InstallationManagerInterface: std::fmt::Debug {
+ /// For testing only: lets a test recover the concrete manager (e.g. the recording mock) from a
+ /// trait object returned by `Composer::get_installation_manager`.
+ fn as_any(&self) -> &dyn std::any::Any {
+ unimplemented!("as_any is only implemented for the concrete InstallationManager")
+ }
+
fn add_installer(&mut self, installer: Box<dyn InstallerInterface>);
fn remove_installer(&mut self, installer: &dyn InstallerInterface);
fn disable_plugins(&mut self);
@@ -815,6 +959,10 @@ pub trait InstallationManagerInterface: std::fmt::Debug {
}
impl InstallationManagerInterface for InstallationManager {
+ fn as_any(&self) -> &dyn std::any::Any {
+ self
+ }
+
fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) {
self.add_installer(installer);
}
diff --git a/crates/shirabe/src/package/version/version_guesser.rs b/crates/shirabe/src/package/version/version_guesser.rs
index 8e8cc32..6fba7c4 100644
--- a/crates/shirabe/src/package/version/version_guesser.rs
+++ b/crates/shirabe/src/package/version/version_guesser.rs
@@ -49,6 +49,10 @@ pub struct VersionGuesser {
/// @var IOInterface|null
io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>,
+
+ /// For testing only: when true, `guess_version` always returns `None`, mirroring
+ /// `Composer\Test\Mock\VersionGuesserMock`. `false` in production.
+ mock: bool,
}
/// PHP: @phpstan-type Version array{version, commit, pretty_version, feature_version?, feature_pretty_version?}
@@ -73,6 +77,21 @@ impl VersionGuesser {
process,
version_parser,
io,
+ mock: false,
+ }
+ }
+
+ /// For testing only: builds a guesser whose `guess_version` always returns `None`, mirroring
+ /// `Composer\Test\Mock\VersionGuesserMock`.
+ pub fn __new_mock(
+ config: std::rc::Rc<std::cell::RefCell<Config>>,
+ process: std::rc::Rc<std::cell::RefCell<ProcessExecutor>>,
+ version_parser: VersionParser,
+ io: Option<std::rc::Rc<std::cell::RefCell<dyn IOInterface>>>,
+ ) -> Self {
+ Self {
+ mock: true,
+ ..Self::new(config, process, version_parser, io)
}
}
@@ -85,6 +104,11 @@ impl VersionGuesser {
package_config: &IndexMap<String, PhpMixed>,
path: &str,
) -> Result<Option<VersionData>> {
+ // For testing only (ref VersionGuesserMock::guessVersion returns null).
+ if self.mock {
+ return Ok(None);
+ }
+
if !function_exists("proc_open") {
return Ok(None);
}
diff --git a/crates/shirabe/src/repository/installed_filesystem_repository.rs b/crates/shirabe/src/repository/installed_filesystem_repository.rs
index 3cd21eb..8ce07e4 100644
--- a/crates/shirabe/src/repository/installed_filesystem_repository.rs
+++ b/crates/shirabe/src/repository/installed_filesystem_repository.rs
@@ -19,6 +19,9 @@ use shirabe_semver::constraint::AnyConstraint;
#[derive(Debug)]
pub struct InstalledFilesystemRepository {
inner: FilesystemRepository,
+ /// For testing only: when true, `reload` and `write` are no-ops, mirroring
+ /// `Composer\Test\Mock\InstalledFilesystemRepositoryMock`. `false` in production.
+ mock: bool,
}
impl InstalledFilesystemRepository {
@@ -35,6 +38,21 @@ impl InstalledFilesystemRepository {
root_package,
filesystem,
)?,
+ mock: false,
+ })
+ }
+
+ /// For testing only: builds a repository whose `reload`/`write` are no-ops, mirroring
+ /// `Composer\Test\Mock\InstalledFilesystemRepositoryMock`.
+ pub fn __new_mock(
+ repository_file: JsonFile,
+ dump_versions: bool,
+ root_package: Option<RootPackageInterfaceHandle>,
+ filesystem: Option<std::rc::Rc<std::cell::RefCell<Filesystem>>>,
+ ) -> Result<Self> {
+ Ok(Self {
+ mock: true,
+ ..Self::new(repository_file, dump_versions, root_package, filesystem)?
})
}
@@ -59,6 +77,10 @@ impl WritableRepositoryInterface for InstalledFilesystemRepository {
dev_mode: bool,
installation_manager: &mut crate::installer::InstallationManager,
) -> anyhow::Result<()> {
+ // For testing only (ref InstalledFilesystemRepositoryMock::write is a noop).
+ if self.mock {
+ return Ok(());
+ }
self.inner.write(dev_mode, installation_manager)
}
@@ -75,6 +97,10 @@ impl WritableRepositoryInterface for InstalledFilesystemRepository {
}
fn reload(&mut self) -> anyhow::Result<()> {
+ // For testing only (ref InstalledFilesystemRepositoryMock::reload is a noop).
+ if self.mock {
+ return Ok(());
+ }
self.inner.reload()
}