aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe/src/util/http_downloader.rs196
-rw-r--r--crates/shirabe/tests/command/main.rs6
-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
-rw-r--r--crates/shirabe/tests/downloader/main.rs7
-rw-r--r--crates/shirabe/tests/repository/main.rs6
-rw-r--r--crates/shirabe/tests/util/main.rs6
8 files changed, 586 insertions, 0 deletions
diff --git a/crates/shirabe/src/util/http_downloader.rs b/crates/shirabe/src/util/http_downloader.rs
index 0c7308d..edc84cc 100644
--- a/crates/shirabe/src/util/http_downloader.rs
+++ b/crates/shirabe/src/util/http_downloader.rs
@@ -54,6 +54,52 @@ pub struct HttpDownloader {
disabled: bool,
/// @var bool
allow_async: bool,
+ /// Test-only internal hook. `None` in production. When `Some`, the request methods
+ /// (`get`/`copy`/`add`/`add_copy`) short-circuit to canned responses from the
+ /// expectation queue instead of performing real I/O. See ADR 0005 for the rationale
+ /// (the same internal-hook pattern used for `ProcessExecutor`). Mirrors
+ /// composer/tests/Composer/Test/Mock/HttpDownloaderMock.php.
+ mock: Option<HttpDownloaderMockState>,
+}
+
+/// For testing only. State backing the `HttpDownloaderMock`: an optional expectation queue,
+/// strict flag, default handler for undefined requests, and a log of received URLs.
+#[derive(Debug, Clone)]
+pub struct HttpDownloaderMockState {
+ expectations: Option<Vec<HttpDownloaderMockExpectation>>,
+ strict: bool,
+ default_handler: HttpDownloaderMockHandler,
+ log: Vec<String>,
+}
+
+/// For testing only. A single expected HTTP request and the canned response to return.
+/// `options` of `None` means "match any options"; otherwise the executed options must be
+/// exactly equal (PHP `===`).
+#[derive(Debug, Clone)]
+pub struct HttpDownloaderMockExpectation {
+ pub url: String,
+ pub options: Option<IndexMap<String, PhpMixed>>,
+ pub status: i64,
+ pub body: String,
+ pub headers: Vec<String>,
+}
+
+/// For testing only. The default response handler used for undefined requests when not strict.
+#[derive(Debug, Clone)]
+pub struct HttpDownloaderMockHandler {
+ pub status: i64,
+ pub body: String,
+ pub headers: Vec<String>,
+}
+
+impl Default for HttpDownloaderMockHandler {
+ fn default() -> Self {
+ Self {
+ status: 200,
+ body: String::new(),
+ headers: Vec::new(),
+ }
+ }
}
struct Job {
@@ -168,11 +214,15 @@ impl HttpDownloader {
id_gen: 0,
disabled,
allow_async: false,
+ mock: None,
}
}
/// Download a file synchronously
pub fn get(&mut self, url: &str, options: IndexMap<String, PhpMixed>) -> Result<Response> {
+ if self.mock.is_some() {
+ return self.mock_get(url, &options);
+ }
if url.is_empty() {
return Err(InvalidArgumentException {
message: "$url must not be an empty string".to_string(),
@@ -201,6 +251,9 @@ impl HttpDownloader {
url: &str,
options: IndexMap<String, PhpMixed>,
) -> Result<Response> {
+ if self.mock.is_some() {
+ return self.mock_get(url, &options);
+ }
if url.is_empty() {
return Err(InvalidArgumentException {
message: "$url must not be an empty string".to_string(),
@@ -228,6 +281,9 @@ impl HttpDownloader {
to: &str,
options: IndexMap<String, PhpMixed>,
) -> Result<Response> {
+ if self.mock.is_some() {
+ return self.mock_get(url, &options);
+ }
if url.is_empty() {
return Err(InvalidArgumentException {
message: "$url must not be an empty string".to_string(),
@@ -255,6 +311,9 @@ impl HttpDownloader {
to: &str,
options: IndexMap<String, PhpMixed>,
) -> Result<Response> {
+ if self.mock.is_some() {
+ return self.mock_get(url, &options);
+ }
if url.is_empty() {
return Err(InvalidArgumentException {
message: "$url must not be an empty string".to_string(),
@@ -823,6 +882,143 @@ impl HttpDownloader {
&& function_exists("curl_multi_exec")
&& function_exists("curl_multi_init")
}
+
+ /// For testing only. Mirrors HttpDownloaderMock::expects: installs the expectation queue,
+ /// strict flag and default handler used by the mock request path.
+ pub fn __expects(
+ &mut self,
+ expectations: Vec<HttpDownloaderMockExpectation>,
+ strict: bool,
+ default_handler: HttpDownloaderMockHandler,
+ ) {
+ self.mock = Some(HttpDownloaderMockState {
+ expectations: Some(expectations),
+ strict,
+ default_handler,
+ log: Vec::new(),
+ });
+ }
+
+ /// For testing only. Mirrors HttpDownloaderMock::assertComplete: panics if any expected
+ /// HTTP requests remain unconsumed.
+ pub fn __assert_complete(&self) {
+ let Some(mock) = &self.mock else {
+ return;
+ };
+ // this was not configured to expect anything, so no need to react here
+ let Some(expectations) = &mock.expectations else {
+ return;
+ };
+ if !expectations.is_empty() {
+ let urls: Vec<String> = expectations.iter().map(|e| e.url.clone()).collect();
+ panic!(
+ "There are still {} expected HTTP requests which have not been consumed:{}{}{}{}Received calls:{}{}",
+ expectations.len(),
+ shirabe_php_shim::PHP_EOL,
+ urls.join(shirabe_php_shim::PHP_EOL),
+ shirabe_php_shim::PHP_EOL,
+ shirabe_php_shim::PHP_EOL,
+ shirabe_php_shim::PHP_EOL,
+ mock.log.join(shirabe_php_shim::PHP_EOL),
+ );
+ }
+ }
+
+ /// For testing only. Mirrors HttpDownloaderMock::get: consumes the next matching expectation
+ /// (or falls back to the default handler when not strict) and returns the canned response.
+ fn mock_get(
+ &mut self,
+ file_url: &str,
+ options: &IndexMap<String, PhpMixed>,
+ ) -> Result<Response> {
+ if file_url.is_empty() {
+ return Err(LogicException {
+ message: "url cannot be an empty string".to_string(),
+ code: 0,
+ }
+ .into());
+ }
+
+ let mock = self.mock.as_mut().expect("mock_get called without a mock");
+ mock.log.push(file_url.to_string());
+
+ let matches_first = mock
+ .expectations
+ .as_ref()
+ .and_then(|e| e.first())
+ .is_some_and(|first| {
+ first.url == file_url
+ && (first.options.is_none() || first.options.as_ref() == Some(options))
+ });
+
+ if matches_first {
+ let expect = mock.expectations.as_mut().unwrap().remove(0);
+ return Self::mock_respond(file_url, expect.status, expect.headers, expect.body);
+ }
+
+ if !mock.strict {
+ let handler = mock.default_handler.clone();
+ return Self::mock_respond(file_url, handler.status, handler.headers, handler.body);
+ }
+
+ let next_expected = mock
+ .expectations
+ .as_ref()
+ .and_then(|e| e.first())
+ .map(|first| {
+ let opts = if first.options.is_some() {
+ format!(
+ "\" with options \"{}",
+ serde_json::to_string(&first.options).unwrap_or_default()
+ )
+ } else {
+ String::new()
+ };
+ format!("Expected \"{}{}\" at this point.", first.url, opts)
+ })
+ .unwrap_or_else(|| "Expected no more calls at this point.".to_string());
+ let prior_calls = if mock.log.len() > 1 {
+ mock.log[..mock.log.len() - 1].join(shirabe_php_shim::PHP_EOL)
+ } else {
+ String::new()
+ };
+ panic!(
+ "Received unexpected request for \"{}\" with options \"{}\"{}{}{}Received calls:{}{}",
+ file_url,
+ serde_json::to_string(options).unwrap_or_default(),
+ shirabe_php_shim::PHP_EOL,
+ next_expected,
+ shirabe_php_shim::PHP_EOL,
+ shirabe_php_shim::PHP_EOL,
+ prior_calls,
+ );
+ }
+
+ /// For testing only. Mirrors HttpDownloaderMock::respond.
+ fn mock_respond(
+ url: &str,
+ status: i64,
+ headers: Vec<String>,
+ body: String,
+ ) -> Result<Response> {
+ if status < 400 {
+ return Ok(Response::new(
+ url.to_string(),
+ Some(status),
+ headers,
+ Some(body),
+ ));
+ }
+
+ let mut e = TransportException::new(
+ format!("The \"{}\" file could not be downloaded", url),
+ status,
+ );
+ e.set_headers(headers);
+ e.set_response(Some(body));
+
+ Err(e.into())
+ }
}
#[derive(Debug, Clone, Copy)]
diff --git a/crates/shirabe/tests/command/main.rs b/crates/shirabe/tests/command/main.rs
index 78345fe..13efc0d 100644
--- a/crates/shirabe/tests/command/main.rs
+++ b/crates/shirabe/tests/command/main.rs
@@ -1,3 +1,9 @@
+#[path = "../common/config_stub.rs"]
+mod config_stub;
+#[path = "../common/http_downloader_mock.rs"]
+mod http_downloader_mock;
+#[path = "../common/io_stub.rs"]
+mod io_stub;
#[path = "../common/test_case.rs"]
mod test_case;
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
+ }
+}
diff --git a/crates/shirabe/tests/downloader/main.rs b/crates/shirabe/tests/downloader/main.rs
index 6e39b85..a2bbab4 100644
--- a/crates/shirabe/tests/downloader/main.rs
+++ b/crates/shirabe/tests/downloader/main.rs
@@ -1,3 +1,10 @@
+#[path = "../common/config_stub.rs"]
+mod config_stub;
+#[path = "../common/http_downloader_mock.rs"]
+mod http_downloader_mock;
+#[path = "../common/io_stub.rs"]
+mod io_stub;
+
mod archive_downloader_test;
mod download_manager_test;
mod file_downloader_test;
diff --git a/crates/shirabe/tests/repository/main.rs b/crates/shirabe/tests/repository/main.rs
index 2995b24..15a42a3 100644
--- a/crates/shirabe/tests/repository/main.rs
+++ b/crates/shirabe/tests/repository/main.rs
@@ -1,3 +1,9 @@
+#[path = "../common/config_stub.rs"]
+mod config_stub;
+#[path = "../common/http_downloader_mock.rs"]
+mod http_downloader_mock;
+#[path = "../common/io_stub.rs"]
+mod io_stub;
#[path = "../common/test_case.rs"]
mod test_case;
diff --git a/crates/shirabe/tests/util/main.rs b/crates/shirabe/tests/util/main.rs
index bf23c6b..d72bcc0 100644
--- a/crates/shirabe/tests/util/main.rs
+++ b/crates/shirabe/tests/util/main.rs
@@ -1,3 +1,9 @@
+#[path = "../common/config_stub.rs"]
+mod config_stub;
+#[path = "../common/http_downloader_mock.rs"]
+mod http_downloader_mock;
+#[path = "../common/io_stub.rs"]
+mod io_stub;
#[path = "../common/process_executor_mock.rs"]
mod process_executor_mock;