From bed0cd33ca32b95aed9d65891f97649a6f9d0069 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Tue, 18 Aug 2026 06:38:26 +0900 Subject: fix(process-executor): model commands as a CommandLine enum Commands were carried as `PhpMixed`, whose `String`/`List` variants do not tell a shell command line apart from an argv list at the type level. `Perforce::execute_command` and `Git::run_command` therefore funnelled string commands through `execute_args`, spawning `p4 set` or `git command` as a single argument instead of running it through a shell as PHP does; their tests were written against that shape. Introduce `CommandLine::{Shell, Args}` and use it for every `ProcessExecutor` entry point, the mock expectation queue and the `Git::run_command` callables. The unreachable "Invalid command type" branches disappear with it, and the affected tests go back to the string expectations the PHP suite uses. Co-Authored-By: Claude Opus 5 (1M context) --- crates/shirabe/src/command/home_command.rs | 18 +- crates/shirabe/src/downloader/gzip_downloader.rs | 16 +- crates/shirabe/src/downloader/rar_downloader.rs | 16 +- crates/shirabe/src/downloader/xz_downloader.rs | 16 +- crates/shirabe/src/package/locker.rs | 18 +- crates/shirabe/src/platform/hhvm_detector.rs | 23 +- crates/shirabe/src/repository/path_repository.rs | 8 +- crates/shirabe/src/util/bitbucket.rs | 2 +- crates/shirabe/src/util/filesystem.rs | 11 +- crates/shirabe/src/util/git.rs | 41 ++- crates/shirabe/src/util/perforce.rs | 64 ++--- crates/shirabe/src/util/process_executor.rs | 277 ++++++++------------- .../shirabe/tests/common/process_executor_mock.rs | 33 +-- crates/shirabe/tests/util/git_test.rs | 52 ++-- crates/shirabe/tests/util/perforce_test.rs | 22 +- 15 files changed, 234 insertions(+), 383 deletions(-) (limited to 'crates/shirabe') diff --git a/crates/shirabe/src/command/home_command.rs b/crates/shirabe/src/command/home_command.rs index a0cc881d..c147bb68 100644 --- a/crates/shirabe/src/command/home_command.rs +++ b/crates/shirabe/src/command/home_command.rs @@ -13,7 +13,7 @@ use crate::repository::RootPackageRepository; use crate::util::Platform; use crate::util::ProcessExecutor; use shirabe_php_shim::filter_var_url; -use shirabe_php_shim::{PhpMixed, impl_php_class}; +use shirabe_php_shim::impl_php_class; use shirabe_symfony_console::command::Command; use shirabe_symfony_console::input::InputInterface; use shirabe_symfony_console::output::OutputInterface; @@ -79,25 +79,19 @@ impl HomeCommand { fn open_browser(&self, url: &str) { let mut process = ProcessExecutor::new(Some(self.get_io().clone())); if Platform::is_windows() { - let _ = process.execute( - PhpMixed::from(vec!["start", "\"web\"", "explorer", url]), - (), - None, - ); + let _ = process.execute(&["start", "\"web\"", "explorer", url], (), None); return; } let linux = process - .execute(PhpMixed::from(vec!["which", "xdg-open"]), (), None) - .unwrap_or(1); - let osx = process - .execute(PhpMixed::from(vec!["which", "open"]), (), None) + .execute(&["which", "xdg-open"], (), None) .unwrap_or(1); + let osx = process.execute(&["which", "open"], (), None).unwrap_or(1); if linux == 0 { - let _ = process.execute(PhpMixed::from(vec!["xdg-open", url]), (), None); + let _ = process.execute(&["xdg-open", url], (), None); } else if osx == 0 { - let _ = process.execute(PhpMixed::from(vec!["open", url]), (), None); + let _ = process.execute(&["open", url], (), None); } else { self.get_io().write_error(&format!( "No suitable browser opening command found, open yourself: {}", diff --git a/crates/shirabe/src/downloader/gzip_downloader.rs b/crates/shirabe/src/downloader/gzip_downloader.rs index ff229d38..222c0473 100644 --- a/crates/shirabe/src/downloader/gzip_downloader.rs +++ b/crates/shirabe/src/downloader/gzip_downloader.rs @@ -107,16 +107,12 @@ impl ArchiveDownloader for GzipDownloader { ]; let mut process_output = PhpMixed::Null; - if self.inner.process.borrow_mut().execute( - PhpMixed::List( - command - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect(), - ), - &mut process_output, - None, - )? == 0 + if self + .inner + .process + .borrow_mut() + .execute(&command, &mut process_output, None)? + == 0 { return Ok(None); } diff --git a/crates/shirabe/src/downloader/rar_downloader.rs b/crates/shirabe/src/downloader/rar_downloader.rs index f4f601bc..89dc05a1 100644 --- a/crates/shirabe/src/downloader/rar_downloader.rs +++ b/crates/shirabe/src/downloader/rar_downloader.rs @@ -79,16 +79,12 @@ impl ArchiveDownloader for RarDownloader { ]; let mut process_output = PhpMixed::Null; - if self.inner.process.borrow_mut().execute( - PhpMixed::List( - command - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect(), - ), - &mut process_output, - None, - )? == 0 + if self + .inner + .process + .borrow_mut() + .execute(&command, &mut process_output, None)? + == 0 { return Ok(None); } diff --git a/crates/shirabe/src/downloader/xz_downloader.rs b/crates/shirabe/src/downloader/xz_downloader.rs index 49dd9deb..b42510a6 100644 --- a/crates/shirabe/src/downloader/xz_downloader.rs +++ b/crates/shirabe/src/downloader/xz_downloader.rs @@ -66,16 +66,12 @@ impl ArchiveDownloader for XzDownloader { let command = ["tar", "-xJf", file, "-C", path]; let mut ignored_output = PhpMixed::Null; - if self.inner.process.borrow_mut().execute( - PhpMixed::List( - command - .iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect(), - ), - &mut ignored_output, - None, - )? == 0 + if self + .inner + .process + .borrow_mut() + .execute(&command, &mut ignored_output, None)? + == 0 { return Ok(None); } diff --git a/crates/shirabe/src/package/locker.rs b/crates/shirabe/src/package/locker.rs index 17911c6e..36edd29f 100644 --- a/crates/shirabe/src/package/locker.rs +++ b/crates/shirabe/src/package/locker.rs @@ -808,7 +808,7 @@ impl Locker { let command = GitUtil::build_rev_list_command(&self.process, args); let mut output = PhpMixed::Null; if 0 == self.process.borrow_mut().execute( - PhpMixed::List(command.into_iter().map(PhpMixed::String).collect()), + command, &mut output, path.as_deref(), )? { @@ -828,14 +828,14 @@ impl Locker { "hg" => { let mut output = PhpMixed::Null; if 0 == self.process.borrow_mut().execute( - PhpMixed::List(vec![ - PhpMixed::String("hg".to_string()), - PhpMixed::String("log".to_string()), - PhpMixed::String("--template".to_string()), - PhpMixed::String("{date|hgdate}".to_string()), - PhpMixed::String("-r".to_string()), - PhpMixed::String(source_ref), - ]), + vec![ + "hg".to_string(), + "log".to_string(), + "--template".to_string(), + "{date|hgdate}".to_string(), + "-r".to_string(), + source_ref, + ], &mut output, path.as_deref(), )? && let Some(m) = preg_match( diff --git a/crates/shirabe/src/platform/hhvm_detector.rs b/crates/shirabe/src/platform/hhvm_detector.rs index a80b9028..ac2bbf78 100644 --- a/crates/shirabe/src/platform/hhvm_detector.rs +++ b/crates/shirabe/src/platform/hhvm_detector.rs @@ -58,22 +58,17 @@ impl HhvmDetectorInterface for HhvmDetector { std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(None))) }); let mut version_output = shirabe_php_shim::PhpMixed::Null; - let cmd = shirabe_php_shim::PhpMixed::List( - [ - hhvm_path.as_str(), - "--php", - "-d", - "hhvm.jit=0", - "-r", - "echo HHVM_VERSION;", - ] - .into_iter() - .map(|s| shirabe_php_shim::PhpMixed::String(s.to_string())) - .collect(), - ); + let cmd = [ + hhvm_path.as_str(), + "--php", + "-d", + "hhvm.jit=0", + "-r", + "echo HHVM_VERSION;", + ]; let exit_code = executor .borrow_mut() - .execute(cmd, &mut version_output, None) + .execute(&cmd, &mut version_output, None) .unwrap_or(1); if exit_code == 0 { *cache = Some(version_output.as_string().map(|s| s.to_string())); diff --git a/crates/shirabe/src/repository/path_repository.rs b/crates/shirabe/src/repository/path_repository.rs index 05b68382..d619865b 100644 --- a/crates/shirabe/src/repository/path_repository.rs +++ b/crates/shirabe/src/repository/path_repository.rs @@ -256,16 +256,16 @@ impl PathRepository { { let mut ref1 = PhpMixed::Null; let mut ref2 = PhpMixed::Null; - let cmd = PhpMixed::from(vec!["git", "rev-parse", "HEAD"]); + let cmd = ["git", "rev-parse", "HEAD"]; let code1 = self .process .borrow_mut() - .execute(cmd.clone(), &mut ref1, Some(path.as_str())) + .execute(&cmd, &mut ref1, Some(path.as_str())) .unwrap_or(1); let code2 = self .process .borrow_mut() - .execute(cmd, &mut ref2, None) + .execute(&cmd, &mut ref2, None) .unwrap_or(1); if code1 == 0 && code2 == 0 && ref1.as_string() == ref2.as_string() { package.insert( @@ -292,7 +292,7 @@ impl PathRepository { && self .process .borrow_mut() - .execute(PhpMixed::from(command), &mut output, Some(path.as_str())) + .execute(command, &mut output, Some(path.as_str())) .unwrap_or(1) == 0 { diff --git a/crates/shirabe/src/util/bitbucket.rs b/crates/shirabe/src/util/bitbucket.rs index d8e245a9..9a16c9a6 100644 --- a/crates/shirabe/src/util/bitbucket.rs +++ b/crates/shirabe/src/util/bitbucket.rs @@ -81,7 +81,7 @@ impl Bitbucket { .process .borrow_mut() .execute( - PhpMixed::from(vec!["git", "config", "bitbucket.accesstoken"]), + &["git", "config", "bitbucket.accesstoken"], &mut output, None, ) diff --git a/crates/shirabe/src/util/filesystem.rs b/crates/shirabe/src/util/filesystem.rs index 2395d1cd..8726df63 100644 --- a/crates/shirabe/src/util/filesystem.rs +++ b/crates/shirabe/src/util/filesystem.rs @@ -145,11 +145,7 @@ impl Filesystem { let mut output = PhpMixed::Null; let result = self .get_process() - .execute( - PhpMixed::List(cmd.iter().map(|s| PhpMixed::String(s.clone())).collect()), - &mut output, - None, - ) + .execute(&cmd, &mut output, None) .map(|n| n == 0) .unwrap_or(false); @@ -198,10 +194,7 @@ impl Filesystem { (fs.get_process_handle(), cmd) }; - let process_future = process_executor.borrow_mut().execute_async( - PhpMixed::List(cmd.iter().map(|s| PhpMixed::String(s.clone())).collect()), - None, - ); + let process_future = process_executor.borrow_mut().execute_async(&cmd, None); let mut process = process_future.await?; // clear stat cache because external processes aren't tracked by the php stat cache diff --git a/crates/shirabe/src/util/git.rs b/crates/shirabe/src/util/git.rs index 78c1acb5..5577250f 100644 --- a/crates/shirabe/src/util/git.rs +++ b/crates/shirabe/src/util/git.rs @@ -5,6 +5,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::util::Bitbucket; +use crate::util::CommandLine; use crate::util::Filesystem; use crate::util::GitHub; use crate::util::GitLab; @@ -101,10 +102,10 @@ impl Git { initial_clone: bool, command_output: impl RunCommandOutput, ) -> anyhow::Result<()> { - let mut callables: Vec Vec>> = vec![]; + let mut callables: Vec CommandLine>> = vec![]; for cmd in commands { let cmd_clone = cmd.clone(); - callables.push(Box::new(move |url: &str| -> Vec { + callables.push(Box::new(move |url: &str| -> CommandLine { let mut map: IndexMap = IndexMap::new(); map.insert("%url%".to_string(), url.to_string()); map.insert( @@ -112,10 +113,10 @@ impl Git { preg_replace(php_regex!(r"{://([^@]+?):(.+?)@}"), "://", url), ); - array_map( + CommandLine::Args(array_map( |value: &String| map.get(value).cloned().unwrap_or_else(|| value.clone()), &cmd_clone, - ) + )) })); } @@ -127,7 +128,7 @@ impl Git { /// mirroring `Git::runCommand` as exercised by `GitTest`. pub fn __run_command( &mut self, - command_callable: Vec Vec>>, + command_callable: Vec CommandLine>>, url: &str, cwd: Option<&str>, initial_clone: bool, @@ -140,14 +141,14 @@ impl Git { /// if a callable is passed it will be used as output handler fn run_command( &mut self, - command_callable: Vec Vec>>, + command_callable: Vec CommandLine>>, url: &str, cwd: Option<&str>, initial_clone: bool, mut command_output: impl RunCommandOutput, ) -> anyhow::Result<()> { let command_callables = command_callable; - let mut last_command: PhpMixed = PhpMixed::String(String::new()); + let mut last_command = CommandLine::Shell(String::new()); // Ensure we are allowed to use this URL by config self.config.borrow_mut().prohibit_url_by_config( @@ -167,7 +168,7 @@ impl Git { // PHP closure: $runCommands = function ($url) use (...) { ... }; let run_commands_inline = |url_arg: &str, this_process: &mut ProcessExecutor, - last_cmd: &mut PhpMixed, + last_cmd: &mut CommandLine, output: &mut dyn RunCommandOutput| -> i64 { let collect_outputs = output.collect_outputs(); @@ -176,8 +177,7 @@ impl Git { let mut status: i64 = 0; for (counter, callable) in command_callables.iter().enumerate() { let cmd = callable(url_arg); - *last_cmd = - PhpMixed::List(cmd.iter().map(|s| PhpMixed::String(s.clone())).collect()); + *last_cmd = cmd.clone(); let exec_cwd = if initial_clone && counter == 0 { None } else { @@ -185,16 +185,13 @@ impl Git { }; if collect_outputs { let mut local_output = String::new(); - status = - this_process.execute_args(&cmd, &mut local_output, exec_cwd.as_deref()); + status = this_process + .execute(cmd, &mut local_output, exec_cwd.as_deref()) + .unwrap_or(1); outputs.push(local_output); } else { status = this_process - .execute( - &cmd[..], - output.make_handler().unwrap(), - exec_cwd.as_deref(), - ) + .execute(cmd, output.make_handler().unwrap(), exec_cwd.as_deref()) .unwrap_or(1); } if status != 0 { @@ -747,14 +744,8 @@ impl Git { } let mut last_command_str = match &last_command { - PhpMixed::List(l) => { - let parts: Vec = l - .iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect(); - implode(" ", &parts) - } - _ => last_command.as_string().unwrap_or("").to_string(), + CommandLine::Args(args) => implode(" ", args), + CommandLine::Shell(command) => command.clone(), }; if (credentials.len() as i64) > 0 { last_command_str = self.mask_credentials(&last_command_str, &credentials); diff --git a/crates/shirabe/src/util/perforce.rs b/crates/shirabe/src/util/perforce.rs index 32e3e9cd..95cff0ca 100644 --- a/crates/shirabe/src/util/perforce.rs +++ b/crates/shirabe/src/util/perforce.rs @@ -2,6 +2,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; +use crate::util::CommandLine; use crate::util::Filesystem; use crate::util::Platform; use crate::util::ProcessExecutor; @@ -138,28 +139,19 @@ impl Perforce { let task = vec!["client".to_string(), "-d".to_string(), client]; let use_p4_client = false; let command = self.generate_p4_command(task, use_p4_client); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let client_spec = self.get_p4_client_spec(); let file_system = self.get_filesystem(); file_system.borrow_mut().remove(&client_spec); } - fn execute_command(&mut self, command: PhpMixed) -> i64 { + fn execute_command(&mut self, command: CommandLine) -> i64 { self.command_result = String::new(); - let cmd_vec: Vec = match &command { - PhpMixed::List(l) => l - .iter() - .filter_map(|v| v.as_string().map(|s| s.to_string())) - .collect(), - PhpMixed::String(s) => vec![s.clone()], - _ => vec![], - }; self.process .borrow_mut() - .execute_args(&cmd_vec, &mut self.command_result, None) + .execute(command, &mut self.command_result, None) + .unwrap_or(1) } pub fn get_client(&mut self) -> String { @@ -272,7 +264,7 @@ impl Perforce { ProcessExecutor::escape(self.p4_user.as_deref().unwrap_or("")) ) }; - self.execute_command(PhpMixed::String(command)); + self.execute_command(CommandLine::Shell(command)); Ok(()) } @@ -280,7 +272,7 @@ impl Perforce { fn get_p4_variable(&mut self, name: &str) -> Option { if self.windows_flag { let command = format!("{} set", Self::get_p4_executable()); - self.execute_command(PhpMixed::String(command)); + self.execute_command(CommandLine::Shell(command)); let result = trim(&self.command_result, None); let res_array = explode(PHP_EOL, &result); for line in &res_array { @@ -302,7 +294,7 @@ impl Perforce { } let command = format!("echo ${}", name); - self.execute_command(PhpMixed::String(command)); + self.execute_command(CommandLine::Shell(command)); let result = trim(&self.command_result, None); Some(result) @@ -346,9 +338,7 @@ impl Perforce { pub fn is_logged_in(&mut self) -> anyhow::Result { let command = self.generate_p4_command(vec!["login".to_string(), "-s".to_string()], false); - let exit_code = self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + let exit_code = self.execute_command(CommandLine::Args(command)); if exit_code != 0 { let error_output = self.process.borrow().get_error_output().to_string(); let user = self.get_user().unwrap_or_default(); @@ -395,9 +385,7 @@ impl Perforce { if let Some(source_reference) = source_reference { p4_sync_command.push(format!("@{}", source_reference)); } - self.execute_command(PhpMixed::List( - p4_sync_command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(p4_sync_command)); chdir(&prev_dir); Ok(()) @@ -587,9 +575,7 @@ impl Perforce { let path = self.get_file_path(file, identifier)?; let command = self.generate_p4_command(vec!["print".to_string(), path], true); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); if trim(&result, None).is_empty() { @@ -613,9 +599,7 @@ impl Perforce { substr(identifier, idx, None) ); let command = self.generate_p4_command(vec!["files".to_string(), path], false); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); let index2 = strpos(&result, "no such file(s)."); if index2.is_none() { @@ -651,9 +635,7 @@ impl Perforce { ], true, ); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); let res_array = explode(PHP_EOL, &result); for line in &res_array { @@ -673,9 +655,7 @@ impl Perforce { vec!["changes".to_string(), format!("{}/...", stream)], false, ); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); let res_array = explode(PHP_EOL, &result); let last_commit = res_array.first().cloned().unwrap_or_default(); @@ -699,9 +679,7 @@ impl Perforce { pub fn get_tags(&mut self) -> IndexMap { let command = self.generate_p4_command(vec!["labels".to_string()], true); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); let res_array = explode(PHP_EOL, &result); let mut tags: IndexMap = IndexMap::new(); @@ -719,9 +697,7 @@ impl Perforce { pub fn check_stream(&mut self) -> bool { let command = self.generate_p4_command(vec!["depots".to_string()], false); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let result = self.command_result.clone(); let res_array = explode(PHP_EOL, &result); for line in &res_array { @@ -747,9 +723,7 @@ impl Perforce { let label = substr(reference, index as i64, None); let command = self.generate_p4_command(vec!["changes".to_string(), "-m1".to_string(), label], true); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); let changes = self.command_result.clone(); if strpos(&changes, "Change") != Some(0) { return None; @@ -771,9 +745,7 @@ impl Perforce { ], true, ); - self.execute_command(PhpMixed::List( - command.into_iter().map(PhpMixed::String).collect(), - )); + self.execute_command(CommandLine::Args(command)); Some(self.command_result.clone()) } diff --git a/crates/shirabe/src/util/process_executor.rs b/crates/shirabe/src/util/process_executor.rs index 6d609421..c1be0e18 100644 --- a/crates/shirabe/src/util/process_executor.rs +++ b/crates/shirabe/src/util/process_executor.rs @@ -10,10 +10,9 @@ use indexmap::IndexMap; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{ LogicException, PHP_EOL, PhpMixed, PregMatches, RuntimeException, array_intersect, array_map, - escapeshellarg, explode, implode, in_array_strict, is_array, is_dir, is_numeric, is_string, - php_regex, preg_is_match, preg_match, preg_replace, preg_replace_callback, preg_replace2, - preg_split, rtrim, str_replace, strcspn, strlen, strpbrk, strtolower, strtr_array, - substr_replace, trim, + escapeshellarg, explode, implode, in_array_strict, is_dir, is_numeric, php_regex, + preg_is_match, preg_match, preg_replace, preg_replace_callback, preg_replace2, preg_split, + rtrim, str_replace, strcspn, strlen, strpbrk, strtolower, strtr_array, substr_replace, trim, }; use shirabe_symfony_process::ExecutableFinder; use shirabe_symfony_process::Process; @@ -27,6 +26,12 @@ static EXECUTABLES: LazyLock>> = static TIMEOUT: LazyLock> = LazyLock::new(|| Mutex::new(300)); +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CommandLine { + Shell(String), + Args(Vec), +} + #[derive(Debug)] pub struct ProcessExecutor { /// @var bool @@ -63,8 +68,7 @@ pub struct ProcessExecutorMockState { /// A single expected command (`array{cmd, return, stdout, stderr, callback}` in PHP). pub struct MockExpectation { - /// `string|list`: a `PhpMixed::String` or `PhpMixed::List`. - pub cmd: PhpMixed, + pub cmd: CommandLine, pub r#return: i64, pub stdout: String, pub stderr: String, @@ -87,7 +91,7 @@ impl std::fmt::Debug for MockExpectation { impl MockExpectation { /// Builds an expectation from just a command (string or list), defaulting the rest, matching /// PHP's handling of bare `string`/`list` entries in `expects`. - pub fn from_cmd(cmd: PhpMixed) -> Self { + pub fn from_cmd(cmd: CommandLine) -> Self { Self { cmd, r#return: 0, @@ -174,15 +178,11 @@ impl ProcessExecutor { output: &mut String, cwd: Option<&str>, ) -> i64 { - let cmd = PhpMixed::List( - command - .iter() - .map(|s| PhpMixed::String(s.clone())) - .collect(), - ); - let mut buf = PhpMixed::String(String::new()); - let rc = self.execute(cmd, &mut buf, cwd).unwrap_or(1); - *output = buf.as_string().unwrap_or("").to_string(); + let mut buf = String::new(); + let rc = self + .execute(CommandLine::Args(command.to_vec()), &mut buf, cwd) + .unwrap_or(1); + *output = buf; rc } @@ -201,7 +201,7 @@ impl ProcessExecutor { fn run_process<'o, O>( &mut self, - command: PhpMixed, + command: CommandLine, cwd: Option<&str>, env: Option>, tty: bool, @@ -213,48 +213,44 @@ impl ProcessExecutor { // On Windows, we don't rely on the OS to find the executable if possible to avoid lookups // in the current directory which could be untrusted. Instead we use the ExecutableFinder. - let mut process: Process; - if is_string(&command) { - let mut command_str = command.as_string().unwrap_or("").to_string(); - if Platform::is_windows() - && let Some(m) = preg_match(php_regex!(r"{^([^:/\\]++) }"), &command_str) - { - let m1 = m.get(1).unwrap_or_default().to_string(); - command_str = substr_replace( - &command_str, - &Self::escape(&Self::get_executable(&m1)), - 0, - Some(strlen(&m1)), - ); - } + let mut process: Process = match command { + CommandLine::Shell(mut command) => { + if Platform::is_windows() + && let Some(m) = preg_match(php_regex!(r"{^([^:/\\]++) }"), &command) + { + let m1 = m.get(1).unwrap_or_default().to_string(); + command = substr_replace( + &command, + &Self::escape(&Self::get_executable(&m1)), + 0, + Some(strlen(&m1)), + ); + } - process = Process::from_shell_commandline( - &command_str, - cwd, - env, - PhpMixed::Null, - Some(Self::get_timeout() as f64), - )?; - } else if let PhpMixed::List(ref list) = command { - let mut cmd_vec: Vec = list - .iter() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect(); - if Platform::is_windows() && strlen(&cmd_vec[0]) == strcspn(&cmd_vec[0], ":/\\") as i64 - { - cmd_vec[0] = Self::get_executable(&cmd_vec[0]); + Process::from_shell_commandline( + &command, + cwd, + env, + PhpMixed::Null, + Some(Self::get_timeout() as f64), + )? } + CommandLine::Args(mut command) => { + if Platform::is_windows() + && strlen(&command[0]) == strcspn(&command[0], ":/\\") as i64 + { + command[0] = Self::get_executable(&command[0]); + } - process = Process::new( - cmd_vec, - cwd.map(String::from), - env, - PhpMixed::Null, - Some(Self::get_timeout() as f64), - )?; - } else { - return Err(LogicException::new("Invalid command type".to_string()).into()); - } + Process::new( + command, + cwd.map(String::from), + env, + PhpMixed::Null, + Some(Self::get_timeout() as f64), + )? + } + }; if !Platform::is_windows() && tty { // PHP: try { $process->setTty(true); } catch (RuntimeException $e) { /* ignore */ } @@ -324,7 +320,7 @@ impl ProcessExecutor { fn do_execute<'o, O>( &mut self, - command: PhpMixed, + command: CommandLine, cwd: Option<&str>, tty: bool, output: O, @@ -349,21 +345,21 @@ impl ProcessExecutor { { let is_bare_repository = !is_dir(format!("{}/.git", rtrim(cwd, Some("/")))); if is_bare_repository { - let mut config_value = PhpMixed::String(String::new()); + let mut config_value = String::new(); let mut git_env: IndexMap = IndexMap::new(); git_env.insert("GIT_DIR".to_string(), cwd.to_string()); self.run_process( - PhpMixed::List(vec![ - PhpMixed::String("git".to_string()), - PhpMixed::String("config".to_string()), - PhpMixed::String("safe.bareRepository".to_string()), + CommandLine::Args(vec![ + "git".to_string(), + "config".to_string(), + "safe.bareRepository".to_string(), ]), Some(cwd), Some(git_env.clone()), tty, &mut config_value, )?; - let trimmed = trim(config_value.as_string().unwrap_or(""), None); + let trimmed = trim(&config_value, None); if trimmed == "explicit" { env = Some(git_env); } @@ -386,27 +382,12 @@ impl ProcessExecutor { /// `execute_async`'s `&self` receiver can still consume it. fn mock_match( &self, - command: &PhpMixed, + command: &CommandLine, cwd: Option<&str>, ) -> anyhow::Result<(String, String, i64)> { - let command_string = if is_array(command) { - match command { - PhpMixed::List(l) => implode( - " ", - &l.iter() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect::>(), - ), - PhpMixed::Array(m) => implode( - " ", - &m.values() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect::>(), - ), - _ => String::new(), - } - } else { - command.as_string().unwrap_or("").to_string() + let command_string = match command { + CommandLine::Args(args) => implode(" ", args), + CommandLine::Shell(command) => command.clone(), }; let mut mock = self.mock.as_ref().unwrap().borrow_mut(); @@ -473,7 +454,7 @@ impl ProcessExecutor { /// emits stdout/stderr through the output target and records `error_output`. fn mock_do_execute<'o, O>( &mut self, - command: PhpMixed, + command: CommandLine, cwd: Option<&str>, output: O, ) -> anyhow::Result @@ -546,26 +527,9 @@ impl ProcessExecutor { if !expectations.is_empty() { let remaining: Vec = expectations .iter() - .map(|expect| { - if is_array(&expect.cmd) { - match &expect.cmd { - PhpMixed::List(l) => implode( - " ", - &l.iter() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect::>(), - ), - PhpMixed::Array(m) => implode( - " ", - &m.values() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect::>(), - ), - _ => String::new(), - } - } else { - expect.cmd.as_string().unwrap_or("").to_string() - } + .map(|expect| match &expect.cmd { + CommandLine::Args(args) => implode(" ", args), + CommandLine::Shell(command) => command.clone(), }) .collect(); panic!( @@ -648,26 +612,21 @@ impl ProcessExecutor { Self::output_command_run_with(&io, &command, cwd.as_deref(), true); // PHP: $job['reject']($e) on process construction/start failure — surfaced as Err here. - let mut process = if is_string(&command) { - Process::from_shell_commandline( - command.as_string().unwrap_or(""), + let mut process = match command { + CommandLine::Shell(command) => Process::from_shell_commandline( + &command, cwd.as_deref(), None, PhpMixed::Null, Some(Self::get_timeout() as f64), - )? - } else if let PhpMixed::List(ref list) = command { - Process::new( - list.iter() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect(), + )?, + CommandLine::Args(command) => Process::new( + command, cwd.clone(), None, PhpMixed::Null, Some(Self::get_timeout() as f64), - )? - } else { - return Err(LogicException::new("Invalid command type".to_string()).into()); + )?, }; process.start(None, IndexMap::new())?; @@ -802,7 +761,7 @@ impl ProcessExecutor { Self::escape_argument(argument) } - fn output_command_run(&self, command: &PhpMixed, cwd: Option<&str>, r#async: bool) { + fn output_command_run(&self, command: &CommandLine, cwd: Option<&str>, r#async: bool) { Self::output_command_run_with(&self.io, command, cwd, r#async); } @@ -810,7 +769,7 @@ impl ProcessExecutor { /// clone of the io handle instead of borrowing the executor. fn output_command_run_with( io: &Option>>, - command: &PhpMixed, + command: &CommandLine, cwd: Option<&str>, r#async: bool, ) { @@ -818,16 +777,9 @@ impl ProcessExecutor { return; } - let command_string = if is_string(command) { - command.as_string().unwrap_or("").to_string() - } else if let PhpMixed::List(list) = command { - let parts: Vec = array_map( - |v| Self::escape(v.as_string().unwrap_or("")), - &list.to_vec(), - ); - implode(" ", &parts) - } else { - String::new() + let command_string = match command { + CommandLine::Shell(command) => command.clone(), + CommandLine::Args(args) => implode(" ", &array_map(|arg| Self::escape(arg), args)), }; let safe_command = preg_replace_callback( php_regex!(r"{://(?P[^:/\s]+):(?P[^@\s/]+)@}i"), @@ -920,21 +872,10 @@ impl ProcessExecutor { argument } - pub fn requires_git_dir_env(&self, command: &PhpMixed) -> bool { - let cmd: Vec = if !is_array(command) { - explode(" ", command.as_string().unwrap_or("")) - } else { - match command { - PhpMixed::List(l) => l - .iter() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect(), - PhpMixed::Array(m) => m - .values() - .map(|v| v.as_string().unwrap_or("").to_string()) - .collect(), - _ => vec![], - } + pub fn requires_git_dir_env(&self, command: &CommandLine) -> bool { + let cmd: Vec = match command { + CommandLine::Shell(command) => explode(" ", command), + CommandLine::Args(args) => args.clone(), }; if cmd.first().map(|s| s.as_str()) != Some("git") { return false; @@ -978,76 +919,68 @@ impl ProcessExecutor { } } -/// Helper trait: convert various command argument forms into `PhpMixed`. +/// Helper trait: convert various command argument forms into a [`CommandLine`]. pub trait IntoExecCommand { - fn into_exec_command(self) -> PhpMixed; + fn into_exec_command(self) -> CommandLine; } -impl IntoExecCommand for PhpMixed { - fn into_exec_command(self) -> PhpMixed { +impl IntoExecCommand for CommandLine { + fn into_exec_command(self) -> CommandLine { self } } -impl IntoExecCommand for &PhpMixed { - fn into_exec_command(self) -> PhpMixed { +impl IntoExecCommand for &CommandLine { + fn into_exec_command(self) -> CommandLine { self.clone() } } impl IntoExecCommand for &str { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::String(self.to_string()) + fn into_exec_command(self) -> CommandLine { + CommandLine::Shell(self.to_string()) } } impl IntoExecCommand for String { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::String(self) + fn into_exec_command(self) -> CommandLine { + CommandLine::Shell(self) } } impl IntoExecCommand for &String { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::String(self.clone()) + fn into_exec_command(self) -> CommandLine { + CommandLine::Shell(self.clone()) } } impl IntoExecCommand for Vec { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::List(self.into_iter().map(PhpMixed::String).collect()) + fn into_exec_command(self) -> CommandLine { + CommandLine::Args(self) } } impl IntoExecCommand for &Vec { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::List(self.iter().map(|s| PhpMixed::String(s.clone())).collect()) + fn into_exec_command(self) -> CommandLine { + CommandLine::Args(self.clone()) } } impl IntoExecCommand for &[&str; N] { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::List( - self.iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect(), - ) + fn into_exec_command(self) -> CommandLine { + CommandLine::Args(self.iter().map(|s| s.to_string()).collect()) } } impl IntoExecCommand for &[&str] { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::List( - self.iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect(), - ) + fn into_exec_command(self) -> CommandLine { + CommandLine::Args(self.iter().map(|s| s.to_string()).collect()) } } impl IntoExecCommand for &[String] { - fn into_exec_command(self) -> PhpMixed { - PhpMixed::List(self.iter().map(|s| PhpMixed::String(s.clone())).collect()) + fn into_exec_command(self) -> CommandLine { + CommandLine::Args(self.to_vec()) } } diff --git a/crates/shirabe/tests/common/process_executor_mock.rs b/crates/shirabe/tests/common/process_executor_mock.rs index 33d9dfba..eb39b9c6 100644 --- a/crates/shirabe/tests/common/process_executor_mock.rs +++ b/crates/shirabe/tests/common/process_executor_mock.rs @@ -1,7 +1,6 @@ //! ref: composer/tests/Composer/Test/Mock/ProcessExecutorMock.php -use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor}; -use shirabe_php_shim::PhpMixed; +use shirabe::util::process_executor::{CommandLine, MockExpectation, MockHandler, ProcessExecutor}; // A command expectation as written in the PHP tests: either a bare command // (`'git command'` / `['git', '--version']`) or the full @@ -29,44 +28,36 @@ pub fn cmd_full( // string args. Comparison against the executed command is exact (PHP `===`), so // the form here must match the form the code under test passes to `execute`. pub trait IntoMockCmd { - fn into_mock_cmd(self) -> PhpMixed; + fn into_mock_cmd(self) -> CommandLine; } impl IntoMockCmd for &str { - fn into_mock_cmd(self) -> PhpMixed { - PhpMixed::String(self.to_string()) + fn into_mock_cmd(self) -> CommandLine { + CommandLine::Shell(self.to_string()) } } impl IntoMockCmd for String { - fn into_mock_cmd(self) -> PhpMixed { - PhpMixed::String(self) + fn into_mock_cmd(self) -> CommandLine { + CommandLine::Shell(self) } } impl IntoMockCmd for Vec<&str> { - fn into_mock_cmd(self) -> PhpMixed { - PhpMixed::List( - self.into_iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect(), - ) + fn into_mock_cmd(self) -> CommandLine { + CommandLine::Args(self.into_iter().map(|s| s.to_string()).collect()) } } impl IntoMockCmd for Vec { - fn into_mock_cmd(self) -> PhpMixed { - PhpMixed::List(self.into_iter().map(PhpMixed::String).collect()) + fn into_mock_cmd(self) -> CommandLine { + CommandLine::Args(self) } } impl IntoMockCmd for [&str; N] { - fn into_mock_cmd(self) -> PhpMixed { - PhpMixed::List( - self.iter() - .map(|s| PhpMixed::String(s.to_string())) - .collect(), - ) + fn into_mock_cmd(self) -> CommandLine { + CommandLine::Args(self.iter().map(|s| s.to_string()).collect()) } } diff --git a/crates/shirabe/tests/util/git_test.rs b/crates/shirabe/tests/util/git_test.rs index dbc06137..3d447553 100644 --- a/crates/shirabe/tests/util/git_test.rs +++ b/crates/shirabe/tests/util/git_test.rs @@ -10,7 +10,7 @@ use shirabe::io::IOInterface; use shirabe::util::filesystem::Filesystem; use shirabe::util::git::Git; use shirabe::util::http_downloader::HttpDownloaderMockHandler; -use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor}; +use shirabe::util::process_executor::{CommandLine, MockExpectation, MockHandler, ProcessExecutor}; use shirabe_php_shim::Catch as _; use shirabe_php_shim::{PhpMixed, RuntimeException}; @@ -118,15 +118,15 @@ fn mock_sync_mirror_config() -> Config { #[test] fn test_run_command_public_git_hub_repository_not_initial_clone_ssh() { let expected_url = "git@github.com:acme/repo"; - let command_callable: Box Vec> = Box::new(move |url: &str| { + let command_callable: Box CommandLine> = Box::new(move |url: &str| { assert_eq!(expected_url, url); - vec!["git command".to_string()] + CommandLine::Shell("git command".to_string()) }); let config = mock_config("ssh"); let (process, _guard) = - get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default()); + get_process_executor_mock(vec![cmd("git command")], true, MockHandler::default()); let mut git = build_git(IOStub::new(), config, process); @@ -144,15 +144,15 @@ fn test_run_command_public_git_hub_repository_not_initial_clone_ssh() { #[test] fn test_run_command_public_git_hub_repository_not_initial_clone_https() { let expected_url = "https://github.com/acme/repo"; - let command_callable: Box Vec> = Box::new(move |url: &str| { + let command_callable: Box CommandLine> = Box::new(move |url: &str| { assert_eq!(expected_url, url); - vec!["git command".to_string()] + CommandLine::Shell("git command".to_string()) }); let config = mock_config("https"); let (process, _guard) = - get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default()); + get_process_executor_mock(vec![cmd("git command")], true, MockHandler::default()); let mut git = build_git(IOStub::new(), config, process); @@ -169,16 +169,16 @@ fn test_run_command_public_git_hub_repository_not_initial_clone_https() { #[test] fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_without_authentication() { - let command_callable: Box Vec> = Box::new(|url: &str| { + let command_callable: Box CommandLine> = Box::new(|url: &str| { assert_eq!("https://github.com/acme/repo", url); - vec!["git command".to_string()] + CommandLine::Shell("git command".to_string()) }); let config = mock_config("https"); let (process, _guard) = get_process_executor_mock( vec![ - cmd_full(vec!["git command"], 1, "", ""), + cmd_full("git command", 1, "", ""), cmd_full(vec!["git", "--version"], 0, "", ""), ], true, @@ -208,20 +208,20 @@ fn run_command_private_github_with_authentication( expected_failures_before_success: usize, ) { let expected_url_owned = expected_url.to_string(); - let command_callable: Box Vec> = Box::new(move |url: &str| { + let command_callable: Box CommandLine> = Box::new(move |url: &str| { if url != expected_url_owned { - return vec!["git command failing".to_string()]; + return CommandLine::Shell("git command failing".to_string()); } - vec!["git command ok".to_string()] + CommandLine::Shell("git command ok".to_string()) }); let config = mock_config(protocol); let mut expected_calls: Vec = Vec::new(); for _ in 0..expected_failures_before_success { - expected_calls.push(cmd_full(vec!["git command failing"], 1, "", "")); + expected_calls.push(cmd_full("git command failing", 1, "", "")); } - expected_calls.push(cmd_full(vec!["git command ok"], 0, "", "")); + expected_calls.push(cmd_full("git command ok", 0, "", "")); let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default()); @@ -273,11 +273,11 @@ fn run_command_private_bitbucket_with_authentication( bitbucket_git_auth_calls: usize, ) { let expected_url_owned = expected_url.to_string(); - let command_callable: Box Vec> = Box::new(move |url: &str| { + let command_callable: Box CommandLine> = Box::new(move |url: &str| { if url != expected_url_owned { - return vec!["git command failing".to_string()]; + return CommandLine::Shell("git command failing".to_string()); } - vec!["git command ok".to_string()] + CommandLine::Shell("git command ok".to_string()) }); let config = ConfigStubBuilder::new() @@ -293,7 +293,7 @@ fn run_command_private_bitbucket_with_authentication( let mut expected_calls: Vec = Vec::new(); for _ in 0..expected_failures_before_success { - expected_calls.push(cmd_full(vec!["git command failing"], 1, "", "")); + expected_calls.push(cmd_full("git command failing", 1, "", "")); } if bitbucket_git_auth_calls > 0 { for _ in 0..bitbucket_git_auth_calls { @@ -305,7 +305,7 @@ fn run_command_private_bitbucket_with_authentication( )); } } - expected_calls.push(cmd_full(vec!["git command ok"], 0, "", "")); + expected_calls.push(cmd_full("git command ok", 0, "", "")); let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default()); @@ -416,11 +416,11 @@ fn run_command_private_bitbucket_interactive_with_oauth( initial_config: Option<(&str, &str)>, ) { let expected_url_owned = expected_url.to_string(); - let command_callable: Box Vec> = Box::new(move |url: &str| { + let command_callable: Box CommandLine> = Box::new(move |url: &str| { if url != expected_url_owned { - return vec!["git command failing".to_string()]; + return CommandLine::Shell("git command failing".to_string()); } - vec!["git command ok".to_string()] + CommandLine::Shell("git command ok".to_string()) }); let mut config = ConfigStubBuilder::new() @@ -437,9 +437,9 @@ fn run_command_private_bitbucket_interactive_with_oauth( config.set_auth_config_source(Box::new(NullConfigSource)); let mut expected_calls: Vec = Vec::new(); - expected_calls.push(cmd_full(vec!["git command failing"], 1, "", "")); + expected_calls.push(cmd_full("git command failing", 1, "", "")); if initial_config.is_some() { - expected_calls.push(cmd_full(vec!["git command failing"], 1, "", "")); + expected_calls.push(cmd_full("git command failing", 1, "", "")); } else { expected_calls.push(cmd_full( vec!["git", "config", "bitbucket.accesstoken"], @@ -448,7 +448,7 @@ fn run_command_private_bitbucket_interactive_with_oauth( "", )); } - expected_calls.push(cmd_full(vec!["git command ok"], 0, "", "")); + expected_calls.push(cmd_full("git command ok", 0, "", "")); let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default()); diff --git a/crates/shirabe/tests/util/perforce_test.rs b/crates/shirabe/tests/util/perforce_test.rs index 293e56d5..dfeae457 100644 --- a/crates/shirabe/tests/util/perforce_test.rs +++ b/crates/shirabe/tests/util/perforce_test.rs @@ -192,7 +192,7 @@ fn test_query_p4_user_with_user_already_set() { fn test_query_p4_user_with_user_set_in_p4_variables_with_windows_os() { let (process, _guard) = get_process_executor_mock( vec![cmd_full( - vec!["p4 set"], + "p4 set", 0, format!("P4USER=TEST_P4VARIABLE_USER{}", shirabe_php_shim::PHP_EOL), "", @@ -216,7 +216,7 @@ fn test_query_p4_user_with_user_set_in_p4_variables_with_windows_os() { fn test_query_p4_user_with_user_set_in_p4_variables_not_windows_os() { let (process, _guard) = get_process_executor_mock( vec![cmd_full( - vec!["echo $P4USER"], + "echo $P4USER", 0, format!("TEST_P4VARIABLE_USER{}", shirabe_php_shim::PHP_EOL), "", @@ -259,7 +259,7 @@ fn test_query_p4_user_stores_response_to_query_for_user_with_windows() { ProcessExecutor::escape("TEST_QUERY_USER") ); let (process, _guard) = get_process_executor_mock( - vec![cmd(vec!["p4 set"]), cmd(vec![expected_command.as_str()])], + vec![cmd("p4 set"), cmd(expected_command.as_str())], true, MockHandler::default(), ); @@ -280,10 +280,7 @@ fn test_query_p4_user_stores_response_to_query_for_user_without_windows() { ProcessExecutor::escape("TEST_QUERY_USER") ); let (process, _guard) = get_process_executor_mock( - vec![ - cmd(vec!["echo $P4USER"]), - cmd(vec![expected_command.as_str()]), - ], + vec![cmd("echo $P4USER"), cmd(expected_command.as_str())], true, MockHandler::default(), ); @@ -304,7 +301,7 @@ fn test_query_p4_user_escapes_injection_on_windows() { ProcessExecutor::escape("foo && calc.exe") ); let (process, _guard) = get_process_executor_mock( - vec![cmd(vec!["p4 set"]), cmd(vec![expected_command.as_str()])], + vec![cmd("p4 set"), cmd(expected_command.as_str())], true, MockHandler::default(), ); @@ -322,10 +319,7 @@ fn test_query_p4_user_escapes_injection_on_windows() { fn test_query_p4_user_escapes_injection_on_unix() { let expected_command = format!("export P4USER={}", ProcessExecutor::escape("foo; id")); let (process, _guard) = get_process_executor_mock( - vec![ - cmd(vec!["echo $P4USER"]), - cmd(vec![expected_command.as_str()]), - ], + vec![cmd("echo $P4USER"), cmd(expected_command.as_str())], true, MockHandler::default(), ); @@ -368,7 +362,7 @@ fn test_query_p4_password_with_password_already_set() { fn test_query_p4_password_with_password_set_in_p4_variables_with_windows_os() { let (process, _guard) = get_process_executor_mock( vec![cmd_full( - vec!["p4 set"], + "p4 set", 0, format!( "P4PASSWD=TEST_P4VARIABLE_PASSWORD{}", @@ -391,7 +385,7 @@ fn test_query_p4_password_with_password_set_in_p4_variables_with_windows_os() { fn test_query_p4_password_with_password_set_in_p4_variables_not_windows_os() { let (process, _guard) = get_process_executor_mock( vec![cmd_full( - vec!["echo $P4PASSWD"], + "echo $P4PASSWD", 0, format!("TEST_P4VARIABLE_PASSWORD{}", shirabe_php_shim::PHP_EOL), "", -- cgit v1.3.1-4-g156e