aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 17:49:35 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commitd4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad (patch)
tree0a85bd085961fcdfd5570cafc3b44ba5eb8cb152 /crates/shirabe
parent22d0b327368e5f39de6f381046c081d08efdba15 (diff)
downloadphp-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.tar.gz
php-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.tar.zst
php-shirabe-d4ec1dcb6fc04ff029ded35cc2fcc14f3bbc02ad.zip
test: port 32 command/repository/downloader tests
Add create_installed_json/create_composer_lock test helpers. Port command (8), repository path/forgejo/perforce/vcs (11), and fossil/hg/download_manager (13) tests. Fix production porting bugs: root_package_loader/forgejo_url/version_bumper regex delimiters, repository_manager create_repository_by_class, array_loader isset, licenses_command RefCell borrow; implement disk_free_space and touch2/touch3 via libc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe')
-rw-r--r--crates/shirabe/src/command/licenses_command.rs10
-rw-r--r--crates/shirabe/src/package/loader/array_loader.rs22
-rw-r--r--crates/shirabe/src/package/loader/root_package_loader.rs14
-rw-r--r--crates/shirabe/src/package/version/version_bumper.rs12
-rw-r--r--crates/shirabe/src/repository/handle.rs8
-rw-r--r--crates/shirabe/src/repository/installed_repository.rs6
-rw-r--r--crates/shirabe/src/repository/repository_manager.rs26
-rw-r--r--crates/shirabe/src/repository/vcs_repository.rs12
-rw-r--r--crates/shirabe/src/util/forgejo_url.rs2
-rw-r--r--crates/shirabe/tests/command/bump_command_test.rs206
-rw-r--r--crates/shirabe/tests/command/fund_command_test.rs198
-rw-r--r--crates/shirabe/tests/command/licenses_command_test.rs321
-rw-r--r--crates/shirabe/tests/command/status_command_test.rs41
-rw-r--r--crates/shirabe/tests/command/suggests_command_test.rs387
-rw-r--r--crates/shirabe/tests/common/test_case.rs100
-rw-r--r--crates/shirabe/tests/dbg_test.rs1
-rw-r--r--crates/shirabe/tests/downloader/download_manager_test.rs109
-rw-r--r--crates/shirabe/tests/downloader/fossil_downloader_test.rs181
-rw-r--r--crates/shirabe/tests/downloader/hg_downloader_test.rs172
-rw-r--r--crates/shirabe/tests/downloader/main.rs2
-rw-r--r--crates/shirabe/tests/repository/path_repository_test.rs16
-rw-r--r--crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs278
-rw-r--r--crates/shirabe/tests/repository/vcs/perforce_driver_test.rs11
-rw-r--r--crates/shirabe/tests/repository/vcs_repository_test.rs220
24 files changed, 2114 insertions, 241 deletions
diff --git a/crates/shirabe/src/command/licenses_command.rs b/crates/shirabe/src/command/licenses_command.rs
index 3c1745a..3be4df3 100644
--- a/crates/shirabe/src/command/licenses_command.rs
+++ b/crates/shirabe/src/command/licenses_command.rs
@@ -107,8 +107,7 @@ impl Command for LicensesCommand {
input: Rc<RefCell<dyn InputInterface>>,
output: Rc<RefCell<dyn OutputInterface>>,
) -> anyhow::Result<i64> {
- let composer = self.require_composer(None, None)?;
- let mut composer = crate::command::composer_full_mut(&composer);
+ let composer_handle = self.require_composer(None, None)?;
// TODO(plugin): dispatch COMMAND event for plugin hooks
let command_event = CommandEvent::new(
@@ -117,11 +116,14 @@ impl Command for LicensesCommand {
input.clone(),
output.clone(),
);
- composer
- .get_event_dispatcher()
+ // The event dispatcher reads back through the shared Composer handle (script listeners), so the
+ // dispatch must run while no other borrow of that handle is held.
+ let event_dispatcher = composer_handle.borrow_partial().get_event_dispatcher();
+ event_dispatcher
.borrow_mut()
.dispatch(Some(command_event.get_name()), None);
+ let mut composer = crate::command::composer_full_mut(&composer_handle);
let root = composer.get_package();
let packages = if input
diff --git a/crates/shirabe/src/package/loader/array_loader.rs b/crates/shirabe/src/package/loader/array_loader.rs
index 844b3ef..20280e0 100644
--- a/crates/shirabe/src/package/loader/array_loader.rs
+++ b/crates/shirabe/src/package/loader/array_loader.rs
@@ -370,7 +370,7 @@ impl ArrayLoader {
package.package_mut().set_is_default_branch(true);
}
- if let Some(source) = config.get("source").cloned() {
+ if let Some(source) = config.get("source").filter(|v| !v.is_null()).cloned() {
let source_map = match &source {
PhpMixed::Array(m) => Some(m.clone()),
_ => None,
@@ -404,9 +404,12 @@ impl ArrayLoader {
package
.package_mut()
.set_source_url(source_map.get("url").map(|v| strval(v)));
- package
- .package_mut()
- .set_source_reference(source_map.get("reference").map(|v| strval(v)));
+ package.package_mut().set_source_reference(
+ source_map
+ .get("reference")
+ .filter(|v| !v.is_null())
+ .map(|v| strval(v)),
+ );
if let Some(mirrors) = source_map.get("mirrors") {
package
.package_mut()
@@ -414,7 +417,7 @@ impl ArrayLoader {
}
}
- if let Some(dist) = config.get("dist").cloned() {
+ if let Some(dist) = config.get("dist").filter(|v| !v.is_null()).cloned() {
let dist_map = match &dist {
PhpMixed::Array(m) => Some(m.clone()),
_ => None,
@@ -446,9 +449,12 @@ impl ArrayLoader {
package
.package_mut()
.set_dist_url(dist_map.get("url").map(|v| strval(v)));
- package
- .package_mut()
- .set_dist_reference(dist_map.get("reference").map(|v| strval(v)));
+ package.package_mut().set_dist_reference(
+ dist_map
+ .get("reference")
+ .filter(|v| !v.is_null())
+ .map(|v| strval(v)),
+ );
package
.package_mut()
.set_dist_sha1_checksum(dist_map.get("shasum").map(|v| strval(v)));
diff --git a/crates/shirabe/src/package/loader/root_package_loader.rs b/crates/shirabe/src/package/loader/root_package_loader.rs
index 9cd1eac..308706f 100644
--- a/crates/shirabe/src/package/loader/root_package_loader.rs
+++ b/crates/shirabe/src/package/loader/root_package_loader.rs
@@ -266,7 +266,7 @@ impl RootPackageLoader {
for (req_name, req_version) in requires {
let mut m: IndexMap<CaptureKey, String> = IndexMap::new();
if Preg::is_match3(
- r"(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *)",
+ r"{(?:^|\| *|, *)([^,\s#|]+)(?:#[^ ]+)? +as +([^,\s|]+)(?:$| *\|| *,)}",
req_version,
Some(&mut m),
) {
@@ -320,7 +320,7 @@ impl RootPackageLoader {
for (req_name, req_version) in requires {
let mut constraints: Vec<String> = vec![];
- let or_split = Preg::split(r"\s*\|\|?\s*", req_version.trim());
+ let or_split = Preg::split(r"{\s*\|\|?\s*}", req_version.trim());
for or_constraint in &or_split {
let and_split = shirabe_semver::split_and_constraints(or_constraint);
for and_constraint in and_split {
@@ -329,7 +329,7 @@ impl RootPackageLoader {
}
let stability_names: Vec<&str> = stabilities.keys().copied().collect();
- let pattern = format!("^[^@]*?@({})$", stability_names.join("|"));
+ let pattern = format!("{{^[^@]*?@({})$}}i", stability_names.join("|"));
let mut matched = false;
for constraint in &constraints {
@@ -353,8 +353,8 @@ impl RootPackageLoader {
}
for constraint in &constraints {
- let req_version_stripped = Preg::replace(r"^([^,\s@]+) as .+$", "$1", constraint);
- if Preg::is_match(r"^[^,\s@]+$", &req_version_stripped) {
+ let req_version_stripped = Preg::replace(r"{^([^,\s@]+) as .+$}", "$1", constraint);
+ if Preg::is_match(r"{^[^,\s@]+$}", &req_version_stripped) {
let stability_name = VersionParser::parse_stability(&req_version_stripped);
if stability_name != "stable" {
let name = strtolower(req_name);
@@ -378,9 +378,9 @@ impl RootPackageLoader {
mut references: IndexMap<String, String>,
) -> IndexMap<String, String> {
for (req_name, req_version) in requires {
- let req_version = Preg::replace(r"^([^,\s@]+) as .+$", "$1", req_version);
+ let req_version = Preg::replace(r"{^([^,\s@]+) as .+$}", "$1", req_version);
let mut m: IndexMap<CaptureKey, String> = IndexMap::new();
- if Preg::is_match3(r"^[^,\s@]+?#([a-f0-9]+)$", &req_version, Some(&mut m))
+ if Preg::is_match3(r"{^[^,\s@]+?#([a-f0-9]+)$}", &req_version, Some(&mut m))
&& VersionParser::parse_stability(&req_version) == "dev"
{
let name = strtolower(req_name);
diff --git a/crates/shirabe/src/package/version/version_bumper.rs b/crates/shirabe/src/package/version/version_bumper.rs
index c029e3b..4deb450 100644
--- a/crates/shirabe/src/package/version/version_bumper.rs
+++ b/crates/shirabe/src/package/version/version_bumper.rs
@@ -54,9 +54,17 @@ impl VersionBumper {
return Ok(pretty_constraint);
}
+ // Regex pattern compatibility:
+ // PHP uses zero-width look-behind `(?<=,|\ |\||^)` and look-ahead `(?=,|$|\ |\||@)` to require
+ // a separator around the constraint. The `regex` crate supports neither, so the separators are
+ // matched as *consuming* groups instead. Only the `constraint` named group's own offset/length
+ // is used for the replacement below (never group 0), so consuming the surrounding separators
+ // does not shift the rewrite. The single-character `|`/space/comma separators are not consumed
+ // by both an adjacent trailing and leading match, but Composer never packs two same-major
+ // constraints around a single such separator (alternatives use `||`, ranges use spaces).
let pattern = format!(
r#"{{
- (?<=,|\ |\||^) # leading separator
+ (?:^|[,\ |]) # leading separator
(?P<constraint>
\^v?{major}(?:\.\d+)* # e.g. ^2.anything
| ~v?{major}(?:\.\d+){{1,3}} # e.g. ~2.2 or ~2.2.2 or ~2.2.2.2
@@ -64,7 +72,7 @@ impl VersionBumper {
| >=v?\d(?:\.\d+)* # e.g. >=2 or >=1.2 etc
| \* # full wildcard
)
- (?=,|$|\ |\||@) # trailing separator
+ (?:$|[,\ |@]) # trailing separator
}}x"#,
major = major
);
diff --git a/crates/shirabe/src/repository/handle.rs b/crates/shirabe/src/repository/handle.rs
index 2e83ef8..90a9090 100644
--- a/crates/shirabe/src/repository/handle.rs
+++ b/crates/shirabe/src/repository/handle.rs
@@ -148,6 +148,14 @@ impl RepositoryInterfaceHandle {
// --- InstalledRepositoryInterface helpers (valid only when the wrapped repository is one) ---
+ /// PHP `$repository instanceof InstalledRepositoryInterface`.
+ pub fn is_installed_repository_interface(&self) -> bool {
+ self.0
+ .borrow()
+ .as_installed_repository_interface()
+ .is_some()
+ }
+
pub fn is_fresh(&self) -> bool {
self.0
.borrow()
diff --git a/crates/shirabe/src/repository/installed_repository.rs b/crates/shirabe/src/repository/installed_repository.rs
index 55ec50f..cfa5a7a 100644
--- a/crates/shirabe/src/repository/installed_repository.rs
+++ b/crates/shirabe/src/repository/installed_repository.rs
@@ -364,12 +364,14 @@ impl InstalledRepository {
}
pub fn add_repository(&mut self, repository: RepositoryInterfaceHandle) {
- // TODO: type guard?
assert!(
repository.is::<LockArrayRepository>()
+ || repository.is_installed_repository_interface()
|| repository.is::<RootPackageRepository>()
|| repository.is::<PlatformRepository>(),
- "An InstalledRepository can contain a repository of type: LockArrayRepository, RootPackageRepository or PlatformRepository"
+ "An InstalledRepository can not contain a repository of type {} ({})",
+ repository.get_repo_name(),
+ repository.get_repo_name()
);
self.inner.add_repository(repository);
diff --git a/crates/shirabe/src/repository/repository_manager.rs b/crates/shirabe/src/repository/repository_manager.rs
index 403b504..247236c 100644
--- a/crates/shirabe/src/repository/repository_manager.rs
+++ b/crates/shirabe/src/repository/repository_manager.rs
@@ -147,10 +147,30 @@ impl RepositoryManager {
fn create_repository_by_class(
&self,
- _class: &str,
- _config: IndexMap<String, PhpMixed>,
+ class: &str,
+ config: IndexMap<String, PhpMixed>,
) -> anyhow::Result<RepositoryInterfaceHandle> {
- todo!("Phase B: dynamic class instantiation by class name")
+ // PHP: `new $class($config, $this->io, $this->config, $this->httpDownloader,
+ // $this->eventDispatcher, $this->process)`. Rust cannot instantiate by string class name, so
+ // dispatch over the classes registered in `createDefaultRepositoryManager`.
+ match class {
+ "Composer\\Repository\\ComposerRepository" => Ok(RepositoryInterfaceHandle::new(
+ crate::repository::ComposerRepository::new(
+ config,
+ self.io.clone(),
+ &self.config.borrow(),
+ self.http_downloader.clone(),
+ self.event_dispatcher.clone(),
+ )?,
+ )),
+ "Composer\\Repository\\PackageRepository" => Ok(RepositoryInterfaceHandle::new(
+ crate::repository::PackageRepository::new(config),
+ )),
+ other => todo!(
+ "Phase B: dynamic class instantiation by class name: {}",
+ other
+ ),
+ }
}
pub fn set_repository_class(&mut self, r#type: &str, class: &str) {
diff --git a/crates/shirabe/src/repository/vcs_repository.rs b/crates/shirabe/src/repository/vcs_repository.rs
index 526aa01..7a143eb 100644
--- a/crates/shirabe/src/repository/vcs_repository.rs
+++ b/crates/shirabe/src/repository/vcs_repository.rs
@@ -264,6 +264,16 @@ impl VcsRepository {
&self.version_transport_exceptions
}
+ /// For testing only: drives `initialize` (which shells out to the VCS driver to discover
+ /// tags/branches and load each package) and returns the packages collected by the inner
+ /// `ArrayRepository`, mirroring the polymorphic `RepositoryInterface::getPackages` dispatch
+ /// in PHP where `VcsRepository` inherits `getPackages` from `ArrayRepository`.
+ pub fn __get_packages(&mut self) -> anyhow::Result<Vec<crate::package::BasePackageHandle>> {
+ self.initialize()?;
+ use crate::repository::RepositoryInterface;
+ self.inner.get_packages()
+ }
+
pub fn initialize(&mut self) -> Result<()> {
self.inner.initialize();
@@ -282,7 +292,7 @@ impl VcsRepository {
self.version_parser = Some(VersionParser::new());
if self.loader.is_none() {
self.loader = Some(Box::new(ArrayLoader::new(
- Some(todo!("phase-b: clone VersionParser")),
+ self.version_parser.clone(),
false,
)));
}
diff --git a/crates/shirabe/src/util/forgejo_url.rs b/crates/shirabe/src/util/forgejo_url.rs
index 85f2238..d336b87 100644
--- a/crates/shirabe/src/util/forgejo_url.rs
+++ b/crates/shirabe/src/util/forgejo_url.rs
@@ -14,7 +14,7 @@ pub struct ForgejoUrl {
impl ForgejoUrl {
pub const URL_REGEX: &'static str =
- r"^(?:(?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/([^/]+?)(?:\.git|/)?$";
+ r"{^(?:(?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/([^/]+?)(?:\.git|/)?$}";
fn new(owner: String, repository: String, origin_url: String, api_url: String) -> Self {
Self {
diff --git a/crates/shirabe/tests/command/bump_command_test.rs b/crates/shirabe/tests/command/bump_command_test.rs
index 7accb75..509b42a 100644
--- a/crates/shirabe/tests/command/bump_command_test.rs
+++ b/crates/shirabe/tests/command/bump_command_test.rs
@@ -1,16 +1,206 @@
//! ref: composer/tests/Composer/Test/Command/BumpCommandTest.php
-use crate::test_case::{RunOptions, get_application_tester, init_temp_composer};
+use crate::test_case::{
+ RunOptions, create_composer_lock, create_installed_json, get_application_tester,
+ get_complete_package, init_temp_composer,
+};
use serial_test::serial;
+use shirabe::json::JsonFile;
+use shirabe::package::handle::PackageInterfaceHandle;
use shirabe_php_shim::PhpMixed;
-#[ignore = "needs create_installed_json / create_composer_lock helpers \
- (InstalledFilesystemRepository / Locker::set_lock_data), and BumpCommand::execute calls \
- require_composer -> Factory, reaching ProcessExecutor (git) -> shirabe-php-shim \
- stream_set_blocking (stream.rs todo!(), requires fcntl(2))"]
+/// ref: BumpCommandTest::testBump (data provider rolled into one body).
+fn run_bump_case(
+ composer_json: serde_json::Value,
+ command: &[(&str, PhpMixed)],
+ expected: serde_json::Value,
+ lock: bool,
+ exit_code: i32,
+) {
+ let tear_down = init_temp_composer(Some(&composer_json), None, None, true);
+
+ let packages: Vec<PackageInterfaceHandle> = vec![
+ get_complete_package("first/pkg", "2.3.4").into(),
+ get_complete_package("second/pkg", "3.4.0").into(),
+ ];
+ let dev_packages: Vec<PackageInterfaceHandle> =
+ vec![get_complete_package("dev/pkg", "2.3.4.5").into()];
+
+ create_installed_json(&packages, &dev_packages, true);
+ if lock {
+ create_composer_lock(&packages, &dev_packages);
+ }
+
+ let mut input: Vec<(PhpMixed, PhpMixed)> =
+ vec![(PhpMixed::from("command"), PhpMixed::from("bump"))];
+ for (k, v) in command {
+ input.push((PhpMixed::from(*k), v.clone()));
+ }
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester.run(input, RunOptions::default()).unwrap();
+ assert_eq!(exit_code, status_code);
+
+ let mut json = JsonFile::new("./composer.json".to_string(), None, None).unwrap();
+ let read = json.read().unwrap();
+ let actual: serde_json::Value = serde_json::from_str(&JsonFile::encode(&read)).unwrap();
+ assert_eq!(expected, actual);
+
+ drop(tear_down);
+}
+
#[test]
+#[serial]
fn test_bump() {
- todo!()
+ // 'bump all by default'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^v2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ &[],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump only dev with --dev-only'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ &[("--dev-only", PhpMixed::from(true))],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump only non-dev with --no-dev-only'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ &[("--no-dev-only", PhpMixed::from(true))],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump only listed with packages arg'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ &[(
+ "packages",
+ PhpMixed::List(vec![
+ PhpMixed::from("first/pkg:3.0.1"),
+ PhpMixed::from("dev/*"),
+ ]),
+ )],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump works from installed repo without lock file'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ }),
+ &[],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ }),
+ false,
+ 0,
+ );
+
+ // 'bump with --dry-run with packages to bump'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ &[("--dry-run", PhpMixed::from(true))],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.0", "second/pkg": "3.*" },
+ "require-dev": { "dev/pkg": "~2.0" },
+ }),
+ true,
+ 1,
+ );
+
+ // 'bump with --dry-run without packages to bump'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ &[("--dry-run", PhpMixed::from(true))],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump works with non-standard package'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "php": ">=5.3", "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ &[],
+ serde_json::json!({
+ "require": { "php": ">=5.3", "first/pkg": "^2.3.4", "second/pkg": "^3.4" },
+ "require-dev": { "dev/pkg": "^2.3.4.5" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump works with unknown package'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4", "third/pkg": "^1.2" },
+ }),
+ &[],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "^3.4", "third/pkg": "^1.2" },
+ }),
+ true,
+ 0,
+ );
+
+ // 'bump works with aliased package'
+ run_bump_case(
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "dev-bugfix as 3.4.x-dev" },
+ }),
+ &[],
+ serde_json::json!({
+ "require": { "first/pkg": "^2.3.4", "second/pkg": "dev-bugfix as 3.4.x-dev" },
+ }),
+ true,
+ 0,
+ );
}
#[test]
@@ -44,10 +234,6 @@ fn test_bump_fails_on_non_existing_composer_file() {
#[test]
#[serial]
-#[ignore = "CurlDownloader::new no longer panics (HTTP layer is now reqwest-based), but this path \
- still reaches an unrelated todo!() in shirabe-php-shim stream_set_blocking (stream.rs), \
- which requires fcntl(2). The companion test_bump_fails_on_non_existing_composer_file \
- covers the same error-output capture path without reaching that shim."]
fn test_bump_fails_on_write_error_to_composer_file() {
if shirabe_php_shim::function_exists("posix_getuid") && shirabe_php_shim::posix_getuid() == 0 {
// ref: $this->markTestSkipped('Cannot run as root');
diff --git a/crates/shirabe/tests/command/fund_command_test.rs b/crates/shirabe/tests/command/fund_command_test.rs
index f75090c..bbcc589 100644
--- a/crates/shirabe/tests/command/fund_command_test.rs
+++ b/crates/shirabe/tests/command/fund_command_test.rs
@@ -1,7 +1,201 @@
//! ref: composer/tests/Composer/Test/Command/FundCommandTest.php
-#[ignore = "missing TestCase::init_temp_composer, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"]
+use crate::test_case::{
+ RunOptions, create_installed_json, get_application_tester, get_complete_package,
+ init_temp_composer,
+};
+use indexmap::IndexMap;
+use serial_test::serial;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe_php_shim::PhpMixed;
+
+fn funding_entry(r#type: &str, url: &str) -> IndexMap<String, PhpMixed> {
+ let mut m = IndexMap::new();
+ m.insert("type".to_string(), PhpMixed::from(r#type));
+ m.insert("url".to_string(), PhpMixed::from(url));
+ m
+}
+
+/// Runs one `useCaseProvider` case.
+fn run_fund_case(
+ composer_json: serde_json::Value,
+ command: &[(&str, PhpMixed)],
+ funding: &[(&str, IndexMap<String, PhpMixed>)],
+ expected: &str,
+) {
+ let tear_down = init_temp_composer(Some(&composer_json), None, None, true);
+
+ let first = get_complete_package("first/pkg", "2.3.4");
+ let stable = get_complete_package("stable/pkg", "1.0.0");
+ let dev = get_complete_package("dev/pkg", "2.3.4.5");
+
+ let by_name: IndexMap<&str, &CompletePackageHandle> = [
+ ("first/pkg", &first),
+ ("stable/pkg", &stable),
+ ("dev/pkg", &dev),
+ ]
+ .into_iter()
+ .collect();
+
+ for (pkg_name, info) in funding {
+ if let Some(pkg) = by_name.get(pkg_name) {
+ pkg.set_funding(vec![info.clone()]);
+ }
+ }
+
+ let packages: Vec<PackageInterfaceHandle> = vec![first.into(), stable.into()];
+ let dev_packages: Vec<PackageInterfaceHandle> = vec![dev.into()];
+
+ create_installed_json(&packages, &dev_packages, true);
+
+ let mut input: Vec<(PhpMixed, PhpMixed)> =
+ vec![(PhpMixed::from("command"), PhpMixed::from("fund"))];
+ for (k, v) in command {
+ input.push((PhpMixed::from(*k), v.clone()));
+ }
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ input,
+ RunOptions {
+ capture_stderr_separately: true,
+ ..RunOptions::default()
+ },
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
+ assert_eq!(expected.trim(), app_tester.get_display().trim());
+
+ drop(tear_down);
+}
+
#[test]
+#[serial]
+#[ignore = "FundCommand queries every repository (incl. the default packagist ComposerRepository) over \
+ HTTP for funding metadata before falling back to locally installed data, reaching \
+ shirabe-php-shim curl.rs curl_version (todo!()); this path needs real network access"]
fn test_fund_command() {
- todo!()
+ // 'no funding links present, locally or remotely'
+ run_fund_case(
+ serde_json::json!({
+ "repositories": [],
+ "require": { "first/pkg": "^2.0" },
+ "require-dev": { "dev/pkg": "~4.0" },
+ }),
+ &[],
+ &[],
+ "No funding links were found in your package dependencies. This doesn't mean they don't need your support!",
+ );
+
+ // 'funding links set locally are used as fallback if not found remotely'
+ run_fund_case(
+ serde_json::json!({
+ "repositories": [],
+ "require": { "first/pkg": "^2.0" },
+ "require-dev": { "dev/pkg": "~4.0" },
+ }),
+ &[],
+ &[
+ (
+ "first/pkg",
+ funding_entry("github", "https://github.com/composer-test-data"),
+ ),
+ (
+ "dev/pkg",
+ funding_entry("github", "https://github.com/composer-test-data-dev"),
+ ),
+ ],
+ "The following packages were found in your dependencies which publish funding information:
+
+dev
+ pkg
+ https://github.com/sponsors/composer-test-data-dev
+
+first
+ https://github.com/sponsors/composer-test-data
+
+Please consider following these links and sponsoring the work of package authors!
+Thank you!",
+ );
+
+ // 'funding links set remotely are used as primary if found'
+ run_fund_case(
+ serde_json::json!({
+ "repositories": [
+ {
+ "type": "package",
+ "package": [
+ { "name": "first/pkg", "version": "dev-foo", "funding": [{ "type": "github", "url": "https://github.com/test-should-not-be-used" }] },
+ { "name": "first/pkg", "version": "dev-main", "default-branch": true, "funding": [{ "type": "custom", "url": "https://example.org" }] },
+ { "name": "dev/pkg", "version": "dev-foo", "default-branch": true, "funding": [{ "type": "github", "url": "https://github.com/org" }] },
+ { "name": "stable/pkg", "version": "1.0.0", "funding": [{ "type": "github", "url": "org2" }] },
+ ],
+ },
+ ],
+ "require": { "first/pkg": "^2.0", "stable/pkg": "^1.0" },
+ "require-dev": { "dev/pkg": "~4.0" },
+ }),
+ &[],
+ &[
+ (
+ "first/pkg",
+ funding_entry("github", "https://github.com/composer-test-data"),
+ ),
+ (
+ "dev/pkg",
+ funding_entry("github", "https://github.com/composer-test-data-dev"),
+ ),
+ (
+ "stable/pkg",
+ funding_entry("github", "https://github.com/composer-test-data-stable"),
+ ),
+ ],
+ "The following packages were found in your dependencies which publish funding information:
+
+dev
+ pkg
+ https://github.com/sponsors/org
+
+first
+ https://example.org
+
+stable
+ https://github.com/sponsors/composer-test-data-stable
+
+Please consider following these links and sponsoring the work of package authors!
+Thank you!",
+ );
+
+ // 'format funding links as JSON'
+ run_fund_case(
+ serde_json::json!({
+ "repositories": [],
+ "require": { "first/pkg": "^2.0" },
+ "require-dev": { "dev/pkg": "~4.0" },
+ }),
+ &[("--format", PhpMixed::from("json"))],
+ &[
+ (
+ "first/pkg",
+ funding_entry("github", "https://github.com/composer-test-data"),
+ ),
+ (
+ "dev/pkg",
+ funding_entry("github", "https://github.com/composer-test-data-dev"),
+ ),
+ ],
+ r#"{
+ "dev": {
+ "https://github.com/sponsors/composer-test-data-dev": [
+ "pkg"
+ ]
+ },
+ "first": {
+ "https://github.com/sponsors/composer-test-data": [
+ "pkg"
+ ]
+ }
+}"#,
+ );
}
diff --git a/crates/shirabe/tests/command/licenses_command_test.rs b/crates/shirabe/tests/command/licenses_command_test.rs
index 6bfe832..f680621 100644
--- a/crates/shirabe/tests/command/licenses_command_test.rs
+++ b/crates/shirabe/tests/command/licenses_command_test.rs
@@ -1,71 +1,336 @@
//! ref: composer/tests/Composer/Test/Command/LicensesCommandTest.php
-fn set_up() {
- // Builds the temp project and installed.json/composer.lock fixtures via
- // initTempComposer/createInstalledJson/createComposerLock, none of which are ported yet.
- todo!()
+use crate::test_case::{
+ RunOptions, TearDown, create_composer_lock, create_installed_json, get_application_tester,
+ get_complete_package, init_temp_composer,
+};
+use serial_test::serial;
+use shirabe::package::handle::PackageInterfaceHandle;
+use shirabe_php_shim::PhpMixed;
+
+/// ref: LicensesCommandTest::setUp
+fn set_up() -> TearDown {
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({
+ "name": "test/pkg",
+ "version": "1.2.3",
+ "license": "MIT",
+ "require": {
+ "first/pkg": "^2.0",
+ "second/pkg": "3.*",
+ "third/pkg": "^1.3",
+ },
+ "require-dev": {
+ "dev/pkg": "~2.0",
+ },
+ })),
+ None,
+ None,
+ true,
+ );
+
+ let first = get_complete_package("first/pkg", "2.3.4");
+ first.set_license(vec!["MIT".to_string()]);
+
+ let second = get_complete_package("second/pkg", "3.4.0");
+ second.set_license(vec!["LGPL-2.0-only".to_string()]);
+ second.set_homepage("https://example.org".to_string());
+
+ let third = get_complete_package("third/pkg", "1.5.4");
+
+ let dev = get_complete_package("dev/pkg", "2.3.4.5");
+ dev.set_license(vec!["MIT".to_string()]);
+
+ let packages: Vec<PackageInterfaceHandle> = vec![first.into(), second.into(), third.into()];
+ let dev_packages: Vec<PackageInterfaceHandle> = vec![dev.into()];
+
+ create_installed_json(&packages, &dev_packages, true);
+ create_composer_lock(&packages, &dev_packages);
+
+ tear_down
+}
+
+/// Asserts each non-blank line of `display` matches the regex built from `expected[i]`
+/// (`implode("\s+", expected[i])` in PHP).
+fn assert_lines(display: &str, expected: &[Vec<&str>]) {
+ let mut i = 0usize;
+ for line in display.split('\n') {
+ if line.trim().is_empty() {
+ i += 1;
+ continue;
+ }
+ assert!(i < expected.len(), "Got more output lines than expected");
+ let pattern = expected[i]
+ .iter()
+ .map(|s| regex::escape(s))
+ .collect::<Vec<_>>()
+ .join(r"\s+");
+ let re = regex::Regex::new(&pattern).unwrap();
+ assert!(
+ re.is_match(line),
+ "line {} {:?} did not match {:?}",
+ i,
+ line,
+ pattern
+ );
+ i += 1;
+ }
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
+#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \
+ PhpMixed::Object carries no runtime class tag, so the Symfony Table helper cannot \
+ distinguish TableSeparator/TableCell rows yet"]
fn test_basic_run() {
- set_up();
+ let _tear_down = set_up();
- todo!()
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![(PhpMixed::from("command"), PhpMixed::from("license"))],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
+
+ let expected = vec![
+ vec!["Name:", "test/pkg"],
+ vec!["Version:", "1.2.3"],
+ vec!["Licenses:", "MIT"],
+ vec!["Dependencies:"],
+ vec![],
+ vec!["Name", "Version", "Licenses"],
+ vec!["dev/pkg", "2.3.4.5", "MIT"],
+ vec!["first/pkg", "2.3.4", "MIT"],
+ vec!["second/pkg", "3.4.0", "LGPL-2.0-only"],
+ vec!["third/pkg", "1.5.4", "none"],
+ ];
+ assert_lines(&app_tester.get_display(), &expected);
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
+#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \
+ see test_basic_run"]
fn test_no_dev() {
- set_up();
+ let _tear_down = set_up();
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--no-dev"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
- todo!()
+ let expected = vec![
+ vec!["Name:", "test/pkg"],
+ vec!["Version:", "1.2.3"],
+ vec!["Licenses:", "MIT"],
+ vec!["Dependencies:"],
+ vec![],
+ vec!["Name", "Version", "Licenses"],
+ vec!["first/pkg", "2.3.4", "MIT"],
+ vec!["second/pkg", "3.4.0", "LGPL-2.0-only"],
+ vec!["third/pkg", "1.5.4", "none"],
+ ];
+ assert_lines(&app_tester.get_display(), &expected);
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
fn test_format_json() {
- set_up();
+ let _tear_down = set_up();
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--format"), PhpMixed::from("json")),
+ ],
+ RunOptions {
+ capture_stderr_separately: true,
+ ..RunOptions::default()
+ },
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
- todo!()
+ let expected = serde_json::json!({
+ "name": "test/pkg",
+ "version": "1.2.3",
+ "license": ["MIT"],
+ "dependencies": {
+ "dev/pkg": { "version": "2.3.4.5", "license": ["MIT"] },
+ "first/pkg": { "version": "2.3.4", "license": ["MIT"] },
+ "second/pkg": { "version": "3.4.0", "license": ["LGPL-2.0-only"] },
+ "third/pkg": { "version": "1.5.4", "license": [] },
+ },
+ });
+
+ let actual: serde_json::Value = serde_json::from_str(&app_tester.get_display()).unwrap();
+ assert_eq!(expected, actual);
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
+#[ignore = "renders a Table via SymfonyStyle::table, which reaches shirabe_php_shim::instance_of \
+ (var.rs todo!()); see test_basic_run"]
fn test_format_summary() {
- set_up();
+ let _tear_down = set_up();
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--format"), PhpMixed::from("summary")),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
- todo!()
+ let expected = vec![
+ ("-", "-"),
+ ("License", "Number of dependencies"),
+ ("-", "-"),
+ ("MIT", "2"),
+ ("LGPL-2.0-only", "1"),
+ ("none", "1"),
+ ("-", "-"),
+ ];
+
+ let display = app_tester.get_display();
+ let lines: Vec<&str> = display.split('\n').collect();
+ for (i, (key, value)) in expected.iter().enumerate() {
+ let pattern = format!(r"{}\s+{}", regex::escape(key), regex::escape(value));
+ let re = regex::Regex::new(&pattern).unwrap();
+ assert!(
+ re.is_match(lines[i]),
+ "line {} {:?} did not match {:?}",
+ i,
+ lines[i],
+ pattern
+ );
+ }
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
fn test_format_unknown() {
- set_up();
+ let _tear_down = set_up();
- todo!()
+ let mut app_tester = get_application_tester();
+ let result = app_tester.run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--format"), PhpMixed::from("unknown")),
+ ],
+ RunOptions::default(),
+ );
+ assert!(
+ result.is_err(),
+ "expected a RuntimeException, got {:?}",
+ result
+ );
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
+#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \
+ see test_basic_run"]
fn test_locked() {
- set_up();
+ let _tear_down = set_up();
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--locked"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
- todo!()
+ let expected = vec![
+ vec!["Name:", "test/pkg"],
+ vec!["Version:", "1.2.3"],
+ vec!["Licenses:", "MIT"],
+ vec!["Dependencies:"],
+ vec![],
+ vec!["Name", "Version", "Licenses"],
+ vec!["dev/pkg", "2.3.4.5", "MIT"],
+ vec!["first/pkg", "2.3.4", "MIT"],
+ vec!["second/pkg", "3.4.0", "LGPL-2.0-only"],
+ vec!["third/pkg", "1.5.4", "none"],
+ ];
+ assert_lines(&app_tester.get_display(), &expected);
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
+#[ignore = "renders the text-format Table, which reaches shirabe_php_shim::instance_of (var.rs todo!()); \
+ see test_basic_run"]
fn test_locked_no_dev() {
- set_up();
+ let _tear_down = set_up();
- todo!()
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--locked"), PhpMixed::from(true)),
+ (PhpMixed::from("--no-dev"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
+
+ let expected = vec![
+ vec!["Name:", "test/pkg"],
+ vec!["Version:", "1.2.3"],
+ vec!["Licenses:", "MIT"],
+ vec!["Dependencies:"],
+ vec![],
+ vec!["Name", "Version", "Licenses"],
+ vec!["first/pkg", "2.3.4", "MIT"],
+ vec!["second/pkg", "3.4.0", "LGPL-2.0-only"],
+ vec!["third/pkg", "1.5.4", "none"],
+ ];
+ assert_lines(&app_tester.get_display(), &expected);
}
#[test]
-#[ignore = "missing TestCase::init_temp_composer / create_installed_json / create_composer_lock and get_application_tester (ApplicationTester) infrastructure"]
+#[serial]
fn test_locked_without_lock_file() {
- set_up();
+ let _tear_down = set_up();
+
+ // Remove the lock file
+ let _ = std::fs::remove_file("./composer.lock");
- todo!()
+ let mut app_tester = get_application_tester();
+ let result = app_tester.run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("license")),
+ (PhpMixed::from("--locked"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ );
+ let err = result.expect_err("expected an UnexpectedValueException");
+ assert!(
+ err.to_string()
+ .contains("Valid composer.json and composer.lock files are required to run this command with --locked"),
+ "unexpected error: {}",
+ err
+ );
}
diff --git a/crates/shirabe/tests/command/status_command_test.rs b/crates/shirabe/tests/command/status_command_test.rs
index 6154ea0..aa0ecc6 100644
--- a/crates/shirabe/tests/command/status_command_test.rs
+++ b/crates/shirabe/tests/command/status_command_test.rs
@@ -1,12 +1,47 @@
//! ref: composer/tests/Composer/Test/Command/StatusCommandTest.php
-#[ignore = "missing TestCase::init_temp_composer, create_composer_lock, create_installed_json, and get_application_tester (ApplicationTester) infrastructure"]
+use crate::test_case::{
+ RunOptions, create_composer_lock, create_installed_json, get_application_tester,
+ get_complete_package, init_temp_composer,
+};
+use serial_test::serial;
+use shirabe::package::handle::PackageInterfaceHandle;
+use shirabe_php_shim::PhpMixed;
+
#[test]
+#[serial]
fn test_no_local_changes() {
- todo!()
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({ "require": { "root/req": "1.*" } })),
+ None,
+ None,
+ true,
+ );
+
+ let package = get_complete_package("root/req", "1.0.0");
+ package.__set_type("metapackage".to_string());
+
+ let packages: Vec<PackageInterfaceHandle> = vec![package.into()];
+
+ create_composer_lock(&packages, &[]);
+ create_installed_json(&packages, &[], true);
+
+ let mut app_tester = get_application_tester();
+ app_tester
+ .run(
+ vec![(PhpMixed::from("command"), PhpMixed::from("status"))],
+ RunOptions::default(),
+ )
+ .unwrap();
+
+ assert_eq!("No local changes", app_tester.get_display().trim());
+
+ drop(tear_down);
}
-#[ignore = "missing TestCase::init_temp_composer and get_application_tester (ApplicationTester) infrastructure"]
+#[ignore = "exercises `install` over the network (downloads composer/class-map-generator from a git \
+ source or smarty/smarty from a dist zip), then mutates the installed package and runs \
+ `status`; the install path needs real network access"]
#[test]
fn test_locally_modified_packages() {
todo!()
diff --git a/crates/shirabe/tests/command/suggests_command_test.rs b/crates/shirabe/tests/command/suggests_command_test.rs
index c23691b..8bbb950 100644
--- a/crates/shirabe/tests/command/suggests_command_test.rs
+++ b/crates/shirabe/tests/command/suggests_command_test.rs
@@ -1,13 +1,392 @@
//! ref: composer/tests/Composer/Test/Command/SuggestsCommandTest.php
-#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, and get_application_tester (ApplicationTester) infrastructure"]
+use crate::test_case::{
+ RunOptions, create_composer_lock, create_installed_json, get_application_tester,
+ get_complete_package, get_version_constraint, init_temp_composer,
+};
+use indexmap::IndexMap;
+use serial_test::serial;
+use shirabe::package::Link;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe_php_shim::PhpMixed;
+
+/// ref: SuggestsCommandTest::getPackageWithSuggestAndRequires
+fn get_package_with_suggest_and_requires(
+ name: &str,
+ version: &str,
+ suggests: &[(&str, &str)],
+ requires: &[(&str, Link)],
+ require_devs: &[(&str, Link)],
+) -> CompletePackageHandle {
+ let pkg = get_complete_package(name, version);
+ let suggests_map: IndexMap<String, String> = suggests
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.to_string()))
+ .collect();
+ pkg.__set_suggests(suggests_map);
+ let requires_map: IndexMap<String, Link> = requires
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.clone()))
+ .collect();
+ pkg.__set_requires(requires_map);
+ let dev_requires_map: IndexMap<String, Link> = require_devs
+ .iter()
+ .map(|(k, v)| (k.to_string(), v.clone()))
+ .collect();
+ pkg.__set_dev_requires(dev_requires_map);
+ pkg
+}
+
+fn link(source: &str, target: &str, description: &str) -> Link {
+ Link::new(
+ source.to_string(),
+ target.to_string(),
+ get_version_constraint(">=", "1.0"),
+ Some(description.to_string()),
+ "^1.0".to_string(),
+ )
+}
+
#[test]
+#[serial]
fn test_installed_packages_with_no_suggestions() {
- todo!()
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({
+ "repositories": {
+ "packages": {
+ "type": "package",
+ "package": [
+ { "name": "vendor1/package1", "version": "1.0.0" },
+ { "name": "vendor2/package2", "version": "1.0.0" },
+ ],
+ },
+ },
+ "require": {
+ "vendor1/package1": "1.*",
+ "vendor2/package2": "1.*",
+ },
+ })),
+ None,
+ None,
+ true,
+ );
+
+ let packages: Vec<PackageInterfaceHandle> = vec![
+ get_complete_package("vendor1/package1", "1.0.0").into(),
+ get_complete_package("vendor2/package2", "1.0.0").into(),
+ ];
+
+ create_installed_json(&packages, &[], true);
+ create_composer_lock(&packages, &[]);
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![(PhpMixed::from("command"), PhpMixed::from("suggest"))],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code);
+ assert!(
+ app_tester.get_display().trim().is_empty(),
+ "expected empty display, got: {:?}",
+ app_tester.get_display()
+ );
+
+ drop(tear_down);
+}
+
+/// Builds the package set shared by every `test_suggest` case.
+fn suggest_packages() -> (Vec<PackageInterfaceHandle>, Vec<PackageInterfaceHandle>) {
+ let packages: Vec<PackageInterfaceHandle> = vec![
+ get_package_with_suggest_and_requires(
+ "vendor1/package1",
+ "1.0.0",
+ &[("vendor3/suggested", "helpful for vendor1/package1")],
+ &[(
+ "vendor6/package6",
+ link("vendor1/package1", "vendor6/package6", Link::TYPE_REQUIRE),
+ )],
+ &[
+ (
+ "vendor4/dev-suggested",
+ link(
+ "vendor1/package1",
+ "vendor4/dev-suggested",
+ Link::TYPE_DEV_REQUIRE,
+ ),
+ ),
+ (
+ "vendor3/suggested",
+ link(
+ "vendor1/package1",
+ "vendor3/suggested",
+ Link::TYPE_DEV_REQUIRE,
+ ),
+ ),
+ ],
+ )
+ .into(),
+ get_package_with_suggest_and_requires(
+ "vendor6/package6",
+ "1.0.0",
+ &[("vendor7/transitive", "helpful for vendor6/package6")],
+ &[],
+ &[],
+ )
+ .into(),
+ ];
+ let dev_packages: Vec<PackageInterfaceHandle> = vec![
+ get_package_with_suggest_and_requires(
+ "vendor2/package2",
+ "1.0.0",
+ &[("vendor4/dev-suggested", "helpful for vendor2/package2")],
+ &[(
+ "vendor5/dev-package",
+ link(
+ "vendor2/package2",
+ "vendor5/dev-package",
+ Link::TYPE_REQUIRE,
+ ),
+ )],
+ &[],
+ )
+ .into(),
+ get_package_with_suggest_and_requires(
+ "vendor5/dev-package",
+ "1.0.0",
+ &[("vendor8/dev-transitive", "helpful for vendor5/dev-package")],
+ &[],
+ &[(
+ "vendor8/dev-transitive",
+ link(
+ "vendor5/dev-package",
+ "vendor8/dev-transitive",
+ Link::TYPE_DEV_REQUIRE,
+ ),
+ )],
+ )
+ .into(),
+ ];
+ (packages, dev_packages)
+}
+
+fn run_suggest_case(has_lock_file: bool, command: &[(&str, PhpMixed)], expected: &str) {
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({
+ "repositories": {
+ "packages": {
+ "type": "package",
+ "package": [
+ { "name": "vendor1/package1", "version": "1.0.0", "suggests": { "vendor3/suggested": "helpful for vendor1/package1" }, "require": { "vendor6/package6": "^1.0" }, "require-dev": { "vendor3/suggested": "^1.0", "vendor4/dev-suggested": "^1.0" } },
+ { "name": "vendor2/package2", "version": "1.0.0", "suggests": { "vendor4/dev-suggested": "helpful for vendor2/package2" }, "require": { "vendor5/dev-package": "^1.0" } },
+ { "name": "vendor5/dev-package", "version": "1.0.0", "suggests": { "vendor8/dev-transitive": "helpful for vendor5/dev-package" }, "require-dev": { "vendor8/dev-transitive": "^1.0" } },
+ { "name": "vendor6/package6", "version": "1.0.0", "suggests": { "vendor7/transitive": "helpful for vendor6/package6" } },
+ ],
+ },
+ },
+ "require": { "vendor1/package1": "^1" },
+ "require-dev": { "vendor2/package2": "^1" },
+ })),
+ None,
+ None,
+ true,
+ );
+
+ let (packages, dev_packages) = suggest_packages();
+ create_installed_json(&packages, &dev_packages, true);
+ if has_lock_file {
+ create_composer_lock(&packages, &dev_packages);
+ }
+
+ let mut input: Vec<(PhpMixed, PhpMixed)> =
+ vec![(PhpMixed::from("command"), PhpMixed::from("suggest"))];
+ for (k, v) in command {
+ input.push((PhpMixed::from(*k), v.clone()));
+ }
+
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester.run(input, RunOptions::default()).unwrap();
+ assert_eq!(0, status_code);
+ assert_eq!(expected.trim(), app_tester.get_display().trim());
+
+ drop(tear_down);
}
-#[ignore = "missing TestCase::init_temp_composer, create_installed_json, create_composer_lock, get_application_tester (ApplicationTester), and get_version_parser infrastructure"]
#[test]
+#[serial]
fn test_suggest() {
- todo!()
+ let t = PhpMixed::from(true);
+ let by_package = ("--by-package", t.clone());
+ let by_suggestion = ("--by-suggestion", t.clone());
+ let no_dev = ("--no-dev", t.clone());
+ let all = ("--all", t.clone());
+ let list = ("--list", t.clone());
+
+ // 'with lockfile, show suggested' / 'without lockfile, show suggested'
+ let basic = "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+vendor2/package2 suggests:
+ - vendor4/dev-suggested: helpful for vendor2/package2
+
+2 additional suggestions by transitive dependencies can be shown with --all";
+ run_suggest_case(true, &[], basic);
+ run_suggest_case(false, &[], basic);
+
+ // 'with lockfile, show suggested (excluding dev)'
+ run_suggest_case(
+ true,
+ &[no_dev.clone()],
+ "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+1 additional suggestions by transitive dependencies can be shown with --all",
+ );
+ // 'without lockfile, show suggested (excluding dev)'
+ run_suggest_case(false, &[no_dev.clone()], basic);
+
+ let all_suggested = "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+vendor2/package2 suggests:
+ - vendor4/dev-suggested: helpful for vendor2/package2
+
+vendor5/dev-package suggests:
+ - vendor8/dev-transitive: helpful for vendor5/dev-package
+
+vendor6/package6 suggests:
+ - vendor7/transitive: helpful for vendor6/package6";
+ run_suggest_case(true, &[all.clone()], all_suggested);
+ run_suggest_case(false, &[all.clone()], all_suggested);
+
+ // 'with lockfile, show all suggested (excluding dev)'
+ run_suggest_case(
+ true,
+ &[all.clone(), no_dev.clone()],
+ "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+vendor6/package6 suggests:
+ - vendor7/transitive: helpful for vendor6/package6",
+ );
+ run_suggest_case(false, &[all.clone(), no_dev.clone()], all_suggested);
+
+ // grouped by package
+ run_suggest_case(true, &[by_package.clone()], basic);
+ run_suggest_case(false, &[by_package.clone()], basic);
+ run_suggest_case(
+ true,
+ &[by_package.clone(), no_dev.clone()],
+ "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+1 additional suggestions by transitive dependencies can be shown with --all",
+ );
+ run_suggest_case(false, &[by_package.clone(), no_dev.clone()], basic);
+
+ // grouped by suggestion
+ let by_suggestion_out = "vendor3/suggested is suggested by:
+ - vendor1/package1: helpful for vendor1/package1
+
+vendor4/dev-suggested is suggested by:
+ - vendor2/package2: helpful for vendor2/package2
+
+2 additional suggestions by transitive dependencies can be shown with --all";
+ run_suggest_case(true, &[by_suggestion.clone()], by_suggestion_out);
+ run_suggest_case(false, &[by_suggestion.clone()], by_suggestion_out);
+ run_suggest_case(
+ true,
+ &[by_suggestion.clone(), no_dev.clone()],
+ "vendor3/suggested is suggested by:
+ - vendor1/package1: helpful for vendor1/package1
+
+1 additional suggestions by transitive dependencies can be shown with --all",
+ );
+ run_suggest_case(
+ false,
+ &[by_suggestion.clone(), no_dev.clone()],
+ by_suggestion_out,
+ );
+
+ // grouped by package and suggestion
+ let by_both = "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+vendor2/package2 suggests:
+ - vendor4/dev-suggested: helpful for vendor2/package2
+
+------------------------------------------------------------------------------
+vendor3/suggested is suggested by:
+ - vendor1/package1: helpful for vendor1/package1
+
+vendor4/dev-suggested is suggested by:
+ - vendor2/package2: helpful for vendor2/package2
+
+2 additional suggestions by transitive dependencies can be shown with --all";
+ run_suggest_case(true, &[by_package.clone(), by_suggestion.clone()], by_both);
+ run_suggest_case(false, &[by_package.clone(), by_suggestion.clone()], by_both);
+ run_suggest_case(
+ true,
+ &[by_package.clone(), by_suggestion.clone(), no_dev.clone()],
+ "vendor1/package1 suggests:
+ - vendor3/suggested: helpful for vendor1/package1
+
+------------------------------------------------------------------------------
+vendor3/suggested is suggested by:
+ - vendor1/package1: helpful for vendor1/package1
+
+1 additional suggestions by transitive dependencies can be shown with --all",
+ );
+ run_suggest_case(
+ false,
+ &[by_package.clone(), by_suggestion.clone(), no_dev.clone()],
+ by_both,
+ );
+
+ // show suggested for package
+ let for_pkg = "vendor2/package2 suggests:
+ - vendor4/dev-suggested: helpful for vendor2/package2";
+ run_suggest_case(
+ true,
+ &[(
+ "packages",
+ PhpMixed::List(vec![PhpMixed::from("vendor2/package2")]),
+ )],
+ for_pkg,
+ );
+ run_suggest_case(
+ false,
+ &[(
+ "packages",
+ PhpMixed::List(vec![PhpMixed::from("vendor2/package2")]),
+ )],
+ for_pkg,
+ );
+
+ // list suggested
+ let list_out = "vendor3/suggested
+vendor4/dev-suggested";
+ run_suggest_case(true, &[list.clone()], list_out);
+ run_suggest_case(false, &[list.clone()], list_out);
+ run_suggest_case(true, &[list.clone(), no_dev.clone()], "vendor3/suggested");
+ run_suggest_case(false, &[list.clone(), no_dev.clone()], list_out);
+
+ let list_all = "vendor3/suggested
+vendor4/dev-suggested
+vendor7/transitive
+vendor8/dev-transitive";
+ run_suggest_case(true, &[list.clone(), all.clone()], list_all);
+ run_suggest_case(false, &[list.clone(), all.clone()], list_all);
+ run_suggest_case(
+ true,
+ &[list.clone(), all.clone(), no_dev.clone()],
+ "vendor3/suggested
+vendor7/transitive",
+ );
+ run_suggest_case(
+ false,
+ &[list.clone(), all.clone(), no_dev.clone()],
+ list_all,
+ );
}
diff --git a/crates/shirabe/tests/common/test_case.rs b/crates/shirabe/tests/common/test_case.rs
index 9972f32..7365f73 100644
--- a/crates/shirabe/tests/common/test_case.rs
+++ b/crates/shirabe/tests/common/test_case.rs
@@ -4,11 +4,22 @@
//! `#[path = "../common/test_case.rs"] mod test_case;`.
#![allow(dead_code)]
+use indexmap::IndexMap;
+use shirabe::config::Config;
use shirabe::console::application::ApplicationHandle;
+use shirabe::installer::InstallationManager;
+use shirabe::io::IOInterface;
+use shirabe::io::null_io::NullIO;
+use shirabe::json::JsonFile;
+use shirabe::package::Locker;
use shirabe::package::handle::{
CompleteAliasPackageHandle, CompletePackageHandle, PackageInterfaceHandle,
};
+use shirabe::repository::{InstalledFilesystemRepository, WritableRepositoryInterface};
+use shirabe::util::http_downloader::HttpDownloader;
+use shirabe::util::r#loop::Loop;
use shirabe::util::platform::Platform;
+use shirabe::util::process_executor::ProcessExecutor;
use shirabe_external_packages::symfony::console::input::array_input::ArrayInput;
use shirabe_external_packages::symfony::console::input::input_interface::InputInterface;
use shirabe_external_packages::symfony::console::input::streamable_input_interface::StreamableInputInterface;
@@ -30,6 +41,13 @@ pub fn get_package(name: &str, version: &str) -> PackageInterfaceHandle {
CompletePackageHandle::new(name.to_string(), norm_version, version.to_string()).into()
}
+/// ref: TestCase::getPackage but returning the concrete `CompletePackageHandle` so the
+/// `CompletePackage`-only setters (`set_license`, `set_homepage`, ...) are reachable.
+pub fn get_complete_package(name: &str, version: &str) -> CompletePackageHandle {
+ let norm_version = VersionParser.normalize(version, None).unwrap();
+ CompletePackageHandle::new(name.to_string(), norm_version, version.to_string())
+}
+
/// ref: TestCase::getAliasPackage (default class CompleteAliasPackage)
pub fn get_alias_package(
package: &PackageInterfaceHandle,
@@ -152,6 +170,88 @@ pub fn init_temp_composer(
}
}
+fn null_io() -> Rc<RefCell<dyn IOInterface>> {
+ Rc::new(RefCell::new(NullIO::new()))
+}
+
+/// ref: FactoryMock::createInstallationManager (the real installers are never created in tests, so a
+/// bare InstallationManager over a mock HttpDownloader suffices).
+fn installation_manager(io: &Rc<RefCell<dyn IOInterface>>) -> Rc<RefCell<InstallationManager>> {
+ let config = Rc::new(RefCell::new(Config::new(false, None)));
+ let http_downloader = Rc::new(RefCell::new(HttpDownloader::__new_mock(io.clone(), config)));
+ let r#loop = Rc::new(RefCell::new(Loop::new(http_downloader, None)));
+ Rc::new(RefCell::new(InstallationManager::new(
+ r#loop,
+ io.clone(),
+ None,
+ )))
+}
+
+/// ref: TestCase::createInstalledJson
+///
+/// Creates a `vendor/composer/installed.json` in the CWD (which `init_temp_composer` has chdir()'d
+/// into) listing the given packages. `dev_packages` are recorded as the dev-package-name set.
+pub fn create_installed_json(
+ packages: &[PackageInterfaceHandle],
+ dev_packages: &[PackageInterfaceHandle],
+ dev_mode: bool,
+) {
+ std::fs::create_dir_all("vendor/composer").unwrap();
+ let json_file =
+ JsonFile::new("vendor/composer/installed.json".to_string(), None, None).unwrap();
+ let mut repo = InstalledFilesystemRepository::new(json_file, false, None, None).unwrap();
+ repo.set_dev_package_names(
+ dev_packages
+ .iter()
+ .map(|pkg| pkg.get_pretty_name())
+ .collect(),
+ );
+ for pkg in packages.iter().chain(dev_packages.iter()) {
+ repo.add_package(pkg.clone()).unwrap();
+ std::fs::create_dir_all(format!("vendor/{}", pkg.get_name())).unwrap();
+ }
+
+ let io = null_io();
+ let im = installation_manager(&io);
+ repo.write(dev_mode, &mut im.borrow_mut()).unwrap();
+}
+
+/// ref: TestCase::createComposerLock
+///
+/// Creates a `composer.lock` in the CWD (chdir()'d into by `init_temp_composer`) listing the given
+/// packages.
+pub fn create_composer_lock(
+ packages: &[PackageInterfaceHandle],
+ dev_packages: &[PackageInterfaceHandle],
+) {
+ let io = null_io();
+ let json_file = JsonFile::new("./composer.lock".to_string(), None, None).unwrap();
+ let composer_file_contents = std::fs::read_to_string("./composer.json").unwrap_or_default();
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(Some(io.clone()))));
+ let mut locker = Locker::new(
+ io.clone(),
+ json_file,
+ installation_manager(&io),
+ &composer_file_contents,
+ process,
+ );
+ locker
+ .set_lock_data(
+ packages.to_vec(),
+ Some(dev_packages.to_vec()),
+ IndexMap::new(),
+ IndexMap::new(),
+ vec![],
+ "dev",
+ IndexMap::new(),
+ false,
+ false,
+ IndexMap::new(),
+ true,
+ )
+ .unwrap();
+}
+
/// ref: TestCase::getApplicationTester
pub fn get_application_tester() -> ApplicationTester {
let application = ApplicationHandle::new("Composer".to_string(), "".to_string()).unwrap();
diff --git a/crates/shirabe/tests/dbg_test.rs b/crates/shirabe/tests/dbg_test.rs
new file mode 100644
index 0000000..5553785
--- /dev/null
+++ b/crates/shirabe/tests/dbg_test.rs
@@ -0,0 +1 @@
+// OBSOLETE SCRATCH — safe to delete.
diff --git a/crates/shirabe/tests/downloader/download_manager_test.rs b/crates/shirabe/tests/downloader/download_manager_test.rs
index b7fa976..2405b4b 100644
--- a/crates/shirabe/tests/downloader/download_manager_test.rs
+++ b/crates/shirabe/tests/downloader/download_manager_test.rs
@@ -1,225 +1,242 @@
//! ref: composer/tests/Composer/Test/Downloader/DownloadManagerTest.php
-fn set_up() {
- // The Filesystem and IO mocks are not ported.
- todo!()
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use shirabe::downloader::DownloaderInterface;
+use shirabe::downloader::download_manager::DownloadManager;
+use shirabe::io::IOInterface;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe_semver::VersionParser;
+
+use crate::downloader_stub::DownloaderStub;
+use crate::io_stub::IOStub;
+
+/// ref: DownloadManagerTest::createPackageMock
+///
+/// PHPUnit returns a `PackageInterface` mock; a real CompletePackage with the
+/// relevant fields left at their defaults is an equivalent stand-in for the
+/// installation-source/type dispatch logic exercised by the ported cases.
+fn create_package_mock() -> PackageInterfaceHandle {
+ let norm_version = VersionParser.normalize("1.0.0", None).unwrap();
+ CompletePackageHandle::new("dummy/pkg".to_string(), norm_version, "1.0.0".to_string()).into()
+}
+
+/// ref: DownloadManagerTest::createDownloaderMock
+fn create_downloader_mock() -> Rc<RefCell<dyn DownloaderInterface>> {
+ Rc::new(RefCell::new(DownloaderStub::new())) as Rc<RefCell<dyn DownloaderInterface>>
+}
+
+fn create_manager() -> DownloadManager {
+ let io = Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>;
+ DownloadManager::new(io, false, None)
}
-// These mock IO and individual downloaders to drive DownloadManager's selection/download/
-// update/remove logic; mocking is not available here.
-#[ignore = "requires PHPUnit mock of DownloaderInterface (createDownloaderMock)"]
#[test]
fn test_set_get_downloader() {
- set_up();
- todo!()
+ let downloader = create_downloader_mock();
+ let mut manager = create_manager();
+
+ manager.set_downloader("test", downloader.clone());
+ assert!(Rc::ptr_eq(
+ &downloader,
+ &manager.get_downloader("test").unwrap()
+ ));
+
+ let result = manager.get_downloader("unregistered");
+ assert!(result.is_err());
}
-#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
#[test]
fn test_get_downloader_for_incorrectly_installed_package() {
- set_up();
- todo!()
+ // getInstallationSource() => null (the default for a fresh package).
+ let package = create_package_mock();
+
+ let manager = create_manager();
+
+ let result = manager.get_downloader_for_package(package);
+ assert!(result.is_err());
+}
+
+#[test]
+fn test_get_downloader_for_metapackage() {
+ let package = create_package_mock();
+ package.__set_type("metapackage".to_string());
+
+ let manager = create_manager();
+
+ assert!(
+ manager
+ .get_downloader_for_package(package)
+ .unwrap()
+ .is_none()
+ );
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_correctly_installed_dist_package() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_incorrectly_installed_dist_package() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_correctly_installed_source_package() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloader"]
#[test]
fn test_get_downloader_for_incorrectly_installed_source_package() {
- set_up();
- todo!()
-}
-
-#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
-#[test]
-fn test_get_downloader_for_metapackage() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download_failover() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
#[test]
fn test_bad_package_download() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_dist_only_package_download() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_source_only_package_download() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_metapackage_package_download() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_full_package_download_with_source_preferred() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_dist_only_package_download_with_source_preferred() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_source_only_package_download_with_source_preferred() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mock of PackageInterface (createPackageMock)"]
#[test]
fn test_bad_package_download_with_source_preferred() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"]
#[test]
fn test_update_dist_with_equal_types() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks of PackageInterface and DownloaderInterface"]
#[test]
fn test_update_dist_with_not_equal_types() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mock of PackageInterface and ReflectionMethod for private getAvailableSources"]
#[test]
fn test_get_available_sources_update_sticks_to_same_source() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_update_metapackage() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_remove() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_metapackage_remove() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_without_preference_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_without_preference_no_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_without_match_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_without_match_no_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_with_match_auto_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_with_match_auto_no_dev() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_with_match_source() {
- set_up();
todo!()
}
#[ignore = "requires PHPUnit mocks and partial mock of DownloadManager::getDownloaderForPackage"]
#[test]
fn test_install_preference_with_match_dist() {
- set_up();
todo!()
}
diff --git a/crates/shirabe/tests/downloader/fossil_downloader_test.rs b/crates/shirabe/tests/downloader/fossil_downloader_test.rs
index 0c45d2c..e403229 100644
--- a/crates/shirabe/tests/downloader/fossil_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/fossil_downloader_test.rs
@@ -1,8 +1,30 @@
//! ref: composer/tests/Composer/Test/Downloader/FossilDownloaderTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use shirabe::config::Config;
+use shirabe::downloader::VcsDownloader;
+use shirabe::downloader::fossil_downloader::FossilDownloader;
+use shirabe::io::IOInterface;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe::util::ProcessExecutor;
use shirabe::util::filesystem::Filesystem;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::VersionParser;
use tempfile::TempDir;
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::get_process_executor_mock;
+
+fn run<F: std::future::Future>(future: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .build()
+ .unwrap()
+ .block_on(future)
+}
+
fn set_up() -> TempDir {
TempDir::new().unwrap()
}
@@ -30,48 +52,168 @@ impl Drop for TearDown {
}
}
-// Every case constructs a FossilDownloader with a mocked IO/Config and a mocked
-// ProcessExecutor to feed fossil command output; a real HttpDownloader reaches
-// curl_multi_init (todo!()), and ProcessExecutor mocking is not available.
+/// ref: TestCase::getMockBuilder('Composer\Package\PackageInterface')->getMock()
+///
+/// A real CompletePackage seeded with the stubbed values is a faithful stand-in
+/// for a PackageInterface mock as long as `getSourceUrls()` equals
+/// `[getSourceUrl()]`, which holds for every non-ignored case here.
+fn get_package(source_reference: Option<&str>, source_url: Option<&str>) -> PackageInterfaceHandle {
+ let norm_version = VersionParser.normalize("1.0.0", None).unwrap();
+ let package =
+ CompletePackageHandle::new("dummy/pkg".to_string(), norm_version, "1.0.0".to_string());
+ package.__set_source_type(Some("fossil".to_string()));
+ package.set_source_reference(source_reference.map(|s| s.to_string()));
+ package.set_source_url(source_url.map(|s| s.to_string()));
+ package.into()
+}
+
+/// ref: FossilDownloaderTest::getDownloaderMock
+fn get_downloader_mock(
+ io: Option<Rc<RefCell<dyn IOInterface>>>,
+ config: Option<Config>,
+ process: Rc<RefCell<ProcessExecutor>>,
+) -> FossilDownloader {
+ let io =
+ io.unwrap_or_else(|| Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>);
+ // ref: getConfig(['secure-http' => false])
+ let config = Rc::new(RefCell::new(config.unwrap_or_else(|| {
+ ConfigStubBuilder::new()
+ .with("secure-http", PhpMixed::Bool(false))
+ .build()
+ })));
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ FossilDownloader::new(io, config, process, fs)
+}
#[test]
-#[ignore = "needs ProcessExecutorMock and PHPUnit mocks of IOInterface/PackageInterface/Filesystem for getDownloaderMock; not available"]
fn test_install_for_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let package = get_package(None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process);
+
+ let path = format!("{}/path", working_dir.path().to_string_lossy());
+ let result = run(downloader.install(package, &path));
+
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
#[test]
-#[ignore = "needs ProcessExecutorMock with expects() command-sequence assertions and PHPUnit PackageInterface mock; not available"]
fn test_install() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+ let package = get_package(Some("trunk"), Some("http://fossil.kd2.org/kd2fw/"));
+
+ let repo_file = format!("{}.fossil", working_dir_str);
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ crate::process_executor_mock::cmd(vec![
+ "fossil",
+ "clone",
+ "--",
+ "http://fossil.kd2.org/kd2fw/",
+ &repo_file,
+ ]),
+ crate::process_executor_mock::cmd(vec!["fossil", "open", "--nested", "--", &repo_file]),
+ crate::process_executor_mock::cmd(vec!["fossil", "update", "--", "trunk"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process);
+ run(downloader.install(package, &working_dir_str)).unwrap();
}
#[test]
-#[ignore = "needs ProcessExecutorMock and PHPUnit mocks of IOInterface/PackageInterface/Filesystem for getDownloaderMock; not available"]
fn test_updatefor_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let initial_package = get_package(Some("ref"), None);
+ let source_package = get_package(None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process);
+
+ let result = run(async {
+ downloader
+ .prepare(
+ "update",
+ source_package.clone(),
+ "/path",
+ Some(initial_package.clone()),
+ )
+ .await?;
+ downloader
+ .update(initial_package.clone(), source_package.clone(), "/path")
+ .await?;
+ downloader
+ .cleanup("update", source_package, "/path", Some(initial_package))
+ .await
+ });
+
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
#[test]
-#[ignore = "needs ProcessExecutorMock with expects() command-sequence assertions and PHPUnit PackageInterface mock; not available"]
fn test_update() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ // Ensure the .fslckout metadata file exists.
+ let fslckout = format!("{}/.fslckout", working_dir_str);
+ if !std::path::Path::new(&fslckout).exists() {
+ std::fs::File::create(&fslckout).unwrap();
+ }
+
+ let package = get_package(Some("trunk"), Some("http://fossil.kd2.org/kd2fw/"));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ crate::process_executor_mock::cmd(vec!["fossil", "changes"]),
+ crate::process_executor_mock::cmd(vec!["fossil", "pull"]),
+ crate::process_executor_mock::cmd(vec!["fossil", "up", "--", "trunk"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process);
+ run(async {
+ downloader
+ .prepare(
+ "update",
+ package.clone(),
+ &working_dir_str,
+ Some(package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(package.clone(), package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", package.clone(), &working_dir_str, Some(package))
+ .await
+ .unwrap();
+ });
}
+#[ignore = "PHP mocks Filesystem::removeDirectoryAsync; with a real Filesystem the actual \
+ removeDirectoryAsync drives its own ProcessExecutor for `rm -rf` through the \
+ unported execute_async mock seam"]
#[test]
-#[ignore = "needs ProcessExecutorMock and a PHPUnit Filesystem mock asserting removeDirectoryAsync; not available"]
fn test_remove() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
@@ -80,10 +222,13 @@ fn test_remove() {
}
#[test]
-#[ignore = "needs ProcessExecutorMock and PHPUnit mocks of IOInterface/Filesystem for getDownloaderMock; not available"]
fn test_get_installation_source() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
let _ = &working_dir;
- todo!()
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let downloader = get_downloader_mock(None, None, process);
+
+ assert_eq!("source", downloader.get_installation_source());
}
diff --git a/crates/shirabe/tests/downloader/hg_downloader_test.rs b/crates/shirabe/tests/downloader/hg_downloader_test.rs
index 67a13cb..c331533 100644
--- a/crates/shirabe/tests/downloader/hg_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/hg_downloader_test.rs
@@ -1,8 +1,28 @@
//! ref: composer/tests/Composer/Test/Downloader/HgDownloaderTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use shirabe::config::Config;
+use shirabe::downloader::VcsDownloader;
+use shirabe::downloader::hg_downloader::HgDownloader;
+use shirabe::io::IOInterface;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe::util::ProcessExecutor;
use shirabe::util::filesystem::Filesystem;
+use shirabe_semver::VersionParser;
use tempfile::TempDir;
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::{cmd, get_process_executor_mock};
+
+fn run<F: std::future::Future>(future: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .build()
+ .unwrap()
+ .block_on(future)
+}
+
fn set_up() -> TempDir {
TempDir::new().unwrap()
}
@@ -30,48 +50,165 @@ impl Drop for TearDown {
}
}
-// Every case constructs an HgDownloader with a mocked IO/Config and a mocked
-// ProcessExecutor to feed hg command output; a real HttpDownloader reaches
-// curl_multi_init (todo!()), and ProcessExecutor mocking is not available.
+/// ref: TestCase::getMockBuilder('Composer\Package\PackageInterface')->getMock()
+///
+/// A real CompletePackage seeded with the stubbed values is a faithful stand-in
+/// for a PackageInterface mock as long as `getSourceUrls()` equals
+/// `[getSourceUrl()]`, which holds for every non-ignored case here.
+fn get_package(source_reference: Option<&str>, source_url: Option<&str>) -> PackageInterfaceHandle {
+ let norm_version = VersionParser.normalize("1.0.0", None).unwrap();
+ let package =
+ CompletePackageHandle::new("dummy/pkg".to_string(), norm_version, "1.0.0".to_string());
+ package.__set_source_type(Some("hg".to_string()));
+ package.set_source_reference(source_reference.map(|s| s.to_string()));
+ package.set_source_url(source_url.map(|s| s.to_string()));
+ package.into()
+}
+
+/// ref: HgDownloaderTest::getDownloaderMock
+///
+/// PHP uses a `getMockBuilder('Composer\Config')->getMock()`, whose `get()`
+/// returns null for everything; a default real `Config` resolves the only
+/// relevant key (`secure-http`) compatibly for the https URLs used here.
+fn get_downloader_mock(
+ io: Option<Rc<RefCell<dyn IOInterface>>>,
+ config: Option<Config>,
+ process: Rc<RefCell<ProcessExecutor>>,
+) -> HgDownloader {
+ let io =
+ io.unwrap_or_else(|| Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>);
+ let config = Rc::new(RefCell::new(
+ config.unwrap_or_else(|| Config::new(false, None)),
+ ));
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ HgDownloader::new(io, config, process, fs)
+}
#[test]
-#[ignore = "needs PHPUnit mocks of IOInterface/Config/Filesystem/PackageInterface for getDownloaderMock; not available"]
fn test_download_for_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
let _ = &working_dir;
- todo!()
+
+ let package = get_package(None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process);
+
+ let result = run(downloader.install(package, "/path"));
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
#[test]
-#[ignore = "needs ProcessExecutorMock with expects() command-sequence assertions and PHPUnit PackageInterface mock; not available"]
fn test_download() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+ let package = get_package(Some("ref"), Some("https://mercurial.dev/l3l0/composer"));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(vec![
+ "hg",
+ "clone",
+ "--",
+ "https://mercurial.dev/l3l0/composer",
+ &working_dir_str,
+ ]),
+ cmd(vec!["hg", "up", "--", "ref"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process);
+ run(downloader.install(package, &working_dir_str)).unwrap();
}
#[test]
-#[ignore = "needs PHPUnit mocks of IOInterface/Config/Filesystem/PackageInterface for getDownloaderMock; not available"]
fn test_updatefor_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
let _ = &working_dir;
- todo!()
+
+ let initial_package = get_package(Some("ref"), None);
+ let source_package = get_package(None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process);
+
+ let result = run(async {
+ downloader
+ .prepare(
+ "update",
+ source_package.clone(),
+ "/path",
+ Some(initial_package.clone()),
+ )
+ .await?;
+ downloader
+ .update(initial_package.clone(), source_package.clone(), "/path")
+ .await?;
+ downloader
+ .cleanup("update", source_package, "/path", Some(initial_package))
+ .await
+ });
+
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
#[test]
-#[ignore = "needs ProcessExecutorMock with expects() command-sequence assertions and PHPUnit PackageInterface mock; not available"]
fn test_update() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.hg", working_dir_str))
+ .unwrap();
+
+ let package = get_package(Some("ref"), Some("https://github.com/l3l0/composer"));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(vec!["hg", "st"]),
+ cmd(vec!["hg", "pull", "--", "https://github.com/l3l0/composer"]),
+ cmd(vec!["hg", "up", "--", "ref"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process);
+ run(async {
+ downloader
+ .prepare(
+ "update",
+ package.clone(),
+ &working_dir_str,
+ Some(package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(package.clone(), package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", package.clone(), &working_dir_str, Some(package))
+ .await
+ .unwrap();
+ });
}
+#[ignore = "PHP mocks Filesystem::removeDirectoryAsync; with a real Filesystem the actual \
+ removeDirectoryAsync drives its own ProcessExecutor for `rm -rf` through the \
+ unported execute_async mock seam"]
#[test]
-#[ignore = "needs ProcessExecutorMock and PHPUnit Filesystem mock with removeDirectoryAsync expectation; not available"]
fn test_remove() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
@@ -80,10 +217,13 @@ fn test_remove() {
}
#[test]
-#[ignore = "needs PHPUnit mocks of IOInterface/Config/Filesystem for getDownloaderMock; not available"]
fn test_get_installation_source() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
let _ = &working_dir;
- todo!()
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let downloader = get_downloader_mock(None, None, process);
+
+ assert_eq!("source", downloader.get_installation_source());
}
diff --git a/crates/shirabe/tests/downloader/main.rs b/crates/shirabe/tests/downloader/main.rs
index 8244010..d6e07e4 100644
--- a/crates/shirabe/tests/downloader/main.rs
+++ b/crates/shirabe/tests/downloader/main.rs
@@ -1,5 +1,7 @@
#[path = "../common/config_stub.rs"]
mod config_stub;
+#[path = "../common/downloader_stub.rs"]
+mod downloader_stub;
#[path = "../common/http_downloader_mock.rs"]
mod http_downloader_mock;
#[path = "../common/io_mock.rs"]
diff --git a/crates/shirabe/tests/repository/path_repository_test.rs b/crates/shirabe/tests/repository/path_repository_test.rs
index 5679c38..bbf4938 100644
--- a/crates/shirabe/tests/repository/path_repository_test.rs
+++ b/crates/shirabe/tests/repository/path_repository_test.rs
@@ -7,6 +7,8 @@ use indexmap::IndexMap;
use shirabe::config::Config;
use shirabe::io::{IOInterface, NullIO};
use shirabe::repository::PathRepository;
+use shirabe::util::http_downloader::HttpDownloader;
+use shirabe::util::r#loop::Loop;
use shirabe::util::{Platform, ProcessExecutor};
use shirabe_php_shim::{
DIRECTORY_SEPARATOR, PhpMixed, file_get_contents, hash, realpath, serialize,
@@ -28,6 +30,16 @@ fn create_path_repo(options: IndexMap<String, PhpMixed>) -> PathRepository {
let config = Rc::new(RefCell::new(Config::new(true, None)));
let proc = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+ // ref: createPathRepo wires the ProcessExecutor through a Loop so the VersionGuesser's async
+ // git calls are permitted; constructing the Loop calls enable_async() on the shared executor.
+ let http_downloader = Rc::new(RefCell::new(HttpDownloader::new(
+ io.clone(),
+ config.clone(),
+ IndexMap::new(),
+ false,
+ )));
+ let _loop = Loop::new(http_downloader, Some(proc.clone()));
+
PathRepository::new(options, io, config, None, None, Some(proc)).unwrap()
}
@@ -68,7 +80,6 @@ fn test_load_package_from_file_system_with_version() {
);
}
-#[ignore = "version guessing for an unversioned package calls VersionGuesser::guess_git_version, which reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"]
#[test]
fn test_load_package_from_file_system_without_version() {
let repository_url = [
@@ -90,7 +101,6 @@ fn test_load_package_from_file_system_without_version() {
assert!(!package_version.is_empty());
}
-#[ignore = "the without-version fixture matched by the wildcard triggers VersionGuesser::guess_git_version, which reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"]
#[test]
fn test_load_package_from_file_system_with_wildcard() {
let repository_url =
@@ -197,7 +207,6 @@ fn test_url_remains_relative() {
assert_eq!(Some(relative_url), package.get_dist_url());
}
-#[ignore = "the wildcard url also matches the without-version fixture, whose version guessing reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"]
#[test]
fn test_reference_none() {
let options = coordinates(vec![("reference", PhpMixed::String("none".to_string()))]);
@@ -216,7 +225,6 @@ fn test_reference_none() {
}
}
-#[ignore = "the wildcard url also matches the without-version fixture, whose version guessing reaches stream_set_blocking (fcntl(2) not implemented, todo!()) and aborts the process"]
#[test]
fn test_reference_config() {
let options = coordinates(vec![
diff --git a/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs b/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs
index ea130ea..1f9ae53 100644
--- a/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs
+++ b/crates/shirabe/tests/repository/vcs/forgejo_driver_test.rs
@@ -9,15 +9,18 @@ use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::repository::vcs::ForgejoDriver;
use shirabe::util::filesystem::Filesystem;
+use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler};
+use shirabe::util::process_executor::{MockHandler, ProcessExecutor};
use shirabe_php_shim::PhpMixed;
use tempfile::TempDir;
+use crate::http_downloader_mock::{HttpDownloaderMockGuard, expect_full, get_http_downloader_mock};
+use crate::process_executor_mock::{ProcessExecutorMockGuard, get_process_executor_mock};
+
struct SetUp {
home: TempDir,
- config: Config,
- // The IOInterface and HttpDownloader mocks are not ported.
- io: (),
- http_downloader: (),
+ config: Rc<RefCell<Config>>,
+ io: Rc<RefCell<dyn IOInterface>>,
}
fn set_up() -> SetUp {
@@ -36,14 +39,14 @@ fn set_up() -> SetUp {
top.insert("config".to_string(), PhpMixed::Array(config_section));
config.merge(&top, Config::SOURCE_UNKNOWN);
- let io = ();
- let http_downloader = ();
+ // PHP mocks IOInterface with isInteractive() => true; on the passing (200) paths
+ // exercised here the interactivity flag is never consulted, so a bare NullIO matches.
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
SetUp {
home,
- config,
+ config: Rc::new(RefCell::new(config)),
io,
- http_downloader,
}
}
@@ -68,82 +71,233 @@ impl Drop for TearDown {
}
}
-fn supports_provider() -> Vec<(bool, &'static str)> {
- vec![
- (false, "https://example.org/acme/repo"),
- (true, "https://codeberg.org/acme/repository"),
- ]
+// ref: ForgejoDriverTest::initializeDriver
+fn initialize_driver(
+ set_up: &SetUp,
+ repo_url: &str,
+ http_downloader: Rc<RefCell<HttpDownloader>>,
+) -> (ForgejoDriver, ProcessExecutorMockGuard) {
+ let (process, process_guard) = get_process_executor_mock(vec![], false, MockHandler::default());
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+ let mut driver = ForgejoDriver::new(
+ repo_config,
+ set_up.io.clone(),
+ set_up.config.clone(),
+ http_downloader,
+ process,
+ );
+ driver.initialize().unwrap();
+
+ (driver, process_guard)
}
-#[test]
-#[ignore]
-fn test_supports() {
- for (expected, repo_url) in supports_provider() {
- let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
- let config = Rc::new(RefCell::new(Config::new(true, None)));
+fn http_mock(
+ expectations: Vec<shirabe::util::http_downloader::HttpDownloaderMockExpectation>,
+) -> (Rc<RefCell<HttpDownloader>>, HttpDownloaderMockGuard) {
+ get_http_downloader_mock(expectations, true, HttpDownloaderMockHandler::default())
+}
- assert_eq!(
- expected,
- ForgejoDriver::supports(io, config, repo_url, false).unwrap()
+fn repo_body() -> String {
+ shirabe::json::JsonFile::encode(&PhpMixed::Array({
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ m.insert(
+ "default_branch".to_string(),
+ PhpMixed::String("main".to_string()),
);
- }
+ m.insert("has_issues".to_string(), PhpMixed::Bool(true));
+ m.insert("archived".to_string(), PhpMixed::Bool(false));
+ m.insert("private".to_string(), PhpMixed::Bool(false));
+ m.insert(
+ "html_url".to_string(),
+ PhpMixed::String("https://codeberg.org/acme/repo".to_string()),
+ );
+ m.insert(
+ "ssh_url".to_string(),
+ PhpMixed::String("git@codeberg.org:acme/repo.git".to_string()),
+ );
+ m.insert(
+ "clone_url".to_string(),
+ PhpMixed::String("https://codeberg.org/acme/repo.git".to_string()),
+ );
+ m
+ }))
}
-// The remaining cases construct a ForgejoDriver and mock the HttpDownloader to return
-// Forgejo API responses; mocking is not available, and a real HttpDownloader reaches
-// curl_multi_init (todo!()).
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_public_repository() {
- let SetUp {
- home,
- config,
- io,
+ let set_up = set_up();
+ let _tear_down = TearDown::new(set_up.home.path().to_path_buf());
+
+ let (http_downloader, _http_guard) = http_mock(vec![expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo",
+ None,
+ 200,
+ repo_body(),
+ vec![String::new()],
+ )]);
+
+ let (mut driver, _process_guard) = initialize_driver(
+ &set_up,
+ "https://codeberg.org/acme/repo.git",
http_downloader,
- } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- let _ = (&config, &io, &http_downloader);
- todo!()
+ );
+ assert_eq!("main", driver.get_root_identifier().unwrap());
+
+ let sha = "SOMESHA";
+ let dist = driver.get_dist(sha).unwrap();
+ assert_eq!("zip", dist.get("type").unwrap());
+ assert_eq!(
+ "https://codeberg.org/api/v1/repos/acme/repo/archive/SOMESHA.zip",
+ dist.get("url").unwrap()
+ );
+ assert_eq!(sha, dist.get("reference").unwrap());
+
+ let source = driver.get_source(sha);
+ assert_eq!("git", source.get("type").unwrap());
+ assert_eq!(
+ "https://codeberg.org/acme/repo.git",
+ source.get("url").unwrap()
+ );
+ assert_eq!(sha, source.get("reference").unwrap());
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_get_branches() {
- let SetUp {
- home,
- config,
- io,
+ let set_up = set_up();
+ let _tear_down = TearDown::new(set_up.home.path().to_path_buf());
+
+ let branches_body = shirabe::json::JsonFile::encode(&PhpMixed::List(vec![PhpMixed::Array({
+ let mut commit: IndexMap<String, PhpMixed> = IndexMap::new();
+ commit.insert("id".to_string(), PhpMixed::String("SOMESHA".to_string()));
+ let mut entry: IndexMap<String, PhpMixed> = IndexMap::new();
+ entry.insert("name".to_string(), PhpMixed::String("main".to_string()));
+ entry.insert("commit".to_string(), PhpMixed::Array(commit));
+ entry
+ })]));
+
+ let (http_downloader, _http_guard) = http_mock(vec![
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo",
+ None,
+ 200,
+ repo_body(),
+ vec![String::new()],
+ ),
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo/branches?per_page=100",
+ None,
+ 200,
+ branches_body,
+ vec![String::new()],
+ ),
+ ]);
+
+ let (mut driver, _process_guard) = initialize_driver(
+ &set_up,
+ "https://codeberg.org/acme/repo.git",
http_downloader,
- } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- let _ = (&config, &io, &http_downloader);
- todo!()
+ );
+
+ let mut expected: IndexMap<String, String> = IndexMap::new();
+ expected.insert("main".to_string(), "SOMESHA".to_string());
+ assert_eq!(expected, driver.get_branches().unwrap());
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_get_tags() {
- let SetUp {
- home,
- config,
- io,
+ let set_up = set_up();
+ let _tear_down = TearDown::new(set_up.home.path().to_path_buf());
+
+ let tags_body = shirabe::json::JsonFile::encode(&PhpMixed::List(vec![PhpMixed::Array({
+ let mut commit: IndexMap<String, PhpMixed> = IndexMap::new();
+ commit.insert("sha".to_string(), PhpMixed::String("SOMESHA".to_string()));
+ let mut entry: IndexMap<String, PhpMixed> = IndexMap::new();
+ entry.insert("name".to_string(), PhpMixed::String("1.0".to_string()));
+ entry.insert("commit".to_string(), PhpMixed::Array(commit));
+ entry
+ })]));
+
+ let (http_downloader, _http_guard) = http_mock(vec![
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo",
+ None,
+ 200,
+ repo_body(),
+ vec![String::new()],
+ ),
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo/tags?per_page=100",
+ None,
+ 200,
+ tags_body,
+ vec![String::new()],
+ ),
+ ]);
+
+ let (mut driver, _process_guard) = initialize_driver(
+ &set_up,
+ "https://codeberg.org/acme/repo.git",
http_downloader,
- } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- let _ = (&config, &io, &http_downloader);
- todo!()
+ );
+
+ let mut expected: IndexMap<String, String> = IndexMap::new();
+ expected.insert("1.0".to_string(), "SOMESHA".to_string());
+ assert_eq!(expected, driver.get_tags().unwrap());
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_get_empty_file_content() {
- let SetUp {
- home,
- config,
- io,
+ let set_up = set_up();
+ let _tear_down = TearDown::new(set_up.home.path().to_path_buf());
+
+ let (http_downloader, _http_guard) = http_mock(vec![
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo",
+ None,
+ 200,
+ repo_body(),
+ vec![String::new()],
+ ),
+ expect_full(
+ "https://codeberg.org/api/v1/repos/acme/repo/contents/composer.json?ref=main",
+ None,
+ 200,
+ "{\"encoding\":\"base64\",\"content\":\"\"}",
+ vec![String::new()],
+ ),
+ ]);
+
+ let (mut driver, _process_guard) = initialize_driver(
+ &set_up,
+ "https://codeberg.org/acme/repo.git",
http_downloader,
- } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- let _ = (&config, &io, &http_downloader);
- todo!()
+ );
+
+ assert_eq!(
+ Some(String::new()),
+ driver.get_file_content("composer.json", "main").unwrap()
+ );
+}
+
+fn supports_provider() -> Vec<(bool, &'static str)> {
+ vec![
+ (false, "https://example.org/acme/repo"),
+ (true, "https://codeberg.org/acme/repository"),
+ ]
+}
+
+#[test]
+fn test_supports() {
+ let set_up = set_up();
+ let _tear_down = TearDown::new(set_up.home.path().to_path_buf());
+
+ for (expected, repo_url) in supports_provider() {
+ assert_eq!(
+ expected,
+ ForgejoDriver::supports(set_up.io.clone(), set_up.config.clone(), repo_url, false)
+ .unwrap()
+ );
+ }
}
diff --git a/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs b/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
index d9178dd..5e9471f 100644
--- a/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
+++ b/crates/shirabe/tests/repository/vcs/perforce_driver_test.rs
@@ -108,7 +108,6 @@ fn test_supports_returns_false_no_deep_check() {
// The remaining cases mock Perforce, the repository config and IO to drive initialization,
// composer-file detection and cleanup; mocking is not available here.
-#[ignore]
#[test]
fn test_initialize_captures_variables_from_repo_config() {
let SetUp {
@@ -132,9 +131,13 @@ fn test_initialize_captures_variables_from_repo_config() {
IndexMap::new(),
false,
)));
- let process = Rc::new(RefCell::new(shirabe::util::ProcessExecutor::new(Some(
- io.clone(),
- ))));
+ // ref: setUp uses getProcessExecutorMock(); the empty, non-strict mock lets Perforce's p4
+ // queries resolve with the default (exit 0) result so initialize() never shells out to p4.
+ let (process, _process_guard) = crate::process_executor_mock::get_process_executor_mock(
+ vec![],
+ false,
+ shirabe::util::process_executor::MockHandler::default(),
+ );
let mut driver = PerforceDriver::new(repo_config, io, config, http_downloader, process);
driver.initialize().unwrap();
diff --git a/crates/shirabe/tests/repository/vcs_repository_test.rs b/crates/shirabe/tests/repository/vcs_repository_test.rs
index d84ed51..279db04 100644
--- a/crates/shirabe/tests/repository/vcs_repository_test.rs
+++ b/crates/shirabe/tests/repository/vcs_repository_test.rs
@@ -1,29 +1,145 @@
//! ref: composer/tests/Composer/Test/Repository/VcsRepositoryTest.php
use std::cell::RefCell;
+use std::collections::HashSet;
+use std::process::Command;
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::dumper::ArrayDumper;
-use shirabe::repository::RepositoryInterface;
use shirabe::repository::VcsRepository;
+use shirabe::util::ProcessExecutor;
use shirabe::util::filesystem::Filesystem;
use shirabe::util::http_downloader::HttpDownloader;
+use shirabe::util::r#loop::Loop;
use shirabe_php_shim::PhpMixed;
+use tempfile::TempDir;
struct SetUp {
- composer_home: std::path::PathBuf,
- git_repo: std::path::PathBuf,
+ composer_home: TempDir,
+ git_repo: TempDir,
}
-fn set_up() -> SetUp {
- // setUp lazily runs initialize(), which shells out to git to build a fixture repository
- // on disk; the ExecutableFinder/ProcessExecutor-driven setup and the markTestSkipped
- // skip path are not ported.
- todo!()
+// ref: VcsRepositoryTest::initialize. Builds a fixture git repository on disk by shelling out to
+// git. Returns None when git is unavailable (mirroring markTestSkipped).
+fn set_up() -> Option<SetUp> {
+ if which_git().is_none() {
+ // 'This test needs a git binary in the PATH to be able to run'
+ return None;
+ }
+
+ let composer_home = TempDir::new().unwrap();
+ let git_repo = TempDir::new().unwrap();
+ let path = git_repo.path();
+
+ let exec = |args: &[&str]| {
+ let status = Command::new("git")
+ .args(args)
+ .current_dir(path)
+ .env("GIT_CONFIG_GLOBAL", "/dev/null")
+ .env("GIT_CONFIG_SYSTEM", "/dev/null")
+ .status()
+ .unwrap_or_else(|e| panic!("Failed to execute git {:?}: {}", args, e));
+ if !status.success() {
+ panic!("Failed to execute git {:?}", args);
+ }
+ };
+ let write_file = |name: &str, contents: &str| {
+ std::fs::write(path.join(name), contents).unwrap();
+ };
+
+ // init
+ exec(&["init", "-q"]);
+ exec(&["checkout", "-b", "master"]);
+ exec(&["config", "user.email", "composertest@example.org"]);
+ exec(&["config", "user.name", "ComposerTest"]);
+ exec(&["config", "commit.gpgsign", "false"]);
+ write_file("foo", "");
+ exec(&["add", "foo"]);
+ exec(&["commit", "-m", "init"]);
+
+ // non-composed tag & branch
+ exec(&["tag", "0.5.0"]);
+ exec(&["branch", "oldbranch"]);
+
+ // add composed tag & master branch
+ write_file(
+ "composer.json",
+ &shirabe::json::JsonFile::encode(&composer(None)),
+ );
+ exec(&["add", "composer.json"]);
+ exec(&["commit", "-m", "addcomposer"]);
+ exec(&["tag", "0.6.0"]);
+
+ // add feature-a branch
+ exec(&["checkout", "-b", "feature/a-1.0-B"]);
+ write_file("foo", "bar feature");
+ exec(&["add", "foo"]);
+ exec(&["commit", "-m", "change-a"]);
+
+ // add foo#bar branch which should result in dev-foo+bar
+ exec(&["branch", "foo#bar"]);
+
+ // add version to composer.json
+ exec(&["checkout", "master"]);
+ write_file(
+ "composer.json",
+ &shirabe::json::JsonFile::encode(&composer(Some("1.0.0"))),
+ );
+ exec(&["add", "composer.json"]);
+ exec(&["commit", "-m", "addversion"]);
+
+ // create tag with wrong version in it
+ exec(&["tag", "0.9.0"]);
+ // create tag with correct version in it
+ exec(&["tag", "1.0.0"]);
+
+ // add feature-b branch
+ exec(&["checkout", "-b", "feature-b"]);
+ write_file("foo", "baz feature");
+ exec(&["add", "foo"]);
+ exec(&["commit", "-m", "change-b"]);
+
+ // add 1.0 branch
+ exec(&["checkout", "master"]);
+ exec(&["branch", "1.0"]);
+
+ // add 1.0.x branch
+ exec(&["branch", "1.1.x"]);
+
+ // update master to 2.0
+ write_file(
+ "composer.json",
+ &shirabe::json::JsonFile::encode(&composer(Some("2.0.0"))),
+ );
+ exec(&["add", "composer.json"]);
+ exec(&["commit", "-m", "bump-version"]);
+
+ Some(SetUp {
+ composer_home,
+ git_repo,
+ })
+}
+
+fn composer(version: Option<&str>) -> PhpMixed {
+ let mut m: IndexMap<String, PhpMixed> = IndexMap::new();
+ m.insert("name".to_string(), PhpMixed::String("a/b".to_string()));
+ if let Some(version) = version {
+ m.insert("version".to_string(), PhpMixed::String(version.to_string()));
+ }
+ PhpMixed::Array(m)
+}
+
+fn which_git() -> Option<()> {
+ Command::new("git")
+ .arg("--version")
+ .output()
+ .ok()
+ .filter(|o| o.status.success())
+ .map(|_| ())
}
fn tear_down(composer_home: &std::path::Path, git_repo: &std::path::Path) {
@@ -52,13 +168,85 @@ impl Drop for TearDown {
}
}
-// testLoadVersions initialises a real git repository on disk and drives a VcsRepository over
-// it, then asserts the loaded package versions; the git fixture setup and constraint parsing
-// (look-around regex) are not ported. In addition, VcsRepository does not implement
-// RepositoryInterface and keeps its inner ArrayRepository pub(crate), so getPackages() (inherited
-// from ArrayRepository in PHP) is not reachable from the test crate.
#[test]
-#[ignore = "VcsRepository does not expose get_packages() to the test crate; git fixture setup not ported"]
+#[serial]
fn test_load_versions() {
- todo!()
+ let Some(set_up) = set_up() else {
+ // git binary unavailable; skip like markTestSkipped.
+ return;
+ };
+ let composer_home = set_up.composer_home.path().to_path_buf();
+ let git_repo = set_up.git_repo.path().to_path_buf();
+
+ let mut expected: HashSet<String> = [
+ "0.6.0",
+ "1.0.0",
+ "1.0.x-dev",
+ "1.1.x-dev",
+ "dev-feature-b",
+ "dev-feature/a-1.0-B",
+ "dev-foo+bar",
+ "dev-master",
+ "9999999-dev", // alias of dev-master
+ ]
+ .iter()
+ .map(|s| s.to_string())
+ .collect();
+
+ let mut config = Config::new(true, None);
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "home".to_string(),
+ PhpMixed::String(composer_home.to_string_lossy().into_owned()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ let config = Rc::new(RefCell::new(config));
+
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let http_downloader = Rc::new(RefCell::new(HttpDownloader::new(
+ io.clone(),
+ config.clone(),
+ IndexMap::new(),
+ false,
+ )));
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(Some(io.clone()))));
+ // VcsRepository's git driver / VersionGuesser run async git processes; constructing a Loop
+ // enables async on the shared ProcessExecutor.
+ let _loop = Loop::new(http_downloader.clone(), Some(process.clone()));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert(
+ "url".to_string(),
+ PhpMixed::String(git_repo.to_string_lossy().into_owned()),
+ );
+ repo_config.insert("type".to_string(), PhpMixed::String("vcs".to_string()));
+
+ let mut repo = VcsRepository::new(
+ repo_config,
+ io,
+ config,
+ http_downloader,
+ None,
+ Some(process),
+ None,
+ None,
+ )
+ .unwrap();
+
+ let _tear_down = TearDown::new(composer_home, git_repo);
+
+ let packages = repo.__get_packages().unwrap();
+
+ for package in &packages {
+ let pretty = package.get_pretty_version();
+ assert!(expected.remove(&pretty), "Unexpected version {}", pretty);
+ }
+
+ assert!(
+ expected.is_empty(),
+ "Missing versions: {}",
+ expected.into_iter().collect::<Vec<_>>().join(", ")
+ );
}