aboutsummaryrefslogtreecommitdiffhomepage
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
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>
-rw-r--r--crates/shirabe/tests/command/archive_command_test.rs260
-rw-r--r--crates/shirabe/tests/common/downloader_stub.rs129
-rw-r--r--crates/shirabe/tests/downloader/download_manager_test.rs3
-rw-r--r--crates/shirabe/tests/downloader/main.rs2
-rw-r--r--crates/shirabe/tests/installer/installation_manager_test.rs195
-rw-r--r--crates/shirabe/tests/installer/library_installer_test.rs265
-rw-r--r--crates/shirabe/tests/installer/main.rs2
-rw-r--r--crates/shirabe/tests/package/loader/root_package_loader_test.rs55
-rw-r--r--crates/shirabe/tests/package/loader/validating_array_loader_test.rs74
-rw-r--r--crates/shirabe/tests/repository/vcs/github_driver_test.rs92
-rw-r--r--crates/shirabe/tests/util/auth_helper_test.rs133
-rw-r--r--crates/shirabe/tests/util/bitbucket_test.rs199
-rw-r--r--crates/shirabe/tests/util/forgejo_test.rs123
-rw-r--r--crates/shirabe/tests/util/github_test.rs123
-rw-r--r--crates/shirabe/tests/util/gitlab_test.rs77
15 files changed, 729 insertions, 1003 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);
}
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<RefCell<Vec<DownloaderCall>>>,
-}
-
-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<RefCell<Vec<DownloaderCall>>> {
- 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<PackageInterfaceHandle>,
- _output: bool,
- ) -> anyhow::Result<Option<PhpMixed>> {
- Ok(None)
- }
-
- async fn prepare(
- &mut self,
- _type: &str,
- _package: PackageInterfaceHandle,
- _path: &str,
- _prev_package: Option<PackageInterfaceHandle>,
- ) -> anyhow::Result<Option<PhpMixed>> {
- Ok(None)
- }
-
- async fn install(
- &mut self,
- package: PackageInterfaceHandle,
- path: &str,
- _output: bool,
- ) -> anyhow::Result<Option<PhpMixed>> {
- 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<Option<PhpMixed>> {
- 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<Option<PhpMixed>> {
- 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<PackageInterfaceHandle>,
- ) -> anyhow::Result<Option<PhpMixed>> {
- 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<RefCell<dyn DownloaderInterface>> {
- Rc::new(RefCell::new(DownloaderStub::new())) as Rc<RefCell<dyn DownloaderInterface>>
+ 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<String>,
- install: Vec<PackageInterfaceHandle>,
- uninstall: Vec<PackageInterfaceHandle>,
- 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<RefCell<InstallerCalls>>,
-}
-
-impl CountingInstaller {
- fn new(supported_type: &str) -> (Self, Rc<RefCell<InstallerCalls>>) {
- 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<Option<PhpMixed>>;
+ fn update(
+ &mut self,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ fn uninstall(
+ &mut self,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<PhpMixed>>;
}
}
#[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<Option<PhpMixed>> {
- 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<Option<PhpMixed>> {
- 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<Option<PhpMixed>> {
- 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<dyn InstallerInterface> 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<Option<Rc<RefCell<dyn DownloaderInterface>>>>;
+ async fn download(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn prepare(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn install(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn update(
+ &self,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn remove(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn cleanup(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ }
+}
+
+// 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<F: std::future::Future>(future: F) -> F::Output {
tokio::runtime::Builder::new_current_thread()
.build()
@@ -31,10 +99,10 @@ fn run<F: std::future::Future>(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<RefCell<dyn IOInterface>>,
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<RefCell<Vec<DownloaderCall>>>,
- _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<RefCell<dyn IOInterface>> = 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<shirabe::package::PackageInterfaceHandle>,
-}
-
-#[derive(Debug)]
-struct RecordingBinaryInstaller {
- calls: Rc<RefCell<BinaryInstallerCalls>>,
-}
-
-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<Cell<u32>>,
-}
-
-impl VersionGuesserInterface for VersionGuesserMock {
- fn guess_version(
- &mut self,
- _package_config: &IndexMap<String, PhpMixed>,
- _path: &str,
- ) -> anyhow::Result<Option<VersionData>> {
- self.guess_version_calls
- .set(self.guess_version_calls.get() + 1);
- Ok(Some(self.version_data.clone()))
- }
+mockall::mock! {
+ #[derive(Debug)]
+ pub VersionGuesser {}
+ impl VersionGuesserInterface for VersionGuesser {
+ fn guess_version(
+ &mut self,
+ package_config: &IndexMap<String, PhpMixed>,
+ path: &str,
+ ) -> anyhow::Result<Option<VersionData>>;
- fn get_root_version_from_env(&self) -> anyhow::Result<String> {
- unreachable!("COMPOSER_ROOT_VERSION is not set in this test")
+ fn get_root_version_from_env(&self) -> anyhow::Result<String>;
}
}
@@ -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<String, PhpMixed> {
m
}
-type Calls = Rc<RefCell<Vec<IndexMap<String, PhpMixed>>>>;
-
-/// 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<String, PhpMixed>,
+ class: Option<String>,
+ ) -> anyhow::Result<PackageInterfaceHandle>;
+ fn as_any(&self) -> &dyn std::any::Any;
}
}
-impl LoaderInterface for MockLoader {
- fn load(
- &self,
- config: IndexMap<String, PhpMixed>,
- _class: Option<String>,
- ) -> anyhow::Result<PackageInterfaceHandle> {
- 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<String> {
@@ -369,7 +355,7 @@ fn success_provider() -> Vec<IndexMap<String, PhpMixed>> {
#[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<String, PhpMixed>, Vec<String>)> {
#[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<String, PhpMixed> = 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<RefCell<Vec<(String, PhpMixed)>>>,
-}
-
-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<String>,
+// 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;
+ }
}
-#[derive(Debug)]
-struct ConfigSourceMock {
- name: String,
- calls: Rc<RefCell<ConfigSourceCalls>>,
-}
-
-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<MockConfigSource> {
+ 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<RefCell<ConfigSourceCalls>>,
- auth_config_source: Rc<RefCell<ConfigSourceCalls>>,
-}
-
+// 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<RefCell<Config>>,
-) -> 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.<origin>')).
-#[derive(Debug)]
-struct ConfigSourceMock {
- name: String,
- removed: Rc<RefCell<Vec<String>>>,
+// 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<Self>, Rc<RefCell<Vec<String>>>) {
- 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<MockConfigSource> {
+ 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.<origin>').
+fn get_conf_json_mock(origin: &str) -> Box<MockConfigSource> {
+ 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.<origin>') 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.<origin>')).
-#[derive(Debug)]
-struct ConfigSourceMock {
- name: String,
- removed: Rc<RefCell<Vec<String>>>,
+// 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<Self>, Rc<RefCell<Vec<String>>>) {
- 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<MockConfigSource> {
+ 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.<origin>').
+fn get_conf_json_mock(origin: &str) -> Box<MockConfigSource> {
+ 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.<origin>') 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<MockAuthJson> {
+ 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<RefCell<IOMock>>, config: &Rc<RefCell<Config>>) {
config
.borrow_mut()
- .set_auth_config_source(Box::new(AuthJsonMock));
+ .set_auth_config_source(get_auth_json_mock());
let _ = io_mock;
}