aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/common
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 14:57:23 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit8dd3d67884a204ca40e3364206868fea77a312be (patch)
treeea0b603220112dc5d6a512796cda616250b50213 /crates/shirabe/tests/common
parent230319ae3db47ed9c35dc7faa733a84f9ce87ff9 (diff)
downloadphp-shirabe-8dd3d67884a204ca40e3364206868fea77a312be.tar.gz
php-shirabe-8dd3d67884a204ca40e3364206868fea77a312be.tar.zst
php-shirabe-8dd3d67884a204ca40e3364206868fea77a312be.zip
feat(test): add HttpDownloaderMock, IOStub, and Config stub helpers
IOStub and ConfigStubBuilder provide getMockBuilder-style configurable stubs. Wired into util/repository/downloader/command test targets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/common')
-rw-r--r--crates/shirabe/tests/common/config_stub.rs77
-rw-r--r--crates/shirabe/tests/common/http_downloader_mock.rs77
-rw-r--r--crates/shirabe/tests/common/io_stub.rs211
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
+ }
+}