Files
vamp/tests/diff_search.rs
T

290 lines
9.7 KiB
Rust

use std::{
fs,
path::{Path, PathBuf},
process::Command,
sync::atomic::{AtomicU64, Ordering},
thread,
time::{Duration, Instant},
};
use vamp::{
diff::{Category, Phase, RevisionTarget},
editor::Input,
session::EditorSession,
};
static NEXT: AtomicU64 = AtomicU64::new(0);
struct Fixture {
root: PathBuf,
target: String,
decoy: String,
}
impl Fixture {
fn new() -> Self {
let root = std::env::temp_dir().join(format!(
"vamp-diff-search-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed),
));
fs::create_dir_all(root.join("repo")).unwrap();
let mut fixture = Self {
root,
target: String::new(),
decoy: String::new(),
};
fixture.git(&["init", "-q", "--initial-branch=fixture-main"]);
fixture.commit("foundation", "base");
fixture.target = fixture.commit("Über parser target", "target");
fixture.decoy = fixture.commit(
&format!("release archaeology mentioning {}", fixture.target),
"decoy",
);
for name in [
"a-r_e_l_e_a_s_e",
"release-candidate",
"zrelease",
"tie-a",
"tie-b",
] {
fixture.git(&["branch", name, &fixture.decoy]);
fixture.git(&["tag", name, &fixture.decoy]);
}
for name in ["release", "feature/parser-rewrite", "feature/Über-parser"] {
fixture.git(&["branch", name, &fixture.target]);
fixture.git(&["tag", name, &fixture.target]);
}
fixture
}
fn git(&self, args: &[&str]) -> String {
let output = Command::new("git")
.args(args)
.current_dir(self.root.join("repo"))
.env("GIT_CONFIG_NOSYSTEM", "1")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_AUTHOR_NAME", "Diff Search Fixture")
.env("GIT_AUTHOR_EMAIL", "fixture@example.invalid")
.env("GIT_COMMITTER_NAME", "Diff Search Fixture")
.env("GIT_COMMITTER_EMAIL", "fixture@example.invalid")
.env("GIT_AUTHOR_DATE", "2000-01-01T00:00:00Z")
.env("GIT_COMMITTER_DATE", "2000-01-01T00:00:00Z")
.output()
.unwrap();
assert!(
output.status.success(),
"git {args:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout).unwrap().trim().to_owned()
}
fn commit(&self, message: &str, content: &str) -> String {
fs::write(self.root.join("repo/source.txt"), content).unwrap();
self.git(&["add", "source.txt"]);
self.git(&["commit", "-qm", message]);
self.git(&["rev-parse", "HEAD"])
}
fn session(&self) -> EditorSession {
let config = self.root.join("config.rb");
fs::write(
&config,
"Vamp.configure do |config|\n config.clipboard_mode = 'separate'\nend\n",
)
.unwrap();
let mut session = EditorSession::open(
Some(self.root.join("repo/source.txt")),
vec![],
Some(&config),
)
.unwrap();
assert!(session.editor().startup_diagnostic().is_none());
session.execute(":diff");
wait_until(&mut session, |session| {
session
.editor()
.diff()
.is_some_and(|diff| diff.catalog.is_some() && !diff.loading)
});
session
}
}
impl Drop for Fixture {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.root);
}
}
fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> bool) {
let deadline = Instant::now() + Duration::from_secs(10);
while !ready(session) {
session.poll();
assert!(Instant::now() < deadline, "{:?}", session.editor().diff());
if let Some(diff) = session.editor().diff() {
assert!(diff.error.is_none(), "{:?}", diff.error);
}
thread::sleep(Duration::from_millis(1));
}
}
fn query(session: &mut EditorSession, text: &str) {
session.handle(Input::Ctrl('u'));
for ch in text.chars() {
session.handle(Input::Char(ch));
}
}
fn category(session: &mut EditorSession, category: Category) {
query(session, "");
for _ in 0..3 {
let diff = session.editor().diff().unwrap();
if diff.catalog.as_ref().unwrap().kind.categories()[diff.category] == category {
return;
}
session.handle(Input::Char('\t'));
}
panic!("category unavailable: {category:?}");
}
fn labels(session: &EditorSession) -> Vec<String> {
let diff = session.editor().diff().unwrap();
assert_eq!(
diff.entries().next().unwrap().target,
RevisionTarget::Working
);
diff.entries()
.skip(1)
.map(|entry| entry.label.clone())
.collect()
}
#[test]
fn branch_and_tag_fuzzy_ranking_and_empty_order_match_in_both_selectors() {
let fixture = Fixture::new();
let mut session = fixture.session();
for phase in [Phase::Before, Phase::After] {
assert_eq!(session.editor().diff().unwrap().phase, phase);
for kind in [Category::Branches, Category::Tags] {
category(&mut session, kind);
let catalogue_order = labels(&session);
query(&mut session, " ");
assert_eq!(labels(&session), catalogue_order);
query(&mut session, "release");
assert_eq!(
labels(&session),
[
"release",
"release-candidate",
"zrelease",
"a-r_e_l_e_a_s_e"
]
);
query(&mut session, "ftprw");
assert_eq!(labels(&session), ["feature/parser-rewrite"]);
query(&mut session, "tie");
let ties: Vec<_> = catalogue_order
.iter()
.filter(|label| label.starts_with("tie-"))
.cloned()
.collect();
assert_eq!(&labels(&session)[..ties.len()], ties);
query(&mut session, "no-such-reference-anywhere");
assert!(labels(&session).is_empty());
assert_eq!(session.editor().diff().unwrap().selected, 0);
}
if phase == Phase::Before {
// Even an unmatched query leaves Working selectable and pinned first.
session.handle(Input::Enter);
}
}
}
#[test]
fn unicode_tokens_and_commit_ids_search_fields_without_cross_field_subsequences() {
let fixture = Fixture::new();
let mut session = fixture.session();
for phase in [Phase::Before, Phase::After] {
assert_eq!(session.editor().diff().unwrap().phase, phase);
category(&mut session, Category::Branches);
for text in [
format!("{} ÜBPR", &fixture.target[..10]),
format!("übpr {}", &fixture.target[..10]),
] {
query(&mut session, &text);
assert_eq!(labels(&session), ["feature/Über-parser"]);
}
category(&mut session, Category::Commits);
for text in [&fixture.target[..], &fixture.target[..10]] {
query(&mut session, text);
assert_eq!(
labels(&session),
[fixture.target.clone(), fixture.decoy.clone()]
);
}
query(&mut session, "ÜB PRSR");
assert_eq!(labels(&session), [fixture.target.clone()]);
// The ID and description can satisfy separate tokens, but not one token
// that would require a subsequence crossing between unrelated fields.
query(&mut session, &format!("{} übpr", &fixture.target[..10]));
assert_eq!(labels(&session), [fixture.target.clone()]);
query(&mut session, &format!("{}über", fixture.target));
assert!(labels(&session).is_empty());
if phase == Phase::Before {
session.handle(Input::Enter);
}
}
}
#[test]
fn selecting_ranked_branch_uses_its_actual_commit_in_before_and_after() {
let fixture = Fixture::new();
for ranked_phase in [Phase::Before, Phase::After] {
let mut session = fixture.session();
if ranked_phase == Phase::After {
session.handle(Input::Enter);
}
category(&mut session, Category::Branches);
query(&mut session, "release");
assert_eq!(labels(&session)[0], "release");
session.handle(Input::Down);
session.handle(Input::Enter);
if ranked_phase == Phase::Before {
// Working selects snapshot mode rather than merge-base mode.
session.handle(Input::Enter);
}
wait_until(&mut session, |session| {
session
.editor()
.diff()
.is_some_and(|diff| diff.phase == Phase::View && diff.selected_file_loaded())
});
let diff = session.editor().diff().unwrap();
let ranked = if ranked_phase == Phase::Before {
diff.before.as_ref().unwrap()
} else {
diff.after.as_ref().unwrap()
};
assert_eq!(ranked.label, "release");
assert_eq!(
ranked.target,
RevisionTarget::Commit(fixture.target.clone())
);
let comparison = diff.comparison.as_ref().unwrap();
assert_eq!(comparison.files.len(), 1);
assert_eq!(comparison.files[0].path, Path::new("source.txt"));
let (before, after) = if ranked_phase == Phase::Before {
(["target"], ["decoy"])
} else {
(["decoy"], ["target"])
};
assert_eq!(comparison.files[0].before, before);
assert_eq!(comparison.files[0].after, after);
}
}