From e97dc6e64c1be4bf78c420b11cec2a18dfd506d4 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Sun, 28 Jun 2026 14:30:41 +0900 Subject: 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) --- .../shirabe/tests/command/archive_command_test.rs | 260 +++++++++----------- crates/shirabe/tests/common/downloader_stub.rs | 129 ---------- .../tests/downloader/download_manager_test.rs | 3 +- crates/shirabe/tests/downloader/main.rs | 2 - .../tests/installer/installation_manager_test.rs | 195 ++++++++------- .../tests/installer/library_installer_test.rs | 265 ++++++++++++--------- crates/shirabe/tests/installer/main.rs | 2 - .../package/loader/root_package_loader_test.rs | 57 ++--- .../package/loader/validating_array_loader_test.rs | 74 +++--- .../tests/repository/vcs/github_driver_test.rs | 92 +++---- crates/shirabe/tests/util/auth_helper_test.rs | 133 ++++------- crates/shirabe/tests/util/bitbucket_test.rs | 199 ++++++---------- crates/shirabe/tests/util/forgejo_test.rs | 123 ++++------ crates/shirabe/tests/util/github_test.rs | 123 ++++------ crates/shirabe/tests/util/gitlab_test.rs | 77 +++--- 15 files changed, 730 insertions(+), 1004 deletions(-) delete mode 100644 crates/shirabe/tests/common/downloader_stub.rs 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, - 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>>, - return_value: String, -} - -impl ArchiveManagerInterface for ArchiveManagerMock { - fn archive( - &mut self, - package: CompletePackageInterfaceHandle, - format: String, - target_dir: String, - file_name: Option, - ignore_filters: bool, - ) -> anyhow::Result { - 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, + ignore_filters: bool, + ) -> anyhow::Result; } } -/// 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 { - Ok(0) - } - - fn dispatch_script( - &mut self, - _event_name: &str, - _dev_mode: bool, - _additional_args: Vec, - _flags: IndexMap, - ) -> anyhow::Result { - 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; + fn create_repository<'a>( + &self, + r#type: &str, + config: IndexMap, + name: Option<&'a str>, + ) -> anyhow::Result; + 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 { - 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; + fn dispatch_script( + &mut self, + event_name: &str, + dev_mode: bool, + additional_args: Vec, + flags: IndexMap, + ) -> anyhow::Result; + fn dispatch_installer_event( + &mut self, + event_name: &str, + dev_mode: bool, + execute_operations: bool, + transaction: Transaction, + ) -> anyhow::Result; + 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, -} - -impl RepositoryManagerInterface for RepositoryManagerMock { - fn get_local_repository(&self) -> RepositoryInterfaceHandle { - self.local.clone() - } - - fn get_repositories(&self) -> &Vec { - &self.repositories - } - - fn create_repository( - &self, - _type: &str, - _config: IndexMap, - _name: Option<&str>, - ) -> anyhow::Result { - 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> = 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); } diff --git a/crates/shirabe/tests/common/downloader_stub.rs b/crates/shirabe/tests/common/downloader_stub.rs deleted file mode 100644 index 0c4432b..0000000 --- a/crates/shirabe/tests/common/downloader_stub.rs +++ /dev/null @@ -1,129 +0,0 @@ -//! No-op DownloaderInterface stub for installer tests. -//! -//! PHP mocks `Composer\Downloader\DownloadManager` directly and asserts its -//! `install`/`update`/`remove` calls. The Rust DownloadManager is a concrete type -//! that dispatches to a registered DownloaderInterface, so the equivalent stub -//! lives one level down: a downloader registered under some dist type that records -//! the calls it receives and resolves to null, the same way the PHP mock returns -//! `\React\Promise\resolve(null)`. -#![allow(dead_code)] - -use std::cell::RefCell; -use std::rc::Rc; - -use shirabe::downloader::DownloaderInterface; -use shirabe::package::PackageInterfaceHandle; -use shirabe_php_shim::PhpMixed; - -/// One recorded downloader operation, capturing the package pretty-names and the -/// target path so tests can assert what the LibraryInstaller forwarded. -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DownloaderCall { - Install { - package: String, - path: String, - }, - Update { - initial: String, - target: String, - path: String, - }, - Remove { - package: String, - path: String, - }, -} - -#[derive(Debug, Default)] -pub struct DownloaderStub { - calls: Rc>>, -} - -impl DownloaderStub { - pub fn new() -> Self { - Self::default() - } - - /// Shared handle to the recorded call log, so tests can inspect it after the - /// stub has been moved into the DownloadManager. - pub fn calls(&self) -> Rc>> { - self.calls.clone() - } -} - -#[async_trait::async_trait(?Send)] -impl DownloaderInterface for DownloaderStub { - fn get_installation_source(&self) -> String { - "dist".to_string() - } - - async fn download( - &mut self, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, - _output: bool, - ) -> anyhow::Result> { - Ok(None) - } - - async fn prepare( - &mut self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, - ) -> anyhow::Result> { - Ok(None) - } - - async fn install( - &mut self, - package: PackageInterfaceHandle, - path: &str, - _output: bool, - ) -> anyhow::Result> { - self.calls.borrow_mut().push(DownloaderCall::Install { - package: package.get_pretty_name(), - path: path.to_string(), - }); - Ok(None) - } - - async fn update( - &mut self, - initial: PackageInterfaceHandle, - target: PackageInterfaceHandle, - path: &str, - ) -> anyhow::Result> { - self.calls.borrow_mut().push(DownloaderCall::Update { - initial: initial.get_pretty_name(), - target: target.get_pretty_name(), - path: path.to_string(), - }); - Ok(None) - } - - async fn remove( - &mut self, - package: PackageInterfaceHandle, - path: &str, - _output: bool, - ) -> anyhow::Result> { - self.calls.borrow_mut().push(DownloaderCall::Remove { - package: package.get_pretty_name(), - path: path.to_string(), - }); - Ok(None) - } - - async fn cleanup( - &mut self, - _type: &str, - _package: PackageInterfaceHandle, - _path: &str, - _prev_package: Option, - ) -> anyhow::Result> { - Ok(None) - } -} diff --git a/crates/shirabe/tests/downloader/download_manager_test.rs b/crates/shirabe/tests/downloader/download_manager_test.rs index 10fd1a9..e18643e 100644 --- a/crates/shirabe/tests/downloader/download_manager_test.rs +++ b/crates/shirabe/tests/downloader/download_manager_test.rs @@ -11,7 +11,6 @@ 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. @@ -95,7 +94,7 @@ fn make_package(name: &str, is_dev: bool) -> PackageInterfaceHandle { /// ref: DownloadManagerTest::createDownloaderMock fn create_downloader_mock() -> Rc> { - Rc::new(RefCell::new(DownloaderStub::new())) as Rc> + as_dyn(MockDownloader::new()) } /// A `createDownloaderMock()` whose `getInstallationSource()` reports the given diff --git a/crates/shirabe/tests/downloader/main.rs b/crates/shirabe/tests/downloader/main.rs index d6e07e4..8244010 100644 --- a/crates/shirabe/tests/downloader/main.rs +++ b/crates/shirabe/tests/downloader/main.rs @@ -1,7 +1,5 @@ #[path = "../common/config_stub.rs"] mod config_stub; -#[path = "../common/downloader_stub.rs"] -mod downloader_stub; #[path = "../common/http_downloader_mock.rs"] mod http_downloader_mock; #[path = "../common/io_mock.rs"] diff --git a/crates/shirabe/tests/installer/installation_manager_test.rs b/crates/shirabe/tests/installer/installation_manager_test.rs index 01a7513..7480a02 100644 --- a/crates/shirabe/tests/installer/installation_manager_test.rs +++ b/crates/shirabe/tests/installer/installation_manager_test.rs @@ -43,48 +43,38 @@ fn set_up() -> SetUp { SetUp { loop_, io } } -/// Records of the calls a `CountingInstaller` received, shared so the test can inspect them -/// after the installer has been moved into the InstallationManager. This reproduces PHPUnit's -/// `expects($this->exactly(n))->method(...)->with(...)` assertions with explicit counters. -#[derive(Debug, Default)] -struct InstallerCalls { - supports_args: Vec, - install: Vec, - uninstall: Vec, - update: Vec<(PackageInterfaceHandle, PackageInterfaceHandle)>, -} - -/// Configurable `InstallerInterface` stub, equivalent to -/// `getMockBuilder(InstallerInterface::class)->getMock()`. `supports` returns true only when the -/// requested type equals `supported_type` (PHP uses a returnCallback comparing `$arg === 'vendor'`), -/// and every method records its arguments into the shared `calls`. -#[derive(Debug)] -struct CountingInstaller { - supported_type: String, - calls: Rc>, -} - -impl CountingInstaller { - fn new(supported_type: &str) -> (Self, Rc>) { - let calls = Rc::new(RefCell::new(InstallerCalls::default())); - ( - Self { - supported_type: supported_type.to_string(), - calls: calls.clone(), - }, - calls, - ) +// Equivalent to `getMockBuilder(InstallerInterface::class)->getMock()`. mockall cannot generate an +// `#[async_trait]` impl for the async methods that take `&mut dyn InstalledRepositoryInterface` +// (the object lifetime async_trait inserts clashes with mockall's generated lifetimes), so the +// expectations live on inherent methods and a thin hand-written InstallerInterface impl forwards to +// them, dropping the unused `repo` argument exactly as the PHPUnit mock ignores it. The methods not +// configured by any test (is_installed/download/prepare/cleanup/get_install_path, and the defaulted +// as_binary_presence_interface/as_plugin_installer_mut) return the same defaults as an unconfigured +// PHPUnit mock. +mockall::mock! { + #[derive(Debug)] + pub Installer { + fn supports(&self, package_type: &str) -> bool; + fn install( + &mut self, + package: PackageInterfaceHandle, + ) -> anyhow::Result>; + fn update( + &mut self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + ) -> anyhow::Result>; + fn uninstall( + &mut self, + package: PackageInterfaceHandle, + ) -> anyhow::Result>; } } #[async_trait::async_trait(?Send)] -impl InstallerInterface for CountingInstaller { +impl InstallerInterface for MockInstaller { fn supports(&self, package_type: &str) -> bool { - self.calls - .borrow_mut() - .supports_args - .push(package_type.to_string()); - package_type == self.supported_type + MockInstaller::supports(self, package_type) } fn is_installed( @@ -117,8 +107,7 @@ impl InstallerInterface for CountingInstaller { _repo: &mut dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> anyhow::Result> { - self.calls.borrow_mut().install.push(package); - Ok(None) + MockInstaller::install(self, package) } async fn update( @@ -127,8 +116,7 @@ impl InstallerInterface for CountingInstaller { initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result> { - self.calls.borrow_mut().update.push((initial, target)); - Ok(None) + MockInstaller::update(self, initial, target) } async fn uninstall( @@ -136,8 +124,7 @@ impl InstallerInterface for CountingInstaller { _repo: &mut dyn InstalledRepositoryInterface, package: PackageInterfaceHandle, ) -> anyhow::Result> { - self.calls.borrow_mut().uninstall.push(package); - Ok(None) + MockInstaller::uninstall(self, package) } async fn cleanup( @@ -283,7 +270,13 @@ fn same_handle(a: &PackageInterfaceHandle, b: &PackageInterfaceHandle) -> bool { #[test] fn test_add_get_installer() { let set_up = set_up(); - let (installer, calls) = CountingInstaller::new("vendor"); + let mut installer = MockInstaller::new(); + // PHP expects supports() to be called exactly twice (once for the cached 'vendor' lookup, + // once for the failing 'unregistered' lookup), returning true only for 'vendor'. + installer + .expect_supports() + .times(2) + .returning(|arg| arg == "vendor"); let mut manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); @@ -292,10 +285,6 @@ fn test_add_get_installer() { assert!(manager.get_installer("vendor").is_ok()); assert!(manager.get_installer("unregistered").is_err()); - - // PHP expects supports() to be called exactly twice (once for the cached 'vendor' lookup, - // once for the failing 'unregistered' lookup). - assert_eq!(calls.borrow().supports_args.len(), 2); } #[ignore = "removeInstaller compares installers by object identity, but add_installer moves the Box into the manager, leaving no &dyn reference to pass back to remove_installer; faithful reproduction needs a shared-ownership installer registry"] @@ -313,87 +302,131 @@ fn test_execute() { #[test] fn test_install() { let set_up = set_up(); - let (installer, calls) = CountingInstaller::new("library"); + let package = get_package("test/pkg", "1.0.0"); + + let mut installer = MockInstaller::new(); + installer + .expect_supports() + .times(1) + .withf(|package_type| package_type == "library") + .returning(|_| true); + let expected = package.clone(); + installer + .expect_install() + .times(1) + .withf_st(move |package| same_handle(package, &expected)) + .returning(|_| Ok(None)); + let mut manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); manager.add_installer(Box::new(installer)); - let package = get_package("test/pkg", "1.0.0"); let operation = InstallOperation::new(package.clone()); let mut repository = InstalledArrayRepository::new().unwrap(); - // install() returns the (empty) installer promise; the call is observed via the recorded args. run(manager.install(&mut repository, &operation)); - - assert_eq!(calls.borrow().supports_args, vec!["library".to_string()]); - assert_eq!(calls.borrow().install.len(), 1); - assert!(same_handle(&calls.borrow().install[0], &package)); } #[test] fn test_update_with_equal_types() { let set_up = set_up(); - let (installer, calls) = CountingInstaller::new("library"); + let initial = get_package("test/initial", "1.0.0"); + let target = get_package("test/target", "1.0.1"); + + let mut installer = MockInstaller::new(); + installer + .expect_supports() + .times(1) + .withf(|package_type| package_type == "library") + .returning(|_| true); + let expected_initial = initial.clone(); + let expected_target = target.clone(); + installer + .expect_update() + .times(1) + .withf_st(move |initial, target| { + same_handle(initial, &expected_initial) && same_handle(target, &expected_target) + }) + .returning(|_, _| Ok(None)); + let mut manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); manager.add_installer(Box::new(installer)); - let initial = get_package("test/initial", "1.0.0"); - let target = get_package("test/target", "1.0.1"); let operation = UpdateOperation::new(initial.clone(), target.clone()); let mut repository = InstalledArrayRepository::new().unwrap(); run(manager.update(&mut repository, &operation)); - - assert_eq!(calls.borrow().supports_args, vec!["library".to_string()]); - assert_eq!(calls.borrow().update.len(), 1); - assert!(same_handle(&calls.borrow().update[0].0, &initial)); - assert!(same_handle(&calls.borrow().update[0].1, &target)); } #[test] fn test_update_with_not_equal_types() { let set_up = set_up(); - let (lib_installer, lib_calls) = CountingInstaller::new("library"); - let (bundle_installer, bundle_calls) = CountingInstaller::new("bundles"); + let initial = typed_package("test/initial", "1.0.0", "library"); + let target = typed_package("test/target", "1.0.1", "bundles"); + + let mut lib_installer = MockInstaller::new(); + lib_installer + .expect_supports() + .times(1) + .withf(|package_type| package_type == "library") + .returning(|_| true); + let expected_initial = initial.clone(); + lib_installer + .expect_uninstall() + .times(1) + .withf_st(move |package| same_handle(package, &expected_initial)) + .returning(|_| Ok(None)); + + let mut bundle_installer = MockInstaller::new(); + bundle_installer + .expect_supports() + .times(2) + .returning(|arg| arg == "bundles"); + let expected_target = target.clone(); + bundle_installer + .expect_install() + .times(1) + .withf_st(move |package| same_handle(package, &expected_target)) + .returning(|_| Ok(None)); + let mut manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); manager.add_installer(Box::new(lib_installer)); manager.add_installer(Box::new(bundle_installer)); - let initial = typed_package("test/initial", "1.0.0", "library"); - let target = typed_package("test/target", "1.0.1", "bundles"); let operation = UpdateOperation::new(initial.clone(), target.clone()); let mut repository = InstalledArrayRepository::new().unwrap(); run(manager.update(&mut repository, &operation)); - - // The lib installer uninstalls the initial package once. - assert_eq!(lib_calls.borrow().uninstall.len(), 1); - assert!(same_handle(&lib_calls.borrow().uninstall[0], &initial)); - - // The bundle installer installs the target package once. - assert_eq!(bundle_calls.borrow().install.len(), 1); - assert!(same_handle(&bundle_calls.borrow().install[0], &target)); } #[test] fn test_uninstall() { let set_up = set_up(); - let (installer, calls) = CountingInstaller::new("library"); + let package = get_package("test/pkg", "1.0.0"); + + let mut installer = MockInstaller::new(); + installer + .expect_supports() + .times(1) + .withf(|package_type| package_type == "library") + .returning(|_| true); + let expected = package.clone(); + installer + .expect_uninstall() + .times(1) + .withf_st(move |package| same_handle(package, &expected)) + .returning(|_| Ok(None)); + let mut manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); manager.add_installer(Box::new(installer)); - let package = get_package("test/pkg", "1.0.0"); let operation = UninstallOperation::new(package.clone()); let mut repository = InstalledArrayRepository::new().unwrap(); run(manager.uninstall(&mut repository, &operation)); - - assert_eq!(calls.borrow().supports_args, vec!["library".to_string()]); - assert_eq!(calls.borrow().uninstall.len(), 1); - assert!(same_handle(&calls.borrow().uninstall[0], &package)); } #[test] diff --git a/crates/shirabe/tests/installer/library_installer_test.rs b/crates/shirabe/tests/installer/library_installer_test.rs index f519764..b91ec51 100644 --- a/crates/shirabe/tests/installer/library_installer_test.rs +++ b/crates/shirabe/tests/installer/library_installer_test.rs @@ -11,19 +11,87 @@ use shirabe::composer::{ ComposerHandle, PartialComposerHandle, PartialComposerWeakHandle, PartialOrFullComposer, }; use shirabe::config::Config; -use shirabe::downloader::DownloadManager; -use shirabe::installer::{InstallerInterface, LibraryInstaller}; +use shirabe::downloader::{DownloadManagerInterface, DownloaderInterface}; +use shirabe::installer::{BinaryInstallerInterface, InstallerInterface, LibraryInstaller}; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; +use shirabe::package::PackageInterfaceHandle; use shirabe::repository::InstalledArrayRepository; use shirabe::repository::RepositoryInterface; use shirabe::repository::WritableRepositoryInterface; use shirabe::util::filesystem::Filesystem; use shirabe_php_shim::PhpMixed; -use crate::downloader_stub::{DownloaderCall, DownloaderStub}; use crate::test_case::get_package; +// PHP mocks `Composer\Downloader\DownloadManager` with getMockBuilder and asserts its +// install/update/remove calls; here the equivalent mock is injected into the Composer via +// setDownloadManager. +mockall::mock! { + #[derive(Debug)] + pub DownloadManager {} + #[async_trait::async_trait(?Send)] + impl DownloadManagerInterface for DownloadManager { + fn set_prefer_source(&mut self, prefer_source: bool); + fn set_prefer_dist(&mut self, prefer_dist: bool); + fn get_downloader_for_package( + &self, + package: PackageInterfaceHandle, + ) -> anyhow::Result>>>; + async fn download( + &self, + package: PackageInterfaceHandle, + target_dir: &str, + prev_package: Option, + ) -> anyhow::Result>; + async fn prepare( + &self, + r#type: &str, + package: PackageInterfaceHandle, + target_dir: &str, + prev_package: Option, + ) -> anyhow::Result>; + async fn install( + &self, + package: PackageInterfaceHandle, + target_dir: &str, + ) -> anyhow::Result>; + async fn update( + &self, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + target_dir: &str, + ) -> anyhow::Result>; + async fn remove( + &self, + package: PackageInterfaceHandle, + target_dir: &str, + ) -> anyhow::Result>; + async fn cleanup( + &self, + r#type: &str, + package: PackageInterfaceHandle, + target_dir: &str, + prev_package: Option, + ) -> anyhow::Result>; + } +} + +// PHP mocks `Composer\Installer\BinaryInstaller`; the Rust seam is `BinaryInstallerInterface`. +mockall::mock! { + #[derive(Debug)] + pub BinaryInstaller {} + impl BinaryInstallerInterface for BinaryInstaller { + fn install_binaries( + &mut self, + package: PackageInterfaceHandle, + install_path: &str, + warn_on_overwrite: bool, + ); + fn remove_binaries(&mut self, package: PackageInterfaceHandle); + } +} + fn run(future: F) -> F::Output { tokio::runtime::Builder::new_current_thread() .build() @@ -31,10 +99,10 @@ fn run(future: F) -> F::Output { .block_on(future) } -/// Mirror of setUp(): builds the Composer/Config over temp root/vendor/bin dirs -/// plus a real DownloadManager and a NullIO. The `_composer_rc` keeps the inner -/// Rc alive for the duration of the test since LibraryInstaller only holds a weak -/// handle. +/// Mirror of setUp(): builds the Composer/Config over temp root/vendor/bin dirs plus a NullIO and a +/// DownloadManager mock. `composer_full` keeps the inner Rc alive for the duration of the test since +/// LibraryInstaller only holds a weak handle, and lets tests swap in a configured DownloadManager +/// mock before constructing the installer. struct SetUp { root: TempDir, vendor_dir: String, @@ -42,10 +110,16 @@ struct SetUp { io: Rc>, composer: PartialComposerWeakHandle, fs: Filesystem, - /// Recorded downloader operations forwarded by the LibraryInstaller, mirroring the - /// PHP DownloadManager mock's expectations on install/update/remove. - downloader_calls: Rc>>, - _composer_rc: ComposerHandle, + composer_full: ComposerHandle, +} + +/// Replaces the Composer's DownloadManager with the given mock. Must be called before +/// `LibraryInstaller::new`, which captures the manager at construction time. +fn set_download_manager(setup: &SetUp, dm: MockDownloadManager) { + setup + .composer_full + .borrow_mut() + .set_download_manager(Rc::new(RefCell::new(dm))); } fn set_up() -> SetUp { @@ -77,16 +151,15 @@ fn set_up() -> SetUp { let io: Rc> = Rc::new(RefCell::new(NullIO::new())); - let mut dm = DownloadManager::new(io.clone(), false, None); - let downloader = DownloaderStub::new(); - let downloader_calls = downloader.calls(); - dm.set_downloader("fake", Rc::new(RefCell::new(downloader))); - let dm_rc = Rc::new(RefCell::new(dm)); - let composer_rc = Rc::new(RefCell::new(PartialOrFullComposer::new_full())); let composer = ComposerHandle::from_rc_unchecked(composer_rc.clone()); composer.borrow_mut().set_config(config_rc); - composer.borrow_mut().set_download_manager(dm_rc); + // Default unconfigured mock so LibraryInstaller::new can resolve a DownloadManager even in + // tests that exercise no downloader call; tests that assert calls install their own via + // set_download_manager. + composer + .borrow_mut() + .set_download_manager(Rc::new(RefCell::new(MockDownloadManager::new()))); let weak = PartialComposerHandle::from_rc(composer_rc).downgrade(); @@ -97,21 +170,10 @@ fn set_up() -> SetUp { io, composer: weak, fs, - downloader_calls, - _composer_rc: composer, + composer_full: composer, } } -/// Builds a `dist`-installable test package backed by the `fake` downloader stub, so the -/// real DownloadManager dispatches install/update/remove to the recording stub instead of -/// erroring on a package with no installation source. -fn get_installable_package(name: &str, version: &str) -> shirabe::package::PackageInterfaceHandle { - let package = get_package(name, version); - package.set_installation_source(Some("dist".to_string())); - package.set_dist_type(Some("fake".to_string())); - package -} - fn tear_down(setup: &mut SetUp) { let root = setup.root.path().to_path_buf(); setup.fs.remove_directory(&root).ok(); @@ -167,24 +229,29 @@ fn test_is_installed() { #[test] fn test_install() { let mut setup = set_up(); + let package = get_package("some/package", "1.0.0"); + + // PHP asserts the DownloadManager mock's install() is called once with + // ($package, vendorDir/some/package). + let mut dm = MockDownloadManager::new(); + let expected_package = package.clone(); + let expected_path = format!("{}/some/package", setup.vendor_dir); + dm.expect_install() + .times(1) + .withf_st(move |package, target_dir| { + Rc::ptr_eq(package.as_rc(), expected_package.as_rc()) + && target_dir == expected_path.as_str() + }) + .returning(|_, _| Ok(None)); + set_download_manager(&setup, dm); + let mut library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); - let package = get_installable_package("some/package", "1.0.0"); - // PHP asserts the DownloadManager mock's install() is called once with - // ($package, vendorDir/some/package); here the recording downloader stub - // captures the forwarded call instead. let mut repository = InstalledArrayRepository::new().unwrap(); run(library.install(&mut repository, package.clone())).unwrap(); - assert_eq!( - vec![DownloaderCall::Install { - package: "some/package".to_string(), - path: format!("{}/some/package", setup.vendor_dir), - }], - *setup.downloader_calls.borrow() - ); // PHP asserts repository->addPackage was called once with $package. assert!(repository.has_package(package)); @@ -204,8 +271,8 @@ fn test_install() { fn test_update() { let mut setup = set_up(); - let initial = get_installable_package("vendor/package1", "1.0.0"); - let target = get_installable_package("vendor/package1", "2.0.0"); + let initial = get_package("vendor/package1", "1.0.0"); + let target = get_package("vendor/package1", "2.0.0"); initial.__set_target_dir(Some("oldtarget".to_string())); target.__set_target_dir(Some("newtarget".to_string())); @@ -217,6 +284,22 @@ fn test_update() { let new_target_dir = format!("{}/vendor/package1/newtarget", setup.vendor_dir); fs::create_dir_all(&old_target_dir).unwrap(); + // PHP asserts the DownloadManager mock's update() is called once with + // ($initial, $target, vendorDir/vendor/package1/newtarget). + let mut dm = MockDownloadManager::new(); + let expected_initial = initial.clone(); + let expected_target = target.clone(); + let expected_path = new_target_dir.clone(); + dm.expect_update() + .times(1) + .withf_st(move |initial, target, target_dir| { + Rc::ptr_eq(initial.as_rc(), expected_initial.as_rc()) + && Rc::ptr_eq(target.as_rc(), expected_target.as_rc()) + && target_dir == expected_path.as_str() + }) + .returning(|_, _, _| Ok(None)); + set_download_manager(&setup, dm); + let mut repository = InstalledArrayRepository::new().unwrap(); repository.add_package(initial.clone()).unwrap(); @@ -232,14 +315,6 @@ fn test_update() { ); assert!(!std::path::Path::new(&old_target_dir).exists()); - assert_eq!( - vec![DownloaderCall::Update { - initial: "vendor/package1".to_string(), - target: "vendor/package1".to_string(), - path: new_target_dir.clone(), - }], - *setup.downloader_calls.borrow() - ); assert!(!repository.has_package(initial.clone())); assert!(repository.has_package(target.clone())); @@ -261,9 +336,24 @@ fn test_update() { #[test] fn test_uninstall() { let mut setup = set_up(); + let package = get_package("vendor/pkg", "1.0.0"); + + // PHP asserts the DownloadManager mock's remove() is called once with + // ($package, vendorDir/vendor/pkg). + let mut dm = MockDownloadManager::new(); + let expected_package = package.clone(); + let expected_path = format!("{}/vendor/pkg", setup.vendor_dir); + dm.expect_remove() + .times(1) + .withf_st(move |package, target_dir| { + Rc::ptr_eq(package.as_rc(), expected_package.as_rc()) + && target_dir == expected_path.as_str() + }) + .returning(|_, _| Ok(None)); + set_download_manager(&setup, dm); + let mut library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None); - let package = get_installable_package("vendor/pkg", "1.0.0"); // PHP mocks hasPackage to return (true, false) over two calls; a real repository // seeded with the package reproduces this naturally: present, then absent after @@ -273,13 +363,6 @@ fn test_uninstall() { run(library.uninstall(&mut repository, package.clone())).unwrap(); - assert_eq!( - vec![DownloaderCall::Remove { - package: "vendor/pkg".to_string(), - path: format!("{}/vendor/pkg", setup.vendor_dir), - }], - *setup.downloader_calls.borrow() - ); assert!(!repository.has_package(package.clone())); // Uninstalling again, with the package no longer installed, fails. @@ -323,42 +406,9 @@ 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, -} - -#[derive(Debug)] -struct RecordingBinaryInstaller { - calls: Rc>, -} - -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] fn test_ensure_binaries_installed() { let mut setup = set_up(); - let calls = Rc::new(RefCell::new(BinaryInstallerCalls::default())); let mut library = LibraryInstaller::new( setup.io.clone(), setup.composer.clone(), @@ -366,26 +416,27 @@ fn test_ensure_binaries_installed() { None, None, ); - library.__set_binary_installer(std::rc::Rc::new(std::cell::RefCell::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(); + let mut binary_installer = MockBinaryInstaller::new(); // PHP asserts removeBinaries is never called. - assert!(recorded.remove_binaries.is_empty()); + binary_installer.expect_remove_binaries().times(0); // 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); + let expected_package = package.clone(); + let expected_install_path = expected_path.clone(); + binary_installer + .expect_install_binaries() + .times(1) + .withf_st(move |package, install_path, warn_on_overwrite| { + Rc::ptr_eq(package.as_rc(), expected_package.as_rc()) + && install_path == expected_install_path.as_str() + && !*warn_on_overwrite + }) + .returning(|_, _, _| ()); + library.__set_binary_installer(Rc::new(RefCell::new(binary_installer))); + + library.ensure_binaries_presence(package.clone()); tear_down(&mut setup); } diff --git a/crates/shirabe/tests/installer/main.rs b/crates/shirabe/tests/installer/main.rs index a5ca671..4b42278 100644 --- a/crates/shirabe/tests/installer/main.rs +++ b/crates/shirabe/tests/installer/main.rs @@ -1,5 +1,3 @@ -#[path = "../common/downloader_stub.rs"] -mod downloader_stub; #[path = "../common/io_mock.rs"] mod io_mock; #[path = "../common/test_case.rs"] 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 7fa970a..93ee93a 100644 --- a/crates/shirabe/tests/package/loader/root_package_loader_test.rs +++ b/crates/shirabe/tests/package/loader/root_package_loader_test.rs @@ -4,7 +4,7 @@ // ProcessExecutor / VersionGuesser or require constraints whose parsing goes through a // look-around regex the regex crate cannot compile. -use std::cell::{Cell, RefCell}; +use std::cell::RefCell; use std::rc::Rc; use indexmap::IndexMap; @@ -87,25 +87,17 @@ impl Drop for GitVersionGuard { } // A test double for the concrete VersionGuesser, supplied through the VersionGuesserInterface seam. -#[derive(Debug)] -struct VersionGuesserMock { - version_data: VersionData, - guess_version_calls: Rc>, -} - -impl VersionGuesserInterface for VersionGuesserMock { - fn guess_version( - &mut self, - _package_config: &IndexMap, - _path: &str, - ) -> anyhow::Result> { - 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 { - unreachable!("COMPOSER_ROOT_VERSION is not set in this test") +mockall::mock! { + #[derive(Debug)] + pub VersionGuesser {} + impl VersionGuesserInterface for VersionGuesser { + fn guess_version( + &mut self, + package_config: &IndexMap, + path: &str, + ) -> anyhow::Result>; + + fn get_root_version_from_env(&self) -> anyhow::Result; } } @@ -253,17 +245,19 @@ fn test_pretty_version_for_root_package_in_version_branch() { 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 version_guesser = MockVersionGuesser::new(); + version_guesser + .expect_guess_version() + .times(1..) + .returning(|_, _| { + Ok(Some(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, + })) + }); let mut loader = RootPackageLoader::new( manager, @@ -277,7 +271,6 @@ fn test_pretty_version_for_root_package_in_version_branch() { .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()); } diff --git a/crates/shirabe/tests/package/loader/validating_array_loader_test.rs b/crates/shirabe/tests/package/loader/validating_array_loader_test.rs index 2394af2..aeafefd 100644 --- a/crates/shirabe/tests/package/loader/validating_array_loader_test.rs +++ b/crates/shirabe/tests/package/loader/validating_array_loader_test.rs @@ -1,8 +1,5 @@ //! ref: composer/tests/Composer/Test/Package/Loader/ValidatingArrayLoaderTest.php -use std::cell::RefCell; -use std::rc::Rc; - use indexmap::IndexMap; use shirabe::package::handle::PackageInterfaceHandle; use shirabe::package::loader::{InvalidPackageException, LoaderInterface, ValidatingArrayLoader}; @@ -49,40 +46,29 @@ fn config(entries: Vec<(&str, PhpMixed)>) -> IndexMap { m } -type Calls = Rc>>>; - -/// Mock LoaderInterface recording every `load` invocation, mirroring PHPUnit's -/// `expects($this->once())->method('load')->with(...)`. -#[derive(Debug)] -struct MockLoader { - calls: Calls, -} - -impl MockLoader { - fn new() -> (Self, Calls) { - let calls: Calls = Rc::new(RefCell::new(Vec::new())); - ( - MockLoader { - calls: calls.clone(), - }, - calls, - ) +// PHP mocks `Composer\Package\Loader\LoaderInterface` with getMockBuilder. +mockall::mock! { + #[derive(Debug)] + Loader {} + impl LoaderInterface for Loader { + fn load( + &self, + config: IndexMap, + class: Option, + ) -> anyhow::Result; + fn as_any(&self) -> &dyn std::any::Any; } } -impl LoaderInterface for MockLoader { - fn load( - &self, - config: IndexMap, - _class: Option, - ) -> anyhow::Result { - self.calls.borrow_mut().push(config); - Ok(test_case::get_package("mock/mock", "1.0.0")) - } - - fn as_any(&self) -> &dyn std::any::Any { - self - } +/// Build a mock inner loader whose `load` returns a dummy package, mirroring +/// PHPUnit's `getMockBuilder(LoaderInterface::class)->getMock()` with no +/// configured expectations. +fn mock_loader() -> MockLoader { + let mut loader = MockLoader::new(); + loader + .expect_load() + .returning(|_, _| Ok(test_case::get_package("mock/mock", "1.0.0"))); + loader } fn invalid_naming_error(name: &str) -> Vec { @@ -369,7 +355,7 @@ fn success_provider() -> Vec> { #[test] fn test_load_success() { for cfg in success_provider() { - let (internal_loader, _calls) = MockLoader::new(); + let internal_loader = mock_loader(); let mut loader = ValidatingArrayLoader::new( Box::new(internal_loader), true, @@ -796,7 +782,7 @@ fn error_provider() -> Vec<(IndexMap, Vec)> { #[test] fn test_load_failure_throws_exception() { for (cfg, mut expected_errors) in error_provider() { - let (internal_loader, _calls) = MockLoader::new(); + let internal_loader = mock_loader(); let mut loader = ValidatingArrayLoader::new( Box::new(internal_loader), true, @@ -978,7 +964,7 @@ fn warning_provider() -> Vec<( #[test] fn test_load_warnings() { for (cfg, mut expected_warnings, _must_check, _expected_array) in warning_provider() { - let (internal_loader, _calls) = MockLoader::new(); + let internal_loader = mock_loader(); let mut loader = ValidatingArrayLoader::new( Box::new(internal_loader), true, @@ -1003,9 +989,16 @@ fn test_load_skips_warning_data_when_ignoring_errors() { if !must_check { continue; } - let (internal_loader, calls) = MockLoader::new(); let expected = expected_array.unwrap_or_else(|| config(vec![("name", s("a/b"))])); + // The inner loader is called exactly once with the (post-validation) config. + let mut internal_loader = MockLoader::new(); + internal_loader + .expect_load() + .times(1) + .withf(move |cfg, _class| *cfg == expected) + .returning(|_, _| Ok(test_case::get_package("mock/mock", "1.0.0"))); + let mut loader = ValidatingArrayLoader::new( Box::new(internal_loader), true, @@ -1016,10 +1009,5 @@ fn test_load_skips_warning_data_when_ignoring_errors() { loader .load(cfg, "Composer\\Package\\CompletePackage") .unwrap(); - - // The mock recorded exactly the (post-validation) config passed to the inner loader. - let recorded = calls.borrow(); - assert_eq!(recorded.len(), 1); - assert_eq!(recorded[0], expected); } } diff --git a/crates/shirabe/tests/repository/vcs/github_driver_test.rs b/crates/shirabe/tests/repository/vcs/github_driver_test.rs index 4bfb608..f1084f5 100644 --- a/crates/shirabe/tests/repository/vcs/github_driver_test.rs +++ b/crates/shirabe/tests/repository/vcs/github_driver_test.rs @@ -62,61 +62,45 @@ impl Drop for TearDown { } } -// No-op ConfigSourceInterface, equivalent to PHPUnit's -// `getMockBuilder(ConfigSourceInterface::class)->getMock()` whose methods all do -// nothing. Used by testPrivateRepository to satisfy the OAuth token-storing path. -#[derive(Debug)] -struct NullConfigSource; - -impl ConfigSourceInterface for NullConfigSource { - fn add_repository( - &mut self, - _name: &str, - _config: PhpMixed, - _append: bool, - ) -> anyhow::Result<()> { - Ok(()) - } - fn insert_repository( - &mut self, - _name: &str, - _config: PhpMixed, - _reference_name: &str, - _offset: i64, - ) -> anyhow::Result<()> { - Ok(()) - } - fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> { - Ok(()) - } - fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> { - Ok(()) - } - fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { - Ok(()) - } - fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> { - Ok(()) - } - fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> { - Ok(()) - } - fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> { - Ok(()) - } - fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()> { - let _ = (r#type, name, value); - Ok(()) - } - fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()> { - let _ = (r#type, name); - Ok(()) - } - fn get_name(&self) -> String { - String::new() +// PHP mocks `Composer\Config\ConfigSourceInterface` with getMockBuilder and sets +// no expectations (a passive mock whose methods all do nothing). Used by +// testPrivateRepository to satisfy the OAuth token-storing path. +mockall::mock! { + #[derive(Debug)] + pub ConfigSource {} + impl ConfigSourceInterface for ConfigSource { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; } } +// A passive ConfigSource mock: every method is stubbed to do nothing, mirroring +// PHPUnit's `getMockBuilder(...)->getMock()` whose stubbed methods return null. +fn null_config_source() -> MockConfigSource { + let mut m = MockConfigSource::new(); + m.expect_add_repository().returning(|_, _, _| Ok(())); + m.expect_insert_repository().returning(|_, _, _, _| Ok(())); + m.expect_set_repository_url().returning(|_, _| Ok(())); + m.expect_remove_repository().returning(|_| Ok(())); + m.expect_add_config_setting().returning(|_, _| Ok(())); + m.expect_remove_config_setting().returning(|_| Ok(())); + m.expect_add_property().returning(|_, _| Ok(())); + m.expect_remove_property().returning(|_| Ok(())); + m.expect_add_link().returning(|_, _, _| Ok(())); + m.expect_remove_link().returning(|_, _| Ok(())); + m.expect_get_name().returning(String::new); + m +} + // Mirrors PHP's $httpDownloader->expects([...], true): an `['url' => .., 'body' => ..]` // entry (status defaults to 200). fn http_body( @@ -208,10 +192,10 @@ fn test_private_repository() { config .borrow_mut() - .set_config_source(Box::new(NullConfigSource)); + .set_config_source(Box::new(null_config_source())); config .borrow_mut() - .set_auth_config_source(Box::new(NullConfigSource)); + .set_auth_config_source(Box::new(null_config_source())); let mut repo_config: IndexMap = IndexMap::new(); repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string())); diff --git a/crates/shirabe/tests/util/auth_helper_test.rs b/crates/shirabe/tests/util/auth_helper_test.rs index b760fc8..7738afb 100644 --- a/crates/shirabe/tests/util/auth_helper_test.rs +++ b/crates/shirabe/tests/util/auth_helper_test.rs @@ -399,59 +399,23 @@ fn test_is_public_bit_bucket_download_with_non_bitbucket_public_url() { ); } -// Records addConfigSetting calls and serves a configurable getName, mirroring the -// PHPUnit mock of ConfigSourceInterface used by the storeAuth tests. -#[derive(Debug)] -struct ConfigSourceMock { - name: String, - added: Rc>>, -} - -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<()> { - self.added.borrow_mut().push((name.to_string(), value)); - Ok(()) - } - fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> { - unreachable!() - } - 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() +// Mirrors the PHPUnit mock of ConfigSourceInterface used by the storeAuth tests. +// Methods left without an expectation panic if called. +mockall::mock! { + #[derive(Debug)] + pub ConfigSource {} + impl ConfigSourceInterface for ConfigSource { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; } } @@ -474,25 +438,23 @@ fn test_store_auth_automatically() { let origin = "github.com"; expects_authentication(&f.io, origin, "my_username", "my_password"); - let added = Rc::new(RefCell::new(Vec::new())); + let mut source = MockConfigSource::new(); + source + .expect_get_name() + .returning(|| "https://api.gitlab.com/source".to_string()); + let expected = expected_auth_setting("my_username", "my_password"); + source + .expect_add_config_setting() + .times(1) + .withf(move |name, value| name == "http-basic.github.com" && *value == expected) + .returning(|_, _| Ok(())); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: "https://api.gitlab.com/source".to_string(), - added: added.clone(), - })); + .set_auth_config_source(Box::new(source)); f.auth_helper .store_auth(origin, StoreAuth::Bool(true)) .unwrap(); - - assert_eq!( - *added.borrow(), - vec![( - "http-basic.github.com".to_string(), - expected_auth_setting("my_username", "my_password"), - )] - ); } #[test] @@ -515,23 +477,22 @@ fn test_store_auth_with_prompt_yes_answer() { ) .unwrap(); - let added = Rc::new(RefCell::new(Vec::new())); + let mut source = MockConfigSource::new(); + source + .expect_get_name() + .times(1) + .returning(move || config_source_name.to_string()); + let expected = expected_auth_setting("my_username", "my_password"); + source + .expect_add_config_setting() + .times(1) + .withf(move |name, value| name == "http-basic.github.com" && *value == expected) + .returning(|_, _| Ok(())); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: config_source_name.to_string(), - added: added.clone(), - })); + .set_auth_config_source(Box::new(source)); f.auth_helper.store_auth(origin, StoreAuth::Prompt).unwrap(); - - assert_eq!( - *added.borrow(), - vec![( - "http-basic.github.com".to_string(), - expected_auth_setting("my_username", "my_password"), - )] - ); } #[test] @@ -553,17 +514,17 @@ fn test_store_auth_with_prompt_no_answer() { ) .unwrap(); - let added = Rc::new(RefCell::new(Vec::new())); + let mut source = MockConfigSource::new(); + source + .expect_get_name() + .times(1) + .returning(move || config_source_name.to_string()); + source.expect_add_config_setting().times(0); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: config_source_name.to_string(), - added: added.clone(), - })); + .set_auth_config_source(Box::new(source)); f.auth_helper.store_auth(origin, StoreAuth::Prompt).unwrap(); - - assert!(added.borrow().is_empty()); } #[test] diff --git a/crates/shirabe/tests/util/bitbucket_test.rs b/crates/shirabe/tests/util/bitbucket_test.rs index d57a49a..2817266 100644 --- a/crates/shirabe/tests/util/bitbucket_test.rs +++ b/crates/shirabe/tests/util/bitbucket_test.rs @@ -27,70 +27,35 @@ const MESSAGE: &str = "mymessage"; const ORIGIN: &str = "bitbucket.org"; const TOKEN: &str = "bitbuckettoken"; -// Records add/removeConfigSetting calls and serves a configurable getName, mirroring -// the PHPUnit mock of ConfigSourceInterface used throughout BitbucketTest. -#[derive(Debug, Clone, Default)] -struct ConfigSourceCalls { - added: Vec<(String, PhpMixed)>, - removed: Vec, -} - -#[derive(Debug)] -struct ConfigSourceMock { - name: String, - calls: Rc>, +// Mirrors the PHPUnit mock of ConfigSourceInterface used throughout BitbucketTest. +// Methods left without an expectation panic if called. +mockall::mock! { + #[derive(Debug)] + pub ConfigSource {} + impl ConfigSourceInterface for ConfigSource { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; + } } -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<()> { - self.calls - .borrow_mut() - .added - .push((name.to_string(), value)); - Ok(()) - } - fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()> { - self.calls.borrow_mut().removed.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() - } +// A bare auth config source placeholder: getAuthConfigSource() is consulted (e.g. while +// printing instructions), but no add/removeConfigSetting expectations are asserted. +fn placeholder_auth_config_source() -> Box { + let mut mock = MockConfigSource::new(); + mock.expect_get_name() + .returning(|| "auth-config-source".to_string()); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_remove_config_setting().returning(|_| Ok(())); + Box::new(mock) } // Mirrors BitbucketTest::setUp: a DEBUG-verbosity IOMock, a mocked HttpDownloader, a @@ -160,34 +125,50 @@ fn access_token_body() -> String { ) } -// Installs recording config/auth config sources and returns their shared call logs, -// mirroring BitbucketTest::setExpectationsForStoringAccessToken. -struct StoreAccessTokenMocks { - config_source: Rc>, - auth_config_source: Rc>, -} - +// Installs config/auth config source mocks with the access-token storing expectations, +// mirroring BitbucketTest::setExpectationsForStoringAccessToken. Verification happens +// when the mocks are dropped together with the Config. fn set_expectations_for_storing_access_token( config: &Rc>, -) -> StoreAccessTokenMocks { - let config_source = Rc::new(RefCell::new(ConfigSourceCalls::default())); - let auth_config_source = Rc::new(RefCell::new(ConfigSourceCalls::default())); + time: i64, + remove_basic_auth: bool, +) { + let mut config_source = MockConfigSource::new(); + config_source + .expect_get_name() + .returning(|| "config-source".to_string()); + let removed_key = format!("bitbucket-oauth.{}", ORIGIN); + config_source + .expect_remove_config_setting() + .times(1) + .withf(move |name| name == removed_key) + .returning(|_| Ok(())); config .borrow_mut() - .set_config_source(Box::new(ConfigSourceMock { - name: "config-source".to_string(), - calls: config_source.clone(), - })); + .set_config_source(Box::new(config_source)); + + let mut auth_config_source = MockConfigSource::new(); + auth_config_source + .expect_get_name() + .returning(|| "auth-config-source".to_string()); + let added_key = format!("bitbucket-oauth.{}", ORIGIN); + let expected_value = expected_stored_token(time); + auth_config_source + .expect_add_config_setting() + .times(1) + .withf(move |name, value| name == added_key && *value == expected_value) + .returning(|_, _| Ok(())); + if remove_basic_auth { + let basic_key = format!("http-basic.{}", ORIGIN); + auth_config_source + .expect_remove_config_setting() + .times(1) + .withf(move |name| name == basic_key) + .returning(|_| Ok(())); + } config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: "auth-config-source".to_string(), - calls: auth_config_source.clone(), - })); - StoreAccessTokenMocks { - config_source, - auth_config_source, - } + .set_auth_config_source(Box::new(auth_config_source)); } fn expected_stored_token(time: i64) -> PhpMixed { @@ -211,26 +192,6 @@ fn expected_stored_token(time: i64) -> PhpMixed { PhpMixed::Array(consumer) } -fn assert_stored_access_token(mocks: &StoreAccessTokenMocks, time: i64, remove_basic_auth: bool) { - assert_eq!( - mocks.config_source.borrow().removed, - vec![format!("bitbucket-oauth.{}", ORIGIN)], - ); - assert_eq!( - mocks.auth_config_source.borrow().added, - vec![( - format!("bitbucket-oauth.{}", ORIGIN), - expected_stored_token(time) - )], - ); - if remove_basic_auth { - assert_eq!( - mocks.auth_config_source.borrow().removed, - vec![format!("http-basic.{}", ORIGIN)], - ); - } -} - #[test] fn test_request_access_token_with_valid_oauth_consumer() { let config = ConfigStubBuilder::new().build_shared(); @@ -256,7 +217,7 @@ fn test_request_access_token_with_valid_oauth_consumer() { ) .unwrap(); - let mocks = set_expectations_for_storing_access_token(&f.config); + set_expectations_for_storing_access_token(&f.config, f.time, false); assert_eq!( f.bitbucket @@ -264,8 +225,6 @@ fn test_request_access_token_with_valid_oauth_consumer() { .unwrap(), TOKEN ); - - assert_stored_access_token(&mocks, f.time, false); } #[test] @@ -357,7 +316,7 @@ fn test_request_access_token_with_valid_oauth_consumer_and_expired_access_token( ) .unwrap(); - let mocks = set_expectations_for_storing_access_token(&f.config); + set_expectations_for_storing_access_token(&f.config, f.time, false); assert_eq!( f.bitbucket @@ -365,8 +324,6 @@ fn test_request_access_token_with_valid_oauth_consumer_and_expired_access_token( .unwrap(), TOKEN ); - - assert_stored_access_token(&mocks, f.time, false); } #[test] @@ -505,15 +462,13 @@ fn test_username_password_authentication_flow() { ) .unwrap(); - let mocks = set_expectations_for_storing_access_token(&f.config); + set_expectations_for_storing_access_token(&f.config, f.time, true); assert!( f.bitbucket .authorize_oauth_interactively(ORIGIN, Some(MESSAGE)) .unwrap() ); - - assert_stored_access_token(&mocks, f.time, true); } #[test] @@ -522,13 +477,9 @@ fn test_authorize_oauth_interactively_with_empty_username() { let mut f = set_up_with_config_and_http(config, vec![]); // getAuthConfigSource() is consulted while printing the instructions. - let auth_calls = Rc::new(RefCell::new(ConfigSourceCalls::default())); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: "auth-config-source".to_string(), - calls: auth_calls, - })); + .set_auth_config_source(placeholder_auth_config_source()); f.io.borrow_mut() .expects(vec![Expectation::ask("Consumer Key (hidden): ", "")], false) @@ -546,13 +497,9 @@ fn test_authorize_oauth_interactively_with_empty_password() { let config = ConfigStubBuilder::new().build_shared(); let mut f = set_up_with_config_and_http(config, vec![]); - let auth_calls = Rc::new(RefCell::new(ConfigSourceCalls::default())); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: "auth-config-source".to_string(), - calls: auth_calls, - })); + .set_auth_config_source(placeholder_auth_config_source()); f.io.borrow_mut() .expects( @@ -579,13 +526,9 @@ fn test_authorize_oauth_interactively_with_request_access_token_failure() { // A 400 status makes the mocked HttpDownloader raise a TransportException(400). let mut f = set_up_with_config_and_http(config, vec![expect_full(url, None, 400, "", vec![])]); - let auth_calls = Rc::new(RefCell::new(ConfigSourceCalls::default())); f.config .borrow_mut() - .set_auth_config_source(Box::new(ConfigSourceMock { - name: "auth-config-source".to_string(), - calls: auth_calls, - })); + .set_auth_config_source(placeholder_auth_config_source()); f.io.borrow_mut() .expects( diff --git a/crates/shirabe/tests/util/forgejo_test.rs b/crates/shirabe/tests/util/forgejo_test.rs index fd6dbc9..a1a8f9c 100644 --- a/crates/shirabe/tests/util/forgejo_test.rs +++ b/crates/shirabe/tests/util/forgejo_test.rs @@ -19,74 +19,49 @@ const ACCESS_TOKEN: &str = "access-token"; const MESSAGE: &str = "mymessage"; const ORIGIN: &str = "codeberg.org"; -// Records the config setting names a source has had removed, plus a fixed getName, -// mirroring ForgejoTest's JsonConfigSource mocks (getName -> "auth.json", and the -// config source stubbing removeConfigSetting('forgejo-token.')). -#[derive(Debug)] -struct ConfigSourceMock { - name: String, - removed: Rc>>, +// Mirrors ForgejoTest's JsonConfigSource mocks. Methods left without an expectation +// panic if called. +mockall::mock! { + #[derive(Debug)] + pub ConfigSource {} + impl ConfigSourceInterface for ConfigSource { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; + } } -impl ConfigSourceMock { - fn new(name: &str) -> (Box, Rc>>) { - let removed = Rc::new(RefCell::new(Vec::new())); - ( - Box::new(Self { - name: name.to_string(), - removed: removed.clone(), - }), - removed, - ) - } +// getAuthJsonMock: getName atLeastOnce -> "auth.json". +fn get_auth_json_mock() -> Box { + let mut mock = MockConfigSource::new(); + mock.expect_get_name() + .times(1..) + .returning(|| "auth.json".to_string()); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_remove_config_setting().returning(|_| Ok(())); + Box::new(mock) } -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() - } +// getConfJsonMock: removeConfigSetting atLeastOnce with('forgejo-token.'). +fn get_conf_json_mock(origin: &str) -> Box { + let mut mock = MockConfigSource::new(); + let expected = format!("forgejo-token.{}", origin); + mock.expect_remove_config_setting() + .times(1..) + .withf(move |name| name == expected) + .returning(|_| Ok(())); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_get_name() + .returning(|| "config.json".to_string()); + Box::new(mock) } fn build_forgejo( @@ -126,10 +101,12 @@ fn test_username_password_authentication_flow() { ); let config = ConfigStubBuilder::new().build_shared(); - 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); + config + .borrow_mut() + .set_auth_config_source(get_auth_json_mock()); + config + .borrow_mut() + .set_config_source(get_conf_json_mock(ORIGIN)); let mut forgejo = build_forgejo(&io_mock, config, http_downloader); @@ -139,10 +116,7 @@ fn test_username_password_authentication_flow() { .unwrap() .unwrap() ); - assert_eq!( - *conf_removed.borrow(), - vec![format!("forgejo-token.{}", ORIGIN)] - ); + // removeConfigSetting('forgejo-token.') verified on the conf source mock drop. } #[test] @@ -172,8 +146,9 @@ fn test_username_password_failure() { ); let config = ConfigStubBuilder::new().build_shared(); - let (auth_source, _) = ConfigSourceMock::new("auth.json"); - config.borrow_mut().set_auth_config_source(auth_source); + config + .borrow_mut() + .set_auth_config_source(get_auth_json_mock()); let mut forgejo = build_forgejo(&io_mock, config, http_downloader); diff --git a/crates/shirabe/tests/util/github_test.rs b/crates/shirabe/tests/util/github_test.rs index 10b5dd7..0069a9c 100644 --- a/crates/shirabe/tests/util/github_test.rs +++ b/crates/shirabe/tests/util/github_test.rs @@ -18,74 +18,49 @@ 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.')). -#[derive(Debug)] -struct ConfigSourceMock { - name: String, - removed: Rc>>, +// Mirrors GitHubTest's JsonConfigSource mocks. Methods left without an expectation +// panic if called. +mockall::mock! { + #[derive(Debug)] + pub ConfigSource {} + impl ConfigSourceInterface for ConfigSource { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; + } } -impl ConfigSourceMock { - fn new(name: &str) -> (Box, Rc>>) { - let removed = Rc::new(RefCell::new(Vec::new())); - ( - Box::new(Self { - name: name.to_string(), - removed: removed.clone(), - }), - removed, - ) - } +// getAuthJsonMock: getName atLeastOnce -> "auth.json". +fn get_auth_json_mock() -> Box { + let mut mock = MockConfigSource::new(); + mock.expect_get_name() + .times(1..) + .returning(|| "auth.json".to_string()); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_remove_config_setting().returning(|_| Ok(())); + Box::new(mock) } -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() - } +// getConfJsonMock: removeConfigSetting atLeastOnce with('github-oauth.'). +fn get_conf_json_mock(origin: &str) -> Box { + let mut mock = MockConfigSource::new(); + let expected = format!("github-oauth.{}", origin); + mock.expect_remove_config_setting() + .times(1..) + .withf(move |name| name == expected) + .returning(|_| Ok(())); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_get_name() + .returning(|| "config.json".to_string()); + Box::new(mock) } fn build_github( @@ -133,10 +108,12 @@ fn test_username_password_authentication_flow() { ); 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); + config + .borrow_mut() + .set_auth_config_source(get_auth_json_mock()); + config + .borrow_mut() + .set_config_source(get_conf_json_mock(ORIGIN)); let mut github = build_github(&io_mock, config, http_downloader); @@ -145,10 +122,7 @@ fn test_username_password_authentication_flow() { .authorize_oauth_interactively(ORIGIN, Some(MESSAGE)) .unwrap() ); - assert_eq!( - *conf_removed.borrow(), - vec![format!("github-oauth.{}", ORIGIN)] - ); + // removeConfigSetting('github-oauth.') verified on the conf source mock drop. } #[test] @@ -172,8 +146,9 @@ fn test_username_password_failure() { ); let config = build_config(); - let (auth_source, _) = ConfigSourceMock::new("auth.json"); - config.borrow_mut().set_auth_config_source(auth_source); + config + .borrow_mut() + .set_auth_config_source(get_auth_json_mock()); let mut github = build_github(&io_mock, config, http_downloader); diff --git a/crates/shirabe/tests/util/gitlab_test.rs b/crates/shirabe/tests/util/gitlab_test.rs index ebf7c08..a3db85b 100644 --- a/crates/shirabe/tests/util/gitlab_test.rs +++ b/crates/shirabe/tests/util/gitlab_test.rs @@ -22,61 +22,40 @@ const TOKEN: &str = "gitlabtoken"; const REFRESHTOKEN: &str = "gitlabrefreshtoken"; // Mirrors GitLabTest::getAuthJsonMock: a JsonConfigSource whose getName() returns -// "auth.json" and whose addConfigSetting is a no-op (the PHP mock never stubs it). -#[derive(Debug)] -struct AuthJsonMock; - -impl ConfigSourceInterface for AuthJsonMock { - 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<()> { - 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 { - "auth.json".to_string() +// "auth.json" (atLeastOnce) and whose addConfigSetting is a no-op (the PHP mock +// never stubs it). Methods left without an expectation panic if called. +mockall::mock! { + #[derive(Debug)] + pub AuthJson {} + impl ConfigSourceInterface for AuthJson { + fn add_repository(&mut self, name: &str, config: PhpMixed, append: bool) -> anyhow::Result<()>; + fn insert_repository(&mut self, name: &str, config: PhpMixed, reference_name: &str, offset: i64) -> anyhow::Result<()>; + fn set_repository_url(&mut self, name: &str, url: &str) -> anyhow::Result<()>; + fn remove_repository(&mut self, name: &str) -> anyhow::Result<()>; + fn add_config_setting(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_config_setting(&mut self, name: &str) -> anyhow::Result<()>; + fn add_property(&mut self, name: &str, value: PhpMixed) -> anyhow::Result<()>; + fn remove_property(&mut self, name: &str) -> anyhow::Result<()>; + fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()>; + fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()>; + fn get_name(&self) -> String; } } +fn get_auth_json_mock() -> Box { + let mut mock = MockAuthJson::new(); + mock.expect_get_name() + .times(1..) + .returning(|| "auth.json".to_string()); + mock.expect_add_config_setting().returning(|_, _| Ok(())); + mock.expect_remove_config_setting().returning(|_| Ok(())); + Box::new(mock) +} + fn set_up(io_mock: &Rc>, config: &Rc>) { config .borrow_mut() - .set_auth_config_source(Box::new(AuthJsonMock)); + .set_auth_config_source(get_auth_json_mock()); let _ = io_mock; } -- cgit v1.3.1