Skip to content

Commit 3d041ff

Browse files
committed
improve test coverage for cli, editor preference, and open_editor
1 parent c34cab8 commit 3d041ff

File tree

3 files changed

+380
-0
lines changed

3 files changed

+380
-0
lines changed

src/cli/mod.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,4 +376,125 @@ mod tests {
376376

377377
Ok(())
378378
}
379+
380+
#[test]
381+
fn parses_create_command_with_base() {
382+
let cli = Cli::try_parse_from(["rsworktree", "create", "feature/test", "--base", "develop"])
383+
.expect("create with base should parse");
384+
match cli.command {
385+
Commands::Create(args) => {
386+
assert_eq!(args.name, "feature/test");
387+
assert_eq!(args.base, Some("develop".into()));
388+
}
389+
_ => panic!("expected Create command"),
390+
}
391+
}
392+
393+
#[test]
394+
fn parses_cd_command_with_print_flag() {
395+
let cli = Cli::try_parse_from(["rsworktree", "cd", "my-worktree", "--print"])
396+
.expect("cd with print should parse");
397+
match cli.command {
398+
Commands::Cd(args) => {
399+
assert_eq!(args.name, "my-worktree");
400+
assert!(args.print);
401+
}
402+
_ => panic!("expected Cd command"),
403+
}
404+
}
405+
406+
#[test]
407+
fn parses_rm_command_with_force_flag() {
408+
let cli = Cli::try_parse_from(["rsworktree", "rm", "old-worktree", "--force"])
409+
.expect("rm with force should parse");
410+
match cli.command {
411+
Commands::Rm(args) => {
412+
assert_eq!(args.name, "old-worktree");
413+
assert!(args.force);
414+
}
415+
_ => panic!("expected Rm command"),
416+
}
417+
}
418+
419+
#[test]
420+
fn parses_pr_github_with_all_flags() {
421+
let cli = Cli::try_parse_from([
422+
"rsworktree",
423+
"pr-github",
424+
"my-feature",
425+
"--no-push",
426+
"--draft",
427+
"--fill",
428+
"--web",
429+
"--remote",
430+
"upstream",
431+
"--reviewer",
432+
"alice",
433+
"--reviewer",
434+
"bob",
435+
"--",
436+
"--label",
437+
"bug",
438+
])
439+
.expect("pr-github with all flags should parse");
440+
match cli.command {
441+
Commands::PrGithub(args) => {
442+
assert_eq!(args.name, Some("my-feature".into()));
443+
assert!(args.no_push);
444+
assert!(args.draft);
445+
assert!(args.fill);
446+
assert!(args.web);
447+
assert_eq!(args.remote, "upstream");
448+
assert_eq!(args.reviewers, vec!["alice", "bob"]);
449+
assert_eq!(args.extra, vec!["--label", "bug"]);
450+
}
451+
_ => panic!("expected PrGithub command"),
452+
}
453+
}
454+
455+
#[test]
456+
fn parses_merge_pr_github_with_remove_flag() {
457+
let cli = Cli::try_parse_from(["rsworktree", "merge-pr-github", "feature", "--remove"])
458+
.expect("merge-pr-github with remove should parse");
459+
match cli.command {
460+
Commands::MergePrGithub(args) => {
461+
assert_eq!(args.name, Some("feature".into()));
462+
assert!(args.remove_remote);
463+
}
464+
_ => panic!("expected MergePrGithub command"),
465+
}
466+
}
467+
468+
#[test]
469+
fn parses_worktree_open_editor_by_name() {
470+
let cli = Cli::try_parse_from(["rsworktree", "worktree", "open-editor", "feature/test"])
471+
.expect("worktree open-editor by name should parse");
472+
match cli.command {
473+
Commands::Worktree(WorktreeCommands::OpenEditor(args)) => {
474+
assert_eq!(args.name, Some("feature/test".into()));
475+
assert!(args.path.is_none());
476+
}
477+
_ => panic!("expected Worktree OpenEditor command"),
478+
}
479+
}
480+
481+
#[test]
482+
fn parses_worktree_open_editor_by_path() {
483+
let cli =
484+
Cli::try_parse_from(["rsworktree", "worktree", "open-editor", "--path", "/some/path"])
485+
.expect("worktree open-editor by path should parse");
486+
match cli.command {
487+
Commands::Worktree(WorktreeCommands::OpenEditor(args)) => {
488+
assert!(args.name.is_none());
489+
assert_eq!(args.path, Some(PathBuf::from("/some/path")));
490+
}
491+
_ => panic!("expected Worktree OpenEditor command"),
492+
}
493+
}
494+
495+
#[test]
496+
fn parses_ls_command() {
497+
let cli = Cli::try_parse_from(["rsworktree", "ls"]).expect("ls should parse");
498+
assert!(matches!(cli.command, Commands::Ls));
499+
}
379500
}

src/editor/preference.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,155 @@ mod tests {
240240
EditorPreferenceResolution::Missing(PreferenceMissingReason::NotConfigured)
241241
));
242242
}
243+
244+
#[test]
245+
fn config_invalid_when_json_malformed() {
246+
let dir = TempDir::new().expect("tempdir");
247+
let repo = init_repo(&dir);
248+
let worktrees_dir = repo.ensure_worktrees_dir().expect("worktrees dir");
249+
let config_path = worktrees_dir.join(CONFIG_FILE_NAME);
250+
251+
fs::write(&config_path, "{ invalid json }").expect("write config");
252+
253+
match resolve_editor_preference(&repo).expect("resolution") {
254+
EditorPreferenceResolution::Missing(PreferenceMissingReason::ConfigInvalid {
255+
path,
256+
error,
257+
}) => {
258+
assert_eq!(path, config_path);
259+
assert!(error.contains("expected") || error.contains("key"));
260+
}
261+
other => panic!("expected ConfigInvalid, got: {other:?}"),
262+
}
263+
}
264+
265+
#[test]
266+
fn config_invalid_when_command_empty() {
267+
let dir = TempDir::new().expect("tempdir");
268+
let repo = init_repo(&dir);
269+
let worktrees_dir = repo.ensure_worktrees_dir().expect("worktrees dir");
270+
let config_path = worktrees_dir.join(CONFIG_FILE_NAME);
271+
272+
let json = serde_json::json!({
273+
"editor": {
274+
"command": " ",
275+
"args": []
276+
}
277+
});
278+
fs::write(&config_path, serde_json::to_vec(&json).unwrap()).expect("write config");
279+
280+
match resolve_editor_preference(&repo).expect("resolution") {
281+
EditorPreferenceResolution::Missing(PreferenceMissingReason::ConfigInvalid {
282+
error,
283+
..
284+
}) => {
285+
assert!(error.contains("must not be empty"));
286+
}
287+
other => panic!("expected ConfigInvalid, got: {other:?}"),
288+
}
289+
}
290+
291+
#[test]
292+
fn config_with_no_editor_key_falls_through() {
293+
let dir = TempDir::new().expect("tempdir");
294+
let repo = init_repo(&dir);
295+
let worktrees_dir = repo.ensure_worktrees_dir().expect("worktrees dir");
296+
let config_path = worktrees_dir.join(CONFIG_FILE_NAME);
297+
298+
let json = serde_json::json!({
299+
"other_setting": true
300+
});
301+
fs::write(&config_path, serde_json::to_vec(&json).unwrap()).expect("write config");
302+
303+
let resolution = resolve_editor_preference(&repo).expect("resolution");
304+
assert!(matches!(
305+
resolution,
306+
EditorPreferenceResolution::Missing(PreferenceMissingReason::NotConfigured)
307+
));
308+
}
309+
310+
#[test]
311+
fn load_from_env_parses_command_with_args() {
312+
let result = load_from_env_value("vim -u NONE", EditorEnvVar::Editor);
313+
match result {
314+
Ok(Some(pref)) => {
315+
assert_eq!(pref.command, OsString::from("vim"));
316+
assert_eq!(pref.args, vec![OsString::from("-u"), OsString::from("NONE")]);
317+
}
318+
other => panic!("expected Some preference, got: {other:?}"),
319+
}
320+
}
321+
322+
#[test]
323+
fn load_from_env_handles_quoted_args() {
324+
let result = load_from_env_value(r#"code --wait --new-window"#, EditorEnvVar::Visual);
325+
match result {
326+
Ok(Some(pref)) => {
327+
assert_eq!(pref.command, OsString::from("code"));
328+
assert_eq!(
329+
pref.args,
330+
vec![OsString::from("--wait"), OsString::from("--new-window")]
331+
);
332+
}
333+
other => panic!("expected Some preference, got: {other:?}"),
334+
}
335+
}
336+
337+
#[test]
338+
fn load_from_env_returns_none_for_empty() {
339+
let result = load_from_env_value("", EditorEnvVar::Editor);
340+
assert!(matches!(result, Ok(None)));
341+
}
342+
343+
#[test]
344+
fn load_from_env_returns_none_for_whitespace_only() {
345+
let result = load_from_env_value(" ", EditorEnvVar::Editor);
346+
assert!(matches!(result, Ok(None)));
347+
}
348+
349+
#[test]
350+
fn load_from_env_errors_on_unclosed_quote() {
351+
let result = load_from_env_value(r#"vim "unclosed"#, EditorEnvVar::Editor);
352+
match result {
353+
Err(PreferenceMissingReason::EnvInvalid { variable, error }) => {
354+
assert_eq!(variable, EditorEnvVar::Editor);
355+
assert!(!error.is_empty());
356+
}
357+
other => panic!("expected EnvInvalid, got: {other:?}"),
358+
}
359+
}
360+
361+
#[test]
362+
fn editor_env_var_name_returns_correct_strings() {
363+
assert_eq!(EditorEnvVar::Editor.name(), "EDITOR");
364+
assert_eq!(EditorEnvVar::Visual.name(), "VISUAL");
365+
}
366+
367+
fn load_from_env_value(
368+
value: &str,
369+
variable: EditorEnvVar,
370+
) -> Result<Option<EditorPreference>, PreferenceMissingReason> {
371+
if value.is_empty() {
372+
return Ok(None);
373+
}
374+
375+
let parts = shell_words::split(value).map_err(|error| PreferenceMissingReason::EnvInvalid {
376+
variable,
377+
error: error.to_string(),
378+
})?;
379+
380+
if parts.is_empty() {
381+
return Ok(None);
382+
}
383+
384+
let mut parts_iter = parts.into_iter();
385+
let command = parts_iter.next().unwrap();
386+
let args = parts_iter.map(OsString::from).collect::<Vec<_>>();
387+
388+
Ok(Some(EditorPreference {
389+
command: OsString::from(command),
390+
args,
391+
source: EditorPreferenceSource::Environment { variable },
392+
}))
393+
}
243394
}

tests/commands/open_editor.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,3 +145,111 @@ impl Drop for EnvGuard {
145145
}
146146
}
147147
}
148+
149+
#[test]
150+
fn open_editor_with_path_flag() -> Result<(), Box<dyn Error>> {
151+
let repo_dir = TempDir::new()?;
152+
init_git_repo(repo_dir.path())?;
153+
create_worktree(repo_dir.path(), "feature/pathtest")?;
154+
155+
let worktree_path = repo_dir.path().join(".rsworktree/feature/pathtest");
156+
let editor_cmd = "/usr/bin/env true";
157+
let guard = EnvGuard::set("EDITOR", editor_cmd);
158+
159+
Command::cargo_bin("rsworktree")?
160+
.current_dir(repo_dir.path())
161+
.args([
162+
"worktree",
163+
"open-editor",
164+
"--path",
165+
worktree_path.to_str().unwrap(),
166+
])
167+
.assert()
168+
.success()
169+
.stdout(predicate::str::contains("Opened"));
170+
171+
drop(guard);
172+
Ok(())
173+
}
174+
175+
#[test]
176+
fn open_editor_errors_when_path_does_not_exist() -> Result<(), Box<dyn Error>> {
177+
let repo_dir = TempDir::new()?;
178+
init_git_repo(repo_dir.path())?;
179+
180+
Command::cargo_bin("rsworktree")?
181+
.current_dir(repo_dir.path())
182+
.args(["worktree", "open-editor", "--path", "/nonexistent/path"])
183+
.assert()
184+
.failure()
185+
.stderr(predicate::str::contains("does not exist"));
186+
187+
Ok(())
188+
}
189+
190+
#[test]
191+
fn open_editor_matches_partial_name() -> Result<(), Box<dyn Error>> {
192+
let repo_dir = TempDir::new()?;
193+
init_git_repo(repo_dir.path())?;
194+
create_worktree(repo_dir.path(), "feature/unique-name")?;
195+
196+
let editor_cmd = "/usr/bin/env true";
197+
let guard = EnvGuard::set("EDITOR", editor_cmd);
198+
199+
// Match by last segment
200+
Command::cargo_bin("rsworktree")?
201+
.current_dir(repo_dir.path())
202+
.args(["worktree", "open-editor", "unique-name"])
203+
.assert()
204+
.success()
205+
.stdout(predicate::str::contains("feature/unique-name"));
206+
207+
drop(guard);
208+
Ok(())
209+
}
210+
211+
#[test]
212+
fn open_editor_errors_on_ambiguous_name() -> Result<(), Box<dyn Error>> {
213+
let repo_dir = TempDir::new()?;
214+
init_git_repo(repo_dir.path())?;
215+
create_worktree(repo_dir.path(), "feature/shared")?;
216+
create_worktree(repo_dir.path(), "bugfix/shared")?;
217+
218+
Command::cargo_bin("rsworktree")?
219+
.current_dir(repo_dir.path())
220+
.args(["worktree", "open-editor", "shared"])
221+
.assert()
222+
.failure()
223+
.stderr(predicate::str::contains("ambiguous"));
224+
225+
Ok(())
226+
}
227+
228+
#[test]
229+
fn open_editor_uses_preferences_file() -> Result<(), Box<dyn Error>> {
230+
let repo_dir = TempDir::new()?;
231+
init_git_repo(repo_dir.path())?;
232+
create_worktree(repo_dir.path(), "feature/prefs")?;
233+
234+
// Create preferences file
235+
let prefs_dir = repo_dir.path().join(".rsworktree");
236+
fs::create_dir_all(&prefs_dir)?;
237+
fs::write(
238+
prefs_dir.join("preferences.json"),
239+
r#"{"editor": {"command": "/usr/bin/env", "args": ["true"]}}"#,
240+
)?;
241+
242+
let guard_editor = EnvGuard::remove("EDITOR");
243+
let guard_visual = EnvGuard::remove("VISUAL");
244+
245+
Command::cargo_bin("rsworktree")?
246+
.current_dir(repo_dir.path())
247+
.args(["worktree", "open-editor", "feature/prefs"])
248+
.assert()
249+
.success()
250+
.stdout(predicate::str::contains("Opened"));
251+
252+
drop(guard_visual);
253+
drop(guard_editor);
254+
Ok(())
255+
}

0 commit comments

Comments
 (0)