aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/command
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-28 14:30:41 +0900
committernsfisis <nsfisis@gmail.com>2026-06-28 14:30:41 +0900
commite97dc6e64c1be4bf78c420b11cec2a18dfd506d4 (patch)
tree6e397f696631ca23e931a1873803e0b1f85af915 /crates/shirabe/tests/command
parentf408ff1ff95ce6e512810ccb10695f773fe5a8b9 (diff)
downloadphp-shirabe-e97dc6e64c1be4bf78c420b11cec2a18dfd506d4.tar.gz
php-shirabe-e97dc6e64c1be4bf78c420b11cec2a18dfd506d4.tar.zst
php-shirabe-e97dc6e64c1be4bf78c420b11cec2a18dfd506d4.zip
test(tests): use mockall for hand-written interface mocks
Replace hand-written mock/stub structs that re-implemented PHPUnit mock-builder behavior (record-and-verify, manual call counters, unreachable!() guards) with mockall::mock! locals across: - package/loader: MockLoader, VersionGuesserMock - command: ArchiveManager/RepositoryManager/EventDispatcher mocks - util: ConfigSource/AuthJson mocks (auth_helper, bitbucket, github, forgejo, gitlab) - repository/vcs: github_driver NullConfigSource - installer: CountingInstaller, RecordingBinaryInstaller, and the DownloadManager mock (formerly common/downloader_stub.rs, now deleted) - downloader: download_manager create_downloader_mock Verification (counts/args) now lives in mockall expectations checked on drop. installation_manager BinaryInstaller is left hand-written because its as_binary_presence_interface seam returns Some(&mut self), which mockall cannot express; io_stub and io_mock are left as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/command')
-rw-r--r--crates/shirabe/tests/command/archive_command_test.rs260
1 files changed, 119 insertions, 141 deletions
diff --git a/crates/shirabe/tests/command/archive_command_test.rs b/crates/shirabe/tests/command/archive_command_test.rs
index c25ea3c..e7730b9 100644
--- a/crates/shirabe/tests/command/archive_command_test.rs
+++ b/crates/shirabe/tests/command/archive_command_test.rs
@@ -27,83 +27,81 @@ 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())
+// PHP mocks `Composer\Package\Archiver\ArchiveManager` with
+// getMockBuilder(...)->disableOriginalConstructor().
+mockall::mock! {
+ #[derive(Debug)]
+ pub ArchiveManager {}
+ impl ArchiveManagerInterface for ArchiveManager {
+ fn archive(
+ &mut self,
+ package: CompletePackageInterfaceHandle,
+ format: String,
+ target_dir: String,
+ file_name: Option<String>,
+ ignore_filters: bool,
+ ) -> anyhow::Result<String>;
}
}
-/// 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)
+// PHP mocks `Composer\Repository\RepositoryManager` with
+// getMockBuilder(...)->disableOriginalConstructor().
+mockall::mock! {
+ #[derive(Debug)]
+ pub RepositoryManager {}
+ impl RepositoryManagerInterface for RepositoryManager {
+ fn get_local_repository(&self) -> RepositoryInterfaceHandle;
+ fn get_repositories(&self) -> &Vec<RepositoryInterfaceHandle>;
+ fn create_repository<'a>(
+ &self,
+ r#type: &str,
+ config: IndexMap<String, PhpMixed>,
+ name: Option<&'a str>,
+ ) -> anyhow::Result<RepositoryInterfaceHandle>;
+ fn add_repository(&mut self, repository: RepositoryInterfaceHandle);
+ fn set_local_repository(&mut self, repository: RepositoryInterfaceHandle);
}
+}
- fn dispatch_installer_event(
- &mut self,
- _event_name: &str,
- _dev_mode: bool,
- _execute_operations: bool,
- _transaction: Transaction,
- ) -> anyhow::Result<i64> {
- Ok(0)
+// PHP mocks `Composer\EventDispatcher\EventDispatcher` with
+// getMockBuilder(...)->disableOriginalConstructor() and no method stubs.
+mockall::mock! {
+ #[derive(Debug)]
+ pub EventDispatcher {}
+ impl EventDispatcherInterface for EventDispatcher {
+ fn dispatch<'a, 'b>(
+ &mut self,
+ event_name: Option<&'a str>,
+ event: Option<&'b mut dyn EventInterface>,
+ ) -> anyhow::Result<i64>;
+ fn dispatch_script(
+ &mut self,
+ event_name: &str,
+ dev_mode: bool,
+ additional_args: Vec<String>,
+ flags: IndexMap<String, PhpMixed>,
+ ) -> anyhow::Result<i64>;
+ fn dispatch_installer_event(
+ &mut self,
+ event_name: &str,
+ dev_mode: bool,
+ execute_operations: bool,
+ transaction: Transaction,
+ ) -> anyhow::Result<i64>;
+ fn add_listener(&mut self, event_name: &str, listener: Callable, priority: i64);
+ fn has_event_listeners(&mut self, event: &dyn EventInterface) -> bool;
}
+}
- fn add_listener(&mut self, _event_name: &str, _listener: Callable, _priority: i64) {}
-
- fn has_event_listeners(&mut self, _event: &dyn EventInterface) -> bool {
- false
- }
+/// A `getMockBuilder(EventDispatcher::class)->disableOriginalConstructor()` mock with no
+/// behavioral expectations: the dispatch methods ArchiveCommand calls are permissive no-ops.
+fn noop_event_dispatcher() -> MockEventDispatcher {
+ let mut event_dispatcher = MockEventDispatcher::new();
+ event_dispatcher.expect_dispatch().returning(|_, _| Ok(0));
+ event_dispatcher
+ .expect_dispatch_script()
+ .returning(|_, _, _, _| Ok(0));
+ event_dispatcher
}
fn root_package(name: &str, version: &str) -> RootPackageHandle {
@@ -126,18 +124,30 @@ fn test_uses_config_from_composer_object() {
.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 cwd = Platform::get_cwd(false).unwrap();
+ let mut manager = MockArchiveManager::new();
+ // PHP: ->expects($this->once())->method('archive')
+ // ->with($package, 'zip', '.', null, false)->willReturn(Platform::getCwd()).
+ // PHP asserts archive() receives `$package` itself; we check the identity via the package name
+ // we set on the Composer.
+ manager
+ .expect_archive()
+ .times(1)
+ .withf(|package, format, target_dir, file_name, ignore_filters| {
+ package.get_name() == "archive/test"
+ && format == "zip"
+ && target_dir == "."
+ && file_name.is_none()
+ && !*ignore_filters
+ })
+ .returning(move |_, _, _, _, _| Ok(cwd.clone()));
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_event_dispatcher(Rc::new(RefCell::new(noop_event_dispatcher())));
composer.set_package(package.into());
let composer = full_composer(composer);
@@ -146,16 +156,6 @@ fn test_uses_config_from_composer_object() {
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]
@@ -188,41 +188,6 @@ fn test_uses_config_from_factory_when_composer_is_not_defined() {
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]
fn test_uses_config_from_composer_object_with_package_name() {
let input: Rc<RefCell<dyn InputInterface>> = Rc::new(RefCell::new(
@@ -239,26 +204,47 @@ fn test_uses_config_from_composer_object_with_package_name() {
.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 cwd = Platform::get_cwd(false).unwrap();
+ let mut manager = MockArchiveManager::new();
+ // PHP: ->expects($this->once())->method('archive')
+ // ->with($package, 'zip', '.', null, false)->willReturn(Platform::getCwd()).
+ manager
+ .expect_archive()
+ .times(1)
+ .withf(|package, format, target_dir, file_name, ignore_filters| {
+ package.get_name() == "foo/bar"
+ && format == "zip"
+ && target_dir == "."
+ && file_name.is_none()
+ && !*ignore_filters
+ })
+ .returning(move |_, _, _, _, _| Ok(cwd.clone()));
let package = root_package("foo/bar", "1.0.0");
+ let mut repository_manager = MockRepositoryManager::new();
+ // PHP: ->expects($this->once())->method('getLocalRepository')->willReturn($installedRepository).
// 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![],
- };
+ // Built inside the closure so it captures nothing (mockall requires `Send` closures).
+ repository_manager
+ .expect_get_local_repository()
+ .times(1)
+ .returning(|| {
+ let package = root_package("foo/bar", "1.0.0");
+ let installed =
+ InstalledArrayRepository::new_with_packages(vec![package.into()]).unwrap();
+ RepositoryInterfaceHandle::new(installed)
+ });
+ // PHP: ->expects($this->once())->method('getRepositories')->willReturn([]).
+ repository_manager
+ .expect_get_repositories()
+ .times(1)
+ .return_const(Vec::new());
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_event_dispatcher(Rc::new(RefCell::new(noop_event_dispatcher())));
composer.set_package(package.into());
composer.set_repository_manager(Rc::new(RefCell::new(repository_manager)));
let composer = full_composer(composer);
@@ -267,12 +253,4 @@ fn test_uses_config_from_composer_object_with_package_name() {
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);
}