aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/command/archive_command_test.rs
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/tests/command/archive_command_test.rs
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/tests/command/archive_command_test.rs')
-rw-r--r--crates/shirabe/tests/command/archive_command_test.rs271
1 files changed, 265 insertions, 6 deletions
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);
}