aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/tests/package/loader/root_package_loader_test.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-27 17:21:00 +0900
committernsfisis <nsfisis@gmail.com>2026-06-27 17:26:28 +0900
commite98823e599eb375b30037cc714710e3309d927d1 (patch)
treee1689ec164086798fc46aa6a58d1e914cf4873b2 /crates/shirabe/tests/package/loader/root_package_loader_test.rs
parent20f620bdd0b5764ed2e9812dc0772f907b0d6f29 (diff)
downloadphp-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.rs232
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()
+ );
}