use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use crate::error::Result;
use super::client::{CommandExecutor, CommandOutput};
#[derive(Debug, Clone)]
pub struct CommandCall {
pub program: String,
pub args: Vec<String>,
pub dir: Option<String>,
}
#[derive(Debug, Clone)]
pub struct MockCommandExecutor {
responses: Arc<Mutex<HashMap<String, CommandOutput>>>,
calls: Arc<Mutex<Vec<CommandCall>>>,
default_response: CommandOutput,
}
impl Default for MockCommandExecutor {
fn default() -> Self {
Self::new()
}
}
impl MockCommandExecutor {
pub fn new() -> Self {
Self {
responses: Arc::new(Mutex::new(HashMap::new())),
calls: Arc::new(Mutex::new(Vec::new())),
default_response: CommandOutput::failure("command not configured"),
}
}
pub fn with_default_response(mut self, output: CommandOutput) -> Self {
self.default_response = output;
self
}
pub fn on_command(&self, program: &str, args: &[&str], output: CommandOutput) {
let key = self.make_key(program, args);
self.responses.lock().unwrap().insert(key, output);
}
pub fn calls(&self) -> Vec<CommandCall> {
self.calls.lock().unwrap().clone()
}
pub fn was_called(&self, program: &str, args: &[&str]) -> bool {
let calls = self.calls.lock().unwrap();
calls.iter().any(|c| {
c.program == program && c.args.iter().map(|s| s.as_str()).collect::<Vec<_>>() == args
})
}
pub fn call_count(&self, program: &str) -> usize {
let calls = self.calls.lock().unwrap();
calls.iter().filter(|c| c.program == program).count()
}
pub fn clear_calls(&self) {
self.calls.lock().unwrap().clear();
}
fn make_key(&self, program: &str, args: &[&str]) -> String {
std::iter::once(program)
.chain(args.iter().copied())
.collect::<Vec<_>>()
.join(" ")
}
fn record_call(&self, program: &str, args: &[&str], dir: Option<&str>) {
self.calls.lock().unwrap().push(CommandCall {
program: program.to_string(),
args: args.iter().map(|s| s.to_string()).collect(),
dir: dir.map(|s| s.to_string()),
});
}
}
impl CommandExecutor for MockCommandExecutor {
fn execute(&self, program: &str, args: &[&str]) -> Result<CommandOutput> {
self.record_call(program, args, None);
let key = self.make_key(program, args);
let responses = self.responses.lock().unwrap();
Ok(responses
.get(&key)
.cloned()
.unwrap_or(self.default_response.clone()))
}
fn execute_in_dir(&self, program: &str, args: &[&str], dir: &str) -> Result<CommandOutput> {
self.record_call(program, args, Some(dir));
let key = self.make_key(program, args);
let responses = self.responses.lock().unwrap();
Ok(responses
.get(&key)
.cloned()
.unwrap_or(self.default_response.clone()))
}
}
pub struct MockScenarioBuilder {
executor: MockCommandExecutor,
}
impl Default for MockScenarioBuilder {
fn default() -> Self {
Self::new()
}
}
impl MockScenarioBuilder {
pub fn new() -> Self {
Self {
executor: MockCommandExecutor::new(),
}
}
pub fn gh_available(self) -> Self {
self.executor.on_command(
"gh",
&["--version"],
CommandOutput::success("gh version 2.40.0"),
);
self.executor.on_command(
"gh",
&["auth", "status"],
CommandOutput::success("Logged in"),
);
self
}
pub fn gh_not_available(self) -> Self {
self.executor.on_command(
"gh",
&["--version"],
CommandOutput::failure("command not found: gh"),
);
self
}
pub fn gh_not_authenticated(self) -> Self {
self.executor.on_command(
"gh",
&["--version"],
CommandOutput::success("gh version 2.40.0"),
);
self.executor.on_command(
"gh",
&["auth", "status"],
CommandOutput::failure("You are not logged into any GitHub hosts. Run gh auth login"),
);
self
}
pub fn with_pr(self, branch: &str, json: &str) -> Self {
self.executor.on_command(
"gh",
&[
"pr",
"view",
branch,
"--json",
"number,title,url,state,baseRefName,mergeCommit,mergedAt",
],
CommandOutput::success(json),
);
self
}
pub fn with_no_pr(self, branch: &str) -> Self {
self.executor.on_command(
"gh",
&[
"pr",
"view",
branch,
"--json",
"number,title,url,state,baseRefName,mergeCommit,mergedAt",
],
CommandOutput::failure("no pull requests found for branch \"branch\""),
);
self
}
pub fn with_auth_error(self, branch: &str) -> Self {
self.executor.on_command(
"gh",
&[
"pr",
"view",
branch,
"--json",
"number,title,url,state,baseRefName,mergeCommit,mergedAt",
],
CommandOutput::failure("To get started with GitHub CLI, please run: gh auth login"),
);
self
}
pub fn with_merge_commit(self, sha: &str, parent_count: usize) -> Self {
let parents = (0..parent_count)
.map(|i| format!("parent abc{:03}", i))
.collect::<Vec<_>>()
.join("\n");
let output = format!(
"tree def456\n{}\nauthor Test <test@example.com>\ncommitter Test <test@example.com>\n\nMerge commit",
parents
);
self.executor.on_command(
"git",
&["cat-file", "-p", sha],
CommandOutput::success(output),
);
self
}
pub fn with_remote_delete_success(self, branch: &str) -> Self {
self.executor.on_command(
"git",
&["push", "origin", "--delete", branch],
CommandOutput::success(""),
);
self
}
pub fn with_remote_already_deleted(self, branch: &str) -> Self {
self.executor.on_command(
"git",
&["push", "origin", "--delete", branch],
CommandOutput::failure("error: unable to delete 'branch': remote ref does not exist"),
);
self
}
pub fn with_remote_delete_failure(self, branch: &str, error: &str) -> Self {
self.executor.on_command(
"git",
&["push", "origin", "--delete", branch],
CommandOutput::failure(error),
);
self
}
pub fn build(self) -> MockCommandExecutor {
self.executor
}
}
pub mod fixtures {
pub fn open_pr(number: u64, _branch: &str) -> String {
format!(
r#"{{"number":{},"title":"feat: add feature","url":"https://github.com/owner/repo/pull/{}","state":"OPEN","baseRefName":"main","mergeCommit":null,"mergedAt":null}}"#,
number, number
)
}
pub fn merged_pr(number: u64, merge_commit: &str) -> String {
format!(
r#"{{"number":{},"title":"feat: merged feature","url":"https://github.com/owner/repo/pull/{}","state":"MERGED","baseRefName":"main","mergeCommit":{{"oid":"{}"}},"mergedAt":"2024-01-01T00:00:00Z"}}"#,
number, number, merge_commit
)
}
pub fn merged_pr_rebase(number: u64) -> String {
format!(
r#"{{"number":{},"title":"feat: rebased feature","url":"https://github.com/owner/repo/pull/{}","state":"MERGED","baseRefName":"main","mergeCommit":null,"mergedAt":"2024-01-01T00:00:00Z"}}"#,
number, number
)
}
pub fn closed_pr(number: u64) -> String {
format!(
r#"{{"number":{},"title":"wip: abandoned","url":"https://github.com/owner/repo/pull/{}","state":"CLOSED","baseRefName":"main","mergeCommit":null,"mergedAt":null}}"#,
number, number
)
}
pub fn pr_with_japanese_title(number: u64) -> String {
format!(
r#"{{"number":{},"title":"feat: 日本語タイトル","url":"https://github.com/owner/repo/pull/{}","state":"OPEN","baseRefName":"main","mergeCommit":null,"mergedAt":null}}"#,
number, number
)
}
pub fn pr_with_develop_base(number: u64) -> String {
format!(
r#"{{"number":{},"title":"feat: develop branch","url":"https://github.com/owner/repo/pull/{}","state":"OPEN","baseRefName":"develop","mergeCommit":null,"mergedAt":null}}"#,
number, number
)
}
}