aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe/src/util/auth_helper.rs2
-rw-r--r--crates/shirabe/tests/common/io_stub.rs93
-rw-r--r--crates/shirabe/tests/util/auth_helper_test.rs235
-rw-r--r--crates/shirabe/tests/util/error_handler_test.rs11
-rw-r--r--crates/shirabe/tests/util/process_executor_test.rs41
-rw-r--r--crates/shirabe/tests/util/remote_filesystem_test.rs56
-rw-r--r--crates/shirabe/tests/util/stream_context_factory_test.rs79
7 files changed, 476 insertions, 41 deletions
diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs
index 13199e9c..1e997fd9 100644
--- a/crates/shirabe/src/util/auth_helper.rs
+++ b/crates/shirabe/src/util/auth_helper.rs
@@ -33,7 +33,7 @@ pub struct PromptAuthResult {
pub store_auth: StoreAuth,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub enum StoreAuth {
Bool(bool),
Prompt,
diff --git a/crates/shirabe/tests/common/io_stub.rs b/crates/shirabe/tests/common/io_stub.rs
index b57dd0ca..d8c60799 100644
--- a/crates/shirabe/tests/common/io_stub.rs
+++ b/crates/shirabe/tests/common/io_stub.rs
@@ -10,6 +10,21 @@ use shirabe::config::Config;
use shirabe::io::{BaseIO, IOInterface, IOInterfaceImmutable, IOInterfaceMutable};
use shirabe_php_shim::PhpMixed;
+// Records call arguments for a stubbed method, equivalent to PHPUnit's
+// `->expects($this->once())->method(...)->with(...)` call-count/argument verification.
+#[derive(Debug, Default)]
+struct CallRecorder<T>(std::cell::RefCell<Vec<T>>);
+
+impl<T: Clone> CallRecorder<T> {
+ fn push(&self, value: T) {
+ self.0.borrow_mut().push(value);
+ }
+
+ fn calls(&self) -> Vec<T> {
+ self.0.borrow().clone()
+ }
+}
+
#[derive(Debug, Default)]
pub struct IOStub {
authentications: indexmap::IndexMap<String, indexmap::IndexMap<String, Option<String>>>,
@@ -27,12 +42,29 @@ pub struct IOStub {
get_authentication: Option<indexmap::IndexMap<String, Option<String>>>,
ask: Option<PhpMixed>,
+ // When set, `askAndValidate` invokes the caller's validator with this value and returns/
+ // propagates its `Result`, equivalent to a PHPUnit `willReturnCallback` that calls the
+ // validator directly. When unset, `askAndValidate` just returns `default` (unvalidated).
+ ask_and_validate_answer: Option<PhpMixed>,
ask_confirmation: Option<bool>,
ask_and_hide_answer: Option<Option<String>>,
// Keyed `askAndHideAnswer` replies, mirroring tests whose willReturnCallback
// switches on the question string. Unknown questions return `''` like PHP's
// `switch` default.
ask_and_hide_answer_responses: Option<indexmap::IndexMap<String, String>>,
+
+ // Records `writeRaw` calls.
+ write_raw_calls: CallRecorder<(String, bool)>,
+ // Records `setAuthentication` calls. Kept separate from `authentications` so
+ // `with_has_authentication`/`with_get_authentication` stay static (matching a PHPUnit
+ // `willReturn`) even while `setAuthentication` is invoked.
+ set_authentication_calls: CallRecorder<(String, String, Option<String>)>,
+ // Records `askAndValidate` calls (question, attempts, default).
+ ask_and_validate_calls: CallRecorder<(String, Option<i64>, PhpMixed)>,
+ // Records `hasAuthentication` calls.
+ has_authentication_calls: CallRecorder<String>,
+ // Records `getAuthentication` calls.
+ get_authentication_calls: CallRecorder<String>,
}
impl IOStub {
@@ -71,10 +103,20 @@ impl IOStub {
self.get_authentication = Some(value);
self
}
+ // Mutator counterpart of `with_get_authentication`, for tests that reconfigure the stub's
+ // response between calls (equivalent to PHPUnit's `willReturnCallback` sequencing via
+ // `array_shift`).
+ pub fn set_get_authentication(&mut self, value: indexmap::IndexMap<String, Option<String>>) {
+ self.get_authentication = Some(value);
+ }
pub fn with_ask(mut self, value: PhpMixed) -> Self {
self.ask = Some(value);
self
}
+ pub fn with_ask_and_validate_answer(mut self, value: PhpMixed) -> Self {
+ self.ask_and_validate_answer = Some(value);
+ self
+ }
pub fn with_ask_confirmation(mut self, value: bool) -> Self {
self.ask_confirmation = Some(value);
self
@@ -106,6 +148,31 @@ impl IOStub {
self.authentications.insert(repository_name.into(), auth);
self
}
+
+ // For testing only. Returns the recorded `writeRaw` calls in call order.
+ pub fn write_raw_calls(&self) -> Vec<(String, bool)> {
+ self.write_raw_calls.calls()
+ }
+
+ // For testing only. Returns the recorded `setAuthentication` calls in call order.
+ pub fn set_authentication_calls(&self) -> Vec<(String, String, Option<String>)> {
+ self.set_authentication_calls.calls()
+ }
+
+ // For testing only. Returns the recorded `askAndValidate` calls in call order.
+ pub fn ask_and_validate_calls(&self) -> Vec<(String, Option<i64>, PhpMixed)> {
+ self.ask_and_validate_calls.calls()
+ }
+
+ // For testing only. Returns the recorded `hasAuthentication` calls in call order.
+ pub fn has_authentication_calls(&self) -> Vec<String> {
+ self.has_authentication_calls.calls()
+ }
+
+ // For testing only. Returns the recorded `getAuthentication` calls in call order.
+ pub fn get_authentication_calls(&self) -> Vec<String> {
+ self.get_authentication_calls.calls()
+ }
}
impl IOInterfaceImmutable for IOStub {
@@ -127,7 +194,9 @@ impl IOInterfaceImmutable for IOStub {
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_raw3(&self, message: &str, newline: bool, _verbosity: i64) {
+ self.write_raw_calls.push((message.to_string(), newline));
+ }
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(
@@ -147,12 +216,17 @@ impl IOInterfaceImmutable for IOStub {
}
fn ask_and_validate(
&self,
- _question: String,
- _validator: Box<dyn Fn(PhpMixed) -> anyhow::Result<PhpMixed>>,
- _attempts: Option<i64>,
+ question: String,
+ validator: Box<dyn Fn(PhpMixed) -> anyhow::Result<PhpMixed>>,
+ attempts: Option<i64>,
default: PhpMixed,
) -> anyhow::Result<PhpMixed> {
- Ok(default)
+ self.ask_and_validate_calls
+ .push((question, attempts, default.clone()));
+ match &self.ask_and_validate_answer {
+ Some(answer) => validator(answer.clone()),
+ None => Ok(default),
+ }
}
fn ask_and_hide_answer(&self, question: String) -> Option<String> {
if let Some(responses) = &self.ask_and_hide_answer_responses {
@@ -178,6 +252,8 @@ impl IOInterfaceImmutable for IOStub {
<Self as BaseIO>::get_authentications(self)
}
fn has_authentication(&self, repository_name: &str) -> bool {
+ self.has_authentication_calls
+ .push(repository_name.to_string());
match self.has_authentication {
Some(value) => value,
None => <Self as BaseIO>::has_authentication(self, repository_name),
@@ -187,6 +263,8 @@ impl IOInterfaceImmutable for IOStub {
&self,
repository_name: &str,
) -> indexmap::IndexMap<String, Option<String>> {
+ self.get_authentication_calls
+ .push(repository_name.to_string());
match &self.get_authentication {
Some(value) => value.clone(),
None => <Self as BaseIO>::get_authentication(self, repository_name),
@@ -211,6 +289,11 @@ impl IOInterfaceMutable for IOStub {
username: String,
password: Option<String>,
) {
+ self.set_authentication_calls.push((
+ repository_name.clone(),
+ username.clone(),
+ password.clone(),
+ ));
<Self as BaseIO>::set_authentication(self, repository_name, username, password)
}
fn load_configuration(&mut self, config: &mut Config) -> anyhow::Result<()> {
diff --git a/crates/shirabe/tests/util/auth_helper_test.rs b/crates/shirabe/tests/util/auth_helper_test.rs
index d719abf3..95cd3a2d 100644
--- a/crates/shirabe/tests/util/auth_helper_test.rs
+++ b/crates/shirabe/tests/util/auth_helper_test.rs
@@ -528,33 +528,241 @@ fn test_store_auth_with_prompt_no_answer() {
f.auth_helper.store_auth(origin, StoreAuth::Prompt).unwrap();
}
+// Mirrors AuthHelperTest::testStoreAuthWithPromptInvalidAnswer. The PHP test mocks
+// `askAndValidate` itself to invoke the validator directly with an invalid answer, so the
+// RuntimeException it raises propagates out of `storeAuth` without exercising any interactive
+// retry loop; `IOStub::with_ask_and_validate_answer` mirrors that directly.
#[test]
-#[ignore = "PHP catches a RuntimeException from the validator; the Rust QuestionHelper exhausts \
-input and panics via expect() rather than returning Err, so it cannot be represented as a \
-recoverable Result"]
fn test_store_auth_with_prompt_invalid_answer() {
- todo!()
+ use crate::io_stub::IOStub;
+ use shirabe_php_shim::RuntimeException;
+
+ let origin = "github.com";
+ let config_source_name = "https://api.gitlab.com/source";
+
+ let io = std::rc::Rc::new(std::cell::RefCell::new(
+ IOStub::new().with_ask_and_validate_answer(PhpMixed::String("invalid".to_string())),
+ ));
+ let config = ConfigStubBuilder::new().build_shared();
+
+ let mut source = MockConfigSource::new();
+ source
+ .expect_get_name()
+ .times(1)
+ .returning(|| "https://api.gitlab.com/source".to_string());
+ config.borrow_mut().set_auth_config_source(Box::new(source));
+
+ let auth_helper = AuthHelper::new(
+ io.clone() as std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config,
+ );
+
+ let err = auth_helper
+ .store_auth(origin, StoreAuth::Prompt)
+ .expect_err("expected a RuntimeException");
+ assert!(err.downcast_ref::<RuntimeException>().is_some());
+
+ // Mirrors PHP's `->with('Do you want to store credentials for '.$origin.' in '.
+ // $configSourceName.' ? [Yn] ', $this->anything(), null, 'y')` verification on askAndValidate.
+ assert_eq!(
+ vec![(
+ format!(
+ "Do you want to store credentials for {origin} in {config_source_name} ? [Yn] "
+ ),
+ None,
+ PhpMixed::String("y".to_string()),
+ )],
+ io.borrow().ask_and_validate_calls()
+ );
}
+// Mirrors AuthHelperTest::testPromptAuthIfNeededGitLabNoAuthChange. `GitLab::authorizeOauth`
+// falls back to the `gitlab-token` config entry after real `git config gitlab.accesstoken` /
+// `gitlab.deploytoken.*` lookups miss (as they do in this repo's test checkout / CI, matching
+// the same host-git-state dependency the upstream PHP test already carries), and stores the
+// same username/password IOStub already returns. Since getAuthentication is a static stub, that
+// looks like "no auth change" and AuthHelper raises a TransportException.
#[test]
-#[ignore = "needs the extra prompt_auth_if_needed params (headers/retry_count/response_body) and \
-GitLab::authorize_oauth which shells out to real `git config`; depends on host git state and is a \
-design-level port concern"]
+#[ignore]
fn test_prompt_auth_if_needed_git_lab_no_auth_change() {
- todo!()
+ use crate::io_stub::IOStub;
+ use shirabe::downloader::TransportException;
+
+ let origin = "gitlab.com";
+
+ let mut auth = IndexMap::new();
+ auth.insert("username".to_string(), Some("gitlab-user".to_string()));
+ auth.insert("password".to_string(), Some("gitlab-password".to_string()));
+ let io = std::rc::Rc::new(std::cell::RefCell::new(
+ IOStub::new()
+ .with_has_authentication(true)
+ .with_get_authentication(auth),
+ ));
+
+ let mut gitlab_token = IndexMap::new();
+ let mut token_entry = IndexMap::new();
+ token_entry.insert(
+ "username".to_string(),
+ PhpMixed::String("gitlab-user".to_string()),
+ );
+ token_entry.insert(
+ "token".to_string(),
+ PhpMixed::String("gitlab-password".to_string()),
+ );
+ gitlab_token.insert("gitlab.com".to_string(), PhpMixed::Array(token_entry));
+
+ let config = ConfigStubBuilder::new()
+ .with("github-domains", PhpMixed::List(vec![]))
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with("gitlab-token", PhpMixed::Array(gitlab_token))
+ .build_shared();
+
+ let mut auth_helper = AuthHelper::new(
+ io.clone() as std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config,
+ );
+
+ let result = auth_helper.prompt_auth_if_needed(
+ "https://gitlab.com/acme/archive.zip",
+ origin,
+ 404,
+ Some("GitLab requires authentication and it was not provided"),
+ vec![],
+ 0,
+ None,
+ );
+
+ let err = result.expect_err("expected a TransportException");
+ assert!(err.downcast_ref::<TransportException>().is_some());
+
+ assert_eq!(
+ vec![(
+ "gitlab.com".to_string(),
+ "gitlab-user".to_string(),
+ Some("gitlab-password".to_string())
+ )],
+ io.borrow().set_authentication_calls()
+ );
}
+// Mirrors AuthHelperTest::testPromptAuthIfNeededMultipleBitbucketDownloads. The pre-seeded
+// `bitbucket-oauth` config entry has a non-expired access-token, so `Bitbucket::request_token`
+// resolves it from `get_token_from_config` without ever making a network call.
#[test]
-#[ignore = "drives Bitbucket::request_token over the network and relies on willReturnCallback \
-sequencing of getAuthentication; design-level port concern"]
fn test_prompt_auth_if_needed_multiple_bitbucket_downloads() {
- todo!()
+ use crate::io_stub::IOStub;
+
+ let origin = "bitbucket.org";
+
+ let mut token_entry = IndexMap::new();
+ token_entry.insert(
+ "access-token".to_string(),
+ PhpMixed::String("bitbucket_access_token".to_string()),
+ );
+ token_entry.insert(
+ "access-token-expiration".to_string(),
+ PhpMixed::Int(shirabe_php_shim::time() + 1800),
+ );
+ let mut bitbucket_oauth = IndexMap::new();
+ bitbucket_oauth.insert("bitbucket.org".to_string(), PhpMixed::Array(token_entry));
+
+ let config = ConfigStubBuilder::new()
+ .with("github-domains", PhpMixed::List(vec![]))
+ .with("gitlab-domains", PhpMixed::List(vec![]))
+ .with("bitbucket-oauth", PhpMixed::Array(bitbucket_oauth))
+ .build_shared();
+
+ let mut first_auth = IndexMap::new();
+ first_auth.insert(
+ "username".to_string(),
+ Some("bitbucket_client_id".to_string()),
+ );
+ first_auth.insert(
+ "password".to_string(),
+ Some("bitbucket_client_secret".to_string()),
+ );
+ let io = std::rc::Rc::new(std::cell::RefCell::new(
+ IOStub::new()
+ .with_has_authentication(true)
+ .with_get_authentication(first_auth),
+ ));
+
+ let mut auth_helper = AuthHelper::new(
+ io.clone() as std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config,
+ );
+
+ let result1 = auth_helper
+ .prompt_auth_if_needed(
+ "https://bitbucket.org/workspace/repo1/get/hash1.zip",
+ origin,
+ 401,
+ Some("HTTP/2 401 "),
+ vec![],
+ 0,
+ None,
+ )
+ .unwrap();
+
+ let mut second_auth = IndexMap::new();
+ second_auth.insert("username".to_string(), Some("x-token-auth".to_string()));
+ second_auth.insert(
+ "password".to_string(),
+ Some("bitbucket_access_token".to_string()),
+ );
+ io.borrow_mut().set_get_authentication(second_auth);
+
+ let result2 = auth_helper
+ .prompt_auth_if_needed(
+ "https://bitbucket.org/workspace/repo2/get/hash2.zip",
+ origin,
+ 401,
+ Some("HTTP/2 401 "),
+ vec![],
+ 0,
+ None,
+ )
+ .unwrap();
+
+ assert!(result1.retry);
+ assert_eq!(StoreAuth::Bool(false), result1.store_auth);
+ assert!(result2.retry);
+ assert_eq!(StoreAuth::Bool(false), result2.store_auth);
+
+ assert_eq!(
+ vec![(
+ "bitbucket.org".to_string(),
+ "x-token-auth".to_string(),
+ Some("bitbucket_access_token".to_string())
+ )],
+ io.borrow().set_authentication_calls()
+ );
+
+ // Mirrors PHP's `->expects($this->exactly(2))->method('hasAuthentication')->with($origin)`
+ // and `->expects($this->exactly(2))->method('getAuthentication')` verification.
+ assert_eq!(
+ vec![origin.to_string(), origin.to_string()],
+ io.borrow().has_authentication_calls()
+ );
+ assert_eq!(
+ vec![origin.to_string(), origin.to_string()],
+ io.borrow().get_authentication_calls()
+ );
}
#[test]
#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \
trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"]
fn test_add_authentication_header_with_custom_headers() {
+ // TODO(phase-d): exercises AuthHelper::addAuthenticationHeader, a deprecated wrapper
+ // around addAuthenticationOptions that PHP implements via
+ // trigger_error(E_USER_DEPRECATED). It has not been ported to Rust (no
+ // add_authentication_header method exists on AuthHelper) because the PHP
+ // error-handler subsystem it relies on is not modeled — same limitation as
+ // error_handler_test.rs.
todo!()
}
@@ -562,6 +770,8 @@ fn test_add_authentication_header_with_custom_headers() {
#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \
trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"]
fn test_add_authentication_header_is_working() {
+ // TODO(phase-d): see test_add_authentication_header_with_custom_headers above — same
+ // unported addAuthenticationHeader deprecated wrapper.
todo!()
}
@@ -569,5 +779,8 @@ fn test_add_authentication_header_is_working() {
#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \
trigger_error/E_USER_DEPRECATED converted to a RuntimeException via set_error_handler; not modeled"]
fn test_add_authentication_header_deprecation() {
+ // TODO(phase-d): asserts that calling addAuthenticationHeader itself raises a
+ // RuntimeException via a custom set_error_handler converting E_USER_DEPRECATED; same
+ // unported wrapper and unmodeled error-handler subsystem as the two tests above.
todo!()
}
diff --git a/crates/shirabe/tests/util/error_handler_test.rs b/crates/shirabe/tests/util/error_handler_test.rs
index 7295cf2a..ed5e6a29 100644
--- a/crates/shirabe/tests/util/error_handler_test.rs
+++ b/crates/shirabe/tests/util/error_handler_test.rs
@@ -5,15 +5,15 @@
// trigger those by undefined-index access / array_merge misuse. There is no equivalent
// runtime mechanism in Rust to port faithfully.
+// TODO(phase-d): ErrorHandler::register() installs a PHP set_error_handler; no Rust equivalent.
#[allow(dead_code)]
fn set_up() {
- // ErrorHandler::register() installs a PHP set_error_handler; no Rust equivalent.
todo!()
}
+// TODO(phase-d): restore_error_handler() is PHP runtime machinery; no Rust equivalent.
#[allow(dead_code)]
fn tear_down() {
- // restore_error_handler() is PHP runtime machinery; no Rust equivalent.
todo!()
}
@@ -29,17 +29,24 @@ impl Drop for TearDown {
#[ignore = "depends on PHP runtime routing an undefined-index notice through set_error_handler; no Rust equivalent for $array['baz'] triggering ErrorHandler::handle"]
#[test]
fn test_error_handler_capture_notice() {
+ // TODO(phase-d): depends on PHP runtime routing an undefined-index notice through
+ // set_error_handler; no Rust equivalent for $array['baz'] triggering
+ // ErrorHandler::handle.
todo!()
}
#[ignore = "depends on PHP runtime emitting a TypeError/warning from array_merge([], 'string') via set_error_handler; no Rust equivalent"]
#[test]
fn test_error_handler_capture_warning() {
+ // TODO(phase-d): depends on PHP runtime emitting a TypeError/warning from
+ // array_merge([], 'string') via set_error_handler; no Rust equivalent.
todo!()
}
#[ignore = "depends on the PHP @ error-suppression operator and trigger_error routing through set_error_handler; no Rust equivalent"]
#[test]
fn test_error_handler_respects_at_operator() {
+ // TODO(phase-d): depends on the PHP @ error-suppression operator and trigger_error
+ // routing through set_error_handler; no Rust equivalent.
todo!()
}
diff --git a/crates/shirabe/tests/util/process_executor_test.rs b/crates/shirabe/tests/util/process_executor_test.rs
index e093a801..2ce43f3c 100644
--- a/crates/shirabe/tests/util/process_executor_test.rs
+++ b/crates/shirabe/tests/util/process_executor_test.rs
@@ -1,8 +1,8 @@
//! ref: composer/tests/Composer/Test/Util/ProcessExecutorTest.php
// These run real subprocesses (capturing output/stderr/timeout) and assert ProcessExecutor's
-// password hiding, line splitting and argument escaping; the subprocess execution and mocked
-// IO are not ported.
+// password hiding, line splitting and argument escaping. A few data points remain unportable —
+// see the individual `// TODO(phase-d)` comments below.
use shirabe::io::ConsoleIO;
use shirabe::io::IOInterface;
@@ -28,16 +28,34 @@ fn test_execute_captures_output() {
#[ignore = "requires PHP output buffering (ob_start/ob_get_clean) to capture stdout; no equivalent symbol"]
#[test]
fn test_execute_outputs_if_not_captured() {
+ // TODO(phase-d): requires PHP output buffering (ob_start/ob_get_clean) to capture
+ // stdout; no equivalent symbol. ProcessExecutor::execute with
+ // ProcessExecutor::FORWARD_OUTPUT and io=None writes straight to the real process
+ // stdout (see output_handler's `print!`), and there is no safe way to capture that
+ // from within a parallel cargo test process without redirecting the real stdout file
+ // descriptor, which is unsafe under `cargo test`'s default multi-threaded runner.
todo!()
}
-#[ignore = "requires getMockBuilder('IOInterface') with expects()->once()->method('writeRaw')->with() expectation verification; no mocking framework"]
#[test]
fn test_use_io_is_not_null_and_if_not_captured() {
- todo!()
+ use crate::io_stub::IOStub;
+
+ let io = std::rc::Rc::new(std::cell::RefCell::new(IOStub::new()));
+ let mut process = ProcessExecutor::new(Some(
+ io.clone() as std::rc::Rc<std::cell::RefCell<dyn IOInterface>>
+ ));
+
+ process
+ .execute("echo foo", ProcessExecutor::FORWARD_OUTPUT, None)
+ .unwrap();
+
+ assert_eq!(
+ vec![(format!("foo{}", PHP_EOL), false)],
+ io.borrow().write_raw_calls()
+ );
}
-#[ignore = "stderr capture works, but the test cwd (crates/shirabe) contains a `foo/` fixture dir, so `cat foo` reports \"Is a directory\" instead of \"No such file or directory\""]
#[test]
fn test_execute_captures_stderr() {
let mut process = ProcessExecutor::new(None);
@@ -124,6 +142,14 @@ fn test_doesnt_hide_ports() {
#[ignore = "splitLines is called with null in the PHP test, but split_lines accepts only &str (no ?string/Option overload)"]
#[test]
fn test_split_lines() {
+ // TODO(phase-d): splitLines is called with null in the PHP test
+ // ($process->splitLines(null)), but ProcessExecutor::split_lines here takes `&str`, not
+ // `Option<&str>` (PHP's `?string`). Porting this data point faithfully means widening
+ // split_lines's signature to Option<&str>, which touches every call site
+ // (package/version/version_guesser.rs, util/git.rs,
+ // repository/vcs/{hg,fossil,git,svn}_driver.rs — 13 call sites in total, all currently
+ // passing `&str`). That is a production API change beyond this test file; flagged for
+ // a design decision rather than made unilaterally.
todo!()
}
@@ -162,6 +188,11 @@ fn test_console_io_does_not_format_symfony_console_style() {
#[ignore = "executeAsync returns a Process, not a cancelable promise; no promise/cancel symbol exists"]
#[test]
fn test_execute_async_cancel() {
+ // TODO(phase-d): PHP's executeAsync returns a React\Promise\PromiseInterface with
+ // cancel(); Rust's execute_async returns anyhow::Result<Process> directly (see the
+ // comment on ProcessExecutor::execute_async: "no test seam in the external-packages
+ // crate"), so there is no promise/cancel symbol to drive this test's
+ // `$promise->cancel()` step.
todo!()
}
diff --git a/crates/shirabe/tests/util/remote_filesystem_test.rs b/crates/shirabe/tests/util/remote_filesystem_test.rs
index 9c1fe0bd..0fdc81b3 100644
--- a/crates/shirabe/tests/util/remote_filesystem_test.rs
+++ b/crates/shirabe/tests/util/remote_filesystem_test.rs
@@ -273,19 +273,71 @@ fn test_copy() {
#[test]
#[ignore = "requires a MockObject subclass of RemoteFilesystem overriding private get_remote_contents; no subclass-mocking infrastructure exists"]
fn test_copy_with_no_retry_on_failure() {
+ // TODO(phase-d): requires a MockObject subclass of RemoteFilesystem overriding the
+ // private get_remote_contents method. There is no subclass-mocking infrastructure in
+ // Rust for this, and get_remote_contents's http(s) branch is itself still a
+ // TODO(phase-c) stub (always returns Ok(None)), so there is nothing yet to intercept
+ // even with a seam.
todo!()
}
#[test]
#[ignore = "requires MockObject subclasses overriding RemoteFilesystem::get_remote_contents and AuthHelper::prompt_auth_if_needed; no subclass-mocking infrastructure exists"]
fn test_copy_with_success_on_retry() {
+ // TODO(phase-d): requires MockObject subclasses overriding
+ // RemoteFilesystem::get_remote_contents and AuthHelper::prompt_auth_if_needed to
+ // simulate a first failure and a retried success; same missing-subclass-mocking-
+ // infrastructure and TODO(phase-c) http(s)-stub blockers as
+ // test_copy_with_no_retry_on_failure above.
todo!()
}
#[test]
-#[ignore = "get_tls_defaults validates the (nonexistent) cafile and errors; constructor swallows it, so no ssl defaults are produced. Faithful porting needs CaBundle::validate_ca_file semantics for a missing file"]
fn test_get_options_for_url_creates_secure_tls_defaults() {
- todo!()
+ let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(IOStub::new()));
+
+ let mut ssl: IndexMap<String, PhpMixed> = IndexMap::new();
+ ssl.insert(
+ "cafile".to_string(),
+ PhpMixed::String("/some/path/file.crt".to_string()),
+ );
+ let mut additional_options: IndexMap<String, PhpMixed> = IndexMap::new();
+ additional_options.insert("ssl".to_string(), PhpMixed::Array(ssl));
+
+ let res = call_get_options_for_url(
+ io,
+ "example.org",
+ additional_options,
+ IndexMap::new(),
+ "http://www.example.org",
+ );
+
+ let ssl_res = res.get("ssl").and_then(|v| v.as_array()).unwrap();
+ let ciphers = ssl_res.get("ciphers").and_then(|v| v.as_string()).unwrap();
+ assert!(ciphers.contains(
+ "!aNULL:!eNULL:!EXPORT:!DES:!3DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA"
+ ));
+ assert_eq!(
+ Some(true),
+ ssl_res.get("verify_peer").and_then(|v| v.as_bool())
+ );
+ assert_eq!(
+ Some(true),
+ ssl_res.get("SNI_enabled").and_then(|v| v.as_bool())
+ );
+ assert_eq!(
+ Some(7),
+ ssl_res.get("verify_depth").and_then(|v| v.as_int())
+ );
+ assert_eq!(
+ Some("/some/path/file.crt"),
+ ssl_res.get("cafile").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some(true),
+ ssl_res.get("disable_compression").and_then(|v| v.as_bool())
+ );
}
// Mirrors RemoteFilesystemTest::provideBitbucketPublicDownloadUrls.
diff --git a/crates/shirabe/tests/util/stream_context_factory_test.rs b/crates/shirabe/tests/util/stream_context_factory_test.rs
index 284077b7..2ef2b6b7 100644
--- a/crates/shirabe/tests/util/stream_context_factory_test.rs
+++ b/crates/shirabe/tests/util/stream_context_factory_test.rs
@@ -1,8 +1,13 @@
//! ref: composer/tests/Composer/Test/Util/StreamContextFactoryTest.php
// These build a stream context and assert proxy/option handling driven by HTTP(S)_PROXY /
-// no_proxy environment variables; the env-dependent setup (without its setUp/tearDown
-// isolation) is not ported.
+// no_proxy environment variables. PHP's setUp/tearDown isolation (which resets the env vars and
+// the ProxyManager singleton before/after every test method) is emulated per-test via
+// set_up()/tear_down()+TearDown below; since env vars and the ProxyManager singleton are global
+// process state, and cargo runs tests in parallel by default (unlike PHPUnit's default serial
+// execution), every test here is also tagged `#[serial_test::serial]` to avoid racing other
+// serial-tagged tests in this binary that touch the same global state (see
+// util/http/proxy_manager_test.rs and http_downloader_test.rs).
use indexmap::IndexMap;
use shirabe::util::http::proxy_manager::ProxyManager;
use shirabe::util::platform::Platform;
@@ -35,6 +40,38 @@ fn map(entries: Vec<(&str, PhpMixed)>) -> IndexMap<String, PhpMixed> {
.collect()
}
+// `PhpMixed`'s `PartialEq` models PHP's `===` (order-sensitive for associative arrays). These
+// tests port PHPUnit's `assertEquals`, which compares associative arrays by key/value regardless
+// of insertion order (PHP `List`/sequential arrays are still position-sensitive, since reordering
+// them changes which value is at which index). This mirrors that PHPUnit semantics for the
+// `IndexMap<String, PhpMixed>` results `stream_context_get_options` returns.
+fn php_equals(a: &PhpMixed, b: &PhpMixed) -> bool {
+ match (a, b) {
+ (PhpMixed::Array(a), PhpMixed::Array(b)) => {
+ a.len() == b.len()
+ && a.iter()
+ .all(|(k, v)| b.get(k).is_some_and(|bv| php_equals(v, bv)))
+ }
+ (PhpMixed::List(a), PhpMixed::List(b)) => {
+ a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| php_equals(x, y))
+ }
+ _ => a == b,
+ }
+}
+
+#[track_caller]
+fn assert_options_eq(expected: &IndexMap<String, PhpMixed>, actual: &IndexMap<String, PhpMixed>) {
+ let matches = expected.len() == actual.len()
+ && expected
+ .iter()
+ .all(|(k, v)| actual.get(k).is_some_and(|av| php_equals(v, av)));
+ assert!(
+ matches,
+ "options mismatch (order-insensitive):\n expected: {:?}\n actual: {:?}",
+ expected, actual
+ );
+}
+
fn set_up() {
Platform::clear_env("HTTP_PROXY");
Platform::clear_env("http_proxy");
@@ -63,18 +100,25 @@ impl Drop for TearDown {
}
}
-// PHP's dataGetContext second data set passes a `notification` closure in both the default and
-// expected params; PhpMixed has no closure variant, so that data set (and thus the all-or-nothing
-// testGetContext) cannot be expressed.
+// TODO(phase-d): PHP's dataGetContext second data set passes a `notification` closure in both
+// the default and expected params; PhpMixed has no closure variant, so that data set (and thus
+// the all-or-nothing testGetContext, which a data provider test cannot partially skip) cannot be
+// expressed.
#[test]
+#[serial_test::serial]
#[ignore = "dataGetContext passes a notification closure in params; PhpMixed cannot represent a PHP closure, so the data set is unportable"]
fn test_get_context() {
let _tear_down = TearDown;
set_up();
+ // TODO(phase-d): dataGetContext's second data set passes a `notification` closure in
+ // params; PhpMixed cannot represent a PHP closure, so that data set (and thus the
+ // all-or-nothing testGetContext, which a data provider test cannot partially skip) is
+ // unportable.
todo!()
}
#[test]
+#[serial_test::serial]
#[ignore]
fn test_http_proxy() {
let _tear_down = TearDown;
@@ -114,11 +158,11 @@ fn test_http_proxy() {
("follow_location", PhpMixed::Int(1)),
]),
)]);
- assert_eq!(expected, options);
+ assert_options_eq(&expected, &options);
}
#[test]
-#[ignore]
+#[serial_test::serial]
fn test_http_proxy_with_no_proxy() {
let _tear_down = TearDown;
set_up();
@@ -146,11 +190,11 @@ fn test_http_proxy_with_no_proxy() {
("header", list(vec![s("User-Agent: foo")])),
]),
)]);
- assert_eq!(expected, options);
+ assert_options_eq(&expected, &options);
}
#[test]
-#[ignore]
+#[serial_test::serial]
fn test_http_proxy_with_no_proxy_wildcard() {
let _tear_down = TearDown;
set_up();
@@ -178,10 +222,11 @@ fn test_http_proxy_with_no_proxy_wildcard() {
("header", list(vec![s("User-Agent: foo")])),
]),
)]);
- assert_eq!(expected, options);
+ assert_options_eq(&expected, &options);
}
#[test]
+#[serial_test::serial]
#[ignore]
fn test_options_are_preserved() {
let _tear_down = TearDown;
@@ -225,10 +270,11 @@ fn test_options_are_preserved() {
("follow_location", PhpMixed::Int(1)),
]),
)]);
- assert_eq!(expected, options);
+ assert_options_eq(&expected, &options);
}
#[test]
+#[serial_test::serial]
#[ignore]
fn test_http_proxy_without_port() {
let _tear_down = TearDown;
@@ -263,11 +309,11 @@ fn test_http_proxy_without_port() {
("follow_location", PhpMixed::Int(1)),
]),
)]);
- assert_eq!(expected, options);
+ assert_options_eq(&expected, &options);
}
#[test]
-#[ignore]
+#[serial_test::serial]
fn test_https_proxy_override() {
let _tear_down = TearDown;
set_up();
@@ -293,7 +339,7 @@ fn test_https_proxy_override() {
}
#[test]
-#[ignore]
+#[serial_test::serial]
fn test_ssl_proxy() {
let _tear_down = TearDown;
for (expected, proxy) in [
@@ -322,7 +368,7 @@ fn test_ssl_proxy() {
("header", list(vec![s("User-Agent: foo")])),
]),
)]);
- assert_eq!(expected_options, options);
+ assert_options_eq(&expected_options, &options);
} else {
// The catch in PHP asserts the exception is a TransportException; the return type
// here already guarantees that.
@@ -339,6 +385,7 @@ fn test_ssl_proxy() {
}
#[test]
+#[serial_test::serial]
fn test_ensure_thatfix_http_header_field_moves_content_type_to_end_of_options() {
let _tear_down = TearDown;
set_up();
@@ -370,6 +417,7 @@ fn test_ensure_thatfix_http_header_field_moves_content_type_to_end_of_options()
}
#[test]
+#[serial_test::serial]
#[ignore]
fn test_init_options_does_include_proxy_auth_headers() {
let _tear_down = TearDown;
@@ -397,6 +445,7 @@ fn test_init_options_does_include_proxy_auth_headers() {
}
#[test]
+#[serial_test::serial]
#[ignore]
fn test_init_options_for_curl_does_not_include_proxy_auth_headers() {
let _tear_down = TearDown;