tests/editor_test.rs
Ref: Size: 11.1 KiB
//! End-to-end tests for the editor integration module.
//!
//! These tests exercise the public API of `git_collab::editor`, including
//! real process spawning (using `true`/`false` as stand-in editors) and
//! the `find_comment_at_scroll` helper used by the TUI.
//!
//! Tests that mutate environment variables (EDITOR, VISUAL) must not run
//! in parallel with each other. We serialise them via a process-wide mutex.
use std::io::Write;
use std::sync::Mutex;
use git_collab::editor::{find_comment_at_scroll, open_editor_at, resolve_editor};
/// Guard to serialise tests that mutate EDITOR / VISUAL env vars.
static ENV_LOCK: Mutex<()> = Mutex::new(());
// ===========================================================================
// resolve_editor — end-to-end (reads real env vars)
// ===========================================================================
#[test]
fn e2e_resolve_editor_returns_visual_when_set() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::set_var("VISUAL", "nvim");
std::env::set_var("EDITOR", "vi");
let result = resolve_editor();
assert_eq!(result, Some("nvim".to_string()));
// Restore
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_resolve_editor_falls_back_to_editor_when_visual_unset() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::remove_var("VISUAL");
std::env::set_var("EDITOR", "nano");
let result = resolve_editor();
assert_eq!(result, Some("nano".to_string()));
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_resolve_editor_none_when_both_unset() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::remove_var("VISUAL");
std::env::remove_var("EDITOR");
let result = resolve_editor();
assert_eq!(result, None);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_resolve_editor_skips_empty_visual() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::set_var("VISUAL", "");
std::env::set_var("EDITOR", "emacs");
let result = resolve_editor();
assert_eq!(result, Some("emacs".to_string()));
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
// ===========================================================================
// open_editor_at — end-to-end (spawns real processes)
// ===========================================================================
#[test]
fn e2e_open_editor_at_succeeds_with_true() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
// `true` exits 0 and ignores all arguments
std::env::set_var("EDITOR", "true");
std::env::remove_var("VISUAL");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "some content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 10);
assert!(result.is_ok(), "Expected Ok, got {:?}", result);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_reports_nonzero_exit() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
// `false` exits 1
std::env::set_var("EDITOR", "false");
std::env::remove_var("VISUAL");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "some content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 1);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("Editor exited with status"), "got: {}", msg);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_errors_when_no_editor_configured() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::remove_var("VISUAL");
std::env::remove_var("EDITOR");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 1);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("No editor configured"),
"Expected 'No editor configured', got: {}",
msg
);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_file_not_found() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::set_var("EDITOR", "true");
std::env::remove_var("VISUAL");
let result = open_editor_at("/tmp/nonexistent_editor_e2e_test_file.txt", 1);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("File not found"), "got: {}", msg);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_bad_binary() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
std::env::set_var("EDITOR", "nonexistent_binary_xyz_999");
std::env::remove_var("VISUAL");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 1);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("Failed to launch editor"),
"Expected launch failure, got: {}",
msg
);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_with_multi_word_editor() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
// Simulate `code --wait` style editor; `true` ignores extra args
std::env::set_var("EDITOR", "true --wait");
std::env::remove_var("VISUAL");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 42);
assert!(result.is_ok(), "Expected Ok with multi-word editor, got {:?}", result);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
#[test]
fn e2e_open_editor_at_visual_takes_precedence_over_editor() {
let _guard = ENV_LOCK.lock().unwrap();
let old_visual = std::env::var("VISUAL").ok();
let old_editor = std::env::var("EDITOR").ok();
// VISUAL=true should be used instead of EDITOR=false
// If EDITOR were used, the test would fail because `false` exits 1
std::env::set_var("VISUAL", "true");
std::env::set_var("EDITOR", "false");
let mut tmp = tempfile::NamedTempFile::new().unwrap();
writeln!(tmp, "content").unwrap();
let path = tmp.path().to_str().unwrap();
let result = open_editor_at(path, 1);
assert!(
result.is_ok(),
"VISUAL=true should take precedence over EDITOR=false, got {:?}",
result
);
restore_env("VISUAL", old_visual);
restore_env("EDITOR", old_editor);
}
// ===========================================================================
// find_comment_at_scroll — realistic scenarios
// ===========================================================================
#[test]
fn e2e_find_comment_at_scroll_empty_list() {
let comments: Vec<(String, u32, usize)> = vec![];
assert_eq!(find_comment_at_scroll(&comments, 50), None);
}
#[test]
fn e2e_find_comment_at_scroll_single_exact() {
let comments = vec![("src/main.rs".to_string(), 10, 20)];
let result = find_comment_at_scroll(&comments, 20);
assert_eq!(result, Some(("src/main.rs".to_string(), 10)));
}
#[test]
fn e2e_find_comment_at_scroll_single_above() {
let comments = vec![("src/lib.rs".to_string(), 5, 10)];
let result = find_comment_at_scroll(&comments, 50);
assert_eq!(result, Some(("src/lib.rs".to_string(), 5)));
}
#[test]
fn e2e_find_comment_at_scroll_single_below() {
let comments = vec![("src/lib.rs".to_string(), 5, 100)];
let result = find_comment_at_scroll(&comments, 50);
assert_eq!(result, None);
}
#[test]
fn e2e_find_comment_at_scroll_picks_closest_above_from_many() {
// Simulate a diff with inline comments scattered across files
let comments = vec![
("src/editor.rs".to_string(), 12, 5),
("src/editor.rs".to_string(), 30, 15),
("src/tui.rs".to_string(), 100, 40),
("src/tui.rs".to_string(), 150, 60),
("src/main.rs".to_string(), 1, 80),
];
// Scroll at 45 — closest at-or-above is rendered_pos=40 (src/tui.rs:100)
let result = find_comment_at_scroll(&comments, 45);
assert_eq!(result, Some(("src/tui.rs".to_string(), 100)));
}
#[test]
fn e2e_find_comment_at_scroll_at_boundary_zero() {
let comments = vec![
("header.rs".to_string(), 1, 0),
("body.rs".to_string(), 10, 20),
];
let result = find_comment_at_scroll(&comments, 0);
assert_eq!(result, Some(("header.rs".to_string(), 1)));
}
#[test]
fn e2e_find_comment_at_scroll_all_at_same_position() {
// Multiple comments at the same rendered position — first wins
let comments = vec![
("a.rs".to_string(), 1, 10),
("b.rs".to_string(), 2, 10),
("c.rs".to_string(), 3, 10),
];
let result = find_comment_at_scroll(&comments, 10);
assert_eq!(result, Some(("a.rs".to_string(), 1)));
}
#[test]
fn e2e_find_comment_at_scroll_large_scroll_position() {
let comments = vec![
("early.rs".to_string(), 1, 10),
("late.rs".to_string(), 999, 500),
];
// Scroll way past all comments — should return the last one
let result = find_comment_at_scroll(&comments, u16::MAX);
assert_eq!(result, Some(("late.rs".to_string(), 999)));
}
#[test]
fn e2e_find_comment_at_scroll_unsorted_input() {
// Comments may not arrive sorted by rendered position
let comments = vec![
("c.rs".to_string(), 30, 50),
("a.rs".to_string(), 10, 5),
("b.rs".to_string(), 20, 25),
];
// Scroll at 30 — closest at-or-above is rendered_pos=25
let result = find_comment_at_scroll(&comments, 30);
assert_eq!(result, Some(("b.rs".to_string(), 20)));
}
// ===========================================================================
// Helpers
// ===========================================================================
fn restore_env(var: &str, old: Option<String>) {
match old {
Some(val) => std::env::set_var(var, val),
None => std::env::remove_var(var),
}
}