diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:21:00 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-06-27 17:26:28 +0900 |
| commit | e98823e599eb375b30037cc714710e3309d927d1 (patch) | |
| tree | e1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/package/loader/root_package_loader_test.rs | |
| parent | 20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff) | |
| download | php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.gz php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.tar.zst php-shirabe-e98823e599eb375b30037cc714710e3309d927d1.zip | |
test: port Composer tests unblocked by mockall, add seams
Port 11 categories of previously-ignored Composer tests now reachable with
the mockall crate: DownloadManager, VCS/Perforce/File downloaders,
VersionSelector, PlatformRepository, Auditor, installer/FilesystemRepository,
RootPackageLoader, util auth/http, commands, and Cache.
Extract test seams additively on concrete structs as *Interface traits
(Runtime, HhvmDetector, VersionGuesser, RepositorySet, Perforce,
BinaryInstaller) plus mock-field seams (Cache, Filesystem); consumers take
trait objects. Mocks are defined locally in the test crates via
mockall::mock!, since automock-generated mocks are cfg(test)-gated and
invisible across the integration-test boundary.
dataProviders are ported in full; tests blocked by unported shims stay
#[ignore] with documented reasons rather than reduced or weakened.
Fix product bugs surfaced by the ports:
- util/github: use the exception code, not the HTTP status, for 401/403
- advisory: serialize empty audit maps as [] to match PHP json_encode
- repository/filesystem and downloader/file: fix RefCell double-borrow panics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/tests/package/loader/root_package_loader_test.rs')
| -rw-r--r-- | crates/shirabe/tests/package/loader/root_package_loader_test.rs | 232 |
1 files changed, 220 insertions, 12 deletions
diff --git a/crates/shirabe/tests/package/loader/root_package_loader_test.rs b/crates/shirabe/tests/package/loader/root_package_loader_test.rs index 23c8beb..7fa970a 100644 --- a/crates/shirabe/tests/package/loader/root_package_loader_test.rs +++ b/crates/shirabe/tests/package/loader/root_package_loader_test.rs @@ -4,21 +4,28 @@ // ProcessExecutor / VersionGuesser or require constraints whose parsing goes through a // look-around regex the regex crate cannot compile. -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::rc::Rc; use indexmap::IndexMap; +use serial_test::serial; use shirabe::config::Config; use shirabe::io::IOInterface; use shirabe::io::null_io::NullIO; +use shirabe::package::RootPackage; use shirabe::package::loader::RootPackageLoader; -use shirabe::package::version::{VersionGuesser, VersionParser}; +use shirabe::package::version::{ + VersionData, VersionGuesser, VersionGuesserInterface, VersionParser, +}; use shirabe::package::{STABILITY_ALPHA, STABILITY_DEV, STABILITY_RC}; use shirabe::repository::RepositoryManager; +use shirabe::util::Git as GitUtil; use shirabe::util::http_downloader::HttpDownloader; -use shirabe::util::process_executor::ProcessExecutor; +use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor}; use shirabe_php_shim::PhpMixed; +use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock}; + fn null_io() -> Rc<RefCell<dyn IOInterface>> { Rc::new(RefCell::new(NullIO::new())) } @@ -35,6 +42,73 @@ fn http_downloader( ))) } +// `$config = new Config; $config->merge(['repositories' => ['packagist' => false]]);` +fn make_config() -> Rc<RefCell<Config>> { + let config = Rc::new(RefCell::new(Config::new(true, None))); + let mut repositories: IndexMap<String, PhpMixed> = IndexMap::new(); + repositories.insert("packagist".to_string(), PhpMixed::Bool(false)); + let mut merge: IndexMap<String, PhpMixed> = IndexMap::new(); + merge.insert("repositories".to_string(), PhpMixed::Array(repositories)); + config.borrow_mut().merge(&merge, "test"); + config +} + +// Stands in for `getMockBuilder('Composer\Repository\RepositoryManager')->disableOriginalConstructor()`. +// The loader only stores it and feeds it default repositories, so a real instance suffices. +fn make_manager( + io: &Rc<RefCell<dyn IOInterface>>, + config: &Rc<RefCell<Config>>, +) -> Rc<RefCell<RepositoryManager>> { + Rc::new(RefCell::new(RepositoryManager::new( + io.clone(), + config.clone(), + http_downloader(io, config), + None, + None, + ))) +} + +fn require_map(entries: &[(&str, &str)]) -> PhpMixed { + let mut m: IndexMap<String, PhpMixed> = IndexMap::new(); + for (k, v) in entries { + m.insert(k.to_string(), PhpMixed::String(v.to_string())); + } + PhpMixed::Array(m) +} + +// Resets the cached git `version` static on drop so a seeded value does not leak into other +// tests in this binary (VersionGuesserTest seeds/resets the same static). +struct GitVersionGuard; + +impl Drop for GitVersionGuard { + fn drop(&mut self) { + GitUtil::__reset_version(); + } +} + +// A test double for the concrete VersionGuesser, supplied through the VersionGuesserInterface seam. +#[derive(Debug)] +struct VersionGuesserMock { + version_data: VersionData, + guess_version_calls: Rc<Cell<u32>>, +} + +impl VersionGuesserInterface for VersionGuesserMock { + fn guess_version( + &mut self, + _package_config: &IndexMap<String, PhpMixed>, + _path: &str, + ) -> anyhow::Result<Option<VersionData>> { + self.guess_version_calls + .set(self.guess_version_calls.get() + 1); + Ok(Some(self.version_data.clone())) + } + + fn get_root_version_from_env(&self) -> anyhow::Result<String> { + unreachable!("COMPOSER_ROOT_VERSION is not set in this test") + } +} + #[test] #[ignore = "process_executor.enable_async() drives the async stream path, which calls stream_set_blocking (fcntl(2) todo!() in shirabe-php-shim::stream)"] fn test_stability_flags_parsing() { @@ -70,7 +144,8 @@ fn test_stability_flags_parsing() { Some(io.clone()), ); - let mut loader = RootPackageLoader::new(manager, config.clone(), None, Some(guesser), None); + let mut loader = + RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None); let mut data = IndexMap::new(); data.insert( @@ -136,25 +211,158 @@ fn test_stability_flags_parsing() { } #[test] -#[ignore = "requires getProcessExecutorMock with expects(['return' => 1]); no ProcessExecutorMock mocking infrastructure exists"] +#[serial] fn test_no_version_is_visible_in_pretty_version() { - todo!() + GitUtil::__reset_version(); + let _git_guard = GitVersionGuard; + + let io = null_io(); + let config = make_config(); + let manager = make_manager(&io, &config); + + let (process, _guard) = get_process_executor_mock( + vec![], + false, + MockHandler { + r#return: 1, + ..Default::default() + }, + ); + let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None); + + let mut loader = + RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None); + + let package = loader + .load(IndexMap::new(), "Composer\\Package\\RootPackage", None) + .unwrap(); + let package = package.as_root().unwrap(); + + assert_eq!("1.0.0.0", package.get_version()); + assert_eq!( + RootPackage::DEFAULT_PRETTY_VERSION, + package.get_pretty_version() + ); } #[test] -#[ignore = "requires getMockBuilder VersionGuesser mock with guessVersion expectation; no VersionGuesser mocking infrastructure exists"] +#[serial] fn test_pretty_version_for_root_package_in_version_branch() { - todo!() + // see #6845 + let io = null_io(); + let config = make_config(); + let manager = make_manager(&io, &config); + + let guess_version_calls = Rc::new(Cell::new(0u32)); + let version_guesser = VersionGuesserMock { + version_data: VersionData { + version: Some("3.0.9999999.9999999-dev".to_string()), + commit: Some("aabbccddee".to_string()), + pretty_version: Some("3.0-dev".to_string()), + feature_version: None, + feature_pretty_version: None, + }, + guess_version_calls: guess_version_calls.clone(), + }; + + let mut loader = RootPackageLoader::new( + manager, + config.clone(), + None, + Some(Box::new(version_guesser)), + None, + ); + + let package = loader + .load(IndexMap::new(), "Composer\\Package\\RootPackage", None) + .unwrap(); + + assert!(guess_version_calls.get() >= 1); + assert_eq!("3.0-dev", package.as_root().unwrap().get_pretty_version()); } #[test] -#[ignore = "requires getProcessExecutorMock with expects() git command expectations; no ProcessExecutorMock mocking infrastructure exists"] +#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"] +#[serial] fn test_feature_branch_pretty_version() { - todo!() + // proc_open() is always available; the PHP markTestSkipped guard does not apply here. + GitUtil::__set_version(Some("2.52.0".to_string())); + let _git_guard = GitVersionGuard; + + let io = null_io(); + let config = make_config(); + let manager = make_manager(&io, &config); + + let expectations: Vec<MockExpectation> = vec![ + cmd_full( + ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"], + 0, + "* latest-production 38137d2f6c70e775e137b2d8a7a7d3eaebf7c7e5 Commit message\n master 4f6ed96b0bc363d2aa4404c3412de1c011f67c66 Commit message\n", + "", + ), + cmd(["git", "rev-list", "master..latest-production"]), + ]; + let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default()); + let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None); + + let mut loader = + RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None); + + let mut data = IndexMap::new(); + data.insert( + "require".to_string(), + require_map(&[("foo/bar", "self.version")]), + ); + + let package = loader + .load(data, "Composer\\Package\\RootPackage", None) + .unwrap(); + + assert_eq!( + "dev-master", + package.as_root().unwrap().get_pretty_version() + ); } #[test] -#[ignore = "requires getProcessExecutorMock with expects() git command expectations; no ProcessExecutorMock mocking infrastructure exists"] +#[serial] fn test_non_feature_branch_pretty_version() { - todo!() + // proc_open() is always available; the PHP markTestSkipped guard does not apply here. + GitUtil::__set_version(Some("2.52.0".to_string())); + let _git_guard = GitVersionGuard; + + let io = null_io(); + let config = make_config(); + let manager = make_manager(&io, &config); + + let expectations: Vec<MockExpectation> = vec![cmd_full( + ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"], + 0, + "* latest-production 38137d2f6c70e775e137b2d8a7a7d3eaebf7c7e5 Commit message\n master 4f6ed96b0bc363d2aa4404c3412de1c011f67c66 Commit message\n", + "", + )]; + let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default()); + let guesser = VersionGuesser::new(config.clone(), process, VersionParser::new(), None); + + let mut loader = + RootPackageLoader::new(manager, config.clone(), None, Some(Box::new(guesser)), None); + + let mut data = IndexMap::new(); + data.insert( + "require".to_string(), + require_map(&[("foo/bar", "self.version")]), + ); + data.insert( + "non-feature-branches".to_string(), + PhpMixed::List(vec![PhpMixed::String("latest-.*".to_string())]), + ); + + let package = loader + .load(data, "Composer\\Package\\RootPackage", None) + .unwrap(); + + assert_eq!( + "dev-latest-production", + package.as_root().unwrap().get_pretty_version() + ); } |
