diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-07-12 01:28:17 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-07-16 01:02:47 +0900 |
| commit | 4d9e3dd6176a0cd2cc5e158b044beeb7b3de21be (patch) | |
| tree | ed19d9660f40a1bc93c5629c7a6c7bf41cf884e8 /crates/shirabe/tests/util | |
| parent | 4e1170c2328dd8007a5d737a759cd18030b1200b (diff) | |
| download | php-shirabe-4d9e3dd6176a0cd2cc5e158b044beeb7b3de21be.tar.gz php-shirabe-4d9e3dd6176a0cd2cc5e158b044beeb7b3de21be.tar.zst php-shirabe-4d9e3dd6176a0cd2cc5e158b044beeb7b3de21be.zip | |
test(util): port remaining todo!() tests in util test suite
Implement previously-todo!() tests in auth_helper_test.rs,
process_executor_test.rs, remote_filesystem_test.rs, and
stream_context_factory_test.rs by porting the corresponding PHPUnit
test methods. Extend IOStub with writeRaw/setAuthentication call
tracking and askAndValidate/getAuthentication overrides to model the
PHPUnit mocks these tests rely on, deduping the resulting
call-recording fields into a small generic CallRecorder<T> helper
instead of repeating the same RefCell<Vec<T>> push/borrow().clone()
boilerplate five times.
testStoreAuthWithPromptInvalidAnswer and
testPromptAuthIfNeededMultipleBitbucketDownloads had initially lost
the ported PHPUnit mock's argument/call-count assertions
(askAndValidate's exact prompt string, and
hasAuthentication/getAuthentication's exactly(2) call counts),
silently narrowing what the tests verify; IOStub now records these
calls and the tests assert on them, matching upstream.
Tests left unportable (PHP set_error_handler machinery, closures in
data providers, network/subclass-mock dependencies, etc.) keep
#[ignore] with a single // TODO(phase-d) reason recorded in the
function body.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/util')
| -rw-r--r-- | crates/shirabe/tests/util/auth_helper_test.rs | 235 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/error_handler_test.rs | 11 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/process_executor_test.rs | 41 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/remote_filesystem_test.rs | 56 | ||||
| -rw-r--r-- | crates/shirabe/tests/util/stream_context_factory_test.rs | 79 |
5 files changed, 387 insertions, 35 deletions
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; |
