Skip to content

Commit 4dd06ec

Browse files
committed
improve coverage
1 parent 8c43cfa commit 4dd06ec

File tree

3 files changed

+258
-1
lines changed

3 files changed

+258
-1
lines changed

src/cli/mod.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,125 @@ fn resolve_worktree_name(name: Option<String>, repo: &Repo) -> color_eyre::Resul
165165

166166
Ok(components.join("/"))
167167
}
168+
169+
#[cfg(test)]
170+
mod tests {
171+
use super::*;
172+
use std::{env, fs, path::Path, process::Command as StdCommand};
173+
174+
use color_eyre::eyre::{self, WrapErr};
175+
176+
use tempfile::TempDir;
177+
178+
struct DirGuard {
179+
original: std::path::PathBuf,
180+
}
181+
182+
impl DirGuard {
183+
fn change_to(path: &Path) -> color_eyre::Result<Self> {
184+
let original = env::current_dir().wrap_err("failed to capture current directory")?;
185+
env::set_current_dir(path)
186+
.wrap_err_with(|| eyre::eyre!("failed to switch to `{}`", path.display()))?;
187+
Ok(Self { original })
188+
}
189+
}
190+
191+
impl Drop for DirGuard {
192+
fn drop(&mut self) {
193+
let _ = env::set_current_dir(&self.original);
194+
}
195+
}
196+
197+
fn init_git_repo(dir: &TempDir) -> color_eyre::Result<()> {
198+
run(dir, ["git", "init"])?;
199+
fs::write(dir.path().join("README.md"), "test")?;
200+
run(dir, ["git", "add", "README.md"])?;
201+
run(
202+
dir,
203+
[
204+
"git",
205+
"-c",
206+
"user.name=Test",
207+
"-c",
208+
"user.email=test@example.com",
209+
"commit",
210+
"-m",
211+
"Initial commit",
212+
],
213+
)?;
214+
Ok(())
215+
}
216+
217+
fn run(dir: &TempDir, cmd: impl IntoIterator<Item = &'static str>) -> color_eyre::Result<()> {
218+
let mut iter = cmd.into_iter();
219+
let program = iter.next().expect("command must not be empty");
220+
let status = StdCommand::new(program)
221+
.current_dir(dir.path())
222+
.args(iter)
223+
.status()
224+
.wrap_err_with(|| eyre::eyre!("failed to run `{program}`"))?;
225+
226+
if !status.success() {
227+
return Err(eyre::eyre!("`{program}` exited with status {status}"));
228+
}
229+
230+
Ok(())
231+
}
232+
233+
#[test]
234+
fn resolve_worktree_name_returns_cli_argument_when_present() -> color_eyre::Result<()> {
235+
let repo_dir = TempDir::new()?;
236+
init_git_repo(&repo_dir)?;
237+
let repo = Repo::discover_from(repo_dir.path())?;
238+
239+
let resolved = resolve_worktree_name(Some("feature/test".into()), &repo)?;
240+
assert_eq!(resolved, "feature/test");
241+
242+
Ok(())
243+
}
244+
245+
#[test]
246+
fn resolve_worktree_name_infers_from_cwd_inside_worktree() -> color_eyre::Result<()> {
247+
let repo_dir = TempDir::new()?;
248+
init_git_repo(&repo_dir)?;
249+
let repo = Repo::discover_from(repo_dir.path())?;
250+
let worktree_dir = repo.ensure_worktrees_dir()?.join("feature/nested");
251+
fs::create_dir_all(&worktree_dir)?;
252+
253+
let _guard = DirGuard::change_to(&worktree_dir)?;
254+
let resolved = resolve_worktree_name(None, &repo)?;
255+
assert_eq!(resolved, "feature/nested");
256+
257+
Ok(())
258+
}
259+
260+
#[test]
261+
fn resolve_worktree_name_requires_running_inside_worktree() -> color_eyre::Result<()> {
262+
let repo_dir = TempDir::new()?;
263+
init_git_repo(&repo_dir)?;
264+
let repo = Repo::discover_from(repo_dir.path())?;
265+
let _guard = DirGuard::change_to(repo.root())?;
266+
267+
let err = resolve_worktree_name(None, &repo).unwrap_err();
268+
assert!(err.to_string().contains("must be run from inside"));
269+
270+
Ok(())
271+
}
272+
273+
#[test]
274+
fn resolve_worktree_name_rejects_rsworktree_root() -> color_eyre::Result<()> {
275+
let repo_dir = TempDir::new()?;
276+
init_git_repo(&repo_dir)?;
277+
let repo = Repo::discover_from(repo_dir.path())?;
278+
let worktrees_dir = repo.ensure_worktrees_dir()?;
279+
let _guard = DirGuard::change_to(&worktrees_dir)?;
280+
281+
let err = resolve_worktree_name(None, &repo).unwrap_err();
282+
assert!(
283+
err.to_string()
284+
.contains("Run `rsworktree pr-github` from inside")
285+
);
286+
287+
Ok(())
288+
}
289+
}

src/commands/cd/mod.rs

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ pub(crate) fn shell_command() -> (String, Vec<String>) {
7979
#[cfg(test)]
8080
mod tests {
8181
use super::*;
82-
use std::{fs, process::Command as StdCommand};
82+
use std::{
83+
env, fs,
84+
process::Command as StdCommand,
85+
sync::{Mutex, OnceLock},
86+
};
8387

8488
use tempfile::TempDir;
8589

@@ -147,4 +151,88 @@ mod tests {
147151
let command = CdCommand::new("missing".into(), true);
148152
assert!(command.execute(&repo).is_err());
149153
}
154+
155+
struct EnvGuard {
156+
key: &'static str,
157+
previous: Option<std::ffi::OsString>,
158+
had_value: bool,
159+
}
160+
161+
impl EnvGuard {
162+
fn set(key: &'static str, value: &str) -> Self {
163+
let previous = env::var_os(key);
164+
unsafe {
165+
env::set_var(key, value);
166+
}
167+
Self {
168+
key,
169+
previous,
170+
had_value: true,
171+
}
172+
}
173+
174+
fn remove(key: &'static str) -> Self {
175+
let previous = env::var_os(key);
176+
unsafe {
177+
env::remove_var(key);
178+
}
179+
Self {
180+
key,
181+
previous,
182+
had_value: false,
183+
}
184+
}
185+
}
186+
187+
impl Drop for EnvGuard {
188+
fn drop(&mut self) {
189+
if let Some(value) = self.previous.take() {
190+
unsafe {
191+
env::set_var(self.key, value);
192+
}
193+
} else if self.had_value {
194+
unsafe {
195+
env::remove_var(self.key);
196+
}
197+
}
198+
}
199+
}
200+
201+
fn env_lock() -> &'static Mutex<()> {
202+
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
203+
LOCK.get_or_init(|| Mutex::new(()))
204+
}
205+
206+
#[test]
207+
fn shell_command_prefers_override_env() {
208+
let _lock = env_lock().lock().unwrap();
209+
let _shell_guard = EnvGuard::remove("SHELL");
210+
let _override_guard = EnvGuard::set(SHELL_OVERRIDE_ENV, "/custom/shell");
211+
212+
let (program, args) = shell_command();
213+
assert_eq!(program, "/custom/shell");
214+
assert!(args.is_empty());
215+
}
216+
217+
#[test]
218+
fn shell_command_uses_shell_env_when_override_missing() {
219+
let _lock = env_lock().lock().unwrap();
220+
let _override_guard = EnvGuard::remove(SHELL_OVERRIDE_ENV);
221+
let _shell_guard = EnvGuard::set("SHELL", "/bin/bash");
222+
223+
let (program, args) = shell_command();
224+
assert_eq!(program, "/bin/bash");
225+
assert_eq!(args, vec![String::from("-i")]);
226+
}
227+
228+
#[test]
229+
fn shell_command_falls_back_to_default() {
230+
let _lock = env_lock().lock().unwrap();
231+
let _override_guard = EnvGuard::set(SHELL_OVERRIDE_ENV, "");
232+
let _shell_guard = EnvGuard::set("SHELL", "");
233+
234+
let (program, args) = shell_command();
235+
assert_eq!(program, "/bin/sh");
236+
assert_eq!(args, vec![String::from("-i")]);
237+
}
150238
}

src/commands/pr_github/mod.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,53 @@ mod tests {
353353

354354
use tempfile::TempDir;
355355

356+
#[test]
357+
fn metadata_flag_allows_known_noninteractive_values() {
358+
for flag in [
359+
"--fill",
360+
"-f",
361+
"--fill-first",
362+
"--fill-verbose",
363+
"--web",
364+
"-w",
365+
"--title",
366+
"--body",
367+
"--body-file",
368+
"--title=Ready",
369+
"--body=Looks good",
370+
"--body-file=notes.md",
371+
] {
372+
assert!(
373+
metadata_flag_allows_noninteractive(flag),
374+
"flag `{flag}` should be accepted"
375+
);
376+
}
377+
}
378+
379+
#[test]
380+
fn metadata_flag_allows_noninteractive_rejects_positional_separator_and_plain_args() {
381+
for flag in ["--", "--reviewer", "ready"] {
382+
assert!(
383+
!metadata_flag_allows_noninteractive(flag),
384+
"flag `{flag}` should be rejected"
385+
);
386+
}
387+
}
388+
389+
#[test]
390+
fn format_command_quotes_arguments_with_special_characters() {
391+
let command = format_command(
392+
"gh",
393+
&[
394+
"pr".into(),
395+
"create".into(),
396+
"--title".into(),
397+
"Ready for review".into(),
398+
],
399+
);
400+
assert_eq!(command, "gh pr create --title 'Ready for review'");
401+
}
402+
356403
#[derive(Debug, Default)]
357404
struct MockCommandRunner {
358405
responses: VecDeque<color_eyre::Result<CommandOutput>>,

0 commit comments

Comments
 (0)