aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--crates/shirabe-php-shim/src/runtime.rs6
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs31
-rw-r--r--crates/shirabe/src/util/auth_helper.rs33
-rw-r--r--crates/shirabe/tests/application_test.rs38
-rw-r--r--crates/shirabe/tests/autoload/autoload_generator_test.rs329
-rw-r--r--crates/shirabe/tests/autoload/class_loader_test.rs39
-rw-r--r--crates/shirabe/tests/command/run_script_command_test.rs239
-rw-r--r--crates/shirabe/tests/command/self_update_command_test.rs138
-rw-r--r--crates/shirabe/tests/downloader/file_downloader_test.rs20
-rw-r--r--crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs261
-rw-r--r--crates/shirabe/tests/event_dispatcher/main.rs2
-rw-r--r--crates/shirabe/tests/util/auth_helper_test.rs50
-rw-r--r--crates/shirabe/tests/util/error_handler_test.rs9
-rw-r--r--crates/shirabe/tests/util/process_executor_test.rs48
14 files changed, 1108 insertions, 135 deletions
diff --git a/crates/shirabe-php-shim/src/runtime.rs b/crates/shirabe-php-shim/src/runtime.rs
index c529c263..f7fb3b86 100644
--- a/crates/shirabe-php-shim/src/runtime.rs
+++ b/crates/shirabe-php-shim/src/runtime.rs
@@ -49,6 +49,12 @@ pub fn constant(_name: &str) -> PhpMixed {
todo!()
}
+pub fn define(_constant_name: &str, _value: PhpMixed) -> bool {
+ // TODO(php-runtime): defining a constant at runtime needs the same registry `constant` and
+ // `defined` would read from; the shim has none.
+ todo!()
+}
+
// Models the constants defined in a standard modern PHP CLI environment on a
// non-Windows platform with the common extensions loaded (curl, openssl, json).
// Windows-only, HHVM and Composer-bootstrap constants are reported undefined.
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index 4aff50c9..5043610b 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -101,6 +101,10 @@ pub struct EventDispatcher {
/// when set, `get_listeners` returns this closure's result verbatim instead of resolving
/// registered listeners and package scripts.
get_listeners_override: Option<GetListenersOverride>,
+ /// For testing only. Mirrors PHPUnit's `getMockBuilder(EventDispatcher)->onlyMethods(['dispatchScript'])`:
+ /// when set, `dispatch_script` returns this closure's result instead of building and
+ /// dispatching a script event.
+ dispatch_script_override: Option<DispatchScriptOverride>,
}
/// For testing only. Holds a closure standing in for an overridden `getListeners` method.
@@ -112,6 +116,17 @@ impl std::fmt::Debug for GetListenersOverride {
}
}
+/// For testing only. Holds a closure standing in for an overridden `dispatchScript` method.
+pub struct DispatchScriptOverride(
+ pub Box<dyn Fn(&str, bool, &[String], &IndexMap<String, PhpMixed>) -> anyhow::Result<i64>>,
+);
+
+impl std::fmt::Debug for DispatchScriptOverride {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str("DispatchScriptOverride(..)")
+ }
+}
+
impl EventDispatcher {
pub fn new(
composer: PartialComposerWeakHandle,
@@ -142,6 +157,7 @@ impl EventDispatcher {
previous_hash: None,
previous_listeners: IndexMap::new(),
get_listeners_override: None,
+ dispatch_script_override: None,
}
}
@@ -155,6 +171,17 @@ impl EventDispatcher {
self.get_listeners_override = Some(GetListenersOverride(callback));
}
+ /// For testing only. Installs a closure that overrides `dispatch_script`, mirroring PHPUnit's
+ /// `onlyMethods(['dispatchScript'])->willReturnCallback(...)`.
+ pub fn __set_dispatch_script_override(
+ &mut self,
+ callback: Box<
+ dyn Fn(&str, bool, &[String], &IndexMap<String, PhpMixed>) -> anyhow::Result<i64>,
+ >,
+ ) {
+ self.dispatch_script_override = Some(DispatchScriptOverride(callback));
+ }
+
/// For testing only. Exposes the protected `getPhpExecCommand`, mirroring the PHP tests'
/// `new \ReflectionMethod($dispatcher, 'getPhpExecCommand')`.
pub fn __get_php_exec_command(&self) -> anyhow::Result<String> {
@@ -197,6 +224,10 @@ impl EventDispatcher {
additional_args: Vec<String>,
flags: IndexMap<String, PhpMixed>,
) -> anyhow::Result<i64> {
+ if let Some(over) = &self.dispatch_script_override {
+ return (over.0)(event_name, dev_mode, &additional_args, &flags);
+ }
+
let composer = self.composer();
assert!(
composer.is_full(),
diff --git a/crates/shirabe/src/util/auth_helper.rs b/crates/shirabe/src/util/auth_helper.rs
index c779a1bb..a323c37e 100644
--- a/crates/shirabe/src/util/auth_helper.rs
+++ b/crates/shirabe/src/util/auth_helper.rs
@@ -436,6 +436,39 @@ impl AuthHelper {
})
}
+ /// @param string[] $headers
+ ///
+ /// @return string[] updated headers array
+ pub fn add_authentication_header(
+ &mut self,
+ headers: Vec<String>,
+ origin: &str,
+ url: &str,
+ ) -> anyhow::Result<Vec<String>> {
+ shirabe_php_shim::trigger_error(
+ "AuthHelper::addAuthenticationHeader is deprecated since Composer 2.9 use addAuthenticationOptions instead.",
+ shirabe_php_shim::E_USER_DEPRECATED,
+ );
+
+ let mut http: IndexMap<String, PhpMixed> = IndexMap::new();
+ http.insert(
+ "header".to_string(),
+ PhpMixed::List(headers.into_iter().map(PhpMixed::String).collect()),
+ );
+ let mut options: IndexMap<String, PhpMixed> = IndexMap::new();
+ options.insert("http".to_string(), PhpMixed::Array(http));
+ let options = self.add_authentication_options(options, origin, url)?;
+
+ Ok(options["http"]
+ .as_array()
+ .and_then(|http| http.get("header"))
+ .and_then(|header| header.as_list())
+ .expect("addAuthenticationOptions always leaves http.header a list")
+ .iter()
+ .map(|v| v.as_string().unwrap_or("").to_string())
+ .collect())
+ }
+
/// @return array<string, mixed> updated options
pub fn add_authentication_options(
&mut self,
diff --git a/crates/shirabe/tests/application_test.rs b/crates/shirabe/tests/application_test.rs
index c7ec6cfd..6ce9166b 100644
--- a/crates/shirabe/tests/application_test.rs
+++ b/crates/shirabe/tests/application_test.rs
@@ -22,7 +22,7 @@ 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::output::buffered_output::BufferedOutput;
use shirabe_external_packages::symfony::console::output::output_interface::OutputInterface;
-use shirabe_php_shim::PhpMixed;
+use shirabe_php_shim::{PHP_EOL, PHP_SERVER, PhpMixed, define, defined, time};
fn set_up() {
Platform::put_env("COMPOSER_DISABLE_XDEBUG_WARN", "1");
@@ -40,15 +40,43 @@ impl Drop for TearDown {
}
}
-#[ignore = "no define() setter exists for the COMPOSER_DEV_WARNING_TIME constant (shim defined() is a fixed matches!)"]
+#[ignore = "shirabe_php_shim::define is a todo!() (there is no runtime constant registry), so COMPOSER_DEV_WARNING_TIME cannot be defined and defined() — a fixed matches! that omits it — keeps the warning branch unreachable"]
#[test]
fn test_dev_warning() {
let _tear_down = TearDown;
set_up();
- // TODO(phase-d): no define() setter exists for the COMPOSER_DEV_WARNING_TIME constant (the
- // shim's defined() is a fixed matches!), so this test's runtime define() cannot be reproduced.
- todo!()
+ let application = ApplicationHandle::new("Composer".to_string(), "".to_string()).unwrap();
+
+ if !defined("COMPOSER_DEV_WARNING_TIME") {
+ define("COMPOSER_DEV_WARNING_TIME", PhpMixed::Int(time() - 1));
+ }
+
+ let output = std::rc::Rc::new(std::cell::RefCell::new(BufferedOutput::new(
+ None, false, None,
+ )));
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(
+ ArrayInput::new(
+ vec![(PhpMixed::from("command"), PhpMixed::from("about"))],
+ None,
+ )
+ .unwrap(),
+ ));
+ let output_trait: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = output.clone();
+ application.do_run(input, output_trait).unwrap();
+
+ let expected_output = format!(
+ "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>{}",
+ PHP_SERVER
+ .lock()
+ .unwrap()
+ .get("PHP_SELF")
+ .unwrap_or_default()
+ .to_string_lossy(),
+ PHP_EOL
+ );
+ assert!(output.borrow().fetch().contains(&expected_output));
}
#[ignore = "SelfUpdateCommand::execute is intentionally stubbed with a Shirabe-specific \"not available\" message instead of the original Composer wording this test expects"]
diff --git a/crates/shirabe/tests/autoload/autoload_generator_test.rs b/crates/shirabe/tests/autoload/autoload_generator_test.rs
index 81f508cc..92baadcd 100644
--- a/crates/shirabe/tests/autoload/autoload_generator_test.rs
+++ b/crates/shirabe/tests/autoload/autoload_generator_test.rs
@@ -10,15 +10,18 @@ use shirabe::event_dispatcher::EventDispatcher;
use shirabe::filter::platform_requirement_filter::PlatformRequirementFilterFactory;
use shirabe::installer::{InstallationManager, InstallerInterface};
use shirabe::io::{BufferIO, IOInterface};
-use shirabe::package::handle::{AliasPackageHandle, PackageHandle, RootPackageHandle};
+use shirabe::package::handle::{
+ AliasPackageHandle, CompletePackageHandle, PackageHandle, RootPackageHandle,
+};
use shirabe::package::{Link, PackageInterfaceHandle, RootPackageInterfaceHandle};
use shirabe::repository::{
InstalledArrayRepository, InstalledRepositoryInterfaceHandle, WritableRepositoryInterface,
};
+use shirabe::script::ScriptEvents;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
use shirabe_external_packages::symfony::console::output::output_interface;
-use shirabe_php_shim::PhpMixed;
+use shirabe_php_shim::{PhpMixed, dirname, preg_quote, realpath, strtr};
use shirabe_semver::VersionParser;
use shirabe_semver::constraint::{AnyConstraint, MatchAllConstraint, SimpleConstraint};
use tempfile::TempDir;
@@ -125,6 +128,8 @@ struct SetUp {
working_dir: String,
vendor_dir: String,
repository: InstalledArrayRepository,
+ /// ref: `$this->configValueMap['use-include-path']`, which testUseGlobalIncludePath mutates.
+ use_include_path: bool,
im: InstallationManager,
io: std::rc::Rc<std::cell::RefCell<BufferIO>>,
generator: AutoloadGenerator,
@@ -199,6 +204,7 @@ fn set_up() -> SetUp {
working_dir,
vendor_dir,
repository,
+ use_include_path: false,
im,
io,
generator,
@@ -213,7 +219,7 @@ impl SetUp {
ConfigStubBuilder::new()
.with("vendor-dir", PhpMixed::String(self.vendor_dir.clone()))
.with("platform-check", PhpMixed::Bool(true))
- .with("use-include-path", PhpMixed::Bool(false))
+ .with("use-include-path", PhpMixed::Bool(self.use_include_path))
.build()
}
@@ -1740,11 +1746,164 @@ fn test_files_autoload_order_by_dependencies() {
}
#[test]
-#[ignore = "needs getCanonicalPackages consecutive-call return values (different package set per dump)"]
+#[serial]
fn test_files_autoload_generation_remove_extra_entities_from_autoload_files() {
- // TODO(phase-d): needs a repository mock returning a different package set on each of several
- // consecutive dump() calls (PHPUnit consecutive-call mock); not modeled by InstalledArrayRepository.
- todo!()
+ let mut s = set_up();
+ let autoload_package = new_root_pkg("root/a");
+ autoload_package.set_autoload(autoload(vec![("files", str_list(&["root.php"]))]));
+ autoload_package.__set_include_paths(vec!["/lib".to_string(), "/src".to_string()]);
+
+ let not_autoload_package = new_root_pkg("root/a");
+
+ let requires_ = || {
+ requires(vec![
+ ("a/a", link("a", "a/a", match_all(), None)),
+ ("b/b", link("a", "b/b", match_all(), None)),
+ ("c/c", link("a", "c/c", match_all(), None)),
+ ])
+ };
+ autoload_package.set_requires(requires_());
+ not_autoload_package.set_requires(requires_());
+
+ let a = new_pkg("a/a");
+ let b = new_pkg("b/b");
+ let c = new_pkg("c/c");
+ a.__set_autoload(autoload(vec![("files", str_list(&["test.php"]))]));
+ a.__set_include_paths(vec!["lib1".to_string(), "src1".to_string()]);
+ b.__set_autoload(autoload(vec![("files", str_list(&["test2.php"]))]));
+ b.__set_include_paths(vec!["lib2".to_string()]);
+ c.__set_autoload(autoload(vec![(
+ "files",
+ str_list(&["test3.php", "foo/bar/test4.php"]),
+ )]));
+ c.__set_include_paths(vec!["lib3".to_string()]);
+ c.__set_target_dir(Some("foo/bar".to_string()));
+ let autoload_packages: Vec<PackageInterfaceHandle> = vec![a.into(), b.into(), c.into()];
+
+ // PHP re-creates the three packages without autoload/include-path settings for the second and
+ // third `getCanonicalPackages` calls; the repository contents are swapped between dumps here.
+ let not_autoload_packages = || -> Vec<PackageInterfaceHandle> {
+ vec![
+ new_pkg("a/a").into(),
+ new_pkg("b/b").into(),
+ new_pkg("c/c").into(),
+ ]
+ };
+
+ s.ensure_dir(&format!("{}/a/a", s.vendor_dir));
+ s.ensure_dir(&format!("{}/b/b", s.vendor_dir));
+ s.ensure_dir(&format!("{}/c/c/foo/bar", s.vendor_dir));
+ s.put(
+ &format!("{}/a/a/test.php", s.vendor_dir),
+ "<?php function testFilesAutoloadGeneration1() {}",
+ );
+ s.put(
+ &format!("{}/b/b/test2.php", s.vendor_dir),
+ "<?php function testFilesAutoloadGeneration2() {}",
+ );
+ s.put(
+ &format!("{}/c/c/foo/bar/test3.php", s.vendor_dir),
+ "<?php function testFilesAutoloadGeneration3() {}",
+ );
+ s.put(
+ &format!("{}/c/c/foo/bar/test4.php", s.vendor_dir),
+ "<?php function testFilesAutoloadGeneration4() {}",
+ );
+ s.put(
+ &format!("{}/root.php", s.working_dir),
+ "<?php function testFilesAutoloadGenerationRoot() {}",
+ );
+
+ let vendor = s.vendor_dir.clone();
+ let composer_out = format!("{}/composer", vendor);
+ let fx = fixtures_dir();
+
+ s.set_canonical_packages(autoload_packages);
+ dump(
+ &mut s,
+ autoload_package.clone().into(),
+ false,
+ "FilesAutoload",
+ )
+ .unwrap();
+ assert_file_content_equals(
+ fx.join("autoload_functions.php").to_str().unwrap(),
+ &format!("{}/autoload.php", vendor),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_real_functions_with_include_paths.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_real.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_static_functions_with_include_paths.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_static.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_files_functions.php").to_str().unwrap(),
+ &format!("{}/autoload_files.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("include_paths_functions.php").to_str().unwrap(),
+ &format!("{}/include_paths.php", composer_out),
+ );
+
+ s.repository = InstalledArrayRepository::new().unwrap();
+ s.set_canonical_packages(not_autoload_packages());
+ dump(
+ &mut s,
+ autoload_package.clone().into(),
+ false,
+ "FilesAutoload",
+ )
+ .unwrap();
+ assert_file_content_equals(
+ fx.join("autoload_functions.php").to_str().unwrap(),
+ &format!("{}/autoload.php", vendor),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_real_functions_with_include_paths.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_real.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_files_functions_with_removed_extra.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_files.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("include_paths_functions_with_removed_extra.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/include_paths.php", composer_out),
+ );
+
+ s.repository = InstalledArrayRepository::new().unwrap();
+ s.set_canonical_packages(not_autoload_packages());
+ dump(&mut s, not_autoload_package.into(), false, "FilesAutoload").unwrap();
+ assert_file_content_equals(
+ fx.join("autoload_functions.php").to_str().unwrap(),
+ &format!("{}/autoload.php", vendor),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_real_functions_with_removed_include_paths_and_autolad_files.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_real.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_static_functions_with_removed_include_paths_and_autolad_files.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_static.php", composer_out),
+ );
+ assert!(!std::path::Path::new(&format!("{}/autoload_files.php", composer_out)).exists());
+ assert!(!std::path::Path::new(&format!("{}/include_paths.php", composer_out)).exists());
}
#[test]
@@ -1764,19 +1923,67 @@ fn test_include_paths_in_root_package() {
}
#[test]
-#[ignore = "EventDispatcher::dispatchScript spy not modeled"]
+#[serial]
fn test_pre_and_post_events_are_dispatched_during_autoload_dump() {
- // TODO(phase-d): requires spying on EventDispatcher::dispatchScript to record the events
- // dispatched around the dump; no dispatcher spy/mock hook is modeled yet.
- todo!()
+ let mut s = set_up();
+
+ let series = std::rc::Rc::new(std::cell::RefCell::new(vec![
+ (ScriptEvents::PRE_AUTOLOAD_DUMP, false),
+ (ScriptEvents::POST_AUTOLOAD_DUMP, false),
+ ]));
+ let dispatched = std::rc::Rc::new(std::cell::Cell::new(0));
+ let series_for_cb = series.clone();
+ let dispatched_for_cb = dispatched.clone();
+ s.event_dispatcher
+ .borrow_mut()
+ .__set_dispatch_script_override(Box::new(move |r#type, dev, _args, _flags| {
+ dispatched_for_cb.set(dispatched_for_cb.get() + 1);
+ let expected = series_for_cb.borrow_mut().remove(0);
+ assert_eq!(expected, (r#type, dev));
+
+ Ok(0)
+ }));
+
+ let package = new_root_pkg("root/a");
+ package.set_autoload(autoload(vec![(
+ "psr-0",
+ str_map(&[("Prefix", pstr("foo/bar/non/existing/"))]),
+ )]));
+
+ s.generator.set_run_scripts(true);
+ dump(&mut s, package.into(), true, "_8").unwrap();
+
+ assert_eq!(2, dispatched.get());
}
#[test]
-#[ignore = "asserts PHP get_include_path()/require behavior with use-include-path"]
+#[serial]
fn test_use_global_include_path() {
- // TODO(phase-d): asserts PHP get_include_path()/require() behavior driven by the
- // use-include-path setting; no Rust equivalent.
- todo!()
+ let mut s = set_up();
+ let package = new_root_pkg("root/a");
+ package.set_autoload(autoload(vec![(
+ "psr-0",
+ str_map(&[("Main\\Foo", pstr("")), ("Main\\Bar", pstr(""))]),
+ )]));
+ package.__set_target_dir(Some("Main/Foo/".to_string()));
+
+ s.use_include_path = true;
+
+ s.ensure_dir(&format!("{}/a", s.vendor_dir));
+
+ let composer_out = format!("{}/composer", s.vendor_dir);
+ dump(&mut s, package.into(), false, "IncludePath").unwrap();
+ let fx = fixtures_dir();
+ assert_file_content_equals(
+ fx.join("autoload_real_include_path.php").to_str().unwrap(),
+ &format!("{}/autoload_real.php", composer_out),
+ );
+ assert_file_content_equals(
+ fx.join("autoload_static_include_path.php")
+ .to_str()
+ .unwrap(),
+ &format!("{}/autoload_static.php", composer_out),
+ );
}
#[test]
@@ -1973,12 +2180,94 @@ fn test_up_level_relative_paths() {
}
#[test]
-#[ignore = "exercises buildPackageMap/parseAutoloads directly with multi-dump mutation"]
+#[serial]
fn test_autoload_rules_in_package_that_does_not_exist_on_disk() {
- // TODO(phase-d): exercises AutoloadGenerator::buildPackageMap/parseAutoloads directly across
- // multiple dump() calls with the package list mutated between calls; needs those internals
- // exposed to tests.
- todo!()
+ let mut s = set_up();
+ let package = new_root_pkg("root/a");
+ package.set_requires(requires(vec![(
+ "dep/a",
+ link("root/a", "dep/a", match_all(), Some("requires")),
+ )]));
+ let dep = CompletePackageHandle::new("dep/a".to_string(), "1.0".to_string(), "1.0".to_string());
+
+ s.set_canonical_packages(vec![dep.clone().into()]);
+
+ dep.__set_autoload(autoload(vec![(
+ "psr-0",
+ str_map(&[("Foo", pstr("./src"))]),
+ )]));
+ dump(&mut s, package.clone().into(), true, "_19").unwrap();
+
+ let expected_namespace = "<?php\n\n// autoload_namespaces.php @generated by Composer\n\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\n\nreturn array(\n 'Foo' => array($vendorDir . '/dep/a/src'),\n);\n";
+ assert_str_equals_file(
+ expected_namespace,
+ &format!("{}/composer/autoload_namespaces.php", s.vendor_dir),
+ );
+
+ dep.__set_autoload(autoload(vec![(
+ "psr-4",
+ str_map(&[("Acme\\Foo\\", pstr("./src-psr4"))]),
+ )]));
+ dump(&mut s, package.clone().into(), true, "_19").unwrap();
+
+ let expected_psr4 = "<?php\n\n// autoload_psr4.php @generated by Composer\n\n$vendorDir = dirname(__DIR__);\n$baseDir = dirname($vendorDir);\n\nreturn array(\n 'Acme\\\\Foo\\\\' => array($vendorDir . '/dep/a/src-psr4'),\n);\n";
+ assert_str_equals_file(
+ expected_psr4,
+ &format!("{}/composer/autoload_psr4.php", s.vendor_dir),
+ );
+
+ dep.__set_autoload(autoload(vec![("classmap", str_list(&["classmap"]))]));
+ let expected_message = format!(
+ "Could not scan for classes inside \"{}/dep/a/classmap\" which does not appear to be a file nor a folder",
+ s.vendor_dir
+ );
+ if let Err(e) = dump(&mut s, package.clone().into(), true, "_19") {
+ assert_eq!(expected_message, e.to_string());
+ }
+
+ dep.__set_autoload(autoload(vec![("files", str_list(&["./test.php"]))]));
+ dump(&mut s, package.clone().into(), true, "_19").unwrap();
+ let files =
+ std::fs::read_to_string(format!("{}/composer/autoload_files.php", s.vendor_dir)).unwrap();
+ assert!(files.contains("$vendorDir . '/dep/a/test.php',\n"));
+
+ package.set_autoload(autoload(vec![(
+ "exclude-from-classmap",
+ str_list(&["../excludedroot", "root/excl"]),
+ )]));
+ dep.__set_autoload(autoload(vec![(
+ "exclude-from-classmap",
+ str_list(&["../../excluded", "foo/bar"]),
+ )]));
+ let map = s
+ .generator
+ .build_package_map(&mut s.im, package.clone().into(), vec![dep.clone().into()])
+ .unwrap();
+ let parsed = s
+ .generator
+ .parse_autoloads(map, package.into(), PhpMixed::Bool(false));
+ let excluded_root = format!(
+ "{}/excludedroot($|/)",
+ preg_quote(
+ &strtr(
+ &realpath(dirname(&s.working_dir)).unwrap_or_default(),
+ "\\",
+ "/"
+ ),
+ None
+ )
+ );
+ let root_excl = format!(
+ "{}/root/excl($|/)",
+ preg_quote(
+ &strtr(&realpath(&s.working_dir).unwrap_or_default(), "\\", "/"),
+ None
+ )
+ );
+ assert_eq!(
+ str_map(&[("0", pstr(&excluded_root)), ("1", pstr(&root_excl))]),
+ parsed["exclude-from-classmap"]
+ );
}
/// ref: AutoloadGeneratorTest::platformCheckProvider — builds the link map for a requires/provides/
diff --git a/crates/shirabe/tests/autoload/class_loader_test.rs b/crates/shirabe/tests/autoload/class_loader_test.rs
index 582e24fa..998683c9 100644
--- a/crates/shirabe/tests/autoload/class_loader_test.rs
+++ b/crates/shirabe/tests/autoload/class_loader_test.rs
@@ -1,13 +1,37 @@
//! ref: composer/tests/Composer/Test/Autoload/ClassLoaderTest.php
use shirabe::autoload::class_loader::ClassLoader;
+use shirabe_php_shim::class_exists;
+
+/// ref: ClassLoaderTest::getLoadClassTests
+fn get_load_class_tests() -> Vec<&'static str> {
+ vec![
+ "Namespaced\\Foo",
+ "Pearlike_Foo",
+ "ShinyVendor\\ShinyPackage\\SubNamespace\\Foo",
+ ]
+}
#[test]
-#[ignore = "depends on PHP runtime class_exists() to verify loadClass defined a class; no Rust equivalent"]
+#[ignore = "shirabe_php_shim::class_exists models a fixed set of classes available in a PHP CLI environment; loadClass cannot add to it because including a PHP file does not define a class on the Rust side"]
fn test_load_class() {
- // TODO(phase-d): loadClass() include()s a fixture and PHPUnit asserts via class_exists();
- // Rust has no equivalent of runtime class definition/loading.
- todo!()
+ let fixtures = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/Autoload/Fixtures")
+ .canonicalize()
+ .unwrap()
+ .display()
+ .to_string();
+
+ for class in get_load_class_tests() {
+ let mut loader = ClassLoader::new(None);
+ loader.add("Namespaced\\", vec![fixtures.clone()], false);
+ loader.add("Pearlike_", vec![fixtures.clone()], false);
+ loader
+ .add_psr4("ShinyVendor\\ShinyPackage\\", vec![fixtures.clone()], false)
+ .unwrap();
+ loader.load_class(class);
+ assert!(class_exists(class), "->loadClass() loads '{}'", class);
+ }
}
#[test]
@@ -17,9 +41,10 @@ fn test_get_prefixes_with_no_psr0_configuration() {
}
#[test]
-#[ignore = "depends on PHP serialize()/unserialize() round-trip of ClassLoader; no Rust equivalent"]
+#[ignore = "the round trip is `unserialize(serialize($loader))`: shirabe_php_shim::serialize takes a PhpMixed (a ClassLoader cannot be turned into one) and there is no unserialize at all, so the ClassLoader under test cannot be round-tripped"]
fn test_serializability() {
- // TODO(phase-d): serializes/unserializes the ClassLoader and compares every getter; PHP
- // serialize()/unserialize() has no Rust equivalent here.
+ // TODO(phase-d): the round trip is `unserialize(serialize($loader))`. serialize() in the shim
+ // takes a PhpMixed, which a ClassLoader cannot be converted into, and there is no unserialize
+ // symbol to produce the second ClassLoader the assertions compare against.
todo!()
}
diff --git a/crates/shirabe/tests/command/run_script_command_test.rs b/crates/shirabe/tests/command/run_script_command_test.rs
index 10e171ba..311091a0 100644
--- a/crates/shirabe/tests/command/run_script_command_test.rs
+++ b/crates/shirabe/tests/command/run_script_command_test.rs
@@ -121,20 +121,239 @@ fn test_can_define_aliases() {
drop(tear_down);
}
+/// ref: RunScriptCommandTest::testExecutionOfSimpleSymfonyCommand
#[test]
-#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"]
+#[serial]
+#[ignore = "invoking the script name as a top-level composer command needs Application::do_run to import the user's PHP Command class as a live application command, which is a todo!() in application.rs, and the worker writes to inherited stdio the in-process application tester cannot capture"]
fn test_execution_of_simple_symfony_command() {
- // TODO(phase-d): the test invokes the script name as a top-level composer command, which
- // requires Application::do_run to import the user's PHP Command class (MyCommand.php) as a
- // live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture.
- todo!()
+ let description = "Sample description for test command";
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({
+ "scripts": {
+ "test-direct": "Test\\MyCommand",
+ "test-ref": ["@test-direct --inneropt innerarg"],
+ },
+ "scripts-descriptions": {
+ "test-direct": description,
+ },
+ "autoload": {
+ "psr-4": {
+ "Test\\": "",
+ },
+ },
+ })),
+ None,
+ None,
+ true,
+ );
+
+ std::fs::write(
+ "MyCommand.php",
+ r#"<?php
+
+namespace Test;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Command\Command;
+
+class MyCommand extends Command
+{
+ protected function configure(): void
+ {
+ $this->setDefinition([
+ new InputArgument('req-arg', InputArgument::REQUIRED, 'Required arg.'),
+ new InputArgument('opt-arg', InputArgument::OPTIONAL, 'Optional arg.'),
+ new InputOption('inneropt', null, InputOption::VALUE_NONE, 'Option.'),
+ new InputOption('outeropt', null, InputOption::VALUE_OPTIONAL, 'Optional option.'),
+ ]);
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->writeln($input->getArgument('req-arg'));
+ $output->writeln((string) $input->getArgument('opt-arg'));
+ $output->writeln('inneropt: '.($input->getOption('inneropt') ? 'set' : 'unset'));
+ $output->writeln('outeropt: '.($input->getOption('outeropt') ? 'set' : 'unset'));
+
+ return 2;
+ }
+}
+"#,
+ )
+ .unwrap();
+
+ let mut app_tester = get_application_tester();
+ app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("test-direct")),
+ (PhpMixed::from("--outeropt"), PhpMixed::from(true)),
+ (PhpMixed::from("req-arg"), PhpMixed::from("lala")),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+
+ assert_eq!(
+ "lala\n\ninneropt: unset\nouteropt: set\n",
+ app_tester.get_display()
+ );
+ assert_eq!(2, app_tester.get_status_code());
+
+ let mut app_tester = get_application_tester();
+ app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("test-ref")),
+ (PhpMixed::from("--outeropt"), PhpMixed::from(true)),
+ (PhpMixed::from("req-arg"), PhpMixed::from("lala")),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+
+ assert_eq!(
+ "innerarg\nlala\ninneropt: set\nouteropt: set\n",
+ app_tester.get_display()
+ );
+ assert_eq!(2, app_tester.get_status_code());
+
+ // check if the description from composer.json is correctly shown
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("run-script")),
+ (PhpMixed::from("--list"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code, "assertCommandIsSuccessful");
+ let output = app_tester.get_display();
+ assert!(
+ output.contains(description),
+ "The contents of scripts-description for the test script should be printed"
+ );
+
+ drop(tear_down);
}
+/// ref: RunScriptCommandTest::testExecutionOfSymfonyCommandWithConfiguration
#[test]
-#[ignore = "the test invokes the script name as a top-level composer command, which requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php) as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the command's output would go to the worker's inherited stdio, which the in-process application tester cannot capture"]
+#[serial]
+#[ignore = "invoking the script name as a top-level composer command needs Application::do_run to import the user's PHP Command class as a live application command, which is a todo!() in application.rs, and the worker writes to inherited stdio the in-process application tester cannot capture"]
fn test_execution_of_symfony_command_with_configuration() {
- // TODO(phase-d): the test invokes the script name as a top-level composer command, which
- // requires Application::do_run to import the user's PHP Command class (MyCommandWithDefinitions.php)
- // as a live application command (todo!() in application.rs: the worker-side console application exists, but the import arm is not wired to it), and the worker writes to inherited stdio the tester cannot capture.
- todo!()
+ let cmd_name = "custom-cmd-123";
+ let cmd_alias = format!("{}-alias", cmd_name);
+ let cmd_desc = "This is a Symfony command with custom configuration";
+ let wrong_desc = "this should be ignored";
+
+ let tear_down = init_temp_composer(
+ Some(&serde_json::json!({
+ "scripts": {
+ cmd_name: "Test\\MyCommandWithDefinitions",
+ },
+ "scripts-descriptions": {
+ cmd_name: wrong_desc,
+ },
+ "autoload": {
+ "psr-4": {
+ "Test\\": "",
+ },
+ },
+ })),
+ None,
+ None,
+ true,
+ );
+
+ std::fs::write(
+ "MyCommandWithDefinitions.php",
+ r#"<?php
+
+namespace Test;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Command\Command;
+
+class MyCommandWithDefinitions extends Command
+{
+ protected function configure(): void
+ {
+ $this
+ ->setDescription('__CMD_DESC__')
+ ->setAliases(['__CMD_ALIAS__'])
+ ->setDefinition([new InputArgument('req-arg', InputArgument::REQUIRED, 'Required arg.')]);
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $output->writeln($input->getArgument('req-arg'));
+ return Command::SUCCESS;
+ }
+}
+"#
+ .replace("__CMD_DESC__", cmd_desc)
+ .replace("__CMD_ALIAS__", &cmd_alias),
+ )
+ .unwrap();
+
+ // makes sure the command executes with the name defined inside its `configure()`...
+ let mut app_tester = get_application_tester();
+ app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from(cmd_name)),
+ (PhpMixed::from("req-arg"), PhpMixed::from("lala")),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!("lala\n", app_tester.get_display());
+
+ // ...with the alias defined there as well...
+ let mut app_tester = get_application_tester();
+ app_tester
+ .run(
+ vec![
+ (
+ PhpMixed::from("command"),
+ PhpMixed::from(cmd_alias.as_str()),
+ ),
+ (PhpMixed::from("req-arg"), PhpMixed::from("lala")),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!("lala\n", app_tester.get_display());
+
+ // ...and also uses its own description, instead of the one in composer.scripts-descriptions
+ let mut app_tester = get_application_tester();
+ let status_code = app_tester
+ .run(
+ vec![
+ (PhpMixed::from("command"), PhpMixed::from("run-script")),
+ (PhpMixed::from("--list"), PhpMixed::from(true)),
+ ],
+ RunOptions::default(),
+ )
+ .unwrap();
+ assert_eq!(0, status_code, "assertCommandIsSuccessful");
+ let output = app_tester.get_display();
+ assert!(
+ output.contains(cmd_desc),
+ "The custom description for the test script should be printed"
+ );
+ assert!(
+ !output.contains(wrong_desc),
+ "The dummy description shouldn't show"
+ );
+
+ drop(tear_down);
}
diff --git a/crates/shirabe/tests/command/self_update_command_test.rs b/crates/shirabe/tests/command/self_update_command_test.rs
index 3b73adbb..6dc64104 100644
--- a/crates/shirabe/tests/command/self_update_command_test.rs
+++ b/crates/shirabe/tests/command/self_update_command_test.rs
@@ -1,40 +1,102 @@
//! ref: composer/tests/Composer/Test/Command/SelfUpdateCommandTest.php
use crate::test_case::{RunOptions, get_application_tester, init_temp_composer};
+use indexmap::IndexMap;
use serial_test::serial;
-use shirabe_php_shim::PhpMixed;
+use shirabe_external_packages::symfony::process::Process;
+use shirabe_php_shim::{PHP_BINARY, PhpMixed};
-/// ref: SelfUpdateCommandTest::setUp (portable part: initTempComposer; the composer-test.phar copy
-/// is omitted because the phar fixture and Symfony Process are not ported).
+/// ref: SelfUpdateCommandTest::setUp. The `composer-test.phar` copy PHP also performs here lives in
+/// `set_up_with_phar` instead, so the one test that never touches the phar is not blocked by the
+/// missing fixture.
fn set_up() -> crate::test_case::TearDown {
init_temp_composer(None, None, None, true)
}
+/// ref: SelfUpdateCommandTest::setUp, including the `composer-test.phar` copy. Returns the tear-down
+/// guard and `$this->phar`.
+fn set_up_with_phar() -> (crate::test_case::TearDown, String) {
+ let tear_down = set_up();
+ let phar = tear_down.working_dir().join("composer.phar");
+ std::fs::copy(
+ std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/composer-test.phar"),
+ &phar,
+ )
+ .unwrap();
+
+ (tear_down, phar.display().to_string())
+}
+
+/// ref: SelfUpdateCommandTest::channelOptions
+fn channel_options() -> Vec<(&'static str, &'static str)> {
+ vec![
+ ("--stable", "stable channel"),
+ ("--preview", "preview channel"),
+ ("--snapshot", "snapshot channel"),
+ ]
+}
+
#[test]
#[serial]
-#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update'])` running composer-test.phar \
- over HTTP; requires Symfony Process and the composer-test.phar fixture, neither ported"]
+#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"]
fn test_successful_update() {
- let _tear_down = set_up();
+ let (_tear_down, phar) = set_up_with_phar();
+
+ if shirabe::composer::VERSION != concat!("@package_version", "@") {
+ eprintln!(
+ "skipping: On releases this test can fail to upgrade as we are already on latest version"
+ );
+ return;
+ }
+
+ let mut app_tester = Process::new(
+ vec![PHP_BINARY.to_string(), phar, "self-update".to_string()],
+ None,
+ None,
+ PhpMixed::Null,
+ None,
+ )
+ .unwrap();
+ let status = app_tester.run(None, IndexMap::new()).unwrap();
+ assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap());
- // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update'])` running
- // composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar fixture,
- // neither ported.
- todo!()
+ assert!(
+ app_tester
+ .get_output()
+ .unwrap()
+ .contains("Upgrading to version")
+ );
}
#[test]
#[serial]
-#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', '2.4.0'])` running \
- composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar \
- fixture, neither ported"]
+#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"]
fn test_update_to_specific_version() {
- let _tear_down = set_up();
+ let (_tear_down, phar) = set_up_with_phar();
+
+ let mut app_tester = Process::new(
+ vec![
+ PHP_BINARY.to_string(),
+ phar,
+ "self-update".to_string(),
+ "2.4.0".to_string(),
+ ],
+ None,
+ None,
+ PhpMixed::Null,
+ None,
+ )
+ .unwrap();
+ let status = app_tester.run(None, IndexMap::new()).unwrap();
+ assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap());
- // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update', '2.4.0'])`
- // running composer-test.phar over HTTP; requires Symfony Process and the composer-test.phar
- // fixture, neither ported.
- todo!()
+ assert!(
+ app_tester
+ .get_output()
+ .unwrap()
+ .contains("Upgrading to version 2.4.0")
+ );
}
#[test]
@@ -63,14 +125,38 @@ fn test_update_with_invalid_option_throws_exception() {
#[test]
#[serial]
-#[ignore = "spawns `new Process([PHP_BINARY, $this->phar, 'self-update', $option])` running \
- composer-test.phar over HTTP (data provider: --stable/--preview/--snapshot); requires \
- Symfony Process and the composer-test.phar fixture, neither ported"]
+#[ignore = "composer-test.phar is built by AllFunctionalTest::testBuildPhar via bin/compile, which has no equivalent here, so the fixture set_up_with_phar copies can never exist"]
fn test_update_to_different_channel() {
- let _tear_down = set_up();
+ for (option, expected_output) in channel_options() {
+ let (_tear_down, phar) = set_up_with_phar();
+
+ if shirabe::composer::VERSION != concat!("@package_version", "@")
+ && ["--stable", "--preview"].contains(&option)
+ {
+ eprintln!(
+ "skipping: On releases this test can fail to upgrade as we are already on latest version"
+ );
+ continue;
+ }
+
+ let mut app_tester = Process::new(
+ vec![
+ PHP_BINARY.to_string(),
+ phar,
+ "self-update".to_string(),
+ option.to_string(),
+ ],
+ None,
+ None,
+ PhpMixed::Null,
+ None,
+ )
+ .unwrap();
+ let status = app_tester.run(None, IndexMap::new()).unwrap();
+ assert_eq!(0, status, "{}", app_tester.get_error_output().unwrap());
- // TODO(phase-d): spawns `new Process([PHP_BINARY, $this->phar, 'self-update', $option])`
- // running composer-test.phar over HTTP (data provider: --stable/--preview/--snapshot);
- // requires Symfony Process and the composer-test.phar fixture, neither ported.
- todo!()
+ let output = app_tester.get_output().unwrap();
+ assert!(output.contains("Upgrading to version"));
+ assert!(output.contains(expected_output));
+ }
}
diff --git a/crates/shirabe/tests/downloader/file_downloader_test.rs b/crates/shirabe/tests/downloader/file_downloader_test.rs
index 65dc1a51..469bf473 100644
--- a/crates/shirabe/tests/downloader/file_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/file_downloader_test.rs
@@ -177,20 +177,24 @@ fn test_download_but_file_is_unsaved() {
}
#[test]
-#[ignore = "requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey plus PreFileDownloadEvent::setProcessedUrl dispatch, which is TODO(plugin) in FileDownloader::download"]
+#[ignore = "the listener is a closure that mutates the event (setProcessedUrl), but Callable::Closure receives `&dyn EventInterface`, so it cannot; and CacheMock has no copy_to/copy_from hooks to assert the cache key on"]
fn test_download_with_custom_processed_url() {
- // TODO(phase-d): requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey
- // plus PreFileDownloadEvent::setProcessedUrl dispatch, which is TODO(plugin) in
- // FileDownloader::download.
+ // TODO(phase-d): the PRE_FILE_DOWNLOAD listener is a closure calling
+ // PreFileDownloadEvent::setProcessedUrl, but Callable::Closure is
+ // `Fn(&dyn EventInterface)`, so a listener cannot mutate the event it receives. The Cache
+ // half is likewise inexpressible: CacheMock carries only finder/gc overrides, with no
+ // copy_to/copy_from hook to assert the cache key on.
todo!()
}
#[test]
-#[ignore = "requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey plus PreFileDownloadEvent::setCustomCacheKey dispatch, which is TODO(plugin) in FileDownloader::download"]
+#[ignore = "the listener is a closure that mutates the event (setCustomCacheKey), but Callable::Closure receives `&dyn EventInterface`, so it cannot; and CacheMock has no copy_to/copy_from hooks to assert the cache key on"]
fn test_download_with_custom_cache_key() {
- // TODO(phase-d): requires PHPUnit mocks of Cache::copyTo/copyFrom asserting on $cacheKey
- // plus PreFileDownloadEvent::setCustomCacheKey dispatch, which is TODO(plugin) in
- // FileDownloader::download.
+ // TODO(phase-d): the PRE_FILE_DOWNLOAD listener is a closure calling
+ // PreFileDownloadEvent::setCustomCacheKey, but Callable::Closure is
+ // `Fn(&dyn EventInterface)`, so a listener cannot mutate the event it receives. The Cache
+ // half is likewise inexpressible: CacheMock carries only finder/gc overrides, with no
+ // copy_to/copy_from hook to assert the cache key on.
todo!()
}
diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
index 674c4a80..d97c14f0 100644
--- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
+++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
@@ -1,5 +1,6 @@
//! ref: composer/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php
+use crate::io_mock::{Expectation, get_io_mock};
use crate::process_executor_mock::{ProcessExecutorMockGuard, cmd, get_process_executor_mock};
use indexmap::IndexMap;
use serial_test::serial;
@@ -13,6 +14,7 @@ use shirabe::filter::PlatformRequirementFilterInterface;
use shirabe::installer::{InstallationManagerInterface, InstallerEvents, InstallerInterface};
use shirabe::io::IOInterface;
use shirabe::io::buffer_io::BufferIO;
+use shirabe::io::io_interface;
use shirabe::package::{
LockerInterface, PackageInterfaceHandle, RootPackageHandle, RootPackageInterfaceHandle,
};
@@ -111,6 +113,18 @@ fn dispatcher_with_listeners(
dispatcher
}
+/// ref: EventDispatcherTest::getDispatcherStubForListenersTest — same mocked `getListeners`, but
+/// constructed without a ProcessExecutor.
+fn dispatcher_stub_for_listeners_test(
+ composer: &ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ listeners: Vec<&str>,
+) -> EventDispatcher {
+ let mut dispatcher = EventDispatcher::new(composer.upcast().downgrade(), io, None);
+ dispatcher.__set_get_listeners_override(listeners_const(listeners));
+ dispatcher
+}
+
fn listeners_const(listeners: Vec<&str>) -> Box<dyn Fn(&dyn EventInterface) -> Vec<Callable>> {
let listeners: Vec<String> = listeners.into_iter().map(|s| s.to_string()).collect();
Box::new(move |_event| listeners.iter().cloned().map(Callable::String).collect())
@@ -342,13 +356,47 @@ fn test_dispatcher_doesnt_return_skipped_scripts() {
// unilaterally under the no-test-alteration rule.
#[test]
+#[serial]
#[ignore = "listener `EventDispatcherTest::call` is a static method of the PHPUnit test class itself; the PHP worker cannot load it (extends PHPUnit\\Framework\\TestCase, phpunit absent from composer/vendor) — see the note above the ignored block"]
fn test_listener_exceptions_are_caught() {
let _tear_down = TearDown;
- // TODO(phase-d): the listener is a static method of the PHPUnit test class itself, which
- // the PHP worker cannot load (phpunit is absent from composer/vendor); pending a decision on
- // providing the listener methods to the child process.
- todo!()
+
+ let (io_mock, _io_guard) = get_io_mock(io_interface::NORMAL).unwrap();
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io_mock.clone();
+
+ let composer = create_composer_instance();
+ let mut dispatcher = dispatcher_stub_for_listeners_test(
+ &composer,
+ io_dyn,
+ vec!["Composer\\Test\\EventDispatcher\\EventDispatcherTest::call"],
+ );
+
+ io_mock
+ .borrow_mut()
+ .expects(
+ vec![
+ Expectation::text("> Composer\\Test\\EventDispatcher\\EventDispatcherTest::call"),
+ Expectation::text(
+ "Script Composer\\Test\\EventDispatcher\\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception",
+ ),
+ ],
+ true,
+ )
+ .unwrap();
+
+ let result = dispatcher.dispatch_script(
+ ScriptEvents::POST_INSTALL_CMD,
+ false,
+ vec![],
+ IndexMap::new(),
+ );
+
+ let e = result.expect_err("expected RuntimeException");
+ assert!(
+ e.downcast_ref::<shirabe_php_shim::RuntimeException>()
+ .is_some(),
+ "got: {e:?}"
+ );
}
// PHP mocks `Composer\Autoload\AutoloadGenerator` with onlyMethods(['buildPackageMap',
@@ -535,43 +583,214 @@ fn test_dispatcher_pass_dev_mode_to_autoload_generator_for_script_events() {
}
#[test]
-#[ignore = "listeners are object-method array callables ([\\$this, 'someMethod']) invoked + removed by object identity; the array-callable invocation path is an unimplemented plugin-runtime stub"]
+#[serial]
+#[ignore = "the object-method listeners are methods of the PHPUnit test class itself; invoking them sends a CallMethod for a phandle that has no counterpart in the worker, which cannot load that class (extends PHPUnit\\Framework\\TestCase, phpunit absent from composer/vendor) — see the note above the ignored block"]
fn test_dispatcher_remove_listener() {
let _tear_down = TearDown;
- // TODO(phase-d): listeners are object-method array callables ([$this, 'someMethod']) invoked
- // and removed by object identity; the array-callable invocation path is an unimplemented
- // plugin-runtime stub
- todo!()
+
+ let composer = create_composer_instance();
+
+ let mut repository_manager = MockRepositoryManager::new();
+ repository_manager
+ .expect_get_local_repository()
+ .returning(|| RepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()));
+ composer
+ .borrow_mut()
+ .set_repository_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ repository_manager,
+ )));
+ composer
+ .borrow_mut()
+ .set_installation_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ MockInstallationManager::new(),
+ )));
+
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+ let io = buffer_io_verbose();
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+ let mut dispatcher = EventDispatcher::new(composer.upcast().downgrade(), io_dyn, Some(process));
+
+ // PHP's `[$this, 'someMethod']` is an array callable whose object half is the test instance.
+ // `Callable::PhpMethod` is the port's shape for an object-half callable: it carries the
+ // cross-RPC identity `remove_listener` compares, and echoes `Class->method` like PHP does.
+ let this = shirabe_php_rpc::PhpObjHandle {
+ phandle: 1,
+ class: "Composer\\Test\\EventDispatcher\\EventDispatcherTest".to_string(),
+ implements: vec![],
+ };
+ let listener = Callable::PhpMethod(this.clone(), "someMethod".to_string());
+ let listener2 = Callable::PhpMethod(this.clone(), "someMethod2".to_string());
+ let listener3 = Callable::String(
+ "Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod".to_string(),
+ );
+
+ dispatcher.add_listener("ev1", listener.clone(), 0);
+ dispatcher.add_listener("ev1", listener.clone(), 1);
+ dispatcher.add_listener("ev1", listener2, 1);
+ dispatcher.add_listener("ev1", listener3.clone(), 0);
+ dispatcher.add_listener("ev2", listener3, 0);
+ dispatcher.add_listener("ev2", listener, 0);
+ dispatcher.dispatch(Some("ev1"), None).unwrap();
+ dispatcher.dispatch(Some("ev2"), None).unwrap();
+
+ let mut expected = format!(
+ "> ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}\
+ > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod2{eol}\
+ > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}\
+ > ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\
+ > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\
+ > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest->someMethod{eol}",
+ eol = PHP_EOL
+ );
+ assert_eq!(expected, io.borrow().get_output());
+
+ dispatcher.remove_listener(&this);
+ dispatcher.dispatch(Some("ev1"), None).unwrap();
+ dispatcher.dispatch(Some("ev2"), None).unwrap();
+
+ expected += &format!(
+ "> ev1: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}\
+ > ev2: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}",
+ eol = PHP_EOL
+ );
+ assert_eq!(expected, io.borrow().get_output());
}
#[test]
+#[serial]
#[ignore = "listener `EventDispatcherTest::someMethod` is a static method of the PHPUnit test class itself; the PHP worker cannot load it — see the note above the ignored block"]
fn test_dispatcher_can_execute_cli_and_php_in_same_event_script_stack() {
let _tear_down = TearDown;
- // TODO(phase-d): the PHP-script listener is a static method of the PHPUnit test class
- // itself, which the PHP worker cannot load; pending a decision on providing the listener
- // methods to the child process.
- todo!()
+
+ let (process, _process_guard) = get_process_executor_mock(
+ vec![cmd("echo -n foo"), cmd("echo -n bar")],
+ true,
+ MockHandler::default(),
+ );
+
+ let composer = create_composer_instance();
+ let io = buffer_io_verbose();
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+
+ let mut dispatcher = dispatcher_with_listeners(
+ &composer,
+ io_dyn,
+ process,
+ listeners_const(vec![
+ "echo -n foo",
+ "Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod",
+ "echo -n bar",
+ ]),
+ );
+
+ dispatcher
+ .dispatch_script(
+ ScriptEvents::POST_INSTALL_CMD,
+ false,
+ vec![],
+ IndexMap::new(),
+ )
+ .unwrap();
+
+ let expected = format!(
+ "> post-install-cmd: echo -n foo{eol}> post-install-cmd: Composer\\Test\\EventDispatcher\\EventDispatcherTest::someMethod{eol}> post-install-cmd: echo -n bar{eol}",
+ eol = PHP_EOL
+ );
+ assert_eq!(expected, io.borrow().get_output());
}
#[test]
+#[serial]
#[ignore = "listener `EventDispatcherTest::getTestEnv` is a static method of the PHPUnit test class itself; the PHP worker cannot load it — see the note above the ignored block"]
fn test_dispatcher_can_put_env() {
let _tear_down = TearDown;
- // TODO(phase-d): the second listener is a static method of the PHPUnit test class itself,
- // which the PHP worker cannot load; pending a decision on providing the listener methods to
- // the child process.
- todo!()
+
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+
+ let composer = create_composer_instance();
+ let io = buffer_io_verbose();
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+
+ let mut dispatcher = dispatcher_with_listeners(
+ &composer,
+ io_dyn,
+ process,
+ listeners_const(vec![
+ "@putenv ABC=123",
+ "Composer\\Test\\EventDispatcher\\EventDispatcherTest::getTestEnv",
+ ]),
+ );
+
+ dispatcher
+ .dispatch_script(
+ ScriptEvents::POST_INSTALL_CMD,
+ false,
+ vec![],
+ IndexMap::new(),
+ )
+ .unwrap();
+
+ let expected = format!(
+ "> post-install-cmd: @putenv ABC=123{eol}> post-install-cmd: Composer\\Test\\EventDispatcher\\EventDispatcherTest::getTestEnv{eol}",
+ eol = PHP_EOL
+ );
+ assert_eq!(expected, io.borrow().get_output());
}
#[test]
+#[serial]
#[ignore = "listeners (createsVendorBinFolderChecksEnv*) are static methods of the PHPUnit test class itself; the PHP worker cannot load them — see the note above the ignored block"]
fn test_dispatcher_appends_dir_bin_on_path_for_every_listener() {
let _tear_down = TearDown;
- // TODO(phase-d): the listeners are static methods of the PHPUnit test class itself, which
- // the PHP worker cannot load; pending a decision on providing the listener methods to the
- // child process.
- todo!()
+
+ let current_directory_bkp = Platform::get_cwd(false).unwrap();
+ let composer_bin_dir_bkp = Platform::get_env("COMPOSER_BIN_DIR");
+ // ref: __DIR__ of EventDispatcherTest.php, where the listeners create `vendor/bin`.
+ let php_test_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/EventDispatcher")
+ .canonicalize()
+ .unwrap();
+ std::env::set_current_dir(&php_test_dir).unwrap();
+ Platform::put_env(
+ "COMPOSER_BIN_DIR",
+ &format!("{}/vendor/bin", php_test_dir.display()),
+ );
+
+ let (process, _process_guard) =
+ get_process_executor_mock(vec![], false, MockHandler::default());
+
+ let composer = create_composer_instance();
+ let io = buffer_io_verbose();
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+
+ let mut dispatcher = dispatcher_with_listeners(
+ &composer,
+ io_dyn,
+ process,
+ listeners_const(vec![
+ "Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvDoesNotContainsBin",
+ "Composer\\Test\\EventDispatcher\\EventDispatcherTest::createsVendorBinFolderChecksEnvContainsBin",
+ ]),
+ );
+
+ dispatcher
+ .dispatch_script(
+ ScriptEvents::POST_INSTALL_CMD,
+ false,
+ vec![],
+ IndexMap::new(),
+ )
+ .unwrap();
+ std::fs::remove_dir(php_test_dir.join("vendor/bin")).unwrap();
+ std::fs::remove_dir(php_test_dir.join("vendor")).unwrap();
+
+ std::env::set_current_dir(&current_directory_bkp).unwrap();
+ match composer_bin_dir_bkp {
+ Some(dir) if !dir.is_empty() => Platform::put_env("COMPOSER_BIN_DIR", &dir),
+ _ => Platform::clear_env("COMPOSER_BIN_DIR"),
+ }
}
#[test]
diff --git a/crates/shirabe/tests/event_dispatcher/main.rs b/crates/shirabe/tests/event_dispatcher/main.rs
index eac717d7..f51b0f2d 100644
--- a/crates/shirabe/tests/event_dispatcher/main.rs
+++ b/crates/shirabe/tests/event_dispatcher/main.rs
@@ -1,3 +1,5 @@
+#[path = "../common/io_mock.rs"]
+mod io_mock;
#[path = "../common/io_stub.rs"]
mod io_stub;
#[path = "../common/process_executor_mock.rs"]
diff --git a/crates/shirabe/tests/util/auth_helper_test.rs b/crates/shirabe/tests/util/auth_helper_test.rs
index 91d9cb75..ef5daaf7 100644
--- a/crates/shirabe/tests/util/auth_helper_test.rs
+++ b/crates/shirabe/tests/util/auth_helper_test.rs
@@ -757,16 +757,48 @@ fn test_prompt_auth_if_needed_multiple_bitbucket_downloads() {
}
#[test]
-#[ignore = "exercises the deprecated addAuthenticationHeader wrapper (not ported) which relies on \
-trigger_error/E_USER_DEPRECATED; the PHP error-handler subsystem is not modeled"]
+#[ignore = "addAuthenticationHeader opens with trigger_error(E_USER_DEPRECATED), and \
+shirabe_php_shim::trigger_error is a todo!()"]
fn test_add_authentication_header_with_custom_headers() {
- // TODO(phase-d): exercises AuthHelper::addAuthenticationHeader, a deprecated wrapper
- // around addAuthenticationOptions that PHP implements via
- // trigger_error(E_USER_DEPRECATED). It has not been ported to Rust (no
- // add_authentication_header method exists on AuthHelper) because the PHP
- // error-handler subsystem it relies on is not modeled — same limitation as
- // error_handler_test.rs.
- todo!()
+ let mut f = set_up();
+ let headers = vec![
+ "Accept-Encoding: gzip".to_string(),
+ "Connection: close".to_string(),
+ ];
+ let origin = "example.org";
+ let url = "https://example.org/packages.json";
+ let custom_headers = vec![
+ "API-TOKEN: abc123".to_string(),
+ "X-CUSTOM-HEADER: value".to_string(),
+ ];
+ let headers_json = json_encode(&PhpMixed::List(
+ custom_headers
+ .iter()
+ .map(|h| PhpMixed::String(h.clone()))
+ .collect(),
+ ))
+ .unwrap();
+
+ expects_authentication(&f.io, origin, &headers_json, "custom-headers");
+
+ f.io.borrow_mut()
+ .expects(
+ vec![Expectation::text(
+ "Using custom HTTP headers for authentication",
+ )],
+ true,
+ )
+ .unwrap();
+
+ let mut expected_headers = headers.clone();
+ expected_headers.extend(custom_headers);
+
+ assert_eq!(
+ expected_headers,
+ f.auth_helper
+ .add_authentication_header(headers, origin, url)
+ .unwrap()
+ );
}
#[test]
diff --git a/crates/shirabe/tests/util/error_handler_test.rs b/crates/shirabe/tests/util/error_handler_test.rs
index ed5e6a29..efaf4e11 100644
--- a/crates/shirabe/tests/util/error_handler_test.rs
+++ b/crates/shirabe/tests/util/error_handler_test.rs
@@ -5,16 +5,17 @@
// trigger those by undefined-index access / array_merge misuse. There is no equivalent
// runtime mechanism in Rust to port faithfully.
-// TODO(phase-d): ErrorHandler::register() installs a PHP set_error_handler; no Rust equivalent.
+use shirabe::util::ErrorHandler;
+use shirabe_php_shim::restore_error_handler;
+
#[allow(dead_code)]
fn set_up() {
- todo!()
+ ErrorHandler::register(None);
}
-// TODO(phase-d): restore_error_handler() is PHP runtime machinery; no Rust equivalent.
#[allow(dead_code)]
fn tear_down() {
- todo!()
+ restore_error_handler();
}
#[allow(dead_code)]
diff --git a/crates/shirabe/tests/util/process_executor_test.rs b/crates/shirabe/tests/util/process_executor_test.rs
index 2ce43f3c..268bc548 100644
--- a/crates/shirabe/tests/util/process_executor_test.rs
+++ b/crates/shirabe/tests/util/process_executor_test.rs
@@ -15,7 +15,7 @@ use shirabe_external_packages::symfony::console::output::buffered_output::Buffer
use shirabe_external_packages::symfony::console::output::output_interface::{
OutputInterface, VERBOSITY_DEBUG, VERBOSITY_NORMAL,
};
-use shirabe_php_shim::{PHP_EOL, trim};
+use shirabe_php_shim::{PHP_EOL, ob_get_clean, ob_start, trim};
#[test]
fn test_execute_captures_output() {
@@ -25,16 +25,16 @@ fn test_execute_captures_output() {
assert_eq!(format!("foo{}", PHP_EOL), output);
}
-#[ignore = "requires PHP output buffering (ob_start/ob_get_clean) to capture stdout; no equivalent symbol"]
+#[ignore = "shirabe_php_shim::ob_start/ob_get_clean are todo!(): the shim has no echo-to-buffer routing, and ProcessExecutor::execute with FORWARD_OUTPUT and io=None writes straight to the real process stdout"]
#[test]
fn test_execute_outputs_if_not_captured() {
- // TODO(phase-d): requires PHP output buffering (ob_start/ob_get_clean) to capture
- // stdout; no equivalent symbol. ProcessExecutor::execute with
- // ProcessExecutor::FORWARD_OUTPUT and io=None writes straight to the real process
- // stdout (see output_handler's `print!`), and there is no safe way to capture that
- // from within a parallel cargo test process without redirecting the real stdout file
- // descriptor, which is unsafe under `cargo test`'s default multi-threaded runner.
- todo!()
+ let mut process = ProcessExecutor::new(None);
+ ob_start();
+ process
+ .execute("echo foo", ProcessExecutor::FORWARD_OUTPUT, None)
+ .unwrap();
+ let output = ob_get_clean();
+ assert_eq!(Some(format!("foo{}", PHP_EOL)), output);
}
#[test]
@@ -139,18 +139,17 @@ fn test_doesnt_hide_ports() {
);
}
-#[ignore = "splitLines is called with null in the PHP test, but split_lines accepts only &str (no ?string/Option overload)"]
#[test]
fn test_split_lines() {
- // TODO(phase-d): splitLines is called with null in the PHP test
- // ($process->splitLines(null)), but ProcessExecutor::split_lines here takes `&str`, not
- // `Option<&str>` (PHP's `?string`). Porting this data point faithfully means widening
- // split_lines's signature to Option<&str>, which touches every call site
- // (package/version/version_guesser.rs, util/git.rs,
- // repository/vcs/{hg,fossil,git,svn}_driver.rs — 13 call sites in total, all currently
- // passing `&str`). That is a production API change beyond this test file; flagged for
- // a design decision rather than made unilaterally.
- todo!()
+ let process = ProcessExecutor::new(None);
+ assert!(process.split_lines("").is_empty());
+ // PHP: $process->splitLines(null). `split_lines` takes `&str` where PHP takes `?string`, and
+ // its body opens with `trim((string) $output)`, so the caller performs the null-to-"" cast.
+ assert!(process.split_lines("").is_empty());
+ assert_eq!(vec!["foo"], process.split_lines("foo"));
+ assert_eq!(vec!["foo", "bar"], process.split_lines("foo\nbar"));
+ assert_eq!(vec!["foo", "bar"], process.split_lines("foo\r\nbar"));
+ assert_eq!(vec!["foo", "bar"], process.split_lines("foo\r\nbar\n"));
}
#[test]
@@ -185,14 +184,13 @@ fn test_console_io_does_not_format_symfony_console_style() {
);
}
-#[ignore = "executeAsync returns a Process, not a cancelable promise; no promise/cancel symbol exists"]
+#[ignore = "none of the three symbols this test drives exist: execute_async returns a plain future with no cancel(), and ProcessExecutor has no count_active_jobs or wait (PHP's $jobs/$maxJobs queue is a tokio semaphore here)"]
#[test]
fn test_execute_async_cancel() {
- // TODO(phase-d): PHP's executeAsync returns a React\Promise\PromiseInterface with
- // cancel(); Rust's execute_async returns anyhow::Result<Process> directly (see the
- // comment on ProcessExecutor::execute_async: "no test seam in the external-packages
- // crate"), so there is no promise/cancel symbol to drive this test's
- // `$promise->cancel()` step.
+ // TODO(phase-d): PHP's executeAsync returns a React\Promise\PromiseInterface with cancel(),
+ // and the test reads countActiveJobs() around it and then calls wait(). execute_async here
+ // returns a plain future with no cancel(), and ProcessExecutor has neither count_active_jobs
+ // nor wait: the PHP job queue those methods expose is a tokio semaphore in this port.
todo!()
}