diff options
Diffstat (limited to 'crates/shirabe/tests/common')
| -rw-r--r-- | crates/shirabe/tests/common/config_stub.rs | 77 | ||||
| -rw-r--r-- | crates/shirabe/tests/common/http_downloader_mock.rs | 77 | ||||
| -rw-r--r-- | crates/shirabe/tests/common/io_stub.rs | 211 |
3 files changed, 365 insertions, 0 deletions
diff --git a/crates/shirabe/tests/common/config_stub.rs b/crates/shirabe/tests/common/config_stub.rs new file mode 100644 index 0000000..7566b89 --- /dev/null +++ b/crates/shirabe/tests/common/config_stub.rs @@ -0,0 +1,77 @@ +//! Configurable Config stub, equivalent to PHPUnit's +//! `getMockBuilder(Config::class)` where `get('x')` is stubbed via +//! `->method('get')->willReturn(y)`. +//! +//! `Config` is a concrete (non-trait) struct, so rather than mocking it we build a +//! real `Config` and seed the requested keys through `Config::merge`, which is how +//! `get('key')` actually resolves its value. This keeps the stub faithful to the +//! real resolution logic instead of intercepting `get`. +#![allow(dead_code)] + +use std::cell::RefCell; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe_php_shim::PhpMixed; + +pub struct ConfigStubBuilder { + use_environment: bool, + base_dir: Option<String>, + // Config values to seed, applied via Config::merge under the `config` section. + values: IndexMap<String, PhpMixed>, + source: String, +} + +impl Default for ConfigStubBuilder { + fn default() -> Self { + Self { + use_environment: false, + base_dir: None, + values: IndexMap::new(), + source: Config::SOURCE_UNKNOWN.to_string(), + } + } +} + +impl ConfigStubBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn use_environment(mut self, value: bool) -> Self { + self.use_environment = value; + self + } + + pub fn base_dir(mut self, base_dir: impl Into<String>) -> Self { + self.base_dir = Some(base_dir.into()); + self + } + + /// Sets the value `Config::get(key)` will return. Equivalent to PHPUnit's + /// `->method('get')->with(key)->willReturn(value)`. + pub fn with(mut self, key: impl Into<String>, value: PhpMixed) -> Self { + self.values.insert(key.into(), value); + self + } + + pub fn build(self) -> Config { + let mut config = Config::new(self.use_environment, self.base_dir); + if !self.values.is_empty() { + let mut merge: IndexMap<String, PhpMixed> = IndexMap::new(); + merge.insert("config".to_string(), PhpMixed::Array(self.values)); + config.merge(&merge, &self.source); + } + config + } + + pub fn build_shared(self) -> Rc<RefCell<Config>> { + Rc::new(RefCell::new(self.build())) + } +} + +// For testing only. Convenience for the common case of a default `Config(false)`. +pub fn get_config_stub() -> Config { + ConfigStubBuilder::new().build() +} diff --git a/crates/shirabe/tests/common/http_downloader_mock.rs b/crates/shirabe/tests/common/http_downloader_mock.rs new file mode 100644 index 0000000..249585c --- /dev/null +++ b/crates/shirabe/tests/common/http_downloader_mock.rs @@ -0,0 +1,77 @@ +//! ref: composer/tests/Composer/Test/Mock/HttpDownloaderMock.php +#![allow(dead_code)] + +use std::cell::RefCell; +use std::rc::Rc; + +use indexmap::IndexMap; +use shirabe::config::Config; +use shirabe::io::IOInterface; +use shirabe::io::null_io::NullIO; +use shirabe::util::http_downloader::{ + HttpDownloader, HttpDownloaderMockExpectation, HttpDownloaderMockHandler, +}; +use shirabe_php_shim::PhpMixed; + +// A single HTTP request expectation as written in the PHP tests: a `url` plus an +// optional response (`status`/`body`/`headers`). `options` of `None` matches any +// options; `Some(..)` requires an exact match against the executed options. +pub fn expect(url: impl Into<String>) -> HttpDownloaderMockExpectation { + HttpDownloaderMockExpectation { + url: url.into(), + options: None, + status: 200, + body: String::new(), + headers: vec![String::new()], + } +} + +pub fn expect_full( + url: impl Into<String>, + options: Option<IndexMap<String, PhpMixed>>, + status: i64, + body: impl Into<String>, + headers: Vec<String>, +) -> HttpDownloaderMockExpectation { + HttpDownloaderMockExpectation { + url: url.into(), + options, + status, + body: body.into(), + headers, + } +} + +pub struct HttpDownloaderMockGuard(Rc<RefCell<HttpDownloader>>); + +impl Drop for HttpDownloaderMockGuard { + fn drop(&mut self) { + // Avoid aborting on a double panic when a test assertion is already unwinding. + if std::thread::panicking() { + return; + } + self.0.borrow().__assert_complete(); + } +} + +// For testing only. Mirrors TestCase::getHttpDownloaderMock: returns a shared +// HttpDownloader handle configured with the given expectations, plus a guard that +// runs `__assert_complete` when it drops at the end of the test scope. +pub fn get_http_downloader_mock( + expectations: Vec<HttpDownloaderMockExpectation>, + strict: bool, + default_handler: HttpDownloaderMockHandler, +) -> (Rc<RefCell<HttpDownloader>>, HttpDownloaderMockGuard) { + let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new())); + let config = Rc::new(RefCell::new(Config::new(false, None))); + let downloader = Rc::new(RefCell::new(HttpDownloader::new( + io, + config, + IndexMap::new(), + false, + ))); + downloader + .borrow_mut() + .__expects(expectations, strict, default_handler); + (downloader.clone(), HttpDownloaderMockGuard(downloader)) +} diff --git a/crates/shirabe/tests/common/io_stub.rs b/crates/shirabe/tests/common/io_stub.rs new file mode 100644 index 0000000..9318d45 --- /dev/null +++ b/crates/shirabe/tests/common/io_stub.rs @@ -0,0 +1,211 @@ +//! Configurable IOInterface stub, equivalent to PHPUnit's +//! `getMockBuilder(IOInterface::class)->getMock()` where individual methods are +//! configured via `->method('x')->willReturn(y)`. +//! +//! Each configurable method is backed by an `Option<ReturnValue>` field plus a +//! builder-style setter. Unset methods fall back to NullIO-equivalent defaults. +#![allow(dead_code)] + +use shirabe::config::Config; +use shirabe::io::{BaseIO, IOInterface, IOInterfaceImmutable, IOInterfaceMutable}; +use shirabe_php_shim::PhpMixed; + +#[derive(Debug, Default)] +pub struct IOStub { + authentications: indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>>, + + is_interactive: Option<bool>, + is_decorated: Option<bool>, + is_verbose: Option<bool>, + is_very_verbose: Option<bool>, + is_debug: Option<bool>, + + // `hasAuthentication` is keyed: PHPUnit tests usually configure a single bool + // willReturn that applies to any argument, so a single Option<bool> matches. + has_authentication: Option<bool>, + // `getAuthentication` willReturn is a `['username' => ..., 'password' => ...]` array. + get_authentication: Option<indexmap::IndexMap<String, Option<String>>>, + + ask: Option<PhpMixed>, + ask_confirmation: Option<bool>, + ask_and_hide_answer: Option<Option<String>>, +} + +impl IOStub { + pub fn new() -> Self { + Self::default() + } + + pub fn with_is_interactive(mut self, value: bool) -> Self { + self.is_interactive = Some(value); + self + } + pub fn with_is_decorated(mut self, value: bool) -> Self { + self.is_decorated = Some(value); + self + } + pub fn with_is_verbose(mut self, value: bool) -> Self { + self.is_verbose = Some(value); + self + } + pub fn with_is_very_verbose(mut self, value: bool) -> Self { + self.is_very_verbose = Some(value); + self + } + pub fn with_is_debug(mut self, value: bool) -> Self { + self.is_debug = Some(value); + self + } + pub fn with_has_authentication(mut self, value: bool) -> Self { + self.has_authentication = Some(value); + self + } + pub fn with_get_authentication( + mut self, + value: indexmap::IndexMap<String, Option<String>>, + ) -> Self { + self.get_authentication = Some(value); + self + } + pub fn with_ask(mut self, value: PhpMixed) -> Self { + self.ask = Some(value); + self + } + pub fn with_ask_confirmation(mut self, value: bool) -> Self { + self.ask_confirmation = Some(value); + self + } + pub fn with_ask_and_hide_answer(mut self, value: Option<String>) -> Self { + self.ask_and_hide_answer = Some(value); + self + } +} + +impl IOInterfaceImmutable for IOStub { + fn is_interactive(&self) -> bool { + self.is_interactive.unwrap_or(false) + } + fn is_verbose(&self) -> bool { + self.is_verbose.unwrap_or(false) + } + fn is_very_verbose(&self) -> bool { + self.is_very_verbose.unwrap_or(false) + } + fn is_debug(&self) -> bool { + self.is_debug.unwrap_or(false) + } + fn is_decorated(&self) -> bool { + self.is_decorated.unwrap_or(false) + } + + fn write3(&self, _message: &str, _newline: bool, _verbosity: i64) {} + fn write_error3(&self, _message: &str, _newline: bool, _verbosity: i64) {} + fn write_raw3(&self, _message: &str, _newline: bool, _verbosity: i64) {} + fn write_error_raw3(&self, _message: &str, _newline: bool, _verbosity: i64) {} + fn overwrite4(&self, _message: &str, _newline: bool, _size: Option<i64>, _verbosity: i64) {} + fn overwrite_error4( + &self, + _message: &str, + _newline: bool, + _size: Option<i64>, + _verbosity: i64, + ) { + } + + fn ask(&self, _question: String, default: PhpMixed) -> PhpMixed { + self.ask.clone().unwrap_or(default) + } + fn ask_confirmation(&self, _question: String, default: bool) -> bool { + self.ask_confirmation.unwrap_or(default) + } + fn ask_and_validate( + &self, + _question: String, + _validator: Box<dyn Fn(PhpMixed) -> anyhow::Result<PhpMixed>>, + _attempts: Option<i64>, + default: PhpMixed, + ) -> anyhow::Result<PhpMixed> { + Ok(default) + } + fn ask_and_hide_answer(&self, _question: String) -> Option<String> { + self.ask_and_hide_answer.clone().unwrap_or(None) + } + fn select( + &self, + _question: String, + _choices: Vec<String>, + default: PhpMixed, + _attempts: PhpMixed, + _error_message: String, + _multiselect: bool, + ) -> PhpMixed { + default + } + + fn get_authentications( + &self, + ) -> indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>> { + <Self as BaseIO>::get_authentications(self) + } + fn has_authentication(&self, repository_name: &str) -> bool { + match self.has_authentication { + Some(value) => value, + None => <Self as BaseIO>::has_authentication(self, repository_name), + } + } + fn get_authentication( + &self, + repository_name: &str, + ) -> indexmap::IndexMap<String, Option<String>> { + match &self.get_authentication { + Some(value) => value.clone(), + None => <Self as BaseIO>::get_authentication(self, repository_name), + } + } + + fn error(&self, message: &str, context: &[(&str, &str)]) { + <Self as BaseIO>::error(self, message, context); + } + fn warning(&self, message: &str, context: &[(&str, &str)]) { + <Self as BaseIO>::warning(self, message, context); + } + fn debug(&self, message: &str, context: &[(&str, &str)]) { + <Self as BaseIO>::debug(self, message, context); + } +} + +impl IOInterfaceMutable for IOStub { + fn set_authentication( + &mut self, + repository_name: String, + username: String, + password: Option<String>, + ) { + <Self as BaseIO>::set_authentication(self, repository_name, username, password) + } + fn load_configuration(&mut self, config: &mut Config) -> anyhow::Result<()> { + <Self as BaseIO>::load_configuration(self, config) + } +} + +impl IOInterface for IOStub { + fn as_any(&self) -> &dyn std::any::Any { + self + } + fn as_base_io_mut(&mut self) -> Option<&mut dyn BaseIO> { + Some(self) + } +} + +impl BaseIO for IOStub { + fn authentications( + &self, + ) -> &indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>> { + &self.authentications + } + fn authentications_mut( + &mut self, + ) -> &mut indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>> { + &mut self.authentications + } +} |
