aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-06-25 15:12:06 +0900
committernsfisis <nsfisis@gmail.com>2026-06-26 00:20:05 +0900
commit8117e5450693d19726da877bce8aacf5c7aa53af (patch)
tree4b06e6f6924599f3402d29cdcdeb0f8a88354d89 /crates
parent8dd3d67884a204ca40e3364206868fea77a312be (diff)
downloadphp-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.tar.gz
php-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.tar.zst
php-shirabe-8117e5450693d19726da877bce8aacf5c7aa53af.zip
test: port 44 vcs/downloader/version tests using mock infra
Port git, version_guesser, gitlab_driver, github_driver, and git_downloader tests using the ProcessExecutor/HttpDownloader mocks and IO/Config stubs. Fix production regex-porting bugs surfaced by the now-reachable paths: Url::sanitize and Response::find_header_value had non-delimited PCRE patterns; implement array_search_mixed non-strict branch and a datetime format mapping. Add HttpDownloader::__new_mock so mocked downloaders skip curl construction. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-shim/src/array.rs17
-rw-r--r--crates/shirabe-php-shim/src/datetime.rs1
-rw-r--r--crates/shirabe/src/repository/vcs/github_driver.rs12
-rw-r--r--crates/shirabe/src/repository/vcs/gitlab_driver.rs12
-rw-r--r--crates/shirabe/src/util/git.rs29
-rw-r--r--crates/shirabe/src/util/http/response.rs2
-rw-r--r--crates/shirabe/src/util/http_downloader.rs24
-rw-r--r--crates/shirabe/src/util/url.rs4
-rw-r--r--crates/shirabe/tests/common/http_downloader_mock.rs7
-rw-r--r--crates/shirabe/tests/downloader/git_downloader_test.rs565
-rw-r--r--crates/shirabe/tests/downloader/main.rs4
-rw-r--r--crates/shirabe/tests/package/main.rs6
-rw-r--r--crates/shirabe/tests/package/version/version_guesser_test.rs571
-rw-r--r--crates/shirabe/tests/repository/main.rs2
-rw-r--r--crates/shirabe/tests/repository/vcs/github_driver_test.rs803
-rw-r--r--crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs780
-rw-r--r--crates/shirabe/tests/util/git_test.rs478
-rw-r--r--crates/shirabe/tests/util/url_test.rs1
18 files changed, 3126 insertions, 192 deletions
diff --git a/crates/shirabe-php-shim/src/array.rs b/crates/shirabe-php-shim/src/array.rs
index 280ec64..ace6a72 100644
--- a/crates/shirabe-php-shim/src/array.rs
+++ b/crates/shirabe-php-shim/src/array.rs
@@ -233,20 +233,21 @@ pub fn array_search_mixed(
haystack: &PhpMixed,
strict: bool,
) -> Option<PhpMixed> {
- if !strict {
- // TODO(phase-c): non-strict array_search needs PHP's loose `==` comparison
- // semantics. Only the strict path is implemented; loose comparison is
- // deferred rather than approximated.
- todo!("non-strict array_search (PHP loose comparison)");
- }
+ let matches = |value: &PhpMixed| -> bool {
+ if strict {
+ value == needle
+ } else {
+ loose_eq(value, needle)
+ }
+ };
match haystack {
PhpMixed::List(items) => items
.iter()
- .position(|value| value == needle)
+ .position(matches)
.map(|i| PhpMixed::Int(i as i64)),
PhpMixed::Array(map) => map
.iter()
- .find(|(_, value)| *value == needle)
+ .find(|(_, value)| matches(value))
.map(|(key, _)| php_key_to_mixed(key)),
_ => None,
}
diff --git a/crates/shirabe-php-shim/src/datetime.rs b/crates/shirabe-php-shim/src/datetime.rs
index bddf5da..e41028e 100644
--- a/crates/shirabe-php-shim/src/datetime.rs
+++ b/crates/shirabe-php-shim/src/datetime.rs
@@ -19,6 +19,7 @@ pub const DATE_ATOM: &str = DATE_RFC3339;
pub fn date_format_to_strftime(format: &str) -> &'static str {
match format {
"Y-m-d H:i:s" => "%Y-%m-%d %H:%M:%S",
+ "Y-m-d Hi" => "%Y-%m-%d %H%M",
"Y-m-d" => "%Y-%m-%d",
"Ymd" => "%Y%m%d",
other => panic!("Unsupported PHP date format: {other:?}"),
diff --git a/crates/shirabe/src/repository/vcs/github_driver.rs b/crates/shirabe/src/repository/vcs/github_driver.rs
index bea7294..7c50a7e 100644
--- a/crates/shirabe/src/repository/vcs/github_driver.rs
+++ b/crates/shirabe/src/repository/vcs/github_driver.rs
@@ -1298,6 +1298,18 @@ impl GitHubDriver {
None
}
+
+ /// For testing only. Mirrors the `setAttribute($driver, 'tags', ...)` reflection
+ /// helper used by GitHubDriverTest to seed private state.
+ pub fn __set_tags(&mut self, tags: Option<IndexMap<String, String>>) {
+ self.tags = tags;
+ }
+
+ /// For testing only. Mirrors the `setAttribute($driver, 'branches', ...)` reflection
+ /// helper used by GitHubDriverTest to seed private state.
+ pub fn __set_branches(&mut self, branches: Option<IndexMap<String, String>>) {
+ self.branches = branches;
+ }
}
impl crate::repository::vcs::VcsDriverInterface for GitHubDriver {
diff --git a/crates/shirabe/src/repository/vcs/gitlab_driver.rs b/crates/shirabe/src/repository/vcs/gitlab_driver.rs
index f1f361e..7e8073d 100644
--- a/crates/shirabe/src/repository/vcs/gitlab_driver.rs
+++ b/crates/shirabe/src/repository/vcs/gitlab_driver.rs
@@ -238,6 +238,18 @@ impl GitLabDriver {
Ok(())
}
+ /// For testing only. Mirrors the `setAttribute($driver, 'branches', ...)` reflection
+ /// helper used by GitLabDriverTest to seed private state.
+ pub fn __set_branches(&mut self, branches: Option<IndexMap<String, String>>) {
+ self.branches = branches;
+ }
+
+ /// For testing only. Mirrors the `setAttribute($driver, 'tags', ...)` reflection
+ /// helper used by GitLabDriverTest to seed private state.
+ pub fn __set_tags(&mut self, tags: Option<IndexMap<String, String>>) {
+ self.tags = tags;
+ }
+
/// Updates the HttpDownloader instance.
/// Mainly useful for tests.
///
diff --git a/crates/shirabe/src/util/git.rs b/crates/shirabe/src/util/git.rs
index f031157..142d5e2 100644
--- a/crates/shirabe/src/util/git.rs
+++ b/crates/shirabe/src/util/git.rs
@@ -132,6 +132,19 @@ impl Git {
self.run_command(callables, url, cwd, initial_clone, command_output)
}
+ /// For testing only. Public seam over the (deprecated) private `run_command`,
+ /// mirroring `Git::runCommand` as exercised by `GitTest`.
+ pub fn __run_command(
+ &mut self,
+ command_callable: Vec<Box<dyn Fn(&str) -> Vec<String>>>,
+ url: &str,
+ cwd: Option<&str>,
+ initial_clone: bool,
+ command_output: Option<&mut PhpMixed>,
+ ) -> Result<()> {
+ self.run_command(command_callable, url, cwd, initial_clone, command_output)
+ }
+
/// @param callable|array<callable> $commandCallable
/// @param mixed $commandOutput the output will be written into this var if passed by ref
/// if a callable is passed it will be used as output handler
@@ -1310,6 +1323,22 @@ impl Git {
version.clone().unwrap_or(None)
}
+ /// For testing only. Resets the cached git `version` static so the next
+ /// `get_version` call re-runs `git --version`, mirroring the
+ /// `ReflectionProperty(GitUtil::class, 'version')->setValue(null, false)`
+ /// done in VersionGuesserTest's setUp/tearDown.
+ pub fn __reset_version() {
+ *VERSION.lock().unwrap() = None;
+ }
+
+ /// For testing only. Seeds the cached git `version` static, mirroring the
+ /// `ReflectionProperty(GitUtil::class, 'version')->setValue(null, $version)`
+ /// done in GitDownloaderTest's `initGitVersion`. `Some(v)` records a detected
+ /// version; `None` records that git is unavailable, both without shelling out.
+ pub fn __set_version(version: Option<String>) {
+ *VERSION.lock().unwrap() = Some(version);
+ }
+
/// @param string[] $credentials
fn mask_credentials(&self, error: &str, credentials: &[String]) -> String {
let mut masked_credentials: Vec<String> = vec![];
diff --git a/crates/shirabe/src/util/http/response.rs b/crates/shirabe/src/util/http/response.rs
index e1da15d..8b7bd34 100644
--- a/crates/shirabe/src/util/http/response.rs
+++ b/crates/shirabe/src/util/http/response.rs
@@ -63,7 +63,7 @@ impl Response {
pub fn find_header_value(headers: &[String], name: &str) -> Option<String> {
let mut value = None;
- let pattern = format!("(?i)^{}:\\s*(.+?)\\s*$", preg_quote(name, None));
+ let pattern = format!("{{^{}:\\s*(.+?)\\s*$}}i", preg_quote(name, None));
for header in headers {
let mut matches: indexmap::IndexMap<
shirabe_external_packages::composer::pcre::CaptureKey,
diff --git a/crates/shirabe/src/util/http_downloader.rs b/crates/shirabe/src/util/http_downloader.rs
index edc84cc..782f04a 100644
--- a/crates/shirabe/src/util/http_downloader.rs
+++ b/crates/shirabe/src/util/http_downloader.rs
@@ -883,6 +883,30 @@ impl HttpDownloader {
&& function_exists("curl_multi_init")
}
+ /// For testing only. Builds an HttpDownloader whose request methods are fully
+ /// short-circuited by the mock (see [`HttpDownloader::__expects`]), without
+ /// constructing the real curl/rfs backends. Mirrors HttpDownloaderMock, which
+ /// extends HttpDownloader but never performs curl I/O.
+ pub fn __new_mock(
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ config: std::rc::Rc<std::cell::RefCell<Config>>,
+ ) -> Self {
+ Self {
+ io,
+ config,
+ jobs: IndexMap::new(),
+ options: IndexMap::new(),
+ running_jobs: 0,
+ max_jobs: 12,
+ curl: None,
+ rfs: None,
+ id_gen: 0,
+ disabled: false,
+ allow_async: false,
+ mock: None,
+ }
+ }
+
/// For testing only. Mirrors HttpDownloaderMock::expects: installs the expectation queue,
/// strict flag and default handler used by the mock request path.
pub fn __expects(
diff --git a/crates/shirabe/src/util/url.rs b/crates/shirabe/src/util/url.rs
index 534e318..5820539 100644
--- a/crates/shirabe/src/util/url.rs
+++ b/crates/shirabe/src/util/url.rs
@@ -164,10 +164,10 @@ impl Url {
pub fn sanitize(url: String) -> String {
// GitHub repository rename result in redirect locations containing the access_token as GET parameter
// e.g. https://api.github.com/repositories/9999999999?access_token=github_token
- let url = Preg::replace(r"([&?]access_token=)[^&]+", "$1***", &url);
+ let url = Preg::replace(r"{([&?]access_token=)[^&]+}", "$1***", &url);
Preg::replace_callback(
- r"(?i)^(?P<prefix>[a-z0-9]+://)?(?P<user>[^:/\s@]+):(?P<password>[^@\s/]+)@",
+ r"{^(?P<prefix>[a-z0-9]+://)?(?P<user>[^:/\s@]+):(?P<password>[^@\s/]+)@}i",
|m| {
let user = m
.get(&CaptureKey::ByName("user".to_string()))
diff --git a/crates/shirabe/tests/common/http_downloader_mock.rs b/crates/shirabe/tests/common/http_downloader_mock.rs
index 249585c..7a021f9 100644
--- a/crates/shirabe/tests/common/http_downloader_mock.rs
+++ b/crates/shirabe/tests/common/http_downloader_mock.rs
@@ -64,12 +64,7 @@ pub fn get_http_downloader_mock(
) -> (Rc<RefCell<HttpDownloader>>, HttpDownloaderMockGuard) {
let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
let config = Rc::new(RefCell::new(Config::new(false, None)));
- let downloader = Rc::new(RefCell::new(HttpDownloader::new(
- io,
- config,
- IndexMap::new(),
- false,
- )));
+ let downloader = Rc::new(RefCell::new(HttpDownloader::__new_mock(io, config)));
downloader
.borrow_mut()
.__expects(expectations, strict, default_handler);
diff --git a/crates/shirabe/tests/downloader/git_downloader_test.rs b/crates/shirabe/tests/downloader/git_downloader_test.rs
index 69b0a6c..07dec08 100644
--- a/crates/shirabe/tests/downloader/git_downloader_test.rs
+++ b/crates/shirabe/tests/downloader/git_downloader_test.rs
@@ -1,18 +1,40 @@
//! ref: composer/tests/Composer/Test/Downloader/GitDownloaderTest.php
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use indexmap::IndexMap;
+use serial_test::serial;
+use shirabe::config::Config;
+use shirabe::downloader::VcsDownloader;
+use shirabe::downloader::git_downloader::GitDownloader;
+use shirabe::io::IOInterface;
+use shirabe::package::handle::{CompletePackageHandle, PackageInterfaceHandle};
+use shirabe::util::Git as GitUtil;
+use shirabe::util::ProcessExecutor;
use shirabe::util::filesystem::Filesystem;
+use shirabe_php_shim::PhpMixed;
+use shirabe_semver::VersionParser;
use tempfile::TempDir;
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
+
+fn run<F: std::future::Future>(future: F) -> F::Output {
+ tokio::runtime::Builder::new_current_thread()
+ .build()
+ .unwrap()
+ .block_on(future)
+}
+
fn set_up() -> TempDir {
- // skipIfNotExecutable('git')
- let () = todo!();
- // initGitVersion('1.0.0') resets the Composer\Util\Git static version cache via
- // reflection; the static cache is not reachable from a test here.
- #[allow(unreachable_code)]
- {
- let _fs = Filesystem::new(None);
- TempDir::new().unwrap()
- }
+ // skipIfNotExecutable('git') is irrelevant because every git invocation is mocked.
+
+ // initGitVersion('1.0.0'): seed the Composer\Util\Git static version cache.
+ GitUtil::__set_version(Some("1.0.0".to_string()));
+
+ TempDir::new().unwrap()
}
fn tear_down(working_dir: &std::path::Path) {
@@ -20,9 +42,8 @@ fn tear_down(working_dir: &std::path::Path) {
let mut fs = Filesystem::new(None);
fs.remove_directory(working_dir).unwrap();
}
- // initGitVersion(false) resets the Composer\Util\Git static version cache via
- // reflection; the static cache is not reachable from a test here.
- todo!()
+ // initGitVersion(false): drop the static version cache so it is re-detected next time.
+ GitUtil::__set_version(None);
}
struct TearDown {
@@ -41,37 +62,280 @@ impl Drop for TearDown {
}
}
-// These construct a GitDownloader with a mocked IO/Config and a mocked ProcessExecutor to
-// feed git command output; mocking is not available, and a real HttpDownloader reaches
-// curl_multi_init (todo!()).
-#[ignore = "requires getProcessExecutorMock/getIOMock and getMockBuilder PackageInterface mock; no ProcessExecutorMock/IOMock mocking infrastructure exists"]
+/// ref: TestCase::getMockBuilder('Composer\Package\PackageInterface')->getMock()
+///
+/// A `getMockBuilder(PackageInterface)` mock returns null/0 for every unstubbed
+/// method, so a real CompletePackage seeded with the stubbed values is a faithful
+/// stand-in only when the resulting `getSourceUrls()` equals `[getSourceUrl()]`.
+/// The multi-URL/mirror cases in PHP configure `getSourceUrls` to an arbitrary
+/// list that a real package cannot reproduce, so those tests stay ignored.
+fn get_package(
+ name: &str,
+ pretty_version: &str,
+ source_reference: Option<&str>,
+ source_url: Option<&str>,
+) -> PackageInterfaceHandle {
+ let norm_version = VersionParser.normalize(pretty_version, None).unwrap();
+ let package =
+ CompletePackageHandle::new(name.to_string(), norm_version, pretty_version.to_string());
+ package.__set_source_type(Some("git".to_string()));
+ package.set_source_reference(source_reference.map(|s| s.to_string()));
+ package.set_source_url(source_url.map(|s| s.to_string()));
+ package.into()
+}
+
+/// ref: GitDownloaderTest::setupConfig (seeds a temp `home` when none is set)
+fn setup_config(config: Config) -> Config {
+ let mut config = config;
+ if !config.has("home") {
+ let tmp_dir = TempDir::new().unwrap().keep();
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut section: IndexMap<String, PhpMixed> = IndexMap::new();
+ section.insert(
+ "home".to_string(),
+ PhpMixed::String(tmp_dir.to_string_lossy().into_owned()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ }
+ config
+}
+
+/// ref: GitDownloaderTest::getDownloaderMock (defaults the IO/Config/Filesystem)
+fn get_downloader_mock(
+ io: Option<Rc<RefCell<dyn IOInterface>>>,
+ config: Option<Config>,
+ process: Rc<RefCell<ProcessExecutor>>,
+ filesystem: Option<Rc<RefCell<Filesystem>>>,
+) -> GitDownloader {
+ let io =
+ io.unwrap_or_else(|| Rc::new(RefCell::new(IOStub::new())) as Rc<RefCell<dyn IOInterface>>);
+ let config = Rc::new(RefCell::new(setup_config(
+ config.unwrap_or_else(|| ConfigStubBuilder::new().build()),
+ )));
+ GitDownloader::new(io, config, Some(process), filesystem)
+}
+
+#[serial]
#[test]
fn test_download_for_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let package = get_package("dummy/pkg", "1.0.0", None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process, None);
+
+ let result = run(async {
+ downloader.download(package.clone(), "/path", None).await?;
+ downloader
+ .prepare("install", package.clone(), "/path", None)
+ .await?;
+ downloader.install(package.clone(), "/path").await?;
+ downloader.cleanup("install", package, "/path", None).await
+ });
+
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_download() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://example.com/composer/composer";
+ let package = get_package(
+ "composer/composer",
+ "dev-master",
+ Some("1234567890123456789012345678901234567890"),
+ Some(url),
+ );
+
+ let expected_path = working_dir
+ .path()
+ .join("composerPath")
+ .to_string_lossy()
+ .into_owned();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(vec![
+ "git",
+ "clone",
+ "--no-checkout",
+ "--",
+ url,
+ &expected_path,
+ ]),
+ cmd(vec!["git", "remote", "add", "composer", "--", url]),
+ cmd(["git", "fetch", "composer"]),
+ cmd(vec!["git", "remote", "set-url", "origin", "--", url]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd(["git", "branch", "-r"]),
+ cmd(["git", "checkout", "master", "--"]),
+ cmd(vec![
+ "git",
+ "reset",
+ "--hard",
+ "1234567890123456789012345678901234567890",
+ "--",
+ ]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process, None);
+
+ run(async {
+ downloader
+ .download(package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .prepare("install", package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .install(package.clone(), &expected_path)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("install", package, &expected_path, None)
+ .await
+ .unwrap();
+ });
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and initGitVersion reflection on Git::version static cache; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_download_with_cache() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://example.com/composer/composer";
+ let package = get_package(
+ "composer/composer",
+ "dev-master",
+ Some("1234567890123456789012345678901234567890"),
+ Some(url),
+ );
+
+ // initGitVersion('2.17.0'): enables the cache (`--dissociate`) code path.
+ GitUtil::__set_version(Some("2.17.0".to_string()));
+
+ let config = setup_config(Config::new(false, None));
+ let cache_vcs_dir = config
+ .get("cache-vcs-dir")
+ .as_string()
+ .unwrap_or("")
+ .to_string();
+ // ref: GitDownloaderTest cachePath = cache-vcs-dir.'/'.preg_replace('{[^a-z0-9.]}i', '-', url).'/'
+ let sanitized: String = url
+ .chars()
+ .map(|c| {
+ if c.is_ascii_alphanumeric() || c == '.' {
+ c
+ } else {
+ '-'
+ }
+ })
+ .collect();
+ let cache_path = format!("{}/{}/", cache_vcs_dir, sanitized);
+
+ let mut filesystem = Filesystem::new(None);
+ filesystem.remove_directory(&cache_path).unwrap();
+
+ let expected_path = working_dir
+ .path()
+ .join("composerPath")
+ .to_string_lossy()
+ .into_owned();
+
+ let cache_path_for_callback = cache_path.clone();
+ let mut clone_mirror = cmd(vec!["git", "clone", "--mirror", "--", url, &cache_path]);
+ clone_mirror.callback = Some(Box::new(move || {
+ std::fs::create_dir_all(&cache_path_for_callback).ok();
+ }));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ clone_mirror,
+ cmd(["git", "remote", "-v"]),
+ cmd(vec!["git", "remote", "set-url", "origin", "--", url]),
+ cmd_full(["git", "rev-parse", "--git-dir"], 0, ".", ""),
+ cmd(vec![
+ "git",
+ "rev-parse",
+ "--quiet",
+ "--verify",
+ "1234567890123456789012345678901234567890^{commit}",
+ ]),
+ cmd(vec![
+ "git",
+ "clone",
+ "--no-checkout",
+ &cache_path,
+ &expected_path,
+ "--dissociate",
+ "--reference",
+ &cache_path,
+ ]),
+ cmd(vec!["git", "remote", "set-url", "origin", "--", url]),
+ cmd(vec!["git", "remote", "add", "composer", "--", url]),
+ cmd(["git", "branch", "-r"]),
+ cmd_full(["git", "checkout", "master", "--"], 1, "", ""),
+ cmd(vec![
+ "git",
+ "checkout",
+ "-B",
+ "master",
+ "composer/master",
+ "--",
+ ]),
+ cmd(vec![
+ "git",
+ "reset",
+ "--hard",
+ "1234567890123456789012345678901234567890",
+ "--",
+ ]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, Some(config), process, None);
+
+ run(async {
+ downloader
+ .download(package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .prepare("install", package.clone(), &expected_path, None)
+ .await
+ .unwrap();
+ downloader
+ .install(package.clone(), &expected_path)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("install", package, &expected_path, None)
+ .await
+ .unwrap();
+ });
+
+ let mut fs = Filesystem::new(None);
+ fs.remove_directory(&cache_path).ok();
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "getSourceUrls returns a mirror list (['https://github.com/mirrors/composer', \
+ 'https://github.com/composer/composer']) that a real CompletePackage cannot reproduce \
+ from a single source_url; no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_download_uses_various_protocols_and_sets_push_url_for_github() {
let working_dir = set_up();
@@ -80,7 +344,8 @@ fn test_download_uses_various_protocols_and_sets_push_url_for_github() {
todo!()
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and pushUrlProvider dataProvider; no mocking infrastructure exists"]
+#[ignore = "pushUrlProvider configures getSourceUrls to a list a real CompletePackage cannot \
+ reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_download_and_set_push_url_use_custom_various_protocols_for_github() {
let working_dir = set_up();
@@ -89,43 +354,243 @@ fn test_download_and_set_push_url_use_custom_various_protocols_for_github() {
todo!()
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_download_throws_runtime_exception_if_git_command_fails() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://example.com/composer/composer";
+ let package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url));
+
+ let expected_path = working_dir
+ .path()
+ .join("composerPath")
+ .to_string_lossy()
+ .into_owned();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![cmd_full(
+ vec!["git", "clone", "--no-checkout", "--", url, &expected_path],
+ 1,
+ "",
+ "",
+ )],
+ false,
+ Default::default(),
+ );
+
+ let mut downloader = get_downloader_mock(None, None, process, None);
+
+ let result = run(async {
+ downloader
+ .download(package.clone(), &expected_path, None)
+ .await?;
+ downloader
+ .prepare("install", package.clone(), &expected_path, None)
+ .await?;
+ downloader.install(package.clone(), &expected_path).await?;
+ downloader
+ .cleanup("install", package, &expected_path, None)
+ .await
+ });
+
+ let e = result.expect_err("failed git clone should throw");
+ assert!(e.to_string().contains(&format!(
+ "Failed to execute git clone --no-checkout -- {} {}",
+ url, expected_path
+ )));
}
-#[ignore = "requires getProcessExecutorMock/getDownloaderMock and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_updatefor_package_without_source_reference() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let initial_package = get_package("dummy/pkg", "1.0.0", Some("ref"), None);
+ let source_package = get_package("dummy/pkg", "1.0.0", None, None);
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let mut downloader = get_downloader_mock(None, None, process, None);
+
+ let result = run(async {
+ downloader
+ .download(
+ source_package.clone(),
+ "/path",
+ Some(initial_package.clone()),
+ )
+ .await?;
+ downloader
+ .prepare(
+ "update",
+ source_package.clone(),
+ "/path",
+ Some(initial_package.clone()),
+ )
+ .await?;
+ downloader
+ .update(initial_package.clone(), source_package.clone(), "/path")
+ .await?;
+ downloader
+ .cleanup("update", source_package, "/path", Some(initial_package))
+ .await
+ });
+
+ let e = result.expect_err("missing source reference should throw");
+ assert!(e.to_string().contains("missing reference information"));
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_update() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://github.com/composer/composer";
+ let package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(["git", "show-ref", "--head", "-d"]),
+ cmd(["git", "status", "--porcelain", "--untracked-files=no"]),
+ cmd_full(
+ ["git", "rev-parse", "--quiet", "--verify", "ref^{commit}"],
+ 1,
+ "",
+ "",
+ ),
+ // fallback commands for the above failing
+ cmd(["git", "remote", "-v"]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd(["git", "fetch", "composer"]),
+ cmd(["git", "fetch", "--tags", "composer"]),
+ cmd(["git", "remote", "-v"]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd(["git", "branch", "-r"]),
+ cmd(["git", "checkout", "ref", "--"]),
+ cmd(["git", "reset", "--hard", "ref", "--"]),
+ cmd(["git", "remote", "-v"]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy()))
+ .unwrap();
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let mut downloader = get_downloader_mock(None, Some(Config::new(false, None)), process, None);
+
+ run(async {
+ downloader
+ .download(package.clone(), &working_dir_str, Some(package.clone()))
+ .await
+ .unwrap();
+ downloader
+ .prepare(
+ "update",
+ package.clone(),
+ &working_dir_str,
+ Some(package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(package.clone(), package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", package.clone(), &working_dir_str, Some(package))
+ .await
+ .unwrap();
+ });
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_update_with_new_repo_url() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
- let _ = &working_dir;
- todo!()
+
+ let url = "https://github.com/composer/composer";
+ let package = get_package("composer/composer", "1.0.0", Some("ref"), Some(url));
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd(["git", "show-ref", "--head", "-d"]),
+ cmd(["git", "status", "--porcelain", "--untracked-files=no"]),
+ cmd_full(
+ ["git", "rev-parse", "--quiet", "--verify", "ref^{commit}"],
+ 0,
+ "",
+ "",
+ ),
+ cmd(["git", "remote", "-v"]),
+ cmd(vec!["git", "remote", "set-url", "composer", "--", url]),
+ cmd(["git", "branch", "-r"]),
+ cmd(["git", "checkout", "ref", "--"]),
+ cmd(["git", "reset", "--hard", "ref", "--"]),
+ cmd_full(
+ ["git", "remote", "-v"],
+ 0,
+ "origin https://github.com/old/url (fetch)\n\
+ origin https://github.com/old/url (push)\n\
+ composer https://github.com/old/url (fetch)\n\
+ composer https://github.com/old/url (push)\n",
+ "",
+ ),
+ cmd(vec!["git", "remote", "set-url", "origin", "--", url]),
+ cmd(vec![
+ "git",
+ "remote",
+ "set-url",
+ "--push",
+ "origin",
+ "--",
+ "git@github.com:composer/composer.git",
+ ]),
+ ],
+ true,
+ Default::default(),
+ );
+
+ let mut fs = Filesystem::new(None);
+ fs.ensure_directory_exists(&format!("{}/.git", working_dir.path().to_string_lossy()))
+ .unwrap();
+ let working_dir_str = working_dir.path().to_string_lossy().into_owned();
+
+ let mut downloader = get_downloader_mock(None, Some(Config::new(false, None)), process, None);
+
+ run(async {
+ downloader
+ .download(package.clone(), &working_dir_str, Some(package.clone()))
+ .await
+ .unwrap();
+ downloader
+ .prepare(
+ "update",
+ package.clone(),
+ &working_dir_str,
+ Some(package.clone()),
+ )
+ .await
+ .unwrap();
+ downloader
+ .update(package.clone(), package.clone(), &working_dir_str)
+ .await
+ .unwrap();
+ downloader
+ .cleanup("update", package.clone(), &working_dir_str, Some(package))
+ .await
+ .unwrap();
+ });
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "getSourceUrls returns a multi-URL list a real CompletePackage cannot reproduce; \
+ no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_update_throws_runtime_exception_if_git_command_fails() {
let working_dir = set_up();
@@ -134,7 +599,8 @@ fn test_update_throws_runtime_exception_if_git_command_fails() {
todo!()
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "getSourceUrls returns a multi-URL list (['/' , github]) a real CompletePackage cannot \
+ reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but_is_able_to_recover()
{
@@ -144,7 +610,8 @@ fn test_update_doesnt_throws_runtime_exception_if_git_command_fails_at_first_but
todo!()
}
-#[ignore = "requires getIOMock with expects() output expectations, getProcessExecutorMock and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \
+ cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_downgrade_shows_appropriate_message() {
let working_dir = set_up();
@@ -153,7 +620,8 @@ fn test_downgrade_shows_appropriate_message() {
todo!()
}
-#[ignore = "requires getIOMock with expects() output expectations, getProcessExecutorMock and getMockBuilder PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "getSourceUrls returns a multi-URL list (['/foo/bar', github]) a real CompletePackage \
+ cannot reproduce; no PackageInterface mock with arbitrary getSourceUrls exists"]
#[test]
fn test_not_using_downgrading_with_references() {
let working_dir = set_up();
@@ -162,7 +630,10 @@ fn test_not_using_downgrading_with_references() {
todo!()
}
-#[ignore = "requires getProcessExecutorMock, getMockBuilder Filesystem mock with removeDirectoryAsync expectation and PackageInterface mock; no mocking infrastructure exists"]
+#[ignore = "PHP mocks Filesystem::removeDirectoryAsync (asserting it is called once with the \
+ working dir). With no Filesystem mock, the real removeDirectoryAsync drives the \
+ Filesystem's own ProcessExecutor for `rm -rf`, which requires a Composer\\Loop and \
+ cannot be redirected through the mocked ProcessExecutor"]
#[test]
fn test_remove() {
let working_dir = set_up();
@@ -171,11 +642,15 @@ fn test_remove() {
todo!()
}
-#[ignore = "requires getDownloaderMock which mocks IOInterface/Filesystem via getMockBuilder; no mocking infrastructure exists"]
+#[serial]
#[test]
fn test_get_installation_source() {
let working_dir = set_up();
let _tear_down = TearDown::new(working_dir.path().to_path_buf());
let _ = &working_dir;
- todo!()
+
+ let (process, _guard) = get_process_executor_mock(vec![], false, Default::default());
+ let downloader = get_downloader_mock(None, None, process, None);
+
+ assert_eq!("source", downloader.get_installation_source());
}
diff --git a/crates/shirabe/tests/downloader/main.rs b/crates/shirabe/tests/downloader/main.rs
index a2bbab4..8244010 100644
--- a/crates/shirabe/tests/downloader/main.rs
+++ b/crates/shirabe/tests/downloader/main.rs
@@ -2,8 +2,12 @@
mod config_stub;
#[path = "../common/http_downloader_mock.rs"]
mod http_downloader_mock;
+#[path = "../common/io_mock.rs"]
+mod io_mock;
#[path = "../common/io_stub.rs"]
mod io_stub;
+#[path = "../common/process_executor_mock.rs"]
+mod process_executor_mock;
mod archive_downloader_test;
mod download_manager_test;
diff --git a/crates/shirabe/tests/package/main.rs b/crates/shirabe/tests/package/main.rs
index 57d7cd3..7d841b4 100644
--- a/crates/shirabe/tests/package/main.rs
+++ b/crates/shirabe/tests/package/main.rs
@@ -1,3 +1,9 @@
+#[path = "../common/config_stub.rs"]
+mod config_stub;
+#[path = "../common/io_stub.rs"]
+mod io_stub;
+#[path = "../common/process_executor_mock.rs"]
+mod process_executor_mock;
#[path = "../common/test_case.rs"]
mod test_case;
diff --git a/crates/shirabe/tests/package/version/version_guesser_test.rs b/crates/shirabe/tests/package/version/version_guesser_test.rs
index 1ec0ed4..6bfe6c1 100644
--- a/crates/shirabe/tests/package/version/version_guesser_test.rs
+++ b/crates/shirabe/tests/package/version/version_guesser_test.rs
@@ -3,121 +3,608 @@
use std::cell::RefCell;
use std::rc::Rc;
+use indexmap::IndexMap;
+use serial_test::serial;
use shirabe::config::Config;
use shirabe::package::version::{VersionGuesser, VersionParser};
+use shirabe::util::Git as GitUtil;
use shirabe::util::platform::Platform;
-use shirabe::util::process_executor::ProcessExecutor;
+use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
+use shirabe_php_shim::PhpMixed;
-#[allow(dead_code)]
-fn set_up() {
- // Resets GitUtil's cached `version` static via ReflectionProperty; the static is not
- // exposed here and reflection-based mutation has no ported equivalent.
- todo!()
-}
+use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
-#[allow(dead_code)]
-fn tear_down() {
- todo!()
+// Mirrors VersionGuesserTest::setUp/tearDown: reset GitUtil's cached `version`
+// static so each test re-runs `git --version` against its own mock.
+fn set_up() {
+ GitUtil::__reset_version();
}
-#[allow(dead_code)]
struct TearDown;
impl Drop for TearDown {
fn drop(&mut self) {
- tear_down();
+ GitUtil::__reset_version();
}
}
-// These drive VersionGuesser with a mocked ProcessExecutor feeding git/hg command output;
-// mocking is not available here.
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+// `$config = new Config; $config->merge(['repositories' => ['packagist' => false]]);`
+fn make_config() -> Rc<RefCell<Config>> {
+ let mut config = Config::new(true, None);
+ let mut repositories: IndexMap<String, PhpMixed> = IndexMap::new();
+ repositories.insert("packagist".to_string(), PhpMixed::Bool(false));
+ let mut merge: IndexMap<String, PhpMixed> = IndexMap::new();
+ merge.insert("repositories".to_string(), PhpMixed::Array(repositories));
+ config.merge(&merge, Config::SOURCE_UNKNOWN);
+ Rc::new(RefCell::new(config))
+}
+
+fn make_guesser(
+ config: Rc<RefCell<Config>>,
+ process: Rc<RefCell<ProcessExecutor>>,
+) -> VersionGuesser {
+ VersionGuesser::new(config, process, VersionParser::new(), None)
+}
+
+// Helper to build a `['key' => 'value']` package config used in the tests.
+fn package_config(entries: &[(&str, PhpMixed)]) -> IndexMap<String, PhpMixed> {
+ let mut map: IndexMap<String, PhpMixed> = IndexMap::new();
+ for (k, v) in entries {
+ map.insert(k.to_string(), v.clone());
+ }
+ map
+}
+
+fn string_list(items: &[&str]) -> PhpMixed {
+ PhpMixed::List(
+ items
+ .iter()
+ .map(|s| PhpMixed::String(s.to_string()))
+ .collect(),
+ )
+}
+
+#[ignore = "guess_hg_version builds an HttpDownloader for HgDriver, which calls curl_multi_init() (todo!() in shirabe-php-shim::curl)"]
#[test]
+#[serial]
fn test_hg_guess_version_returns_data() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let branch = "default";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.33.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 128,
+ "",
+ "",
+ ),
+ cmd_full(["git", "describe", "--exact-match", "--tags"], 128, "", ""),
+ cmd_full(
+ [
+ "git",
+ "rev-list",
+ "--no-commit-header",
+ "--format=%H",
+ "-n1",
+ "HEAD",
+ "--no-show-signature",
+ ],
+ 128,
+ "",
+ "",
+ ),
+ cmd_full(["hg", "branch"], 0, branch, ""),
+ cmd_full(["hg", "branches"], 0, "", ""),
+ cmd_full(["hg", "bookmarks"], 0, "", ""),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!(format!("dev-{}", branch), version_data.version.unwrap());
+ assert_eq!(
+ format!("dev-{}", branch),
+ version_data.pretty_version.unwrap()
+ );
+ assert!(version_data.commit.as_deref().unwrap_or("").is_empty());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_guess_version_returns_data() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+ let another_commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ "* master {} Commit message\n(no branch) {} Commit message\n",
+ commit_hash, another_commit_hash
+ ),
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-master", version_data.version.unwrap());
+ assert_eq!("dev-master", version_data.pretty_version.unwrap());
+ assert!(version_data.feature_version.is_none());
+ assert!(version_data.feature_pretty_version.is_none());
+ assert_eq!(commit_hash, version_data.commit.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_guess_version_does_not_see_custom_default_branch_as_non_feature_branch() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+ let another_commit_hash = "13a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ // Assumption here is that arbitrary would be the default branch
+ format!(
+ " arbitrary {} Commit message\n* current {} Another message\n",
+ commit_hash, another_commit_hash
+ ),
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(
+ &package_config(&[("version", PhpMixed::String("self.version".to_string()))]),
+ "dummy/path",
+ )
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-current", version_data.version.unwrap());
+ assert_eq!(another_commit_hash, version_data.commit.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"]
#[test]
+#[serial]
fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+ let another_commit_hash = "13a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ " arbitrary {} Commit message\n* feature {} Another message\n",
+ commit_hash, another_commit_hash
+ ),
+ "",
+ ),
+ cmd_full(
+ ["git", "rev-list", "arbitrary..feature"],
+ 0,
+ format!("{}\n", another_commit_hash),
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(
+ &package_config(&[
+ ("version", PhpMixed::String("self.version".to_string())),
+ ("non-feature-branches", string_list(&["arbitrary"])),
+ ]),
+ "dummy/path",
+ )
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-arbitrary", version_data.version.unwrap());
+ assert_eq!(another_commit_hash, version_data.commit.unwrap());
+ assert_eq!(
+ "dev-feature",
+ version_data.feature_version.as_deref().unwrap()
+ );
+ assert_eq!(
+ "dev-feature",
+ version_data.feature_pretty_version.as_deref().unwrap()
+ );
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+#[ignore = "feature-branch guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"]
#[test]
+#[serial]
fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming_regex()
{
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+ let another_commit_hash = "13a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ " latest-testing {} Commit message\n* feature {} Another message\n",
+ commit_hash, another_commit_hash
+ ),
+ "",
+ ),
+ cmd_full(
+ ["git", "rev-list", "latest-testing..feature"],
+ 0,
+ format!("{}\n", another_commit_hash),
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(
+ &package_config(&[
+ ("version", PhpMixed::String("self.version".to_string())),
+ ("non-feature-branches", string_list(&["latest-.*"])),
+ ]),
+ "dummy/path",
+ )
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-latest-testing", version_data.version.unwrap());
+ assert_eq!(another_commit_hash, version_data.commit.unwrap());
+ assert_eq!(
+ "dev-feature",
+ version_data.feature_version.as_deref().unwrap()
+ );
+ assert_eq!(
+ "dev-feature",
+ version_data.feature_pretty_version.as_deref().unwrap()
+ );
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_guess_version_reads_and_respects_non_feature_branches_configuration_for_arbitrary_naming_when_on_non_feature_branch()
{
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+ let another_commit_hash = "13a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ "* latest-testing {} Commit message\n current {} Another message\n master {} Another message\n",
+ commit_hash, another_commit_hash, another_commit_hash
+ ),
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(
+ &package_config(&[
+ ("version", PhpMixed::String("self.version".to_string())),
+ ("non-feature-branches", string_list(&["latest-.*"])),
+ ]),
+ "dummy/path",
+ )
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-latest-testing", version_data.version.unwrap());
+ assert_eq!(commit_hash, version_data.commit.unwrap());
+ assert!(version_data.feature_version.is_none());
+ assert!(version_data.feature_pretty_version.is_none());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_detached_head_becomes_dev_hash() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!("* (no branch) {} Commit message\n", commit_hash),
+ "",
+ ),
+ cmd(["git", "describe", "--exact-match", "--tags"]),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!(
+ format!("dev-{}", commit_hash),
+ version_data.version.unwrap()
+ );
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_detached_fetch_head_becomes_dev_hash_git2() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ "* (HEAD detached at FETCH_HEAD) {} Commit message\n",
+ commit_hash
+ ),
+ "",
+ ),
+ cmd(["git", "describe", "--exact-match", "--tags"]),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!(
+ format!("dev-{}", commit_hash),
+ version_data.version.unwrap()
+ );
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_detached_commit_head_becomes_dev_hash_git2() {
- todo!()
+ set_up();
+ let _td = TearDown;
+ let commit_hash = "03a15d220da53c52eddd5f32ffca64a7b3801bea";
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ format!(
+ "* (HEAD detached at 03a15d220) {} Commit message\n",
+ commit_hash
+ ),
+ "",
+ ),
+ cmd(["git", "describe", "--exact-match", "--tags"]),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!(
+ format!("dev-{}", commit_hash),
+ version_data.version.unwrap()
+ );
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_tag_becomes_version() {
- todo!()
+ set_up();
+ let _td = TearDown;
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* (HEAD detached at v2.0.5-alpha2) 433b98d4218c181bae01865901aac045585e8a1a Commit message\n",
+ "",
+ ),
+ cmd_full(
+ ["git", "describe", "--exact-match", "--tags"],
+ 0,
+ "v2.0.5-alpha2",
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("2.0.5.0-alpha2", version_data.version.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_tag_becomes_pretty_version() {
- todo!()
+ set_up();
+ let _td = TearDown;
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* (HEAD detached at 1.0.0) c006f0c12bbbf197b5c071ffb1c0e9812bb14a4d Commit message\n",
+ "",
+ ),
+ cmd_full(
+ ["git", "describe", "--exact-match", "--tags"],
+ 0,
+ "1.0.0",
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("1.0.0.0", version_data.version.unwrap());
+ assert_eq!("1.0.0", version_data.pretty_version.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_invalid_tag_becomes_version() {
- todo!()
+ set_up();
+ let _td = TearDown;
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* foo 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n",
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("dev-foo", version_data.version.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
#[test]
+#[serial]
fn test_numeric_branches_show_nicely() {
- todo!()
+ set_up();
+ let _td = TearDown;
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* 1.5 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n",
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(&IndexMap::new(), "dummy/path")
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("1.5.x-dev", version_data.pretty_version.unwrap());
+ assert_eq!("1.5.9999999.9999999-dev", version_data.version.unwrap());
}
-#[ignore = "requires getProcessExecutorMock with expects() command expectations; no ProcessExecutorMock mocking infrastructure exists"]
+#[ignore = "remote-branch feature guessing calls ProcessExecutor::execute_async, whose mock path is todo!()"]
#[test]
+#[serial]
fn test_remote_branches_are_selected() {
- todo!()
+ set_up();
+ let _td = TearDown;
+
+ let expectations: Vec<MockExpectation> = vec![
+ cmd_full(["git", "--version"], 0, "git version 2.52.0", ""),
+ cmd_full(
+ ["git", "branch", "-a", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ "* feature-branch 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n\
+ remotes/origin/1.5 03a15d220da53c52eddd5f32ffca64a7b3801bea Commit message\n",
+ "",
+ ),
+ cmd_full(
+ ["git", "rev-list", "remotes/origin/1.5..feature-branch"],
+ 0,
+ "\n",
+ "",
+ ),
+ ];
+ let (process, _guard) = get_process_executor_mock(expectations, true, MockHandler::default());
+
+ let config = make_config();
+ let mut guesser = make_guesser(config, process);
+ let version_data = guesser
+ .guess_version(
+ &package_config(&[("version", PhpMixed::String("self.version".to_string()))]),
+ "dummy/path",
+ )
+ .unwrap()
+ .expect("expected version data");
+
+ assert_eq!("1.5.x-dev", version_data.pretty_version.unwrap());
+ assert_eq!("1.5.9999999.9999999-dev", version_data.version.unwrap());
}
#[test]
+#[serial]
fn test_get_root_version_from_env() {
// @dataProvider rootEnvVersionsProvider
let root_env_versions: Vec<(&str, &str)> = vec![
diff --git a/crates/shirabe/tests/repository/main.rs b/crates/shirabe/tests/repository/main.rs
index 15a42a3..f5ecbc6 100644
--- a/crates/shirabe/tests/repository/main.rs
+++ b/crates/shirabe/tests/repository/main.rs
@@ -4,6 +4,8 @@ mod config_stub;
mod http_downloader_mock;
#[path = "../common/io_stub.rs"]
mod io_stub;
+#[path = "../common/process_executor_mock.rs"]
+mod process_executor_mock;
#[path = "../common/test_case.rs"]
mod test_case;
diff --git a/crates/shirabe/tests/repository/vcs/github_driver_test.rs b/crates/shirabe/tests/repository/vcs/github_driver_test.rs
index f5958a4..0469edf 100644
--- a/crates/shirabe/tests/repository/vcs/github_driver_test.rs
+++ b/crates/shirabe/tests/repository/vcs/github_driver_test.rs
@@ -5,13 +5,22 @@ use std::rc::Rc;
use indexmap::IndexMap;
use shirabe::config::Config;
+use shirabe::config::ConfigSourceInterface;
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::repository::vcs::GitHubDriver;
use shirabe::util::filesystem::Filesystem;
+use shirabe::util::http_downloader::HttpDownloaderMockHandler;
+use shirabe::util::process_executor::{MockHandler, ProcessExecutor};
use shirabe_php_shim::PhpMixed;
use tempfile::TempDir;
+use crate::http_downloader_mock::{HttpDownloaderMockGuard, expect_full, get_http_downloader_mock};
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::{
+ ProcessExecutorMockGuard, cmd, cmd_full, get_process_executor_mock,
+};
+
struct SetUp {
home: TempDir,
config: Config,
@@ -53,6 +62,82 @@ impl Drop for TearDown {
}
}
+// No-op ConfigSourceInterface, equivalent to PHPUnit's
+// `getMockBuilder(ConfigSourceInterface::class)->getMock()` whose methods all do
+// nothing. Used by testPrivateRepository to satisfy the OAuth token-storing path.
+#[derive(Debug)]
+struct NullConfigSource;
+
+impl ConfigSourceInterface for NullConfigSource {
+ fn add_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _append: bool,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn insert_repository(
+ &mut self,
+ _name: &str,
+ _config: PhpMixed,
+ _reference_name: &str,
+ _offset: i64,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn set_repository_url(&mut self, _name: &str, _url: &str) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn remove_repository(&mut self, _name: &str) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn add_config_setting(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn remove_config_setting(&mut self, _name: &str) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn add_property(&mut self, _name: &str, _value: PhpMixed) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn remove_property(&mut self, _name: &str) -> anyhow::Result<()> {
+ Ok(())
+ }
+ fn add_link(&mut self, r#type: &str, name: &str, value: &str) -> anyhow::Result<()> {
+ let _ = (r#type, name, value);
+ Ok(())
+ }
+ fn remove_link(&mut self, r#type: &str, name: &str) -> anyhow::Result<()> {
+ let _ = (r#type, name);
+ Ok(())
+ }
+ fn get_name(&self) -> String {
+ String::new()
+ }
+}
+
+// Mirrors PHP's $httpDownloader->expects([...], true): an `['url' => .., 'body' => ..]`
+// entry (status defaults to 200).
+fn http_body(
+ url: &str,
+ body: impl Into<String>,
+) -> shirabe::util::http_downloader::HttpDownloaderMockExpectation {
+ expect_full(url, None, 200, body, vec![String::new()])
+}
+
+// Mirrors PHP's `['url' => .., 'status' => 404]` entry (no body).
+fn http_status(
+ url: &str,
+ status: i64,
+) -> shirabe::util::http_downloader::HttpDownloaderMockExpectation {
+ expect_full(url, None, status, String::new(), vec![String::new()])
+}
+
+fn b64(s: &str) -> String {
+ shirabe_php_shim::base64_encode(s)
+}
+
fn supports_provider() -> Vec<(bool, &'static str)> {
vec![
(false, "https://github.com/acme"),
@@ -80,77 +165,731 @@ fn test_supports() {
}
}
-// The remaining cases construct a GitHubDriver and mock the HttpDownloader/IO to return
-// GitHub API responses; mocking is not available, and a real HttpDownloader reaches
-// curl_multi_init (todo!()).
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject (askAndHideAnswer/setAuthentication), and setAttribute reflection are not ported"]
fn test_private_repository() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let repo_ssh_url = "git@github.com:composer/packagist.git";
+ let identifier = "v0.0.0";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(
+ IOStub::new()
+ .with_is_interactive(true)
+ .with_ask_and_hide_answer(Some("sometoken".to_string())),
+ ));
+
+ let (http_downloader, _http_guard): (_, HttpDownloaderMockGuard) = get_http_downloader_mock(
+ vec![
+ http_status(repo_api_url, 404),
+ http_body("https://api.github.com/", "{}"),
+ http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "private": true, "owner": {"login": "composer"}, "name": "packagist"}"#,
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let (process, _process_guard): (_, ProcessExecutorMockGuard) = get_process_executor_mock(
+ vec![],
+ false,
+ MockHandler {
+ r#return: 1,
+ stdout: String::new(),
+ stderr: String::new(),
+ },
+ );
+
+ config
+ .borrow_mut()
+ .set_config_source(Box::new(NullConfigSource));
+ config
+ .borrow_mut()
+ .set_auth_config_source(Box::new(NullConfigSource));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+
+ assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap());
+
+ let dist = git_hub_driver.get_dist(sha).unwrap();
+ assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"),
+ dist.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some("SOMESHA"),
+ dist.get("reference").and_then(|v| v.as_string())
+ );
+
+ let source = git_hub_driver.get_source(sha);
+ assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some(repo_ssh_url),
+ source.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some("SOMESHA"),
+ source.get("reference").and_then(|v| v.as_string())
+ );
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"]
fn test_public_repository() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let identifier = "v0.0.0";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#,
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+ let repo_url = "https://github.com/composer/packagist.git";
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+
+ assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap());
+
+ let dist = git_hub_driver.get_dist(sha).unwrap();
+ assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"),
+ dist.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string()));
+
+ let source = git_hub_driver.get_source(sha);
+ assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some(repo_url),
+ source.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some(sha),
+ source.get("reference").and_then(|v| v.as_string())
+ );
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"]
+#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"]
fn test_public_repository2() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let identifier = "feature/3.2-foo";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo",
+ format!(
+ r#"{{"encoding":"base64","content":"{}"}}"#,
+ b64(&format!(r#"{{"support": {{"source": "{}" }}}}"#, repo_url))
+ ),
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo",
+ r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml",
+ format!(
+ r#"{{"encoding": "base64", "content": "{}"}}"#,
+ b64("custom: https://example.com")
+ ),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+ let repo_url = "https://github.com/composer/packagist.git";
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+
+ assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap());
+
+ let dist = git_hub_driver.get_dist(sha).unwrap();
+ assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"),
+ dist.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string()));
+
+ let source = git_hub_driver.get_source(sha);
+ assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some(repo_url),
+ source.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some(sha),
+ source.get("reference").and_then(|v| v.as_string())
+ );
+
+ let data = git_hub_driver
+ .get_composer_information(identifier)
+ .unwrap()
+ .unwrap();
+ assert!(!data.contains_key("abandoned"));
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"]
+#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"]
fn test_invalid_support_data() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let identifier = "feature/3.2-foo";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo",
+ format!(
+ r#"{{"encoding":"base64","content":"{}"}}"#,
+ b64(&format!(r#"{{"support": "{}" }}"#, repo_url))
+ ),
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo",
+ r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml",
+ format!(
+ r#"{{"encoding": "base64", "content": "{}"}}"#,
+ b64("custom: https://example.com")
+ ),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+ let mut branches = IndexMap::new();
+ branches.insert("test_master".to_string(), sha.to_string());
+ git_hub_driver.__set_branches(Some(branches));
+
+ let data = git_hub_driver
+ .get_composer_information(identifier)
+ .unwrap()
+ .unwrap();
+
+ let support = data.get("support").and_then(|v| v.as_array()).unwrap();
+ assert_eq!(
+ Some("https://github.com/composer/packagist/tree/feature/3.2-foo"),
+ support.get("source").and_then(|v| v.as_string())
+ );
+}
+
+fn funding_url_provider() -> Vec<(&'static str, Option<Vec<(&'static str, &'static str)>>)> {
+ let all_named_platforms = "community_bridge: project-name
+github: [userA, userB]
+issuehunt: userName
+ko_fi: userName
+liberapay: userName
+open_collective: userName
+patreon: userName
+tidelift: Platform/Package
+polar: userName
+buy_me_a_coffee: userName
+thanks_dev: u/gh/userName
+otechie: userName";
+
+ vec![
+ (
+ all_named_platforms,
+ Some(vec![
+ (
+ "community_bridge",
+ "https://funding.communitybridge.org/projects/project-name",
+ ),
+ ("github", "https://github.com/userA"),
+ ("github", "https://github.com/userB"),
+ ("issuehunt", "https://issuehunt.io/r/userName"),
+ ("ko_fi", "https://ko-fi.com/userName"),
+ ("liberapay", "https://liberapay.com/userName"),
+ ("open_collective", "https://opencollective.com/userName"),
+ ("patreon", "https://www.patreon.com/userName"),
+ (
+ "tidelift",
+ "https://tidelift.com/funding/github/Platform/Package",
+ ),
+ ("polar", "https://polar.sh/userName"),
+ ("buy_me_a_coffee", "https://www.buymeacoffee.com/userName"),
+ ("thanks_dev", "https://thanks.dev/u/gh/userName"),
+ ("otechie", "https://otechie.com/userName"),
+ ]),
+ ),
+ (
+ "custom: example.com",
+ Some(vec![("custom", "https://example.com")]),
+ ),
+ (
+ "custom: [example.com]",
+ Some(vec![("custom", "https://example.com")]),
+ ),
+ (
+ "custom: \"https://example.com\"",
+ Some(vec![("custom", "https://example.com")]),
+ ),
+ (
+ "custom: [\"https://example.com\"]",
+ Some(vec![("custom", "https://example.com")]),
+ ),
+ (
+ "custom: [\"https://example.com\", example.org]",
+ Some(vec![
+ ("custom", "https://example.com"),
+ ("custom", "https://example.org"),
+ ]),
+ ),
+ (
+ "custom: [example.net/funding, \"https://example.com\", example.org]",
+ Some(vec![
+ ("custom", "https://example.com"),
+ ("custom", "https://example.org"),
+ ]),
+ ),
+ ]
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, setAttribute reflection, and the fundingUrlProvider data provider are not ported"]
+#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"]
fn test_funding_format() {
- let SetUp { home, config: _ } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+ for (funding, expected) in funding_url_provider() {
+ let SetUp { home, config } = set_up();
+ let _tear_down = TearDown::new(home.path().to_path_buf());
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let identifier = "feature/3.2-foo";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist"}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=feature%2F3.2-foo",
+ format!(
+ r#"{{"encoding":"base64","content":"{}"}}"#,
+ b64(&format!(r#"{{"support": {{"source": "{}" }}}}"#, repo_url))
+ ),
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/commits/feature%2F3.2-foo",
+ r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml",
+ format!(r#"{{"encoding": "base64", "content": "{}"}}"#, b64(funding)),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver =
+ GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+ let mut branches = IndexMap::new();
+ branches.insert("test_master".to_string(), sha.to_string());
+ git_hub_driver.__set_branches(Some(branches));
+
+ let data = git_hub_driver
+ .get_composer_information(identifier)
+ .unwrap()
+ .unwrap();
+
+ match expected {
+ None => assert!(!data.contains_key("funding")),
+ Some(expected) => {
+ let funding_list = match data.get("funding") {
+ Some(PhpMixed::List(l)) => l.clone(),
+ Some(PhpMixed::Array(m)) => m.values().cloned().collect(),
+ other => panic!("unexpected funding value: {:?}", other),
+ };
+ let actual: Vec<(String, String)> = funding_list
+ .iter()
+ .map(|entry| {
+ let m = entry.as_array().unwrap();
+ (
+ m.get("type")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string(),
+ m.get("url")
+ .and_then(|v| v.as_string())
+ .unwrap_or("")
+ .to_string(),
+ )
+ })
+ .collect();
+ let expected_vec: Vec<(String, String)> = expected
+ .iter()
+ .map(|(t, u)| (t.to_string(), u.to_string()))
+ .collect();
+ assert_eq!(expected_vec, actual);
+ }
+ }
+ }
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, the IOInterface MockObject, and setAttribute reflection are not ported"]
+#[ignore = "blocked by shim: getComposerInformation -> get_change_date -> shirabe_php_shim::date_create is todo!()"]
fn test_public_repository_archived() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let identifier = "v0.0.0";
+ let sha = "SOMESHA";
+ let composer_json_url = format!(
+ "https://api.github.com/repos/composer/packagist/contents/composer.json?ref={}",
+ sha
+ );
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ repo_api_url,
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}"#,
+ ),
+ http_body(
+ &composer_json_url,
+ format!(
+ r#"{{"encoding": "base64", "content": "{}"}}"#,
+ b64(r#"{"name": "composer/packagist"}"#)
+ ),
+ ),
+ http_body(
+ &format!(
+ "https://api.github.com/repos/composer/packagist/commits/{}",
+ sha
+ ),
+ r#"{"commit": {"committer":{ "date": "2012-09-10"}}}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/.github/FUNDING.yml",
+ format!(
+ r#"{{"encoding": "base64", "content": "{}"}}"#,
+ b64("custom: https://example.com")
+ ),
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+ let mut tags = IndexMap::new();
+ tags.insert(identifier.to_string(), sha.to_string());
+ git_hub_driver.__set_tags(Some(tags));
+
+ let data = git_hub_driver
+ .get_composer_information(sha)
+ .unwrap()
+ .unwrap();
+
+ assert_eq!(Some(true), data.get("abandoned").and_then(|v| v.as_bool()));
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, and the IOInterface MockObject are not ported"]
+#[ignore = "GitDriver clone-fallback path runs an unexpected `git --version` (Git::get_version) not in the PHP mock expectation list; needs the version static seeded and the Rust sync_mirror command sequence to match"]
fn test_private_repository_no_interaction() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+ let repo_api_url = "https://api.github.com/repos/composer/packagist";
+ let repo_ssh_url = "git@github.com:composer/packagist.git";
+ let identifier = "v0.0.0";
+ let sha = "SOMESHA";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(false)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![http_status(repo_api_url, 404)],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ // clean local clone if present
+ let cache_dir = std::env::temp_dir().join("composer-test");
+ let mut fs = Filesystem::new(None);
+ let _ = fs.remove_directory(&cache_dir);
+ let cache_vcs_dir = std::env::temp_dir()
+ .join("composer-test/cache")
+ .to_string_lossy()
+ .into_owned();
+ {
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "cache-vcs-dir".to_string(),
+ PhpMixed::String(cache_vcs_dir.clone()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.borrow_mut().merge(&top, Config::SOURCE_UNKNOWN);
+ }
+ let resolved_cache_vcs_dir = config
+ .borrow_mut()
+ .get("cache-vcs-dir")
+ .as_string()
+ .unwrap_or("")
+ .to_string();
+
+ let (process, _process_guard): (_, ProcessExecutorMockGuard) = get_process_executor_mock(
+ vec![
+ cmd_full(["git", "config", "github.accesstoken"], 1, "", ""),
+ cmd([
+ "git",
+ "clone",
+ "--mirror",
+ "--",
+ repo_ssh_url,
+ &format!(
+ "{}/git-github.com-composer-packagist.git/",
+ resolved_cache_vcs_dir
+ ),
+ ]),
+ cmd(["git", "remote", "-v"]),
+ cmd(["git", "remote", "set-url", "origin", "--", repo_ssh_url]),
+ cmd_full(
+ ["git", "show-ref", "--tags", "--dereference"],
+ 0,
+ format!("{} refs/tags/{}", sha, identifier),
+ "",
+ ),
+ cmd_full(
+ ["git", "branch", "--no-color", "--no-abbrev", "-v"],
+ 0,
+ " test_master edf93f1fccaebd8764383dc12016d0a1a9672d89 Fix test & behavior",
+ "",
+ ),
+ cmd_full(["git", "branch", "--no-color"], 0, "* test_master", ""),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+
+ assert_eq!("test_master", git_hub_driver.get_root_identifier().unwrap());
+
+ let dist = git_hub_driver.get_dist(sha).unwrap();
+ assert_eq!(Some("zip"), dist.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some("https://api.github.com/repos/composer/packagist/zipball/SOMESHA"),
+ dist.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(Some(sha), dist.get("reference").and_then(|v| v.as_string()));
+
+ let source = git_hub_driver.get_source(identifier);
+ assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some(repo_ssh_url),
+ source.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some(identifier),
+ source.get("reference").and_then(|v| v.as_string())
+ );
+
+ let source = git_hub_driver.get_source(sha);
+ assert_eq!(Some("git"), source.get("type").and_then(|v| v.as_string()));
+ assert_eq!(
+ Some(repo_ssh_url),
+ source.get("url").and_then(|v| v.as_string())
+ );
+ assert_eq!(
+ Some(sha),
+ source.get("reference").and_then(|v| v.as_string())
+ );
+}
+
+fn invalid_url_provider() -> Vec<&'static str> {
+ vec![
+ "https://github.com/acme",
+ "https://github.com/acme/repository/releases",
+ "https://github.com/acme/repository/pulls",
+ ]
}
#[test]
-#[ignore = "IOInterface MockObject (getMockBuilder) and HttpDownloader MockObject (getMockBuilder) plus the invalidUrlProvider data provider are not ported"]
fn test_initialize_invalid_repo_url() {
- let SetUp { home, config: _ } = set_up();
- let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+ for url in invalid_url_provider() {
+ let SetUp { home, config } = set_up();
+ let _tear_down = TearDown::new(home.path().to_path_buf());
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(IOStub::new()));
+
+ let (http_downloader, _http_guard) =
+ get_http_downloader_mock(vec![], true, HttpDownloaderMockHandler::default());
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+
+ let mut git_hub_driver =
+ GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ let result = git_hub_driver.initialize();
+ assert!(
+ result.is_err(),
+ "expected InvalidArgumentException for url {}",
+ url
+ );
+ }
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock), ProcessExecutorMock, and the IOInterface MockObject are not ported"]
fn test_get_empty_file_content() {
- let SetUp { home, config: _ } = set_up();
+ let SetUp { home, config } = set_up();
let _tear_down = TearDown::new(home.path().to_path_buf());
- todo!()
+
+ let repo_url = "http://github.com/composer/packagist";
+
+ let config = Rc::new(RefCell::new(config));
+ let io: Rc<RefCell<dyn IOInterface>> =
+ Rc::new(RefCell::new(IOStub::new().with_is_interactive(true)));
+
+ let (http_downloader, _http_guard) = get_http_downloader_mock(
+ vec![
+ http_body(
+ "https://api.github.com/repos/composer/packagist",
+ r#"{"master_branch": "test_master", "owner": {"login": "composer"}, "name": "packagist", "archived": true}"#,
+ ),
+ http_body(
+ "https://api.github.com/repos/composer/packagist/contents/composer.json?ref=main",
+ r#"{"encoding":"base64","content":""}"#,
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+
+ let process = Rc::new(RefCell::new(ProcessExecutor::new(None)));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(repo_url.to_string()));
+
+ let mut git_hub_driver = GitHubDriver::new(repo_config, io, config, http_downloader, process);
+ git_hub_driver.initialize().unwrap();
+
+ assert_eq!(
+ Some(String::new()),
+ git_hub_driver
+ .get_file_content("composer.json", "main")
+ .unwrap()
+ );
}
diff --git a/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs b/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs
index 0333a2b..e21835d 100644
--- a/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs
+++ b/crates/shirabe/tests/repository/vcs/gitlab_driver_test.rs
@@ -8,8 +8,15 @@ use shirabe::config::Config;
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::repository::vcs::GitLabDriver;
+use shirabe::util::http_downloader::{HttpDownloader, HttpDownloaderMockHandler};
+use shirabe::util::process_executor::{MockHandler, ProcessExecutor};
use shirabe_php_shim::{PhpMixed, extension_loaded};
+use crate::http_downloader_mock::{
+ HttpDownloaderMockGuard, expect, expect_full, get_http_downloader_mock,
+};
+use crate::process_executor_mock::{ProcessExecutorMockGuard, get_process_executor_mock};
+
// Mirrors GitLabDriverTest::setUp's `gitlab-domains` configuration.
fn make_config() -> Config {
let mut config = Config::new(true, None);
@@ -30,74 +37,596 @@ fn make_config() -> Config {
config
}
+// Common test fixtures: the IO mock (a bare NullIO, matching PHP's unconfigured
+// getMock), the config and a no-op process executor mock. The PHP test passes a
+// bare `getMock()` ProcessExecutor; none of these tests invoke git, so an empty
+// strict expectation set both matches PHP and catches unexpected process calls.
+struct Fixtures {
+ io: Rc<RefCell<dyn IOInterface>>,
+ config: Rc<RefCell<Config>>,
+ process: Rc<RefCell<ProcessExecutor>>,
+ _process_guard: ProcessExecutorMockGuard,
+}
+
+fn fixtures() -> Fixtures {
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
+ let config = Rc::new(RefCell::new(make_config()));
+ let (process, _process_guard) = get_process_executor_mock(vec![], true, MockHandler::default());
+ Fixtures {
+ io,
+ config,
+ process,
+ _process_guard,
+ }
+}
+
+// Mirrors $this->getHttpDownloaderMock() with a single project-data expectation.
+fn http_mock_with_body(
+ url: &str,
+ body: &str,
+) -> (Rc<RefCell<HttpDownloader>>, HttpDownloaderMockGuard) {
+ get_http_downloader_mock(
+ vec![expect_full(url, None, 200, body, vec![String::new()])],
+ true,
+ HttpDownloaderMockHandler::default(),
+ )
+}
+
+fn provide_initialize_urls() -> Vec<(&'static str, &'static str)> {
+ vec![
+ (
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ ),
+ (
+ "http://gitlab.com/mygroup/myproject",
+ "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ ),
+ (
+ "git@gitlab.com:mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ ),
+ ]
+}
+
+// Mirrors testInitialize: returns the initialized driver together with the http
+// mock guard (kept alive by the caller so __assert_complete runs at scope end).
+fn do_test_initialize(
+ fixtures: &Fixtures,
+ url: &str,
+ api_url: &str,
+) -> (GitLabDriver, HttpDownloaderMockGuard) {
+ // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "issues_enabled": true,
+ "archived": false,
+ "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/myproject",
+ "web_url": "https://gitlab.com/mygroup/myproject"
+}"#;
+
+ let (http_downloader, guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
+ assert_eq!(
+ "mymaster",
+ driver.get_root_identifier().unwrap(),
+ "Root identifier is the default branch in GitLab"
+ );
+ assert_eq!(
+ "git@gitlab.com:mygroup/myproject.git",
+ driver.get_repository_url(),
+ "The repository URL is the SSH one by default"
+ );
+ assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url());
+
+ (driver, guard)
+}
+
+// Mirrors testInitializePublicProject.
+fn do_test_initialize_public_project(
+ fixtures: &Fixtures,
+ url: &str,
+ api_url: &str,
+) -> (GitLabDriver, HttpDownloaderMockGuard) {
+ // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "public",
+ "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/myproject",
+ "web_url": "https://gitlab.com/mygroup/myproject"
+}"#;
+
+ let (http_downloader, guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
+ assert_eq!(
+ "mymaster",
+ driver.get_root_identifier().unwrap(),
+ "Root identifier is the default branch in GitLab"
+ );
+ assert_eq!(
+ "https://gitlab.com/mygroup/myproject.git",
+ driver.get_repository_url(),
+ "The repository URL is the SSH one by default"
+ );
+ assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url());
+
+ (driver, guard)
+}
+
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_initialize() {
- todo!()
+ let fixtures = fixtures();
+ for (url, api_url) in provide_initialize_urls() {
+ let (_driver, _guard) = do_test_initialize(&fixtures, url, api_url);
+ }
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_initialize_public_project() {
- todo!()
+ let fixtures = fixtures();
+ for (url, api_url) in provide_initialize_urls() {
+ let (_driver, _guard) = do_test_initialize_public_project(&fixtures, url, api_url);
+ }
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_initialize_public_project_as_anonymous() {
- todo!()
+ let fixtures = fixtures();
+ for (url, api_url) in provide_initialize_urls() {
+ // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/myproject",
+ "web_url": "https://gitlab.com/mygroup/myproject"
+}"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
+ assert_eq!(
+ "mymaster",
+ driver.get_root_identifier().unwrap(),
+ "Root identifier is the default branch in GitLab"
+ );
+ assert_eq!(
+ "https://gitlab.com/mygroup/myproject.git",
+ driver.get_repository_url(),
+ "The repository URL is the SSH one by default"
+ );
+ assert_eq!("https://gitlab.com/mygroup/myproject", driver.get_url());
+ }
}
+/// Also support repositories over HTTP (TLS) and has a port number.
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_initialize_with_port_number() {
- todo!()
+ let fixtures = fixtures();
+ let domain = "gitlab.mycompany.com";
+ let port = "5443";
+ let namespace = "mygroup/myproject";
+ let url = format!("https://{}:{}/{}", domain, port, namespace);
+ // urlencode($namespace) replaces '/' with '%2F'.
+ let api_url = format!(
+ "https://{}:{}/api/v4/projects/{}",
+ domain, port, "mygroup%2Fmyproject"
+ );
+
+ // An incomplete single project API response payload.
+ // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
+ let project_data = format!(
+ r#"{{
+ "default_branch": "1.0.x",
+ "http_url_to_repo": "https://{0}:{1}/{2}.git",
+ "path": "myproject",
+ "path_with_namespace": "{2}",
+ "web_url": "https://{0}:{1}/{2}"
+}}"#,
+ domain, port, namespace
+ );
+
+ let (http_downloader, _guard) = http_mock_with_body(&api_url, &project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.clone()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
+ assert_eq!(
+ "1.0.x",
+ driver.get_root_identifier().unwrap(),
+ "Root identifier is the default branch in GitLab"
+ );
+ assert_eq!(
+ format!("{}.git", url),
+ driver.get_repository_url(),
+ "The repository URL is the SSH one by default"
+ );
+ assert_eq!(url, driver.get_url());
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + setAttribute (\\ReflectionProperty) not ported"]
fn test_invalid_support_data() {
- todo!()
+ let fixtures = fixtures();
+ let repo_url = "https://gitlab.com/mygroup/myproject";
+ let (mut driver, _init_guard) = do_test_initialize(
+ &fixtures,
+ repo_url,
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let mut branches: IndexMap<String, String> = IndexMap::new();
+ branches.insert("main".to_string(), "SOMESHA".to_string());
+ driver.__set_branches(Some(branches));
+ driver.__set_tags(Some(IndexMap::new()));
+
+ let (http_downloader, _guard) = get_http_downloader_mock(
+ vec![expect_full(
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/files/composer%2Ejson/raw?ref=SOMESHA",
+ None,
+ 200,
+ format!(r#"{{"support": "{}" }}"#, repo_url),
+ vec![String::new()],
+ )],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+ driver.set_http_downloader(http_downloader);
+
+ let data = driver.get_composer_information("main").unwrap().unwrap();
+
+ let source = data
+ .get("support")
+ .and_then(|v| v.as_array())
+ .and_then(|m| m.get("source"))
+ .and_then(|v| v.as_string())
+ .unwrap();
+ assert_eq!("https://gitlab.com/mygroup/myproject/-/tree/main", source);
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
fn test_get_dist() {
- todo!()
+ let fixtures = fixtures();
+ let (driver, _guard) = do_test_initialize(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363";
+ let mut expected: IndexMap<String, PhpMixed> = IndexMap::new();
+ expected.insert("type".to_string(), PhpMixed::String("zip".to_string()));
+ expected.insert(
+ "url".to_string(),
+ PhpMixed::String(format!(
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/archive.zip?sha={}",
+ reference
+ )),
+ );
+ expected.insert(
+ "reference".to_string(),
+ PhpMixed::String(reference.to_string()),
+ );
+ expected.insert("shasum".to_string(), PhpMixed::String(String::new()));
+
+ assert_eq!(Some(expected), driver.get_dist(reference));
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
fn test_get_source() {
- todo!()
+ let fixtures = fixtures();
+ let (driver, _guard) = do_test_initialize(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363";
+ let mut expected: IndexMap<String, PhpMixed> = IndexMap::new();
+ expected.insert("type".to_string(), PhpMixed::String("git".to_string()));
+ expected.insert(
+ "url".to_string(),
+ PhpMixed::String("git@gitlab.com:mygroup/myproject.git".to_string()),
+ );
+ expected.insert(
+ "reference".to_string(),
+ PhpMixed::String(reference.to_string()),
+ );
+
+ assert_eq!(expected, driver.get_source(reference));
}
#[test]
-#[ignore = "depends on testInitializePublicProject, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
fn test_get_source_given_public_project() {
- todo!()
+ let fixtures = fixtures();
+ let (driver, _guard) = do_test_initialize_public_project(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let reference = "c3ebdbf9cceddb82cd2089aaef8c7b992e536363";
+ let mut expected: IndexMap<String, PhpMixed> = IndexMap::new();
+ expected.insert("type".to_string(), PhpMixed::String("git".to_string()));
+ expected.insert(
+ "url".to_string(),
+ PhpMixed::String("https://gitlab.com/mygroup/myproject.git".to_string()),
+ );
+ expected.insert(
+ "reference".to_string(),
+ PhpMixed::String(reference.to_string()),
+ );
+
+ assert_eq!(expected, driver.get_source(reference));
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
fn test_get_tags() {
- todo!()
+ let fixtures = fixtures();
+ let (mut driver, _init_guard) = do_test_initialize(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let api_url =
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?per_page=100";
+
+ // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-tags
+ let tag_data = r#"[
+ {
+ "name": "v1.0.0",
+ "commit": {
+ "id": "092ed2c762bbae331e3f51d4a17f67310bf99a81",
+ "committed_date": "2012-05-28T04:42:42-07:00"
+ }
+ },
+ {
+ "name": "v2.0.0",
+ "commit": {
+ "id": "8e8f60b3ec86d63733db3bd6371117a758027ec6",
+ "committed_date": "2014-07-06T12:59:11.000+02:00"
+ }
+ }
+]"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, tag_data);
+ driver.set_http_downloader(http_downloader);
+
+ let mut expected: IndexMap<String, String> = IndexMap::new();
+ expected.insert(
+ "v1.0.0".to_string(),
+ "092ed2c762bbae331e3f51d4a17f67310bf99a81".to_string(),
+ );
+ expected.insert(
+ "v2.0.0".to_string(),
+ "8e8f60b3ec86d63733db3bd6371117a758027ec6".to_string(),
+ );
+
+ assert_eq!(expected, driver.get_tags().unwrap());
+ assert_eq!(expected, driver.get_tags().unwrap(), "Tags are cached");
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
+#[ignore = "Response::find_header_value passes a `(?i)`-prefixed (non-delimited) pattern that compile_php_pattern (Preg) mis-translates to an invalid regex (`(?s)?i)^link:...`), so get_next_page panics when parsing the Link header. Pre-existing Preg/Response porting bug, unrelated to GitLabDriver."]
fn test_get_paginated_refs() {
- todo!()
+ let fixtures = fixtures();
+ let (mut driver, _init_guard) = do_test_initialize(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-branches
+ let mut branch_data: Vec<PhpMixed> = vec![
+ branch_entry(
+ "mymaster",
+ "97eda36b5c1dd953a3792865c222d4e85e5f302e",
+ "2013-01-03T21:04:07.000+01:00",
+ ),
+ branch_entry(
+ "staging",
+ "502cffe49f136443f2059803f2e7192d1ac066cd",
+ "2013-03-09T16:35:23.000+01:00",
+ ),
+ ];
+ for _ in 0..98 {
+ branch_data.push(branch_entry(
+ "stagingdupe",
+ "502cffe49f136443f2059803f2e7192d1ac066cd",
+ "2013-03-09T16:35:23.000+01:00",
+ ));
+ }
+ let branch_data = shirabe::json::JsonFile::encode(&PhpMixed::List(branch_data));
+
+ let (http_downloader, _guard) = get_http_downloader_mock(
+ vec![
+ expect_full(
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/branches?per_page=100",
+ None,
+ 200,
+ branch_data.clone(),
+ vec!["Link: <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20>; rel=\"next\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=1&per_page=20>; rel=\"first\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=3&per_page=20>; rel=\"last\"".to_string()],
+ ),
+ expect_full(
+ "http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20",
+ None,
+ 200,
+ branch_data.clone(),
+ vec!["Link: <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=2&per_page=20>; rel=\"prev\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=1&per_page=20>; rel=\"first\", <http://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/tags?id=mygroup%2Fmyproject&page=3&per_page=20>; rel=\"last\"".to_string()],
+ ),
+ ],
+ true,
+ HttpDownloaderMockHandler::default(),
+ );
+ driver.set_http_downloader(http_downloader);
+
+ let mut expected: IndexMap<String, String> = IndexMap::new();
+ expected.insert(
+ "mymaster".to_string(),
+ "97eda36b5c1dd953a3792865c222d4e85e5f302e".to_string(),
+ );
+ expected.insert(
+ "staging".to_string(),
+ "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(),
+ );
+ expected.insert(
+ "stagingdupe".to_string(),
+ "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(),
+ );
+
+ assert_eq!(expected, driver.get_branches().unwrap());
+ assert_eq!(
+ expected,
+ driver.get_branches().unwrap(),
+ "Branches are cached"
+ );
+}
+
+fn branch_entry(name: &str, id: &str, committed_date: &str) -> PhpMixed {
+ let mut commit: IndexMap<String, PhpMixed> = IndexMap::new();
+ commit.insert("id".to_string(), PhpMixed::String(id.to_string()));
+ commit.insert(
+ "committed_date".to_string(),
+ PhpMixed::String(committed_date.to_string()),
+ );
+ let mut entry: IndexMap<String, PhpMixed> = IndexMap::new();
+ entry.insert("name".to_string(), PhpMixed::String(name.to_string()));
+ entry.insert("commit".to_string(), PhpMixed::Array(commit));
+ PhpMixed::Array(entry)
}
#[test]
-#[ignore = "depends on testInitialize, which needs HttpDownloaderMock + the IOInterface MockObject not ported"]
fn test_get_branches() {
- todo!()
+ let fixtures = fixtures();
+ let (mut driver, _init_guard) = do_test_initialize(
+ &fixtures,
+ "https://gitlab.com/mygroup/myproject",
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject",
+ );
+
+ let api_url =
+ "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject/repository/branches?per_page=100";
+
+ // @link http://doc.gitlab.com/ce/api/repositories.html#list-project-repository-branches
+ let branch_data = r#"[
+ {
+ "name": "mymaster",
+ "commit": {
+ "id": "97eda36b5c1dd953a3792865c222d4e85e5f302e",
+ "committed_date": "2013-01-03T21:04:07.000+01:00"
+ }
+ },
+ {
+ "name": "staging",
+ "commit": {
+ "id": "502cffe49f136443f2059803f2e7192d1ac066cd",
+ "committed_date": "2013-03-09T16:35:23.000+01:00"
+ }
+ }
+]"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, branch_data);
+ driver.set_http_downloader(http_downloader);
+
+ let mut expected: IndexMap<String, String> = IndexMap::new();
+ expected.insert(
+ "mymaster".to_string(),
+ "97eda36b5c1dd953a3792865c222d4e85e5f302e".to_string(),
+ );
+ expected.insert(
+ "staging".to_string(),
+ "502cffe49f136443f2059803f2e7192d1ac066cd".to_string(),
+ );
+
+ assert_eq!(expected, driver.get_branches().unwrap());
+ assert_eq!(
+ expected,
+ driver.get_branches().unwrap(),
+ "Branches are cached"
+ );
}
#[test]
-#[ignore]
fn test_supports() {
for (url, expected) in data_for_test_supports() {
let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(NullIO::new()));
@@ -147,31 +676,216 @@ fn data_for_test_supports() -> Vec<(&'static str, bool)> {
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_gitlab_sub_directory() {
- todo!()
+ let fixtures = fixtures();
+ let url = "https://mycompany.com/gitlab/mygroup/my-pro.ject";
+ let api_url = "https://mycompany.com/gitlab/api/v4/projects/mygroup%2Fmy-pro%2Eject";
+
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "http_url_to_repo": "https://gitlab.com/gitlab/mygroup/my-pro.ject",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/my-pro.ject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/my-pro.ject",
+ "web_url": "https://gitlab.com/gitlab/mygroup/my-pro.ject"
+}"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_gitlab_sub_group() {
- todo!()
+ let fixtures = fixtures();
+ let url = "https://gitlab.com/mygroup/mysubgroup/myproject";
+ let api_url = "https://gitlab.com/api/v4/projects/mygroup%2Fmysubgroup%2Fmyproject";
+
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "http_url_to_repo": "https://gitlab.com/mygroup/mysubgroup/my-pro.ject",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/mysubgroup/my-pro.ject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/mysubgroup/my-pro.ject",
+ "web_url": "https://gitlab.com/mygroup/mysubgroup/my-pro.ject"
+}"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_gitlab_sub_directory_sub_group() {
- todo!()
+ let fixtures = fixtures();
+ let url = "https://mycompany.com/gitlab/mygroup/mysubgroup/myproject";
+ let api_url = "https://mycompany.com/gitlab/api/v4/projects/mygroup%2Fmysubgroup%2Fmyproject";
+
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "http_url_to_repo": "https://mycompany.com/gitlab/mygroup/mysubgroup/my-pro.ject",
+ "ssh_url_to_repo": "git@mycompany.com:mygroup/mysubgroup/my-pro.ject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/mysubgroup/my-pro.ject",
+ "web_url": "https://mycompany.com/gitlab/mygroup/mysubgroup/my-pro.ject"
+}"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, project_data);
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+
+ assert_eq!(
+ api_url,
+ driver.get_api_url(),
+ "API URL is derived from the repository URL"
+ );
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_forwards_options() {
- todo!()
+ let fixtures = fixtures();
+ let mut ssl: IndexMap<String, PhpMixed> = IndexMap::new();
+ ssl.insert("verify_peer".to_string(), PhpMixed::Bool(false));
+ let mut options: IndexMap<String, PhpMixed> = IndexMap::new();
+ options.insert("ssl".to_string(), PhpMixed::Array(ssl));
+
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "http_url_to_repo": "https://gitlab.mycompany.local/mygroup/myproject",
+ "ssh_url_to_repo": "git@gitlab.mycompany.local:mygroup/myproject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/myproject",
+ "web_url": "https://gitlab.mycompany.local/mygroup/myproject"
+}"#;
+
+ let (http_downloader, _guard) = http_mock_with_body(
+ "https://gitlab.mycompany.local/api/v4/projects/mygroup%2Fmyproject",
+ project_data,
+ );
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert(
+ "url".to_string(),
+ PhpMixed::String("https://gitlab.mycompany.local/mygroup/myproject".to_string()),
+ );
+ repo_config.insert("options".to_string(), PhpMixed::Array(options));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ fixtures.config.clone(),
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
}
#[test]
-#[ignore = "HttpDownloaderMock (getHttpDownloaderMock) and the IOInterface MockObject are not ported"]
fn test_protocol_override_repository_url_generation() {
- todo!()
+ let fixtures = fixtures();
+ // @link http://doc.gitlab.com/ce/api/projects.html#get-single-project
+ let project_data = r#"{
+ "id": 17,
+ "default_branch": "mymaster",
+ "visibility": "private",
+ "http_url_to_repo": "https://gitlab.com/mygroup/myproject.git",
+ "ssh_url_to_repo": "git@gitlab.com:mygroup/myproject.git",
+ "last_activity_at": "2014-12-01T09:17:51.000+01:00",
+ "name": "My Project",
+ "name_with_namespace": "My Group / My Project",
+ "path": "myproject",
+ "path_with_namespace": "mygroup/myproject",
+ "web_url": "https://gitlab.com/mygroup/myproject"
+}"#;
+
+ let api_url = "https://gitlab.com/api/v4/projects/mygroup%2Fmyproject";
+ let url = "git@gitlab.com:mygroup/myproject";
+
+ let (http_downloader, _guard) = http_mock_with_body(api_url, project_data);
+
+ // clone $this->config and merge gitlab-protocol => http.
+ let mut config = make_config();
+ let mut top: IndexMap<String, PhpMixed> = IndexMap::new();
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "gitlab-protocol".to_string(),
+ PhpMixed::String("http".to_string()),
+ );
+ top.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.merge(&top, Config::SOURCE_UNKNOWN);
+ let config = Rc::new(RefCell::new(config));
+
+ let mut repo_config: IndexMap<String, PhpMixed> = IndexMap::new();
+ repo_config.insert("url".to_string(), PhpMixed::String(url.to_string()));
+ let mut driver = GitLabDriver::new(
+ repo_config,
+ fixtures.io.clone(),
+ config,
+ http_downloader,
+ fixtures.process.clone(),
+ );
+ driver.initialize().unwrap();
+ assert_eq!(
+ "https://gitlab.com/mygroup/myproject.git",
+ driver.get_repository_url(),
+ "Repository URL matches config request for http not git"
+ );
}
diff --git a/crates/shirabe/tests/util/git_test.rs b/crates/shirabe/tests/util/git_test.rs
index 40ca9da..6d615c5 100644
--- a/crates/shirabe/tests/util/git_test.rs
+++ b/crates/shirabe/tests/util/git_test.rs
@@ -1,55 +1,489 @@
//! ref: composer/tests/Composer/Test/Util/GitTest.php
-// These mock IO/Config/ProcessExecutor to drive Git::runCommand and mirror syncing; mocking
-// is not available here.
+use std::cell::RefCell;
+use std::rc::Rc;
-#[allow(dead_code)]
-fn set_up() {
- // Builds mocked IO/Config/ProcessExecutor/Filesystem and a real Git; mocking is not available.
- todo!()
+use indexmap::IndexMap;
+use shirabe::config::Config;
+use shirabe::io::IOInterface;
+use shirabe::util::filesystem::Filesystem;
+use shirabe::util::git::Git;
+use shirabe::util::process_executor::{MockExpectation, MockHandler, ProcessExecutor};
+use shirabe_php_shim::{PhpMixed, RuntimeException};
+
+use crate::config_stub::ConfigStubBuilder;
+use crate::io_stub::IOStub;
+use crate::process_executor_mock::{cmd, cmd_full, get_process_executor_mock};
+
+// PHP's `commandCallable` returns a bare string (`'git command'`); Rust's `run_command`
+// flattens each callable to a `Vec<String>` and hands it to `execute_args`, which always
+// builds a `PhpMixed::List`. So the single-token string command becomes a one-element list,
+// and the corresponding process expectation is a one-element list as well.
+fn build_git(io: IOStub, config: Config, process: Rc<RefCell<ProcessExecutor>>) -> Git {
+ let io: Rc<RefCell<dyn IOInterface>> = Rc::new(RefCell::new(io));
+ let config = Rc::new(RefCell::new(config));
+ let fs = Rc::new(RefCell::new(Filesystem::new(None)));
+ Git::new(io, config, process, fs)
}
+fn mock_config(protocol: &str) -> Config {
+ ConfigStubBuilder::new()
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .with(
+ "github-protocols",
+ PhpMixed::List(vec![PhpMixed::String(protocol.to_string())]),
+ )
+ .build()
+}
+
+fn mock_sync_mirror_config() -> Config {
+ ConfigStubBuilder::new()
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with(
+ "github-protocols",
+ PhpMixed::List(vec![PhpMixed::String("https".to_string())]),
+ )
+ .build()
+}
+
+// publicGithubNoCredentialsProvider: ['ssh', 'git@github.com:acme/repo']
#[test]
-#[ignore = "requires mocked Config (getMockBuilder Config) and getProcessExecutorMock with expects() command expectations; no mocking infrastructure exists"]
-fn test_run_command_public_git_hub_repository_not_initial_clone() {
- todo!()
+fn test_run_command_public_git_hub_repository_not_initial_clone_ssh() {
+ let expected_url = "git@github.com:acme/repo";
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ assert_eq!(expected_url, url);
+ vec!["git command".to_string()]
+ });
+
+ let config = mock_config("ssh");
+
+ let (process, _guard) =
+ get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default());
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ )
+ .unwrap();
}
+// publicGithubNoCredentialsProvider: ['https', 'https://github.com/acme/repo']
#[test]
-#[ignore = "requires mocked Config and getProcessExecutorMock with expects() command expectations; no mocking infrastructure exists"]
+fn test_run_command_public_git_hub_repository_not_initial_clone_https() {
+ let expected_url = "https://github.com/acme/repo";
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ assert_eq!(expected_url, url);
+ vec!["git command".to_string()]
+ });
+
+ let config = mock_config("https");
+
+ let (process, _guard) =
+ get_process_executor_mock(vec![cmd(vec!["git command"])], true, MockHandler::default());
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ )
+ .unwrap();
+}
+
+#[test]
+#[ignore = "reaches Git::throw_exception -> Url::sanitize, whose preg pattern fails to parse in the regex crate (preg shim bug, unrelated to this test); production Url::sanitize must be fixed first"]
fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_without_authentication()
{
- todo!()
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(|url: &str| {
+ assert_eq!("https://github.com/acme/repo", url);
+ vec!["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(vec!["git", "--version"], 0, "", ""),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git.__run_command(
+ vec![command_callable],
+ "https://github.com/acme/repo",
+ None,
+ true,
+ None,
+ );
+
+ let err = result.expect_err("expected a RuntimeException");
+ assert!(err.downcast_ref::<RuntimeException>().is_some());
+}
+
+// privateGithubWithCredentialsProvider helper.
+fn run_command_private_github_with_authentication(
+ git_url: &str,
+ protocol: &str,
+ git_hub_token: &str,
+ expected_url: &str,
+ expected_failures_before_success: usize,
+) {
+ let expected_url_owned = expected_url.to_string();
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ if url != expected_url_owned {
+ return vec!["git command failing".to_string()];
+ }
+ vec!["git command ok".to_string()]
+ });
+
+ let config = mock_config(protocol);
+
+ let mut expected_calls: Vec<MockExpectation> = Vec::new();
+ for _ in 0..expected_failures_before_success {
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ }
+ expected_calls.push(cmd_full(vec!["git command ok"], 0, "", ""));
+
+ let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default());
+
+ let mut auth: IndexMap<String, Option<String>> = IndexMap::new();
+ auth.insert("username".to_string(), Some("token".to_string()));
+ auth.insert("password".to_string(), Some(git_hub_token.to_string()));
+
+ let io = IOStub::new()
+ .with_is_interactive(false)
+ .with_has_authentication(true)
+ .with_get_authentication(auth);
+
+ let mut git = build_git(io, config, process);
+
+ git.__run_command(vec![command_callable], git_url, None, true, None)
+ .unwrap();
}
#[test]
-#[ignore = "requires getProcessExecutorMock and mocked IO (getMockBuilder IOInterface with hasAuthentication/getAuthentication/isInteractive expectations); no mocking infrastructure exists"]
-fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication()
+fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication_ssh()
{
- todo!()
+ run_command_private_github_with_authentication(
+ "git@github.com:acme/repo.git",
+ "ssh",
+ "MY_GITHUB_TOKEN",
+ "https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git",
+ 1,
+ );
}
#[test]
-#[ignore = "requires getProcessExecutorMock and mocked IO/Config (getMockBuilder with hasAuthentication/getAuthentication/isInteractive expectations); no mocking infrastructure exists"]
-fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication()
+fn test_run_command_private_git_hub_repository_not_initial_clone_not_interactive_with_authentication_https()
{
- todo!()
+ run_command_private_github_with_authentication(
+ "https://github.com/acme/repo",
+ "https",
+ "MY_GITHUB_TOKEN",
+ "https://token:MY_GITHUB_TOKEN@github.com/acme/repo.git",
+ 2,
+ );
+}
+
+// privateBitbucketWithCredentialsProvider helper.
+fn run_command_private_bitbucket_with_authentication(
+ git_url: &str,
+ bitbucket_token: Option<&str>,
+ expected_url: &str,
+ expected_failures_before_success: usize,
+ bitbucket_git_auth_calls: usize,
+) {
+ let expected_url_owned = expected_url.to_string();
+ let command_callable: Box<dyn Fn(&str) -> Vec<String>> = Box::new(move |url: &str| {
+ if url != expected_url_owned {
+ return vec!["git command failing".to_string()];
+ }
+ vec!["git command ok".to_string()]
+ });
+
+ let config = ConfigStubBuilder::new()
+ .with(
+ "gitlab-domains",
+ PhpMixed::List(vec![PhpMixed::String("gitlab.com".to_string())]),
+ )
+ .with(
+ "github-domains",
+ PhpMixed::List(vec![PhpMixed::String("github.com".to_string())]),
+ )
+ .build();
+
+ let mut expected_calls: Vec<MockExpectation> = Vec::new();
+ for _ in 0..expected_failures_before_success {
+ expected_calls.push(cmd_full(vec!["git command failing"], 1, "", ""));
+ }
+ if bitbucket_git_auth_calls > 0 {
+ for _ in 0..bitbucket_git_auth_calls {
+ expected_calls.push(cmd_full(
+ vec!["git", "config", "bitbucket.accesstoken"],
+ 1,
+ "",
+ "",
+ ));
+ }
+ }
+ expected_calls.push(cmd_full(vec!["git command ok"], 0, "", ""));
+
+ let (process, _guard) = get_process_executor_mock(expected_calls, true, MockHandler::default());
+
+ let mut io = IOStub::new().with_is_interactive(false);
+ if let Some(token) = bitbucket_token {
+ let mut auth: IndexMap<String, Option<String>> = IndexMap::new();
+ auth.insert("username".to_string(), Some("token".to_string()));
+ auth.insert("password".to_string(), Some(token.to_string()));
+ io = io
+ .with_has_authentication(true)
+ .with_get_authentication(auth);
+ }
+
+ let mut git = build_git(io, config, process);
+
+ git.__run_command(vec![command_callable], git_url, None, true, None)
+ .unwrap();
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_ssh_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "git@bitbucket.org:acme/repo.git",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_git_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ Some("MY_BITBUCKET_TOKEN"),
+ "https://token:MY_BITBUCKET_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_ssh_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "git@bitbucket.org:acme/repo.git",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 0,
+ 0,
+ );
}
#[test]
-#[ignore = "requires getProcessExecutorMock, getHttpDownloaderMock and mocked IO/Config with askConfirmation/askAndHideAnswer/setAuthentication expectations; no mocking infrastructure exists"]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 1,
+ 1,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_https_git_no_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ None,
+ "git@bitbucket.org:acme/repo.git",
+ 1,
+ 1,
+ );
+}
+
+#[test]
+#[ignore = "after the first failing git command, Bitbucket::new constructs a real HttpDownloader -> CurlDownloader::new -> curl_multi_init(), which is todo!() in the curl shim; needs the curl shim or an injected HttpDownloader mock on Git"]
+fn test_run_command_private_bitbucket_repository_not_initial_clone_not_interactive_with_authentication_atat_token()
+ {
+ run_command_private_bitbucket_with_authentication(
+ "https://bitbucket.org/acme/repo.git",
+ Some("ATAT_BITBUCKET_API_TOKEN"),
+ "https://x-bitbucket-api-token-auth:ATAT_BITBUCKET_API_TOKEN@bitbucket.org/acme/repo.git",
+ 1,
+ 0,
+ );
+}
+
+#[test]
+#[ignore = "interactive Bitbucket OAuth flow needs IOStub::askConfirmation/askAndHideAnswer/setAuthentication stateful callbacks and getHttpDownloaderMock; IOStub only supports fixed willReturn values, not the willReturnCallback-based stateful initial_config mutation this test relies on"]
fn test_run_command_private_bitbucket_repository_not_initial_clone_interactive_with_oauth() {
todo!()
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and mocked Config/Filesystem (getMockBuilder removeDirectory); no mocking infrastructure exists"]
fn test_sync_mirror_sanitizes_url_after_initial_clone() {
- todo!()
+ let non_existent_dir = format!(
+ "{}/composer-test-nonexistent-{}",
+ std::env::temp_dir().display(),
+ rand_hex()
+ );
+
+ let config = mock_sync_mirror_config();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full(
+ vec![
+ "git",
+ "clone",
+ "--mirror",
+ "--",
+ "https://user:secret@example.com/repo.git",
+ &non_existent_dir,
+ ],
+ 0,
+ "",
+ "",
+ ),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git
+ .sync_mirror(
+ "https://user:secret@example.com/repo.git",
+ &non_existent_dir,
+ )
+ .unwrap();
+
+ assert!(result);
}
#[test]
-#[ignore = "requires getProcessExecutorMock with expects() command expectations and mocked Config; no mocking infrastructure exists"]
+#[ignore = "the failed-update branch reaches Git::throw_exception -> Url::sanitize, whose preg pattern fails to parse in the regex crate (preg shim bug, unrelated to this test); production Url::sanitize must be fixed first"]
fn test_sync_mirror_sanitizes_url_even_after_failed_update() {
- todo!()
+ let dir = std::env::temp_dir().display().to_string();
+
+ let config = mock_sync_mirror_config();
+
+ let (process, _guard) = get_process_executor_mock(
+ vec![
+ cmd_full(vec!["git", "rev-parse", "--git-dir"], 0, ".\n", ""),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://user:secret@example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ cmd_full(
+ vec!["git", "remote", "update", "--prune", "origin"],
+ 1,
+ "",
+ "",
+ ),
+ cmd_full(vec!["git", "--version"], 0, "", ""),
+ cmd_full(vec!["git", "remote", "-v"], 0, "", ""),
+ cmd_full(
+ vec![
+ "git",
+ "remote",
+ "set-url",
+ "origin",
+ "--",
+ "https://example.com/repo.git",
+ ],
+ 0,
+ "",
+ "",
+ ),
+ ],
+ true,
+ MockHandler::default(),
+ );
+
+ let mut git = build_git(IOStub::new(), config, process);
+
+ let result = git
+ .sync_mirror("https://user:secret@example.com/repo.git", &dir)
+ .unwrap();
+
+ assert!(!result);
+}
+
+fn rand_hex() -> String {
+ use std::time::{SystemTime, UNIX_EPOCH};
+ let nanos = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap()
+ .as_nanos();
+ format!("{:016x}", nanos as u64)
}
diff --git a/crates/shirabe/tests/util/url_test.rs b/crates/shirabe/tests/util/url_test.rs
index 4adb4f2..3691d26 100644
--- a/crates/shirabe/tests/util/url_test.rs
+++ b/crates/shirabe/tests/util/url_test.rs
@@ -143,7 +143,6 @@ fn dist_refs_provider() -> Vec<(
}
#[test]
-#[ignore]
fn test_sanitize() {
for (expected, url) in sanitize_provider() {
assert_eq!(expected, Url::sanitize(url.to_string()));